Replace PySocks with aiosocks due to bugs (see #1192)

related to blocking behaviour of PySocks on socket connection.

Also small fixes and additional checks on local_addr parameter.
Updated documentation.
This commit is contained in:
Serhii Dylda 2020-11-08 05:58:10 +01:00
parent 9c833b601a
commit e4eaecb1cd
7 changed files with 110 additions and 58 deletions

View File

@ -1,4 +1,3 @@
cryptg cryptg
pysocks
hachoir hachoir
pillow pillow

View File

@ -117,7 +117,7 @@ Signing In behind a Proxy
========================= =========================
If you need to use a proxy to access Telegram, If you need to use a proxy to access Telegram,
you will need to `install PySocks`__ and then change: you will need to `install aiosocks`__ and then change:
.. code-block:: python .. code-block:: python
@ -127,15 +127,36 @@ with
.. code-block:: python .. code-block:: python
TelegramClient('anon', api_id, api_hash, proxy=(socks.SOCKS5, '127.0.0.1', 4444)) TelegramClient('anon', api_id, api_hash, proxy=("socks5", '127.0.0.1', 4444))
(of course, replacing the IP and port with the IP and port of the proxy). (of course, replacing the IP and port with the IP and port of the proxy).
The ``proxy=`` argument should be a tuple, a list or a dict, The ``proxy=`` argument should be a tuple, a list or a dict,
consisting of parameters described `in PySocks usage`__. consisting of parameters described `in aiosocks usage`__.
.. __: https://github.com/Anorov/PySocks#installation Example:
.. __: https://github.com/Anorov/PySocks#usage-1
* .. code-block:: python
proxy = ('socks5', '1.1.1.1', 5555, 'foo', 'bar', True)
* .. code-block:: python
proxy = ['socks5', '1.1.1.1', 5555, 'foo', 'bar', True]
* .. code-block:: python
proxy = {
'protocol': 'socks5', # (mandatory) protocol to use, default socks5, allowed values: 'socks5' (or 2), 'socks4' (or 1)
'host': '1.1.1.1', # (mandatory) proxy IP address
'port': 5555, # (mandatory) proxy port number
'username': 'foo', # (optional) username if the proxy requires auth
'password': 'bar', # (optional) password if the proxy requires auth
'remote_resolve': True # (optional) whether to use remote or local resolve, default remote
}
.. __: https://github.com/nibrag/aiosocks#installation
.. __: https://github.com/nibrag/aiosocks#usage
Using MTProto Proxies Using MTProto Proxies

View File

@ -1,2 +1,3 @@
aiosocks
pyaes pyaes
rsa rsa

View File

@ -107,11 +107,11 @@ class TelegramBaseClient(abc.ABC):
An iterable consisting of the proxy info. If `connection` is An iterable consisting of the proxy info. If `connection` is
one of `MTProxy`, then it should contain MTProxy credentials: one of `MTProxy`, then it should contain MTProxy credentials:
``('hostname', port, 'secret')``. Otherwise, it's meant to store ``('hostname', port, 'secret')``. Otherwise, it's meant to store
function parameters for PySocks, like ``(type, 'hostname', port)``. function parameters for AioSocks, like ``(type, 'hostname', port)``.
See https://github.com/Anorov/PySocks#usage-1 for more. See https://github.com/nibrag/aiosocks for more.
local_addr (`str`, optional): local_addr (`str` | `tuple`, optional):
Local host address used to bind the socket to locally. Local host address (and port, optionally) used to bind the socket to locally.
You only need to use this if you have multiple network cards and You only need to use this if you have multiple network cards and
want to use a specific one. want to use a specific one.
@ -220,10 +220,10 @@ class TelegramBaseClient(abc.ABC):
connection: 'typing.Type[Connection]' = ConnectionTcpFull, connection: 'typing.Type[Connection]' = ConnectionTcpFull,
use_ipv6: bool = False, use_ipv6: bool = False,
proxy: typing.Union[tuple, dict] = None, proxy: typing.Union[tuple, dict] = None,
local_addr=None, local_addr: typing.Union[tuple, str] = None,
timeout: int = 10, timeout: int = 10,
request_retries: int = 5, request_retries: int = 5,
connection_retries: int =5, connection_retries: int = 5,
retry_delay: int = 1, retry_delay: int = 1,
auto_reconnect: bool = True, auto_reconnect: bool = True,
sequential_updates: bool = False, sequential_updates: bool = False,

View File

@ -1,6 +1,5 @@
import abc import abc
import asyncio import asyncio
import socket
import sys import sys
try: try:
@ -11,6 +10,20 @@ except ImportError:
from ...errors import InvalidChecksumError from ...errors import InvalidChecksumError
from ... import helpers from ... import helpers
import aiosocks
# For some reason, `aiosocks` internal errors are not inherited from
# builtin IOError (just from Exception). Instead of adding those
# in exceptions clauses everywhere through the code, we
# rather monkey-patch them in place.
aiosocks.errors.SocksError = ConnectionError
aiosocks.errors.NoAcceptableAuthMethods = ConnectionError
aiosocks.errors.LoginAuthenticationFailed = ConnectionError
aiosocks.errors.InvalidServerVersion = ConnectionError
aiosocks.errors.InvalidServerReply = ConnectionError
aiosocks.errors.SocksConnectionError = ConnectionError
class Connection(abc.ABC): class Connection(abc.ABC):
""" """
@ -46,58 +59,76 @@ class Connection(abc.ABC):
self._recv_queue = asyncio.Queue(1) self._recv_queue = asyncio.Queue(1)
async def _connect(self, timeout=None, ssl=None): async def _connect(self, timeout=None, ssl=None):
if not self._proxy:
if self._local_addr is not None:
local_addr = (self._local_addr, None)
else:
local_addr = None
self._reader, self._writer = await asyncio.wait_for( if self._local_addr is not None:
asyncio.open_connection(self._ip, self._port, ssl=ssl, local_addr=local_addr),
timeout=timeout # NOTE: If port is not specified, we use 0 port
) # to notify the OS that port should be chosen randomly
# from the available ones.
if isinstance(self._local_addr, tuple) and len(self._local_addr) == 2:
local_addr = self._local_addr
elif isinstance(self._local_addr, str):
local_addr = (self._local_addr, 0)
else:
raise ValueError("Unknown local address format: {}".format(self._local_addr))
else: else:
import socks local_addr = None
if ':' in self._ip:
mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0) if not self._proxy:
connect_coroutine = asyncio.open_connection(
host=self._ip,
port=self._port,
ssl=ssl,
local_addr=local_addr)
else:
if isinstance(self._proxy, (tuple, list)):
proxy, auth, remote_resolve = self._parse_proxy(*self._proxy)
elif isinstance(self._proxy, dict):
proxy, auth, remote_resolve = self._parse_proxy(**self._proxy)
else: else:
mode, address = socket.AF_INET, (self._ip, self._port) raise ValueError("Unknown proxy format: {}".format(self._proxy.__class__.__name__))
s = socks.socksocket(mode, socket.SOCK_STREAM) connect_coroutine = aiosocks.open_connection(
if isinstance(self._proxy, dict): proxy=proxy,
s.set_proxy(**self._proxy) proxy_auth=auth,
else: dst=(self._ip, self._port),
s.set_proxy(*self._proxy) remote_resolve=remote_resolve,
ssl=ssl,
s.settimeout(timeout) local_addr=local_addr)
if self._local_addr is not None:
s.bind((self._local_addr, None))
await asyncio.wait_for(
asyncio.get_event_loop().sock_connect(s, address),
timeout=timeout
)
if ssl:
if ssl_mod is None:
raise RuntimeError(
'Cannot use proxy that requires SSL'
'without the SSL module being available'
)
s = ssl_mod.wrap_socket(
s,
do_handshake_on_connect=True,
ssl_version=ssl_mod.PROTOCOL_SSLv23,
ciphers='ADH-AES256-SHA'
)
s.setblocking(False)
self._reader, self._writer = await asyncio.open_connection(sock=s)
self._reader, self._writer = await asyncio.wait_for(connect_coroutine, timeout=timeout)
self._codec = self.packet_codec(self) self._codec = self.packet_codec(self)
self._init_conn() self._init_conn()
await self._writer.drain() await self._writer.drain()
@staticmethod
def _parse_proxy(protocol, host, port, username=None, password=None, remote_resolve=True):
proxy, auth = None, None
if isinstance(protocol, str):
protocol = protocol.lower()
# We do the check for numerical values here
# to be backwards compatible with PySocks proxy format,
# (since socks.SOCKS5 = 2 and socks.SOCKS4 = 1)
if protocol == 'socks5' or protocol == 2:
proxy = aiosocks.Socks5Addr(host, port)
if username and password:
auth = aiosocks.Socks5Auth(username, password)
elif protocol == 'socks4' or protocol == 1:
proxy = aiosocks.Socks4Addr(host, port)
if username:
auth = aiosocks.Socks4Auth(username)
else:
raise ValueError('Unsupported proxy protocol {}'.format(protocol))
return proxy, auth, remote_resolve
async def connect(self, timeout=None, ssl=None): async def connect(self, timeout=None, ssl=None):
""" """
Establishes a connection with the server. Establishes a connection with the server.

