|
|
|
import asyncio
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
|
|
|
|
from . import exceptions
|
|
|
|
from . import packet
|
|
|
|
from . import payload
|
|
|
|
from . import socket
|
|
|
|
|
|
|
|
|
|
|
|
class AsyncSocket(socket.Socket):
|
|
|
|
async def poll(self):
|
|
|
|
"""Wait for packets to send to the client."""
|
|
|
|
try:
|
|
|
|
packets = [await asyncio.wait_for(
|
|
|
|
self.queue.get(),
|
|
|
|
self.server.ping_interval + self.server.ping_timeout)]
|
|
|
|
self.queue.task_done()
|
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
raise exceptions.QueueEmpty()
|
|
|
|
if packets == [None]:
|
|
|
|
return []
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
pkt = self.queue.get_nowait()
|
|
|
|
self.queue.task_done()
|
|
|
|
if pkt is None:
|
|
|
|
self.queue.put_nowait(None)
|
|
|
|
break
|
|
|
|
packets.append(pkt)
|
|
|
|
except asyncio.QueueEmpty:
|
|
|
|
break
|
|
|
|
return packets
|
|
|
|
|
|
|
|
async def receive(self, pkt):
|
|
|
|
"""Receive packet from the client."""
|
|
|
|
self.server.logger.info('%s: Received packet %s data %s',
|
|
|
|
self.sid, packet.packet_names[pkt.packet_type],
|
|
|
|
pkt.data if not isinstance(pkt.data, bytes)
|
|
|
|
else '<binary>')
|
|
|
|
if pkt.packet_type == packet.PONG:
|
|
|
|
self.schedule_ping()
|
|
|
|
elif pkt.packet_type == packet.MESSAGE:
|
|
|
|
await self.server._trigger_event(
|
|
|
|
'message', self.sid, pkt.data,
|
|
|
|
run_async=self.server.async_handlers)
|
|
|
|
elif pkt.packet_type == packet.UPGRADE:
|
|
|
|
await self.send(packet.Packet(packet.NOOP))
|
|
|
|
elif pkt.packet_type == packet.CLOSE:
|
|
|
|
await self.close(wait=False, abort=True)
|
|
|
|
else:
|
|
|
|
raise exceptions.UnknownPacketError()
|
|
|
|
|
|
|
|
async def check_ping_timeout(self):
|
|
|
|
"""Make sure the client is still sending pings."""
|
|
|
|
if self.closed:
|
|
|
|
raise exceptions.SocketIsClosedError()
|
|
|
|
if self.last_ping and \
|
|
|
|
time.time() - self.last_ping > self.server.ping_timeout:
|
|
|
|
self.server.logger.info('%s: Client is gone, closing socket',
|
|
|
|
self.sid)
|
|
|
|
# Passing abort=False here will cause close() to write a
|
|
|
|
# CLOSE packet. This has the effect of updating half-open sockets
|
|
|
|
# to their correct state of disconnected
|
|
|
|
await self.close(wait=False, abort=False)
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
async def send(self, pkt):
|
|
|
|
"""Send a packet to the client."""
|
|
|
|
if not await self.check_ping_timeout():
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
await self.queue.put(pkt)
|
|
|
|
self.server.logger.info('%s: Sending packet %s data %s',
|
|
|
|
self.sid, packet.packet_names[pkt.packet_type],
|
|
|
|
pkt.data if not isinstance(pkt.data, bytes)
|
|
|
|
else '<binary>')
|
|
|
|
|
|
|
|
async def handle_get_request(self, environ):
|
|
|
|
"""Handle a long-polling GET request from the client."""
|
|
|
|
connections = [
|
|
|
|
s.strip()
|
|
|
|
for s in environ.get('HTTP_CONNECTION', '').lower().split(',')]
|
|
|
|
transport = environ.get('HTTP_UPGRADE', '').lower()
|
|
|
|
if 'upgrade' in connections and transport in self.upgrade_protocols:
|
|
|
|
self.server.logger.info('%s: Received request to upgrade to %s',
|
|
|
|
self.sid, transport)
|
|
|
|
return await getattr(self, '_upgrade_' + transport)(environ)
|
|
|
|
if self.upgrading or self.upgraded:
|
|
|
|
# we are upgrading to WebSocket, do not return any more packets
|
|
|
|
# through the polling endpoint
|
|
|
|
return [packet.Packet(packet.NOOP)]
|
|
|
|
try:
|
|
|
|
packets = await self.poll()
|
|
|
|
except exceptions.QueueEmpty:
|
|
|
|
exc = sys.exc_info()
|
|
|
|
await self.close(wait=False)
|
|
|
|
raise exc[1].with_traceback(exc[2])
|
|
|
|
return packets
|
|
|
|
|
|
|
|
async def handle_post_request(self, environ):
|
|
|
|
"""Handle a long-polling POST request from the client."""
|
|
|
|
length = int(environ.get('CONTENT_LENGTH', '0'))
|
|
|
|
if length > self.server.max_http_buffer_size:
|
|
|
|
raise exceptions.ContentTooLongError()
|
|
|
|
else:
|
|
|
|
body = (await environ['wsgi.input'].read(length)).decode('utf-8')
|
|
|
|
p = payload.Payload(encoded_payload=body)
|
|
|
|
for pkt in p.packets:
|
|
|
|
await self.receive(pkt)
|
|
|
|
|
|
|
|
async def close(self, wait=True, abort=False):
|
|
|
|
"""Close the socket connection."""
|
|
|
|
if not self.closed and not self.closing:
|
|
|
|
self.closing = True
|
|
|
|
await self.server._trigger_event('disconnect', self.sid)
|
|
|
|
if not abort:
|
|
|
|
await self.send(packet.Packet(packet.CLOSE))
|
|
|
|
self.closed = True
|
|
|
|
if wait:
|
|
|
|
await self.queue.join()
|
|
|
|
|
|
|
|
def schedule_ping(self):
|
|
|
|
async def send_ping():
|
|
|
|
self.last_ping = None
|
|
|
|
await asyncio.sleep(self.server.ping_interval)
|
|
|
|
if not self.closing and not self.closed:
|
|
|
|
self.last_ping = time.time()
|
|
|
|
await self.send(packet.Packet(packet.PING))
|
|
|
|
|
|
|
|
self.server.start_background_task(send_ping)
|
|
|
|
|
|
|
|
async def _upgrade_websocket(self, environ):
|
|
|
|
"""Upgrade the connection from polling to websocket."""
|
|
|
|
if self.upgraded:
|
|
|
|
raise IOError('Socket has been upgraded already')
|
|
|
|
if self.server._async['websocket'] is None:
|
|
|
|
# the selected async mode does not support websocket
|
|
|
|
return self.server._bad_request()
|
|
|
|
ws = self.server._async['websocket'](self._websocket_handler)
|
|
|
|
return await ws(environ)
|
|
|
|
|
|
|
|
async def _websocket_handler(self, ws):
|
|
|
|
"""Engine.IO handler for websocket transport."""
|
|
|
|
async def websocket_wait():
|
|
|
|
data = await ws.wait()
|
|
|
|
if data and len(data) > self.server.max_http_buffer_size:
|
|
|
|
raise ValueError('packet is too large')
|
|
|
|
return data
|
|
|
|
|
|
|
|
if self.connected:
|
|
|
|
# the socket was already connected, so this is an upgrade
|
|
|
|
self.upgrading = True # hold packet sends during the upgrade
|
|
|
|
|
|
|
|
try:
|
|
|
|
pkt = await websocket_wait()
|
|
|
|
except IOError: # pragma: no cover
|
|
|
|
return
|
|
|
|
decoded_pkt = packet.Packet(encoded_packet=pkt)
|
|
|
|
if decoded_pkt.packet_type != packet.PING or \
|
|
|
|
decoded_pkt.data != 'probe':
|
|
|
|
self.server.logger.info(
|
|
|
|
'%s: Failed websocket upgrade, no PING packet', self.sid)
|
|
|
|
self.upgrading = False
|
|
|
|
return
|
|
|
|
await ws.send(packet.Packet(packet.PONG, data='probe').encode())
|
|
|
|
await self.queue.put(packet.Packet(packet.NOOP)) # end poll
|
|
|
|
|
|
|
|
try:
|
|
|
|
pkt = await websocket_wait()
|
|
|
|
except IOError: # pragma: no cover
|
|
|
|
self.upgrading = False
|
|
|
|
return
|
|
|
|
decoded_pkt = packet.Packet(encoded_packet=pkt)
|
|
|
|
if decoded_pkt.packet_type != packet.UPGRADE:
|
|
|
|
self.upgraded = False
|
|
|
|
self.server.logger.info(
|
|
|
|
('%s: Failed websocket upgrade, expected UPGRADE packet, '
|
|
|
|
'received %s instead.'),
|
|
|
|
self.sid, pkt)
|
|
|
|
self.upgrading = False
|
|
|
|
return
|
|
|
|
self.upgraded = True
|
|
|
|
self.upgrading = False
|
|
|
|
else:
|
|
|
|
self.connected = True
|
|
|
|
self.upgraded = True
|
|
|
|
|
|
|
|
# start separate writer thread
|
|
|
|
async def writer():
|
|
|
|
while True:
|
|
|
|
packets = None
|
|
|
|
try:
|
|
|
|
packets = await self.poll()
|
|
|
|
except exceptions.QueueEmpty:
|
|
|
|
break
|
|
|
|
if not packets:
|
|
|
|
# empty packet list returned -> connection closed
|
|
|
|
break
|
|
|
|
try:
|
|
|
|
for pkt in packets:
|
|
|
|
await ws.send(pkt.encode())
|
|
|
|
except:
|
|
|
|
break
|
|
|
|
writer_task = asyncio.ensure_future(writer())
|
|
|
|
|
|
|
|
self.server.logger.info(
|
|
|
|
'%s: Upgrade to websocket successful', self.sid)
|
|
|
|
|
|
|
|
while True:
|
|
|
|
p = None
|
|
|
|
wait_task = asyncio.ensure_future(websocket_wait())
|
|
|
|
try:
|
|
|
|
p = await asyncio.wait_for(
|
|
|
|
wait_task,
|
|
|
|
self.server.ping_interval + self.server.ping_timeout)
|
|
|
|
except asyncio.CancelledError: # pragma: no cover
|
|
|
|
# there is a bug (https://bugs.python.org/issue30508) in
|
|
|
|
# asyncio that causes a "Task exception never retrieved" error
|
|
|
|
# to appear when wait_task raises an exception before it gets
|
|
|
|
# cancelled. Calling wait_task.exception() prevents the error
|
|
|
|
# from being issued in Python 3.6, but causes other errors in
|
|
|
|
# other versions, so we run it with all errors suppressed and
|
|
|
|
# hope for the best.
|
|
|
|
try:
|
|
|
|
wait_task.exception()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
break
|
|
|
|
except:
|
|
|
|
break
|
|
|
|
if p is None:
|
|
|
|
# connection closed by client
|
|
|
|
break
|
|
|
|
pkt = packet.Packet(encoded_packet=p)
|
|
|
|
try:
|
|
|
|
await self.receive(pkt)
|
|
|
|
except exceptions.UnknownPacketError: # pragma: no cover
|
|
|
|
pass
|
|
|
|
except exceptions.SocketIsClosedError: # pragma: no cover
|
|
|
|
self.server.logger.info('Receive error -- socket is closed')
|
|
|
|
break
|
|
|
|
except: # pragma: no cover
|
|
|
|
# if we get an unexpected exception we log the error and exit
|
|
|
|
# the connection properly
|
|
|
|
self.server.logger.exception('Unknown receive error')
|
|
|
|
|
|
|
|
await self.queue.put(None) # unlock the writer task so it can exit
|
|
|
|
await asyncio.wait_for(writer_task, timeout=None)
|
|
|
|
await self.close(wait=False, abort=True)
|