修复代理 Fake-IP 导致 API 任务恢复查询和下载失败
This commit is contained in:
@@ -214,6 +214,10 @@ internal signing secret, and shared output directory across restarts. The secret
|
||||
needed to decrypt pending query credentials; terminal checkpoints discard credentials.
|
||||
Multiple workers must share these paths and support the recovery file lock.
|
||||
|
||||
Recovery queries reuse the original API endpoint and system proxy routing. When a
|
||||
configured HTTP(S) proxy uses Fake-IP DNS, media address validation checks the
|
||||
hostname through AliDNS over HTTPS; private addresses remain blocked.
|
||||
|
||||
## API Access
|
||||
|
||||
Authenticated API calls can include either:
|
||||
|
||||
@@ -183,7 +183,8 @@ class ApiRecoveryTests(unittest.TestCase):
|
||||
response.__enter__.return_value = response
|
||||
response.status_code = 200
|
||||
response.json.return_value = {"status": "running"}
|
||||
with patch.object(recovery, "_validate_public_http_url"), patch.object(recovery.requests, "request", return_value=response) as request:
|
||||
# A local proxy may resolve public API hosts to benchmark-range Fake-IP addresses.
|
||||
with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("198.18.0.83", 443))]), patch.object(recovery.requests, "request", return_value=response) as request:
|
||||
self.manager._query(row)
|
||||
args, kwargs = request.call_args
|
||||
self.assertEqual(("GET", "https://api.example/api/v3/contents/generations/tasks/task-1"), args)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import io
|
||||
import json
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from test_cloud_archive import cloud
|
||||
|
||||
|
||||
class RemoteAddressTests(unittest.TestCase):
|
||||
def validate(self, address, *, host="cdn.example", proxy=True, public_answer="8.8.8.8"):
|
||||
response = io.BytesIO(json.dumps({"Status": 0, "Answer": [
|
||||
{"type": 1, "data": public_answer},
|
||||
]}).encode())
|
||||
with patch.object(cloud.socket, "getaddrinfo", return_value=[(2, 1, 6, "", (address, 443))]), \
|
||||
patch.object(cloud.urllib.request, "getproxies", return_value={"https": "http://127.0.0.1:7897"} if proxy else {}), \
|
||||
patch.object(cloud.urllib.request, "proxy_bypass", return_value=False), \
|
||||
patch.object(cloud.urllib.request, "urlopen", return_value=response) as lookup:
|
||||
cloud._validate_public_http_url("https://" + host + "/video.mp4?signature=private")
|
||||
return lookup.call_args_list
|
||||
|
||||
def test_proxy_fake_ip_validates_public_dns_without_sending_signed_url(self):
|
||||
calls = self.validate("198.18.0.86")
|
||||
self.assertEqual(1, len(calls))
|
||||
url = calls[0].args[0]
|
||||
self.assertIn("name=cdn.example", url)
|
||||
self.assertNotIn("signature", url)
|
||||
|
||||
def test_public_address_needs_no_extra_lookup(self):
|
||||
self.assertEqual([], self.validate("8.8.8.8"))
|
||||
|
||||
def test_private_loopback_and_metadata_addresses_are_rejected(self):
|
||||
for address in ("127.0.0.1", "10.0.0.1", "169.254.169.254", "::1"):
|
||||
with self.subTest(address=address), self.assertRaises(ValueError):
|
||||
self.validate(address)
|
||||
|
||||
def test_fake_ip_without_proxy_is_rejected(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.validate("198.18.0.86", proxy=False)
|
||||
|
||||
def test_literal_fake_ip_is_rejected_even_with_proxy(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.validate("198.18.0.86", host="198.18.0.86")
|
||||
|
||||
def test_private_public_dns_answer_is_rejected(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.validate("198.18.0.86", public_answer="10.0.0.1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -17,7 +17,7 @@ from cryptography.fernet import Fernet
|
||||
from filelock import FileLock, Timeout
|
||||
|
||||
from .api_audit import suspend_api_audit
|
||||
from .cloud_archive import download_remote, _validate_public_http_url
|
||||
from .cloud_archive import download_remote
|
||||
from .recovery_providers import submission, result
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@ class ApiRecoveryManager:
|
||||
logger.error("API result recovery iteration failed; retrying")
|
||||
|
||||
def _query(self, row):
|
||||
_validate_public_http_url(row["query_url"])
|
||||
# This endpoint comes from the authenticated submission, not returned media.
|
||||
# Use the original client's proxy/DNS routing; Fake-IP is not the API's address.
|
||||
authorization = self.cipher.decrypt(row["credentials"]).decode()
|
||||
with requests.request(
|
||||
row["query_method"], row["query_url"],
|
||||
|
||||
+20
-2
@@ -894,8 +894,26 @@ def _validate_public_http_url(url: str) -> None:
|
||||
addresses = socket.getaddrinfo(parsed.hostname, parsed.port or 443, type=socket.SOCK_STREAM)
|
||||
except socket.gaierror as error:
|
||||
raise ValueError(f"Unable to resolve artifact host: {error}") from error
|
||||
for address in addresses:
|
||||
ip = ipaddress.ip_address(address[4][0])
|
||||
ips = [ipaddress.ip_address(address[4][0]) for address in addresses]
|
||||
fake_network = ipaddress.ip_network("198.18.0.0/15")
|
||||
if any(ip in fake_network for ip in ips):
|
||||
# Proxy Fake-IP is a routing token. Validate the hostname's public DNS
|
||||
# answer instead; never allow literal Fake-IP URLs or private answers.
|
||||
try:
|
||||
ipaddress.ip_address(parsed.hostname)
|
||||
literal = True
|
||||
except ValueError:
|
||||
literal = False
|
||||
proxied = urllib.request.getproxies().get(parsed.scheme) and not urllib.request.proxy_bypass(parsed.hostname)
|
||||
if not literal and proxied and all(ip in fake_network or ip.is_global for ip in ips):
|
||||
query = urllib.parse.urlencode({"name": parsed.hostname, "type": "A"})
|
||||
with urllib.request.urlopen("https://dns.alidns.com/resolve?" + query, timeout=10) as response:
|
||||
answer = json.loads(response.read(65536))
|
||||
ips = [ipaddress.ip_address(record["data"]) for record in answer.get("Answer", [])
|
||||
if record.get("type") in (1, 28)] if answer.get("Status") == 0 else []
|
||||
if not ips:
|
||||
raise ValueError("Public DNS lookup returned no artifact address")
|
||||
for ip in ips:
|
||||
if not ip.is_global:
|
||||
raise ValueError("Remote artifact URL resolves to a non-public address")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user