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 |