diff --git a/src/File.php b/src/File.php index cf22f0633..da451ad3b 100644 --- a/src/File.php +++ b/src/File.php @@ -114,6 +114,8 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, } if (!$force_fsockopen && function_exists('curl_exec')) { $resolve = false; // FreshRSS + $proxy = $curl_options[CURLOPT_PROXY] ?? null; // FreshRSS + $proxy_type = $curl_options[CURLOPT_PROXYTYPE] ?? null; // FreshRSS if (empty($curl_options[CURLOPT_PROXY] ?? null)) { // FreshRSS $resolve = $this->get_curl_resolve_info($url); if ($resolve === null) { @@ -128,6 +130,52 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, if (!empty($resolve)) { $curl_options[CURLOPT_RESOLVE] = $resolve; // Prevent DNS rebinding } + } else { // FreshRSS + defined('CURLPROXY_HTTPS') or define('CURLPROXY_HTTPS', 2); // Compatibility cURL 7.51 + $proxy_scheme = null; + switch ($proxy_type) { + case CURLPROXY_HTTP: + $proxy_scheme = 'http'; + break; + case CURLPROXY_HTTPS: + $proxy_scheme = 'https'; + break; + case CURLPROXY_SOCKS4: + $proxy_scheme = 'socks4'; + break; + case CURLPROXY_SOCKS4A: + $proxy_scheme = 'socks4a'; + break; + case CURLPROXY_SOCKS5: + $proxy_scheme = 'socks5'; + break; + case CURLPROXY_SOCKS5_HOSTNAME: + $proxy_scheme = 'socks5h'; + break; + }; + if ($proxy_scheme === null) { + $this->error = 'Unsupported proxy type'; + $this->success = false; + return; + } + $proxy_url = "$proxy_scheme://$proxy"; // CURLOPT_PROXY ($proxy) is formatted as user:pass@hostname:port, with the part before @ being optional + $resolve = $this->get_curl_resolve_info($proxy_url, true); + if ($resolve === null) { + $this->error = 'Failed to fetch this URL, because the proxy’s IP is not in the allowlist [' . + \SimplePie\Misc::url_remove_credentials($url) . '] [' . + \SimplePie\Misc::url_remove_credentials($proxy_url) . ']'; + $this->success = false; + return; + } elseif ($resolve === false) { + $this->error = 'Failed to resolve proxy hostname: ' . \SimplePie\Misc::url_remove_credentials($proxy_url); + $this->success = false; + return; + } + $curl_options[CURLOPT_PROXY] = $resolve; // Translate from a hostname:port value to ip:port, in order to prevent DNS rebinding + if (defined('CURLOPT_PROXY_SSL_VERIFYHOST')) { + // Available as of PHP 7.3.0 and cURL 7.52.0 + $curl_options[CURLOPT_PROXY_SSL_VERIFYHOST] = 0; // Skip verifying the hostname (a bit unsafe, but needed since there is no CURLOPT_RESOLVE equivalent for proxy hostnames) + } } $this->method = \SimplePie\SimplePie::FILE_SOURCE_REMOTE | \SimplePie\SimplePie::FILE_SOURCE_CURL; $fp = self::curlInit($url, $timeout, $headers, $useragent, $curl_options); @@ -444,10 +492,27 @@ protected function on_http_response($response, array $curl_options = []): void /** * Event to allow inheriting classes to control fetching certain URLs. * @param string $url - * @return array|null|false Returns a value for CURLOPT_RESOLVE as an array, null if no allowed IPs were found, false if the domain failed to resolve. + * @return array|string|null|false Returns a value for CURLOPT_RESOLVE as an array, null if no allowed IPs were found, false if the domain failed to resolve. Can also be used for checking if the CURLOPT_PROXY value is allowed, by providing a proxy URL with the `for_proxy` parameter set to `true`. In that case, a string value will be returned with the hostname resolved to an IP if allowed. */ - protected function get_curl_resolve_info(string $url) + protected function get_curl_resolve_info(string $url, bool $for_proxy = false) { + if ($for_proxy) { + $parsed = parse_url($url); + if ($parsed === false) { + return false; + } + $credentials = ''; + $user = $parsed['user'] ?? null; + $pass = $parsed['pass'] ?? null; + if (is_string($user) && is_string($pass)) { + $credentials = "$user:$pass@"; + } + $proxy = $parsed['host'] ?? ''; + if (is_int($parsed['port'] ?? null)) { + $proxy .= ':' . $parsed['port']; + } + return $credentials . $proxy; + } return []; }