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:
user
2026-02-15 15:53:49 +01:00
parent 10f62631be
commit 97bbc6a825
19 changed files with 151 additions and 47 deletions

View File

@@ -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()

View File

@@ -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())

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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", "")

View File

@@ -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()

View File

@@ -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:

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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", "")