| OLD | NEW |
| (Empty) |
| 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_TRU
NC, 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.listener
s))) | |
| 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.po
rt) | |
| 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.po
rt) | |
| 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, filetransf
er.ISFTPServer) | |
| 457 components.registerAdapter(SSHSessionForUnixConchUser, UnixConchUser, session.IS
ession) | |
| OLD | NEW |