1# Copyright (C) 2007 Giampaolo Rodola' <g.rodola@gmail.com>. 2# Use of this source code is governed by MIT license that can be 3# found in the LICENSE file. 4 5import asynchat 6import contextlib 7import errno 8import glob 9import logging 10import os 11import random 12import socket 13import sys 14import time 15import traceback 16import warnings 17from datetime import datetime 18 19try: 20 import pwd 21 import grp 22except ImportError: 23 pwd = grp = None 24 25try: 26 from OpenSSL import SSL # requires "pip install pyopenssl" 27except ImportError: 28 SSL = None 29 30try: 31 from collections import OrderedDict # python >= 2.7 32except ImportError: 33 OrderedDict = dict 34 35from . import __ver__ 36from ._compat import b 37from ._compat import getcwdu 38from ._compat import PY3 39from ._compat import u 40from ._compat import unicode 41from ._compat import xrange 42from .authorizers import AuthenticationFailed 43from .authorizers import AuthorizerError 44from .authorizers import DummyAuthorizer 45from .filesystems import AbstractedFS 46from .filesystems import FilesystemError 47from .ioloop import _ERRNOS_DISCONNECTED 48from .ioloop import _ERRNOS_RETRY 49from .ioloop import Acceptor 50from .ioloop import AsyncChat 51from .ioloop import Connector 52from .ioloop import RetryError 53from .ioloop import timer 54from .log import debug 55from .log import logger 56 57 58CR_BYTE = ord('\r') 59 60 61def _import_sendfile(): 62 # By default attempt to use os.sendfile introduced in Python 3.3: 63 # http://bugs.python.org/issue10882 64 # ...otherwise fallback on using third-party pysendfile module: 65 # https://github.com/giampaolo/pysendfile/ 66 if os.name == 'posix': 67 try: 68 return os.sendfile # py >= 3.3 69 except AttributeError: 70 try: 71 import sendfile as sf 72 # dirty hack to detect whether old 1.2.4 version is installed 73 if hasattr(sf, 'has_sf_hdtr'): 74 raise ImportError 75 return sf.sendfile 76 except ImportError: 77 pass 78 return None 79 80 81sendfile = _import_sendfile() 82 83proto_cmds = { 84 'ABOR': dict( 85 perm=None, auth=True, arg=False, 86 help='Syntax: ABOR (abort transfer).'), 87 'ALLO': dict( 88 perm=None, auth=True, arg=True, 89 help='Syntax: ALLO <SP> bytes (noop; allocate storage).'), 90 'APPE': dict( 91 perm='a', auth=True, arg=True, 92 help='Syntax: APPE <SP> file-name (append data to file).'), 93 'CDUP': dict( 94 perm='e', auth=True, arg=False, 95 help='Syntax: CDUP (go to parent directory).'), 96 'CWD': dict( 97 perm='e', auth=True, arg=None, 98 help='Syntax: CWD [<SP> dir-name] (change working directory).'), 99 'DELE': dict( 100 perm='d', auth=True, arg=True, 101 help='Syntax: DELE <SP> file-name (delete file).'), 102 'EPRT': dict( 103 perm=None, auth=True, arg=True, 104 help='Syntax: EPRT <SP> |proto|ip|port| (extended active mode).'), 105 'EPSV': dict( 106 perm=None, auth=True, arg=None, 107 help='Syntax: EPSV [<SP> proto/"ALL"] (extended passive mode).'), 108 'FEAT': dict( 109 perm=None, auth=False, arg=False, 110 help='Syntax: FEAT (list all new features supported).'), 111 'HELP': dict( 112 perm=None, auth=False, arg=None, 113 help='Syntax: HELP [<SP> cmd] (show help).'), 114 'LIST': dict( 115 perm='l', auth=True, arg=None, 116 help='Syntax: LIST [<SP> path] (list files).'), 117 'MDTM': dict( 118 perm='l', auth=True, arg=True, 119 help='Syntax: MDTM [<SP> path] (file last modification time).'), 120 'MFMT': dict( 121 perm='T', auth=True, arg=True, 122 help='Syntax: MFMT <SP> timeval <SP> path (file update last ' 123 'modification time).'), 124 'MLSD': dict( 125 perm='l', auth=True, arg=None, 126 help='Syntax: MLSD [<SP> path] (list directory).'), 127 'MLST': dict( 128 perm='l', auth=True, arg=None, 129 help='Syntax: MLST [<SP> path] (show information about path).'), 130 'MODE': dict( 131 perm=None, auth=True, arg=True, 132 help='Syntax: MODE <SP> mode (noop; set data transfer mode).'), 133 'MKD': dict( 134 perm='m', auth=True, arg=True, 135 help='Syntax: MKD <SP> path (create directory).'), 136 'NLST': dict( 137 perm='l', auth=True, arg=None, 138 help='Syntax: NLST [<SP> path] (list path in a compact form).'), 139 'NOOP': dict( 140 perm=None, auth=False, arg=False, 141 help='Syntax: NOOP (just do nothing).'), 142 'OPTS': dict( 143 perm=None, auth=True, arg=True, 144 help='Syntax: OPTS <SP> cmd [<SP> option] (set option for command).'), 145 'PASS': dict( 146 perm=None, auth=False, arg=None, 147 help='Syntax: PASS [<SP> password] (set user password).'), 148 'PASV': dict( 149 perm=None, auth=True, arg=False, 150 help='Syntax: PASV (open passive data connection).'), 151 'PORT': dict( 152 perm=None, auth=True, arg=True, 153 help='Syntax: PORT <sp> h,h,h,h,p,p (open active data connection).'), 154 'PWD': dict( 155 perm=None, auth=True, arg=False, 156 help='Syntax: PWD (get current working directory).'), 157 'QUIT': dict( 158 perm=None, auth=False, arg=False, 159 help='Syntax: QUIT (quit current session).'), 160 'REIN': dict( 161 perm=None, auth=True, arg=False, 162 help='Syntax: REIN (flush account).'), 163 'REST': dict( 164 perm=None, auth=True, arg=True, 165 help='Syntax: REST <SP> offset (set file offset).'), 166 'RETR': dict( 167 perm='r', auth=True, arg=True, 168 help='Syntax: RETR <SP> file-name (retrieve a file).'), 169 'RMD': dict( 170 perm='d', auth=True, arg=True, 171 help='Syntax: RMD <SP> dir-name (remove directory).'), 172 'RNFR': dict( 173 perm='f', auth=True, arg=True, 174 help='Syntax: RNFR <SP> file-name (rename (source name)).'), 175 'RNTO': dict( 176 perm='f', auth=True, arg=True, 177 help='Syntax: RNTO <SP> file-name (rename (destination name)).'), 178 'SITE': dict( 179 perm=None, auth=False, arg=True, 180 help='Syntax: SITE <SP> site-command (execute SITE command).'), 181 'SITE HELP': dict( 182 perm=None, auth=False, arg=None, 183 help='Syntax: SITE HELP [<SP> cmd] (show SITE command help).'), 184 'SITE CHMOD': dict( 185 perm='M', auth=True, arg=True, 186 help='Syntax: SITE CHMOD <SP> mode path (change file mode).'), 187 'SIZE': dict( 188 perm='l', auth=True, arg=True, 189 help='Syntax: SIZE <SP> file-name (get file size).'), 190 'STAT': dict( 191 perm='l', auth=False, arg=None, 192 help='Syntax: STAT [<SP> path name] (server stats [list files]).'), 193 'STOR': dict( 194 perm='w', auth=True, arg=True, 195 help='Syntax: STOR <SP> file-name (store a file).'), 196 'STOU': dict( 197 perm='w', auth=True, arg=None, 198 help='Syntax: STOU [<SP> name] (store a file with a unique name).'), 199 'STRU': dict( 200 perm=None, auth=True, arg=True, 201 help='Syntax: STRU <SP> type (noop; set file structure).'), 202 'SYST': dict( 203 perm=None, auth=False, arg=False, 204 help='Syntax: SYST (get operating system type).'), 205 'TYPE': dict( 206 perm=None, auth=True, arg=True, 207 help='Syntax: TYPE <SP> [A | I] (set transfer type).'), 208 'USER': dict( 209 perm=None, auth=False, arg=True, 210 help='Syntax: USER <SP> user-name (set username).'), 211 'XCUP': dict( 212 perm='e', auth=True, arg=False, 213 help='Syntax: XCUP (obsolete; go to parent directory).'), 214 'XCWD': dict( 215 perm='e', auth=True, arg=None, 216 help='Syntax: XCWD [<SP> dir-name] (obsolete; change directory).'), 217 'XMKD': dict( 218 perm='m', auth=True, arg=True, 219 help='Syntax: XMKD <SP> dir-name (obsolete; create directory).'), 220 'XPWD': dict( 221 perm=None, auth=True, arg=False, 222 help='Syntax: XPWD (obsolete; get current dir).'), 223 'XRMD': dict( 224 perm='d', auth=True, arg=True, 225 help='Syntax: XRMD <SP> dir-name (obsolete; remove directory).'), 226} 227 228if not hasattr(os, 'chmod'): 229 del proto_cmds['SITE CHMOD'] 230 231 232def _strerror(err): 233 if isinstance(err, EnvironmentError): 234 try: 235 return os.strerror(err.errno) 236 except AttributeError: 237 # not available on PythonCE 238 if not hasattr(os, 'strerror'): 239 return err.strerror 240 raise 241 else: 242 return str(err) 243 244 245def _is_ssl_sock(sock): 246 return SSL is not None and isinstance(sock, SSL.Connection) 247 248 249def _support_hybrid_ipv6(): 250 """Return True if it is possible to use hybrid IPv6/IPv4 sockets 251 on this platform. 252 """ 253 # Note: IPPROTO_IPV6 constant is broken on Windows, see: 254 # http://bugs.python.org/issue6926 255 try: 256 if not socket.has_ipv6: 257 return False 258 with contextlib.closing(socket.socket(socket.AF_INET6)) as sock: 259 return not sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) 260 except (socket.error, AttributeError): 261 return False 262 263 264SUPPORTS_HYBRID_IPV6 = _support_hybrid_ipv6() 265 266 267class _FileReadWriteError(OSError): 268 """Exception raised when reading or writing a file during a transfer.""" 269 270 271class _GiveUpOnSendfile(Exception): 272 """Exception raised in case use of sendfile() fails on first try, 273 in which case send() will be used. 274 """ 275 276 277# --- DTP classes 278 279class PassiveDTP(Acceptor): 280 """Creates a socket listening on a local port, dispatching the 281 resultant connection to DTPHandler. Used for handling PASV command. 282 283 - (int) timeout: the timeout for a remote client to establish 284 connection with the listening socket. Defaults to 30 seconds. 285 286 - (int) backlog: the maximum number of queued connections passed 287 to listen(). If a connection request arrives when the queue is 288 full the client may raise ECONNRESET. Defaults to 5. 289 """ 290 timeout = 30 291 backlog = None 292 293 def __init__(self, cmd_channel, extmode=False): 294 """Initialize the passive data server. 295 296 - (instance) cmd_channel: the command channel class instance. 297 - (bool) extmode: wheter use extended passive mode response type. 298 """ 299 self.cmd_channel = cmd_channel 300 self.log = cmd_channel.log 301 self.log_exception = cmd_channel.log_exception 302 Acceptor.__init__(self, ioloop=cmd_channel.ioloop) 303 304 local_ip = self.cmd_channel.socket.getsockname()[0] 305 if local_ip in self.cmd_channel.masquerade_address_map: 306 masqueraded_ip = self.cmd_channel.masquerade_address_map[local_ip] 307 elif self.cmd_channel.masquerade_address: 308 masqueraded_ip = self.cmd_channel.masquerade_address 309 else: 310 masqueraded_ip = None 311 312 if self.cmd_channel.server.socket.family != socket.AF_INET: 313 # dual stack IPv4/IPv6 support 314 af = self.bind_af_unspecified((local_ip, 0)) 315 self.socket.close() 316 else: 317 af = self.cmd_channel.socket.family 318 319 self.create_socket(af, socket.SOCK_STREAM) 320 321 if self.cmd_channel.passive_ports is None: 322 # By using 0 as port number value we let kernel choose a 323 # free unprivileged random port. 324 self.bind((local_ip, 0)) 325 else: 326 ports = list(self.cmd_channel.passive_ports) 327 while ports: 328 port = ports.pop(random.randint(0, len(ports) - 1)) 329 self.set_reuse_addr() 330 try: 331 self.bind((local_ip, port)) 332 except socket.error as err: 333 if err.errno == errno.EADDRINUSE: # port already in use 334 if ports: 335 continue 336 # If cannot use one of the ports in the configured 337 # range we'll use a kernel-assigned port, and log 338 # a message reporting the issue. 339 # By using 0 as port number value we let kernel 340 # choose a free unprivileged random port. 341 else: 342 self.bind((local_ip, 0)) 343 self.cmd_channel.log( 344 "Can't find a valid passive port in the " 345 "configured range. A random kernel-assigned " 346 "port will be used.", 347 logfun=logger.warning 348 ) 349 else: 350 raise 351 else: 352 break 353 self.listen(self.backlog or self.cmd_channel.server.backlog) 354 355 port = self.socket.getsockname()[1] 356 if not extmode: 357 ip = masqueraded_ip or local_ip 358 if ip.startswith('::ffff:'): 359 # In this scenario, the server has an IPv6 socket, but 360 # the remote client is using IPv4 and its address is 361 # represented as an IPv4-mapped IPv6 address which 362 # looks like this ::ffff:151.12.5.65, see: 363 # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses 364 # http://tools.ietf.org/html/rfc3493.html#section-3.7 365 # We truncate the first bytes to make it look like a 366 # common IPv4 address. 367 ip = ip[7:] 368 # The format of 227 response in not standardized. 369 # This is the most expected: 370 resp = '227 Entering passive mode (%s,%d,%d).' % ( 371 ip.replace('.', ','), port // 256, port % 256) 372 self.cmd_channel.respond(resp) 373 else: 374 self.cmd_channel.respond('229 Entering extended passive mode ' 375 '(|||%d|).' % port) 376 if self.timeout: 377 self.call_later(self.timeout, self.handle_timeout) 378 379 # --- connection / overridden 380 381 def handle_accepted(self, sock, addr): 382 """Called when remote client initiates a connection.""" 383 if not self.cmd_channel.connected: 384 return self.close() 385 386 # Check the origin of data connection. If not expressively 387 # configured we drop the incoming data connection if remote 388 # IP address does not match the client's IP address. 389 if self.cmd_channel.remote_ip != addr[0]: 390 if not self.cmd_channel.permit_foreign_addresses: 391 try: 392 sock.close() 393 except socket.error: 394 pass 395 msg = '425 Rejected data connection from foreign address ' \ 396 '%s:%s.' % (addr[0], addr[1]) 397 self.cmd_channel.respond_w_warning(msg) 398 # do not close listening socket: it couldn't be client's blame 399 return 400 else: 401 # site-to-site FTP allowed 402 msg = 'Established data connection with foreign address ' \ 403 '%s:%s.' % (addr[0], addr[1]) 404 self.cmd_channel.log(msg, logfun=logger.warning) 405 # Immediately close the current channel (we accept only one 406 # connection at time) and avoid running out of max connections 407 # limit. 408 self.close() 409 # delegate such connection to DTP handler 410 if self.cmd_channel.connected: 411 handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel) 412 if handler.connected: 413 self.cmd_channel.data_channel = handler 414 self.cmd_channel._on_dtp_connection() 415 416 def handle_timeout(self): 417 if self.cmd_channel.connected: 418 self.cmd_channel.respond("421 Passive data channel timed out.", 419 logfun=logger.info) 420 self.close() 421 422 def handle_error(self): 423 """Called to handle any uncaught exceptions.""" 424 try: 425 raise 426 except Exception: 427 logger.error(traceback.format_exc()) 428 try: 429 self.close() 430 except Exception: 431 logger.critical(traceback.format_exc()) 432 433 def close(self): 434 debug("call: close()", inst=self) 435 Acceptor.close(self) 436 437 438class ActiveDTP(Connector): 439 """Connects to remote client and dispatches the resulting connection 440 to DTPHandler. Used for handling PORT command. 441 442 - (int) timeout: the timeout for us to establish connection with 443 the client's listening data socket. 444 """ 445 timeout = 30 446 447 def __init__(self, ip, port, cmd_channel): 448 """Initialize the active data channel attemping to connect 449 to remote data socket. 450 451 - (str) ip: the remote IP address. 452 - (int) port: the remote port. 453 - (instance) cmd_channel: the command channel class instance. 454 """ 455 Connector.__init__(self, ioloop=cmd_channel.ioloop) 456 self.cmd_channel = cmd_channel 457 self.log = cmd_channel.log 458 self.log_exception = cmd_channel.log_exception 459 self._idler = None 460 if self.timeout: 461 self._idler = self.ioloop.call_later(self.timeout, 462 self.handle_timeout, 463 _errback=self.handle_error) 464 465 if ip.count('.') == 4: 466 self._cmd = "PORT" 467 self._normalized_addr = "%s:%s" % (ip, port) 468 else: 469 self._cmd = "EPRT" 470 self._normalized_addr = "[%s]:%s" % (ip, port) 471 472 source_ip = self.cmd_channel.socket.getsockname()[0] 473 # dual stack IPv4/IPv6 support 474 try: 475 self.connect_af_unspecified((ip, port), (source_ip, 0)) 476 except (socket.gaierror, socket.error): 477 self.handle_close() 478 479 def readable(self): 480 return False 481 482 def handle_write(self): 483 # overridden to prevent unhandled read/write event messages to 484 # be printed by asyncore on Python < 2.6 485 pass 486 487 def handle_connect(self): 488 """Called when connection is established.""" 489 self.del_channel() 490 if self._idler is not None and not self._idler.cancelled: 491 self._idler.cancel() 492 if not self.cmd_channel.connected: 493 return self.close() 494 # fix for asyncore on python < 2.6, meaning we aren't 495 # actually connected. 496 # test_active_conn_error tests this condition 497 err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) 498 if err != 0: 499 raise socket.error(err) 500 # 501 msg = 'Active data connection established.' 502 self.cmd_channel.respond('200 ' + msg) 503 self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 200, msg) 504 # 505 if not self.cmd_channel.connected: 506 return self.close() 507 # delegate such connection to DTP handler 508 handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel) 509 self.cmd_channel.data_channel = handler 510 self.cmd_channel._on_dtp_connection() 511 512 def handle_timeout(self): 513 if self.cmd_channel.connected: 514 msg = "Active data channel timed out." 515 self.cmd_channel.respond("421 " + msg, logfun=logger.info) 516 self.cmd_channel.log_cmd( 517 self._cmd, self._normalized_addr, 421, msg) 518 self.close() 519 520 def handle_close(self): 521 # With the new IO loop, handle_close() gets called in case 522 # the fd appears in the list of exceptional fds. 523 # This means connect() failed. 524 if not self._closed: 525 self.close() 526 if self.cmd_channel.connected: 527 msg = "Can't connect to specified address." 528 self.cmd_channel.respond("425 " + msg) 529 self.cmd_channel.log_cmd( 530 self._cmd, self._normalized_addr, 425, msg) 531 532 def handle_error(self): 533 """Called to handle any uncaught exceptions.""" 534 try: 535 raise 536 except (socket.gaierror, socket.error): 537 pass 538 except Exception: 539 self.log_exception(self) 540 try: 541 self.handle_close() 542 except Exception: 543 logger.critical(traceback.format_exc()) 544 545 def close(self): 546 debug("call: close()", inst=self) 547 if not self._closed: 548 Connector.close(self) 549 if self._idler is not None and not self._idler.cancelled: 550 self._idler.cancel() 551 552 553class DTPHandler(AsyncChat): 554 """Class handling server-data-transfer-process (server-DTP, see 555 RFC-959) managing data-transfer operations involving sending 556 and receiving data. 557 558 Class attributes: 559 560 - (int) timeout: the timeout which roughly is the maximum time we 561 permit data transfers to stall for with no progress. If the 562 timeout triggers, the remote client will be kicked off 563 (defaults 300). 564 565 - (int) ac_in_buffer_size: incoming data buffer size (defaults 65536) 566 567 - (int) ac_out_buffer_size: outgoing data buffer size (defaults 65536) 568 """ 569 570 timeout = 300 571 ac_in_buffer_size = 65536 572 ac_out_buffer_size = 65536 573 574 def __init__(self, sock, cmd_channel): 575 """Initialize the command channel. 576 577 - (instance) sock: the socket object instance of the newly 578 established connection. 579 - (instance) cmd_channel: the command channel class instance. 580 """ 581 self.cmd_channel = cmd_channel 582 self.file_obj = None 583 self.receive = False 584 self.transfer_finished = False 585 self.tot_bytes_sent = 0 586 self.tot_bytes_received = 0 587 self.cmd = None 588 self.log = cmd_channel.log 589 self.log_exception = cmd_channel.log_exception 590 self._data_wrapper = None 591 self._lastdata = 0 592 self._had_cr = False 593 self._start_time = timer() 594 self._resp = () 595 self._offset = None 596 self._filefd = None 597 self._idler = None 598 self._initialized = False 599 try: 600 AsyncChat.__init__(self, sock, ioloop=cmd_channel.ioloop) 601 except socket.error as err: 602 # if we get an exception here we want the dispatcher 603 # instance to set socket attribute before closing, see: 604 # https://github.com/giampaolo/pyftpdlib/issues/188 605 AsyncChat.__init__( 606 self, socket.socket(), ioloop=cmd_channel.ioloop) 607 # https://github.com/giampaolo/pyftpdlib/issues/143 608 self.close() 609 if err.errno == errno.EINVAL: 610 return 611 self.handle_error() 612 return 613 614 # remove this instance from IOLoop's socket map 615 if not self.connected: 616 self.close() 617 return 618 if self.timeout: 619 self._idler = self.ioloop.call_every(self.timeout, 620 self.handle_timeout, 621 _errback=self.handle_error) 622 623 def __repr__(self): 624 return '<%s(%s)>' % (self.__class__.__name__, 625 self.cmd_channel.get_repr_info(as_str=True)) 626 627 __str__ = __repr__ 628 629 def use_sendfile(self): 630 if not self.cmd_channel.use_sendfile: 631 # as per server config 632 return False 633 if self.file_obj is None or not hasattr(self.file_obj, "fileno"): 634 # directory listing or unusual file obj 635 return False 636 if self.cmd_channel._current_type != 'i': 637 # text file transfer (need to transform file content on the fly) 638 return False 639 return True 640 641 def push(self, data): 642 self._initialized = True 643 self.modify_ioloop_events(self.ioloop.WRITE) 644 self._wanted_io_events = self.ioloop.WRITE 645 AsyncChat.push(self, data) 646 647 def push_with_producer(self, producer): 648 self._initialized = True 649 self.modify_ioloop_events(self.ioloop.WRITE) 650 self._wanted_io_events = self.ioloop.WRITE 651 if self.use_sendfile(): 652 self._offset = producer.file.tell() 653 self._filefd = self.file_obj.fileno() 654 try: 655 self.initiate_sendfile() 656 except _GiveUpOnSendfile: 657 pass 658 else: 659 self.initiate_send = self.initiate_sendfile 660 return 661 debug("starting transfer using send()", self) 662 AsyncChat.push_with_producer(self, producer) 663 664 def close_when_done(self): 665 asynchat.async_chat.close_when_done(self) 666 667 def initiate_send(self): 668 asynchat.async_chat.initiate_send(self) 669 670 def initiate_sendfile(self): 671 """A wrapper around sendfile.""" 672 try: 673 sent = sendfile(self._fileno, self._filefd, self._offset, 674 self.ac_out_buffer_size) 675 except OSError as err: 676 if err.errno in _ERRNOS_RETRY or err.errno == errno.EBUSY: 677 return 678 elif err.errno in _ERRNOS_DISCONNECTED: 679 self.handle_close() 680 else: 681 if self.tot_bytes_sent == 0: 682 logger.warning( 683 "sendfile() failed; falling back on using plain send") 684 raise _GiveUpOnSendfile 685 else: 686 raise 687 else: 688 if sent == 0: 689 # this signals the channel that the transfer is completed 690 self.discard_buffers() 691 self.handle_close() 692 else: 693 self._offset += sent 694 self.tot_bytes_sent += sent 695 696 # --- utility methods 697 698 def _posix_ascii_data_wrapper(self, chunk): 699 """The data wrapper used for receiving data in ASCII mode on 700 systems using a single line terminator, handling those cases 701 where CRLF ('\r\n') gets delivered in two chunks. 702 """ 703 if self._had_cr: 704 chunk = b'\r' + chunk 705 706 if chunk.endswith(b'\r'): 707 self._had_cr = True 708 chunk = chunk[:-1] 709 else: 710 self._had_cr = False 711 712 return chunk.replace(b'\r\n', b(os.linesep)) 713 714 def enable_receiving(self, type, cmd): 715 """Enable receiving of data over the channel. Depending on the 716 TYPE currently in use it creates an appropriate wrapper for the 717 incoming data. 718 719 - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary). 720 """ 721 self._initialized = True 722 self.modify_ioloop_events(self.ioloop.READ) 723 self._wanted_io_events = self.ioloop.READ 724 self.cmd = cmd 725 if type == 'a': 726 if os.linesep == '\r\n': 727 self._data_wrapper = None 728 else: 729 self._data_wrapper = self._posix_ascii_data_wrapper 730 elif type == 'i': 731 self._data_wrapper = None 732 else: 733 raise TypeError("unsupported type") 734 self.receive = True 735 736 def get_transmitted_bytes(self): 737 """Return the number of transmitted bytes.""" 738 return self.tot_bytes_sent + self.tot_bytes_received 739 740 def get_elapsed_time(self): 741 """Return the transfer elapsed time in seconds.""" 742 return timer() - self._start_time 743 744 def transfer_in_progress(self): 745 """Return True if a transfer is in progress, else False.""" 746 return self.get_transmitted_bytes() != 0 747 748 # --- connection 749 750 def send(self, data): 751 result = AsyncChat.send(self, data) 752 self.tot_bytes_sent += result 753 return result 754 755 def refill_buffer(self): # pragma: no cover 756 """Overridden as a fix around http://bugs.python.org/issue1740572 757 (when the producer is consumed, close() was called instead of 758 handle_close()). 759 """ 760 while True: 761 if len(self.producer_fifo): 762 p = self.producer_fifo.first() 763 # a 'None' in the producer fifo is a sentinel, 764 # telling us to close the channel. 765 if p is None: 766 if not self.ac_out_buffer: 767 self.producer_fifo.pop() 768 # self.close() 769 self.handle_close() 770 return 771 elif isinstance(p, str): 772 self.producer_fifo.pop() 773 self.ac_out_buffer += p 774 return 775 data = p.more() 776 if data: 777 self.ac_out_buffer = self.ac_out_buffer + data 778 return 779 else: 780 self.producer_fifo.pop() 781 else: 782 return 783 784 def handle_read(self): 785 """Called when there is data waiting to be read.""" 786 try: 787 chunk = self.recv(self.ac_in_buffer_size) 788 except RetryError: 789 pass 790 except socket.error: 791 self.handle_error() 792 else: 793 self.tot_bytes_received += len(chunk) 794 if not chunk: 795 self.transfer_finished = True 796 # self.close() # <-- asyncore.recv() already do that... 797 return 798 if self._data_wrapper is not None: 799 chunk = self._data_wrapper(chunk) 800 try: 801 self.file_obj.write(chunk) 802 except OSError as err: 803 raise _FileReadWriteError(err) 804 805 handle_read_event = handle_read # small speedup 806 807 def readable(self): 808 """Predicate for inclusion in the readable for select().""" 809 # It the channel is not supposed to be receiving but yet it's 810 # in the list of readable events, that means it has been 811 # disconnected, in which case we explicitly close() it. 812 # This is necessary as differently from FTPHandler this channel 813 # is not supposed to be readable/writable at first, meaning the 814 # upper IOLoop might end up calling readable() repeatedly, 815 # hogging CPU resources. 816 if not self.receive and not self._initialized: 817 return self.close() 818 return self.receive 819 820 def writable(self): 821 """Predicate for inclusion in the writable for select().""" 822 return not self.receive and asynchat.async_chat.writable(self) 823 824 def handle_timeout(self): 825 """Called cyclically to check if data trasfer is stalling with 826 no progress in which case the client is kicked off. 827 """ 828 if self.get_transmitted_bytes() > self._lastdata: 829 self._lastdata = self.get_transmitted_bytes() 830 else: 831 msg = "Data connection timed out." 832 self._resp = ("421 " + msg, logger.info) 833 self.close() 834 self.cmd_channel.close_when_done() 835 836 def handle_error(self): 837 """Called when an exception is raised and not otherwise handled.""" 838 try: 839 raise 840 # an error could occur in case we fail reading / writing 841 # from / to file (e.g. file system gets full) 842 except _FileReadWriteError as err: 843 error = _strerror(err.errno) 844 except Exception: 845 # some other exception occurred; we don't want to provide 846 # confidential error messages 847 self.log_exception(self) 848 error = "Internal error" 849 try: 850 self._resp = ("426 %s; transfer aborted." % error, logger.warning) 851 self.close() 852 except Exception: 853 logger.critical(traceback.format_exc()) 854 855 def handle_close(self): 856 """Called when the socket is closed.""" 857 # If we used channel for receiving we assume that transfer is 858 # finished when client closes the connection, if we used channel 859 # for sending we have to check that all data has been sent 860 # (responding with 226) or not (responding with 426). 861 # In both cases handle_close() is automatically called by the 862 # underlying asynchat module. 863 if not self._closed: 864 if self.receive: 865 self.transfer_finished = True 866 else: 867 self.transfer_finished = len(self.producer_fifo) == 0 868 try: 869 if self.transfer_finished: 870 self._resp = ("226 Transfer complete.", logger.debug) 871 else: 872 tot_bytes = self.get_transmitted_bytes() 873 self._resp = ("426 Transfer aborted; %d bytes transmitted." 874 % tot_bytes, logger.debug) 875 finally: 876 self.close() 877 878 def close(self): 879 """Close the data channel, first attempting to close any remaining 880 file handles.""" 881 debug("call: close()", inst=self) 882 if not self._closed: 883 # RFC-959 says we must close the connection before replying 884 AsyncChat.close(self) 885 886 # Close file object before responding successfully to client 887 if self.file_obj is not None and not self.file_obj.closed: 888 self.file_obj.close() 889 890 if self._resp: 891 self.cmd_channel.respond(self._resp[0], logfun=self._resp[1]) 892 893 if self._idler is not None and not self._idler.cancelled: 894 self._idler.cancel() 895 if self.file_obj is not None: 896 filename = self.file_obj.name 897 elapsed_time = round(self.get_elapsed_time(), 3) 898 self.cmd_channel.log_transfer( 899 cmd=self.cmd, 900 filename=self.file_obj.name, 901 receive=self.receive, 902 completed=self.transfer_finished, 903 elapsed=elapsed_time, 904 bytes=self.get_transmitted_bytes()) 905 if self.transfer_finished: 906 if self.receive: 907 self.cmd_channel.on_file_received(filename) 908 else: 909 self.cmd_channel.on_file_sent(filename) 910 else: 911 if self.receive: 912 self.cmd_channel.on_incomplete_file_received(filename) 913 else: 914 self.cmd_channel.on_incomplete_file_sent(filename) 915 self.cmd_channel._on_dtp_close() 916 917 918# dirty hack in order to turn AsyncChat into a new style class in 919# python 2.x so that we can use super() 920if PY3: 921 class _AsyncChatNewStyle(AsyncChat): 922 pass 923else: 924 class _AsyncChatNewStyle(object, AsyncChat): 925 926 def __init__(self, *args, **kwargs): 927 super(object, self).__init__(*args, **kwargs) # bypass object 928 929 930class ThrottledDTPHandler(_AsyncChatNewStyle, DTPHandler): 931 """A DTPHandler subclass which wraps sending and receiving in a data 932 counter and temporarily "sleeps" the channel so that you burst to no 933 more than x Kb/sec average. 934 935 - (int) read_limit: the maximum number of bytes to read (receive) 936 in one second (defaults to 0 == no limit). 937 938 - (int) write_limit: the maximum number of bytes to write (send) 939 in one second (defaults to 0 == no limit). 940 941 - (bool) auto_sized_buffers: this option only applies when read 942 and/or write limits are specified. When enabled it bumps down 943 the data buffer sizes so that they are never greater than read 944 and write limits which results in a less bursty and smoother 945 throughput (default: True). 946 """ 947 read_limit = 0 948 write_limit = 0 949 auto_sized_buffers = True 950 951 def __init__(self, sock, cmd_channel): 952 super(ThrottledDTPHandler, self).__init__(sock, cmd_channel) 953 self._timenext = 0 954 self._datacount = 0 955 self.sleeping = False 956 self._throttler = None 957 if self.auto_sized_buffers: 958 if self.read_limit: 959 while self.ac_in_buffer_size > self.read_limit: 960 self.ac_in_buffer_size /= 2 961 if self.write_limit: 962 while self.ac_out_buffer_size > self.write_limit: 963 self.ac_out_buffer_size /= 2 964 self.ac_in_buffer_size = int(self.ac_in_buffer_size) 965 self.ac_out_buffer_size = int(self.ac_out_buffer_size) 966 967 def __repr__(self): 968 return DTPHandler.__repr__(self) 969 970 def use_sendfile(self): 971 return False 972 973 def recv(self, buffer_size): 974 chunk = super(ThrottledDTPHandler, self).recv(buffer_size) 975 if self.read_limit: 976 self._throttle_bandwidth(len(chunk), self.read_limit) 977 return chunk 978 979 def send(self, data): 980 num_sent = super(ThrottledDTPHandler, self).send(data) 981 if self.write_limit: 982 self._throttle_bandwidth(num_sent, self.write_limit) 983 return num_sent 984 985 def _cancel_throttler(self): 986 if self._throttler is not None and not self._throttler.cancelled: 987 self._throttler.cancel() 988 989 def _throttle_bandwidth(self, len_chunk, max_speed): 990 """A method which counts data transmitted so that you burst to 991 no more than x Kb/sec average. 992 """ 993 self._datacount += len_chunk 994 if self._datacount >= max_speed: 995 self._datacount = 0 996 now = timer() 997 sleepfor = (self._timenext - now) * 2 998 if sleepfor > 0: 999 # we've passed bandwidth limits 1000 def unsleep(): 1001 if self.receive: 1002 event = self.ioloop.READ 1003 else: 1004 event = self.ioloop.WRITE 1005 self.add_channel(events=event) 1006 1007 self.del_channel() 1008 self._cancel_throttler() 1009 self._throttler = self.ioloop.call_later( 1010 sleepfor, unsleep, _errback=self.handle_error) 1011 self._timenext = now + 1 1012 1013 def close(self): 1014 self._cancel_throttler() 1015 super(ThrottledDTPHandler, self).close() 1016 1017 1018# --- producers 1019 1020 1021class FileProducer(object): 1022 """Producer wrapper for file[-like] objects.""" 1023 1024 buffer_size = 65536 1025 1026 def __init__(self, file, type): 1027 """Initialize the producer with a data_wrapper appropriate to TYPE. 1028 1029 - (file) file: the file[-like] object. 1030 - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary). 1031 """ 1032 self.file = file 1033 self.type = type 1034 self._prev_chunk_endswith_cr = False 1035 if type == 'a' and os.linesep != '\r\n': 1036 self._data_wrapper = self._posix_ascii_data_wrapper 1037 else: 1038 self._data_wrapper = None 1039 1040 def _posix_ascii_data_wrapper(self, chunk): 1041 """The data wrapper used for sending data in ASCII mode on 1042 systems using a single line terminator, handling those cases 1043 where CRLF ('\r\n') gets delivered in two chunks. 1044 """ 1045 chunk = bytearray(chunk) 1046 pos = 0 1047 if self._prev_chunk_endswith_cr and chunk.startswith(b'\n'): 1048 pos += 1 1049 while True: 1050 pos = chunk.find(b'\n', pos) 1051 if pos == -1: 1052 break 1053 if chunk[pos - 1] != CR_BYTE: 1054 chunk.insert(pos, CR_BYTE) 1055 pos += 1 1056 pos += 1 1057 self._prev_chunk_endswith_cr = chunk.endswith(b'\r') 1058 return chunk 1059 1060 def more(self): 1061 """Attempt a chunk of data of size self.buffer_size.""" 1062 try: 1063 data = self.file.read(self.buffer_size) 1064 except OSError as err: 1065 raise _FileReadWriteError(err) 1066 else: 1067 if self._data_wrapper is not None: 1068 data = self._data_wrapper(data) 1069 return data 1070 1071 1072class BufferedIteratorProducer(object): 1073 """Producer for iterator objects with buffer capabilities.""" 1074 # how many times iterator.next() will be called before 1075 # returning some data 1076 loops = 20 1077 1078 def __init__(self, iterator): 1079 self.iterator = iterator 1080 1081 def more(self): 1082 """Attempt a chunk of data from iterator by calling 1083 its next() method different times. 1084 """ 1085 buffer = [] 1086 for x in xrange(self.loops): 1087 try: 1088 buffer.append(next(self.iterator)) 1089 except StopIteration: 1090 break 1091 return b''.join(buffer) 1092 1093 1094# --- FTP 1095 1096class FTPHandler(AsyncChat): 1097 """Implements the FTP server Protocol Interpreter (see RFC-959), 1098 handling commands received from the client on the control channel. 1099 1100 All relevant session information is stored in class attributes 1101 reproduced below and can be modified before instantiating this 1102 class. 1103 1104 - (int) timeout: 1105 The timeout which is the maximum time a remote client may spend 1106 between FTP commands. If the timeout triggers, the remote client 1107 will be kicked off. Defaults to 300 seconds. 1108 1109 - (str) banner: the string sent when client connects. 1110 1111 - (int) max_login_attempts: 1112 the maximum number of wrong authentications before disconnecting 1113 the client (default 3). 1114 1115 - (bool)permit_foreign_addresses: 1116 FTP site-to-site transfer feature: also referenced as "FXP" it 1117 permits for transferring a file between two remote FTP servers 1118 without the transfer going through the client's host (not 1119 recommended for security reasons as described in RFC-2577). 1120 Having this attribute set to False means that all data 1121 connections from/to remote IP addresses which do not match the 1122 client's IP address will be dropped (defualt False). 1123 1124 - (bool) permit_privileged_ports: 1125 set to True if you want to permit active data connections (PORT) 1126 over privileged ports (not recommended, defaulting to False). 1127 1128 - (str) masquerade_address: 1129 the "masqueraded" IP address to provide along PASV reply when 1130 pyftpdlib is running behind a NAT or other types of gateways. 1131 When configured pyftpdlib will hide its local address and 1132 instead use the public address of your NAT (default None). 1133 1134 - (dict) masquerade_address_map: 1135 in case the server has multiple IP addresses which are all 1136 behind a NAT router, you may wish to specify individual 1137 masquerade_addresses for each of them. The map expects a 1138 dictionary containing private IP addresses as keys, and their 1139 corresponding public (masquerade) addresses as values. 1140 1141 - (list) passive_ports: 1142 what ports the ftpd will use for its passive data transfers. 1143 Value expected is a list of integers (e.g. range(60000, 65535)). 1144 When configured pyftpdlib will no longer use kernel-assigned 1145 random ports (default None). 1146 1147 - (bool) use_gmt_times: 1148 when True causes the server to report all ls and MDTM times in 1149 GMT and not local time (default True). 1150 1151 - (bool) use_sendfile: when True uses sendfile() system call to 1152 send a file resulting in faster uploads (from server to client). 1153 Works on UNIX only and requires pysendfile module to be 1154 installed separately: 1155 https://github.com/giampaolo/pysendfile/ 1156 Automatically defaults to True if pysendfile module is 1157 installed. 1158 1159 - (bool) tcp_no_delay: controls the use of the TCP_NODELAY socket 1160 option which disables the Nagle algorithm resulting in 1161 significantly better performances (default True on all systems 1162 where it is supported). 1163 1164 - (str) unicode_errors: 1165 the error handler passed to ''.encode() and ''.decode(): 1166 http://docs.python.org/library/stdtypes.html#str.decode 1167 (detaults to 'replace'). 1168 1169 - (str) log_prefix: 1170 the prefix string preceding any log line; all instance 1171 attributes can be used as arguments. 1172 1173 1174 All relevant instance attributes initialized when client connects 1175 are reproduced below. You may be interested in them in case you 1176 want to subclass the original FTPHandler. 1177 1178 - (bool) authenticated: True if client authenticated himself. 1179 - (str) username: the name of the connected user (if any). 1180 - (int) attempted_logins: number of currently attempted logins. 1181 - (str) current_type: the current transfer type (default "a") 1182 - (int) af: the connection's address family (IPv4/IPv6) 1183 - (instance) server: the FTPServer class instance. 1184 - (instance) data_channel: the data channel instance (if any). 1185 """ 1186 # these are overridable defaults 1187 1188 # default classes 1189 authorizer = DummyAuthorizer() 1190 active_dtp = ActiveDTP 1191 passive_dtp = PassiveDTP 1192 dtp_handler = DTPHandler 1193 abstracted_fs = AbstractedFS 1194 proto_cmds = proto_cmds 1195 1196 # session attributes (explained in the docstring) 1197 timeout = 300 1198 banner = "pyftpdlib %s ready." % __ver__ 1199 max_login_attempts = 3 1200 permit_foreign_addresses = False 1201 permit_privileged_ports = False 1202 masquerade_address = None 1203 masquerade_address_map = {} 1204 passive_ports = None 1205 use_gmt_times = True 1206 use_sendfile = sendfile is not None 1207 tcp_no_delay = hasattr(socket, "TCP_NODELAY") 1208 unicode_errors = 'replace' 1209 log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]' 1210 auth_failed_timeout = 3 1211 1212 def __init__(self, conn, server, ioloop=None): 1213 """Initialize the command channel. 1214 1215 - (instance) conn: the socket object instance of the newly 1216 established connection. 1217 - (instance) server: the ftp server class instance. 1218 """ 1219 # public session attributes 1220 self.server = server 1221 self.fs = None 1222 self.authenticated = False 1223 self.username = "" 1224 self.password = "" 1225 self.attempted_logins = 0 1226 self.data_channel = None 1227 self.remote_ip = "" 1228 self.remote_port = "" 1229 self.started = time.time() 1230 1231 # private session attributes 1232 self._last_response = "" 1233 self._current_type = 'a' 1234 self._restart_position = 0 1235 self._quit_pending = False 1236 self._in_buffer = [] 1237 self._in_buffer_len = 0 1238 self._epsvall = False 1239 self._dtp_acceptor = None 1240 self._dtp_connector = None 1241 self._in_dtp_queue = None 1242 self._out_dtp_queue = None 1243 self._extra_feats = [] 1244 self._current_facts = ['type', 'perm', 'size', 'modify'] 1245 self._rnfr = None 1246 self._idler = None 1247 self._log_debug = logging.getLogger('pyftpdlib').getEffectiveLevel() \ 1248 <= logging.DEBUG 1249 1250 if os.name == 'posix': 1251 self._current_facts.append('unique') 1252 self._available_facts = self._current_facts[:] 1253 if pwd and grp: 1254 self._available_facts += ['unix.mode', 'unix.uid', 'unix.gid'] 1255 if os.name == 'nt': 1256 self._available_facts.append('create') 1257 1258 try: 1259 AsyncChat.__init__(self, conn, ioloop=ioloop) 1260 except socket.error as err: 1261 # if we get an exception here we want the dispatcher 1262 # instance to set socket attribute before closing, see: 1263 # https://github.com/giampaolo/pyftpdlib/issues/188 1264 AsyncChat.__init__(self, socket.socket(), ioloop=ioloop) 1265 self.close() 1266 debug("call: FTPHandler.__init__, err %r" % err, self) 1267 if err.errno == errno.EINVAL: 1268 # https://github.com/giampaolo/pyftpdlib/issues/143 1269 return 1270 self.handle_error() 1271 return 1272 self.set_terminator(b"\r\n") 1273 1274 # connection properties 1275 try: 1276 self.remote_ip, self.remote_port = self.socket.getpeername()[:2] 1277 except socket.error as err: 1278 debug("call: FTPHandler.__init__, err on getpeername() %r" % err, 1279 self) 1280 # A race condition may occur if the other end is closing 1281 # before we can get the peername, hence ENOTCONN (see issue 1282 # #100) while EINVAL can occur on OSX (see issue #143). 1283 self.connected = False 1284 if err.errno in (errno.ENOTCONN, errno.EINVAL): 1285 self.close() 1286 else: 1287 self.handle_error() 1288 return 1289 else: 1290 self.log("FTP session opened (connect)") 1291 1292 # try to handle urgent data inline 1293 try: 1294 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_OOBINLINE, 1) 1295 except socket.error as err: 1296 debug("call: FTPHandler.__init__, err on SO_OOBINLINE %r" % err, 1297 self) 1298 1299 # disable Nagle algorithm for the control socket only, resulting 1300 # in significantly better performances 1301 if self.tcp_no_delay: 1302 try: 1303 self.socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) 1304 except socket.error as err: 1305 debug( 1306 "call: FTPHandler.__init__, err on TCP_NODELAY %r" % err, 1307 self) 1308 1309 # remove this instance from IOLoop's socket_map 1310 if not self.connected: 1311 self.close() 1312 return 1313 1314 if self.timeout: 1315 self._idler = self.ioloop.call_later( 1316 self.timeout, self.handle_timeout, _errback=self.handle_error) 1317 1318 def get_repr_info(self, as_str=False, extra_info={}): 1319 info = OrderedDict() 1320 info['id'] = id(self) 1321 info['addr'] = "%s:%s" % (self.remote_ip, self.remote_port) 1322 if _is_ssl_sock(self.socket): 1323 info['ssl'] = True 1324 if self.username: 1325 info['user'] = self.username 1326 # If threads are involved sometimes "self" may be None (?!?). 1327 dc = getattr(self, 'data_channel', None) 1328 if dc is not None: 1329 if _is_ssl_sock(dc.socket): 1330 info['ssl-data'] = True 1331 if dc.file_obj: 1332 if self.data_channel.receive: 1333 info['sending-file'] = dc.file_obj 1334 if dc.use_sendfile(): 1335 info['use-sendfile(2)'] = True 1336 else: 1337 info['receiving-file'] = dc.file_obj 1338 info['bytes-trans'] = dc.get_transmitted_bytes() 1339 info.update(extra_info) 1340 if as_str: 1341 return ', '.join(['%s=%r' % (k, v) for (k, v) in info.items()]) 1342 return info 1343 1344 def __repr__(self): 1345 return '<%s(%s)>' % (self.__class__.__name__, self.get_repr_info(True)) 1346 1347 __str__ = __repr__ 1348 1349 def handle(self): 1350 """Return a 220 'ready' response to the client over the command 1351 channel. 1352 """ 1353 self.on_connect() 1354 if not self._closed and not self._closing: 1355 if len(self.banner) <= 75: 1356 self.respond("220 %s" % str(self.banner)) 1357 else: 1358 self.push('220-%s\r\n' % str(self.banner)) 1359 self.respond('220 ') 1360 1361 def handle_max_cons(self): 1362 """Called when limit for maximum number of connections is reached.""" 1363 msg = "421 Too many connections. Service temporarily unavailable." 1364 self.respond_w_warning(msg) 1365 # If self.push is used, data could not be sent immediately in 1366 # which case a new "loop" will occur exposing us to the risk of 1367 # accepting new connections. Since this could cause asyncore to 1368 # run out of fds in case we're using select() on Windows we 1369 # immediately close the channel by using close() instead of 1370 # close_when_done(). If data has not been sent yet client will 1371 # be silently disconnected. 1372 self.close() 1373 1374 def handle_max_cons_per_ip(self): 1375 """Called when too many clients are connected from the same IP.""" 1376 msg = "421 Too many connections from the same IP address." 1377 self.respond_w_warning(msg) 1378 self.close_when_done() 1379 1380 def handle_timeout(self): 1381 """Called when client does not send any command within the time 1382 specified in <timeout> attribute.""" 1383 msg = "Control connection timed out." 1384 self.respond("421 " + msg, logfun=logger.info) 1385 self.close_when_done() 1386 1387 # --- asyncore / asynchat overridden methods 1388 1389 def readable(self): 1390 # Checking for self.connected seems to be necessary as per: 1391 # https://github.com/giampaolo/pyftpdlib/issues/188#c18 1392 # In contrast to DTPHandler, here we are not interested in 1393 # attempting to receive any further data from a closed socket. 1394 return self.connected and AsyncChat.readable(self) 1395 1396 def writable(self): 1397 return self.connected and AsyncChat.writable(self) 1398 1399 def collect_incoming_data(self, data): 1400 """Read incoming data and append to the input buffer.""" 1401 self._in_buffer.append(data) 1402 self._in_buffer_len += len(data) 1403 # Flush buffer if it gets too long (possible DoS attacks). 1404 # RFC-959 specifies that a 500 response could be given in 1405 # such cases 1406 buflimit = 2048 1407 if self._in_buffer_len > buflimit: 1408 self.respond_w_warning('500 Command too long.') 1409 self._in_buffer = [] 1410 self._in_buffer_len = 0 1411 1412 def decode(self, bytes): 1413 return bytes.decode('utf8', self.unicode_errors) 1414 1415 def found_terminator(self): 1416 r"""Called when the incoming data stream matches the \r\n 1417 terminator. 1418 """ 1419 if self._idler is not None and not self._idler.cancelled: 1420 self._idler.reset() 1421 1422 line = b''.join(self._in_buffer) 1423 try: 1424 line = self.decode(line) 1425 except UnicodeDecodeError: 1426 # By default we'll never get here as we replace errors 1427 # but user might want to override this behavior. 1428 # RFC-2640 doesn't mention what to do in this case so 1429 # we'll just return 501 (bad arg). 1430 return self.respond("501 Can't decode command.") 1431 1432 self._in_buffer = [] 1433 self._in_buffer_len = 0 1434 1435 cmd = line.split(' ')[0].upper() 1436 arg = line[len(cmd) + 1:] 1437 try: 1438 self.pre_process_command(line, cmd, arg) 1439 except UnicodeEncodeError: 1440 self.respond("501 can't decode path (server filesystem encoding " 1441 "is %s)" % sys.getfilesystemencoding()) 1442 1443 def pre_process_command(self, line, cmd, arg): 1444 kwargs = {} 1445 if cmd == "SITE" and arg: 1446 cmd = "SITE %s" % arg.split(' ')[0].upper() 1447 arg = line[len(cmd) + 1:] 1448 1449 if cmd != 'PASS': 1450 self.logline("<- %s" % line) 1451 else: 1452 self.logline("<- %s %s" % (line.split(' ')[0], '*' * 6)) 1453 1454 # Recognize those commands having a "special semantic". They 1455 # should be sent by following the RFC-959 procedure of sending 1456 # Telnet IP/Synch sequence (chr 242 and 255) as OOB data but 1457 # since many ftp clients don't do it correctly we check the 1458 # last 4 characters only. 1459 if cmd not in self.proto_cmds: 1460 if cmd[-4:] in ('ABOR', 'STAT', 'QUIT'): 1461 cmd = cmd[-4:] 1462 else: 1463 msg = 'Command "%s" not understood.' % cmd 1464 self.respond('500 ' + msg) 1465 if cmd: 1466 self.log_cmd(cmd, arg, 500, msg) 1467 return 1468 1469 if not arg and self.proto_cmds[cmd]['arg'] == True: # NOQA 1470 msg = "Syntax error: command needs an argument." 1471 self.respond("501 " + msg) 1472 self.log_cmd(cmd, "", 501, msg) 1473 return 1474 if arg and self.proto_cmds[cmd]['arg'] == False: # NOQA 1475 msg = "Syntax error: command does not accept arguments." 1476 self.respond("501 " + msg) 1477 self.log_cmd(cmd, arg, 501, msg) 1478 return 1479 1480 if not self.authenticated: 1481 if self.proto_cmds[cmd]['auth'] or (cmd == 'STAT' and arg): 1482 msg = "Log in with USER and PASS first." 1483 self.respond("530 " + msg) 1484 self.log_cmd(cmd, arg, 530, msg) 1485 else: 1486 # call the proper ftp_* method 1487 self.process_command(cmd, arg) 1488 return 1489 else: 1490 if (cmd == 'STAT') and not arg: 1491 self.ftp_STAT(u('')) 1492 return 1493 1494 # for file-system related commands check whether real path 1495 # destination is valid 1496 if self.proto_cmds[cmd]['perm'] and (cmd != 'STOU'): 1497 if cmd in ('CWD', 'XCWD'): 1498 arg = self.fs.ftp2fs(arg or u('/')) 1499 elif cmd in ('CDUP', 'XCUP'): 1500 arg = self.fs.ftp2fs(u('..')) 1501 elif cmd == 'LIST': 1502 if arg.lower() in ('-a', '-l', '-al', '-la'): 1503 arg = self.fs.ftp2fs(self.fs.cwd) 1504 else: 1505 arg = self.fs.ftp2fs(arg or self.fs.cwd) 1506 elif cmd == 'STAT': 1507 if glob.has_magic(arg): 1508 msg = 'Globbing not supported.' 1509 self.respond('550 ' + msg) 1510 self.log_cmd(cmd, arg, 550, msg) 1511 return 1512 arg = self.fs.ftp2fs(arg or self.fs.cwd) 1513 elif cmd == 'SITE CHMOD': 1514 if ' ' not in arg: 1515 msg = "Syntax error: command needs two arguments." 1516 self.respond("501 " + msg) 1517 self.log_cmd(cmd, "", 501, msg) 1518 return 1519 else: 1520 mode, arg = arg.split(' ', 1) 1521 arg = self.fs.ftp2fs(arg) 1522 kwargs = dict(mode=mode) 1523 elif cmd == 'MFMT': 1524 if ' ' not in arg: 1525 msg = "Syntax error: command needs two arguments." 1526 self.respond("501 " + msg) 1527 self.log_cmd(cmd, "", 501, msg) 1528 return 1529 else: 1530 timeval, arg = arg.split(' ', 1) 1531 arg = self.fs.ftp2fs(arg) 1532 kwargs = dict(timeval=timeval) 1533 1534 else: # LIST, NLST, MLSD, MLST 1535 arg = self.fs.ftp2fs(arg or self.fs.cwd) 1536 1537 if not self.fs.validpath(arg): 1538 line = self.fs.fs2ftp(arg) 1539 msg = '"%s" points to a path which is outside ' \ 1540 "the user's root directory" % line 1541 self.respond("550 %s." % msg) 1542 self.log_cmd(cmd, arg, 550, msg) 1543 return 1544 1545 # check permission 1546 perm = self.proto_cmds[cmd]['perm'] 1547 if perm is not None and cmd != 'STOU': 1548 if not self.authorizer.has_perm(self.username, perm, arg): 1549 msg = "Not enough privileges." 1550 self.respond("550 " + msg) 1551 self.log_cmd(cmd, arg, 550, msg) 1552 return 1553 1554 # call the proper ftp_* method 1555 self.process_command(cmd, arg, **kwargs) 1556 1557 def process_command(self, cmd, *args, **kwargs): 1558 """Process command by calling the corresponding ftp_* class 1559 method (e.g. for received command "MKD pathname", ftp_MKD() 1560 method is called with "pathname" as the argument). 1561 """ 1562 if self._closed: 1563 return 1564 self._last_response = "" 1565 method = getattr(self, 'ftp_' + cmd.replace(' ', '_')) 1566 method(*args, **kwargs) 1567 if self._last_response: 1568 code = int(self._last_response[:3]) 1569 resp = self._last_response[4:] 1570 self.log_cmd(cmd, args[0], code, resp) 1571 1572 def handle_error(self): 1573 try: 1574 self.log_exception(self) 1575 self.close() 1576 except Exception: 1577 logger.critical(traceback.format_exc()) 1578 1579 def handle_close(self): 1580 self.close() 1581 1582 def close(self): 1583 """Close the current channel disconnecting the client.""" 1584 debug("call: close()", inst=self) 1585 if not self._closed: 1586 AsyncChat.close(self) 1587 1588 self._shutdown_connecting_dtp() 1589 1590 if self.data_channel is not None: 1591 self.data_channel.close() 1592 del self.data_channel 1593 1594 if self._out_dtp_queue is not None: 1595 file = self._out_dtp_queue[2] 1596 if file is not None: 1597 file.close() 1598 if self._in_dtp_queue is not None: 1599 file = self._in_dtp_queue[0] 1600 if file is not None: 1601 file.close() 1602 1603 del self._out_dtp_queue 1604 del self._in_dtp_queue 1605 1606 if self._idler is not None and not self._idler.cancelled: 1607 self._idler.cancel() 1608 1609 # remove client IP address from ip map 1610 if self.remote_ip in self.server.ip_map: 1611 self.server.ip_map.remove(self.remote_ip) 1612 1613 if self.fs is not None: 1614 self.fs.cmd_channel = None 1615 self.fs = None 1616 self.log("FTP session closed (disconnect).") 1617 # Having self.remote_ip not set means that no connection 1618 # actually took place, hence we're not interested in 1619 # invoking the callback. 1620 if self.remote_ip: 1621 self.ioloop.call_later(0, self.on_disconnect, 1622 _errback=self.handle_error) 1623 1624 def _shutdown_connecting_dtp(self): 1625 """Close any ActiveDTP or PassiveDTP instance waiting to 1626 establish a connection (passive or active). 1627 """ 1628 if self._dtp_acceptor is not None: 1629 self._dtp_acceptor.close() 1630 self._dtp_acceptor = None 1631 if self._dtp_connector is not None: 1632 self._dtp_connector.close() 1633 self._dtp_connector = None 1634 1635 # --- public callbacks 1636 # Note: to run a time consuming task make sure to use a separate 1637 # process or thread (see FAQs). 1638 1639 def on_connect(self): 1640 """Called when client connects, *before* sending the initial 1641 220 reply. 1642 """ 1643 1644 def on_disconnect(self): 1645 """Called when connection is closed.""" 1646 1647 def on_login(self, username): 1648 """Called on user login.""" 1649 1650 def on_login_failed(self, username, password): 1651 """Called on failed login attempt. 1652 At this point client might have already been disconnected if it 1653 failed too many times. 1654 """ 1655 1656 def on_logout(self, username): 1657 """Called when user "cleanly" logs out due to QUIT or USER 1658 issued twice (re-login). This is not called if the connection 1659 is simply closed by client. 1660 """ 1661 1662 def on_file_sent(self, file): 1663 """Called every time a file has been succesfully sent. 1664 "file" is the absolute name of the file just being sent. 1665 """ 1666 1667 def on_file_received(self, file): 1668 """Called every time a file has been succesfully received. 1669 "file" is the absolute name of the file just being received. 1670 """ 1671 1672 def on_incomplete_file_sent(self, file): 1673 """Called every time a file has not been entirely sent. 1674 (e.g. ABOR during transfer or client disconnected). 1675 "file" is the absolute name of that file. 1676 """ 1677 1678 def on_incomplete_file_received(self, file): 1679 """Called every time a file has not been entirely received 1680 (e.g. ABOR during transfer or client disconnected). 1681 "file" is the absolute name of that file. 1682 """ 1683 1684 # --- internal callbacks 1685 1686 def _on_dtp_connection(self): 1687 """Called every time data channel connects, either active or 1688 passive. 1689 1690 Incoming and outgoing queues are checked for pending data. 1691 If outbound data is pending, it is pushed into the data channel. 1692 If awaiting inbound data, the data channel is enabled for 1693 receiving. 1694 """ 1695 # Close accepting DTP only. By closing ActiveDTP DTPHandler 1696 # would receive a closed socket object. 1697 # self._shutdown_connecting_dtp() 1698 if self._dtp_acceptor is not None: 1699 self._dtp_acceptor.close() 1700 self._dtp_acceptor = None 1701 1702 # stop the idle timer as long as the data transfer is not finished 1703 if self._idler is not None and not self._idler.cancelled: 1704 self._idler.cancel() 1705 1706 # check for data to send 1707 if self._out_dtp_queue is not None: 1708 data, isproducer, file, cmd = self._out_dtp_queue 1709 self._out_dtp_queue = None 1710 self.data_channel.cmd = cmd 1711 if file: 1712 self.data_channel.file_obj = file 1713 try: 1714 if not isproducer: 1715 self.data_channel.push(data) 1716 else: 1717 self.data_channel.push_with_producer(data) 1718 if self.data_channel is not None: 1719 self.data_channel.close_when_done() 1720 except Exception: 1721 # dealing with this exception is up to DTP (see bug #84) 1722 self.data_channel.handle_error() 1723 1724 # check for data to receive 1725 elif self._in_dtp_queue is not None: 1726 file, cmd = self._in_dtp_queue 1727 self.data_channel.file_obj = file 1728 self._in_dtp_queue = None 1729 self.data_channel.enable_receiving(self._current_type, cmd) 1730 1731 def _on_dtp_close(self): 1732 """Called every time the data channel is closed.""" 1733 self.data_channel = None 1734 if self._quit_pending: 1735 self.close() 1736 elif self.timeout: 1737 # data transfer finished, restart the idle timer 1738 if self._idler is not None and not self._idler.cancelled: 1739 self._idler.cancel() 1740 self._idler = self.ioloop.call_later( 1741 self.timeout, self.handle_timeout, _errback=self.handle_error) 1742 1743 # --- utility 1744 1745 def push(self, s): 1746 asynchat.async_chat.push(self, s.encode('utf8')) 1747 1748 def respond(self, resp, logfun=logger.debug): 1749 """Send a response to the client using the command channel.""" 1750 self._last_response = resp 1751 self.push(resp + '\r\n') 1752 if self._log_debug: 1753 self.logline('-> %s' % resp, logfun=logfun) 1754 else: 1755 self.log(resp[4:], logfun=logfun) 1756 1757 def respond_w_warning(self, resp): 1758 self.respond(resp, logfun=logger.warning) 1759 1760 def push_dtp_data(self, data, isproducer=False, file=None, cmd=None): 1761 """Pushes data into the data channel. 1762 1763 It is usually called for those commands requiring some data to 1764 be sent over the data channel (e.g. RETR). 1765 If data channel does not exist yet, it queues the data to send 1766 later; data will then be pushed into data channel when 1767 _on_dtp_connection() will be called. 1768 1769 - (str/classobj) data: the data to send which may be a string 1770 or a producer object). 1771 - (bool) isproducer: whether treat data as a producer. 1772 - (file) file: the file[-like] object to send (if any). 1773 """ 1774 if self.data_channel is not None: 1775 self.respond( 1776 "125 Data connection already open. Transfer starting.") 1777 if file: 1778 self.data_channel.file_obj = file 1779 try: 1780 if not isproducer: 1781 self.data_channel.push(data) 1782 else: 1783 self.data_channel.push_with_producer(data) 1784 if self.data_channel is not None: 1785 self.data_channel.cmd = cmd 1786 self.data_channel.close_when_done() 1787 except Exception: 1788 # dealing with this exception is up to DTP (see bug #84) 1789 self.data_channel.handle_error() 1790 else: 1791 self.respond( 1792 "150 File status okay. About to open data connection.") 1793 self._out_dtp_queue = (data, isproducer, file, cmd) 1794 1795 def flush_account(self): 1796 """Flush account information by clearing attributes that need 1797 to be reset on a REIN or new USER command. 1798 """ 1799 self._shutdown_connecting_dtp() 1800 # if there's a transfer in progress RFC-959 states we are 1801 # supposed to let it finish 1802 if self.data_channel is not None: 1803 if not self.data_channel.transfer_in_progress(): 1804 self.data_channel.close() 1805 self.data_channel = None 1806 1807 username = self.username 1808 if self.authenticated and username: 1809 self.on_logout(username) 1810 self.authenticated = False 1811 self.username = "" 1812 self.password = "" 1813 self.attempted_logins = 0 1814 self._current_type = 'a' 1815 self._restart_position = 0 1816 self._quit_pending = False 1817 self._in_dtp_queue = None 1818 self._rnfr = None 1819 self._out_dtp_queue = None 1820 1821 def run_as_current_user(self, function, *args, **kwargs): 1822 """Execute a function impersonating the current logged-in user.""" 1823 self.authorizer.impersonate_user(self.username, self.password) 1824 try: 1825 return function(*args, **kwargs) 1826 finally: 1827 self.authorizer.terminate_impersonation(self.username) 1828 1829 # --- logging wrappers 1830 1831 # this is defined earlier 1832 # log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]' 1833 1834 def log(self, msg, logfun=logger.info): 1835 """Log a message, including additional identifying session data.""" 1836 prefix = self.log_prefix % self.__dict__ 1837 logfun("%s %s" % (prefix, msg)) 1838 1839 def logline(self, msg, logfun=logger.debug): 1840 """Log a line including additional indentifying session data. 1841 By default this is disabled unless logging level == DEBUG. 1842 """ 1843 if self._log_debug: 1844 prefix = self.log_prefix % self.__dict__ 1845 logfun("%s %s" % (prefix, msg)) 1846 1847 def logerror(self, msg): 1848 """Log an error including additional indentifying session data.""" 1849 prefix = self.log_prefix % self.__dict__ 1850 logger.error("%s %s" % (prefix, msg)) 1851 1852 def log_exception(self, instance): 1853 """Log an unhandled exception. 'instance' is the instance 1854 where the exception was generated. 1855 """ 1856 logger.exception("unhandled exception in instance %r", instance) 1857 1858 # the list of commands which gets logged when logging level 1859 # is >= logging.INFO 1860 log_cmds_list = ["DELE", "RNFR", "RNTO", "MKD", "RMD", "CWD", 1861 "XMKD", "XRMD", "XCWD", 1862 "REIN", "SITE CHMOD", "MFMT"] 1863 1864 def log_cmd(self, cmd, arg, respcode, respstr): 1865 """Log commands and responses in a standardized format. 1866 This is disabled in case the logging level is set to DEBUG. 1867 1868 - (str) cmd: 1869 the command sent by client 1870 1871 - (str) arg: 1872 the command argument sent by client. 1873 For filesystem commands such as DELE, MKD, etc. this is 1874 already represented as an absolute real filesystem path 1875 like "/home/user/file.ext". 1876 1877 - (int) respcode: 1878 the response code as being sent by server. Response codes 1879 starting with 4xx or 5xx are returned if the command has 1880 been rejected for some reason. 1881 1882 - (str) respstr: 1883 the response string as being sent by server. 1884 1885 By default only DELE, RMD, RNTO, MKD, CWD, ABOR, REIN, SITE CHMOD 1886 commands are logged and the output is redirected to self.log 1887 method. 1888 1889 Can be overridden to provide alternate formats or to log 1890 further commands. 1891 """ 1892 if not self._log_debug and cmd in self.log_cmds_list: 1893 line = '%s %s' % (' '.join([cmd, arg]).strip(), respcode) 1894 if str(respcode)[0] in ('4', '5'): 1895 line += ' %r' % respstr 1896 self.log(line) 1897 1898 def log_transfer(self, cmd, filename, receive, completed, elapsed, bytes): 1899 """Log all file transfers in a standardized format. 1900 1901 - (str) cmd: 1902 the original command who caused the tranfer. 1903 1904 - (str) filename: 1905 the absolutized name of the file on disk. 1906 1907 - (bool) receive: 1908 True if the transfer was used for client uploading (STOR, 1909 STOU, APPE), False otherwise (RETR). 1910 1911 - (bool) completed: 1912 True if the file has been entirely sent, else False. 1913 1914 - (float) elapsed: 1915 transfer elapsed time in seconds. 1916 1917 - (int) bytes: 1918 number of bytes transmitted. 1919 """ 1920 line = '%s %s completed=%s bytes=%s seconds=%s' % \ 1921 (cmd, filename, completed and 1 or 0, bytes, elapsed) 1922 self.log(line) 1923 1924 # --- connection 1925 def _make_eport(self, ip, port): 1926 """Establish an active data channel with remote client which 1927 issued a PORT or EPRT command. 1928 """ 1929 # FTP bounce attacks protection: according to RFC-2577 it's 1930 # recommended to reject PORT if IP address specified in it 1931 # does not match client IP address. 1932 remote_ip = self.remote_ip 1933 if remote_ip.startswith('::ffff:'): 1934 # In this scenario, the server has an IPv6 socket, but 1935 # the remote client is using IPv4 and its address is 1936 # represented as an IPv4-mapped IPv6 address which 1937 # looks like this ::ffff:151.12.5.65, see: 1938 # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses 1939 # http://tools.ietf.org/html/rfc3493.html#section-3.7 1940 # We truncate the first bytes to make it look like a 1941 # common IPv4 address. 1942 remote_ip = remote_ip[7:] 1943 if not self.permit_foreign_addresses and ip != remote_ip: 1944 msg = "501 Rejected data connection to foreign address %s:%s." \ 1945 % (ip, port) 1946 self.respond_w_warning(msg) 1947 return 1948 1949 # ...another RFC-2577 recommendation is rejecting connections 1950 # to privileged ports (< 1024) for security reasons. 1951 if not self.permit_privileged_ports and port < 1024: 1952 msg = '501 PORT against the privileged port "%s" refused.' % port 1953 self.respond_w_warning(msg) 1954 return 1955 1956 # close establishing DTP instances, if any 1957 self._shutdown_connecting_dtp() 1958 1959 if self.data_channel is not None: 1960 self.data_channel.close() 1961 self.data_channel = None 1962 1963 # make sure we are not hitting the max connections limit 1964 if not self.server._accept_new_cons(): 1965 msg = "425 Too many connections. Can't open data channel." 1966 self.respond_w_warning(msg) 1967 return 1968 1969 # open data channel 1970 self._dtp_connector = self.active_dtp(ip, port, self) 1971 1972 def _make_epasv(self, extmode=False): 1973 """Initialize a passive data channel with remote client which 1974 issued a PASV or EPSV command. 1975 If extmode argument is True we assume that client issued EPSV in 1976 which case extended passive mode will be used (see RFC-2428). 1977 """ 1978 # close establishing DTP instances, if any 1979 self._shutdown_connecting_dtp() 1980 1981 # close established data connections, if any 1982 if self.data_channel is not None: 1983 self.data_channel.close() 1984 self.data_channel = None 1985 1986 # make sure we are not hitting the max connections limit 1987 if not self.server._accept_new_cons(): 1988 msg = "425 Too many connections. Can't open data channel." 1989 self.respond_w_warning(msg) 1990 return 1991 1992 # open data channel 1993 self._dtp_acceptor = self.passive_dtp(self, extmode) 1994 1995 def ftp_PORT(self, line): 1996 """Start an active data channel by using IPv4.""" 1997 if self._epsvall: 1998 self.respond("501 PORT not allowed after EPSV ALL.") 1999 return 2000 # Parse PORT request for getting IP and PORT. 2001 # Request comes in as: 2002 # > h1,h2,h3,h4,p1,p2 2003 # ...where the client's IP address is h1.h2.h3.h4 and the TCP 2004 # port number is (p1 * 256) + p2. 2005 try: 2006 addr = list(map(int, line.split(','))) 2007 if len(addr) != 6: 2008 raise ValueError 2009 for x in addr[:4]: 2010 if not 0 <= x <= 255: 2011 raise ValueError 2012 ip = '%d.%d.%d.%d' % tuple(addr[:4]) 2013 port = (addr[4] * 256) + addr[5] 2014 if not 0 <= port <= 65535: 2015 raise ValueError 2016 except (ValueError, OverflowError): 2017 self.respond("501 Invalid PORT format.") 2018 return 2019 self._make_eport(ip, port) 2020 2021 def ftp_EPRT(self, line): 2022 """Start an active data channel by choosing the network protocol 2023 to use (IPv4/IPv6) as defined in RFC-2428. 2024 """ 2025 if self._epsvall: 2026 self.respond("501 EPRT not allowed after EPSV ALL.") 2027 return 2028 # Parse EPRT request for getting protocol, IP and PORT. 2029 # Request comes in as: 2030 # <d>proto<d>ip<d>port<d> 2031 # ...where <d> is an arbitrary delimiter character (usually "|") and 2032 # <proto> is the network protocol to use (1 for IPv4, 2 for IPv6). 2033 try: 2034 af, ip, port = line.split(line[0])[1:-1] 2035 port = int(port) 2036 if not 0 <= port <= 65535: 2037 raise ValueError 2038 except (ValueError, IndexError, OverflowError): 2039 self.respond("501 Invalid EPRT format.") 2040 return 2041 2042 if af == "1": 2043 # test if AF_INET6 and IPV6_V6ONLY 2044 if (self.socket.family == socket.AF_INET6 and not 2045 SUPPORTS_HYBRID_IPV6): 2046 self.respond('522 Network protocol not supported (use 2).') 2047 else: 2048 try: 2049 octs = list(map(int, ip.split('.'))) 2050 if len(octs) != 4: 2051 raise ValueError 2052 for x in octs: 2053 if not 0 <= x <= 255: 2054 raise ValueError 2055 except (ValueError, OverflowError): 2056 self.respond("501 Invalid EPRT format.") 2057 else: 2058 self._make_eport(ip, port) 2059 elif af == "2": 2060 if self.socket.family == socket.AF_INET: 2061 self.respond('522 Network protocol not supported (use 1).') 2062 else: 2063 self._make_eport(ip, port) 2064 else: 2065 if self.socket.family == socket.AF_INET: 2066 self.respond('501 Unknown network protocol (use 1).') 2067 else: 2068 self.respond('501 Unknown network protocol (use 2).') 2069 2070 def ftp_PASV(self, line): 2071 """Start a passive data channel by using IPv4.""" 2072 if self._epsvall: 2073 self.respond("501 PASV not allowed after EPSV ALL.") 2074 return 2075 self._make_epasv(extmode=False) 2076 2077 def ftp_EPSV(self, line): 2078 """Start a passive data channel by using IPv4 or IPv6 as defined 2079 in RFC-2428. 2080 """ 2081 # RFC-2428 specifies that if an optional parameter is given, 2082 # we have to determine the address family from that otherwise 2083 # use the same address family used on the control connection. 2084 # In such a scenario a client may use IPv4 on the control channel 2085 # and choose to use IPv6 for the data channel. 2086 # But how could we use IPv6 on the data channel without knowing 2087 # which IPv6 address to use for binding the socket? 2088 # Unfortunately RFC-2428 does not provide satisfing information 2089 # on how to do that. The assumption is that we don't have any way 2090 # to know wich address to use, hence we just use the same address 2091 # family used on the control connection. 2092 if not line: 2093 self._make_epasv(extmode=True) 2094 # IPv4 2095 elif line == "1": 2096 if self.socket.family != socket.AF_INET: 2097 self.respond('522 Network protocol not supported (use 2).') 2098 else: 2099 self._make_epasv(extmode=True) 2100 # IPv6 2101 elif line == "2": 2102 if self.socket.family == socket.AF_INET: 2103 self.respond('522 Network protocol not supported (use 1).') 2104 else: 2105 self._make_epasv(extmode=True) 2106 elif line.lower() == 'all': 2107 self._epsvall = True 2108 self.respond( 2109 '220 Other commands other than EPSV are now disabled.') 2110 else: 2111 if self.socket.family == socket.AF_INET: 2112 self.respond('501 Unknown network protocol (use 1).') 2113 else: 2114 self.respond('501 Unknown network protocol (use 2).') 2115 2116 def ftp_QUIT(self, line): 2117 """Quit the current session disconnecting the client.""" 2118 if self.authenticated: 2119 msg_quit = self.authorizer.get_msg_quit(self.username) 2120 else: 2121 msg_quit = "Goodbye." 2122 if len(msg_quit) <= 75: 2123 self.respond("221 %s" % msg_quit) 2124 else: 2125 self.push("221-%s\r\n" % msg_quit) 2126 self.respond("221 ") 2127 2128 # From RFC-959: 2129 # If file transfer is in progress, the connection must remain 2130 # open for result response and the server will then close it. 2131 # We also stop responding to any further command. 2132 if self.data_channel: 2133 self._quit_pending = True 2134 self.del_channel() 2135 else: 2136 self._shutdown_connecting_dtp() 2137 self.close_when_done() 2138 if self.authenticated and self.username: 2139 self.on_logout(self.username) 2140 2141 # --- data transferring 2142 2143 def ftp_LIST(self, path): 2144 """Return a list of files in the specified directory to the 2145 client. 2146 On success return the directory path, else None. 2147 """ 2148 # - If no argument, fall back on cwd as default. 2149 # - Some older FTP clients erroneously issue /bin/ls-like LIST 2150 # formats in which case we fall back on cwd as default. 2151 try: 2152 isdir = self.fs.isdir(path) 2153 if isdir: 2154 listing = self.run_as_current_user(self.fs.listdir, path) 2155 if isinstance(listing, list): 2156 try: 2157 # RFC 959 recommends the listing to be sorted. 2158 listing.sort() 2159 except UnicodeDecodeError: 2160 # (Python 2 only) might happen on filesystem not 2161 # supporting UTF8 meaning os.listdir() returned a list 2162 # of mixed bytes and unicode strings: 2163 # http://goo.gl/6DLHD 2164 # http://bugs.python.org/issue683592 2165 pass 2166 iterator = self.fs.format_list(path, listing) 2167 else: 2168 basedir, filename = os.path.split(path) 2169 self.fs.lstat(path) # raise exc in case of problems 2170 iterator = self.fs.format_list(basedir, [filename]) 2171 except (OSError, FilesystemError) as err: 2172 why = _strerror(err) 2173 self.respond('550 %s.' % why) 2174 else: 2175 producer = BufferedIteratorProducer(iterator) 2176 self.push_dtp_data(producer, isproducer=True, cmd="LIST") 2177 return path 2178 2179 def ftp_NLST(self, path): 2180 """Return a list of files in the specified directory in a 2181 compact form to the client. 2182 On success return the directory path, else None. 2183 """ 2184 try: 2185 if self.fs.isdir(path): 2186 listing = list(self.run_as_current_user(self.fs.listdir, path)) 2187 else: 2188 # if path is a file we just list its name 2189 self.fs.lstat(path) # raise exc in case of problems 2190 listing = [os.path.basename(path)] 2191 except (OSError, FilesystemError) as err: 2192 self.respond('550 %s.' % _strerror(err)) 2193 else: 2194 data = '' 2195 if listing: 2196 try: 2197 listing.sort() 2198 except UnicodeDecodeError: 2199 # (Python 2 only) might happen on filesystem not 2200 # supporting UTF8 meaning os.listdir() returned a list 2201 # of mixed bytes and unicode strings: 2202 # http://goo.gl/6DLHD 2203 # http://bugs.python.org/issue683592 2204 ls = [] 2205 for x in listing: 2206 if not isinstance(x, unicode): 2207 x = unicode(x, 'utf8') 2208 ls.append(x) 2209 listing = sorted(ls) 2210 data = '\r\n'.join(listing) + '\r\n' 2211 data = data.encode('utf8', self.unicode_errors) 2212 self.push_dtp_data(data, cmd="NLST") 2213 return path 2214 2215 # --- MLST and MLSD commands 2216 2217 # The MLST and MLSD commands are intended to standardize the file and 2218 # directory information returned by the server-FTP process. These 2219 # commands differ from the LIST command in that the format of the 2220 # replies is strictly defined although extensible. 2221 2222 def ftp_MLST(self, path): 2223 """Return information about a pathname in a machine-processable 2224 form as defined in RFC-3659. 2225 On success return the path just listed, else None. 2226 """ 2227 line = self.fs.fs2ftp(path) 2228 basedir, basename = os.path.split(path) 2229 perms = self.authorizer.get_perms(self.username) 2230 try: 2231 iterator = self.run_as_current_user( 2232 self.fs.format_mlsx, basedir, [basename], perms, 2233 self._current_facts, ignore_err=False) 2234 data = b''.join(iterator) 2235 except (OSError, FilesystemError) as err: 2236 self.respond('550 %s.' % _strerror(err)) 2237 else: 2238 data = data.decode('utf8', self.unicode_errors) 2239 # since TVFS is supported (see RFC-3659 chapter 6), a fully 2240 # qualified pathname should be returned 2241 data = data.split(' ')[0] + ' %s\r\n' % line 2242 # response is expected on the command channel 2243 self.push('250-Listing "%s":\r\n' % line) 2244 # the fact set must be preceded by a space 2245 self.push(' ' + data) 2246 self.respond('250 End MLST.') 2247 return path 2248 2249 def ftp_MLSD(self, path): 2250 """Return contents of a directory in a machine-processable form 2251 as defined in RFC-3659. 2252 On success return the path just listed, else None. 2253 """ 2254 # RFC-3659 requires 501 response code if path is not a directory 2255 if not self.fs.isdir(path): 2256 self.respond("501 No such directory.") 2257 return 2258 try: 2259 listing = self.run_as_current_user(self.fs.listdir, path) 2260 except (OSError, FilesystemError) as err: 2261 why = _strerror(err) 2262 self.respond('550 %s.' % why) 2263 else: 2264 perms = self.authorizer.get_perms(self.username) 2265 iterator = self.fs.format_mlsx(path, listing, perms, 2266 self._current_facts) 2267 producer = BufferedIteratorProducer(iterator) 2268 self.push_dtp_data(producer, isproducer=True, cmd="MLSD") 2269 return path 2270 2271 def ftp_RETR(self, file): 2272 """Retrieve the specified file (transfer from the server to the 2273 client). On success return the file path else None. 2274 """ 2275 rest_pos = self._restart_position 2276 self._restart_position = 0 2277 try: 2278 fd = self.run_as_current_user(self.fs.open, file, 'rb') 2279 except (EnvironmentError, FilesystemError) as err: 2280 why = _strerror(err) 2281 self.respond('550 %s.' % why) 2282 return 2283 2284 try: 2285 if rest_pos: 2286 # Make sure that the requested offset is valid (within the 2287 # size of the file being resumed). 2288 # According to RFC-1123 a 554 reply may result in case that 2289 # the existing file cannot be repositioned as specified in 2290 # the REST. 2291 ok = 0 2292 try: 2293 if rest_pos > self.fs.getsize(file): 2294 raise ValueError 2295 fd.seek(rest_pos) 2296 ok = 1 2297 except ValueError: 2298 why = "Invalid REST parameter" 2299 except (EnvironmentError, FilesystemError) as err: 2300 why = _strerror(err) 2301 if not ok: 2302 fd.close() 2303 self.respond('554 %s' % why) 2304 return 2305 producer = FileProducer(fd, self._current_type) 2306 self.push_dtp_data(producer, isproducer=True, file=fd, cmd="RETR") 2307 return file 2308 except Exception: 2309 fd.close() 2310 raise 2311 2312 def ftp_STOR(self, file, mode='w'): 2313 """Store a file (transfer from the client to the server). 2314 On success return the file path, else None. 2315 """ 2316 # A resume could occur in case of APPE or REST commands. 2317 # In that case we have to open file object in different ways: 2318 # STOR: mode = 'w' 2319 # APPE: mode = 'a' 2320 # REST: mode = 'r+' (to permit seeking on file object) 2321 if 'a' in mode: 2322 cmd = 'APPE' 2323 else: 2324 cmd = 'STOR' 2325 rest_pos = self._restart_position 2326 self._restart_position = 0 2327 if rest_pos: 2328 mode = 'r+' 2329 try: 2330 fd = self.run_as_current_user(self.fs.open, file, mode + 'b') 2331 except (EnvironmentError, FilesystemError) as err: 2332 why = _strerror(err) 2333 self.respond('550 %s.' % why) 2334 return 2335 2336 try: 2337 if rest_pos: 2338 # Make sure that the requested offset is valid (within the 2339 # size of the file being resumed). 2340 # According to RFC-1123 a 554 reply may result in case 2341 # that the existing file cannot be repositioned as 2342 # specified in the REST. 2343 ok = 0 2344 try: 2345 if rest_pos > self.fs.getsize(file): 2346 raise ValueError 2347 fd.seek(rest_pos) 2348 ok = 1 2349 except ValueError: 2350 why = "Invalid REST parameter" 2351 except (EnvironmentError, FilesystemError) as err: 2352 why = _strerror(err) 2353 if not ok: 2354 fd.close() 2355 self.respond('554 %s' % why) 2356 return 2357 2358 if self.data_channel is not None: 2359 resp = "Data connection already open. Transfer starting." 2360 self.respond("125 " + resp) 2361 self.data_channel.file_obj = fd 2362 self.data_channel.enable_receiving(self._current_type, cmd) 2363 else: 2364 resp = "File status okay. About to open data connection." 2365 self.respond("150 " + resp) 2366 self._in_dtp_queue = (fd, cmd) 2367 return file 2368 except Exception: 2369 fd.close() 2370 raise 2371 2372 def ftp_STOU(self, line): 2373 """Store a file on the server with a unique name. 2374 On success return the file path, else None. 2375 """ 2376 # Note 1: RFC-959 prohibited STOU parameters, but this 2377 # prohibition is obsolete. 2378 # Note 2: 250 response wanted by RFC-959 has been declared 2379 # incorrect in RFC-1123 that wants 125/150 instead. 2380 # Note 3: RFC-1123 also provided an exact output format 2381 # defined to be as follow: 2382 # > 125 FILE: pppp 2383 # ...where pppp represents the unique path name of the 2384 # file that will be written. 2385 2386 # watch for STOU preceded by REST, which makes no sense. 2387 if self._restart_position: 2388 self.respond("450 Can't STOU while REST request is pending.") 2389 return 2390 2391 if line: 2392 basedir, prefix = os.path.split(self.fs.ftp2fs(line)) 2393 prefix = prefix + '.' 2394 else: 2395 basedir = self.fs.ftp2fs(self.fs.cwd) 2396 prefix = 'ftpd.' 2397 try: 2398 fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix, 2399 dir=basedir) 2400 except (EnvironmentError, FilesystemError) as err: 2401 # likely, we hit the max number of retries to find out a 2402 # file with a unique name 2403 if getattr(err, "errno", -1) == errno.EEXIST: 2404 why = 'No usable unique file name found' 2405 # something else happened 2406 else: 2407 why = _strerror(err) 2408 self.respond("450 %s." % why) 2409 return 2410 2411 try: 2412 if not self.authorizer.has_perm(self.username, 'w', fd.name): 2413 try: 2414 fd.close() 2415 self.run_as_current_user(self.fs.remove, fd.name) 2416 except (OSError, FilesystemError): 2417 pass 2418 self.respond("550 Not enough privileges.") 2419 return 2420 2421 # now just acts like STOR except that restarting isn't allowed 2422 filename = os.path.basename(fd.name) 2423 if self.data_channel is not None: 2424 self.respond("125 FILE: %s" % filename) 2425 self.data_channel.file_obj = fd 2426 self.data_channel.enable_receiving(self._current_type, "STOU") 2427 else: 2428 self.respond("150 FILE: %s" % filename) 2429 self._in_dtp_queue = (fd, "STOU") 2430 return filename 2431 except Exception: 2432 fd.close() 2433 raise 2434 2435 def ftp_APPE(self, file): 2436 """Append data to an existing file on the server. 2437 On success return the file path, else None. 2438 """ 2439 # watch for APPE preceded by REST, which makes no sense. 2440 if self._restart_position: 2441 self.respond("450 Can't APPE while REST request is pending.") 2442 else: 2443 return self.ftp_STOR(file, mode='a') 2444 2445 def ftp_REST(self, line): 2446 """Restart a file transfer from a previous mark.""" 2447 if self._current_type == 'a': 2448 self.respond('501 Resuming transfers not allowed in ASCII mode.') 2449 return 2450 try: 2451 marker = int(line) 2452 if marker < 0: 2453 raise ValueError 2454 except (ValueError, OverflowError): 2455 self.respond("501 Invalid parameter.") 2456 else: 2457 self.respond("350 Restarting at position %s." % marker) 2458 self._restart_position = marker 2459 2460 def ftp_ABOR(self, line): 2461 """Abort the current data transfer.""" 2462 # ABOR received while no data channel exists 2463 if (self._dtp_acceptor is None and 2464 self._dtp_connector is None and 2465 self.data_channel is None): 2466 self.respond("225 No transfer to abort.") 2467 return 2468 else: 2469 # a PASV or PORT was received but connection wasn't made yet 2470 if (self._dtp_acceptor is not None or 2471 self._dtp_connector is not None): 2472 self._shutdown_connecting_dtp() 2473 resp = "225 ABOR command successful; data channel closed." 2474 2475 # If a data transfer is in progress the server must first 2476 # close the data connection, returning a 426 reply to 2477 # indicate that the transfer terminated abnormally, then it 2478 # must send a 226 reply, indicating that the abort command 2479 # was successfully processed. 2480 # If no data has been transmitted we just respond with 225 2481 # indicating that no transfer was in progress. 2482 if self.data_channel is not None: 2483 if self.data_channel.transfer_in_progress(): 2484 self.data_channel.close() 2485 self.data_channel = None 2486 self.respond("426 Transfer aborted via ABOR.", 2487 logfun=logger.info) 2488 resp = "226 ABOR command successful." 2489 else: 2490 self.data_channel.close() 2491 self.data_channel = None 2492 resp = "225 ABOR command successful; data channel closed." 2493 self.respond(resp) 2494 2495 # --- authentication 2496 def ftp_USER(self, line): 2497 """Set the username for the current session.""" 2498 # RFC-959 specifies a 530 response to the USER command if the 2499 # username is not valid. If the username is valid is required 2500 # ftpd returns a 331 response instead. In order to prevent a 2501 # malicious client from determining valid usernames on a server, 2502 # it is suggested by RFC-2577 that a server always return 331 to 2503 # the USER command and then reject the combination of username 2504 # and password for an invalid username when PASS is provided later. 2505 if not self.authenticated: 2506 self.respond('331 Username ok, send password.') 2507 else: 2508 # a new USER command could be entered at any point in order 2509 # to change the access control flushing any user, password, 2510 # and account information already supplied and beginning the 2511 # login sequence again. 2512 self.flush_account() 2513 msg = 'Previous account information was flushed' 2514 self.respond('331 %s, send password.' % msg, logfun=logger.info) 2515 self.username = line 2516 2517 def handle_auth_failed(self, msg, password): 2518 def callback(username, password, msg): 2519 self.add_channel() 2520 if hasattr(self, '_closed') and not self._closed: 2521 self.attempted_logins += 1 2522 if self.attempted_logins >= self.max_login_attempts: 2523 msg += " Disconnecting." 2524 self.respond("530 " + msg) 2525 self.close_when_done() 2526 else: 2527 self.respond("530 " + msg) 2528 self.log("USER '%s' failed login." % username) 2529 self.on_login_failed(username, password) 2530 2531 self.del_channel() 2532 if not msg: 2533 if self.username == 'anonymous': 2534 msg = "Anonymous access not allowed." 2535 else: 2536 msg = "Authentication failed." 2537 else: 2538 # response string should be capitalized as per RFC-959 2539 msg = msg.capitalize() 2540 self.ioloop.call_later(self.auth_failed_timeout, callback, 2541 self.username, password, msg, 2542 _errback=self.handle_error) 2543 self.username = "" 2544 2545 def handle_auth_success(self, home, password, msg_login): 2546 if not isinstance(home, unicode): 2547 if PY3: 2548 raise TypeError('type(home) != text') 2549 else: 2550 warnings.warn( 2551 '%s.get_home_dir returned a non-unicode string; now ' 2552 'casting to unicode' % ( 2553 self.authorizer.__class__.__name__), 2554 RuntimeWarning) 2555 home = home.decode('utf8') 2556 2557 if len(msg_login) <= 75: 2558 self.respond('230 %s' % msg_login) 2559 else: 2560 self.push("230-%s\r\n" % msg_login) 2561 self.respond("230 ") 2562 self.log("USER '%s' logged in." % self.username) 2563 self.authenticated = True 2564 self.password = password 2565 self.attempted_logins = 0 2566 2567 self.fs = self.abstracted_fs(home, self) 2568 self.on_login(self.username) 2569 2570 def ftp_PASS(self, line): 2571 """Check username's password against the authorizer.""" 2572 if self.authenticated: 2573 self.respond("503 User already authenticated.") 2574 return 2575 if not self.username: 2576 self.respond("503 Login with USER first.") 2577 return 2578 2579 try: 2580 self.authorizer.validate_authentication(self.username, line, self) 2581 home = self.authorizer.get_home_dir(self.username) 2582 msg_login = self.authorizer.get_msg_login(self.username) 2583 except (AuthenticationFailed, AuthorizerError) as err: 2584 self.handle_auth_failed(str(err), line) 2585 else: 2586 self.handle_auth_success(home, line, msg_login) 2587 2588 def ftp_REIN(self, line): 2589 """Reinitialize user's current session.""" 2590 # From RFC-959: 2591 # REIN command terminates a USER, flushing all I/O and account 2592 # information, except to allow any transfer in progress to be 2593 # completed. All parameters are reset to the default settings 2594 # and the control connection is left open. This is identical 2595 # to the state in which a user finds himself immediately after 2596 # the control connection is opened. 2597 self.flush_account() 2598 # Note: RFC-959 erroneously mention "220" as the correct response 2599 # code to be given in this case, but this is wrong... 2600 self.respond("230 Ready for new user.") 2601 2602 # --- filesystem operations 2603 def ftp_PWD(self, line): 2604 """Return the name of the current working directory to the client.""" 2605 # The 257 response is supposed to include the directory 2606 # name and in case it contains embedded double-quotes 2607 # they must be doubled (see RFC-959, chapter 7, appendix 2). 2608 cwd = self.fs.cwd 2609 assert isinstance(cwd, unicode), cwd 2610 self.respond('257 "%s" is the current directory.' 2611 % cwd.replace('"', '""')) 2612 2613 def ftp_CWD(self, path): 2614 """Change the current working directory. 2615 On success return the new directory path, else None. 2616 """ 2617 # Temporarily join the specified directory to see if we have 2618 # permissions to do so, then get back to original process's 2619 # current working directory. 2620 # Note that if for some reason os.getcwd() gets removed after 2621 # the process is started we'll get into troubles (os.getcwd() 2622 # will fail with ENOENT) but we can't do anything about that 2623 # except logging an error. 2624 init_cwd = getcwdu() 2625 try: 2626 self.run_as_current_user(self.fs.chdir, path) 2627 except (OSError, FilesystemError) as err: 2628 why = _strerror(err) 2629 self.respond('550 %s.' % why) 2630 else: 2631 cwd = self.fs.cwd 2632 assert isinstance(cwd, unicode), cwd 2633 self.respond('250 "%s" is the current directory.' % cwd) 2634 if getcwdu() != init_cwd: 2635 os.chdir(init_cwd) 2636 return path 2637 2638 def ftp_CDUP(self, path): 2639 """Change into the parent directory. 2640 On success return the new directory, else None. 2641 """ 2642 # Note: RFC-959 says that code 200 is required but it also says 2643 # that CDUP uses the same codes as CWD. 2644 return self.ftp_CWD(path) 2645 2646 def ftp_SIZE(self, path): 2647 """Return size of file in a format suitable for using with 2648 RESTart as defined in RFC-3659.""" 2649 2650 # Implementation note: properly handling the SIZE command when 2651 # TYPE ASCII is used would require to scan the entire file to 2652 # perform the ASCII translation logic 2653 # (file.read().replace(os.linesep, '\r\n')) and then calculating 2654 # the len of such data which may be different than the actual 2655 # size of the file on the server. Considering that calculating 2656 # such result could be very resource-intensive and also dangerous 2657 # (DoS) we reject SIZE when the current TYPE is ASCII. 2658 # However, clients in general should not be resuming downloads 2659 # in ASCII mode. Resuming downloads in binary mode is the 2660 # recommended way as specified in RFC-3659. 2661 2662 line = self.fs.fs2ftp(path) 2663 if self._current_type == 'a': 2664 why = "SIZE not allowed in ASCII mode" 2665 self.respond("550 %s." % why) 2666 return 2667 if not self.fs.isfile(self.fs.realpath(path)): 2668 why = "%s is not retrievable" % line 2669 self.respond("550 %s." % why) 2670 return 2671 try: 2672 size = self.run_as_current_user(self.fs.getsize, path) 2673 except (OSError, FilesystemError) as err: 2674 why = _strerror(err) 2675 self.respond('550 %s.' % why) 2676 else: 2677 self.respond("213 %s" % size) 2678 2679 def ftp_MDTM(self, path): 2680 """Return last modification time of file to the client as an ISO 2681 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659. 2682 On success return the file path, else None. 2683 """ 2684 line = self.fs.fs2ftp(path) 2685 if not self.fs.isfile(self.fs.realpath(path)): 2686 self.respond("550 %s is not retrievable" % line) 2687 return 2688 if self.use_gmt_times: 2689 timefunc = time.gmtime 2690 else: 2691 timefunc = time.localtime 2692 try: 2693 secs = self.run_as_current_user(self.fs.getmtime, path) 2694 lmt = time.strftime("%Y%m%d%H%M%S", timefunc(secs)) 2695 except (ValueError, OSError, FilesystemError) as err: 2696 if isinstance(err, ValueError): 2697 # It could happen if file's last modification time 2698 # happens to be too old (prior to year 1900) 2699 why = "Can't determine file's last modification time" 2700 else: 2701 why = _strerror(err) 2702 self.respond('550 %s.' % why) 2703 else: 2704 self.respond("213 %s" % lmt) 2705 return path 2706 2707 def ftp_MFMT(self, path, timeval): 2708 """ Sets the last modification time of file to timeval 2709 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659. 2710 On success return the modified time and file path, else None. 2711 """ 2712 # Note: the MFMT command is not a formal RFC command 2713 # but stated in the following MEMO: 2714 # https://tools.ietf.org/html/draft-somers-ftp-mfxx-04 2715 # this is implemented to assist with file synchronization 2716 2717 line = self.fs.fs2ftp(path) 2718 2719 if len(timeval) != len("YYYYMMDDHHMMSS"): 2720 why = "Invalid time format; expected: YYYYMMDDHHMMSS" 2721 self.respond('550 %s.' % why) 2722 return 2723 if not self.fs.isfile(self.fs.realpath(path)): 2724 self.respond("550 %s is not retrievable" % line) 2725 return 2726 if self.use_gmt_times: 2727 timefunc = time.gmtime 2728 else: 2729 timefunc = time.localtime 2730 try: 2731 # convert timeval string to epoch seconds 2732 epoch = datetime.utcfromtimestamp(0) 2733 timeval_datetime_obj = datetime.strptime(timeval, '%Y%m%d%H%M%S') 2734 timeval_secs = (timeval_datetime_obj - epoch).total_seconds() 2735 except ValueError: 2736 why = "Invalid time format; expected: YYYYMMDDHHMMSS" 2737 self.respond('550 %s.' % why) 2738 return 2739 try: 2740 # Modify Time 2741 self.run_as_current_user(self.fs.utime, path, timeval_secs) 2742 # Fetch Time 2743 secs = self.run_as_current_user(self.fs.getmtime, path) 2744 lmt = time.strftime("%Y%m%d%H%M%S", timefunc(secs)) 2745 except (ValueError, OSError, FilesystemError) as err: 2746 if isinstance(err, ValueError): 2747 # It could happen if file's last modification time 2748 # happens to be too old (prior to year 1900) 2749 why = "Can't determine file's last modification time" 2750 else: 2751 why = _strerror(err) 2752 self.respond('550 %s.' % why) 2753 else: 2754 self.respond("213 Modify=%s; %s." % (lmt, line)) 2755 return (lmt, path) 2756 2757 def ftp_MKD(self, path): 2758 """Create the specified directory. 2759 On success return the directory path, else None. 2760 """ 2761 line = self.fs.fs2ftp(path) 2762 try: 2763 self.run_as_current_user(self.fs.mkdir, path) 2764 except (OSError, FilesystemError) as err: 2765 why = _strerror(err) 2766 self.respond('550 %s.' % why) 2767 else: 2768 # The 257 response is supposed to include the directory 2769 # name and in case it contains embedded double-quotes 2770 # they must be doubled (see RFC-959, chapter 7, appendix 2). 2771 self.respond( 2772 '257 "%s" directory created.' % line.replace('"', '""')) 2773 return path 2774 2775 def ftp_RMD(self, path): 2776 """Remove the specified directory. 2777 On success return the directory path, else None. 2778 """ 2779 if self.fs.realpath(path) == self.fs.realpath(self.fs.root): 2780 msg = "Can't remove root directory." 2781 self.respond("550 %s" % msg) 2782 return 2783 try: 2784 self.run_as_current_user(self.fs.rmdir, path) 2785 except (OSError, FilesystemError) as err: 2786 why = _strerror(err) 2787 self.respond('550 %s.' % why) 2788 else: 2789 self.respond("250 Directory removed.") 2790 2791 def ftp_DELE(self, path): 2792 """Delete the specified file. 2793 On success return the file path, else None. 2794 """ 2795 try: 2796 self.run_as_current_user(self.fs.remove, path) 2797 except (OSError, FilesystemError) as err: 2798 why = _strerror(err) 2799 self.respond('550 %s.' % why) 2800 else: 2801 self.respond("250 File removed.") 2802 return path 2803 2804 def ftp_RNFR(self, path): 2805 """Rename the specified (only the source name is specified 2806 here, see RNTO command)""" 2807 if not self.fs.lexists(path): 2808 self.respond("550 No such file or directory.") 2809 elif self.fs.realpath(path) == self.fs.realpath(self.fs.root): 2810 self.respond("550 Can't rename home directory.") 2811 else: 2812 self._rnfr = path 2813 self.respond("350 Ready for destination name.") 2814 2815 def ftp_RNTO(self, path): 2816 """Rename file (destination name only, source is specified with 2817 RNFR). 2818 On success return a (source_path, destination_path) tuple. 2819 """ 2820 if not self._rnfr: 2821 self.respond("503 Bad sequence of commands: use RNFR first.") 2822 return 2823 src = self._rnfr 2824 self._rnfr = None 2825 try: 2826 self.run_as_current_user(self.fs.rename, src, path) 2827 except (OSError, FilesystemError) as err: 2828 why = _strerror(err) 2829 self.respond('550 %s.' % why) 2830 else: 2831 self.respond("250 Renaming ok.") 2832 return (src, path) 2833 2834 # --- others 2835 def ftp_TYPE(self, line): 2836 """Set current type data type to binary/ascii""" 2837 type = line.upper().replace(' ', '') 2838 if type in ("A", "L7"): 2839 self.respond("200 Type set to: ASCII.") 2840 self._current_type = 'a' 2841 elif type in ("I", "L8"): 2842 self.respond("200 Type set to: Binary.") 2843 self._current_type = 'i' 2844 else: 2845 self.respond('504 Unsupported type "%s".' % line) 2846 2847 def ftp_STRU(self, line): 2848 """Set file structure ("F" is the only one supported (noop)).""" 2849 stru = line.upper() 2850 if stru == 'F': 2851 self.respond('200 File transfer structure set to: F.') 2852 elif stru in ('P', 'R'): 2853 # R is required in minimum implementations by RFC-959, 5.1. 2854 # RFC-1123, 4.1.2.13, amends this to only apply to servers 2855 # whose file systems support record structures, but also 2856 # suggests that such a server "may still accept files with 2857 # STRU R, recording the byte stream literally". 2858 # Should we accept R but with no operational difference from 2859 # F? proftpd and wu-ftpd don't accept STRU R. We just do 2860 # the same. 2861 # 2862 # RFC-1123 recommends against implementing P. 2863 self.respond('504 Unimplemented STRU type.') 2864 else: 2865 self.respond('501 Unrecognized STRU type.') 2866 2867 def ftp_MODE(self, line): 2868 """Set data transfer mode ("S" is the only one supported (noop)).""" 2869 mode = line.upper() 2870 if mode == 'S': 2871 self.respond('200 Transfer mode set to: S') 2872 elif mode in ('B', 'C'): 2873 self.respond('504 Unimplemented MODE type.') 2874 else: 2875 self.respond('501 Unrecognized MODE type.') 2876 2877 def ftp_STAT(self, path): 2878 """Return statistics about current ftp session. If an argument 2879 is provided return directory listing over command channel. 2880 2881 Implementation note: 2882 2883 RFC-959 does not explicitly mention globbing but many FTP 2884 servers do support it as a measure of convenience for FTP 2885 clients and users. 2886 2887 In order to search for and match the given globbing expression, 2888 the code has to search (possibly) many directories, examine 2889 each contained filename, and build a list of matching files in 2890 memory. Since this operation can be quite intensive, both CPU- 2891 and memory-wise, we do not support globbing. 2892 """ 2893 # return STATus information about ftpd 2894 if not path: 2895 s = [] 2896 s.append('Connected to: %s:%s' % self.socket.getsockname()[:2]) 2897 if self.authenticated: 2898 s.append('Logged in as: %s' % self.username) 2899 else: 2900 if not self.username: 2901 s.append("Waiting for username.") 2902 else: 2903 s.append("Waiting for password.") 2904 if self._current_type == 'a': 2905 type = 'ASCII' 2906 else: 2907 type = 'Binary' 2908 s.append("TYPE: %s; STRUcture: File; MODE: Stream" % type) 2909 if self._dtp_acceptor is not None: 2910 s.append('Passive data channel waiting for connection.') 2911 elif self.data_channel is not None: 2912 bytes_sent = self.data_channel.tot_bytes_sent 2913 bytes_recv = self.data_channel.tot_bytes_received 2914 elapsed_time = self.data_channel.get_elapsed_time() 2915 s.append('Data connection open:') 2916 s.append('Total bytes sent: %s' % bytes_sent) 2917 s.append('Total bytes received: %s' % bytes_recv) 2918 s.append('Transfer elapsed time: %s secs' % elapsed_time) 2919 else: 2920 s.append('Data connection closed.') 2921 2922 self.push('211-FTP server status:\r\n') 2923 self.push(''.join([' %s\r\n' % item for item in s])) 2924 self.respond('211 End of status.') 2925 # return directory LISTing over the command channel 2926 else: 2927 line = self.fs.fs2ftp(path) 2928 try: 2929 isdir = self.fs.isdir(path) 2930 if isdir: 2931 listing = self.run_as_current_user(self.fs.listdir, path) 2932 if isinstance(listing, list): 2933 try: 2934 # RFC 959 recommends the listing to be sorted. 2935 listing.sort() 2936 except UnicodeDecodeError: 2937 # (Python 2 only) might happen on filesystem not 2938 # supporting UTF8 meaning os.listdir() returned a 2939 # list of mixed bytes and unicode strings: 2940 # http://goo.gl/6DLHD 2941 # http://bugs.python.org/issue683592 2942 pass 2943 iterator = self.fs.format_list(path, listing) 2944 else: 2945 basedir, filename = os.path.split(path) 2946 self.fs.lstat(path) # raise exc in case of problems 2947 iterator = self.fs.format_list(basedir, [filename]) 2948 except (OSError, FilesystemError) as err: 2949 why = _strerror(err) 2950 self.respond('550 %s.' % why) 2951 else: 2952 self.push('213-Status of "%s":\r\n' % line) 2953 self.push_with_producer(BufferedIteratorProducer(iterator)) 2954 self.respond('213 End of status.') 2955 return path 2956 2957 def ftp_FEAT(self, line): 2958 """List all new features supported as defined in RFC-2398.""" 2959 features = set(['UTF8', 'TVFS']) 2960 features.update([feat for feat in 2961 ('EPRT', 'EPSV', 'MDTM', 'MFMT', 'SIZE') 2962 if feat in self.proto_cmds]) 2963 features.update(self._extra_feats) 2964 if 'MLST' in self.proto_cmds or 'MLSD' in self.proto_cmds: 2965 facts = '' 2966 for fact in self._available_facts: 2967 if fact in self._current_facts: 2968 facts += fact + '*;' 2969 else: 2970 facts += fact + ';' 2971 features.add('MLST ' + facts) 2972 if 'REST' in self.proto_cmds: 2973 features.add('REST STREAM') 2974 features = sorted(features) 2975 self.push("211-Features supported:\r\n") 2976 self.push("".join([" %s\r\n" % x for x in features])) 2977 self.respond('211 End FEAT.') 2978 2979 def ftp_OPTS(self, line): 2980 """Specify options for FTP commands as specified in RFC-2389.""" 2981 try: 2982 if line.count(' ') > 1: 2983 raise ValueError('Invalid number of arguments') 2984 if ' ' in line: 2985 cmd, arg = line.split(' ') 2986 if ';' not in arg: 2987 raise ValueError('Invalid argument') 2988 else: 2989 cmd, arg = line, '' 2990 # actually the only command able to accept options is MLST 2991 if cmd.upper() != 'MLST' or 'MLST' not in self.proto_cmds: 2992 raise ValueError('Unsupported command "%s"' % cmd) 2993 except ValueError as err: 2994 self.respond('501 %s.' % err) 2995 else: 2996 facts = [x.lower() for x in arg.split(';')] 2997 self._current_facts = \ 2998 [x for x in facts if x in self._available_facts] 2999 f = ''.join([x + ';' for x in self._current_facts]) 3000 self.respond('200 MLST OPTS ' + f) 3001 3002 def ftp_NOOP(self, line): 3003 """Do nothing.""" 3004 self.respond("200 I successfully done nothin'.") 3005 3006 def ftp_SYST(self, line): 3007 """Return system type (always returns UNIX type: L8).""" 3008 # This command is used to find out the type of operating system 3009 # at the server. The reply shall have as its first word one of 3010 # the system names listed in RFC-943. 3011 # Since that we always return a "/bin/ls -lA"-like output on 3012 # LIST we prefer to respond as if we would on Unix in any case. 3013 self.respond("215 UNIX Type: L8") 3014 3015 def ftp_ALLO(self, line): 3016 """Allocate bytes for storage (noop).""" 3017 # not necessary (always respond with 202) 3018 self.respond("202 No storage allocation necessary.") 3019 3020 def ftp_HELP(self, line): 3021 """Return help text to the client.""" 3022 if line: 3023 line = line.upper() 3024 if line in self.proto_cmds: 3025 self.respond("214 %s" % self.proto_cmds[line]['help']) 3026 else: 3027 self.respond("501 Unrecognized command.") 3028 else: 3029 # provide a compact list of recognized commands 3030 def formatted_help(): 3031 cmds = [] 3032 keys = sorted([x for x in self.proto_cmds.keys() 3033 if not x.startswith('SITE ')]) 3034 while keys: 3035 elems = tuple((keys[0:8])) 3036 cmds.append(' %-6s' * len(elems) % elems + '\r\n') 3037 del keys[0:8] 3038 return ''.join(cmds) 3039 3040 self.push("214-The following commands are recognized:\r\n") 3041 self.push(formatted_help()) 3042 self.respond("214 Help command successful.") 3043 3044 # --- site commands 3045 3046 # The user willing to add support for a specific SITE command must 3047 # update self.proto_cmds dictionary and define a new ftp_SITE_%CMD% 3048 # method in the subclass. 3049 3050 def ftp_SITE_CHMOD(self, path, mode): 3051 """Change file mode. 3052 On success return a (file_path, mode) tuple. 3053 """ 3054 # Note: although most UNIX servers implement it, SITE CHMOD is not 3055 # defined in any official RFC. 3056 try: 3057 assert len(mode) in (3, 4) 3058 for x in mode: 3059 assert 0 <= int(x) <= 7 3060 mode = int(mode, 8) 3061 except (AssertionError, ValueError): 3062 self.respond("501 Invalid SITE CHMOD format.") 3063 else: 3064 try: 3065 self.run_as_current_user(self.fs.chmod, path, mode) 3066 except (OSError, FilesystemError) as err: 3067 why = _strerror(err) 3068 self.respond('550 %s.' % why) 3069 else: 3070 self.respond('200 SITE CHMOD successful.') 3071 return (path, mode) 3072 3073 def ftp_SITE_HELP(self, line): 3074 """Return help text to the client for a given SITE command.""" 3075 if line: 3076 line = line.upper() 3077 if line in self.proto_cmds: 3078 self.respond("214 %s" % self.proto_cmds[line]['help']) 3079 else: 3080 self.respond("501 Unrecognized SITE command.") 3081 else: 3082 self.push("214-The following SITE commands are recognized:\r\n") 3083 site_cmds = [] 3084 for cmd in sorted(self.proto_cmds.keys()): 3085 if cmd.startswith('SITE '): 3086 site_cmds.append(' %s\r\n' % cmd[5:]) 3087 self.push(''.join(site_cmds)) 3088 self.respond("214 Help SITE command successful.") 3089 3090 # --- support for deprecated cmds 3091 3092 # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD 3093 # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD. 3094 # Such commands are obsoleted but some ftp clients (e.g. Windows 3095 # ftp.exe) still use them. 3096 3097 def ftp_XCUP(self, line): 3098 "Change to the parent directory. Synonym for CDUP. Deprecated." 3099 return self.ftp_CDUP(line) 3100 3101 def ftp_XCWD(self, line): 3102 "Change the current working directory. Synonym for CWD. Deprecated." 3103 return self.ftp_CWD(line) 3104 3105 def ftp_XMKD(self, line): 3106 "Create the specified directory. Synonym for MKD. Deprecated." 3107 return self.ftp_MKD(line) 3108 3109 def ftp_XPWD(self, line): 3110 "Return the current working directory. Synonym for PWD. Deprecated." 3111 return self.ftp_PWD(line) 3112 3113 def ftp_XRMD(self, line): 3114 "Remove the specified directory. Synonym for RMD. Deprecated." 3115 return self.ftp_RMD(line) 3116 3117 3118# =================================================================== 3119# --- FTP over SSL 3120# =================================================================== 3121 3122 3123if SSL is not None: 3124 3125 class SSLConnection(_AsyncChatNewStyle): 3126 """An AsyncChat subclass supporting TLS/SSL.""" 3127 3128 _ssl_accepting = False 3129 _ssl_established = False 3130 _ssl_closing = False 3131 _ssl_requested = False 3132 3133 def __init__(self, *args, **kwargs): 3134 super(SSLConnection, self).__init__(*args, **kwargs) 3135 self._error = False 3136 self._ssl_want_read = False 3137 self._ssl_want_write = False 3138 3139 def readable(self): 3140 return self._ssl_want_read or \ 3141 super(SSLConnection, self).readable() 3142 3143 def writable(self): 3144 return self._ssl_want_write or \ 3145 super(SSLConnection, self).writable() 3146 3147 def secure_connection(self, ssl_context): 3148 """Secure the connection switching from plain-text to 3149 SSL/TLS. 3150 """ 3151 debug("securing SSL connection", self) 3152 self._ssl_requested = True 3153 try: 3154 self.socket = SSL.Connection(ssl_context, self.socket) 3155 except socket.error as err: 3156 # may happen in case the client connects/disconnects 3157 # very quickly 3158 debug( 3159 "call: secure_connection(); can't secure SSL connection " 3160 "%r; closing" % err, self) 3161 self.close() 3162 except ValueError: 3163 # may happen in case the client connects/disconnects 3164 # very quickly 3165 if self.socket.fileno() == -1: 3166 debug( 3167 "ValueError and fd == -1 on secure_connection()", self) 3168 return 3169 raise 3170 else: 3171 self.socket.set_accept_state() 3172 self._ssl_accepting = True 3173 3174 @contextlib.contextmanager 3175 def _handle_ssl_want_rw(self): 3176 prev_row_pending = self._ssl_want_read or self._ssl_want_write 3177 try: 3178 yield 3179 except SSL.WantReadError: 3180 # we should never get here; it's just for extra safety 3181 self._ssl_want_read = True 3182 except SSL.WantWriteError: 3183 # we should never get here; it's just for extra safety 3184 self._ssl_want_write = True 3185 3186 if self._ssl_want_read: 3187 self.modify_ioloop_events( 3188 self._wanted_io_events | self.ioloop.READ, logdebug=True) 3189 elif self._ssl_want_write: 3190 self.modify_ioloop_events( 3191 self._wanted_io_events | self.ioloop.WRITE, logdebug=True) 3192 else: 3193 if prev_row_pending: 3194 self.modify_ioloop_events(self._wanted_io_events) 3195 3196 def _do_ssl_handshake(self): 3197 self._ssl_accepting = True 3198 self._ssl_want_read = False 3199 self._ssl_want_write = False 3200 try: 3201 self.socket.do_handshake() 3202 except SSL.WantReadError: 3203 self._ssl_want_read = True 3204 debug("call: _do_ssl_handshake, err: ssl-want-read", inst=self) 3205 except SSL.WantWriteError: 3206 self._ssl_want_write = True 3207 debug("call: _do_ssl_handshake, err: ssl-want-write", 3208 inst=self) 3209 except SSL.SysCallError as err: 3210 debug("call: _do_ssl_handshake, err: %r" % err, inst=self) 3211 retval, desc = err.args 3212 if (retval == -1 and desc == 'Unexpected EOF') or retval > 0: 3213 return self.handle_close() 3214 raise 3215 except SSL.Error as err: 3216 debug("call: _do_ssl_handshake, err: %r" % err, inst=self) 3217 return self.handle_failed_ssl_handshake() 3218 else: 3219 debug("SSL connection established", self) 3220 self._ssl_accepting = False 3221 self._ssl_established = True 3222 self.handle_ssl_established() 3223 3224 def handle_ssl_established(self): 3225 """Called when SSL handshake has completed.""" 3226 pass 3227 3228 def handle_ssl_shutdown(self): 3229 """Called when SSL shutdown() has completed.""" 3230 super(SSLConnection, self).close() 3231 3232 def handle_failed_ssl_handshake(self): 3233 raise NotImplementedError("must be implemented in subclass") 3234 3235 def handle_read_event(self): 3236 if not self._ssl_requested: 3237 super(SSLConnection, self).handle_read_event() 3238 else: 3239 with self._handle_ssl_want_rw(): 3240 self._ssl_want_read = False 3241 if self._ssl_accepting: 3242 self._do_ssl_handshake() 3243 elif self._ssl_closing: 3244 self._do_ssl_shutdown() 3245 else: 3246 super(SSLConnection, self).handle_read_event() 3247 3248 def handle_write_event(self): 3249 if not self._ssl_requested: 3250 super(SSLConnection, self).handle_write_event() 3251 else: 3252 with self._handle_ssl_want_rw(): 3253 self._ssl_want_write = False 3254 if self._ssl_accepting: 3255 self._do_ssl_handshake() 3256 elif self._ssl_closing: 3257 self._do_ssl_shutdown() 3258 else: 3259 super(SSLConnection, self).handle_write_event() 3260 3261 def handle_error(self): 3262 self._error = True 3263 try: 3264 raise 3265 except Exception: 3266 self.log_exception(self) 3267 # when facing an unhandled exception in here it's better 3268 # to rely on base class (FTPHandler or DTPHandler) 3269 # close() method as it does not imply SSL shutdown logic 3270 try: 3271 super(SSLConnection, self).close() 3272 except Exception: 3273 logger.critical(traceback.format_exc()) 3274 3275 def send(self, data): 3276 if not isinstance(data, bytes): 3277 data = bytes(data) 3278 try: 3279 return super(SSLConnection, self).send(data) 3280 except SSL.WantReadError: 3281 debug("call: send(), err: ssl-want-read", inst=self) 3282 self._ssl_want_read = True 3283 return 0 3284 except SSL.WantWriteError: 3285 debug("call: send(), err: ssl-want-write", inst=self) 3286 self._ssl_want_write = True 3287 return 0 3288 except SSL.ZeroReturnError as err: 3289 debug( 3290 "call: send() -> shutdown(), err: zero-return", inst=self) 3291 super(SSLConnection, self).handle_close() 3292 return 0 3293 except SSL.SysCallError as err: 3294 debug("call: send(), err: %r" % err, inst=self) 3295 errnum, errstr = err.args 3296 if errnum == errno.EWOULDBLOCK: 3297 return 0 3298 elif (errnum in _ERRNOS_DISCONNECTED or 3299 errstr == 'Unexpected EOF'): 3300 super(SSLConnection, self).handle_close() 3301 return 0 3302 else: 3303 raise 3304 3305 def recv(self, buffer_size): 3306 try: 3307 return super(SSLConnection, self).recv(buffer_size) 3308 except SSL.WantReadError: 3309 debug("call: recv(), err: ssl-want-read", inst=self) 3310 self._ssl_want_read = True 3311 raise RetryError 3312 except SSL.WantWriteError: 3313 debug("call: recv(), err: ssl-want-write", inst=self) 3314 self._ssl_want_write = True 3315 raise RetryError 3316 except SSL.ZeroReturnError as err: 3317 debug("call: recv() -> shutdown(), err: zero-return", 3318 inst=self) 3319 super(SSLConnection, self).handle_close() 3320 return b'' 3321 except SSL.SysCallError as err: 3322 debug("call: recv(), err: %r" % err, inst=self) 3323 errnum, errstr = err.args 3324 if (errnum in _ERRNOS_DISCONNECTED or 3325 errstr == 'Unexpected EOF'): 3326 super(SSLConnection, self).handle_close() 3327 return b'' 3328 else: 3329 raise 3330 3331 def _do_ssl_shutdown(self): 3332 """Executes a SSL_shutdown() call to revert the connection 3333 back to clear-text. 3334 twisted/internet/tcp.py code has been used as an example. 3335 """ 3336 self._ssl_closing = True 3337 if os.name == 'posix': 3338 # since SSL_shutdown() doesn't report errors, an empty 3339 # write call is done first, to try to detect if the 3340 # connection has gone away 3341 try: 3342 os.write(self.socket.fileno(), b'') 3343 except (OSError, socket.error) as err: 3344 debug( 3345 "call: _do_ssl_shutdown() -> os.write, err: %r" % err, 3346 inst=self) 3347 if err.errno in (errno.EINTR, errno.EWOULDBLOCK, 3348 errno.ENOBUFS): 3349 return 3350 elif err.errno in _ERRNOS_DISCONNECTED: 3351 return super(SSLConnection, self).close() 3352 else: 3353 raise 3354 # Ok, this a mess, but the underlying OpenSSL API simply 3355 # *SUCKS* and I really couldn't do any better. 3356 # 3357 # Here we just want to shutdown() the SSL layer and then 3358 # close() the connection so we're not interested in a 3359 # complete SSL shutdown() handshake, so let's pretend 3360 # we already received a "RECEIVED" shutdown notification 3361 # from the client. 3362 # Once the client received our "SENT" shutdown notification 3363 # then we close() the connection. 3364 # 3365 # Since it is not clear what errors to expect during the 3366 # entire procedure we catch them all and assume the 3367 # following: 3368 # - WantReadError and WantWriteError means "retry" 3369 # - ZeroReturnError, SysCallError[EOF], Error[] are all 3370 # aliases for disconnection 3371 try: 3372 laststate = self.socket.get_shutdown() 3373 self.socket.set_shutdown(laststate | SSL.RECEIVED_SHUTDOWN) 3374 done = self.socket.shutdown() 3375 if not (laststate & SSL.RECEIVED_SHUTDOWN): 3376 self.socket.set_shutdown(SSL.SENT_SHUTDOWN) 3377 except SSL.WantReadError: 3378 self._ssl_want_read = True 3379 debug("call: _do_ssl_shutdown, err: ssl-want-read", inst=self) 3380 except SSL.WantWriteError: 3381 self._ssl_want_write = True 3382 debug("call: _do_ssl_shutdown, err: ssl-want-write", inst=self) 3383 except SSL.ZeroReturnError as err: 3384 debug( 3385 "call: _do_ssl_shutdown() -> shutdown(), err: zero-return", 3386 inst=self) 3387 super(SSLConnection, self).close() 3388 except SSL.SysCallError as err: 3389 debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err, 3390 inst=self) 3391 errnum, errstr = err.args 3392 if (errnum in _ERRNOS_DISCONNECTED or 3393 errstr == 'Unexpected EOF'): 3394 super(SSLConnection, self).close() 3395 else: 3396 raise 3397 except SSL.Error as err: 3398 debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err, 3399 inst=self) 3400 # see: 3401 # https://github.com/giampaolo/pyftpdlib/issues/171 3402 # https://bugs.launchpad.net/pyopenssl/+bug/785985 3403 if err.args and not getattr(err, "errno", None): 3404 pass 3405 else: 3406 raise 3407 except socket.error as err: 3408 debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err, 3409 inst=self) 3410 if err.errno in _ERRNOS_DISCONNECTED: 3411 super(SSLConnection, self).close() 3412 else: 3413 raise 3414 else: 3415 if done: 3416 debug("call: _do_ssl_shutdown(), shutdown completed", 3417 inst=self) 3418 self._ssl_established = False 3419 self._ssl_closing = False 3420 self.handle_ssl_shutdown() 3421 else: 3422 debug( 3423 "call: _do_ssl_shutdown(), shutdown not completed yet", 3424 inst=self) 3425 3426 def close(self): 3427 if self._ssl_established and not self._error: 3428 self._do_ssl_shutdown() 3429 else: 3430 self._ssl_accepting = False 3431 self._ssl_established = False 3432 self._ssl_closing = False 3433 super(SSLConnection, self).close() 3434 3435 class TLS_DTPHandler(SSLConnection, DTPHandler): 3436 """A DTPHandler subclass supporting TLS/SSL.""" 3437 3438 def __init__(self, sock, cmd_channel): 3439 super(TLS_DTPHandler, self).__init__(sock, cmd_channel) 3440 if self.cmd_channel._prot: 3441 self.secure_connection(self.cmd_channel.ssl_context) 3442 3443 def __repr__(self): 3444 return DTPHandler.__repr__(self) 3445 3446 def use_sendfile(self): 3447 if isinstance(self.socket, SSL.Connection): 3448 return False 3449 else: 3450 return super(TLS_DTPHandler, self).use_sendfile() 3451 3452 def handle_failed_ssl_handshake(self): 3453 # TLS/SSL handshake failure, probably client's fault which 3454 # used a SSL version different from server's. 3455 # RFC-4217, chapter 10.2 expects us to return 522 over the 3456 # command channel. 3457 self.cmd_channel.respond("522 SSL handshake failed.") 3458 self.cmd_channel.log_cmd("PROT", "P", 522, "SSL handshake failed.") 3459 self.close() 3460 3461 class TLS_FTPHandler(SSLConnection, FTPHandler): 3462 """A FTPHandler subclass supporting TLS/SSL. 3463 Implements AUTH, PBSZ and PROT commands (RFC-2228 and RFC-4217). 3464 3465 Configurable attributes: 3466 3467 - (bool) tls_control_required: 3468 When True requires SSL/TLS to be established on the control 3469 channel, before logging in. This means the user will have 3470 to issue AUTH before USER/PASS (default False). 3471 3472 - (bool) tls_data_required: 3473 When True requires SSL/TLS to be established on the data 3474 channel. This means the user will have to issue PROT 3475 before PASV or PORT (default False). 3476 3477 SSL-specific options: 3478 3479 - (string) certfile: 3480 the path to the file which contains a certificate to be 3481 used to identify the local side of the connection. 3482 This must always be specified, unless context is provided 3483 instead. 3484 3485 - (string) keyfile: 3486 the path to the file containing the private RSA key; 3487 can be omitted if certfile already contains the private 3488 key (defaults: None). 3489 3490 - (int) ssl_protocol: 3491 the desired SSL protocol version to use. This defaults to 3492 PROTOCOL_SSLv23 which will negotiate the highest protocol 3493 that both the server and your installation of OpenSSL 3494 support. 3495 3496 - (int) ssl_options: 3497 specific OpenSSL options. These default to: 3498 SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3| SSL.OP_NO_COMPRESSION 3499 which are all considered insecure features. 3500 Can be set to None in order to improve compatibilty with 3501 older (insecure) FTP clients. 3502 3503 - (instance) ssl_context: 3504 a SSL Context object previously configured; if specified 3505 all other parameters will be ignored. 3506 (default None). 3507 """ 3508 3509 # configurable attributes 3510 tls_control_required = False 3511 tls_data_required = False 3512 certfile = None 3513 keyfile = None 3514 ssl_protocol = SSL.SSLv23_METHOD 3515 # - SSLv2 is easily broken and is considered harmful and dangerous 3516 # - SSLv3 has several problems and is now dangerous 3517 # - Disable compression to prevent CRIME attacks for OpenSSL 1.0+ 3518 # (see https://github.com/shazow/urllib3/pull/309) 3519 ssl_options = SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 3520 if hasattr(SSL, "OP_NO_COMPRESSION"): 3521 ssl_options |= SSL.OP_NO_COMPRESSION 3522 ssl_context = None 3523 3524 # overridden attributes 3525 dtp_handler = TLS_DTPHandler 3526 proto_cmds = FTPHandler.proto_cmds.copy() 3527 proto_cmds.update({ 3528 'AUTH': dict( 3529 perm=None, auth=False, arg=True, 3530 help='Syntax: AUTH <SP> TLS|SSL (set up secure control ' 3531 'channel).'), 3532 'PBSZ': dict( 3533 perm=None, auth=False, arg=True, 3534 help='Syntax: PBSZ <SP> 0 (negotiate TLS buffer).'), 3535 'PROT': dict( 3536 perm=None, auth=False, arg=True, 3537 help='Syntax: PROT <SP> [C|P] (set up un/secure data ' 3538 'channel).'), 3539 }) 3540 3541 def __init__(self, conn, server, ioloop=None): 3542 super(TLS_FTPHandler, self).__init__(conn, server, ioloop) 3543 if not self.connected: 3544 return 3545 self._extra_feats = ['AUTH TLS', 'AUTH SSL', 'PBSZ', 'PROT'] 3546 self._pbsz = False 3547 self._prot = False 3548 self.ssl_context = self.get_ssl_context() 3549 3550 def __repr__(self): 3551 return FTPHandler.__repr__(self) 3552 3553 @classmethod 3554 def get_ssl_context(cls): 3555 if cls.ssl_context is None: 3556 if cls.certfile is None: 3557 raise ValueError("at least certfile must be specified") 3558 cls.ssl_context = SSL.Context(cls.ssl_protocol) 3559 if cls.ssl_protocol != SSL.SSLv2_METHOD: 3560 cls.ssl_context.set_options(SSL.OP_NO_SSLv2) 3561 else: 3562 warnings.warn("SSLv2 protocol is insecure", RuntimeWarning) 3563 cls.ssl_context.use_certificate_chain_file(cls.certfile) 3564 if not cls.keyfile: 3565 cls.keyfile = cls.certfile 3566 cls.ssl_context.use_privatekey_file(cls.keyfile) 3567 if cls.ssl_options: 3568 cls.ssl_context.set_options(cls.ssl_options) 3569 return cls.ssl_context 3570 3571 # --- overridden methods 3572 3573 def flush_account(self): 3574 FTPHandler.flush_account(self) 3575 self._pbsz = False 3576 self._prot = False 3577 3578 def process_command(self, cmd, *args, **kwargs): 3579 if cmd in ('USER', 'PASS'): 3580 if self.tls_control_required and not self._ssl_established: 3581 msg = "SSL/TLS required on the control channel." 3582 self.respond("550 " + msg) 3583 self.log_cmd(cmd, args[0], 550, msg) 3584 return 3585 elif cmd in ('PASV', 'EPSV', 'PORT', 'EPRT'): 3586 if self.tls_data_required and not self._prot: 3587 msg = "SSL/TLS required on the data channel." 3588 self.respond("550 " + msg) 3589 self.log_cmd(cmd, args[0], 550, msg) 3590 return 3591 FTPHandler.process_command(self, cmd, *args, **kwargs) 3592 3593 # --- new methods 3594 3595 def handle_failed_ssl_handshake(self): 3596 # TLS/SSL handshake failure, probably client's fault which 3597 # used a SSL version different from server's. 3598 # We can't rely on the control connection anymore so we just 3599 # disconnect the client without sending any response. 3600 self.log("SSL handshake failed.") 3601 self.close() 3602 3603 def ftp_AUTH(self, line): 3604 """Set up secure control channel.""" 3605 arg = line.upper() 3606 if isinstance(self.socket, SSL.Connection): 3607 self.respond("503 Already using TLS.") 3608 elif arg in ('TLS', 'TLS-C', 'SSL', 'TLS-P'): 3609 # From RFC-4217: "As the SSL/TLS protocols self-negotiate 3610 # their levels, there is no need to distinguish between SSL 3611 # and TLS in the application layer". 3612 self.respond('234 AUTH %s successful.' % arg) 3613 self.secure_connection(self.ssl_context) 3614 else: 3615 self.respond( 3616 "502 Unrecognized encryption type (use TLS or SSL).") 3617 3618 def ftp_PBSZ(self, line): 3619 """Negotiate size of buffer for secure data transfer. 3620 For TLS/SSL the only valid value for the parameter is '0'. 3621 Any other value is accepted but ignored. 3622 """ 3623 if not isinstance(self.socket, SSL.Connection): 3624 self.respond( 3625 "503 PBSZ not allowed on insecure control connection.") 3626 else: 3627 self.respond('200 PBSZ=0 successful.') 3628 self._pbsz = True 3629 3630 def ftp_PROT(self, line): 3631 """Setup un/secure data channel.""" 3632 arg = line.upper() 3633 if not isinstance(self.socket, SSL.Connection): 3634 self.respond( 3635 "503 PROT not allowed on insecure control connection.") 3636 elif not self._pbsz: 3637 self.respond( 3638 "503 You must issue the PBSZ command prior to PROT.") 3639 elif arg == 'C': 3640 self.respond('200 Protection set to Clear') 3641 self._prot = False 3642 elif arg == 'P': 3643 self.respond('200 Protection set to Private') 3644 self._prot = True 3645 elif arg in ('S', 'E'): 3646 self.respond('521 PROT %s unsupported (use C or P).' % arg) 3647 else: 3648 self.respond("502 Unrecognized PROT type (use C or P).") 3649