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
pysocks
hachoir
pillow

View File

@ -117,7 +117,7 @@ Signing In behind a Proxy
=========================
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
@ -127,15 +127,36 @@ with
.. 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).
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
.. __: https://github.com/Anorov/PySocks#usage-1
Example:
* .. 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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import abc
import asyncio
import socket
import sys
try:
@ -11,6 +10,20 @@ except ImportError:
from ...errors import InvalidChecksumError
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):
"""
@ -46,58 +59,76 @@ class Connection(abc.ABC):
self._recv_queue = asyncio.Queue(1)
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(
asyncio.open_connection(self._ip, self._port, ssl=ssl, local_addr=local_addr),
timeout=timeout
)
if self._local_addr is not None:
# 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:
import socks
if ':' in self._ip:
mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0)
local_addr = None
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:
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)
if isinstance(self._proxy, dict):
s.set_proxy(**self._proxy)
else:
s.set_proxy(*self._proxy)
s.settimeout(timeout)
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)
connect_coroutine = aiosocks.open_connection(
proxy=proxy,
proxy_auth=auth,
dst=(self._ip, self._port),
remote_resolve=remote_resolve,
ssl=ssl,
local_addr=local_addr)
self._reader, self._writer = await asyncio.wait_for(connect_coroutine, timeout=timeout)
self._codec = self.packet_codec(self)
self._init_conn()
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):
"""
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')
api_id = get_env('TG_API_ID', 'Enter your API ID: ', int)
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)
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')
api_id = get_env('TG_API_ID', 'Enter your API ID: ', int)
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.