|
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 """WebSocket handshaking defined in draft-hixie-thewebsocketprotocol-75.""" |
|
32 |
|
33 |
|
34 # Note: request.connection.write is used in this module, even though mod_python |
|
35 # document says that it should be used only in connection handlers. |
|
36 # Unfortunately, we have no other options. For example, request.write is not |
|
37 # suitable because it doesn't allow direct raw bytes writing. |
|
38 |
|
39 |
|
40 import logging |
|
41 import re |
|
42 |
|
43 from mod_pywebsocket import common |
|
44 from mod_pywebsocket.stream import StreamHixie75 |
|
45 from mod_pywebsocket import util |
|
46 from mod_pywebsocket.handshake._base import HandshakeException |
|
47 from mod_pywebsocket.handshake._base import build_location |
|
48 from mod_pywebsocket.handshake._base import validate_subprotocol |
|
49 |
|
50 |
|
51 _MANDATORY_HEADERS = [ |
|
52 # key, expected value or None |
|
53 ['Upgrade', 'WebSocket'], |
|
54 ['Connection', 'Upgrade'], |
|
55 ['Host', None], |
|
56 ['Origin', None], |
|
57 ] |
|
58 |
|
59 _FIRST_FIVE_LINES = map(re.compile, [ |
|
60 r'^GET /[\S]* HTTP/1.1\r\n$', |
|
61 r'^Upgrade: WebSocket\r\n$', |
|
62 r'^Connection: Upgrade\r\n$', |
|
63 r'^Host: [\S]+\r\n$', |
|
64 r'^Origin: [\S]+\r\n$', |
|
65 ]) |
|
66 |
|
67 _SIXTH_AND_LATER = re.compile( |
|
68 r'^' |
|
69 r'(WebSocket-Protocol: [\x20-\x7e]+\r\n)?' |
|
70 r'(Cookie: [^\r]*\r\n)*' |
|
71 r'(Cookie2: [^\r]*\r\n)?' |
|
72 r'(Cookie: [^\r]*\r\n)*' |
|
73 r'\r\n') |
|
74 |
|
75 |
|
76 class Handshaker(object): |
|
77 """This class performs WebSocket handshake.""" |
|
78 |
|
79 def __init__(self, request, dispatcher, strict=False): |
|
80 """Construct an instance. |
|
81 |
|
82 Args: |
|
83 request: mod_python request. |
|
84 dispatcher: Dispatcher (dispatch.Dispatcher). |
|
85 strict: Strictly check handshake request. Default: False. |
|
86 If True, request.connection must provide get_memorized_lines |
|
87 method. |
|
88 |
|
89 Handshaker will add attributes such as ws_resource in performing |
|
90 handshake. |
|
91 """ |
|
92 |
|
93 self._logger = util.get_class_logger(self) |
|
94 |
|
95 self._request = request |
|
96 self._dispatcher = dispatcher |
|
97 self._strict = strict |
|
98 |
|
99 def do_handshake(self): |
|
100 """Perform WebSocket Handshake. |
|
101 |
|
102 On _request, we set |
|
103 ws_resource, ws_origin, ws_location, ws_protocol |
|
104 ws_challenge_md5: WebSocket handshake information. |
|
105 ws_stream: Frame generation/parsing class. |
|
106 ws_version: Protocol version. |
|
107 """ |
|
108 |
|
109 self._check_header_lines() |
|
110 self._set_resource() |
|
111 self._set_origin() |
|
112 self._set_location() |
|
113 self._set_subprotocol() |
|
114 self._set_protocol_version() |
|
115 |
|
116 self._dispatcher.do_extra_handshake(self._request) |
|
117 |
|
118 self._send_handshake() |
|
119 |
|
120 self._logger.debug('Sent opening handshake response') |
|
121 |
|
122 def _set_resource(self): |
|
123 self._request.ws_resource = self._request.uri |
|
124 |
|
125 def _set_origin(self): |
|
126 self._request.ws_origin = self._request.headers_in['Origin'] |
|
127 |
|
128 def _set_location(self): |
|
129 self._request.ws_location = build_location(self._request) |
|
130 |
|
131 def _set_subprotocol(self): |
|
132 subprotocol = self._request.headers_in.get('WebSocket-Protocol') |
|
133 if subprotocol is not None: |
|
134 validate_subprotocol(subprotocol, hixie=True) |
|
135 self._request.ws_protocol = subprotocol |
|
136 |
|
137 def _set_protocol_version(self): |
|
138 self._logger.debug('IETF Hixie 75 protocol') |
|
139 self._request.ws_version = common.VERSION_HIXIE75 |
|
140 self._request.ws_stream = StreamHixie75(self._request) |
|
141 |
|
142 def _sendall(self, data): |
|
143 self._request.connection.write(data) |
|
144 |
|
145 def _send_handshake(self): |
|
146 self._sendall('HTTP/1.1 101 Web Socket Protocol Handshake\r\n') |
|
147 self._sendall('Upgrade: WebSocket\r\n') |
|
148 self._sendall('Connection: Upgrade\r\n') |
|
149 self._sendall('WebSocket-Origin: %s\r\n' % self._request.ws_origin) |
|
150 self._sendall('WebSocket-Location: %s\r\n' % self._request.ws_location) |
|
151 if self._request.ws_protocol: |
|
152 self._sendall( |
|
153 'WebSocket-Protocol: %s\r\n' % self._request.ws_protocol) |
|
154 self._sendall('\r\n') |
|
155 |
|
156 def _check_header_lines(self): |
|
157 for key, expected_value in _MANDATORY_HEADERS: |
|
158 actual_value = self._request.headers_in.get(key) |
|
159 if not actual_value: |
|
160 raise HandshakeException('Header %s is not defined' % key) |
|
161 if expected_value: |
|
162 if actual_value != expected_value: |
|
163 raise HandshakeException( |
|
164 'Expected %r for header %s but found %r' % |
|
165 (expected_value, key, actual_value)) |
|
166 if self._strict: |
|
167 try: |
|
168 lines = self._request.connection.get_memorized_lines() |
|
169 except AttributeError, e: |
|
170 raise AttributeError( |
|
171 'Strict handshake is specified but the connection ' |
|
172 'doesn\'t provide get_memorized_lines()') |
|
173 self._check_first_lines(lines) |
|
174 |
|
175 def _check_first_lines(self, lines): |
|
176 if len(lines) < len(_FIRST_FIVE_LINES): |
|
177 raise HandshakeException('Too few header lines: %d' % len(lines)) |
|
178 for line, regexp in zip(lines, _FIRST_FIVE_LINES): |
|
179 if not regexp.search(line): |
|
180 raise HandshakeException( |
|
181 'Unexpected header: %r doesn\'t match %r' |
|
182 % (line, regexp.pattern)) |
|
183 sixth_and_later = ''.join(lines[5:]) |
|
184 if not _SIXTH_AND_LATER.search(sixth_and_later): |
|
185 raise HandshakeException( |
|
186 'Unexpected header: %r doesn\'t match %r' |
|
187 % (sixth_and_later, _SIXTH_AND_LATER.pattern)) |
|
188 |
|
189 |
|
190 # vi:sts=4 sw=4 et |