From 3d73026ff21bc88383964754898f50238bad02a6 Mon Sep 17 00:00:00 2001 From: Chocobo1 Date: Sun, 13 Apr 2025 16:25:38 +0800 Subject: [PATCH] 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. --- src/base/search/searchpluginmanager.cpp | 38 ++++++++++++++------ src/searchengine/nova3/helpers.py | 46 +++++++++++++++++-------- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/base/search/searchpluginmanager.cpp b/src/base/search/searchpluginmanager.cpp index 7d5122ef3..d3f3b0297 100644 --- a/src/base/search/searchpluginmanager.cpp +++ b/src/base/search/searchpluginmanager.cpp @@ -409,11 +409,11 @@ Path SearchPluginManager::engineLocation() 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 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()) { @@ -427,7 +427,6 @@ void SearchPluginManager::applyProxySettings() switch (proxyConfig.type) { case Net::ProxyType::None: - case Net::ProxyType::SOCKS4: // TODO: implement python code m_proxyEnv.remove(HTTP_PROXY); m_proxyEnv.remove(HTTPS_PROXY); m_proxyEnv.remove(SOCKS_PROXY); @@ -435,9 +434,12 @@ void SearchPluginManager::applyProxySettings() case Net::ProxyType::HTTP: { - const QString proxyURL = proxyConfig.authEnabled - ? u"http://%1:%2@%3:%4"_s.arg(proxyConfig.username, proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port)) - : u"http://%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port)); + const QString credential = proxyConfig.authEnabled + ? (proxyConfig.username + u':' + proxyConfig.password + u'@') + : 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(HTTPS_PROXY, proxyURL); m_proxyEnv.remove(SOCKS_PROXY); @@ -446,9 +448,25 @@ void SearchPluginManager::applyProxySettings() case Net::ProxyType::SOCKS5: { - const QString proxyURL = proxyConfig.authEnabled - ? u"%1:%2@%3:%4"_s.arg(proxyConfig.username, proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port)) - : u"%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port)); + const QString scheme = proxyConfig.hostnameLookupEnabled ? u"socks5h"_s : u"socks5"_s; + const QString credential = proxyConfig.authEnabled + ? (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(HTTPS_PROXY); m_proxyEnv.insert(SOCKS_PROXY, proxyURL); diff --git a/src/searchengine/nova3/helpers.py b/src/searchengine/nova3/helpers.py index 47db27bcc..85eec1541 100644 --- a/src/searchengine/nova3/helpers.py +++ b/src/searchengine/nova3/helpers.py @@ -1,4 +1,4 @@ -#VERSION: 1.51 +#VERSION: 1.52 # Author: # Christophe DUMEZ (chris@qbittorrent.org) @@ -32,19 +32,19 @@ import gzip import html import io import os -import re import socket import socks import ssl import sys import tempfile import urllib.error +import urllib.parse import urllib.request from collections.abc import Mapping from typing import Any, Optional -def getBrowserUserAgent() -> str: +def _getBrowserUserAgent() -> str: """ Disguise as browser to circumvent website blocking """ # 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" -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: - proxy_str = os.environ["sock_proxy"].strip() - m = re.match(r"^(?:(?P[^:]+):(?P[^@]+)@)?(?P[^:]+):(?P\w+)$", - proxy_str) - if m is not None: - socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, m.group('host'), - int(m.group('port')), True, m.group('username'), m.group('password')) - socket.socket = socks.socksocket # type: ignore[misc] + +def injectSOCKSProxySocket() -> None: + socksURL = os.environ.get("qbt_socks_proxy") + if socksURL is not None: + parts = urllib.parse.urlsplit(socksURL) + resolveHostname = (parts.scheme == "socks4a") or (parts.scheme == "socks5h") + if (parts.scheme == "socks4") or (parts.scheme == "socks4a"): + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS4, parts.hostname, parts.port, resolveHostname) + 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 @@ -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: """ 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: response = urllib.request.urlopen(request, context=ssl_context) 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 url - request = urllib.request.Request(url, headers=headers) + request = urllib.request.Request(url, headers=_headers) if referer is not None: request.add_header('referer', referer) response = urllib.request.urlopen(request, context=ssl_context)