| 1 | |
|---|
| 2 | |
|---|
| 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_TRUNC, 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 = {} |
|---|
| 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] |
|---|
| 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 | |
|---|
| 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 | |
|---|
| 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: |
|---|
| 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 | |
|---|
| 221 | |
|---|
| 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: |
|---|
| 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: |
|---|
| 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, filetransfer.ISFTPServer) |
|---|
| 457 | components.registerAdapter(SSHSessionForUnixConchUser, UnixConchUser, session.ISession) |
|---|