|
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 a class for parsing/building frames of the WebSocket |
|
32 protocol version HyBi 00 and Hixie 75. |
|
33 |
|
34 Specification: |
|
35 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 |
|
36 """ |
|
37 |
|
38 |
|
39 from mod_pywebsocket import common |
|
40 from mod_pywebsocket._stream_base import BadOperationException |
|
41 from mod_pywebsocket._stream_base import ConnectionTerminatedException |
|
42 from mod_pywebsocket._stream_base import InvalidFrameException |
|
43 from mod_pywebsocket._stream_base import StreamBase |
|
44 from mod_pywebsocket._stream_base import UnsupportedFrameException |
|
45 from mod_pywebsocket import util |
|
46 |
|
47 |
|
48 class StreamHixie75(StreamBase): |
|
49 """A class for parsing/building frames of the WebSocket protocol version |
|
50 HyBi 00 and Hixie 75. |
|
51 """ |
|
52 |
|
53 def __init__(self, request, enable_closing_handshake=False): |
|
54 """Construct an instance. |
|
55 |
|
56 Args: |
|
57 request: mod_python request. |
|
58 enable_closing_handshake: to let StreamHixie75 perform closing |
|
59 handshake as specified in HyBi 00, set |
|
60 this option to True. |
|
61 """ |
|
62 |
|
63 StreamBase.__init__(self, request) |
|
64 |
|
65 self._logger = util.get_class_logger(self) |
|
66 |
|
67 self._enable_closing_handshake = enable_closing_handshake |
|
68 |
|
69 self._request.client_terminated = False |
|
70 self._request.server_terminated = False |
|
71 |
|
72 def send_message(self, message, end=True, binary=False): |
|
73 """Send message. |
|
74 |
|
75 Args: |
|
76 message: unicode string to send. |
|
77 binary: not used in hixie75. |
|
78 |
|
79 Raises: |
|
80 BadOperationException: when called on a server-terminated |
|
81 connection. |
|
82 """ |
|
83 |
|
84 if not end: |
|
85 raise BadOperationException( |
|
86 'StreamHixie75 doesn\'t support send_message with end=False') |
|
87 |
|
88 if binary: |
|
89 raise BadOperationException( |
|
90 'StreamHixie75 doesn\'t support send_message with binary=True') |
|
91 |
|
92 if self._request.server_terminated: |
|
93 raise BadOperationException( |
|
94 'Requested send_message after sending out a closing handshake') |
|
95 |
|
96 self._write(''.join(['\x00', message.encode('utf-8'), '\xff'])) |
|
97 |
|
98 def _read_payload_length_hixie75(self): |
|
99 """Reads a length header in a Hixie75 version frame with length. |
|
100 |
|
101 Raises: |
|
102 ConnectionTerminatedException: when read returns empty string. |
|
103 """ |
|
104 |
|
105 length = 0 |
|
106 while True: |
|
107 b_str = self._read(1) |
|
108 b = ord(b_str) |
|
109 length = length * 128 + (b & 0x7f) |
|
110 if (b & 0x80) == 0: |
|
111 break |
|
112 return length |
|
113 |
|
114 def receive_message(self): |
|
115 """Receive a WebSocket frame and return its payload an unicode string. |
|
116 |
|
117 Returns: |
|
118 payload unicode string in a WebSocket frame. |
|
119 |
|
120 Raises: |
|
121 ConnectionTerminatedException: when read returns empty |
|
122 string. |
|
123 BadOperationException: when called on a client-terminated |
|
124 connection. |
|
125 """ |
|
126 |
|
127 if self._request.client_terminated: |
|
128 raise BadOperationException( |
|
129 'Requested receive_message after receiving a closing ' |
|
130 'handshake') |
|
131 |
|
132 while True: |
|
133 # Read 1 byte. |
|
134 # mp_conn.read will block if no bytes are available. |
|
135 # Timeout is controlled by TimeOut directive of Apache. |
|
136 frame_type_str = self.receive_bytes(1) |
|
137 frame_type = ord(frame_type_str) |
|
138 if (frame_type & 0x80) == 0x80: |
|
139 # The payload length is specified in the frame. |
|
140 # Read and discard. |
|
141 length = self._read_payload_length_hixie75() |
|
142 if length > 0: |
|
143 _ = self.receive_bytes(length) |
|
144 # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the |
|
145 # /client terminated/ flag and abort these steps. |
|
146 if not self._enable_closing_handshake: |
|
147 continue |
|
148 |
|
149 if frame_type == 0xFF and length == 0: |
|
150 self._request.client_terminated = True |
|
151 |
|
152 if self._request.server_terminated: |
|
153 self._logger.debug( |
|
154 'Received ack for server-initiated closing ' |
|
155 'handshake') |
|
156 return None |
|
157 |
|
158 self._logger.debug( |
|
159 'Received client-initiated closing handshake') |
|
160 |
|
161 self._send_closing_handshake() |
|
162 self._logger.debug( |
|
163 'Sent ack for client-initiated closing handshake') |
|
164 return None |
|
165 else: |
|
166 # The payload is delimited with \xff. |
|
167 bytes = self._read_until('\xff') |
|
168 # The WebSocket protocol section 4.4 specifies that invalid |
|
169 # characters must be replaced with U+fffd REPLACEMENT |
|
170 # CHARACTER. |
|
171 message = bytes.decode('utf-8', 'replace') |
|
172 if frame_type == 0x00: |
|
173 return message |
|
174 # Discard data of other types. |
|
175 |
|
176 def _send_closing_handshake(self): |
|
177 if not self._enable_closing_handshake: |
|
178 raise BadOperationException( |
|
179 'Closing handshake is not supported in Hixie 75 protocol') |
|
180 |
|
181 self._request.server_terminated = True |
|
182 |
|
183 # 5.3 the server may decide to terminate the WebSocket connection by |
|
184 # running through the following steps: |
|
185 # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the |
|
186 # start of the closing handshake. |
|
187 self._write('\xff\x00') |
|
188 |
|
189 def close_connection(self, unused_code='', unused_reason=''): |
|
190 """Closes a WebSocket connection. |
|
191 |
|
192 Raises: |
|
193 ConnectionTerminatedException: when closing handshake was |
|
194 not successfull. |
|
195 """ |
|
196 |
|
197 if self._request.server_terminated: |
|
198 self._logger.debug( |
|
199 'Requested close_connection but server is already terminated') |
|
200 return |
|
201 |
|
202 if not self._enable_closing_handshake: |
|
203 self._request.server_terminated = True |
|
204 self._logger.debug('Connection closed') |
|
205 return |
|
206 |
|
207 self._send_closing_handshake() |
|
208 self._logger.debug('Sent server-initiated closing handshake') |
|
209 |
|
210 # TODO(ukai): 2. wait until the /client terminated/ flag has been set, |
|
211 # or until a server-defined timeout expires. |
|
212 # |
|
213 # For now, we expect receiving closing handshake right after sending |
|
214 # out closing handshake, and if we couldn't receive non-handshake |
|
215 # frame, we take it as ConnectionTerminatedException. |
|
216 message = self.receive_message() |
|
217 if message is not None: |
|
218 raise ConnectionTerminatedException( |
|
219 'Didn\'t receive valid ack for closing handshake') |
|
220 # TODO: 3. close the WebSocket connection. |
|
221 # note: mod_python Connection (mp_conn) doesn't have close method. |
|
222 |
|
223 def send_ping(self, body): |
|
224 raise BadOperationException( |
|
225 'StreamHixie75 doesn\'t support send_ping') |
|
226 |
|
227 |
|
228 # vi:sts=4 sw=4 et |