ZHIWEI666 commited on
Commit
77d7d28
·
verified ·
1 Parent(s): b39e3b7

Upload 2 files

Browse files
Files changed (2) hide show
  1. router_proxy.py +5 -17
  2. safe_http_transport.py +119 -6
router_proxy.py CHANGED
@@ -10,8 +10,6 @@ import ipaddress
10
  import httpx
11
  import os
12
  import re
13
- import asyncio
14
- import socket
15
  import urllib.request
16
  import urllib.error
17
  import 数据库连接 as json_db
@@ -22,7 +20,7 @@ from proxy_access import authorized_proxy_item, ProxyAccessError
22
  from github_source import repository_parts, select_github_token
23
  from github_archive import ARCHIVE_MAX_BYTES
24
  from 安全认证 import require_auth, JWT_SECRET
25
- from safe_http_transport import ValidatedAsyncHTTPTransport
26
  from workflow_download_service import prepare_workflow_download
27
  from zip_stream import ZIP_DOWNLOAD_MAX_BYTES
28
  from github_zip_service import prepare_github_zip
@@ -47,20 +45,10 @@ async def _validate_public_http_target(url: str) -> str:
47
 
48
  host = parsed.hostname.rstrip(".").lower()
49
  try:
50
- addresses = [ipaddress.ip_address(host)]
51
- except ValueError:
52
- try:
53
- infos = await asyncio.to_thread(
54
- socket.getaddrinfo, host, port or (443 if parsed.scheme == "https" else 80),
55
- type=socket.SOCK_STREAM,
56
- )
57
- except OSError:
58
- raise HTTPException(status_code=400, detail="下载地址无法解析")
59
- addresses = [ipaddress.ip_address(info[4][0].split("%", 1)[0]) for info in infos]
60
- if not addresses or any(not address.is_global or address.is_multicast
61
- or (getattr(address, 'ipv4_mapped', None) is not None
62
- and address.ipv4_mapped.is_multicast) for address in addresses):
63
- raise HTTPException(status_code=400, detail="下载地址解析到非公网地址")
64
  return host
65
 
66
 
 
10
  import httpx
11
  import os
12
  import re
 
 
13
  import urllib.request
14
  import urllib.error
15
  import 数据库连接 as json_db
 
20
  from github_source import repository_parts, select_github_token
21
  from github_archive import ARCHIVE_MAX_BYTES
22
  from 安全认证 import require_auth, JWT_SECRET
23
+ from safe_http_transport import ValidatedAsyncHTTPTransport, _resolve_public_ips, PublicResolutionError
24
  from workflow_download_service import prepare_workflow_download
25
  from zip_stream import ZIP_DOWNLOAD_MAX_BYTES
26
  from github_zip_service import prepare_github_zip
 
45
 
46
  host = parsed.hostname.rstrip(".").lower()
47
  try:
48
+ await _resolve_public_ips(host, port or (443 if parsed.scheme == "https" else 80))
49
+ except PublicResolutionError as error:
50
+ detail = "下载地址无法解析" if error.reason == "dns_unavailable" else "下载地址解析到非公网地址"
51
+ raise HTTPException(status_code=400, detail=detail) from None
 
 
 
 
 
 
 
 
 
 
52
  return host
53
 
54
 
safe_http_transport.py CHANGED
@@ -2,13 +2,114 @@
2
  from contextvars import ContextVar
3
  import asyncio
4
  import ipaddress
 
5
  import socket
6
 
7
  import httpcore
8
  import httpx
 
9
 
10
 
11
  _validated_ip = ContextVar("ranking_validated_ip", default=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
 
14
  async def _resolve_public_ips(host: str, port: int) -> tuple[str, ...]:
@@ -22,16 +123,18 @@ async def _resolve_public_ips(host: str, port: int) -> tuple[str, ...]:
22
  socket.getaddrinfo, normalized_host, port, type=socket.SOCK_STREAM
23
  )
24
  except OSError as exc:
25
- raise httpx.ConnectError("目标域名无法解析") from exc
 
 
26
  addresses = [
27
  ipaddress.ip_address(record[4][0].split("%", 1)[0])
28
  for record in records
29
  ]
