import socket
from typing import AnyStr, Dict, List, TypeVar
from .logger import get_logger
KT = TypeVar('KT')
VT = TypeVar('VT')
def isomorphic_decode(s: AnyStr) -> str:
"""Decodes a binary string into a text string using iso-8859-1.
Returns `str`. The function is a no-op if the argument already has a text
type. iso-8859-1 is chosen because it is an 8-bit encoding whose code
points range from 0x0 to 0xFF and the values are the same as the binary
representations, so any binary string can be decoded into and encoded from
iso-8859-1 without any errors or data loss. Python 3 also uses iso-8859-1
(or latin-1) extensively in http:
https://github.com/python/cpython/blob/273fc220b25933e443c82af6888eb1871d032fb8/Lib/http/client.py#L213
"""
if isinstance(s, str):
return s
if isinstance(s, bytes):
return s.decode("iso-8859-1")
raise TypeError("Unexpected value (expecting string-like): %r" % s)
def isomorphic_encode(s: AnyStr) -> bytes:
"""Encodes a text-type string into binary data using iso-8859-1.
Returns `bytes`. The function is a no-op if the argument already has a
binary type. This is the counterpart of isomorphic_decode.
"""
if isinstance(s, bytes):
return s
if isinstance(s, str):
return s.encode("iso-8859-1")
raise TypeError("Unexpected value (expecting string-like): %r" % s)
def invert_dict(dict: Dict[KT, List[VT]]) -> Dict[VT, KT]:
rv = {}
for key, values in dict.items():
for value in values:
if value in rv:
raise ValueError
rv[value] = key
return rv
class HTTPException(Exception):
def __init__(self, code: int, message: str = ""):
self.code = code
self.message = message
def _open_socket(host: str, port: int) -> socket.socket:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if port != 0:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
sock.listen(5)
return sock
def is_bad_port(port: int) -> bool:
"""
Bad port as per https://fetch.spec.whatwg.org/#port-blocking
"""
return port in [
1, # tcpmux
7, # echo
9, # discard
11, # systat
13, # daytime
15, # netstat
17, # qotd
19, # chargen
20, # ftp-data
21, # ftp
22, # ssh
23, # telnet
25, # smtp
37, # time
42, # name
43, # nicname
53, # domain
69, # tftp
77, # priv-rjs
79, # finger
87, # ttylink
95, # supdup
101, # hostriame
102, # iso-tsap
103, # gppitnp
104, # acr-nema
109, # pop2
110, # pop3
111, # sunrpc
113, # auth
115, # sftp
117, # uucp-path
119, # nntp
123, # ntp
135, # loc-srv / epmap
137, # netbios-ns
139, # netbios-ssn
143, # imap2
161, # snmp
179, # bgp
389, # ldap
427, # afp (alternate)
465, # smtp (alternate)
512, # print / exec
513, # login
514, # shell
515, # printer
526, # tempo
530, # courier
531, # chat
532, # netnews
540, # uucp
548, # afp
554, # rtsp
556, # remotefs
563, # nntp+ssl
587, # smtp (outgoing)
601, # syslog-conn
636, # ldap+ssl
989, # ftps-data
999, # ftps
993, # ldap+ssl
995, # pop3+ssl
1719, # h323gatestat
1720, # h323hostcall
1723, # pptp
2049, # nfs
3659, # apple-sasl
4045, # lockd
4190, # sieve
5060, # sip
5061, # sips
6000, # x11
6566, # sane-port
6665, # irc (alternate)
6666, # irc (alternate)
6667, # irc (default)
6668, # irc (alternate)
6669, # irc (alternate)
6679, # osaut
6697, # irc+tls
10080, # amanda
]
def get_port(host: str = '') -> int:
host = host or '127.0.0.1'
port = 0
while True:
free_socket = _open_socket(host, 0)
port = free_socket.getsockname()[1]
free_socket.close()
if not is_bad_port(port):
break
return port
def http2_compatible() -> bool:
# The HTTP/2.0 server requires OpenSSL 1.0.2+.
#
# For systems using other SSL libraries (e.g. LibreSSL), we assume they
# have the necessary support.
import ssl
if not ssl.OPENSSL_VERSION.startswith("OpenSSL"):
logger = get_logger()
logger.warning(
'Skipping HTTP/2.0 compatibility check as system is not using '
'OpenSSL (found: %s)' % ssl.OPENSSL_VERSION)
return True
# Note that OpenSSL's versioning scheme differs between 1.1.1 and
# earlier and 3.0.0. ssl.OPENSSL_VERSION_INFO returns a
# (major, minor, 0, patch, 0)
# tuple with OpenSSL 3.0.0 and later, and a
# (major, minor, fix, patch, status)
# tuple for older releases.
# Semantically, "patch" in 3.0.0+ is similar to "fix" in previous versions.
#
# What we do in the check below is allow OpenSSL 3.x.y+, 1.1.x+ and 1.0.2+.
ssl_v = ssl.OPENSSL_VERSION_INFO
return (ssl_v[0] > 1 or
(ssl_v[0] == 1 and
(ssl_v[1] == 1 or
(ssl_v[1] == 0 and ssl_v[2] >= 2))))
def get_error_cause(exc: BaseException) -> BaseException:
"""Get the parent cause/context from an exception"""
if exc.__cause__ is not None:
return exc.__cause__
if exc.__context__ is not None:
return exc.__context__
return exc