admin / Synapse-Cortex
publicSelf Hosted ITSM Tool with RBAC/Tenanting and MFA
Synapse-Cortex / Synapse-Cortexv2 / .venv / Lib / site-packages / websockets / proxy.py
5119 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | from __future__ import annotations import dataclasses import urllib.parse import urllib.request from .datastructures import Headers from .exceptions import InvalidProxy from .headers import build_authorization_basic, build_host from .http11 import USER_AGENT from .uri import DELIMS, WebSocketURI __all__ = ["get_proxy", "parse_proxy", "Proxy"] @dataclasses.dataclass class Proxy: """ Proxy address. Attributes: scheme: ``"socks5h"``, ``"socks5"``, ``"socks4a"``, ``"socks4"``, ``"https"``, or ``"http"``. host: Normalized to lower case. port: Always set even if it's the default. username: Available when the proxy address contains `User Information`_. password: Available when the proxy address contains `User Information`_. .. _User Information: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1 """ scheme: str host: str port: int username: str | None = None password: str | None = None @property def user_info(self) -> tuple[str, str] | None: if self.username is None: return None assert self.password is not None return (self.username, self.password) def parse_proxy(proxy: str) -> Proxy: """ Parse and validate a proxy. Args: proxy: proxy. Returns: Parsed proxy. Raises: InvalidProxy: If ``proxy`` isn't a valid proxy. """ parsed = urllib.parse.urlparse(proxy) if parsed.scheme not in ["socks5h", "socks5", "socks4a", "socks4", "https", "http"]: raise InvalidProxy(proxy, f"scheme {parsed.scheme} isn't supported") if parsed.hostname is None: raise InvalidProxy(proxy, "hostname isn't provided") if parsed.path not in ["", "/"]: raise InvalidProxy(proxy, "path is meaningless") if parsed.query != "": raise InvalidProxy(proxy, "query is meaningless") if parsed.fragment != "": raise InvalidProxy(proxy, "fragment is meaningless") scheme = parsed.scheme host = parsed.hostname port = parsed.port or (443 if parsed.scheme == "https" else 80) username = parsed.username password = parsed.password # urllib.parse.urlparse accepts URLs with a username but without a # password. This doesn't make sense for HTTP Basic Auth credentials. if username is not None and password is None: raise InvalidProxy(proxy, "username provided without password") try: proxy.encode("ascii") except UnicodeEncodeError: # Input contains non-ASCII characters. # It must be an IRI. Convert it to a URI. host = host.encode("idna").decode() if username is not None: assert password is not None username = urllib.parse.quote(username, safe=DELIMS) password = urllib.parse.quote(password, safe=DELIMS) return Proxy(scheme, host, port, username, password) def get_proxy(uri: WebSocketURI) -> str | None: """ Return the proxy to use for connecting to the given WebSocket URI, if any. """ if urllib.request.proxy_bypass(f"{uri.host}:{uri.port}"): return None # According to the _Proxy Usage_ section of RFC 6455, use a SOCKS5 proxy if # available, else favor the proxy for HTTPS connections over the proxy for # HTTP connections. # The priority of a proxy for WebSocket connections is unspecified. We give # it the highest priority. This makes it easy to configure a specific proxy # for websockets. # getproxies() may return SOCKS proxies as {"socks": "http://host:port"} or # as {"https": "socks5h://host:port"} depending on whether they're declared # in the operating system or in environment variables. proxies = urllib.request.getproxies() if uri.secure: schemes = ["wss", "socks", "https"] else: schemes = ["ws", "socks", "https", "http"] for scheme in schemes: proxy = proxies.get(scheme) if proxy is not None: if scheme == "socks" and proxy.startswith("http://"): proxy = "socks5h://" + proxy[7:] return proxy else: return None def prepare_connect_request( proxy: Proxy, ws_uri: WebSocketURI, user_agent_header: str | None = USER_AGENT, ) -> bytes: host = build_host(ws_uri.host, ws_uri.port, ws_uri.secure, always_include_port=True) headers = Headers() headers["Host"] = build_host(ws_uri.host, ws_uri.port, ws_uri.secure) if user_agent_header is not None: headers["User-Agent"] = user_agent_header if proxy.username is not None: assert proxy.password is not None # enforced by parse_proxy() headers["Proxy-Authorization"] = build_authorization_basic( proxy.username, proxy.password ) # We cannot use the Request class because it supports only GET requests. return f"CONNECT {host} HTTP/1.1\r\n".encode() + headers.serialize() |