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