Skip to content

Commit 06adf2f

Browse files
authored
Add proxy util from IG/FB bridges (#134)
* Add proxy util from IG/FB bridges * Add comment about blocking request
1 parent 1410a3a commit 06adf2f

File tree

1 file changed

+113
-0
lines changed

1 file changed

+113
-0
lines changed

mautrix/util/proxy.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from __future__ import annotations
2+
3+
from typing import Awaitable, Callable, TypeVar
4+
import asyncio
5+
import json
6+
import logging
7+
import urllib.request
8+
9+
from aiohttp import ClientConnectionError
10+
from yarl import URL
11+
12+
from mautrix.util.logging import TraceLogger
13+
14+
try:
15+
from aiohttp_socks import ProxyConnectionError, ProxyError, ProxyTimeoutError
16+
except ImportError:
17+
18+
class ProxyError(Exception):
19+
pass
20+
21+
ProxyConnectionError = ProxyTimeoutError = ProxyError
22+
23+
RETRYABLE_PROXY_EXCEPTIONS = (
24+
ProxyError,
25+
ProxyTimeoutError,
26+
ProxyConnectionError,
27+
ClientConnectionError,
28+
ConnectionError,
29+
asyncio.TimeoutError,
30+
)
31+
32+
33+
class ProxyHandler:
34+
current_proxy_url: str | None = None
35+
log = logging.getLogger("mau.proxy")
36+
37+
def __init__(self, api_url: str | None) -> None:
38+
self.api_url = api_url
39+
40+
def get_proxy_url_from_api(self, reason: str | None = None) -> str | None:
41+
assert self.api_url is not None
42+
43+
api_url = str(URL(self.api_url).update_query({"reason": reason} if reason else {}))
44+
45+
# NOTE: using urllib.request to intentionally block the whole bridge until the proxy change applied
46+
request = urllib.request.Request(api_url, method="GET")
47+
self.log.debug("Requesting proxy from: %s", api_url)
48+
49+
try:
50+
with urllib.request.urlopen(request) as f:
51+
response = json.loads(f.read().decode())
52+
except Exception:
53+
self.log.exception("Failed to retrieve proxy from API")
54+
else:
55+
return response["proxy_url"]
56+
57+
return None
58+
59+
def update_proxy_url(self, reason: str | None = None) -> bool:
60+
old_proxy = self.current_proxy_url
61+
new_proxy = None
62+
63+
if self.api_url is not None:
64+
new_proxy = self.get_proxy_url_from_api(reason)
65+
else:
66+
new_proxy = urllib.request.getproxies().get("http")
67+
68+
if old_proxy != new_proxy:
69+
self.log.debug("Set new proxy URL: %s", new_proxy)
70+
self.current_proxy_url = new_proxy
71+
return True
72+
73+
self.log.debug("Got same proxy URL: %s", new_proxy)
74+
return False
75+
76+
def get_proxy_url(self) -> str | None:
77+
if not self.current_proxy_url:
78+
self.update_proxy_url()
79+
80+
return self.current_proxy_url
81+
82+
83+
T = TypeVar("T")
84+
85+
86+
async def proxy_with_retry(
87+
name: str,
88+
func: Callable[[], Awaitable[T]],
89+
logger: TraceLogger,
90+
proxy_handler: ProxyHandler,
91+
on_proxy_change: Callable[[], Awaitable[None]],
92+
max_retries: int = 10,
93+
) -> T:
94+
errors = 0
95+
96+
while True:
97+
try:
98+
return await func()
99+
except RETRYABLE_PROXY_EXCEPTIONS as e:
100+
errors += 1
101+
if errors > max_retries:
102+
raise
103+
wait = min(errors * 10, 60)
104+
logger.warning(
105+
"%s while trying to %s, retrying in %d seconds",
106+
e.__class__.__name__,
107+
name,
108+
wait,
109+
)
110+
if errors > 1 and proxy_handler.update_proxy_url(
111+
f"{e.__class__.__name__} while trying to {name}"
112+
):
113+
await on_proxy_change()

0 commit comments

Comments
 (0)