Package paramiko :: Module sftp_server
[frames] | no frames]

Source Code for Module paramiko.sftp_server

  1  # Copyright (C) 2003-2007  Robey Pointer <robeypointer@gmail.com> 
  2  # 
  3  # This file is part of paramiko. 
  4  # 
  5  # Paramiko is free software; you can redistribute it and/or modify it under the 
  6  # terms of the GNU Lesser General Public License as published by the Free 
  7  # Software Foundation; either version 2.1 of the License, or (at your option) 
  8  # any later version. 
  9  # 
 10  # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 
 11  # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 
 12  # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more 
 13  # details. 
 14  # 
 15  # You should have received a copy of the GNU Lesser General Public License 
 16  # along with Paramiko; if not, write to the Free Software Foundation, Inc., 
 17  # 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA. 
 18   
 19  """ 
 20  Server-mode SFTP support. 
 21  """ 
 22   
 23  import os 
 24  import errno 
 25  import sys 
 26  from hashlib import md5, sha1 
 27   
 28  from paramiko import util 
 29  from paramiko.sftp import BaseSFTP, Message, SFTP_FAILURE, \ 
 30      SFTP_PERMISSION_DENIED, SFTP_NO_SUCH_FILE 
 31  from paramiko.sftp_si import SFTPServerInterface 
 32  from paramiko.sftp_attr import SFTPAttributes 
 33  from paramiko.common import DEBUG 
 34  from paramiko.py3compat import long, string_types, bytes_types, b 
 35  from paramiko.server import SubsystemHandler 
 36   
 37   
 38  # known hash algorithms for the "check-file" extension 
 39  from paramiko.sftp import CMD_HANDLE, SFTP_DESC, CMD_STATUS, SFTP_EOF, CMD_NAME, \ 
 40      SFTP_BAD_MESSAGE, CMD_EXTENDED_REPLY, SFTP_FLAG_READ, SFTP_FLAG_WRITE, \ 
 41      SFTP_FLAG_APPEND, SFTP_FLAG_CREATE, SFTP_FLAG_TRUNC, SFTP_FLAG_EXCL, \ 
 42      CMD_NAMES, CMD_OPEN, CMD_CLOSE, SFTP_OK, CMD_READ, CMD_DATA, CMD_WRITE, \ 
 43      CMD_REMOVE, CMD_RENAME, CMD_MKDIR, CMD_RMDIR, CMD_OPENDIR, CMD_READDIR, \ 
 44      CMD_STAT, CMD_ATTRS, CMD_LSTAT, CMD_FSTAT, CMD_SETSTAT, CMD_FSETSTAT, \ 
 45      CMD_READLINK, CMD_SYMLINK, CMD_REALPATH, CMD_EXTENDED, SFTP_OP_UNSUPPORTED 
 46   
 47  _hash_class = { 
 48      'sha1': sha1, 
 49      'md5': md5, 
 50  } 
 51   
 52   
