1
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
2
# See LICENSE for details.
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
14
from avatar import ConchUser
15
from error import ConchError
16
from interfaces import ISession, ISFTPServer, ISFTPFile
18
import struct, os, time, socket
30
interface.implements(portal.IRealm)
32
def requestAvatar(self, username, mind, *interfaces):
33
user = UnixConchUser(username)
34
return interfaces[0], user, user.logout
37
class UnixConchUser(ConchUser):
39
def __init__(self, username):
40
ConchUser.__init__(self)
41
self.username = username
42
self.pwdData = pwd.getpwnam(self.username)
44
for groupname, password, gid, userlist in grp.getgrall():
45
if username in userlist:
48
self.listeners = {} # dict mapping (interface, port) -> listener
49
self.channelLookup.update(
50
{"session": session.SSHSession,
51
"direct-tcpip": forwarding.openConnectForwardingClient})
53
self.subsystemLookup.update(
54
{"sftp": filetransfer.FileTransferServer})
56
def getUserGroupId(self):
57
return self.pwdData[2:4]
59
def getOtherGroups(self):
60
return self.otherGroups
63
return self.pwdData[5]
66
return self.pwdData[6]
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)
80
self.listeners[(hostToBind, portToBind)] = listener
82
portToBind = listener.getHost()[2] # the port
83
return 1, struct.pack('>L', portToBind)
87
def global_cancel_tcpip_forward(self, data):
88
hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data)
89
listener = self.listeners.get((hostToBind, portToBind), None)
92
del self.listeners[(hostToBind, portToBind)]
93
self._runAsUser(listener.stopListening)
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)))
102
def _runAsUser(self, f, *args, **kw):
105
groups = os.getgroups()
106
uid, gid = self.getUserGroupId()
109
os.setgroups(self.getOtherGroups())
119
args = len(i)>1 and i[1] or ()
120
kw = len(i)>2 and i[2] or {}
121
r = func(*args, **kw)
130
class SSHSessionForUnixConchUser:
132
interface.implements(ISession)
134
def __init__(self, avatar):
136
self. environ = {'PATH':'/bin:/usr/bin:/usr/local/bin'}
140
def addUTMPEntry(self, loggedIn=1):
143
ipAddress = self.avatar.conn.transport.transport.getPeer().host
144
packedIp ,= struct.unpack('L', socket.inet_aton(ipAddress))
145
ttyName = self.ptyTuple[2][5:]
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)
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)
162
b = utmp.UtmpRecord(utmp.WTMP_FILE)
167
def getPty(self, term, windowSize, modes):
168
self.environ['TERM'] = term
169
self.winSize = windowSize
171
master, slave = pty.openpty()
172
ttyname = os.ttyname(slave)
173
self.environ['SSH_TTY'] = ttyname
174
self.ptyTuple = (master, slave, ttyname)
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)
196
fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ,
197
struct.pack('4H', *self.winSize))
200
self.oldWrite = proto.transport.write
201
proto.transport.write = self._writeHack
202
self.avatar.conn.transport.transport.setTcpNoDelay(1)
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)
214
self.getPtyOwnership()
215
self.pty = reactor.spawnProcess(proto, \
216
shell, command, self.environ, homeDir,
217
uid, gid, usePTY = self.ptyTuple or 0)
223
# tty.setraw(self.pty.pipes[0].fileno(), tty.TCSANOW)
224
self.avatar.conn.transport.transport.setTcpNoDelay(1)
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()
233
os.chown(self.ptyTuple[2], uid, ttyGid)
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)
249
attr[flag] = attr[flag]|ttyval
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)
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)
262
def eofReceived(self):
264
self.pty.closeStdin()
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)
272
self.pty.signalProcess('HUP')
273
except (OSError,ProcessExitedAlready):
275
self.pty.loseConnection()
277
log.msg('shell closed')
279
def windowChanged(self, winSize):
280
self.winSize = winSize
281
fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ,
282
struct.pack('4H', *self.winSize))
284
def _writeHack(self, data):
286
Hack to send ignore messages when we aren't echoing.
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)))
295
class SFTPServerForUnixConchUser:
297
interface.implements(ISFTPServer)
299
def __init__(self, avatar):
303
def _setAttrs(self, path, attrs):
305
NOTE: this function assumes it runs as the logged-in user:
306
i.e. under _runAsUser()
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"]))
315
def _getAttrs(self, s):
320
"permissions" : s.st_mode,
321
"atime" : int(s.st_atime),
322
"mtime" : int(s.st_mtime)
325
def _absPath(self, path):
326
home = self.avatar.getHomeDir()
327
return os.path.abspath(os.path.join(home, path))
329
def gotVersion(self, otherVersion, extData):
332
def openFile(self, filename, flags, attrs):
333
return UnixSFTPFile(self, self._absPath(filename), flags, attrs)
335
def removeFile(self, filename):
336
filename = self._absPath(filename)
337
return self.avatar._runAsUser(os.remove, filename)
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)
344
def makeDirectory(self, path, attrs):
345
path = self._absPath(path)
346
return self.avatar._runAsUser([(os.mkdir, (path,)),
347
(self._setAttrs, (path, attrs))])
349
def removeDirectory(self, path):
350
path = self._absPath(path)
351
self.avatar._runAsUser(os.rmdir, path)
353
def openDirectory(self, path):
354
return UnixSFTPDirectory(self, self._absPath(path))
356
def getAttrs(self, path, followLinks):
357
path = self._absPath(path)
359
s = self.avatar._runAsUser(os.stat, path)
361
s = self.avatar._runAsUser(os.lstat, path)
362
return self._getAttrs(s)
364
def setAttrs(self, path, attrs):
365
path = self._absPath(path)
366
self.avatar._runAsUser(self._setAttrs, path, attrs)
368
def readLink(self, path):
369
path = self._absPath(path)
370
return self.avatar._runAsUser(os.readlink, path)
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)
377
def realPath(self, path):
378
return os.path.realpath(self._absPath(path))
380
def extendedRequest(self, extName, extData):
381
raise NotImplementedError
385
interface.implements(ISFTPFile)
387
def __init__(self, server, filename, flags, attrs):
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"]
409
fd = server.avatar._runAsUser(os.open, filename, openFlags, mode)
411
server.avatar._runAsUser(server._setAttrs, filename, attrs)
415
return self.server.avatar._runAsUser(os.close, self.fd)
417
def readChunk(self, offset, length):
418
return self.server.avatar._runAsUser([ (os.lseek, (self.fd, offset, 0)),
419
(os.read, (self.fd, length)) ])
421
def writeChunk(self, offset, data):
422
return self.server.avatar._runAsUser([(os.lseek, (self.fd, offset, 0)),
423
(os.write, (self.fd, data))])
426
s = self.server.avatar._runAsUser(os.fstat, self.fd)
427
return self.server._getAttrs(s)
429
def setAttrs(self, attrs):
430
raise NotImplementedError
433
class UnixSFTPDirectory:
435
def __init__(self, server, directory):
437
self.files = server.avatar._runAsUser(os.listdir, directory)
445
f = self.files.pop(0)
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)
458
components.registerAdapter(SFTPServerForUnixConchUser, UnixConchUser, filetransfer.ISFTPServer)
459
components.registerAdapter(SSHSessionForUnixConchUser, UnixConchUser, session.ISession)