request_handler.py (12195B)
1 # Copyright 2020, Google Inc. 2 # All rights reserved. 3 # 4 # Redistribution and use in source and binary forms, with or without 5 # modification, are permitted provided that the following conditions are 6 # met: 7 # 8 # * Redistributions of source code must retain the above copyright 9 # notice, this list of conditions and the following disclaimer. 10 # * Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following disclaimer 12 # in the documentation and/or other materials provided with the 13 # distribution. 14 # * Neither the name of Google Inc. nor the names of its 15 # contributors may be used to endorse or promote products derived from 16 # this software without specific prior written permission. 17 # 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 """Request Handler and Request/Connection classes for standalone server. 30 """ 31 32 import os 33 34 from six.moves import CGIHTTPServer 35 from six.moves import http_client 36 37 from mod_pywebsocket import common 38 from mod_pywebsocket import dispatch 39 from mod_pywebsocket import handshake 40 from mod_pywebsocket import http_header_util 41 from mod_pywebsocket import memorizingfile 42 from mod_pywebsocket import util 43 44 # 1024 is practically large enough to contain WebSocket handshake lines. 45 _MAX_MEMORIZED_LINES = 1024 46 47 48 class _StandaloneConnection(object): 49 """Mimic mod_python mp_conn.""" 50 def __init__(self, request_handler): 51 """Construct an instance. 52 53 Args: 54 request_handler: A WebSocketRequestHandler instance. 55 """ 56 57 self._request_handler = request_handler 58 59 def get_local_addr(self): 60 """Getter to mimic mp_conn.local_addr.""" 61 62 return (self._request_handler.server.server_name, 63 self._request_handler.server.server_port) 64 65 local_addr = property(get_local_addr) 66 67 def get_remote_addr(self): 68 """Getter to mimic mp_conn.remote_addr. 69 70 Setting the property in __init__ won't work because the request 71 handler is not initialized yet there.""" 72 73 return self._request_handler.client_address 74 75 remote_addr = property(get_remote_addr) 76 77 def write(self, data): 78 """Mimic mp_conn.write().""" 79 80 return self._request_handler.wfile.write(data) 81 82 def read(self, length): 83 """Mimic mp_conn.read().""" 84 85 return self._request_handler.rfile.read(length) 86 87 def get_memorized_lines(self): 88 """Get memorized lines.""" 89 90 return self._request_handler.rfile.get_memorized_lines() 91 92 93 class _StandaloneRequest(object): 94 """Mimic mod_python request.""" 95 def __init__(self, request_handler, use_tls): 96 """Construct an instance. 97 98 Args: 99 request_handler: A WebSocketRequestHandler instance. 100 """ 101 102 self._logger = util.get_class_logger(self) 103 104 self._request_handler = request_handler 105 self.connection = _StandaloneConnection(request_handler) 106 self._use_tls = use_tls 107 self.headers_in = request_handler.headers 108 109 def get_uri(self): 110 """Getter to mimic request.uri. 111 112 This method returns the raw data at the Request-URI part of the 113 Request-Line, while the uri method on the request object of mod_python 114 returns the path portion after parsing the raw data. This behavior is 115 kept for compatibility. 116 """ 117 118 return self._request_handler.path 119 120 uri = property(get_uri) 121 122 def get_unparsed_uri(self): 123 """Getter to mimic request.unparsed_uri.""" 124 125 return self._request_handler.path 126 127 unparsed_uri = property(get_unparsed_uri) 128 129 def get_method(self): 130 """Getter to mimic request.method.""" 131 132 return self._request_handler.command 133 134 method = property(get_method) 135 136 def get_protocol(self): 137 """Getter to mimic request.protocol.""" 138 139 return self._request_handler.request_version 140 141 protocol = property(get_protocol) 142 143 def is_https(self): 144 """Mimic request.is_https().""" 145 146 return self._use_tls 147 148 149 class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): 150 """CGIHTTPRequestHandler specialized for WebSocket.""" 151 152 # Use httplib.HTTPMessage instead of mimetools.Message. 153 MessageClass = http_client.HTTPMessage 154 155 def setup(self): 156 """Override SocketServer.StreamRequestHandler.setup to wrap rfile 157 with MemorizingFile. 158 159 This method will be called by BaseRequestHandler's constructor 160 before calling BaseHTTPRequestHandler.handle. 161 BaseHTTPRequestHandler.handle will call 162 BaseHTTPRequestHandler.handle_one_request and it will call 163 WebSocketRequestHandler.parse_request. 164 """ 165 166 # Call superclass's setup to prepare rfile, wfile, etc. See setup 167 # definition on the root class SocketServer.StreamRequestHandler to 168 # understand what this does. 169 CGIHTTPServer.CGIHTTPRequestHandler.setup(self) 170 171 self.rfile = memorizingfile.MemorizingFile( 172 self.rfile, max_memorized_lines=_MAX_MEMORIZED_LINES) 173 174 def __init__(self, request, client_address, server): 175 self._logger = util.get_class_logger(self) 176 177 self._options = server.websocket_server_options 178 179 # Overrides CGIHTTPServerRequestHandler.cgi_directories. 180 self.cgi_directories = self._options.cgi_directories 181 # Replace CGIHTTPRequestHandler.is_executable method. 182 if self._options.is_executable_method is not None: 183 self.is_executable = self._options.is_executable_method 184 185 # This actually calls BaseRequestHandler.__init__. 186 CGIHTTPServer.CGIHTTPRequestHandler.__init__(self, request, 187 client_address, server) 188 189 def parse_request(self): 190 """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request. 191 192 Return True to continue processing for HTTP(S), False otherwise. 193 194 See BaseHTTPRequestHandler.handle_one_request method which calls 195 this method to understand how the return value will be handled. 196 """ 197 198 # We hook parse_request method, but also call the original 199 # CGIHTTPRequestHandler.parse_request since when we return False, 200 # CGIHTTPRequestHandler.handle_one_request continues processing and 201 # it needs variables set by CGIHTTPRequestHandler.parse_request. 202 # 203 # Variables set by this method will be also used by WebSocket request 204 # handling (self.path, self.command, self.requestline, etc. See also 205 # how _StandaloneRequest's members are implemented using these 206 # attributes). 207 if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self): 208 return False 209 210 if self._options.use_basic_auth: 211 auth = self.headers.get('Authorization') 212 if auth != self._options.basic_auth_credential: 213 self.send_response(401) 214 self.send_header('WWW-Authenticate', 215 'Basic realm="Pywebsocket"') 216 self.end_headers() 217 self._logger.info('Request basic authentication') 218 return False 219 220 host, port, resource = http_header_util.parse_uri(self.path) 221 if resource is None: 222 self._logger.info('Invalid URI: %r', self.path) 223 self._logger.info('Fallback to CGIHTTPRequestHandler') 224 return True 225 server_options = self.server.websocket_server_options 226 if host is not None: 227 validation_host = server_options.validation_host 228 if validation_host is not None and host != validation_host: 229 self._logger.info('Invalid host: %r (expected: %r)', host, 230 validation_host) 231 self._logger.info('Fallback to CGIHTTPRequestHandler') 232 return True 233 if port is not None: 234 validation_port = server_options.validation_port 235 if validation_port is not None and port != validation_port: 236 self._logger.info('Invalid port: %r (expected: %r)', port, 237 validation_port) 238 self._logger.info('Fallback to CGIHTTPRequestHandler') 239 return True 240 self.path = resource 241 242 request = _StandaloneRequest(self, self._options.use_tls) 243 244 try: 245 # Fallback to default http handler for request paths for which 246 # we don't have request handlers. 247 if not self._options.dispatcher.get_handler_suite(self.path): 248 self._logger.info('No handler for resource: %r', self.path) 249 self._logger.info('Fallback to CGIHTTPRequestHandler') 250 return True 251 except dispatch.DispatchException as e: 252 self._logger.info('Dispatch failed for error: %s', e) 253 self.send_error(e.status) 254 return False 255 256 # If any Exceptions without except clause setup (including 257 # DispatchException) is raised below this point, it will be caught 258 # and logged by WebSocketServer. 259 260 try: 261 try: 262 handshake.do_handshake(request, self._options.dispatcher) 263 except handshake.VersionException as e: 264 self._logger.info('Handshake failed for version error: %s', e) 265 self.send_response(common.HTTP_STATUS_BAD_REQUEST) 266 self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER, 267 e.supported_versions) 268 self.end_headers() 269 return False 270 except handshake.HandshakeException as e: 271 # Handshake for ws(s) failed. 272 self._logger.info('Handshake failed for error: %s', e) 273 self.send_error(e.status) 274 return False 275 276 request._dispatcher = self._options.dispatcher 277 self._options.dispatcher.transfer_data(request) 278 except handshake.AbortedByUserException as e: 279 self._logger.info('Aborted: %s', e) 280 return False 281 282 def log_request(self, code='-', size='-'): 283 """Override BaseHTTPServer.log_request.""" 284 285 self._logger.info('"%s" %s %s', self.requestline, str(code), str(size)) 286 287 def log_error(self, *args): 288 """Override BaseHTTPServer.log_error.""" 289 290 # Despite the name, this method is for warnings than for errors. 291 # For example, HTTP status code is logged by this method. 292 self._logger.warning('%s - %s', self.address_string(), 293 args[0] % args[1:]) 294 295 def is_cgi(self): 296 """Test whether self.path corresponds to a CGI script. 297 298 Add extra check that self.path doesn't contains .. 299 Also check if the file is a executable file or not. 300 If the file is not executable, it is handled as static file or dir 301 rather than a CGI script. 302 """ 303 304 if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self): 305 if '..' in self.path: 306 return False 307 # strip query parameter from request path 308 resource_name = self.path.split('?', 2)[0] 309 # convert resource_name into real path name in filesystem. 310 scriptfile = self.translate_path(resource_name) 311 if not os.path.isfile(scriptfile): 312 return False 313 if not self.is_executable(scriptfile): 314 return False 315 return True 316 return False 317 318 319 # vi:sts=4 sw=4 et