Преглед изворни кода

Exclude local networks for domain-wide Retry-After (#8195)

* Exclude local networks for domain-wide Retry-After
Retry-After will be applied by URL and not by domain for local networks.
fix https://github.com/FreshRSS/FreshRSS/issues/7880

* Improved logic for detection of local domains

* Support ip6-localhost and a couple more variants

* On more: .lan

* Resolve IP address

* Add .intranet
Alexandre Alapetite пре 4 месеци
родитељ
комит
947a8c015a
2 измењених фајлова са 46 додато и 18 уклоњено
  1. 4 1
      app/Utils/httpUtil.php
  2. 42 17
      lib/Minz/Request.php

+ 4 - 1
app/Utils/httpUtil.php

@@ -10,11 +10,14 @@ final class FreshRSS_http_Util {
 		if (!is_string($domain) || $domain === '') {
 			return '';
 		}
+		$domainWide = Minz_Request::serverIsPublic($domain);
 		$port = parse_url($url, PHP_URL_PORT);
 		if (is_int($port)) {
 			$domain .= ':' . $port;
 		}
-		return self::RETRY_AFTER_PATH . urlencode($domain) . (empty($proxy) ? '' : ('_' . urlencode($proxy))) . '.txt';
+		return self::RETRY_AFTER_PATH . urlencode($domain) .
+			($domainWide ? '' : '_' . hash('sha256', $url)) .
+			(empty($proxy) ? '' : '_' . urlencode($proxy)) . '.txt';
 	}
 
 	/**

+ 42 - 17
lib/Minz/Request.php

@@ -369,14 +369,37 @@ class Minz_Request {
 	}
 
 	/**
-	 * Test if a given server address is publicly accessible.
+	 * Resolve a hostname to its first found IP address (IPv4 or IPv6).
 	 *
-	 * Note: for the moment it tests only if address is corresponding to a
-	 * localhost address.
+	 * @param string $hostname the hostname to resolve (from IP to DNS)
+	 * @return string|null the resolved IP address, or null if resolution fails
+	 */
+	private static function resolveHostname(string $hostname): ?string {
+		if (filter_var($hostname, FILTER_VALIDATE_IP) !== false) {
+			return $hostname;	// Already an IP address
+		}
+
+		$fqdn = rtrim($hostname, '.') . '.';	// Ensure fully qualified domain name
+		$records = @dns_get_record($fqdn, DNS_A + DNS_AAAA);
+		if (!is_array($records) || empty($records)) {
+			return null;
+		}
+
+		// Return the first resolved IP (IPv4 or IPv6)
+		if (is_string($records[0]['ip'] ?? null)) {
+			return $records[0]['ip'];
+		}
+		if (is_string($records[0]['ipv6'] ?? null)) {
+			return $records[0]['ipv6'];
+		}
+		return null;
+	}
+
+	/**
+	 * Test whether a given server address appears to be publicly accessible.
 	 *
-	 * @param string $address the address to test, can be an IP or a URL.
-	 * @return bool true if server is accessible, false otherwise.
-	 * @todo improve test with a more valid technique (e.g. test with an external server?)
+	 * @param string $address the address to test, which can be an URL with a DNS or an IP.
+	 * @return bool true if server does not appear to be on some kind of local network, false otherwise (probably public).
 	 */
 	public static function serverIsPublic(string $address): bool {
 		if (strlen($address) < strlen('http://a.bc')) {
@@ -387,18 +410,20 @@ class Minz_Request {
 			return false;
 		}
 
-		$is_public = !in_array($host, [
-			'localhost',
-			'localhost.localdomain',
-			'[::1]',
-			'ip6-localhost',
-			'localhost6',
-			'localhost6.localdomain6',
-		], true);
+		$is_public = (str_contains($host, '.') || str_contains($host, ':'))	// TLD
+			&& !preg_match('/(^|\\.)(ipv6-)?(internal|intranet|lan|local|localdomain|localhost)6?$/', $host)	// DNS
+			&& !preg_match('/^(10|127|172[.](1[6-9]|2[0-9]|3[01])|192[.]168)[.]/', $host)	// IPv4
+			&& !preg_match('/^(\\[)?(::1|f[c-d][0-9a-f]{2}:|fe80:)(\\])?/i', $host);	// IPv6
 
-		if ($is_public) {
-			$is_public &= !preg_match('/^(10|127|172[.]16|192[.]168)[.]/', $host);
-			$is_public &= !preg_match('/^(\[)?(::1$|fc00::|fe80::)/i', $host);
+		// If $host looks public and is not an IP address, try to resolve it
+		if ($is_public && filter_var($host, FILTER_VALIDATE_IP) === false) {
+			$resolvedIp = self::resolveHostname($host);
+			if ($resolvedIp !== null && $resolvedIp !== $host) {
+				$resolvedAddress = str_contains($resolvedIp, ':') ? "http://[{$resolvedIp}]/" : "http://{$resolvedIp}/";
+				if ($resolvedAddress !== $address) {
+					$is_public &= self::serverIsPublic($resolvedAddress);
+				}
+			}
 		}
 
 		return (bool)$is_public;