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>
91 lines
3.6 KiB
Python
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"
|