# Copyright 2020, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Request Handler and Request/Connection classes for standalone server. """ from __future__ import absolute_import import os from six.moves import CGIHTTPServer from six.moves import http_client from mod_pywebsocket import common from mod_pywebsocket import dispatch from mod_pywebsocket import handshake from mod_pywebsocket import http_header_util from mod_pywebsocket import memorizingfile from mod_pywebsocket import util # 1024 is practically large enough to contain WebSocket handshake lines. _MAX_MEMORIZED_LINES = 1024 class _StandaloneConnection(object): """Mimic mod_python mp_conn.""" def __init__(self, request_handler): """Construct an instance. Args: request_handler: A WebSocketRequestHandler instance. """ self._request_handler = request_handler def get_local_addr(self): """Getter to mimic mp_conn.local_addr.""" return (self._request_handler.server.server_name, self._request_handler.server.server_port) local_addr = property(get_local_addr) def get_remote_addr(self): """Getter to mimic mp_conn.remote_addr. Setting the property in __init__ won't work because the request handler is not initialized yet there.""" return self._request_handler.client_address remote_addr = property(get_remote_addr) def write(self, data): """Mimic mp_conn.write().""" return self._request_handler.wfile.write(data) def read(self, length): """Mimic mp_conn.read().""" return self._request_handler.rfile.read(length) def get_memorized_lines(self): """Get memorized lines.""" return self._request_handler.rfile.get_memorized_lines() class _StandaloneRequest(object): """Mimic mod_python request.""" def __init__(self, request_handler, use_tls): """Construct an instance. Args: request_handler: A WebSocketRequestHandler instance. """ self._logger = util.get_class_logger(self) self._request_handler = request_handler self.connection = _StandaloneConnection(request_handler) self._use_tls = use_tls self.headers_in = request_handler.headers def get_uri(self): """Getter to mimic request.uri. This method returns the raw data at the Request-URI part of the Request-Line, while the uri method on the request object of mod_python returns the path portion after parsing the raw data. This behavior is kept for compatibility. """ return self._request_handler.path uri = property(get_uri) def get_unparsed_uri(self): """Getter to mimic request.unparsed_uri.""" return self._request_handler.path unparsed_uri = property(get_unparsed_uri) def get_method(self): """Getter to mimic request.method.""" return self._request_handler.command method = property(get_method) def get_protocol(self): """Getter to mimic request.protocol.""" return self._request_handler.request_version protocol = property(get_protocol) def is_https(self): """Mimic request.is_https().""" return self._use_tls class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): """CGIHTTPRequestHandler specialized for WebSocket.""" # Use httplib.HTTPMessage instead of mimetools.Message. MessageClass = http_client.HTTPMessage def setup(self): """Override SocketServer.StreamRequestHandler.setup to wrap rfile with MemorizingFile. This method will be called by BaseRequestHandler's constructor before calling BaseHTTPRequestHandler.handle. BaseHTTPRequestHandler.handle will call BaseHTTPRequestHandler.handle_one_request and it will call WebSocketRequestHandler.parse_request. """ # Call superclass's setup to prepare rfile, wfile, etc. See setup # definition on the root class SocketServer.StreamRequestHandler to # understand what this does. CGIHTTPServer.CGIHTTPRequestHandler.setup(self) self.rfile = memorizingfile.MemorizingFile( self.rfile, max_memorized_lines=_MAX_MEMORIZED_LINES) def __init__(self, request, client_address, server): self._logger = util.get_class_logger(self) self._options = server.websocket_server_options # Overrides CGIHTTPServerRequestHandler.cgi_directories. self.cgi_directories = self._options.cgi_directories # Replace CGIHTTPRequestHandler.is_executable method. if self._options.is_executable_method is not None: self.is_executable = self._options.is_executable_method # This actually calls BaseRequestHandler.__init__. CGIHTTPServer.CGIHTTPRequestHandler.__init__(self, request, client_address, server) def parse_request(self): """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request. Return True to continue processing for HTTP(S), False otherwise. See BaseHTTPRequestHandler.handle_one_request method which calls this method to understand how the return value will be handled. """ # We hook parse_request method, but also call the original # CGIHTTPRequestHandler.parse_request since when we return False, # CGIHTTPRequestHandler.handle_one_request continues processing and # it needs variables set by CGIHTTPRequestHandler.parse_request. # # Variables set by this method will be also used by WebSocket request # handling (self.path, self.command, self.requestline, etc. See also # how _StandaloneRequest's members are implemented using these # attributes). if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self): return False if self._options.use_basic_auth: auth = self.headers.get('Authorization') if auth != self._options.basic_auth_credential: self.send_response(401) self.send_header('WWW-Authenticate', 'Basic realm="Pywebsocket"') self.end_headers() self._logger.info('Request basic authentication') return False host, port, resource = http_header_util.parse_uri(self.path) if resource is None: self._logger.info('Invalid URI: %r', self.path) self._logger.info('Fallback to CGIHTTPRequestHandler') return True server_options = self.server.websocket_server_options if host is not None: validation_host = server_options.validation_host if validation_host is not None and host != validation_host: self._logger.info('Invalid host: %r (expected: %r)', host, validation_host) self._logger.info('Fallback to CGIHTTPRequestHandler') return True if port is not None: validation_port = server_options.validation_port if validation_port is not None and port != validation_port: self._logger.info('Invalid port: %r (expected: %r)', port, validation_port) self._logger.info('Fallback to CGIHTTPRequestHandler') return True self.path = resource request = _StandaloneRequest(self, self._options.use_tls) try: # Fallback to default http handler for request paths for which # we don't have request handlers. if not self._options.dispatcher.get_handler_suite(self.path): self._logger.info('No handler for resource: %r', self.path) self._logger.info('Fallback to CGIHTTPRequestHandler') return True except dispatch.DispatchException as e: self._logger.info('Dispatch failed for error: %s', e) self.send_error(e.status) return False # If any Exceptions without except clause setup (including # DispatchException) is raised below this point, it will be caught # and logged by WebSocketServer. try: try: handshake.do_handshake(request, self._options.dispatcher) except handshake.VersionException as e: self._logger.info('Handshake failed for version error: %s', e) self.send_response(common.HTTP_STATUS_BAD_REQUEST) self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER, e.supported_versions) self.end_headers() return False except handshake.HandshakeException as e: # Handshake for ws(s) failed. self._logger.info('Handshake failed for error: %s', e) self.send_error(e.status) return False request._dispatcher = self._options.dispatcher self._options.dispatcher.transfer_data(request) except handshake.AbortedByUserException as e: self._logger.info('Aborted: %s', e) return False def log_request(self, code='-', size='-'): """Override BaseHTTPServer.log_request.""" self._logger.info('"%s" %s %s', self.requestline, str(code), str(size)) def log_error(self, *args): """Override BaseHTTPServer.log_error.""" # Despite the name, this method is for warnings than for errors. # For example, HTTP status code is logged by this method. self._logger.warning('%s - %s', self.address_string(), args[0] % args[1:]) def is_cgi(self): """Test whether self.path corresponds to a CGI script. Add extra check that self.path doesn't contains .. Also check if the file is a executable file or not. If the file is not executable, it is handled as static file or dir rather than a CGI script. """ if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self): if '..' in self.path: return False # strip query parameter from request path resource_name = self.path.split('?', 2)[0] # convert resource_name into real path name in filesystem. scriptfile = self.translate_path(resource_name) if not os.path.isfile(scriptfile): return False if not self.is_executable(scriptfile): return False return True return False # vi:sts=4 sw=4 et