2018-09-27 19:45:20 +03:00
|
|
|
import abc
|
|
|
|
import asyncio
|
2018-10-03 15:46:10 +03:00
|
|
|
import logging
|
2018-10-04 18:11:31 +03:00
|
|
|
import socket
|
|
|
|
import ssl as ssl_mod
|
2018-10-03 15:46:10 +03:00
|
|
|
|
2018-10-19 15:41:50 +03:00
|
|
|
from ...errors import InvalidChecksumError
|
|
|
|
|
2018-10-03 15:46:10 +03:00
|
|
|
__log__ = logging.getLogger(__name__)
|
2018-09-27 19:45:20 +03:00
|
|
|
|
|
|
|
|
|
|
|
class Connection(abc.ABC):
|
|
|
|
"""
|
|
|
|
The `Connection` class is a wrapper around ``asyncio.open_connection``.
|
|
|
|
|
2018-10-19 11:35:22 +03:00
|
|
|
Subclasses will implement different transport modes as atomic operations,
|
|
|
|
which this class eases doing since the exposed interface simply puts and
|
|
|
|
gets complete data payloads to and from queues.
|
2018-09-27 19:45:20 +03:00
|
|
|
|
2018-10-19 11:35:22 +03:00
|
|
|
The only error that will raise from send and receive methods is
|
|
|
|
``ConnectionError``, which will raise when attempting to send if
|
|
|
|
the client is disconnected (includes remote disconnections).
|
2018-09-27 19:45:20 +03:00
|
|
|
"""
|
2018-10-04 18:11:31 +03:00
|
|
|
def __init__(self, ip, port, *, loop, proxy=None):
|
2018-09-27 19:45:20 +03:00
|
|
|
self._ip = ip
|
|
|
|
self._port = port
|
|
|
|
self._loop = loop
|
2018-10-04 18:11:31 +03:00
|
|
|
self._proxy = proxy
|
2018-09-27 19:45:20 +03:00
|
|
|
self._reader = None
|
|
|
|
self._writer = None
|
2018-10-21 17:20:05 +03:00
|
|
|
self._connected = False
|
2018-09-27 19:45:20 +03:00
|
|
|
self._send_task = None
|
|
|
|
self._recv_task = None
|
|
|
|
self._send_queue = asyncio.Queue(1)
|
|
|
|
self._recv_queue = asyncio.Queue(1)
|
|
|
|
|
2018-10-04 18:11:31 +03:00
|
|
|
async def connect(self, timeout=None, ssl=None):
|
2018-09-27 19:45:20 +03:00
|
|
|
"""
|
|
|
|
Establishes a connection with the server.
|
|
|
|
"""
|
2018-10-04 18:11:31 +03:00
|
|
|
if not self._proxy:
|
|
|
|
self._reader, self._writer = await asyncio.wait_for(
|
|
|
|
asyncio.open_connection(
|
|
|
|
self._ip, self._port, loop=self._loop, ssl=ssl),
|
|
|
|
loop=self._loop, timeout=timeout
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
import socks
|
|
|
|
if ':' in self._ip:
|
|
|
|
mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0)
|
|
|
|
else:
|
|
|
|
mode, address = socket.AF_INET, (self._ip, self._port)
|
|
|
|
|
|
|
|
s = socks.socksocket(mode, socket.SOCK_STREAM)
|
|
|
|
if isinstance(self._proxy, dict):
|
|
|
|
s.set_proxy(**self._proxy)
|
|
|
|
else:
|
|
|
|
s.set_proxy(*self._proxy)
|
|
|
|
|
|
|
|
s.setblocking(False)
|
|
|
|
await asyncio.wait_for(
|
|
|
|
self._loop.sock_connect(s, address),
|
|
|
|
timeout=timeout,
|
|
|
|
loop=self._loop
|
|
|
|
)
|
|
|
|
if ssl:
|
2018-12-10 16:43:48 +03:00
|
|
|
s.settimeout(timeout)
|
|
|
|
s = ssl_mod.wrap_socket(
|
2018-10-04 18:11:31 +03:00
|
|
|
s,
|
|
|
|
do_handshake_on_connect=True,
|
|
|
|
ssl_version=ssl_mod.PROTOCOL_SSLv23,
|
|
|
|
ciphers='ADH-AES256-SHA'
|
|
|
|
)
|
2018-12-10 16:43:48 +03:00
|
|
|
s.setblocking(False)
|
2018-10-04 18:11:31 +03:00
|
|
|
|
2018-10-06 21:56:09 +03:00
|
|
|
self._reader, self._writer = \
|
|
|
|
await asyncio.open_connection(sock=s, loop=self._loop)
|
2018-09-27 19:45:20 +03:00
|
|
|
|
2018-10-21 17:20:05 +03:00
|
|
|
self._connected = True
|
2018-09-27 19:45:20 +03:00
|
|
|
self._send_task = self._loop.create_task(self._send_loop())
|
2018-09-28 18:45:45 +03:00
|
|
|
self._recv_task = self._loop.create_task(self._recv_loop())
|
2018-09-27 19:45:20 +03:00
|
|
|
|
|
|
|
def disconnect(self):
|
|
|
|
"""
|
2018-10-19 11:35:22 +03:00
|
|
|
Disconnects from the server, and clears
|
|
|
|
pending outgoing and incoming messages.
|
2018-09-27 19:45:20 +03:00
|
|
|
"""
|
2018-10-21 17:20:05 +03:00
|
|
|
self._connected = False
|
2018-10-19 11:35:22 +03:00
|
|
|
|
2018-09-30 12:58:46 +03:00
|
|
|
if self._send_task:
|
|
|
|
self._send_task.cancel()
|
|
|
|
|
|
|
|
if self._recv_task:
|
|
|
|
self._recv_task.cancel()
|
|
|
|
|
|
|
|
if self._writer:
|
|
|
|
self._writer.close()
|
2018-09-27 19:45:20 +03:00
|
|
|
|
2018-09-28 18:45:45 +03:00
|
|
|
def clone(self):
|
|
|
|
"""
|
|
|
|
Creates a clone of the connection.
|
|
|
|
"""
|
|
|
|
return self.__class__(self._ip, self._port, loop=self._loop)
|
|
|
|
|
2018-09-27 19:45:20 +03:00
|
|
|
def send(self, data):
|
|
|
|
"""
|
|
|
|
Sends a packet of data through this connection mode.
|
|
|
|
|
|
|
|
This method returns a coroutine.
|
|
|
|
"""
|
2018-10-21 17:20:05 +03:00
|
|
|
if not self._connected:
|
2018-10-19 11:35:22 +03:00
|
|
|
raise ConnectionError('Not connected')
|
|
|
|
|
2018-09-27 19:45:20 +03:00
|
|
|
return self._send_queue.put(data)
|
|
|
|
|
2018-10-03 15:46:10 +03:00
|
|
|
async def recv(self):
|
2018-09-27 19:45:20 +03:00
|
|
|
"""
|
|
|
|
Receives a packet of data through this connection mode.
|
|
|
|
|
|
|
|
This method returns a coroutine.
|
|
|
|
"""
|
2018-11-24 22:51:32 +03:00
|
|
|
while self._connected:
|
|
|
|
result = await self._recv_queue.get()
|
|
|
|
if result: # None = sentinel value = keep trying
|
|
|
|
return result
|
2018-10-28 13:52:58 +03:00
|
|
|
|
2018-11-24 22:51:32 +03:00
|
|
|
raise ConnectionError('Not connected')
|
2018-09-27 19:45:20 +03:00
|
|
|
|
|
|
|
async def _send_loop(self):
|
|
|
|
"""
|
|
|
|
This loop is constantly popping items off the queue to send them.
|
|
|
|
"""
|
2018-10-03 15:46:10 +03:00
|
|
|
try:
|
2018-10-21 17:20:05 +03:00
|
|
|
while self._connected:
|
2018-10-03 15:46:10 +03:00
|
|
|
self._send(await self._send_queue.get())
|
|
|
|
await self._writer.drain()
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
pass
|
2018-12-06 18:27:09 +03:00
|
|
|
except Exception as e:
|
|
|
|
if isinstance(e, ConnectionError):
|
|
|
|
__log__.info('The server closed the connection while sending')
|
|
|
|
else:
|
|
|
|
__log__.exception('Unexpected exception in the send loop')
|
|
|
|
|
2018-11-10 13:23:19 +03:00
|
|
|
self.disconnect()
|
2018-09-27 19:45:20 +03:00
|
|
|
|
|
|
|
async def _recv_loop(self):
|
|
|
|
"""
|
|
|
|
This loop is constantly putting items on the queue as they're read.
|
|
|
|
"""
|
2018-11-10 13:23:19 +03:00
|
|
|
while self._connected:
|
|
|
|
try:
|
2018-09-28 18:45:45 +03:00
|
|
|
data = await self._recv()
|
2018-11-10 13:23:19 +03:00
|
|
|
except asyncio.CancelledError:
|
|
|
|
break
|
|
|
|
except Exception as e:
|
|
|
|
if isinstance(e, (ConnectionError, asyncio.IncompleteReadError)):
|
|
|
|
msg = 'The server closed the connection'
|
|
|
|
__log__.info(msg)
|
|
|
|
elif isinstance(e, InvalidChecksumError):
|
|
|
|
msg = 'The server response had an invalid checksum'
|
|
|
|
__log__.info(msg)
|
|
|
|
else:
|
|
|
|
msg = 'Unexpected exception in the receive loop'
|
|
|
|
__log__.exception(msg)
|
|
|
|
|
|
|
|
self.disconnect()
|
2018-11-24 22:51:32 +03:00
|
|
|
|
|
|
|
# Add a sentinel value to unstuck recv
|
|
|
|
if self._recv_queue.empty():
|
|
|
|
self._recv_queue.put_nowait(None)
|
|
|
|
|
2018-11-10 13:23:19 +03:00
|
|
|
break
|
|
|
|
|
|
|
|
try:
|
2018-10-19 11:35:22 +03:00
|
|
|
await self._recv_queue.put(data)
|
2018-11-10 13:23:19 +03:00
|
|
|
except asyncio.CancelledError:
|
|
|
|
break
|
2018-09-27 19:45:20 +03:00
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def _send(self, data):
|
|
|
|
"""
|
|
|
|
This method should be implemented differently under each
|
|
|
|
connection mode and serialize the data into the packet
|
|
|
|
the way it should be sent through `self._writer`.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
async def _recv(self):
|
|
|
|
"""
|
|
|
|
This method should be implemented differently under each
|
|
|
|
connection mode and deserialize the data from the packet
|
|
|
|
the way it should be read from `self._reader`.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
2018-09-29 11:58:45 +03:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return '{}:{}/{}'.format(
|
|
|
|
self._ip, self._port,
|
|
|
|
self.__class__.__name__.replace('Connection', '')
|
|
|
|
)
|