def _process_request(self, req_data, req_addr): # The TFTP specification (RFC 1350) says that a request that is denied # should result in an error packet being sent. It also specifies that an # invalid packet received as part of a connection should result in an # error packet being sent. However, it does not specify what should # happen if an invalid packet is sent to the request port. # Most other implementations seem to ignore requests with an invalid # opcode, so we choose to do the same. We still log a debug message in # these cases. # A request must have at least two bytes for the opcode. if len(req_data) < 2: logger.debug('Invalid request from %s: The request was too short.', socket_address_to_str(req_addr)) return (opcode_num, ) = struct.unpack_from('!H', req_data) try: opcode = Opcode(opcode_num) except Exception: logger.debug( 'Invalid request from %s: Opcode %s is not recognized.', socket_address_to_str(req_addr), opcode_num) return if opcode == Opcode.READ_REQUEST: self._process_read_request(req_data, req_addr) elif opcode == Opcode.WRITE_REQUEST: self._process_write_request(req_data, req_addr) else: self._process_invalid_request(opcode, req_addr)
def _process_write_request(self, req_data, req_addr): logger.error( 'Received write request from %s, but this server only supports ' 'read requests', socket_address_to_str(req_addr)) data = error_packet(ErrorCode.ACCESS_VIOLATION, 'Write requests are not allowed by this server.') self._socket.sendto(data, req_addr)
def _delegate_request(self): try: response_started = False # No sane HTTP client will ever send a request path that does # not start with a slash or contains a null byte. We do not # check that the request path does not contain a null byte after # decoding, this is the job of the handler that does the # decoding. if (not self.path.startswith('/')) or ('\0' in self.path): response_started = True self.send_error(http.HTTPStatus.BAD_REQUEST) return for handler in self.server.real_request_handlers: context = handler.prepare_context(self.path) if handler.can_handle(self.path, context): try: (status, headers, body) = handler.handle( self.path, self.command, self.headers, self.rfile, self.client_address, context) except Exception: logger.exception( 'Request handler for %s request for file "%s" ' 'from client %s raised an exception.', self.command, self.path, socket_address_to_str(self.client_address)) response_started = True self.send_error( http.HTTPStatus.INTERNAL_SERVER_ERROR) return # If status is an error code and there is neither a body # nor are there headers, we use send_error instead of # send_response. response_started = True if ((status.value >= 400) and (not headers) and (body is None)): self.send_error(status) return self.send_response(status) if headers is not None: for header_name, header_value in headers.items(): self.send_header(header_name, header_value) self.end_headers() if body is not None: shutil.copyfileobj(body, self.wfile) return self.send_error(http.HTTPStatus.NOT_FOUND) except Exception: # We do not want a problem with a request to bubble up into the # calling code, so we log the problem and continue. logger.exception('Request processing failed.') if not response_started: self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR) return
def _process_invalid_request(self, opcode, req_addr): logger.debug( 'Received request from %s with opcode %s, but only READ or WRITE ' 'requests are allowed on this server port.', socket_address_to_str(req_addr), opcode) data = error_packet( ErrorCode.ILLEGAL_OPERATION, 'Only read or write requests are allowed on this port.') self._socket.sendto(data, req_addr)
def _handle_read(self, filename, transfer_mode, options, client_address, handler_function, handler_context): # If debugging is enabled, we make the info message more verbose. if logger.isEnabledFor(logging.DEBUG): logger.info( 'Received read request for file "%s" from client %s using mode ' '%s and options %s.', filename, socket_address_to_str(client_address), transfer_mode, options) else: logger.info('Handling read request for file "%s" from client %s.', filename, socket_address_to_str(client_address)) # Constructing the request object is sufficient for handling the # request. The actual request handling is done by a daemon thread that # is created when constructing the object. _TftpReadRequest(filename, transfer_mode, options, client_address, handler_function, handler_context, self._default_timeout, self._max_timeout, self._max_retries, self._max_block_size, self._block_counter_wrap_value)
def _process_read_request(self, req_data, req_addr): try: (filename, transfer_mode, options) = decode_read_request(req_data) except Exception: # If we cannot decode the read request, this is an error, but in the # client, not the server, so we log it with a level of INFO. logger.info( 'Decoding read request from %s resulted in an exception.', socket_address_to_str(req_addr)) data = error_packet(ErrorCode.ILLEGAL_OPERATION, 'Malformed read request.') self._socket.sendto(data, req_addr) return # The mail transfer mode is only valid for write requests (and we do not # support it anyway). if transfer_mode == TransferMode.MAIL: # If the client requests transfer mode "mail" this is not an error # in the server, so we log it with a level of INFO. logger.info( 'Read request from %s requested unsupported transfer mode ' '"mail".', socket_address_to_str(req_addr)) data = error_packet( ErrorCode.ILLEGAL_OPERATION, 'Transfer mode mail is not allowed for read requests.') self._socket.sendto(data, req_addr) return # We try the request handlers in order until we find one that can handle # the request. for request_handler in self._request_handlers: handler_context = request_handler.prepare_context(filename) if request_handler.can_handle(filename, handler_context): self._handle_read(filename, transfer_mode, options, req_addr, request_handler.handle, handler_context) return # If we cannot find such a request handler, we tell the client that the # file does not exist. logger.info( 'No handler can fulfill request for file "%s" from client %s.', filename, socket_address_to_str(req_addr)) data = error_packet(ErrorCode.FILE_NOT_FOUND, 'The requested file does not exist.') self._socket.sendto(data, req_addr)
def _receive(self): self._set_socket_timeout() (data, from_addr) = self._socket.recvfrom(MAX_REQUEST_PACKET_SIZE) # The TFTP specification demands that we send an error packet to each # client that sends a packet to a port that belongs to the connection # of a different client. For this connection, we are supposed to ignore # such a packet and continue waiting for a packet from the right client. while from_addr != self._client_address: logger.debug( 'Received unexpected packet from %s on socket handling ' 'connection from %s.', socket_address_to_str(from_addr), socket_address_to_str(self._client_address)) data = error_packet( ErrorCode.UNKNOWN_TRANSFER_ID, 'This port is associated with a different client connection.') self._socket.sendto(data, from_addr) # Some time has already passed, so we have to reset the socket # timeout. self._set_socket_timeout() (data, from_addr) = self._socket.recvfrom(MAX_REQUEST_PACKET_SIZE) return data
def start(self): """ Starts this server instance. This opens the server socket, binds it, and creates a deamon thread that processes requests. If the server is already running, this method does nothing. """ with self._running_lock: if self._running: return self._socket = socket.socket(family=socket.AF_INET6, type=socket.SOCK_DGRAM) try: self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # This timeout specifies how quickly we can shutdown the server. self._socket.settimeout(0.1) # socket.IPPROTO_IPV6 is not available when running on Windows # and using Python < 3.8, so we fall back to a fixed value if it # is not available. try: ipproto_ipv6 = socket.IPPROTO_IPV6 except AttributeError: ipproto_ipv6 = 41 # socket.IPV6_V6ONLY, on the other hand, should be available on # Windows, at least for the Python versions we care about # (>= 3.5). If it is not available or if the call to setsockopt # fails, we log a warning, but continue. try: self._socket.setsockopt(ipproto_ipv6, socket.IPV6_V6ONLY, 0) except Exception: logger.warning( 'Cannot set IPV6_V6ONLY socket option to 0, socket ' 'might not be reachable via IPv4.') self._socket.bind((self._bind_address, self._bind_port)) logger.info( 'TFTP server is listening on %s.', socket_address_to_str( (self._bind_address, self._bind_port))) self._main_thread = threading.Thread(target=self._run, daemon=True) self._main_thread.start() self._running = True except BaseException: self._socket.close() self._socket = None raise
def start(self): """ Start this server instance. If the server is already running, this method does nothing. """ with self._running_lock: if self._running: return self._server = self._ThreadingHTTPServer( (self._bind_address, self._bind_port), self._DelegatingRequestHandler) logger.info( 'HTTP server is listening on %s.', socket_address_to_str((self._bind_address, self._bind_port))) self._server.real_request_handlers = self._request_handlers self._main_thread = threading.Thread(target=self._run, daemon=True) self._main_thread.start() self._running = True
def _send_options_ack(self): data = options_ack_packet(self._options) tries_left = self._max_retries + 1 ack_received = False while not ack_received and tries_left > 0: # With each try, we have to reset the timeout. self._reset_timeout() logger.debug('Sending OACK with options %s to %s.', self._options, socket_address_to_str(self._client_address)) self._send(data) try: while not ack_received: block_number = self._receive_ack() # The ACK for the options ACK has to use a block number of # zero. ack_received = (block_number == 0) except socket.timeout: if tries_left > 0: tries_left -= 1 else: raise
def _send_data_block(self, block_number, data): packet_data = data_packet(block_number, data) tries_left = self._max_retries + 1 ack_received = False while not ack_received and tries_left > 0: # With each try, we have to reset the timeout. self._reset_timeout() logger.debug( 'Sending DATA with block # %s and %s bytes of data to %s.', block_number, len(data), socket_address_to_str(self._client_address)) try: self._send(packet_data) while not ack_received: ack_block_number = self._receive_ack() # The block number in the ACK packet has to match the # block number in the DATA packet. ack_received = (ack_block_number == block_number) except socket.timeout: if tries_left > 0: tries_left -= 1 else: raise
def _receive_ack(self): data = self._receive() if len(data) < 2: raise _TftpReadRequest._InvalidPacket('Short packet received.') (opcode_num, ) = struct.unpack_from('!H', data) try: opcode = Opcode(opcode_num) except ValueError: raise _TftpReadRequest._InvalidPacket( 'Received packet with invalid opcode %s.', opcode_num) if opcode == Opcode.ACK: try: block_number = decode_ack(data) logger.debug('Received ACK for block # %s from %s.', block_number, socket_address_to_str(self._client_address)) return block_number except ValueError: raise _TftpReadRequest._InvalidPacket( 'Received malformed ACK packet.') elif opcode == Opcode.ERROR: (error_code, error_message) = decode_error(data) if error_code == ErrorCode.TRANSFER_ABORTED: raise _TftpReadRequest._TransferAborted() if error_code is not None and error_message: raise _TftpReadRequest._ClientError( 'Error code {0}: {1}'.format(error_code.value, error_message)) elif error_code is not None: raise _TftpReadRequest._ClientError('Error code {0}.'.format( error_code.value)) elif error_message: raise _TftpReadRequest._ClientError( 'Error code unknown: {0}'.format(error_message)) else: raise _TftpReadRequest._ClientError('Unknown error.')
def _run(self): try: self._socket = socket.socket(family=socket.AF_INET6, type=socket.SOCK_DGRAM) except Exception: logger.exception( 'Error creating socket for read request for file "%s" from ' 'client %s.', self._filename, socket_address_to_str(self._client_address)) return # We want to make sure that we always close the socket, so we use it in # a with statement. with self._socket: self._socket.settimeout(self._timeout) # socket.IPPROTO_IPV6 is not available when running on Windows and # using Python < 3.8, so we fall back to a fixed value if it is not # available. try: ipproto_ipv6 = socket.IPPROTO_IPV6 except AttributeError: ipproto_ipv6 = 41 # socket.IPV6_V6ONLY, on the other hand, should be available on # Windows, at least for the Python versions we care about (>= 3.5). # If it is not available or if the call to setsockopt fails, we do # not even log a warning because we most likely already logged that # warning for the main socket. try: self._socket.setsockopt(ipproto_ipv6, socket.IPV6_V6ONLY, 0) except Exception: pass try: self._file = self._handler_function(self._filename, self._client_address, self._handler_context) except TftpError as e: # A TftpError is not necessarily a "real" error, so we only log # it with a level of info. logger.info( 'Request handler for read request for file "%s" from ' 'client %s signalled an error with error code %s and ' 'message "%s".', self._filename, socket_address_to_str(self._client_address), e.error_code, e.message) data = error_packet(e.error_code, e.message) # When sending the error, we want to use a fresh timeout value. self._socket.settimeout(self._timeout) self._send(data) return except Exception: logger.exception( 'Request handler for read request for file "%s" from ' 'client %s raised an exception.', self._filename, socket_address_to_str(self._client_address)) data = error_packet( ErrorCode.NOT_DEFINED, 'An internal error occurred while trying to fulfill the ' 'request.') # When sending the error, we want to use a fresh timeout value. self._socket.settimeout(self._timeout) self._send(data) return # We always want to close the file-like object, so we use it in a # with statement. with self._file: # If the client requested the file size, we tell it if possible. # We do this here because we need access to the file in order to # determine its size. self._process_transfer_size_option() if self._netascii_mode: self._file_reader = _TftpReadRequest._NetasciiReader( self._file) else: self._file_reader = _TftpReadRequest._OctetReader( self._file) self._process_request()
def _process_request(self): try: if self._options: self._send_options_ack() self._send_data() except socket.timeout: # If we got a timeout the max. number of retries has been reached # and we give up. logger.info('Request for file "%s" from client %s timed out.', self._filename, socket_address_to_str(self._client_address)) except _TftpReadRequest._BlockCounterOverflow: # This exception is raised if a file is so large that the limit of # the block counter is reached, but wrapping of the counter is not # allowed. logger.error( 'Processing of request for file "%s" from client %s was ' 'aborted due the block counter reaching its limit.', self._filename, socket_address_to_str(self._client_address)) # When sending the error, we want to use a fresh timeout value. self._socket.settimeout(self._timeout) self._send( error_packet(ErrorCode.NOT_DEFINED, 'File is too large to complete the transfer.')) except _TftpReadRequest._ClientError as e: # If the client sent an error message, we log it and abort this # connection. logger.info( 'Processing of request for file "%s" from client %s was ' 'aborted due to a client error: %s', self._filename, socket_address_to_str(self._client_address), e.args[0]) except _TftpReadRequest._InvalidPacket as e: message = e.args[0] # If we received an invalid packet, we log this, send an error to # the client, and abort this connection. logger.info( 'Processing of request for file "%s" from client %s was ' 'aborted due to an invalid client packet: %s', self._filename, socket_address_to_str(self._client_address), message) # When sending the error, we want to use a fresh timeout value. self._socket.settimeout(self._timeout) self._send(error_packet(ErrorCode.NOT_DEFINED, message)) except _TftpReadRequest._TransferAborted: # The client aborting a transfer is not an error, but we log a # simple informational message. logger.info( 'Processing of request for file "%s" from client %s was ' 'aborted on client request.', self._filename, socket_address_to_str(self._client_address)) except Exception: # Otherwise, there must be some kind of internal error, so we log # the exception and send an error code to the client. logger.exception( 'Exception while processing read request for file "%s" from ' 'client %s.', self._filename, socket_address_to_str(self._client_address)) data = error_packet( ErrorCode.NOT_DEFINED, 'An internal error occurred while trying to fulfill the ' 'request.') # When sending the error, we want to use a fresh timeout value. self._socket.settimeout(self._timeout) self._send(data)