_stream_hixie75.py 8.6 KB

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