1
# Copyright (c) 2001-2007 Twisted Matrix Laboratories.
2
# See LICENSE for details.
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
12
from avatar import ConchUser
13
from error import ConchError
14
from interfaces import ISession, ISFTPServer, ISFTPFile
16
import struct, os, time, socket
28
interface.implements(portal.IRealm)
30
def requestAvatar(self, username, mind, *interfaces):
31
user = UnixConchUser(username)
32
return interfaces[0], user, user.logout
35
class UnixConchUser(ConchUser):
37
def __init__(self, username):
38
ConchUser.__init__(self)
39
self.username = username
40
self.pwdData = pwd.getpwnam(self.username)
42
for groupname, password, gid, userlist in grp.getgrall():
43
if username in userlist:
46
self.listeners = {} # dict mapping (interface, port) -> listener
47
self.channelLookup.update(
48
{"session": session.SSHSession,
49
"direct-tcpip": forwarding.openConnectForwardingClient})
51
self.subsystemLookup.update(
52
{"sftp": filetransfer.FileTransferServer})
54
def getUserGroupId(self):
55
return self.pwdData[2:4]
57
def getOtherGroups(self):
58
return self.otherGroups
61
return self.pwdData[5]
64
return self.pwdData[6]
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)
78
self.listeners[(hostToBind, portToBind)] = listener
80
portToBind = listener.getHost()[2] # the port
81
return 1, struct.pack('>L', portToBind)
85
def global_cancel_tcpip_forward(self, data):
86
hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data)
87
listener = self.listeners.get((hostToBind, portToBind), None)
90
del self.listeners[(hostToBind, portToBind)]
91
self._runAsUser(listener.stopListening)
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)))
100
def _runAsUser(self, f, *args, **kw):
103
groups = os.getgroups()
104
uid, gid = self.getUserGroupId()
107
os.setgroups(self.getOtherGroups())
117
args = len(i)>1 and i[1] or ()
118
kw = len(i)>2 and i[2] or {}
119
r = func(*args, **kw)
128
class SSHSessionForUnixConchUser:
130
interface.implements(ISession)
132
def __init__(self, avatar):
134
self. environ = {'PATH':'/bin:/usr/bin:/usr/local/bin'}
138
def addUTMPEntry(self, loggedIn=1):
141
ipAddress = self.avatar.conn.transport.transport.getPeer().host
142
packedIp ,= struct.unpack('L', socket.inet_aton(ipAddress))
143
ttyName = self.ptyTuple[2][5:]
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)
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)
160
b = utmp.UtmpRecord(utmp.WTMP_FILE)
165
def getPty(self, term, windowSize, modes):
166
self.environ['TERM'] = term
167
self.winSize = windowSize
169
master, slave = pty.openpty()
170
ttyname = os.ttyname(slave)
171
self.environ['SSH_TTY'] = ttyname
172
self.ptyTuple = (master, slave, ttyname)
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)
194
fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ,
195
struct.pack('4H', *self.winSize))
198
self.oldWrite = proto.transport.write
199
proto.transport.write = self._writeHack
200
self.avatar.conn.transport.transport.setTcpNoDelay(1)
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)
212
self.getPtyOwnership()
213
self.pty = reactor.spawnProcess(proto, \
214
shell, command, self.environ, homeDir,
215
uid, gid, usePTY = self.ptyTuple or 0)
221
# tty.setraw(self.pty.pipes[0].fileno(), tty.TCSANOW)
222
self.avatar.conn.transport.transport.setTcpNoDelay(1)
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()
231
os.chown(self.ptyTuple[2], uid, ttyGid)
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)
247
attr[flag] = attr[flag]|ttyval
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)
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)
260
def eofReceived(self):
262
self.pty.closeStdin()
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)
270
self.pty.signalProcess('HUP')
271
except (OSError,ProcessExitedAlready):
273
self.pty.loseConnection()
275
log.msg('shell closed')
277
def windowChanged(self, winSize):
278
self.winSize = winSize
279
fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ,
280
struct.pack('4H', *self.winSize))
282
def _writeHack(self, data):
284
Hack to send ignore messages when we aren't echoing.
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)))
293
class SFTPServerForUnixConchUser:
295
interface.implements(ISFTPServer)
297
def __init__(self, avatar):
301
def _setAttrs(self, path, attrs):
303
NOTE: this function assumes it runs as the logged-in user:
304
i.e. under _runAsUser()
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"]))
313
def _getAttrs(self, s):
318
"permissions" : s.st_mode,
319
"atime" : int(s.st_atime),
320
"mtime" : int(s.st_mtime)
323
def _absPath(self, path):
324
home = self.avatar.getHomeDir()
325
return os.path.abspath(os.path.join(home, path))
327
def gotVersion(self, otherVersion, extData):
330
def openFile(self, filename, flags, attrs):
331
return UnixSFTPFile(self, self._absPath(filename), flags, attrs)
333
def removeFile(self, filename):
334
filename = self._absPath(filename)
335
return self.avatar._runAsUser(os.remove, filename)
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)
342
def makeDirectory(self, path, attrs):
343
path = self._absPath(path)
344
return self.avatar._runAsUser([(os.mkdir, (path,)),
345
(self._setAttrs, (path, attrs))])
347
def removeDirectory(self, path):
348
path = self._absPath(path)
349
self.avatar._runAsUser(os.rmdir, path)
351
def openDirectory(self, path):
352
return UnixSFTPDirectory(self, self._absPath(path))
354
def getAttrs(self, path, followLinks):
355
path = self._absPath(path)
357
s = self.avatar._runAsUser(os.stat, path)
359
s = self.avatar._runAsUser(os.lstat, path)
360
return self._getAttrs(s)
362
def setAttrs(self, path, attrs):
363
path = self._absPath(path)
364
self.avatar._runAsUser(self._setAttrs, path, attrs)
366
def readLink(self, path):
367
path = self._absPath(path)
368
return self.avatar._runAsUser(os.readlink, path)
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)
375
def realPath(self, path):
376
return os.path.realpath(self._absPath(path))
378
def extendedRequest(self, extName, extData):
379
raise NotImplementedError
383
interface.implements(ISFTPFile)
385
def __init__(self, server, filename, flags, attrs):
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"]
407
fd = server.avatar._runAsUser(os.open, filename, openFlags, mode)
409
server.avatar._runAsUser(server._setAttrs, filename, attrs)
413
return self.server.avatar._runAsUser(os.close, self.fd)
415
def readChunk(self, offset, length):
416
return self.server.avatar._runAsUser([ (os.lseek, (self.fd, offset, 0)),
417
(os.read, (self.fd, length)) ])
419
def writeChunk(self, offset, data):
420
return self.server.avatar._runAsUser([(os.lseek, (self.fd, offset, 0)),
421
(os.write, (self.fd, data))])
424
s = self.server.avatar._runAsUser(os.fstat, self.fd)
425
return self.server._getAttrs(s)
427
def setAttrs(self, attrs):
428
raise NotImplementedError
431
class UnixSFTPDirectory:
433
def __init__(self, server, directory):
435
self.files = server.avatar._runAsUser(os.listdir, directory)
443
f = self.files.pop(0)
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)
456
components.registerAdapter(SFTPServerForUnixConchUser, UnixConchUser, filetransfer.ISFTPServer)
457
components.registerAdapter(SSHSessionForUnixConchUser, UnixConchUser, session.ISession)