root / trunk / twisted / conch / unix.py

Revision 19815, 15.4 kB (checked in by therve, 2 years ago)

Merge fdesc-2419-2

Author: itamar
Reviewer: exarkun, therve
Fixes #2419

Improve twisted.internet.fdesc: add writeToFD, add tests, and manage EINTR
errors.

Line 
1 # Copyright (c) 2001-2007 Twisted Matrix Laboratories.
2 # See LICENSE for details.
3
4 from twisted.cred import portal
5 from twisted.python import components, log
6 from twisted.internet.error import ProcessExitedAlready
7 from zope import interface
8 from ssh import session, forwarding, filetransfer
9 from ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
10 from twisted.conch.ls import lsLine
11
12 from avatar import ConchUser
13 from error import ConchError
14 from interfaces import ISession, ISFTPServer, ISFTPFile
15
16 import struct, os, time, socket
17 import fcntl, tty
18 import pwd, grp
19 import pty
20 import ttymodes
21
22 try:
23     import utmp
24 except ImportError:
25     utmp = None
26
27 class UnixSSHRealm:
28     interface.implements(portal.IRealm)
29
30     def requestAvatar(self, username, mind, *interfaces):
31         user = UnixConchUser(username)
32         return interfaces[0], user, user.logout
33
34
35 class UnixConchUser(ConchUser):
36
37     def __init__(self, username):
38         ConchUser.__init__(self)
39         self.username = username
40         self.pwdData = pwd.getpwnam(self.username)
41         l = [self.pwdData[3]]
42         for groupname, password, gid, userlist in grp.getgrall():
43             if username in userlist:
44                 l.append(gid)
45         self.otherGroups = l
46         self.listeners = {}  # dict mapping (interface, port) -> listener
47         self.channelLookup.update(
48                 {"session": session.SSHSession,
49                  "direct-tcpip": forwarding.openConnectForwardingClient})
50
51         self.subsystemLookup.update(
52                 {"sftp": filetransfer.FileTransferServer})
53
54     def getUserGroupId(self):
55         return self.pwdData[2:4]
56
57     def getOtherGroups(self):
58         return self.otherGroups
59
60     def getHomeDir(self):
61         return self.pwdData[5]
62
63     def getShell(self):
64         return self.pwdData[6]
65
66     def global_tcpip_forward(self, data):
67         hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data)
68         from twisted.internet import reactor
69         try: listener = self._runAsUser(
70                             reactor.listenTCP, portToBind,
71                             forwarding.SSHListenForwardingFactory(self.conn,
72                                 (hostToBind, portToBind),
73                                 forwarding.SSHListenServerForwardingChannel),
74                             interface = hostToBind)
75         except:
76             return 0
77         else:
78             self.listeners[(hostToBind, portToBind)] = listener
79             if portToBind == 0:
80                 portToBind = listener.getHost()[2] # the port
81                 return 1, struct.pack('>L', portToBind)
82             else:
83                 return 1
84
85     def global_cancel_tcpip_forward(self, data):
86         hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data)
87         listener = self.listeners.get((hostToBind, portToBind), None)
88         if not listener:
89             return 0
90         del self.listeners[(hostToBind, portToBind)]
91         self._runAsUser(listener.stopListening)
92         return 1
93
94     def logout(self):
95         # remove all listeners
96         for listener in self.listeners.itervalues():
97             self._runAsUser(listener.stopListening)
98         log.msg('avatar %s logging out (%i)' % (self.username, len(self.listeners)))
99
100     def _runAsUser(self, f, *args, **kw):
101         euid = os.geteuid()
102         egid = os.getegid()
103         groups = os.getgroups()
104         uid, gid = self.getUserGroupId()
105         os.setegid(0)
106         os.seteuid(0)
107         os.setgroups(self.getOtherGroups())
108         os.setegid(gid)
109         os.seteuid(uid)
110         try:
111             f = iter(f)
112         except TypeError:
113             f = [(f, args, kw)]
114         try:
115             for i in f:
116                 func = i[0]
117                 args = len(i)>1 and i[1] or ()
118                 kw = len(i)>2 and i[2] or {}
119                 r = func(*args, **kw)
120         finally:
121             os.setegid(0)
122             os.seteuid(0)
123             os.setgroups(groups)
124             os.setegid(egid)
125             os.seteuid(euid)
126         return r
127
128 class SSHSessionForUnixConchUser:
129
130     interface.implements(ISession)
131
132     def __init__(self, avatar):
133         self.avatar = avatar
134         self. environ = {'PATH':'/bin:/usr/bin:/usr/local/bin'}
135         self.pty = None
136         self.ptyTuple = 0
137
138     def addUTMPEntry(self, loggedIn=1):
139         if not utmp:
140             return
141         ipAddress = self.avatar.conn.transport.transport.getPeer().host
142         packedIp ,= struct.unpack('L', socket.inet_aton(ipAddress))
143         ttyName = self.ptyTuple[2][5:]
144         t = time.time()
145         t1 = int(t)
146         t2 = int((t-t1) * 1e6)
147         entry = utmp.UtmpEntry()
148         entry.ut_type = loggedIn and utmp.USER_PROCESS or utmp.DEAD_PROCESS
149         entry.ut_pid = self.pty.pid
150         entry.ut_line = ttyName
151         entry.ut_id = ttyName[-4:]
152         entry.ut_tv = (t1,t2)
153         if loggedIn:
154             entry.ut_user = self.avatar.username
155             entry.ut_host = socket.gethostbyaddr(ipAddress)[0]
156             entry.ut_addr_v6 = (packedIp, 0, 0, 0)
157         a = utmp.UtmpRecord(utmp.UTMP_FILE)
158         a.pututline(entry)
159         a.endutent()
160         b = utmp.UtmpRecord(utmp.WTMP_FILE)
161         b.pututline(entry)
162         b.endutent()
163                            
164
165     def getPty(self, term, windowSize, modes):
166         self.environ['TERM'] = term
167         self.winSize = windowSize
168         self.modes = modes
169         master, slave = pty.openpty()
170         ttyname = os.ttyname(slave)
171         self.environ['SSH_TTY'] = ttyname
172         self.ptyTuple = (master, slave, ttyname)
173
174     def openShell(self, proto):
175         from twisted.internet import reactor
176         if not self.ptyTuple: # we didn't get a pty-req
177             log.msg('tried to get shell without pty, failing')
178             raise ConchError("no pty")
179         uid, gid = self.avatar.getUserGroupId()
180         homeDir = self.avatar.getHomeDir()
181         shell = self.avatar.getShell()
182         self.environ['USER'] = self.avatar.username
183         self.environ['HOME'] = homeDir
184         self.environ['SHELL'] = shell
185         shellExec = os.path.basename(shell)
186         peer = self.avatar.conn.transport.transport.getPeer()
187         host = self.avatar.conn.transport.transport.getHost()
188         self.environ['SSH_CLIENT'] = '%s %s %s' % (peer.host, peer.port, host.port)
189         self.getPtyOwnership()
190         self.pty = reactor.spawnProcess(proto, \
191                   shell, ['-%s' % shellExec], self.environ, homeDir, uid, gid,
192                    usePTY = self.ptyTuple)
193         self.addUTMPEntry()
194         fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ,
195                     struct.pack('4H', *self.winSize))
196         if self.modes:
197             self.setModes()
198         self.oldWrite = proto.transport.write
199         proto.transport.write = self._writeHack
200         self.avatar.conn.transport.transport.setTcpNoDelay(1)
201
202     def execCommand(self, proto, cmd):
203         from twisted.internet import reactor
204         uid, gid = self.avatar.getUserGroupId()
205         homeDir = self.avatar.getHomeDir()
206         shell = self.avatar.getShell() or '/bin/sh'
207         command = (shell, '-c', cmd)
208         peer = self.avatar.conn.transport.transport.getPeer()
209         host = self.avatar.conn.transport.transport.getHost()
210         self.environ['SSH_CLIENT'] = '%s %s %s' % (peer.host, peer.port, host.port)
211         if self.ptyTuple:
212             self.getPtyOwnership()
213         self.pty = reactor.spawnProcess(proto, \
214                 shell, command, self.environ, homeDir,
215                 uid, gid, usePTY = self.ptyTuple or 0)
216         if self.ptyTuple:
217             self.addUTMPEntry()
218             if self.modes:
219                 self.setModes()
220 #        else:
221 #            tty.setraw(self.pty.pipes[0].fileno(), tty.TCSANOW)
222         self.avatar.conn.transport.transport.setTcpNoDelay(1)
223
224     def getPtyOwnership(self):
225         ttyGid = os.stat(self.ptyTuple[2])[5]
226         uid, gid = self.avatar.getUserGroupId()
227         euid, egid = os.geteuid(), os.getegid()
228         os.setegid(0)
229         os.seteuid(0)
230         try:
231             os.chown(self.ptyTuple[2], uid, ttyGid)
232         finally:
233             os.setegid(egid)
234             os.seteuid(euid)
235        
236     def setModes(self):
237         pty = self.pty
238         attr = tty.tcgetattr(pty.fileno())
239         for mode, modeValue in self.modes:
240             if not ttymodes.TTYMODES.has_key(mode): continue
241             ttyMode = ttymodes.TTYMODES[mode]
242             if len(ttyMode) == 2: # flag
243                 flag, ttyAttr = ttyMode
244                 if not hasattr(tty, ttyAttr): continue
245                 ttyval = getattr(tty, ttyAttr)
246                 if modeValue:
247                     attr[flag] = attr[flag]|ttyval
248                 else:
249                     attr[flag] = attr[flag]&~ttyval
250             elif ttyMode == 'OSPEED':
251                 attr[tty.OSPEED] = getattr(tty, 'B%s'%modeValue)
252             elif ttyMode == 'ISPEED':
253                 attr[tty.ISPEED] = getattr(tty, 'B%s'%modeValue)
254             else:
255                 if not hasattr(tty, ttyMode): continue
256                 ttyval = getattr(tty, ttyMode)
257                 attr[tty.CC][ttyval] = chr(modeValue)
258         tty.tcsetattr(pty.fileno(), tty.TCSANOW, attr)
259
260     def eofReceived(self):
261         if self.pty:
262             self.pty.closeStdin()
263
264     def closed(self):
265         if self.ptyTuple and os.path.exists(self.ptyTuple[2]):
266             ttyGID = os.stat(self.ptyTuple[2])[5]
267             os.chown(self.ptyTuple[2], 0, ttyGID)
268         if self.pty:
269             try:
270                 self.pty.signalProcess('HUP')
271             except (OSError,ProcessExitedAlready):
272                 pass
273             self.pty.loseConnection()
274             self.addUTMPEntry(0)
275         log.msg('shell closed')
276
277     def windowChanged(self, winSize):
278         self.winSize = winSize
279         fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ,
280                         struct.pack('4H', *self.winSize))
281
282     def _writeHack(self, data):
283         """
284         Hack to send ignore messages when we aren't echoing.
285         """
286         if self.pty is not None:
287             attr = tty.tcgetattr(self.pty.fileno())[3]
288             if not attr & tty.ECHO and attr & tty.ICANON: # no echo
289                 self.avatar.conn.transport.sendIgnore('\x00'*(8+len(data)))
290         self.oldWrite(data)
291
292
293 class SFTPServerForUnixConchUser:
294
295     interface.implements(ISFTPServer)
296
297     def __init__(self, avatar):
298         self.avatar = avatar
299
300
301     def _setAttrs(self, path, attrs):
302         """
303         NOTE: this function assumes it runs as the logged-in user:
304         i.e. under _runAsUser()
305         """
306         if attrs.has_key("uid") and attrs.has_key("gid"):
307             os.chown(path, attrs["uid"], attrs["gid"])
308         if attrs.has_key("permissions"):
309             os.chmod(path, attrs["permissions"])
310         if attrs.has_key("atime") and attrs.has_key("mtime"):
311             os.utime(path, (attrs["atime"], attrs["mtime"]))
312
313     def _getAttrs(self, s):
314         return {
315             "size" : s.st_size,
316             "uid" : s.st_uid,
317             "gid" : s.st_gid,
318             "permissions" : s.st_mode,
319             "atime" : int(s.st_atime),
320             "mtime" : int(s.st_mtime)
321         }
322
323     def _absPath(self, path):
324         home = self.avatar.getHomeDir()
325         return os.path.abspath(os.path.join(home, path))
326
327     def gotVersion(self, otherVersion, extData):
328         return {}
329
330     def openFile(self, filename, flags, attrs):
331         return UnixSFTPFile(self, self._absPath(filename), flags, attrs)
332
333     def removeFile(self, filename):
334         filename = self._absPath(filename)
335         return self.avatar._runAsUser(os.remove, filename)
336
337     def renameFile(self, oldpath, newpath):
338         oldpath = self._absPath(oldpath)
339         newpath = self._absPath(newpath)
340         return self.avatar._runAsUser(os.rename, oldpath, newpath)
341
342     def makeDirectory(self, path, attrs):
343         path = self._absPath(path)
344         return self.avatar._runAsUser([(os.mkdir, (path,)),
345                                 (self._setAttrs, (path, attrs))])
346
347     def removeDirectory(self, path):
348         path = self._absPath(path)
349         self.avatar._runAsUser(os.rmdir, path)
350
351     def openDirectory(self, path):
352         return UnixSFTPDirectory(self, self._absPath(path))
353
354     def getAttrs(self, path, followLinks):
355         path = self._absPath(path)
356         if followLinks:
357             s = self.avatar._runAsUser(os.stat, path)
358         else:
359             s = self.avatar._runAsUser(os.lstat, path)
360         return self._getAttrs(s)
361
362     def setAttrs(self, path, attrs):
363         path = self._absPath(path)
364         self.avatar._runAsUser(self._setAttrs, path, attrs)
365
366     def readLink(self, path):
367         path = self._absPath(path)
368         return self.avatar._runAsUser(os.readlink, path)
369
370     def makeLink(self, linkPath, targetPath):
371         linkPath = self._absPath(linkPath)
372         targetPath = self._absPath(targetPath)
373         return self.avatar._runAsUser(os.symlink, targetPath, linkPath)
374
375     def realPath(self, path):
376         return os.path.realpath(self._absPath(path))
377
378     def extendedRequest(self, extName, extData):
379         raise NotImplementedError
380
381 class UnixSFTPFile:
382
383     interface.implements(ISFTPFile)
384
385     def __init__(self, server, filename, flags, attrs):
386         self.server = server
387         openFlags = 0
388         if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
389             openFlags = os.O_RDONLY
390         if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
391             openFlags = os.O_WRONLY
392         if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
393             openFlags = os.O_RDWR
394         if flags & FXF_APPEND == FXF_APPEND:
395             openFlags |= os.O_APPEND
396         if flags & FXF_CREAT == FXF_CREAT:
397             openFlags |= os.O_CREAT
398         if flags & FXF_TRUNC == FXF_TRUNC:
399             openFlags |= os.O_TRUNC
400         if flags & FXF_EXCL == FXF_EXCL:
401             openFlags |= os.O_EXCL
402         if attrs.has_key("permissions"):
403             mode = attrs["permissions"]
404             del attrs["permissions"]
405         else:
406             mode = 0777
407         fd = server.avatar._runAsUser(os.open, filename, openFlags, mode)
408         if attrs:
409             server.avatar._runAsUser(server._setAttrs, filename, attrs)
410         self.fd = fd
411
412     def close(self):
413         return self.server.avatar._runAsUser(os.close, self.fd)
414
415     def readChunk(self, offset, length):
416         return self.server.avatar._runAsUser([ (os.lseek, (self.fd, offset, 0)),
417                                                (os.read, (self.fd, length)) ])
418
419     def writeChunk(self, offset, data):
420         return self.server.avatar._runAsUser([(os.lseek, (self.fd, offset, 0)),
421                                        (os.write, (self.fd, data))])
422
423     def getAttrs(self):
424         s = self.server.avatar._runAsUser(os.fstat, self.fd)
425         return self.server._getAttrs(s)
426
427     def setAttrs(self, attrs):
428         raise NotImplementedError
429
430
431 class UnixSFTPDirectory:
432
433     def __init__(self, server, directory):
434         self.server = server
435         self.files = server.avatar._runAsUser(os.listdir, directory)
436         self.dir = directory
437
438     def __iter__(self):
439         return self
440
441     def next(self):
442         try:
443             f = self.files.pop(0)
444         except IndexError:
445             raise StopIteration
446         else:
447             s = self.server.avatar._runAsUser(os.lstat, os.path.join(self.dir, f))
448             longname = lsLine(f, s)
449             attrs = self.server._getAttrs(s)
450             return (f, longname, attrs)
451
452     def close(self):
453         self.files = []
454
455
456 components.registerAdapter(SFTPServerForUnixConchUser, UnixConchUser, filetransfer.ISFTPServer)
457 components.registerAdapter(SSHSessionForUnixConchUser, UnixConchUser, session.ISession)
Note: See TracBrowser for help on using the browser.