Files
DemonGatanjieu-Anomalous_Mo…/net_proxy.py
T
DemonGatanjieuandClaude Opus 5.5 6f4347d88d Settings -> Network: a proxy for the plugin, and why Civitai cannot be reached
A Bilibili user's scan said Civitai could not be reached while their
browser reached it through a VPN app: the plugin's urllib requests only
use HTTPS_PROXY or the Windows proxy, not a PAC script or a browser-only
proxy.

net_proxy.py: the proxy address saved in api/config.json (NETWORK_PROXY)
that every request of the plugin goes through (Civitai, Hugging Face,
GitHub, translators), handed to the scan process as HTTP(S)_PROXY; empty
keeps the old behaviour. It also names why a request failed (timeout,
refused, dns, ssl, reset, proxy, http:<code>).

Settings -> Network has the address (SOCKS and malformed ones refused
with what to write) and Test connection, which calls 1.60's
/anomalous/civitai_check (brought here unchanged, now through the proxy
and with the failure's name). A scan that loses Civitai records why
(civitai_error) and the scan page and activity log say it with what to
try.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:10:34 +08:00

91 lines
3.6 KiB
Python

"""The proxy the plugin's own web requests go through, and why a request failed.
Settings -> Network keeps a proxy address in api/config.json (NETWORK_PROXY). When it is set,
every request of the plugin (Civitai, Hugging Face, GitHub, the translators) goes through it,
and the scan process gets it as HTTP(S)_PROXY. When it is empty, urllib finds the proxy itself:
HTTPS_PROXY, else the Windows proxy (not a PAC script, not a browser extension). ComfyUI's own
requests and other node packs are not touched.
`failure()` names why a request failed in a few fixed words the pages explain. No imports from
ComfyUI or the api package, so the scan process (scraper.py) uses it too.
"""
import json
import os
import re
import socket
import ssl
import urllib.error
import urllib.request
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "api", "config.json")
CONFIG_KEY = "NETWORK_PROXY"
_ADDRESS = re.compile(r"^(?:(https?)://)?([A-Za-z0-9.\-]+|\[[0-9A-Fa-f:]+\]):(\d{1,5})/?$", re.IGNORECASE)
def normalize(text) -> str:
"""'' or 'http://host:port'. ValueError('socks') for a SOCKS address (urllib cannot use
one; proxy apps also offer an HTTP port), ValueError('format') for anything else."""
value = str(text or "").strip()
if not value:
return ""
if value.lower().startswith("socks"):
raise ValueError("socks")
match = _ADDRESS.match(value)
if not match or not 0 < int(match.group(3)) < 65536:
raise ValueError("format")
return f"{(match.group(1) or 'http').lower()}://{match.group(2)}:{int(match.group(3))}"
def configured() -> str:
"""The saved proxy address, or '' (none saved, or the file unreadable)."""
try:
with open(CONFIG_PATH, encoding="utf-8") as f:
return normalize(json.load(f).get(CONFIG_KEY, ""))
except (OSError, ValueError, AttributeError):
return ""
def urlopen(request, timeout, context=None):
"""urllib.request.urlopen through the saved proxy, when there is one."""
proxy = configured()
if not proxy:
if context is None:
return urllib.request.urlopen(request, timeout=timeout)
return urllib.request.urlopen(request, timeout=timeout, context=context)
handlers = [urllib.request.ProxyHandler({"http": proxy, "https": proxy})]
if context is not None:
handlers.append(urllib.request.HTTPSHandler(context=context))
return urllib.request.build_opener(*handlers).open(request, timeout=timeout)
def child_env() -> dict:
"""The environment for a process of ours (the scan) that should use the same proxy."""
env = dict(os.environ)
proxy = configured()
if proxy:
env.update(HTTP_PROXY=proxy, HTTPS_PROXY=proxy, http_proxy=proxy, https_proxy=proxy)
return env
def failure(error) -> str:
"""'timeout' | 'refused' | 'dns' | 'ssl' | 'reset' | 'proxy' | 'http:<code>' | 'other'."""
if isinstance(error, urllib.error.HTTPError):
return f"http:{error.code}"
reason = error.reason if isinstance(error, urllib.error.URLError) else error
if isinstance(reason, (socket.timeout, TimeoutError)):
return "timeout"
if isinstance(reason, ConnectionRefusedError):
return "refused"
if isinstance(reason, socket.gaierror):
return "dns"
if isinstance(reason, (ssl.SSLError, ssl.CertificateError)):
return "ssl"
if isinstance(reason, (ConnectionResetError, ConnectionAbortedError)):
return "reset"
if "tunnel connection failed" in str(reason).lower():
return "proxy"
if "timed out" in str(reason).lower():
return "timeout"
return "other"