1# Copyright 2012, 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"""Common functions and exceptions used by WebSocket opening handshake 32processors. 33""" 34 35 36from mod_pywebsocket import common 37from mod_pywebsocket import http_header_util 38 39 40class AbortedByUserException(Exception): 41 """Exception for aborting a connection intentionally. 42 43 If this exception is raised in do_extra_handshake handler, the connection 44 will be abandoned. No other WebSocket or HTTP(S) handler will be invoked. 45 46 If this exception is raised in transfer_data_handler, the connection will 47 be closed without closing handshake. No other WebSocket or HTTP(S) handler 48 will be invoked. 49 """ 50 51 pass 52 53 54class HandshakeException(Exception): 55 """This exception will be raised when an error occurred while processing 56 WebSocket initial handshake. 57 """ 58 59 def __init__(self, name, status=None): 60 super(HandshakeException, self).__init__(name) 61 self.status = status 62 63 64class VersionException(Exception): 65 """This exception will be raised when a version of client request does not 66 match with version the server supports. 67 """ 68 69 def __init__(self, name, supported_versions=''): 70 """Construct an instance. 71 72 Args: 73 supported_version: a str object to show supported hybi versions. 74 (e.g. '8, 13') 75 """ 76 super(VersionException, self).__init__(name) 77 self.supported_versions = supported_versions 78 79 80def get_default_port(is_secure): 81 if is_secure: 82 return common.DEFAULT_WEB_SOCKET_SECURE_PORT 83 else: 84 return common.DEFAULT_WEB_SOCKET_PORT 85 86 87def validate_subprotocol(subprotocol, hixie): 88 """Validate a value in subprotocol fields such as WebSocket-Protocol, 89 Sec-WebSocket-Protocol. 90 91 See 92 - RFC 6455: Section 4.1., 4.2.2., and 4.3. 93 - HyBi 00: Section 4.1. Opening handshake 94 - Hixie 75: Section 4.1. Handshake 95 """ 96 97 if not subprotocol: 98 raise HandshakeException('Invalid subprotocol name: empty') 99 if hixie: 100 # Parameter should be in the range U+0020 to U+007E. 101 for c in subprotocol: 102 if not 0x20 <= ord(c) <= 0x7e: 103 raise HandshakeException( 104 'Illegal character in subprotocol name: %r' % c) 105 else: 106 # Parameter should be encoded HTTP token. 107 state = http_header_util.ParsingState(subprotocol) 108 token = http_header_util.consume_token(state) 109 rest = http_header_util.peek(state) 110 # If |rest| is not None, |subprotocol| is not one token or invalid. If 111 # |rest| is None, |token| must not be None because |subprotocol| is 112 # concatenation of |token| and |rest| and is not None. 113 if rest is not None: 114 raise HandshakeException('Invalid non-token string in subprotocol ' 115 'name: %r' % rest) 116 117 118def parse_host_header(request): 119 fields = request.headers_in['Host'].split(':', 1) 120 if len(fields) == 1: 121 return fields[0], get_default_port(request.is_https()) 122 try: 123 return fields[0], int(fields[1]) 124 except ValueError, e: 125 raise HandshakeException('Invalid port number format: %r' % e) 126 127 128def format_header(name, value): 129 return '%s: %s\r\n' % (name, value) 130 131 132def build_location(request): 133 """Build WebSocket location for request.""" 134 location_parts = [] 135 if request.is_https(): 136 location_parts.append(common.WEB_SOCKET_SECURE_SCHEME) 137 else: 138 location_parts.append(common.WEB_SOCKET_SCHEME) 139 location_parts.append('://') 140 host, port = parse_host_header(request) 141 connection_port = request.connection.local_addr[1] 142 if port != connection_port: 143 raise HandshakeException('Header/connection port mismatch: %d/%d' % 144 (port, connection_port)) 145 location_parts.append(host) 146 if (port != get_default_port(request.is_https())): 147 location_parts.append(':') 148 location_parts.append(str(port)) 149 location_parts.append(request.uri) 150 return ''.join(location_parts) 151 152 153def get_mandatory_header(request, key): 154 value = request.headers_in.get(key) 155 if value is None: 156 raise HandshakeException('Header %s is not defined' % key) 157 return value 158 159 160def validate_mandatory_header(request, key, expected_value, fail_status=None): 161 value = get_mandatory_header(request, key) 162 163 if value.lower() != expected_value.lower(): 164 raise HandshakeException( 165 'Expected %r for header %s but found %r (case-insensitive)' % 166 (expected_value, key, value), status=fail_status) 167 168 169def check_request_line(request): 170 # 5.1 1. The three character UTF-8 string "GET". 171 # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte). 172 if request.method != 'GET': 173 raise HandshakeException('Method is not GET') 174 175 176def check_header_lines(request, mandatory_headers): 177 check_request_line(request) 178 179 # The expected field names, and the meaning of their corresponding 180 # values, are as follows. 181 # |Upgrade| and |Connection| 182 for key, expected_value in mandatory_headers: 183 validate_mandatory_header(request, key, expected_value) 184 185 186def parse_token_list(data): 187 """Parses a header value which follows 1#token and returns parsed elements 188 as a list of strings. 189 190 Leading LWSes must be trimmed. 191 """ 192 193 state = http_header_util.ParsingState(data) 194 195 token_list = [] 196 197 while True: 198 token = http_header_util.consume_token(state) 199 if token is not None: 200 token_list.append(token) 201 202 http_header_util.consume_lwses(state) 203 204 if http_header_util.peek(state) is None: 205 break 206 207 if not http_header_util.consume_string(state, ','): 208 raise HandshakeException( 209 'Expected a comma but found %r' % http_header_util.peek(state)) 210 211 http_header_util.consume_lwses(state) 212 213 if len(token_list) == 0: 214 raise HandshakeException('No valid token found') 215 216 return token_list 217 218 219# vi:sts=4 sw=4 et 220