30
 
31
- if not addresses or any(not address.is_global or address.is_multicast
32
- or (getattr(address, 'ipv4_mapped', None) is not None
33
- and address.ipv4_mapped.is_multicast) for address in addresses):
34
- raise httpx.ConnectError("目标域名解析到非公网地址")
35
  return tuple(dict.fromkeys(str(address) for address in addresses))
36
 
37
 
@@ -83,10 +186,13 @@ class ValidatedAsyncHTTPTransport(httpx.AsyncHTTPTransport):
83
  network_backend=self._network_backend,
84
  )
85
 
 
 
 
86
  async def handle_async_request(self, request):
87
  host = request.url.host.rstrip(".").lower()
88
  port = request.url.port or (443 if request.url.scheme == "https" else 80)
89
- pinned_ips = await _resolve_public_ips(host, port)
90
  token = _validated_ip.set(pinned_ips)
91
  try:
92
  # HTTP Core passes the original origin hostname to start_tls(), preserving
@@ -94,3 +200,10 @@ class ValidatedAsyncHTTPTransport(httpx.AsyncHTTPTransport):
94
  return await super().handle_async_request(request)
95
  finally:
96
  _validated_ip.reset(token)
 
 
 
 
 
 
 
 
2
  from contextvars import ContextVar
3
  import asyncio
4
  import ipaddress
5
+ import json
6
  import socket
7
 
8
  import httpcore
9
  import httpx
10
+ from proxy_resources import ProxyResources
11
 
12
 
13
  _validated_ip = ContextVar("ranking_validated_ip", default=None)
14
+ _DNS_MAX_BYTES = 16384
15
+ _DNS_TIMEOUT = 8
16
+
17
+
18
+ class PublicResolutionError(httpx.ConnectError):
19
+ def __init__(self, reason):
20
+ self.reason = reason
21
+ super().__init__("目标域名无法解析" if reason == "dns_unavailable" else "目标域名解析到非公网地址")
22
+
23
+
24
+ def _public(address):
25
+ mapped = getattr(address, "ipv4_mapped", None)
26
+ return (address.is_global and not address.is_multicast
27
+ and (mapped is None or (mapped.is_global and not mapped.is_multicast)))
28
+
29
+
30
+ def _dns_object(pairs):
31
+ result = {}
32
+ for key, value in pairs:
33
+ if key in result:
34
+ raise ValueError()
35
+ result[key] = value
36
+ return result
37
+
38
+
39
+ def _dns_addresses(body, host, record_type):
40
+ data = json.loads(body.decode("utf-8"), object_pairs_hook=_dns_object,
41
+ parse_constant=lambda _: (_ for _ in ()).throw(ValueError()))
42
+ if (not isinstance(data, dict) or type(data.get("Status")) is not int
43
+ or data["Status"] != 0 or data.get("TC") is not False):
44
+ raise ValueError()
45
+ question = data.get("Question")
46
+ if (not isinstance(question, list) or len(question) != 1
47
+ or not isinstance(question[0], dict)
48
+ or question[0].get("name", "").rstrip(".").lower() != host
49
+ or type(question[0].get("type")) is not int or question[0]["type"] != record_type):
50
+ raise ValueError()
51
+ answers = data.get("Answer", [])
52
+ if not isinstance(answers, list) or len(answers) > 32:
53
+ raise ValueError()
54
+ names = {host}
55
+ for _ in answers:
56
+ for answer in answers:
57
+ if not isinstance(answer, dict) or type(answer.get("type")) is not int:
58
+ raise ValueError()
59
+ if answer["type"] == 5 and answer.get("name", "").rstrip(".").lower() in names:
60
+ alias = answer.get("data")
61
+ if not isinstance(alias, str) or not alias or len(alias) > 253:
62
+ raise ValueError()
63
+ names.add(alias.rstrip(".").lower())
64
+ addresses = []
65
+ for answer in answers:
66
+ if answer["type"] in (1, 28):
67
+ address = ipaddress.ip_address(answer.get("data"))
68
+ if not _public(address) or address.version != (4 if answer["type"] == 1 else 6):
69
+ raise ValueError()
70
+ if (answer["type"] == record_type
71
+ and answer.get("name", "").rstrip(".").lower() in names):
72
+ addresses.append(str(address))
73
+ return tuple(dict.fromkeys(addresses))
74
+
75
+
76
+ async def _resolve_hf_public_ips(host):
77
+ client = httpx.AsyncClient(transport=_DNSHTTPTransport(), verify=True, trust_env=False,
78
+ follow_redirects=False, timeout=_DNS_TIMEOUT)
79
+ resources = ProxyResources(client)
80
+ try:
81
+ async with asyncio.timeout(_DNS_TIMEOUT):
82
+ for record_type in (1, 28):
83
+ request = client.build_request("GET", "https://cloudflare-dns.com/dns-query",
84
+ params={"name": host, "type": str(record_type)},
85
+ headers={"Accept": "application/dns-json", "Accept-Encoding": "identity"})
86
+ response = resources.track(await client.send(request, stream=True, follow_redirects=False))
87
+ if (response.status_code != 200
88
+ or response.headers.get("content-encoding", "identity").lower() != "identity"
89
+ or response.headers.get("content-type", "").split(";", 1)[0].lower()
90
+ not in ("application/dns-json", "application/json")):
91
+ raise ValueError()
92
+ length = response.headers.get("content-length")
93
+ if length is not None and (not length.isascii() or not length.isdecimal()
94
+ or len(length) > 6 or int(length) > _DNS_MAX_BYTES):
95
+ raise ValueError()
96
+ body = bytearray()
97
+ async for chunk in response.aiter_raw(4096):
98
+ if len(body) + len(chunk) > _DNS_MAX_BYTES:
99
+ raise ValueError()
100
+ body.extend(chunk)
101
+ if length is not None and len(body) != int(length):
102
+ raise ValueError()
103
+ addresses = _dns_addresses(body, host, record_type)
104
+ await resources.close_response(response)
105
+ if addresses:
106
+ return addresses
107
+ raise ValueError()
108
+ except (ValueError, TypeError, AttributeError, UnicodeError, RecursionError,
109
+ httpx.HTTPError, TimeoutError):
110
+ raise PublicResolutionError("dns_unavailable") from None
111
+ finally:
112
+ await resources.close()
113
 
