~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/conch/unix.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mto: (2.2.3 sid)
  • mto: This revision was merged to the branch mainline in revision 15.
  • Revision ID: james.westby@ubuntu.com-20070117145235-7gaj253qxi5wiq16
Tags: upstream-2.5.0
ImportĀ upstreamĀ versionĀ 2.5.0

Show diffs side-by-side

added added

removed removed

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