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