feat: route plugin HTTP traffic through SOCKS5 proxy
Add PySocks dependency and shared src/derp/http.py module providing proxy-aware urlopen() and build_opener() that route through socks5h://127.0.0.1:1080. Subclassed SocksiPyHandler passes SSL context through to HTTPS connections. Swapped 14 external-facing plugins to use the proxied helpers. Local-only traffic (SearXNG, raw DNS/TLS sockets) stays direct. Updated test mocks in test_twitch and test_alert accordingly.
This commit is contained in:
@@ -5,10 +5,10 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command, event
|
||||
|
||||
# -- Constants ---------------------------------------------------------------
|
||||
@@ -106,8 +106,7 @@ def _search_youtube(keyword: str) -> list[dict]:
|
||||
req = urllib.request.Request(_YT_SEARCH_URL, data=payload, method="POST")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
resp = urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT, context=ctx)
|
||||
resp = _urlopen(req, timeout=_FETCH_TIMEOUT)
|
||||
raw = resp.read()
|
||||
resp.close()
|
||||
|
||||
@@ -141,8 +140,7 @@ def _search_twitch(keyword: str) -> list[dict]:
|
||||
req.add_header("Client-Id", _GQL_CLIENT_ID)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
resp = urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT, context=ctx)
|
||||
resp = _urlopen(req, timeout=_FETCH_TIMEOUT)
|
||||
raw = resp.read()
|
||||
resp.close()
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -32,7 +33,7 @@ def fetch_crtsh(domain: str) -> list[dict]:
|
||||
"""GET crt.sh JSON for a domain. Blocking."""
|
||||
url = _CRTSH_URL.format(domain=domain)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "derp-irc-bot"})
|
||||
with urllib.request.urlopen(req, timeout=_CRTSH_TIMEOUT) as resp:
|
||||
with _urlopen(req, timeout=_CRTSH_TIMEOUT) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -121,7 +122,7 @@ async def _download_nvd() -> tuple[int, str]:
|
||||
|
||||
def _fetch(url):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "derp-bot"})
|
||||
with urllib.request.urlopen(req, timeout=120) as resp: # noqa: S310
|
||||
with _urlopen(req, timeout=120) as resp:
|
||||
return resp.read()
|
||||
|
||||
try:
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -85,7 +86,7 @@ async def _download_csv() -> tuple[int, str]:
|
||||
|
||||
def _fetch():
|
||||
req = urllib.request.Request(_CSV_URL, headers={"User-Agent": "derp-bot"})
|
||||
with urllib.request.urlopen(req, timeout=60) as resp: # noqa: S310
|
||||
with _urlopen(req, timeout=60) as resp:
|
||||
return resp.read()
|
||||
|
||||
try:
|
||||
|
||||
@@ -8,6 +8,7 @@ import re
|
||||
import ssl
|
||||
import urllib.request
|
||||
|
||||
from derp.http import build_opener as _build_opener
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -84,9 +85,7 @@ def _fetch_headers(url: str) -> tuple[dict[str, str], str]:
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPSHandler(context=ctx),
|
||||
)
|
||||
opener = _build_opener(context=ctx)
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
req.add_header("User-Agent", _USER_AGENT)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import ssl
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from derp.http import build_opener as _build_opener
|
||||
from derp.plugin import command
|
||||
|
||||
_TIMEOUT = 10
|
||||
@@ -41,10 +42,7 @@ def _check(url: str) -> dict:
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
opener = urllib.request.build_opener(
|
||||
NoRedirect,
|
||||
urllib.request.HTTPSHandler(context=ctx),
|
||||
)
|
||||
opener = _build_opener(NoRedirect, context=ctx)
|
||||
|
||||
req = urllib.request.Request(url, method="HEAD")
|
||||
req.add_header("User-Agent", _USER_AGENT)
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -111,7 +112,7 @@ async def _download_feeds() -> tuple[int, int]:
|
||||
async def _fetch_one(filename: str, url: str) -> bool:
|
||||
def _do():
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "derp-bot"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310
|
||||
with _urlopen(req, timeout=30) as resp:
|
||||
return resp.read()
|
||||
|
||||
try:
|
||||
|
||||
@@ -5,12 +5,12 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command, event
|
||||
|
||||
# -- Constants ---------------------------------------------------------------
|
||||
@@ -111,10 +111,8 @@ def _fetch_feed(url: str, etag: str = "", last_modified: str = "") -> dict:
|
||||
if last_modified:
|
||||
req.add_header("If-Modified-Since", last_modified)
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT, context=ctx)
|
||||
resp = _urlopen(req, timeout=_FETCH_TIMEOUT)
|
||||
result["status"] = resp.status
|
||||
result["body"] = resp.read()
|
||||
result["etag"] = resp.headers.get("ETag", "")
|
||||
|
||||
@@ -12,6 +12,7 @@ import socket
|
||||
import struct
|
||||
import urllib.request
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -130,7 +131,7 @@ def _fetch_crtsh(domain: str) -> set[str]:
|
||||
"""Fetch subdomains from crt.sh CT logs. Blocking."""
|
||||
url = _CRTSH_URL.format(domain=domain)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "derp-bot"})
|
||||
with urllib.request.urlopen(req, timeout=_CRTSH_TIMEOUT) as resp: # noqa: S310
|
||||
with _urlopen(req, timeout=_CRTSH_TIMEOUT) as resp:
|
||||
data = json.loads(resp.read())
|
||||
|
||||
subs: set[str] = set()
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -66,7 +67,7 @@ async def _download_exits() -> int:
|
||||
|
||||
def _fetch():
|
||||
req = urllib.request.Request(_TOR_EXIT_URL, headers={"User-Agent": "derp-bot"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310
|
||||
with _urlopen(req, timeout=30) as resp:
|
||||
return resp.read().decode("utf-8", errors="replace")
|
||||
|
||||
try:
|
||||
|
||||
@@ -5,10 +5,10 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command, event
|
||||
|
||||
# -- Constants ---------------------------------------------------------------
|
||||
@@ -79,10 +79,8 @@ def _query_stream(login: str) -> dict:
|
||||
req.add_header("Client-Id", _GQL_CLIENT_ID)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT, context=ctx)
|
||||
resp = _urlopen(req, timeout=_FETCH_TIMEOUT)
|
||||
raw = resp.read()
|
||||
resp.close()
|
||||
data = json.loads(raw)
|
||||
|
||||
@@ -17,6 +17,7 @@ import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -94,7 +95,7 @@ def _http_get(url: str, timeout: int = _TIMEOUT) -> tuple[int, str]:
|
||||
|
||||
req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||
with _urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||
body = resp.read().decode("utf-8", errors="replace")
|
||||
return resp.status, body
|
||||
except urllib.error.HTTPError as exc:
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import logging
|
||||
import urllib.request
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -28,7 +29,7 @@ def _lookup(url: str, timestamp: str = "") -> dict:
|
||||
)
|
||||
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=_TIMEOUT)
|
||||
resp = _urlopen(req, timeout=_TIMEOUT)
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
resp.close()
|
||||
return data
|
||||
|
||||
@@ -5,12 +5,12 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command, event
|
||||
|
||||
# -- Constants ---------------------------------------------------------------
|
||||
@@ -97,9 +97,8 @@ def _resolve_channel(url: str) -> str | None:
|
||||
"""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
req.add_header("User-Agent", _BROWSER_UA)
|
||||
ctx = ssl.create_default_context()
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT, context=ctx)
|
||||
resp = _urlopen(req, timeout=_FETCH_TIMEOUT)
|
||||
body = resp.read(1_048_576) # Read up to 1MB
|
||||
resp.close()
|
||||
except Exception:
|
||||
@@ -128,10 +127,8 @@ def _fetch_feed(url: str, etag: str = "", last_modified: str = "") -> dict:
|
||||
if last_modified:
|
||||
req.add_header("If-Modified-Since", last_modified)
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT, context=ctx)
|
||||
resp = _urlopen(req, timeout=_FETCH_TIMEOUT)
|
||||
result["status"] = resp.status
|
||||
result["body"] = resp.read()
|
||||
result["etag"] = resp.headers.get("ETag", "")
|
||||
|
||||
Reference in New Issue
Block a user