53 -class SFTPServer (BaseSFTP, SubsystemHandler):
54 """ 55 Server-side SFTP subsystem support. Since this is a `.SubsystemHandler`, 56 it can be (and is meant to be) set as the handler for ``"sftp"`` requests. 57 Use `.Transport.set_subsystem_handler` to activate this class. 58 """ 59
60 - def __init__(self, channel, name, server, sftp_si=SFTPServerInterface, *largs, **kwargs):
61 """ 62 The constructor for SFTPServer is meant to be called from within the 63 `.Transport` as a subsystem handler. ``server`` and any additional 64 parameters or keyword parameters are passed from the original call to 65 `.Transport.set_subsystem_handler`. 66 67 :param .Channel channel: channel passed from the `.Transport`. 68 :param str name: name of the requested subsystem. 69 :param .ServerInterface server: 70 the server object associated with this channel and subsystem 71 :param class sftp_si: 72 a subclass of `.SFTPServerInterface` to use for handling individual 73 requests. 74 """ 75 BaseSFTP.__init__(self) 76 SubsystemHandler.__init__(self, channel, name, server) 77 transport = channel.get_transport() 78 self.logger = util.get_logger(transport.get_log_channel() + '.sftp') 79 self.ultra_debug = transport.get_hexdump() 80 self.next_handle = 1 81 # map of handle-string to SFTPHandle for files & folders: 82 self.file_table = {} 83 self.folder_table = {} 84 self.server = sftp_si(server, *largs, **kwargs)
85
86 - def _log(self, level, msg):
87 if issubclass(type(msg), list): 88 for m in msg: 89 super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + m) 90 else: 91 super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg)
92
93 - def start_subsystem(self, name, transport, channel):
94 self.sock = channel 95 self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel)) 96 self._send_server_version() 97 self.server.session_started() 98 while True: 99 try: 100 t, data = self._read_packet() 101 except EOFError: 102 self._log(DEBUG, 'EOF -- end of session') 103 return 104 except Exception as e: 105 self._log(DEBUG, 'Exception on channel: ' + str(e)) 106 self._log(DEBUG, util.tb_strings()) 107 return 108 msg = Message(data) 109 request_number = msg.get_int() 110 try: 111 self._process(t, request_number, msg) 112 except Exception as e: 113 self._log(DEBUG, 'Exception in server processing: ' + str(e)) 114 self._log(DEBUG, util.tb_strings()) 115 # send some kind of failure message, at least 116 try: 117 self._send_status(request_number, SFTP_FAILURE) 118 except: 119 pass
120
121 - def finish_subsystem(self):
122 self.server.session_ended() 123 super(SFTPServer, self).finish_subsystem() 124 # close any file handles that were left open (so we can return them to the OS quickly) 125 for f in self.file_table.values(): 126 f.close() 127 for f in self.folder_table.values(): 128 f.close() 129 self.file_table = {} 130 self.folder_table = {}
131
132 - def convert_errno(e):
133 """ 134 Convert an errno value (as from an ``OSError`` or ``IOError``) into a 135 standard SFTP result code. This is a convenience function for trapping 136 exceptions in server code and returning an appropriate result. 137 138 :param int e: an errno code, as from ``OSError.errno``. 139 :return: an `int` SFTP error code like ``SFTP_NO_SUCH_FILE``. 140 """ 141 if e == errno.EACCES: 142 # permission denied 143 return SFTP_PERMISSION_DENIED 144 elif (e == errno.ENOENT) or (e == errno.ENOTDIR): 145 # no such file 146 return SFTP_NO_SUCH_FILE 147 else: 148 return SFTP_FAILURE
149 convert_errno = staticmethod(convert_errno) 150
151 - def set_file_attr(filename, attr):
152 """ 153 Change a file's attributes on the local filesystem. The contents of 154 ``attr`` are used to change the permissions, owner, group ownership, 155 and/or modification & access time of the file, depending on which 156 attributes are present in ``attr``. 157 158 This is meant to be a handy helper function for translating SFTP file 159 requests into local file operations. 160 161 :param str filename: 162 name of the file to alter (should usually be an absolute path). 163 :param .SFTPAttributes attr: attributes to change. 164 """ 165 if sys.platform != 'win32': 166 # mode operations are meaningless on win32 167 if attr._flags & attr.FLAG_PERMISSIONS: 168 os.chmod(filename, attr.st_mode) 169 if attr._flags & attr.FLAG_UIDGID: 170 os.chown(filename, attr.st_uid, attr.st_gid) 171 if attr._flags & attr.FLAG_AMTIME: 172 os.utime(filename, (attr.st_atime, attr.st_mtime)) 173 if attr._flags & attr.FLAG_SIZE: 174 with open(filename, 'w+') as f: 175 f.truncate(attr.st_size)
176 set_file_attr = staticmethod(set_file_attr) 177 178 ### internals... 179
180 - def _response(self, request_number, t, *arg):
181 msg = Message() 182 msg.add_int(request_number) 183 for item in arg: 184 if isinstance(item, long): 185 msg.add_int64(item) 186 elif isinstance(item, int): 187 msg.add_int(item) 188 elif isinstance(item, (string_types, bytes_types)): 189 msg.add_string(item) 190 elif type(item) is SFTPAttributes: 191 item._pack(msg) 192 else: 193 raise Exception('unknown type for ' + repr(item) + ' type ' + repr(type(item))) 194 self._send_packet(t, msg)
195
196 - def _send_handle_response(self, request_number, handle, folder=False):
197 if not issubclass(type(handle), SFTPHandle): 198 # must be error code 199 self._send_status(request_number, handle) 200 return 201 handle._set_name(b('hx%d' % self.next_handle)) 202 self.next_handle += 1 203 if folder: 204 self.folder_table[handle._get_name()] = handle 205 else: 206 self.file_table[handle._get_name()] = handle 207 self._response(request_number, CMD_HANDLE, handle._get_name())
208
209 - def _send_status(self, request_number, code, desc=None):
210 if desc is None: 211 try: 212 desc = SFTP_DESC[code] 213 except IndexError: 214 desc = 'Unknown' 215 # some clients expect a "langauge" tag at the end (but don't mind it being blank) 216 self._response(request_number, CMD_STATUS, code, desc, '')
217
218 - def _open_folder(self, request_number, path):
219 resp = self.server.list_folder(path) 220 if issubclass(type(resp), list): 221 # got an actual list of filenames in the folder 222 folder = SFTPHandle() 223 folder._set_files(resp) 224 self._send_handle_response(request_number, folder, True) 225 return 226 # must be an error code 227 self._send_status(request_number, resp)
228
229 - def _read_folder(self, request_number, folder):
230 flist = folder._get_next_files() 231 if len(flist) == 0: 232 self._send_status(request_number, SFTP_EOF) 233 return 234 msg = Message() 235 msg.add_int(request_number) 236 msg.add_int(len(flist)) 237 for attr in flist: 238 msg.add_string(attr.filename) 239 msg.add_string(attr) 240 attr._pack(msg) 241 self._send_packet(CMD_NAME, msg)
242
243 - def _check_file(self, request_number, msg):
244 # this extension actually comes from v6 protocol, but since it's an 245 # extension, i feel like we can reasonably support it backported. 246 # it's very useful for verifying uploaded files or checking for 247 # rsync-like differences between local and remote files. 248 handle = msg.get_binary() 249 alg_list = msg.get_list() 250 start = msg.get_int64() 251 length = msg.get_int64() 252 block_size = msg.get_int() 253 if handle not in self.file_table: 254 self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') 255 return 256 f = self.file_table[handle] 257 for x in alg_list: 258 if x in _hash_class: 259 algname = x 260 alg = _hash_class[x] 261 break 262 else: 263 self._send_status(request_number, SFTP_FAILURE, 'No supported hash types found') 264 return 265 if length == 0: 266 st = f.stat() 267 if not issubclass(type(st), SFTPAttributes): 268 self._send_status(request_number, st, 'Unable to stat file') 269 return 270 length = st.st_size - start 271 if block_size == 0: 272 block_size = length 273 if block_size < 256: 274 self._send_status(request_number, SFTP_FAILURE, 'Block size too small') 275 return 276 277 sum_out = bytes() 278 offset = start 279 while offset < start + length: 280 blocklen = min(block_size, start + length - offset) 281 # don't try to read more than about 64KB at a time 282 chunklen = min(blocklen, 65536) 283 count = 0 284 hash_obj = alg() 285 while count < blocklen: 286 data = f.read(offset, chunklen) 287 if not isinstance(data, bytes_types): 288 self._send_status(request_number, data, 'Unable to hash file') 289 return 290 hash_obj.update(data) 291 count += len(data) 292 offset += count 293 sum_out += hash_obj.digest() 294 295 msg = Message() 296 msg.add_int(request_number) 297 msg.add_string('check-file') 298 msg.add_string(algname) 299 msg.add_bytes(sum_out) 300 self._send_packet(CMD_EXTENDED_REPLY, msg)
301
302 - def _convert_pflags(self, pflags):
303 """convert SFTP-style open() flags to Python's os.open() flags""" 304 if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE): 305 flags = os.O_RDWR 306 elif pflags & SFTP_FLAG_WRITE: 307 flags = os.O_WRONLY 308 else: 309 flags = os.O_RDONLY 310 if pflags & SFTP_FLAG_APPEND: 311 flags |= os.O_APPEND 312 if pflags & SFTP_FLAG_CREATE: 313 flags |= os.O_CREAT 314 if pflags & SFTP_FLAG_TRUNC: 315 flags |= os.O_TRUNC 316 if pflags & SFTP_FLAG_EXCL: 317 flags |= os.O_EXCL 318 return flags
319
320 - def _process(self, t, request_number, msg):
321 self._log(DEBUG, 'Request: %s' % CMD_NAMES[t]) 322 if t == CMD_OPEN: 323 path = msg.get_text() 324 flags = self._convert_pflags(msg.get_int()) 325 attr = SFTPAttributes._from_msg(msg) 326 self._send_handle_response(request_number, self.server.open(path, flags, attr)) 327 elif t == CMD_CLOSE: 328 handle = msg.get_binary() 329 if handle in self.folder_table: 330 del self.folder_table[handle] 331 self._send_status(request_number, SFTP_OK) 332 return 333 if handle in self.file_table: 334 self.file_table[handle].close() 335 del self.file_table[handle] 336 self._send_status(request_number, SFTP_OK) 337 return 338 self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') 339 elif t == CMD_READ: 340 handle = msg.get_binary() 341 offset = msg.get_int64() 342 length = msg.get_int() 343 if handle not in self.file_table: 344 self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') 345 return 346 data = self.file_table[handle].read(offset, length) 347 if isinstance(data, (bytes_types, string_types)): 348 if len(data) == 0: 349 self._send_status(request_number, SFTP_EOF) 350 else: 351 self._response(request_number, CMD_DATA, data) 352 else: 353 self._send_status(request_number, data) 354 elif t == CMD_WRITE: 355 handle = msg.get_binary() 356 offset = msg.get_int64() 357 data = msg.get_binary() 358 if handle not in self.file_table: 359 self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') 360 return 361 self._send_status(request_number, self.file_table[handle].write(offset, data)) 362 elif t == CMD_REMOVE: 363 path = msg.get_text() 364 self._send_status(request_number, self.server.remove(path)) 365 elif t == CMD_RENAME: 366 oldpath = msg.get_text() 367 newpath = msg.get_text() 368 self._send_status(request_number, self.server.rename(oldpath, newpath)) 369 elif t == CMD_MKDIR: 370 path = msg.get_text() 371 attr = SFTPAttributes._from_msg(msg) 372 self._send_status(request_number, self.server.mkdir(path, attr)) 373 elif t == CMD_RMDIR: 374 path = msg.get_text() 375 self._send_status(request_number, self.server.rmdir(path)) 376 elif t == CMD_OPENDIR: 377 path = msg.get_text() 378 self._open_folder(request_number, path) 379 return 380 elif t == CMD_READDIR: 381 handle = msg.get_binary() 382 if handle not in self.folder_table: 383 self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') 384 return 385 folder = self.folder_table[handle] 386 self._read_folder(request_number, folder) 387 elif t == CMD_STAT: 388 path = msg.get_text() 389 resp = self.server.stat(path) 390 if issubclass(type(resp), SFTPAttributes): 391 self._response(request_number, CMD_ATTRS, resp) 392 else: 393 self._send_status(request_number, resp) 394 elif t == CMD_LSTAT: 395 path = msg.get_text() 396 resp = self.server.lstat(path) 397 if issubclass(type(resp), SFTPAttributes): 398 self._response(request_number, CMD_ATTRS, resp) 399 else: 400 self._send_status(request_number, resp) 401 elif t == CMD_FSTAT: 402 handle = msg.get_binary() 403 if handle not in self.file_table: 404 self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') 405 return 406 resp = self.file_table[handle].stat() 407 if issubclass(type(resp), SFTPAttributes): 408 self._response(request_number, CMD_ATTRS, resp) 409 else: 410 self._send_status(request_number, resp) 411 elif t == CMD_SETSTAT: 412 path = msg.get_text() 413 attr = SFTPAttributes._from_msg(msg) 414 self._send_status(request_number, self.server.chattr(path, attr)) 415 elif t == CMD_FSETSTAT: 416 handle = msg.get_binary() 417 attr = SFTPAttributes._from_msg(msg) 418 if handle not in self.file_table: 419 self._response(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') 420 return 421 self._send_status(request_number, self.file_table[handle].chattr(attr)) 422 elif t == CMD_READLINK: 423 path = msg.get_text() 424 resp = self.server.readlink(path) 425 if isinstance(resp, (bytes_types, string_types)): 426 self._response(request_number, CMD_NAME, 1, resp, '', SFTPAttributes()) 427 else: 428 self._send_status(request_number, resp) 429 elif t == CMD_SYMLINK: 430 # the sftp 2 draft is incorrect here! path always follows target_path 431 target_path = msg.get_text() 432 path = msg.get_text() 433 self._send_status(request_number, self.server.symlink(target_path, path)) 434 elif t == CMD_REALPATH: 435 path = msg.get_text() 436 rpath = self.server.canonicalize(path) 437 self._response(request_number, CMD_NAME, 1, rpath, '', SFTPAttributes()) 438 elif t == CMD_EXTENDED: 439 tag = msg.get_text() 440 if tag == 'check-file': 441 self._check_file(request_number, msg) 442 else: 443 self._send_status(request_number, SFTP_OP_UNSUPPORTED) 444 else: 445 self._send_status(request_number, SFTP_OP_UNSUPPORTED)
446 447 448 from paramiko.sftp_handle import SFTPHandle 449