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
32protocol version HyBi 00 and Hixie 75.
33
34Specification:
35http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
36"""
37
38
39from mod_pywebsocket import common
40from mod_pywebsocket._stream_base import BadOperationException
41from mod_pywebsocket._stream_base import ConnectionTerminatedException
42from mod_pywebsocket._stream_base import InvalidFrameException
43from mod_pywebsocket._stream_base import StreamBase
44from mod_pywebsocket._stream_base import UnsupportedFrameException
45from mod_pywebsocket import util
46
47
48class 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
229