|
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 """This file provides the opening handshake processor for the WebSocket |
|
32 protocol version HyBi 00. |
|
33 |
|
34 Specification: |
|
35 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 |
|
36 """ |
|
37 |
|
38 |
|
39 # Note: request.connection.write/read are used in this module, even though |
|
40 # mod_python document says that they should be used only in connection |
|
41 # handlers. Unfortunately, we have no other options. For example, |
|
42 # request.write/read are not suitable because they don't allow direct raw bytes |
|
43 # writing/reading. |
|
44 |
|
45 |
|
46 import logging |
|
47 import re |
|
48 import struct |
|
49 |
|
50 from mod_pywebsocket import common |
|
51 from mod_pywebsocket.stream import StreamHixie75 |
|
52 from mod_pywebsocket import util |
|
53 from mod_pywebsocket.handshake._base import HandshakeException |
|
54 from mod_pywebsocket.handshake._base import build_location |
|
55 from mod_pywebsocket.handshake._base import check_header_lines |
|
56 from mod_pywebsocket.handshake._base import format_header |
|
57 from mod_pywebsocket.handshake._base import get_mandatory_header |
|
58 from mod_pywebsocket.handshake._base import validate_subprotocol |
|
59 |
|
60 |
|
61 _MANDATORY_HEADERS = [ |
|
62 # key, expected value or None |
|
63 [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75], |
|
64 [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE], |
|
65 ] |
|
66 |
|
67 |
|
68 class Handshaker(object): |
|
69 """Opening handshake processor for the WebSocket protocol version HyBi 00. |
|
70 """ |
|
71 |
|
72 def __init__(self, request, dispatcher): |
|
73 """Construct an instance. |
|
74 |
|
75 Args: |
|
76 request: mod_python request. |
|
77 dispatcher: Dispatcher (dispatch.Dispatcher). |
|
78 |
|
79 Handshaker will add attributes such as ws_resource in performing |
|
80 handshake. |
|
81 """ |
|
82 |
|
83 self._logger = util.get_class_logger(self) |
|
84 |
|
85 self._request = request |
|
86 self._dispatcher = dispatcher |
|
87 |
|
88 def do_handshake(self): |
|
89 """Perform WebSocket Handshake. |
|
90 |
|
91 On _request, we set |
|
92 ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge, |
|
93 ws_challenge_md5: WebSocket handshake information. |
|
94 ws_stream: Frame generation/parsing class. |
|
95 ws_version: Protocol version. |
|
96 |
|
97 Raises: |
|
98 HandshakeException: when any error happened in parsing the opening |
|
99 handshake request. |
|
100 """ |
|
101 |
|
102 # 5.1 Reading the client's opening handshake. |
|
103 # dispatcher sets it in self._request. |
|
104 check_header_lines(self._request, _MANDATORY_HEADERS) |
|
105 self._set_resource() |
|
106 self._set_subprotocol() |
|
107 self._set_location() |
|
108 self._set_origin() |
|
109 self._set_challenge_response() |
|
110 self._set_protocol_version() |
|
111 |
|
112 self._dispatcher.do_extra_handshake(self._request) |
|
113 |
|
114 self._send_handshake() |
|
115 |
|
116 def _set_resource(self): |
|
117 self._request.ws_resource = self._request.uri |
|
118 |
|
119 def _set_subprotocol(self): |
|
120 # |Sec-WebSocket-Protocol| |
|
121 subprotocol = self._request.headers_in.get( |
|
122 common.SEC_WEBSOCKET_PROTOCOL_HEADER) |
|
123 if subprotocol is not None: |
|
124 validate_subprotocol(subprotocol, hixie=True) |
|
125 self._request.ws_protocol = subprotocol |
|
126 |
|
127 def _set_location(self): |
|
128 # |Host| |
|
129 host = self._request.headers_in.get(common.HOST_HEADER) |
|
130 if host is not None: |
|
131 self._request.ws_location = build_location(self._request) |
|
132 # TODO(ukai): check host is this host. |
|
133 |
|
134 def _set_origin(self): |
|
135 # |Origin| |
|
136 origin = self._request.headers_in.get(common.ORIGIN_HEADER) |
|
137 if origin is not None: |
|
138 self._request.ws_origin = origin |
|
139 |
|
140 def _set_protocol_version(self): |
|
141 # |Sec-WebSocket-Draft| |
|
142 draft = self._request.headers_in.get(common.SEC_WEBSOCKET_DRAFT_HEADER) |
|
143 if draft is not None and draft != '0': |
|
144 raise HandshakeException('Illegal value for %s: %s' % |
|
145 (common.SEC_WEBSOCKET_DRAFT_HEADER, |
|
146 draft)) |
|
147 |
|
148 self._logger.debug('Protocol version is HyBi 00') |
|
149 self._request.ws_version = common.VERSION_HYBI00 |
|
150 self._request.ws_stream = StreamHixie75(self._request, True) |
|
151 |
|
152 def _set_challenge_response(self): |
|
153 # 5.2 4-8. |
|
154 self._request.ws_challenge = self._get_challenge() |
|
155 # 5.2 9. let /response/ be the MD5 finterprint of /challenge/ |
|
156 self._request.ws_challenge_md5 = util.md5_hash( |
|
157 self._request.ws_challenge).digest() |
|
158 self._logger.debug( |
|
159 'Challenge: %r (%s)', |
|
160 self._request.ws_challenge, |
|
161 util.hexify(self._request.ws_challenge)) |
|
162 self._logger.debug( |
|
163 'Challenge response: %r (%s)', |
|
164 self._request.ws_challenge_md5, |
|
165 util.hexify(self._request.ws_challenge_md5)) |
|
166 |
|
167 def _get_key_value(self, key_field): |
|
168 key_value = get_mandatory_header(self._request, key_field) |
|
169 |
|
170 self._logger.debug('%s: %r', key_field, key_value) |
|
171 |
|
172 # 5.2 4. let /key-number_n/ be the digits (characters in the range |
|
173 # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/, |
|
174 # interpreted as a base ten integer, ignoring all other characters |
|
175 # in /key_n/. |
|
176 try: |
|
177 key_number = int(re.sub("\\D", "", key_value)) |
|
178 except: |
|
179 raise HandshakeException('%s field contains no digit' % key_field) |
|
180 # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters |
|
181 # in /key_n/. |
|
182 spaces = re.subn(" ", "", key_value)[1] |
|
183 if spaces == 0: |
|
184 raise HandshakeException('%s field contains no space' % key_field) |
|
185 |
|
186 self._logger.debug( |
|
187 '%s: Key-number is %d and number of spaces is %d', |
|
188 key_field, key_number, spaces) |
|
189 |
|
190 # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/ |
|
191 # then abort the WebSocket connection. |
|
192 if key_number % spaces != 0: |
|
193 raise HandshakeException( |
|
194 '%s: Key-number (%d) is not an integral multiple of spaces ' |
|
195 '(%d)' % (key_field, key_number, spaces)) |
|
196 # 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/. |
|
197 part = key_number / spaces |
|
198 self._logger.debug('%s: Part is %d', key_field, part) |
|
199 return part |
|
200 |
|
201 def _get_challenge(self): |
|
202 # 5.2 4-7. |
|
203 key1 = self._get_key_value(common.SEC_WEBSOCKET_KEY1_HEADER) |
|
204 key2 = self._get_key_value(common.SEC_WEBSOCKET_KEY2_HEADER) |
|
205 # 5.2 8. let /challenge/ be the concatenation of /part_1/, |
|
206 challenge = '' |
|
207 challenge += struct.pack('!I', key1) # network byteorder int |
|
208 challenge += struct.pack('!I', key2) # network byteorder int |
|
209 challenge += self._request.connection.read(8) |
|
210 return challenge |
|
211 |
|
212 def _send_handshake(self): |
|
213 response = [] |
|
214 |
|
215 # 5.2 10. send the following line. |
|
216 response.append('HTTP/1.1 101 WebSocket Protocol Handshake\r\n') |
|
217 |
|
218 # 5.2 11. send the following fields to the client. |
|
219 response.append(format_header( |
|
220 common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75)) |
|
221 response.append(format_header( |
|
222 common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) |
|
223 response.append(format_header( |
|
224 common.SEC_WEBSOCKET_LOCATION_HEADER, self._request.ws_location)) |
|
225 response.append(format_header( |
|
226 common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin)) |
|
227 if self._request.ws_protocol: |
|
228 response.append(format_header( |
|
229 common.SEC_WEBSOCKET_PROTOCOL_HEADER, |
|
230 self._request.ws_protocol)) |
|
231 # 5.2 12. send two bytes 0x0D 0x0A. |
|
232 response.append('\r\n') |
|
233 # 5.2 13. send /response/ |
|
234 response.append(self._request.ws_challenge_md5) |
|
235 |
|
236 raw_response = ''.join(response) |
|
237 self._request.connection.write(raw_response) |
|
238 self._logger.debug('Sent server\'s opening handshake: %r', |
|
239 raw_response) |
|
240 |
|
241 |
|
242 # vi:sts=4 sw=4 et |