114
 
115
  async def _resolve_public_ips(host: str, port: int) -> tuple[str, ...]:
 
123
  socket.getaddrinfo, normalized_host, port, type=socket.SOCK_STREAM
124
  )
125
  except OSError as exc:
126
+ if normalized_host == "huggingface.co":
127
+ return await _resolve_hf_public_ips(normalized_host)
128
+ raise PublicResolutionError("dns_unavailable") from exc
129
  addresses = [
130
  ipaddress.ip_address(record[4][0].split("%", 1)[0])
131
  for record in records
132
  ]
133
 
134
+ if not addresses or any(not _public(address) for address in addresses):
135
+ if normalized_host == "huggingface.co":
136
+ return await _resolve_hf_public_ips(normalized_host)
137
+ raise PublicResolutionError("dns_non_public")
138
  return tuple(dict.fromkeys(str(address) for address in addresses))
139
 
140
 
 
186
  network_backend=self._network_backend,
187
  )
188
 
189
+ async def _resolve(self, host, port):
190
+ return await _resolve_public_ips(host, port)
191
+
192
  async def handle_async_request(self, request):
193
  host = request.url.host.rstrip(".").lower()
194
  port = request.url.port or (443 if request.url.scheme == "https" else 80)
195
+ pinned_ips = await self._resolve(host, port)
196
  token = _validated_ip.set(pinned_ips)
197
  try:
198
  # HTTP Core passes the original origin hostname to start_tls(), preserving
 
200
  return await super().handle_async_request(request)
201
  finally:
202
  _validated_ip.reset(token)
203
+
204
+
205
+ class _DNSHTTPTransport(ValidatedAsyncHTTPTransport):
206
+ async def _resolve(self, host, port):
207
+ if host != "cloudflare-dns.com" or port != 443:
208
+ raise PublicResolutionError("dns_unavailable")
209
+ return ("1.1.1.1",)