1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 """\
24 For sharing local folders via sFTP/sshfs Python X2go implements its own sFTP
25 server (as end point of reverse forwarding tunnel requests). Thus, Python X2go
26 does not need a locally installed SSH daemon on the client side machine.
27
28 The Python X2go sFTP server code was originally written by Richard Murri,
29 for further information see his website: http://www.richardmurri.com
30
31 """
32 __NAME__ = "x2gosftpserver-pylib"
33
34 import base64, os, sys
35 import shutil
36 import copy, types
37 import threading
38 import paramiko
39 import gevent
40
41
42 import rforward
43 import defaults
44 import utils
45 import log
46
48 """\
49 Implementation of a basic SSH server that is supposed
50 to run with its sFTP server implementation.
51
52 """
54 """\
55 Initialize a new sFTP server interface.
56
57 @param auth_key: Server key that the client has to authenticate against
58 @type auth_key: C{paramiko.RSAKey} instance
59 @param session_instance: the calling L{X2goSession} instance
60 @type session_instance: L{X2goSession} instance
61 @param logger: you can pass an L{X2goLogger} object to the L{X2goClientXConfig} constructor
62 @type logger: C{instance}
63 @param loglevel: if no L{X2goLogger} object has been supplied a new one will be
64 constructed with the given loglevel
65 @type loglevel: C{int}
66
67 """
68 if logger is None:
69 self.logger = log.X2goLogger(loglevel=loglevel)
70 else:
71 self.logger = copy.deepcopy(logger)
72 self.logger.tag = __NAME__
73
74 self.current_local_user = defaults.CURRENT_LOCAL_USER
75 self.auth_key = auth_key
76 self.session_instance = session_instance
77 paramiko.ServerInterface.__init__(self, *args, **kwargs)
78 logger('initializing internal SSH server for handling incoming sFTP requests, allowing connections for user ,,%s\'\' only' % self.current_local_user, loglevel=log.loglevel_DEBUG)
79
81 """\
82 Only allow session requests.
83
84 @param kind: request type
85 @type kind: C{str}
86 @param chanid: channel id (unused)
87 @type chanid: C{any}
88 @return: returns a Paramiko/SSH return code
89 @rtype: C{int}
90
91 """
92 self.logger('detected a channel request for sFTP', loglevel=log.loglevel_DEBUG_SFTPXFER)
93 if kind == 'session':
94 return paramiko.OPEN_SUCCEEDED
95 return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
96
98 """\
99 Ensure proper authentication.
100
101 @param username: username of incoming authentication request
102 @type username: C{str}
103 @param key: incoming SSH key to be used for authentication
104 @type key: C{paramiko.RSAKey} instance
105 @return: returns a Paramiko/SSH return code
106 @rtype: C{int}
107 """
108 if username == self.current_local_user:
109 self.logger('sFTP server %s: username is %s' % (self, self.current_local_user), loglevel=log.loglevel_DEBUG)
110 if type(key) == paramiko.RSAKey and key == self.auth_key:
111 self.logger('sFTP server %s: publickey auth has been successful' % (self), loglevel=log.loglevel_INFO)
112 return paramiko.AUTH_SUCCESSFUL
113 self.logger('sFTP server %s: publickey auth failed' % (self), loglevel=log.loglevel_WARN)
114 return paramiko.AUTH_FAILED
115
117 """\
118 Only allow public key authentication.
119
120 @param username: username of incoming authentication request
121 @type username: C{str}
122 @return: statically returns C{publickey} as auth mechanism
123 @rtype: C{str}
124
125 """
126 self.logger('sFTP client asked for support auth methods, answering: publickey', loglevel=log.loglevel_DEBUG_SFTPXFER)
127 return 'publickey'
128
129
131 """\
132 Represents a handle to an open file.
133
134 """
136 try:
137 return paramiko.SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
138 except OSError, e:
139 return paramiko.SFTPServer.convert_errno(e.errno)
140
141
143 """\
144 sFTP server implementation.
145
146 """
148 """\
149 Make user information accessible as well as set chroot jail directory.
150
151 @param server: a C{paramiko.ServerInterface} instance to use with this SFTP server interface
152 @type server: C{paramiko.ServerInterface} instance
153 @param chroot: chroot environment for this SFTP interface
154 @type chroot: C{str}
155 @param logger: you can pass an L{X2goLogger} object to the L{X2goClientXConfig} constructor
156 @type logger: C{instance}
157 @param loglevel: if no L{X2goLogger} object has been supplied a new one will be
158 constructed with the given loglevel
159 @type loglevel: C{int}
160 @param server_event: a C{threading.Event} instance that can signal SFTP session termination
161 @type server_event: C{threading.Event} instance
162
163 """
164 if logger is None:
165 self.logger = log.X2goLogger(loglevel=loglevel)
166 else:
167 self.logger = copy.deepcopy(logger)
168 self.logger.tag = __NAME__
169 self.server_event = server_event
170
171 self.logger('sFTP server: initializing new channel...', loglevel=log.loglevel_DEBUG)
172 self.CHROOT = chroot or '/tmp'
173
175 """\
176 Enforce the chroot jail. On Windows systems the drive letter is incorporated in the
177 chroot path name (/windrive/<drive_letter>/path/to/file/or/folder).
178
179 @param path: path name within chroot
180 @type path: C{str}
181 @return: real path name (including drive letter on Windows systems)
182 @rtype: C{str}
183 """
184 if defaults.X2GOCLIENT_OS == 'Windows' and path.startswith('/windrive'):
185 _path_components = path.split('/')
186 _drive = _path_components[2]
187 _tail_components = (len(_path_components) > 3) and _path_components[3:] or ''
188 _tail = os.path.normpath('/'.join(_tail_components))
189 path = os.path.join('%s:' % _drive, '/', _tail)
190 else:
191 path = self.CHROOT + self.canonicalize(path)
192 path = path.replace('//', '/')
193 return path
194
196 """\
197 List the contents of a folder.
198
199 @param path: path to folder
200 @type path: C{str}
201
202 @return: returns the folder contents, on failure returns a Paramiko/SSH return code
203 @rtype: C{dict} or C{int}
204
205 """
206 path = self._realpath(path)
207 self.logger('sFTP server: listing files in folder: %s' % path, loglevel=log.loglevel_DEBUG_SFTPXFER)
208
209 try:
210 out = []
211 flist = os.listdir(path)
212 for fname in flist:
213
214 try:
215 attr = paramiko.SFTPAttributes.from_stat(os.lstat(os.path.join(path, fname)))
216 attr.filename = fname
217 self.logger('sFTP server %s: file attributes ok: %s' % (self, fname), loglevel=log.loglevel_DEBUG_SFTPXFER)
218 out.append(attr)
219 except OSError, e:
220 self.logger('sFTP server %s: encountered error processing attributes of file %s: %s' % (self, fname, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
221
222 self.logger('sFTP server: folder list is : %s' % str([ a.filename for a in out ]), loglevel=log.loglevel_DEBUG_SFTPXFER)
223 return out
224 except OSError, e:
225 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
226 return paramiko.SFTPServer.convert_errno(e.errno)
227
228 - def stat(self, path):
229 """\
230 Stat on a file.
231
232 @param path: path to file/folder
233 @type path: C{str}
234 @return: returns the file's stat output, on failure: returns a Paramiko/SSH return code
235 @rtype: C{class} or C{int}
236
237 """
238 path = self._realpath(path)
239 self.logger('sFTP server %s: calling stat on path: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
240 try:
241 return paramiko.SFTPAttributes.from_stat(os.stat(path))
242 except OSError, e:
243 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
244 return paramiko.SFTPServer.convert_errno(e.errno)
245
247 """\
248 LStat on a file.
249
250 @param path: path to folder
251 @type path: C{str}
252 @return: returns the file's lstat output, on failure: returns a Paramiko/SSH return code
253 @rtype: C{class} or C{int}
254
255 """
256 path = self._realpath(path)
257 self.logger('sFTP server: calling lstat on path: %s' % path, loglevel=log.loglevel_DEBUG_SFTPXFER)
258 try:
259 return paramiko.SFTPAttributes.from_stat(os.lstat(path))
260 except OSError, e:
261 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
262 return paramiko.SFTPServer.convert_errno(e.errno)
263
264 - def open(self, path, flags, attr):
265 """\
266 Open a file for reading, writing, appending etc.
267
268 @param path: path to file
269 @type path: C{str}
270 @param flags: file flags
271 @type flags: C{str}
272 @param attr: file attributes
273 @type attr: C{class}
274 @return: file handle/object for remote file, on failure: returns a Paramiko/SSH return code
275 @rtype: L{_SFTPHandle} instance or C{int}
276
277 """
278 path = self._realpath(path)
279 self.logger('sFTP server %s: opening file: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
280 try:
281 binary_flag = getattr(os, 'O_BINARY', 0)
282 flags |= binary_flag
283 mode = getattr(attr, 'st_mode', None)
284 if mode is not None:
285 fd = os.open(path, flags, mode)
286 else:
287
288
289 fd = os.open(path, flags, 0666)
290 except OSError, e:
291 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
292 return paramiko.SFTPServer.convert_errno(e.errno)
293 if (flags & os.O_CREAT) and (attr is not None):
294 attr._flags &= ~attr.FLAG_PERMISSIONS
295 paramiko.SFTPServer.set_file_attr(path, attr)
296 if flags & os.O_WRONLY:
297 if flags & os.O_APPEND:
298 fstr = 'ab'
299 else:
300 fstr = 'wb'
301 elif flags & os.O_RDWR:
302 if flags & os.O_APPEND:
303 fstr = 'a+b'
304 else:
305 fstr = 'r+b'
306 else:
307
308 fstr = 'rb'
309 try:
310 f = os.fdopen(fd, fstr)
311 except OSError, e:
312 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
313 return paramiko.SFTPServer.convert_errno(e.errno)
314 fobj = _SFTPHandle(flags)
315 fobj.filename = path
316 fobj.readfile = f
317 fobj.writefile = f
318 return fobj
319
321 """\
322 Remove a file.
323
324 @param path: path to file
325 @type path: C{str}
326 @return: returns Paramiko/SSH return code
327 @rtype: C{int}
328 """
329 path = self._realpath(path)
330 os.remove(path)
331 self.logger('sFTP server %s: removing file: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
332 return paramiko.SFTP_OK
333
334 - def rename(self, oldpath, newpath):
335 """\
336 Rename/Move a file.
337
338 @param oldpath: old path/location/file name
339 @type oldpath: C{str}
340 @param newpath: new path/location/file name
341 @type newpath: C{str}
342 @return: returns Paramiko/SSH return code
343 @rtype: C{int}
344
345 """
346 self.logger('sFTP server %s: renaming path from %s to %s' % (self, oldpath, newpath), loglevel=log.loglevel_DEBUG_SFTPXFER)
347 oldpath = self._realpath(oldpath)
348 newpath = self._realpath(newpath)
349 try:
350 shutil.move(oldpath, newpath)
351 except OSError, e:
352 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
353 return paramiko.SFTPServer.convert_errno(e.errno)
354 return paramiko.SFTP_OK
355
356 - def mkdir(self, path, attr):
357 """\
358 Make a directory.
359
360 @param path: path to new folder
361 @type path: C{str}
362 @param attr: file attributes
363 @type attr: C{class}
364 @return: returns Paramiko/SSH return code
365 @rtype: C{int}
366
367 """
368 self.logger('sFTP server: creating new dir (perms: %s): %s' % (attr.st_mode, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
369 path = self._realpath(path)
370 try:
371 os.mkdir(path, attr.st_mode)
372 except OSError, e:
373 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
374 return paramiko.SFTPServer.convert_errno(e.errno)
375 return paramiko.SFTP_OK
376
378 """\
379 Remove a directory (if needed recursively).
380
381 @param path: folder to be removed
382 @type path: C{str}
383 @return: returns Paramiko/SSH return code
384 @rtype: C{int}
385
386 """
387 self.logger('sFTP server %s: removing dir: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
388 path = self._realpath(path)
389 try:
390 shutil.rmtree(path)
391 except OSError, e:
392 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
393 return paramiko.SFTPServer.convert_errno(e.errno)
394 return paramiko.SFTP_OK
395
396 - def chattr(self, path, attr):
397 """\
398 Change file attributes.
399
400 @param path: path of file/folder
401 @type path: C{str}
402 @param attr: new file attributes
403 @type attr: C{class}
404 @return: returns Paramiko/SSH return code
405 @rtype: C{int}
406
407 """
408 self.logger('sFTP server %s: modifying attributes of path: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
409 path = self._realpath(path)
410 try:
411 if attr.st_mode is not None:
412 os.chmod(path, attr.st_mode)
413 if attr.st_uid is not None:
414 os.chown(path, attr.st_uid, attr.st_gid)
415 except OSError, e:
416 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
417 return paramiko.SFTPServer.convert_errno(e.errno)
418 return paramiko.SFTP_OK
419
420 - def symlink(self, target_path, path):
421 """\
422 Create a symbolic link.
423
424 @param target_path: link shall point to this path
425 @type target_path: C{str}
426 @param path: link location
427 @type path: C{str}
428 @return: returns Paramiko/SSH return code
429 @rtype: C{int}
430
431 """
432 self.logger('sFTP server %s: creating symlink from: %s to target: %s' % (self, path, target_path), loglevel=log.loglevel_DEBUG_SFTPXFER)
433 path = self._realpath(path)
434 if target_path.startswith('/'):
435 target_path = self._realpath(target_path)
436 try:
437 os.symlink(target_path, path)
438 except OSError, e:
439 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
440 return paramiko.SFTPServer.convert_errno(e.errno)
441 return paramiko.SFTP_OK
442
444 """\
445 Read the target of a symbolic link.
446
447 @param path: path of symbolic link
448 @type path: C{str}
449 @return: target location of the symbolic link, on failure: returns a Paramiko/SSH return code
450 @rtype: C{str} or C{int}
451 """
452 path = self._realpath(path)
453 try:
454 return os.readlink(path)
455 except OSError, e:
456 self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
457 return paramiko.SFTPServer.convert_errno(e.errno)
458
460 """\
461 Tidy up when the sFTP session has ended.
462
463 """
464 if self.server_event is not None:
465 self.logger('sFTP server %s: session has ended' % self, loglevel=log.loglevel_DEBUG_SFTPXFER)
466 self.server_event.set()
467
468
470 """\
471 A reverse fowarding tunnel with an sFTP server at its endpoint. This blend of a Paramiko/SSH
472 reverse forwarding tunnel is used to provide access to local X2go client folders
473 from within the the remote X2go server session.
474
475 """
477 """\
478 Start a Paramiko/SSH reverse forwarding tunnel, that has an sFTP server listening at
479 the endpoint of the tunnel.
480
481 @param server_port: the TCP/IP port on the X2go server (starting point of the tunnel),
482 normally some number above 30000
483 @type server_port: int
484 @param ssh_transport: the L{X2goSession}'s Paramiko/SSH transport instance
485 @type ssh_transport: C{paramiko.Transport} instance
486 @param auth_key: Paramiko/SSH RSAkey object that has to be authenticated against by
487 the remote sFTP client
488 @type auth_key: C{paramiko.RSAKey} instance
489 @param logger: you can pass an L{X2goLogger} object to the
490 L{X2goRevFwTunnelToSFTP} constructor
491 @type logger: L{X2goLogger} instance
492 @param loglevel: if no L{X2goLogger} object has been supplied a new one will be
493 constructed with the given loglevel
494 @type loglevel: int
495
496 """
497 if logger is None:
498 self.logger = log.X2goLogger(loglevel=loglevel)
499 else:
500 self.logger = copy.deepcopy(logger)
501 self.logger.tag = __NAME__
502
503 self.server_port = server_port
504 self.ssh_transport = ssh_transport
505 self.session_instance = session_instance
506 if type(auth_key) is not paramiko.RSAKey:
507 auth_key = None
508 self.auth_key = auth_key
509
510 self.open_channels = {}
511 self.incoming_channel = threading.Condition()
512
513 threading.Thread.__init__(self)
514 self.daemon = True
515 self._accept_channels = True
516
518 """\
519 This method gets run once an L{X2goRevFwTunnelToSFTP} has been started with its
520 L{start()} method. Use L{X2goRevFwTunnelToSFTP}.stop_thread() to stop the
521 reverse forwarding tunnel again (refer also to its pause() and resume() method).
522
523 L{X2goRevFwTunnelToSFTP.run()} waits for notifications of an appropriate incoming
524 Paramiko/SSH channel (issued by L{X2goRevFwTunnelToSFTP.notify()}). Appropriate in
525 this context means, that its starting point on the X2go server matches the class's
526 property C{server_port}.
527
528 Once a new incoming channel gets announced by the L{notify()} method, a new
529 L{X2goRevFwSFTPChannelThread} instance will be initialized. As a data stream handler,
530 the function L{x2go_rev_forward_sftpchannel_handler()} will be used.
531
532 The channel will last till the connection gets dropped on the X2go server side or
533 until the tunnel gets paused by an L{X2goRevFwTunnelToSFTP.pause()} call or
534 stopped via the C{X2goRevFwTunnelToSFTP.stop_thread()} method.
535
536 """
537 self._request_port_forwarding()
538 self._keepalive = True
539 while self._keepalive:
540
541 self.incoming_channel.acquire()
542
543 self.logger('waiting for incoming sFTP channel on X2go server port: [localhost]:%s' % self.server_port, loglevel=log.loglevel_DEBUG)
544 self.incoming_channel.wait()
545 if self._keepalive:
546 self.logger('Detected incoming sFTP channel on X2go server port: [localhost]:%s' % self.server_port, loglevel=log.loglevel_DEBUG)
547 _chan = self.ssh_transport.accept()
548 self.logger('sFTP channel %s for server port [localhost]:%s is up' % (_chan, self.server_port), loglevel=log.loglevel_DEBUG)
549 else:
550 self.logger('closing down rev forwarding sFTP tunnel on remote end [localhost]:%s' % self.server_port, loglevel=log.loglevel_DEBUG)
551
552 self.incoming_channel.release()
553 if self._accept_channels and self._keepalive:
554 _new_chan_thread = X2goRevFwSFTPChannelThread(_chan,
555 target=x2go_rev_forward_sftpchannel_handler,
556 kwargs={
557 'chan': _chan,
558 'auth_key': self.auth_key,
559 'logger': self.logger,
560 }
561 )
562 _new_chan_thread.start()
563 self.open_channels['[%s]:%s' % _chan.origin_addr] = _new_chan_thread
564
565
567 """\
568 Handle incoming sFTP channels that got setup by an L{X2goRevFwTunnelToSFTP} instance.
569
570 The channel (and the corresponding connections) close either ...
571
572 - ... if the connecting application closes the connection and thus, drops
573 the sFTP channel, or
574 - ... if the L{X2goRevFwTunnelToSFTP} parent thread gets paused. The call
575 of L{X2goRevFwTunnelToSFTP.pause()} on the instance can be used to shut down all incoming
576 tunneled SSH connections associated to this L{X2goRevFwTunnelToSFTP} instance
577 from within a Python X2go application.
578
579 @param chan: an incoming sFTP channel
580 @type chan: paramiko.Channel instance
581 @param auth_key: Paramiko/SSH RSAkey object that has to be authenticated against by
582 the remote sFTP client
583 @type auth_key: C{paramiko.RSAKey} instance
584 @param logger: you must pass an L{X2goLogger} object to this handler method
585 @type logger: C{X2goLogger} instance
586
587 """
588 if logger is None:
589 def _dummy_logger(msg, l):
590 pass
591 logger = _dummy_logger
592
593 if auth_key is None:
594 logger('sFTP channel %s closed because of missing authentication key' % chan, loglevel=log.loglevel_DEBUG)
595 return
596
597
598 t = paramiko.Transport(chan)
599 t.daemon = True
600 t.load_server_moduli()
601 t.add_server_key(defaults.RSAHostKey)
602
603
604 event = threading.Event()
605 t.set_subsystem_handler('sftp', paramiko.SFTPServer, sftp_si=_SFTPServerInterface, chroot='/', logger=logger, server_event=event)
606 logger('registered sFTP subsystem handler', loglevel=log.loglevel_DEBUG_SFTPXFER)
607 server = _SSHServer(auth_key=auth_key, logger=logger)
608
609
610 t.start_server(server=server, event=event)
611
612 while t.is_active():
613 gevent.sleep(1)
614
615 t.stop_thread()
616 logger('sFTP channel %s closed down' % chan, loglevel=log.loglevel_DEBUG)
617
618
620 """A clone of L{rforward.X2goRevFwChannelThread}."""
621