Package x2go :: Module sftpserver
[frames] | no frames]

Source Code for Module x2go.sftpserver

  1  # -*- coding: utf-8 -*- 
  2   
  3  # Copyright (C) 2010-2011 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de> 
  4   
  5  # The Python X2go sFTPServer code was originally written by Richard Murri,  
  6  # for further information see his website: http://www.richardmurri.com 
  7   
  8  # Python X2go is free software; you can redistribute it and/or modify 
  9  # it under the terms of the GNU General Public License as published by 
 10  # the Free Software Foundation; either version 3 of the License, or 
 11  # (at your option) any later version. 
 12  # 
 13  # Python X2go is distributed in the hope that it will be useful, 
 14  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 16  # GNU General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU General Public License 
 19  # along with this program; if not, write to the 
 20  # Free Software Foundation, Inc., 
 21  # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. 
 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  # Python X2go modules 
 42  import rforward 
 43  import defaults 
 44  import utils 
 45  import log 
 46   
47 -class _SSHServer(paramiko.ServerInterface):
48 """\ 49 Implementation of a basic SSH server that is supposed 50 to run with its sFTP server implementation. 51 52 """
53 - def __init__(self, auth_key=None, session_instance=None, logger=None, loglevel=log.loglevel_DEFAULT, *args, **kwargs):
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
80 - def check_channel_request(self, kind, chanid):
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
97 - def check_auth_publickey(self, username, key):
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
116 - def get_allowed_auths(self, username):
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
130 -class _SFTPHandle(paramiko.SFTPHandle):
131 """\ 132 Represents a handle to an open file. 133 134 """
135 - def stat(self):
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
142 -class _SFTPServerInterface(paramiko.SFTPServerInterface):
143 """\ 144 sFTP server implementation. 145 146 """
147 - def __init__(self, server, chroot=None, logger=None, loglevel=log.loglevel_DEFAULT, server_event=None, *args, **kwargs):
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
174 - def _realpath(self, path):
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
195 - def list_folder(self, path):
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
246 - def lstat(self, path):
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 # os.open() defaults to 0777 which is 288 # an odd default mode for files 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 # O_RDONLY (== 0) 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
320 - def remove(self, path):
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
377 - def rmdir(self, path):
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 442 458
459 - def session_ended(self):
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
469 -class X2goRevFwTunnelToSFTP(rforward.X2goRevFwTunnel):
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 """
476 - def __init__(self, server_port, ssh_transport, auth_key=None, session_instance=None, logger=None, loglevel=log.loglevel_DEFAULT):
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
517 - def run(self):
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
566 -def x2go_rev_forward_sftpchannel_handler(chan=None, auth_key=None, logger=None):
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 # set up server 598 t = paramiko.Transport(chan) 599 t.daemon = True 600 t.load_server_moduli() 601 t.add_server_key(defaults.RSAHostKey) 602 603 # set up sftp handler, server and event 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 # start ssh server session 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
619 -class X2goRevFwSFTPChannelThread(rforward.X2goRevFwChannelThread): pass
620 """A clone of L{rforward.X2goRevFwChannelThread}.""" 621