Keita Suzuki | e0d3b5a | 2020-02-18 15:33:23 +0900 | [diff] [blame] | 1 | # Copyright 2020, Google Inc. |
Keita Suzuki | 77233ae | 2020-02-04 12:55:08 +0900 | [diff] [blame] | 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 | """Standalone WebsocketServer |
| 30 | |
| 31 | This file deals with the main module of standalone server. Although it is fine |
| 32 | to import this file directly to use WebSocketServer, it is strongly recommended |
| 33 | to use standalone.py, since it is intended to act as a skeleton of this module. |
| 34 | """ |
| 35 | |
| 36 | from __future__ import absolute_import |
| 37 | from six.moves import BaseHTTPServer |
| 38 | from six.moves import socketserver |
| 39 | import logging |
| 40 | import re |
| 41 | import select |
| 42 | import socket |
| 43 | import ssl |
| 44 | import threading |
| 45 | import traceback |
| 46 | |
| 47 | from mod_pywebsocket import dispatch |
| 48 | from mod_pywebsocket import util |
| 49 | from mod_pywebsocket.request_handler import WebSocketRequestHandler |
| 50 | |
| 51 | |
| 52 | def _alias_handlers(dispatcher, websock_handlers_map_file): |
| 53 | """Set aliases specified in websock_handler_map_file in dispatcher. |
| 54 | |
| 55 | Args: |
| 56 | dispatcher: dispatch.Dispatcher instance |
| 57 | websock_handler_map_file: alias map file |
| 58 | """ |
| 59 | |
| 60 | with open(websock_handlers_map_file) as f: |
| 61 | for line in f: |
| 62 | if line[0] == '#' or line.isspace(): |
| 63 | continue |
Adam Rice | cdb83e3 | 2021-04-01 20:38:42 +0900 | [diff] [blame] | 64 | m = re.match(r'(\S+)\s+(\S+)$', line) |
Keita Suzuki | 77233ae | 2020-02-04 12:55:08 +0900 | [diff] [blame] | 65 | if not m: |
| 66 | logging.warning('Wrong format in map file:' + line) |
| 67 | continue |
| 68 | try: |
| 69 | dispatcher.add_resource_path_alias(m.group(1), m.group(2)) |
| 70 | except dispatch.DispatchException as e: |
| 71 | logging.error(str(e)) |
| 72 | |
| 73 | |
| 74 | class WebSocketServer(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer): |
| 75 | """HTTPServer specialized for WebSocket.""" |
| 76 | |
| 77 | # Overrides SocketServer.ThreadingMixIn.daemon_threads |
| 78 | daemon_threads = True |
| 79 | # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address |
| 80 | allow_reuse_address = True |
| 81 | |
| 82 | def __init__(self, options): |
| 83 | """Override SocketServer.TCPServer.__init__ to set SSL enabled |
| 84 | socket object to self.socket before server_bind and server_activate, |
| 85 | if necessary. |
| 86 | """ |
| 87 | |
Adam Rice | 50602a1 | 2021-08-20 16:16:08 +0900 | [diff] [blame^] | 88 | # Fall back to None for embedders that don't know about the |
| 89 | # handler_encoding option. |
| 90 | handler_encoding = getattr(options, "handler_encoding", None) |
| 91 | |
Keita Suzuki | 77233ae | 2020-02-04 12:55:08 +0900 | [diff] [blame] | 92 | # Share a Dispatcher among request handlers to save time for |
| 93 | # instantiation. Dispatcher can be shared because it is thread-safe. |
| 94 | options.dispatcher = dispatch.Dispatcher( |
| 95 | options.websock_handlers, options.scan_dir, |
Adam Rice | 50602a1 | 2021-08-20 16:16:08 +0900 | [diff] [blame^] | 96 | options.allow_handlers_outside_root_dir, handler_encoding) |
Keita Suzuki | 77233ae | 2020-02-04 12:55:08 +0900 | [diff] [blame] | 97 | if options.websock_handlers_map_file: |
| 98 | _alias_handlers(options.dispatcher, |
| 99 | options.websock_handlers_map_file) |
| 100 | warnings = options.dispatcher.source_warnings() |
| 101 | if warnings: |
| 102 | for warning in warnings: |
| 103 | logging.warning('Warning in source loading: %s' % warning) |
| 104 | |
| 105 | self._logger = util.get_class_logger(self) |
| 106 | |
| 107 | self.request_queue_size = options.request_queue_size |
| 108 | self.__ws_is_shut_down = threading.Event() |
| 109 | self.__ws_serving = False |
| 110 | |
| 111 | socketserver.BaseServer.__init__(self, |
| 112 | (options.server_host, options.port), |
| 113 | WebSocketRequestHandler) |
| 114 | |
| 115 | # Expose the options object to allow handler objects access it. We name |
| 116 | # it with websocket_ prefix to avoid conflict. |
| 117 | self.websocket_server_options = options |
| 118 | |
| 119 | self._create_sockets() |
| 120 | self.server_bind() |
| 121 | self.server_activate() |
| 122 | |
| 123 | def _create_sockets(self): |
| 124 | self.server_name, self.server_port = self.server_address |
| 125 | self._sockets = [] |
| 126 | if not self.server_name: |
| 127 | # On platforms that doesn't support IPv6, the first bind fails. |
| 128 | # On platforms that supports IPv6 |
| 129 | # - If it binds both IPv4 and IPv6 on call with AF_INET6, the |
| 130 | # first bind succeeds and the second fails (we'll see 'Address |
| 131 | # already in use' error). |
| 132 | # - If it binds only IPv6 on call with AF_INET6, both call are |
| 133 | # expected to succeed to listen both protocol. |
| 134 | addrinfo_array = [(socket.AF_INET6, socket.SOCK_STREAM, '', '', |
| 135 | ''), |
| 136 | (socket.AF_INET, socket.SOCK_STREAM, '', '', '')] |
| 137 | else: |
| 138 | addrinfo_array = socket.getaddrinfo(self.server_name, |
| 139 | self.server_port, |
| 140 | socket.AF_UNSPEC, |
| 141 | socket.SOCK_STREAM, |
| 142 | socket.IPPROTO_TCP) |
| 143 | for addrinfo in addrinfo_array: |
| 144 | self._logger.info('Create socket on: %r', addrinfo) |
| 145 | family, socktype, proto, canonname, sockaddr = addrinfo |
| 146 | try: |
| 147 | socket_ = socket.socket(family, socktype) |
| 148 | except Exception as e: |
| 149 | self._logger.info('Skip by failure: %r', e) |
| 150 | continue |
| 151 | server_options = self.websocket_server_options |
| 152 | if server_options.use_tls: |
| 153 | if server_options.tls_client_auth: |
| 154 | if server_options.tls_client_cert_optional: |
| 155 | client_cert_ = ssl.CERT_OPTIONAL |
| 156 | else: |
| 157 | client_cert_ = ssl.CERT_REQUIRED |
| 158 | else: |
| 159 | client_cert_ = ssl.CERT_NONE |
| 160 | socket_ = ssl.wrap_socket( |
| 161 | socket_, |
| 162 | keyfile=server_options.private_key, |
| 163 | certfile=server_options.certificate, |
| 164 | ca_certs=server_options.tls_client_ca, |
| 165 | cert_reqs=client_cert_) |
| 166 | self._sockets.append((socket_, addrinfo)) |
| 167 | |
| 168 | def server_bind(self): |
| 169 | """Override SocketServer.TCPServer.server_bind to enable multiple |
| 170 | sockets bind. |
| 171 | """ |
| 172 | |
| 173 | failed_sockets = [] |
| 174 | |
| 175 | for socketinfo in self._sockets: |
| 176 | socket_, addrinfo = socketinfo |
| 177 | self._logger.info('Bind on: %r', addrinfo) |
| 178 | if self.allow_reuse_address: |
| 179 | socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 180 | try: |
| 181 | socket_.bind(self.server_address) |
| 182 | except Exception as e: |
| 183 | self._logger.info('Skip by failure: %r', e) |
| 184 | socket_.close() |
| 185 | failed_sockets.append(socketinfo) |
| 186 | if self.server_address[1] == 0: |
| 187 | # The operating system assigns the actual port number for port |
| 188 | # number 0. This case, the second and later sockets should use |
| 189 | # the same port number. Also self.server_port is rewritten |
| 190 | # because it is exported, and will be used by external code. |
| 191 | self.server_address = (self.server_name, |
| 192 | socket_.getsockname()[1]) |
| 193 | self.server_port = self.server_address[1] |
| 194 | self._logger.info('Port %r is assigned', self.server_port) |
| 195 | |
| 196 | for socketinfo in failed_sockets: |
| 197 | self._sockets.remove(socketinfo) |
| 198 | |
| 199 | def server_activate(self): |
| 200 | """Override SocketServer.TCPServer.server_activate to enable multiple |
| 201 | sockets listen. |
| 202 | """ |
| 203 | |
| 204 | failed_sockets = [] |
| 205 | |
| 206 | for socketinfo in self._sockets: |
| 207 | socket_, addrinfo = socketinfo |
| 208 | self._logger.info('Listen on: %r', addrinfo) |
| 209 | try: |
| 210 | socket_.listen(self.request_queue_size) |
| 211 | except Exception as e: |
| 212 | self._logger.info('Skip by failure: %r', e) |
| 213 | socket_.close() |
| 214 | failed_sockets.append(socketinfo) |
| 215 | |
| 216 | for socketinfo in failed_sockets: |
| 217 | self._sockets.remove(socketinfo) |
| 218 | |
| 219 | if len(self._sockets) == 0: |
| 220 | self._logger.critical( |
| 221 | 'No sockets activated. Use info log level to see the reason.') |
| 222 | |
| 223 | def server_close(self): |
| 224 | """Override SocketServer.TCPServer.server_close to enable multiple |
| 225 | sockets close. |
| 226 | """ |
| 227 | |
| 228 | for socketinfo in self._sockets: |
| 229 | socket_, addrinfo = socketinfo |
| 230 | self._logger.info('Close on: %r', addrinfo) |
| 231 | socket_.close() |
| 232 | |
| 233 | def fileno(self): |
| 234 | """Override SocketServer.TCPServer.fileno.""" |
| 235 | |
| 236 | self._logger.critical('Not supported: fileno') |
| 237 | return self._sockets[0][0].fileno() |
| 238 | |
| 239 | def handle_error(self, request, client_address): |
| 240 | """Override SocketServer.handle_error.""" |
| 241 | |
| 242 | self._logger.error('Exception in processing request from: %r\n%s', |
| 243 | client_address, traceback.format_exc()) |
| 244 | # Note: client_address is a tuple. |
| 245 | |
| 246 | def get_request(self): |
| 247 | """Override TCPServer.get_request.""" |
| 248 | |
| 249 | accepted_socket, client_address = self.socket.accept() |
| 250 | |
| 251 | server_options = self.websocket_server_options |
| 252 | if server_options.use_tls: |
| 253 | # Print cipher in use. Handshake is done on accept. |
| 254 | self._logger.debug('Cipher: %s', accepted_socket.cipher()) |
| 255 | self._logger.debug('Client cert: %r', |
| 256 | accepted_socket.getpeercert()) |
| 257 | |
| 258 | return accepted_socket, client_address |
| 259 | |
| 260 | def serve_forever(self, poll_interval=0.5): |
| 261 | """Override SocketServer.BaseServer.serve_forever.""" |
| 262 | |
| 263 | self.__ws_serving = True |
| 264 | self.__ws_is_shut_down.clear() |
| 265 | handle_request = self.handle_request |
| 266 | if hasattr(self, '_handle_request_noblock'): |
| 267 | handle_request = self._handle_request_noblock |
| 268 | else: |
| 269 | self._logger.warning('Fallback to blocking request handler') |
| 270 | try: |
| 271 | while self.__ws_serving: |
| 272 | r, w, e = select.select( |
| 273 | [socket_[0] for socket_ in self._sockets], [], [], |
| 274 | poll_interval) |
| 275 | for socket_ in r: |
| 276 | self.socket = socket_ |
| 277 | handle_request() |
| 278 | self.socket = None |
| 279 | finally: |
| 280 | self.__ws_is_shut_down.set() |
| 281 | |
| 282 | def shutdown(self): |
| 283 | """Override SocketServer.BaseServer.shutdown.""" |
| 284 | |
| 285 | self.__ws_serving = False |
| 286 | self.__ws_is_shut_down.wait() |
| 287 | |
| 288 | |
| 289 | # vi:sts=4 sw=4 et |