root/trunk/twisted/conch/unix.py

Revision 19815, 15.4 KB (checked in by therve, 3 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
4from twisted.cred import portal
5from twisted.python import components, log
6from twisted.internet.error import ProcessExitedAlready
7from zope import interface
8from ssh import session, forwarding, filetransfer
9from ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
10from twisted.conch.ls import lsLine
11
12from avatar import ConchUser
13from error import ConchError
14from interfaces import ISession, ISFTPServer, ISFTPFile
15
16import struct, os, time, socket
17import fcntl, tty
18import pwd, grp
19import pty
20import ttymodes
21
22try:
23    import utmp
24except ImportError:
25    utmp = None
26
27class 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
35class 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
128class 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
293class 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
381class 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
431class 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
456components.registerAdapter(SFTPServerForUnixConchUser, UnixConchUser, filetransfer.ISFTPServer)
457components.registerAdapter(SSHSessionForUnixConchUser, UnixConchUser, session.ISession)
Note: See TracBrowser for help on using the browser.