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

Source Code for Module x2go.sftpserver

  1  # -*- coding: utf-8 -*- 
  2   
  3  # Copyright (C) 2010-2016 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 Affero 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 Affero General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU Affero 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 os 
 35  import shutil 
 36  import copy 
 37  import threading 
 38  import paramiko 
 39  import gevent 
 40   
 41  # Python X2Go modules 
 42  import rforward 
 43  import defaults 
 44  import log 
 45   
46 -class _SSHServer(paramiko.ServerInterface):
47 """\ 48 Implementation of a basic SSH server that is supposed 49 to run with its sFTP server implementation. 50 51 """
52 - def __init__(self, auth_key=None, session_instance=None, logger=None, loglevel=log.loglevel_DEFAULT, *args, **kwargs):
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
79 - def check_channel_request(self, kind, chanid):
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
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 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
118 - def get_allowed_auths(self, username):
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
133 -class _SFTPHandle(paramiko.SFTPHandle):
134 """\ 135 Represents a handle to an open file. 136 137 """
138 - def stat(self):
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
152 -class _SFTPServerInterface(paramiko.SFTPServerInterface):
153 """\ 154 sFTP server implementation. 155 156 """
157 - def __init__(self, server, chroot=None, logger=None, loglevel=log.loglevel_DEFAULT, server_event=None, *args, **kwargs):
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
184 - def _realpath(self, path):
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
207 - def list_folder(self, path):
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
259 - def lstat(self, path):
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 # os.open() defaults to 0777 which is 303 # an odd default mode for files 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 # O_RDONLY (== 0) 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
335 - def remove(self, path):
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
396 - def rmdir(self, path):
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 464 482
483 - def session_ended(self):
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
493 -class X2GoRevFwTunnelToSFTP(rforward.X2GoRevFwTunnel):
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 """
500 - def __init__(self, server_port, ssh_transport, auth_key=None, session_instance=None, logger=None, loglevel=log.loglevel_DEFAULT):
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
542 - def run(self):
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
593 -def x2go_rev_forward_sftpchannel_handler(chan=None, auth_key=None, logger=None):
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 # set up server 625 t = paramiko.Transport(chan) 626 t.daemon = True 627 t.load_server_moduli() 628 t.add_server_key(defaults.RSAHostKey) 629 630 # set up sftp handler, server and event 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 # start ssh server session 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
646 -class X2GoRevFwSFTPChannelThread(rforward.X2GoRevFwChannelThread): pass
647 """A clone of L{rforward.X2GoRevFwChannelThread}.""" 648