Add SOCKS4/SOCKS4a proxy support to search engine

Pass 'Perform hostname lookup via proxy' setting along the way.
Also add underline to variables and functions that are private to the python module.

PR #22510.
This commit is contained in:
Chocobo1 2025-04-13 16:25:38 +08:00 committed by GitHub
commit 3d73026ff2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 25 deletions

View file

@ -409,11 +409,11 @@ Path SearchPluginManager::engineLocation()
void SearchPluginManager::applyProxySettings() void SearchPluginManager::applyProxySettings()
{ {
// Define environment variables for urllib in search engine plugins // for python `urllib`: https://docs.python.org/3/library/urllib.request.html#urllib.request.ProxyHandler
const QString HTTP_PROXY = u"http_proxy"_s; const QString HTTP_PROXY = u"http_proxy"_s;
const QString HTTPS_PROXY = u"https_proxy"_s; const QString HTTPS_PROXY = u"https_proxy"_s;
const QString SOCKS_PROXY = u"sock_proxy"_s; // for `helpers.setupSOCKSProxy()`: https://everything.curl.dev/usingcurl/proxies/socks.html
const QString SOCKS_PROXY = u"qbt_socks_proxy"_s;
if (!Preferences::instance()->useProxyForGeneralPurposes()) if (!Preferences::instance()->useProxyForGeneralPurposes())
{ {
@ -427,7 +427,6 @@ void SearchPluginManager::applyProxySettings()
switch (proxyConfig.type) switch (proxyConfig.type)
{ {
case Net::ProxyType::None: case Net::ProxyType::None:
case Net::ProxyType::SOCKS4: // TODO: implement python code
m_proxyEnv.remove(HTTP_PROXY); m_proxyEnv.remove(HTTP_PROXY);
m_proxyEnv.remove(HTTPS_PROXY); m_proxyEnv.remove(HTTPS_PROXY);
m_proxyEnv.remove(SOCKS_PROXY); m_proxyEnv.remove(SOCKS_PROXY);
@ -435,9 +434,12 @@ void SearchPluginManager::applyProxySettings()
case Net::ProxyType::HTTP: case Net::ProxyType::HTTP:
{ {
const QString proxyURL = proxyConfig.authEnabled const QString credential = proxyConfig.authEnabled
? u"http://%1:%2@%3:%4"_s.arg(proxyConfig.username, proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port)) ? (proxyConfig.username + u':' + proxyConfig.password + u'@')
: u"http://%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port)); : QString();
const QString proxyURL = u"http://%1%2:%3"_s
.arg(credential, proxyConfig.ip, QString::number(proxyConfig.port));
m_proxyEnv.insert(HTTP_PROXY, proxyURL); m_proxyEnv.insert(HTTP_PROXY, proxyURL);
m_proxyEnv.insert(HTTPS_PROXY, proxyURL); m_proxyEnv.insert(HTTPS_PROXY, proxyURL);
m_proxyEnv.remove(SOCKS_PROXY); m_proxyEnv.remove(SOCKS_PROXY);
@ -446,9 +448,25 @@ void SearchPluginManager::applyProxySettings()
case Net::ProxyType::SOCKS5: case Net::ProxyType::SOCKS5:
{ {
const QString proxyURL = proxyConfig.authEnabled const QString scheme = proxyConfig.hostnameLookupEnabled ? u"socks5h"_s : u"socks5"_s;
? u"%1:%2@%3:%4"_s.arg(proxyConfig.username, proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port)) const QString credential = proxyConfig.authEnabled
: u"%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port)); ? (proxyConfig.username + u':' + proxyConfig.password + u'@')
: QString();
const QString proxyURL = u"%1://%2%3:%4"_s
.arg(scheme, credential, proxyConfig.ip, QString::number(proxyConfig.port));
m_proxyEnv.remove(HTTP_PROXY);
m_proxyEnv.remove(HTTPS_PROXY);
m_proxyEnv.insert(SOCKS_PROXY, proxyURL);
}
break;
case Net::ProxyType::SOCKS4:
{
const QString scheme = proxyConfig.hostnameLookupEnabled ? u"socks4a"_s : u"socks4"_s;
const QString proxyURL = u"%1://%2:%3"_s
.arg(scheme, proxyConfig.ip, QString::number(proxyConfig.port));
m_proxyEnv.remove(HTTP_PROXY); m_proxyEnv.remove(HTTP_PROXY);
m_proxyEnv.remove(HTTPS_PROXY); m_proxyEnv.remove(HTTPS_PROXY);
m_proxyEnv.insert(SOCKS_PROXY, proxyURL); m_proxyEnv.insert(SOCKS_PROXY, proxyURL);

View file

@ -1,4 +1,4 @@
#VERSION: 1.51 #VERSION: 1.52
# Author: # Author:
# Christophe DUMEZ (chris@qbittorrent.org) # Christophe DUMEZ (chris@qbittorrent.org)
@ -32,19 +32,19 @@ import gzip
import html import html
import io import io
import os import os
import re
import socket import socket
import socks import socks
import ssl import ssl
import sys import sys
import tempfile import tempfile
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any, Optional from typing import Any, Optional
def getBrowserUserAgent() -> str: def _getBrowserUserAgent() -> str:
""" Disguise as browser to circumvent website blocking """ """ Disguise as browser to circumvent website blocking """
# Firefox release calendar # Firefox release calendar
@ -60,17 +60,33 @@ def getBrowserUserAgent() -> str:
return f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{nowVersion}.0) Gecko/20100101 Firefox/{nowVersion}.0" return f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{nowVersion}.0) Gecko/20100101 Firefox/{nowVersion}.0"
headers: dict[str, Any] = {'User-Agent': getBrowserUserAgent()} _headers: dict[str, Any] = {'User-Agent': _getBrowserUserAgent()}
# SOCKS5 Proxy support
if "sock_proxy" in os.environ and len(os.environ["sock_proxy"].strip()) > 0: def injectSOCKSProxySocket() -> None:
proxy_str = os.environ["sock_proxy"].strip() socksURL = os.environ.get("qbt_socks_proxy")
m = re.match(r"^(?:(?P<username>[^:]+):(?P<password>[^@]+)@)?(?P<host>[^:]+):(?P<port>\w+)$", if socksURL is not None:
proxy_str) parts = urllib.parse.urlsplit(socksURL)
if m is not None: resolveHostname = (parts.scheme == "socks4a") or (parts.scheme == "socks5h")
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, m.group('host'), if (parts.scheme == "socks4") or (parts.scheme == "socks4a"):
int(m.group('port')), True, m.group('username'), m.group('password')) socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS4, parts.hostname, parts.port, resolveHostname)
socket.socket = socks.socksocket # type: ignore[misc] socket.socket = socks.socksocket # type: ignore[misc]
elif (parts.scheme == "socks5") or (parts.scheme == "socks5h"):
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, parts.hostname, parts.port, resolveHostname, parts.username, parts.password)
socket.socket = socks.socksocket # type: ignore[misc]
else:
# the following code provide backward compatibility for older qbt versions
# TODO: scheduled be removed with qbt >= 5.3
legacySocksURL = os.environ.get("sock_proxy")
if legacySocksURL is not None:
legacySocksURL = f"socks5h://{legacySocksURL.strip()}"
parts = urllib.parse.urlsplit(legacySocksURL)
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, parts.hostname, parts.port, True, parts.username, parts.password)
socket.socket = socks.socksocket # type: ignore[misc]
# monkey patching, run it now
injectSOCKSProxySocket()
# This is only provided for backward compatibility, new code should not use it # This is only provided for backward compatibility, new code should not use it
@ -80,7 +96,7 @@ htmlentitydecode = html.unescape
def retrieve_url(url: str, custom_headers: Mapping[str, Any] = {}, request_data: Optional[Any] = None, ssl_context: Optional[ssl.SSLContext] = None, unescape_html_entities: bool = True) -> str: def retrieve_url(url: str, custom_headers: Mapping[str, Any] = {}, request_data: Optional[Any] = None, ssl_context: Optional[ssl.SSLContext] = None, unescape_html_entities: bool = True) -> str:
""" Return the content of the url page as a string """ """ Return the content of the url page as a string """
request = urllib.request.Request(url, request_data, {**headers, **custom_headers}) request = urllib.request.Request(url, request_data, {**_headers, **custom_headers})
try: try:
response = urllib.request.urlopen(request, context=ssl_context) response = urllib.request.urlopen(request, context=ssl_context)
except urllib.error.URLError as errno: except urllib.error.URLError as errno:
@ -112,7 +128,7 @@ def download_file(url: str, referer: Optional[str] = None, ssl_context: Optional
""" Download file at url and write it to a file, return the path to the file and the url """ """ Download file at url and write it to a file, return the path to the file and the url """
# Download url # Download url
request = urllib.request.Request(url, headers=headers) request = urllib.request.Request(url, headers=_headers)
if referer is not None: if referer is not None:
request.add_header('referer', referer) request.add_header('referer', referer)
response = urllib.request.urlopen(request, context=ssl_context) response = urllib.request.urlopen(request, context=ssl_context)