You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

236 lines
9.4KB

  1. import asyncio
  2. import six
  3. import sys
  4. import time
  5. from . import exceptions
  6. from . import packet
  7. from . import payload
  8. from . import socket
  9. class AsyncSocket(socket.Socket):
  10. async def poll(self):
  11. """Wait for packets to send to the client."""
  12. try:
  13. packets = [await asyncio.wait_for(self.queue.get(),
  14. self.server.ping_timeout)]
  15. self.queue.task_done()
  16. except (asyncio.TimeoutError, asyncio.CancelledError):
  17. raise exceptions.QueueEmpty()
  18. if packets == [None]:
  19. return []
  20. try:
  21. packets.append(self.queue.get_nowait())
  22. self.queue.task_done()
  23. except asyncio.QueueEmpty:
  24. pass
  25. return packets
  26. async def receive(self, pkt):
  27. """Receive packet from the client."""
  28. self.server.logger.info('%s: Received packet %s data %s',
  29. self.sid, packet.packet_names[pkt.packet_type],
  30. pkt.data if not isinstance(pkt.data, bytes)
  31. else '<binary>')
  32. if pkt.packet_type == packet.PING:
  33. self.last_ping = time.time()
  34. await self.send(packet.Packet(packet.PONG, pkt.data))
  35. elif pkt.packet_type == packet.MESSAGE:
  36. await self.server._trigger_event(
  37. 'message', self.sid, pkt.data,
  38. run_async=self.server.async_handlers)
  39. elif pkt.packet_type == packet.UPGRADE:
  40. await self.send(packet.Packet(packet.NOOP))
  41. elif pkt.packet_type == packet.CLOSE:
  42. await self.close(wait=False, abort=True)
  43. else:
  44. raise exceptions.UnknownPacketError()
  45. async def check_ping_timeout(self):
  46. """Make sure the client is still sending pings.
  47. This helps detect disconnections for long-polling clients.
  48. """
  49. if self.closed:
  50. raise exceptions.SocketIsClosedError()
  51. if time.time() - self.last_ping > self.server.ping_interval + 5:
  52. self.server.logger.info('%s: Client is gone, closing socket',
  53. self.sid)
  54. # Passing abort=False here will cause close() to write a
  55. # CLOSE packet. This has the effect of updating half-open sockets
  56. # to their correct state of disconnected
  57. await self.close(wait=False, abort=False)
  58. return False
  59. return True
  60. async def send(self, pkt):
  61. """Send a packet to the client."""
  62. if not await self.check_ping_timeout():
  63. return
  64. if self.upgrading:
  65. self.packet_backlog.append(pkt)
  66. else:
  67. await self.queue.put(pkt)
  68. self.server.logger.info('%s: Sending packet %s data %s',
  69. self.sid, packet.packet_names[pkt.packet_type],
  70. pkt.data if not isinstance(pkt.data, bytes)
  71. else '<binary>')
  72. async def handle_get_request(self, environ):
  73. """Handle a long-polling GET request from the client."""
  74. connections = [
  75. s.strip()
  76. for s in environ.get('HTTP_CONNECTION', '').lower().split(',')]
  77. transport = environ.get('HTTP_UPGRADE', '').lower()
  78. if 'upgrade' in connections and transport in self.upgrade_protocols:
  79. self.server.logger.info('%s: Received request to upgrade to %s',
  80. self.sid, transport)
  81. return await getattr(self, '_upgrade_' + transport)(environ)
  82. try:
  83. packets = await self.poll()
  84. except exceptions.QueueEmpty:
  85. exc = sys.exc_info()
  86. await self.close(wait=False)
  87. six.reraise(*exc)
  88. return packets
  89. async def handle_post_request(self, environ):
  90. """Handle a long-polling POST request from the client."""
  91. length = int(environ.get('CONTENT_LENGTH', '0'))
  92. if length > self.server.max_http_buffer_size:
  93. raise exceptions.ContentTooLongError()
  94. else:
  95. body = await environ['wsgi.input'].read(length)
  96. p = payload.Payload(encoded_payload=body)
  97. for pkt in p.packets:
  98. await self.receive(pkt)
  99. async def close(self, wait=True, abort=False):
  100. """Close the socket connection."""
  101. if not self.closed and not self.closing:
  102. self.closing = True
  103. await self.server._trigger_event('disconnect', self.sid)
  104. if not abort:
  105. await self.send(packet.Packet(packet.CLOSE))
  106. self.closed = True
  107. if wait:
  108. await self.queue.join()
  109. async def _upgrade_websocket(self, environ):
  110. """Upgrade the connection from polling to websocket."""
  111. if self.upgraded:
  112. raise IOError('Socket has been upgraded already')
  113. if self.server._async['websocket'] is None:
  114. # the selected async mode does not support websocket
  115. return self.server._bad_request()
  116. ws = self.server._async['websocket'](self._websocket_handler)
  117. return await ws(environ)
  118. async def _websocket_handler(self, ws):
  119. """Engine.IO handler for websocket transport."""
  120. if self.connected:
  121. # the socket was already connected, so this is an upgrade
  122. self.upgrading = True # hold packet sends during the upgrade
  123. try:
  124. pkt = await ws.wait()
  125. except IOError: # pragma: no cover
  126. return
  127. decoded_pkt = packet.Packet(encoded_packet=pkt)
  128. if decoded_pkt.packet_type != packet.PING or \
  129. decoded_pkt.data != 'probe':
  130. self.server.logger.info(
  131. '%s: Failed websocket upgrade, no PING packet', self.sid)
  132. return
  133. await ws.send(packet.Packet(
  134. packet.PONG,
  135. data=six.text_type('probe')).encode(always_bytes=False))
  136. await self.queue.put(packet.Packet(packet.NOOP)) # end poll
  137. try:
  138. pkt = await ws.wait()
  139. except IOError: # pragma: no cover
  140. return
  141. decoded_pkt = packet.Packet(encoded_packet=pkt)
  142. if decoded_pkt.packet_type != packet.UPGRADE:
  143. self.upgraded = False
  144. self.server.logger.info(
  145. ('%s: Failed websocket upgrade, expected UPGRADE packet, '
  146. 'received %s instead.'),
  147. self.sid, pkt)
  148. return
  149. self.upgraded = True
  150. # flush any packets that were sent during the upgrade
  151. for pkt in self.packet_backlog:
  152. await self.queue.put(pkt)
  153. self.packet_backlog = []
  154. self.upgrading = False
  155. else:
  156. self.connected = True
  157. self.upgraded = True
  158. # start separate writer thread
  159. async def writer():
  160. while True:
  161. packets = None
  162. try:
  163. packets = await self.poll()
  164. except exceptions.QueueEmpty:
  165. break
  166. if not packets:
  167. # empty packet list returned -> connection closed
  168. break
  169. try:
  170. for pkt in packets:
  171. await ws.send(pkt.encode(always_bytes=False))
  172. except:
  173. break
  174. writer_task = asyncio.ensure_future(writer())
  175. self.server.logger.info(
  176. '%s: Upgrade to websocket successful', self.sid)
  177. while True:
  178. p = None
  179. wait_task = asyncio.ensure_future(ws.wait())
  180. try:
  181. p = await asyncio.wait_for(wait_task, self.server.ping_timeout)
  182. except asyncio.CancelledError: # pragma: no cover
  183. # there is a bug (https://bugs.python.org/issue30508) in
  184. # asyncio that causes a "Task exception never retrieved" error
  185. # to appear when wait_task raises an exception before it gets
  186. # cancelled. Calling wait_task.exception() prevents the error
  187. # from being issued in Python 3.6, but causes other errors in
  188. # other versions, so we run it with all errors suppressed and
  189. # hope for the best.
  190. try:
  191. wait_task.exception()
  192. except:
  193. pass
  194. break
  195. except:
  196. break
  197. if p is None:
  198. # connection closed by client
  199. break
  200. if isinstance(p, six.text_type): # pragma: no cover
  201. p = p.encode('utf-8')
  202. pkt = packet.Packet(encoded_packet=p)
  203. try:
  204. await self.receive(pkt)
  205. except exceptions.UnknownPacketError: # pragma: no cover
  206. pass
  207. except exceptions.SocketIsClosedError: # pragma: no cover
  208. self.server.logger.info('Receive error -- socket is closed')
  209. break
  210. except: # pragma: no cover
  211. # if we get an unexpected exception we log the error and exit
  212. # the connection properly
  213. self.server.logger.exception('Unknown receive error')
  214. await self.queue.put(None) # unlock the writer task so it can exit
  215. await asyncio.wait_for(writer_task, timeout=None)
  216. await self.close(wait=False, abort=True)