codekingpro/portable-devtools
115k
1from __future__ import annotations2 3import ipaddress4import os5import re6import typing7from urllib.request import getproxies8 9from ._types import PrimitiveData10 11if typing.TYPE_CHECKING: # pragma: no cover12 from ._urls import URL13 14 15def primitive_value_to_str(value: PrimitiveData) -> str:16 """17 Coerce a primitive data type into a string value.18 19 Note that we prefer JSON-style 'true'/'false' for boolean values here.20 """21 if value is True:22 return "true"23 elif value is False:24 return "false"25 elif value is None:26 return ""27 return str(value)28 29 30def get_environment_proxies() -> dict[str, str | None]:31 """Gets proxy information from the environment"""32 33 # urllib.request.getproxies() falls back on System34 # Registry and Config for proxies on Windows and macOS.35 # We don't want to propagate non-HTTP proxies into36 # our configuration such as 'TRAVIS_APT_PROXY'.37 proxy_info = getproxies()38 mounts: dict[str, str | None] = {}39 40 for scheme in ("http", "https", "all"):41 if proxy_info.get(scheme):42 hostname = proxy_info[scheme]43 mounts[f"{scheme}://"] = (44 hostname if "://" in hostname else f"http://{hostname}"45 )46 47 no_proxy_hosts = [host.strip() for host in proxy_info.get("no", "").split(",")]48 for hostname in no_proxy_hosts:49 # See https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html for details50 # on how names in `NO_PROXY` are handled.51 if hostname == "*":52 # If NO_PROXY=* is used or if "*" occurs as any one of the comma53 # separated hostnames, then we should just bypass any information54 # from HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and always ignore55 # proxies.56 return {}57 elif hostname:58 # NO_PROXY=.google.com is marked as "all://*.google.com,59 # which disables "www.google.com" but not "google.com"60 # NO_PROXY=google.com is marked as "all://*google.com,61 # which disables "www.google.com" and "google.com".62 # (But not "wwwgoogle.com")63 # NO_PROXY can include domains, IPv6, IPv4 addresses and "localhost"64 # NO_PROXY=example.com,::1,localhost,192.168.0.0/1665 if "://" in hostname:66 mounts[hostname] = None67 elif is_ipv4_hostname(hostname):68 mounts[f"all://{hostname}"] = None69 elif is_ipv6_hostname(hostname):70 mounts[f"all://[{hostname}]"] = None71 elif hostname.lower() == "localhost":72 mounts[f"all://{hostname}"] = None73 else:74 mounts[f"all://*{hostname}"] = None75 76 return mounts77 78 79def to_bytes(value: str | bytes, encoding: str = "utf-8") -> bytes:80 return value.encode(encoding) if isinstance(value, str) else value81 82 83def to_str(value: str | bytes, encoding: str = "utf-8") -> str:84 return value if isinstance(value, str) else value.decode(encoding)85 86 87def to_bytes_or_str(value: str, match_type_of: typing.AnyStr) -> typing.AnyStr:88 return value if isinstance(match_type_of, str) else value.encode()89 90 91def unquote(value: str) -> str:92 return value[1:-1] if value[0] == value[-1] == '"' else value93 94 95def peek_filelike_length(stream: typing.Any) -> int | None:96 """97 Given a file-like stream object, return its length in number of bytes98 without reading it into memory.99 """100 try:101 # Is it an actual file?102 fd = stream.fileno()103 # Yup, seems to be an actual file.104 length = os.fstat(fd).st_size105 except (AttributeError, OSError):106 # No... Maybe it's something that supports random access, like `io.BytesIO`?107 try:108 # Assuming so, go to end of stream to figure out its length,109 # then put it back in place.110 offset = stream.tell()111 length = stream.seek(0, os.SEEK_END)112 stream.seek(offset)113 except (AttributeError, OSError):114 # Not even that? Sorry, we're doomed...115 return None116 117 return length118 119 120class URLPattern:121 """122 A utility class currently used for making lookups against proxy keys...123 124 # Wildcard matching...125 >>> pattern = URLPattern("all://")126 >>> pattern.matches(httpx.URL("http://example.com"))127 True128 129 # Witch scheme matching...130 >>> pattern = URLPattern("https://")131 >>> pattern.matches(httpx.URL("https://example.com"))132 True133 >>> pattern.matches(httpx.URL("http://example.com"))134 False135 136 # With domain matching...137 >>> pattern = URLPattern("https://example.com")138 >>> pattern.matches(httpx.URL("https://example.com"))139 True140 >>> pattern.matches(httpx.URL("http://example.com"))141 False142 >>> pattern.matches(httpx.URL("https://other.com"))143 False144 145 # Wildcard scheme, with domain matching...146 >>> pattern = URLPattern("all://example.com")147 >>> pattern.matches(httpx.URL("https://example.com"))148 True149 >>> pattern.matches(httpx.URL("http://example.com"))150 True151 >>> pattern.matches(httpx.URL("https://other.com"))152 False153 154 # With port matching...155 >>> pattern = URLPattern("https://example.com:1234")156 >>> pattern.matches(httpx.URL("https://example.com:1234"))157 True158 >>> pattern.matches(httpx.URL("https://example.com"))159 False160 """161 162 def __init__(self, pattern: str) -> None:163 from ._urls import URL164 165 if pattern and ":" not in pattern:166 raise ValueError(167 f"Proxy keys should use proper URL forms rather "168 f"than plain scheme strings. "169 f'Instead of "{pattern}", use "{pattern}://"'170 )171 172 url = URL(pattern)173 self.pattern = pattern174 self.scheme = "" if url.scheme == "all" else url.scheme175 self.host = "" if url.host == "*" else url.host176 self.port = url.port177 if not url.host or url.host == "*":178 self.host_regex: typing.Pattern[str] | None = None179 elif url.host.startswith("*."):180 # *.example.com should match "www.example.com", but not "example.com"181 domain = re.escape(url.host[2:])182 self.host_regex = re.compile(f"^.+\\.{domain}$")183 elif url.host.startswith("*"):184 # *example.com should match "www.example.com" and "example.com"185 domain = re.escape(url.host[1:])186 self.host_regex = re.compile(f"^(.+\\.)?{domain}$")187 else:188 # example.com should match "example.com" but not "www.example.com"189 domain = re.escape(url.host)190 self.host_regex = re.compile(f"^{domain}$")191 192 def matches(self, other: URL) -> bool:193 if self.scheme and self.scheme != other.scheme:194 return False195 if (196 self.host197 and self.host_regex is not None198 and not self.host_regex.match(other.host)199 ):200 return False201 if self.port is not None and self.port != other.port:202 return False203 return True204 205 @property206 def priority(self) -> tuple[int, int, int]:207 """208 The priority allows URLPattern instances to be sortable, so that209 we can match from most specific to least specific.210 """211 # URLs with a port should take priority over URLs without a port.212 port_priority = 0 if self.port is not None else 1213 # Longer hostnames should match first.214 host_priority = -len(self.host)215 # Longer schemes should match first.216 scheme_priority = -len(self.scheme)217 return (port_priority, host_priority, scheme_priority)218 219 def __hash__(self) -> int:220 return hash(self.pattern)221 222 def __lt__(self, other: URLPattern) -> bool:223 return self.priority < other.priority224 225 def __eq__(self, other: typing.Any) -> bool:226 return isinstance(other, URLPattern) and self.pattern == other.pattern227 228 229def is_ipv4_hostname(hostname: str) -> bool:230 try:231 ipaddress.IPv4Address(hostname.split("/")[0])232 except Exception:233 return False234 return True235 236 237def is_ipv6_hostname(hostname: str) -> bool:238 try:239 ipaddress.IPv6Address(hostname.split("/")[0])240 except Exception:241 return False242 return True243 