44from __future__ import annotations
55
66import argparse
7+ import ipaddress
78import json
89import os
910import sys
1011import urllib .error
12+ import urllib .parse
1113import urllib .request
1214from typing import Any
1315
1416
17+ def validate_api_base (value : str ) -> str :
18+ """Return a canonical HTTP origin for an explicit numeric-loopback listener."""
19+
20+ message = "--api-base must be an HTTP numeric-loopback root origin with an explicit port"
21+ try :
22+ parsed = urllib .parse .urlsplit (value )
23+ port = parsed .port
24+ address = ipaddress .ip_address (parsed .hostname or "" )
25+ except (TypeError , ValueError ) as exc :
26+ raise ValueError (message ) from exc
27+
28+ if (
29+ parsed .scheme != "http"
30+ or not address .is_loopback
31+ or getattr (address , "ipv4_mapped" , None ) is not None
32+ or getattr (address , "scope_id" , None ) is not None
33+ or parsed .username is not None
34+ or parsed .password is not None
35+ or port is None
36+ or parsed .path not in {"" , "/" }
37+ or parsed .query
38+ or parsed .fragment
39+ ):
40+ raise ValueError (message )
41+
42+ host = f"[{ address .compressed } ]" if address .version == 6 else address .compressed
43+ return f"http://{ host } :{ port } "
44+
45+
1546def request_json (
1647 api_base : str ,
1748 path : str ,
1849 * ,
1950 body : dict [str , object ] | None = None ,
2051 token : str | None = None ,
2152) -> dict [str , Any ]:
53+ api_origin = validate_api_base (api_base )
2254 headers = {"Accept" : "application/json" }
2355 data = None
2456 method = "GET"
@@ -29,20 +61,21 @@ def request_json(
2961 if token :
3062 headers ["Authorization" ] = f"Bearer { token } "
3163
32- request = urllib .request .Request (
33- api_base .rstrip ("/" ) + path ,
64+ # The URL origin is canonicalized above without DNS resolution or userinfo.
65+ request = urllib .request .Request ( # noqa: S310
66+ api_origin + path ,
3467 data = data ,
3568 headers = headers ,
3669 method = method ,
3770 )
3871 try :
39- with urllib .request .urlopen (request , timeout = 30 ) as response :
72+ with urllib .request .urlopen (request , timeout = 30 ) as response : # noqa: S310
4073 return json .load (response )
4174 except urllib .error .HTTPError as exc :
4275 detail = exc .read ().decode ("utf-8" , errors = "replace" )
4376 raise RuntimeError (f"{ path } returned HTTP { exc .code } : { detail } " ) from exc
4477 except urllib .error .URLError as exc :
45- raise RuntimeError (f"could not reach { api_base } : { exc .reason } " ) from exc
78+ raise RuntimeError (f"could not reach { api_origin } : { exc .reason } " ) from exc
4679
4780
4881def print_result (label : str , payload : dict [str , Any ]) -> None :
0 commit comments