1 # Copyright 2011, 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 30 31 """PythonHeaderParserHandler for mod_pywebsocket. 32 33 Apache HTTP Server and mod_python must be configured such that this 34 function is called to handle WebSocket request. 35 """ 36 37 38 import logging 39 40 from mod_python import apache 41 42 from mod_pywebsocket import common 43 from mod_pywebsocket import dispatch 44 from mod_pywebsocket import handshake 45 from mod_pywebsocket import util 46 47 48 # PythonOption to specify the handler root directory. 49 _PYOPT_HANDLER_ROOT = 'mod_pywebsocket.handler_root' 50 51 # PythonOption to specify the handler scan directory. 52 # This must be a directory under the root directory. 53 # The default is the root directory. 54 _PYOPT_HANDLER_SCAN = 'mod_pywebsocket.handler_scan' 55 56 # PythonOption to allow handlers whose canonical path is 57 # not under the root directory. It's disallowed by default. 58 # Set this option with value of 'yes' to allow. 59 _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT = ( 60 'mod_pywebsocket.allow_handlers_outside_root_dir') 61 # Map from values to their meanings. 'Yes' and 'No' are allowed just for 62 # compatibility. 63 _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION = { 64 'off': False, 'no': False, 'on': True, 'yes': True} 65 66 # (Obsolete option. Ignored.) 67 # PythonOption to specify to allow handshake defined in Hixie 75 version 68 # protocol. The default is None (Off) 69 _PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75' 70 # Map from values to their meanings. 71 _PYOPT_ALLOW_DRAFT75_DEFINITION = {'off': False, 'on': True} 72 73 74 class ApacheLogHandler(logging.Handler): 75 """Wrapper logging.Handler to emit log message to apache's error.log.""" 76 77 _LEVELS = { 78 logging.DEBUG: apache.APLOG_DEBUG, 79 logging.INFO: apache.APLOG_INFO, 80 logging.WARNING: apache.APLOG_WARNING, 81 logging.ERROR: apache.APLOG_ERR, 82 logging.CRITICAL: apache.APLOG_CRIT, 83 } 84 85 def __init__(self, request=None): 86 logging.Handler.__init__(self) 87 self._log_error = apache.log_error 88 if request is not None: 89 self._log_error = request.log_error 90 91 # Time and level will be printed by Apache. 92 self._formatter = logging.Formatter('%(name)s: %(message)s') 93 94 def emit(self, record): 95 apache_level = apache.APLOG_DEBUG 96 if record.levelno in ApacheLogHandler._LEVELS: 97 apache_level = ApacheLogHandler._LEVELS[record.levelno] 98 99 msg = self._formatter.format(record) 100 101 # "server" parameter must be passed to have "level" parameter work. 102 # If only "level" parameter is passed, nothing shows up on Apache's 103 # log. However, at this point, we cannot get the server object of the 104 # virtual host which will process WebSocket requests. The only server 105 # object we can get here is apache.main_server. But Wherever (server 106 # configuration context or virtual host context) we put 107 # PythonHeaderParserHandler directive, apache.main_server just points 108 # the main server instance (not any of virtual server instance). Then, 109 # Apache follows LogLevel directive in the server configuration context 110 # to filter logs. So, we need to specify LogLevel in the server 111 # configuration context. Even if we specify "LogLevel debug" in the 112 # virtual host context which actually handles WebSocket connections, 113 # DEBUG level logs never show up unless "LogLevel debug" is specified 114 # in the server configuration context. 115 # 116 # TODO(tyoshino): Provide logging methods on request object. When 117 # request is mp_request object (when used together with Apache), the 118 # methods call request.log_error indirectly. When request is 119 # _StandaloneRequest, the methods call Python's logging facility which 120 # we create in standalone.py. 121 self._log_error(msg, apache_level, apache.main_server) 122 123 124 def _configure_logging(): 125 logger = logging.getLogger() 126 # Logs are filtered by Apache based on LogLevel directive in Apache 127 # configuration file. We must just pass logs for all levels to 128 # ApacheLogHandler. 129 logger.setLevel(logging.DEBUG) 130 logger.addHandler(ApacheLogHandler()) 131 132 133 _configure_logging() 134 135 _LOGGER = logging.getLogger(__name__) 136 137 138 def _parse_option(name, value, definition): 139 if value is None: 140 return False 141 142 meaning = definition.get(value.lower()) 143 if meaning is None: 144 raise Exception('Invalid value for PythonOption %s: %r' % 145 (name, value)) 146 return meaning 147 148 149 def _create_dispatcher(): 150 _LOGGER.info('Initializing Dispatcher') 151 152 options = apache.main_server.get_options() 153 154 handler_root = options.get(_PYOPT_HANDLER_ROOT, None) 155 if not handler_root: 156 raise Exception('PythonOption %s is not defined' % _PYOPT_HANDLER_ROOT, 157 apache.APLOG_ERR) 158 159 handler_scan = options.get(_PYOPT_HANDLER_SCAN, handler_root) 160 161 allow_handlers_outside_root = _parse_option( 162 _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT, 163 options.get(_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT), 164 _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION) 165 166 dispatcher = dispatch.Dispatcher( 167 handler_root, handler_scan, allow_handlers_outside_root) 168 169 for warning in dispatcher.source_warnings(): 170 apache.log_error( 171 'mod_pywebsocket: Warning in source loading: %s' % warning, 172 apache.APLOG_WARNING) 173 174 return dispatcher 175 176 177 # Initialize 178 _dispatcher = _create_dispatcher() 179 180 181 def headerparserhandler(request): 182 """Handle request. 183 184 Args: 185 request: mod_python request. 186 187 This function is named headerparserhandler because it is the default 188 name for a PythonHeaderParserHandler. 189 """ 190 191 handshake_is_done = False 192 try: 193 # Fallback to default http handler for request paths for which 194 # we don't have request handlers. 195 if not _dispatcher.get_handler_suite(request.uri): 196 request.log_error( 197 'mod_pywebsocket: No handler for resource: %r' % request.uri, 198 apache.APLOG_INFO) 199 request.log_error( 200 'mod_pywebsocket: Fallback to Apache', apache.APLOG_INFO) 201 return apache.DECLINED 202 except dispatch.DispatchException, e: 203 request.log_error( 204 'mod_pywebsocket: Dispatch failed for error: %s' % e, 205 apache.APLOG_INFO) 206 if not handshake_is_done: 207 return e.status 208 209 try: 210 allow_draft75 = _parse_option( 211 _PYOPT_ALLOW_DRAFT75, 212 apache.main_server.get_options().get(_PYOPT_ALLOW_DRAFT75), 213 _PYOPT_ALLOW_DRAFT75_DEFINITION) 214 215 try: 216 handshake.do_handshake( 217 request, _dispatcher, allowDraft75=allow_draft75) 218 except handshake.VersionException, e: 219 request.log_error( 220 'mod_pywebsocket: Handshake failed for version error: %s' % e, 221 apache.APLOG_INFO) 222 request.err_headers_out.add(common.SEC_WEBSOCKET_VERSION_HEADER, 223 e.supported_versions) 224 return apache.HTTP_BAD_REQUEST 225 except handshake.HandshakeException, e: 226 # Handshake for ws/wss failed. 227 # Send http response with error status. 228 request.log_error( 229 'mod_pywebsocket: Handshake failed for error: %s' % e, 230 apache.APLOG_INFO) 231 return e.status 232 233 handshake_is_done = True 234 request._dispatcher = _dispatcher 235 _dispatcher.transfer_data(request) 236 except handshake.AbortedByUserException, e: 237 request.log_error('mod_pywebsocket: Aborted: %s' % e, apache.APLOG_INFO) 238 except Exception, e: 239 # DispatchException can also be thrown if something is wrong in 240 # pywebsocket code. It's caught here, then. 241 242 request.log_error('mod_pywebsocket: Exception occurred: %s\n%s' % 243 (e, util.get_stack_trace()), 244 apache.APLOG_ERR) 245 # Unknown exceptions before handshake mean Apache must handle its 246 # request with another handler. 247 if not handshake_is_done: 248 return apache.DECLINED 249 # Set assbackwards to suppress response header generation by Apache. 250 request.assbackwards = 1 251 return apache.DONE # Return DONE such that no other handlers are invoked. 252 253 254 # vi:sts=4 sw=4 et 255