View File

@ -22,7 +22,7 @@ def get_env(name, message, cast=str):
session = os.environ.get('TG_SESSION', 'printer') session = os.environ.get('TG_SESSION', 'printer')
api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) api_id = get_env('TG_API_ID', 'Enter your API ID: ', int)
api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') api_hash = get_env('TG_API_HASH', 'Enter your API hash: ')
proxy = None # https://github.com/Anorov/PySocks proxy = None # https://docs.telethon.dev/en/latest/basic/signing-in.html#signing-in-behind-a-proxy
# Create and start the client so we can make requests (we don't here) # Create and start the client so we can make requests (we don't here)
client = TelegramClient(session, api_id, api_hash, proxy=proxy).start() client = TelegramClient(session, api_id, api_hash, proxy=proxy).start()

View File

@ -27,7 +27,7 @@ def get_env(name, message, cast=str):
session = os.environ.get('TG_SESSION', 'printer') session = os.environ.get('TG_SESSION', 'printer')
api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) api_id = get_env('TG_API_ID', 'Enter your API ID: ', int)
api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') api_hash = get_env('TG_API_HASH', 'Enter your API hash: ')
proxy = None # https://github.com/Anorov/PySocks proxy = None # https://docs.telethon.dev/en/latest/basic/signing-in.html#signing-in-behind-a-proxy
# This is our update handler. It is called when a new update arrives. # This is our update handler. It is called when a new update arrives.