_config.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import logging
  2. import os
  3. import ssl
  4. import typing
  5. from pathlib import Path
  6. import certifi
  7. from ._compat import set_minimum_tls_version_1_2
  8. from ._models import Headers
  9. from ._types import CertTypes, HeaderTypes, TimeoutTypes, URLTypes, VerifyTypes
  10. from ._urls import URL
  11. from ._utils import get_ca_bundle_from_env
  12. DEFAULT_CIPHERS = ":".join(
  13. [
  14. "ECDHE+AESGCM",
  15. "ECDHE+CHACHA20",
  16. "DHE+AESGCM",
  17. "DHE+CHACHA20",
  18. "ECDH+AESGCM",
  19. "DH+AESGCM",
  20. "ECDH+AES",
  21. "DH+AES",
  22. "RSA+AESGCM",
  23. "RSA+AES",
  24. "!aNULL",
  25. "!eNULL",
  26. "!MD5",
  27. "!DSS",
  28. ]
  29. )
  30. logger = logging.getLogger("httpx")
  31. class UnsetType:
  32. pass # pragma: no cover
  33. UNSET = UnsetType()
  34. def create_ssl_context(
  35. cert: typing.Optional[CertTypes] = None,
  36. verify: VerifyTypes = True,
  37. trust_env: bool = True,
  38. http2: bool = False,
  39. ) -> ssl.SSLContext:
  40. return SSLConfig(
  41. cert=cert, verify=verify, trust_env=trust_env, http2=http2
  42. ).ssl_context
  43. class SSLConfig:
  44. """
  45. SSL Configuration.
  46. """
  47. DEFAULT_CA_BUNDLE_PATH = Path(certifi.where())
  48. def __init__(
  49. self,
  50. *,
  51. cert: typing.Optional[CertTypes] = None,
  52. verify: VerifyTypes = True,
  53. trust_env: bool = True,
  54. http2: bool = False,
  55. ) -> None:
  56. self.cert = cert
  57. self.verify = verify
  58. self.trust_env = trust_env
  59. self.http2 = http2
  60. self.ssl_context = self.load_ssl_context()
  61. def load_ssl_context(self) -> ssl.SSLContext:
  62. logger.debug(
  63. "load_ssl_context verify=%r cert=%r trust_env=%r http2=%r",
  64. self.verify,
  65. self.cert,
  66. self.trust_env,
  67. self.http2,
  68. )
  69. if self.verify:
  70. return self.load_ssl_context_verify()
  71. return self.load_ssl_context_no_verify()
  72. def load_ssl_context_no_verify(self) -> ssl.SSLContext:
  73. """
  74. Return an SSL context for unverified connections.
  75. """
  76. context = self._create_default_ssl_context()
  77. context.check_hostname = False
  78. context.verify_mode = ssl.CERT_NONE
  79. self._load_client_certs(context)
  80. return context
  81. def load_ssl_context_verify(self) -> ssl.SSLContext:
  82. """
  83. Return an SSL context for verified connections.
  84. """
  85. if self.trust_env and self.verify is True:
  86. ca_bundle = get_ca_bundle_from_env()
  87. if ca_bundle is not None:
  88. self.verify = ca_bundle
  89. if isinstance(self.verify, ssl.SSLContext):
  90. # Allow passing in our own SSLContext object that's pre-configured.
  91. context = self.verify
  92. self._load_client_certs(context)
  93. return context
  94. elif isinstance(self.verify, bool):
  95. ca_bundle_path = self.DEFAULT_CA_BUNDLE_PATH
  96. elif Path(self.verify).exists():
  97. ca_bundle_path = Path(self.verify)
  98. else:
  99. raise IOError(
  100. "Could not find a suitable TLS CA certificate bundle, "
  101. "invalid path: {}".format(self.verify)
  102. )
  103. context = self._create_default_ssl_context()
  104. context.verify_mode = ssl.CERT_REQUIRED
  105. context.check_hostname = True
  106. # Signal to server support for PHA in TLS 1.3. Raises an
  107. # AttributeError if only read-only access is implemented.
  108. try:
  109. context.post_handshake_auth = True
  110. except AttributeError: # pragma: no cover
  111. pass
  112. # Disable using 'commonName' for SSLContext.check_hostname
  113. # when the 'subjectAltName' extension isn't available.
  114. try:
  115. context.hostname_checks_common_name = False
  116. except AttributeError: # pragma: no cover
  117. pass
  118. if ca_bundle_path.is_file():
  119. cafile = str(ca_bundle_path)
  120. logger.debug("load_verify_locations cafile=%r", cafile)
  121. context.load_verify_locations(cafile=cafile)
  122. elif ca_bundle_path.is_dir():
  123. capath = str(ca_bundle_path)
  124. logger.debug("load_verify_locations capath=%r", capath)
  125. context.load_verify_locations(capath=capath)
  126. self._load_client_certs(context)
  127. return context
  128. def _create_default_ssl_context(self) -> ssl.SSLContext:
  129. """
  130. Creates the default SSLContext object that's used for both verified
  131. and unverified connections.
  132. """
  133. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  134. set_minimum_tls_version_1_2(context)
  135. context.options |= ssl.OP_NO_COMPRESSION
  136. context.set_ciphers(DEFAULT_CIPHERS)
  137. if ssl.HAS_ALPN:
  138. alpn_idents = ["http/1.1", "h2"] if self.http2 else ["http/1.1"]
  139. context.set_alpn_protocols(alpn_idents)
  140. keylogfile = os.environ.get("SSLKEYLOGFILE")
  141. if keylogfile and self.trust_env:
  142. context.keylog_filename = keylogfile
  143. return context
  144. def _load_client_certs(self, ssl_context: ssl.SSLContext) -> None:
  145. """
  146. Loads client certificates into our SSLContext object
  147. """
  148. if self.cert is not None:
  149. if isinstance(self.cert, str):
  150. ssl_context.load_cert_chain(certfile=self.cert)
  151. elif isinstance(self.cert, tuple) and len(self.cert) == 2:
  152. ssl_context.load_cert_chain(certfile=self.cert[0], keyfile=self.cert[1])
  153. elif isinstance(self.cert, tuple) and len(self.cert) == 3:
  154. ssl_context.load_cert_chain(
  155. certfile=self.cert[0],
  156. keyfile=self.cert[1],
  157. password=self.cert[2], # type: ignore
  158. )
  159. class Timeout:
  160. """
  161. Timeout configuration.
  162. **Usage**:
  163. Timeout(None) # No timeouts.
  164. Timeout(5.0) # 5s timeout on all operations.
  165. Timeout(None, connect=5.0) # 5s timeout on connect, no other timeouts.
  166. Timeout(5.0, connect=10.0) # 10s timeout on connect. 5s timeout elsewhere.
  167. Timeout(5.0, pool=None) # No timeout on acquiring connection from pool.
  168. # 5s timeout elsewhere.
  169. """
  170. def __init__(
  171. self,
  172. timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
  173. *,
  174. connect: typing.Union[None, float, UnsetType] = UNSET,
  175. read: typing.Union[None, float, UnsetType] = UNSET,
  176. write: typing.Union[None, float, UnsetType] = UNSET,
  177. pool: typing.Union[None, float, UnsetType] = UNSET,
  178. ) -> None:
  179. if isinstance(timeout, Timeout):
  180. # Passed as a single explicit Timeout.
  181. assert connect is UNSET
  182. assert read is UNSET
  183. assert write is UNSET
  184. assert pool is UNSET
  185. self.connect = timeout.connect # type: typing.Optional[float]
  186. self.read = timeout.read # type: typing.Optional[float]
  187. self.write = timeout.write # type: typing.Optional[float]
  188. self.pool = timeout.pool # type: typing.Optional[float]
  189. elif isinstance(timeout, tuple):
  190. # Passed as a tuple.
  191. self.connect = timeout[0]
  192. self.read = timeout[1]
  193. self.write = None if len(timeout) < 3 else timeout[2]
  194. self.pool = None if len(timeout) < 4 else timeout[3]
  195. elif not (
  196. isinstance(connect, UnsetType)
  197. or isinstance(read, UnsetType)
  198. or isinstance(write, UnsetType)
  199. or isinstance(pool, UnsetType)
  200. ):
  201. self.connect = connect
  202. self.read = read
  203. self.write = write
  204. self.pool = pool
  205. else:
  206. if isinstance(timeout, UnsetType):
  207. raise ValueError(
  208. "httpx.Timeout must either include a default, or set all "
  209. "four parameters explicitly."
  210. )
  211. self.connect = timeout if isinstance(connect, UnsetType) else connect
  212. self.read = timeout if isinstance(read, UnsetType) else read
  213. self.write = timeout if isinstance(write, UnsetType) else write
  214. self.pool = timeout if isinstance(pool, UnsetType) else pool
  215. def as_dict(self) -> typing.Dict[str, typing.Optional[float]]:
  216. return {
  217. "connect": self.connect,
  218. "read": self.read,
  219. "write": self.write,
  220. "pool": self.pool,
  221. }
  222. def __eq__(self, other: typing.Any) -> bool:
  223. return (
  224. isinstance(other, self.__class__)
  225. and self.connect == other.connect
  226. and self.read == other.read
  227. and self.write == other.write
  228. and self.pool == other.pool
  229. )
  230. def __repr__(self) -> str:
  231. class_name = self.__class__.__name__
  232. if len({self.connect, self.read, self.write, self.pool}) == 1:
  233. return f"{class_name}(timeout={self.connect})"
  234. return (
  235. f"{class_name}(connect={self.connect}, "
  236. f"read={self.read}, write={self.write}, pool={self.pool})"
  237. )
  238. class Limits:
  239. """
  240. Configuration for limits to various client behaviors.
  241. **Parameters:**
  242. * **max_connections** - The maximum number of concurrent connections that may be
  243. established.
  244. * **max_keepalive_connections** - Allow the connection pool to maintain
  245. keep-alive connections below this point. Should be less than or equal
  246. to `max_connections`.
  247. * **keepalive_expiry** - Time limit on idle keep-alive connections in seconds.
  248. """
  249. def __init__(
  250. self,
  251. *,
  252. max_connections: typing.Optional[int] = None,
  253. max_keepalive_connections: typing.Optional[int] = None,
  254. keepalive_expiry: typing.Optional[float] = 5.0,
  255. ) -> None:
  256. self.max_connections = max_connections
  257. self.max_keepalive_connections = max_keepalive_connections
  258. self.keepalive_expiry = keepalive_expiry
  259. def __eq__(self, other: typing.Any) -> bool:
  260. return (
  261. isinstance(other, self.__class__)
  262. and self.max_connections == other.max_connections
  263. and self.max_keepalive_connections == other.max_keepalive_connections
  264. and self.keepalive_expiry == other.keepalive_expiry
  265. )
  266. def __repr__(self) -> str:
  267. class_name = self.__class__.__name__
  268. return (
  269. f"{class_name}(max_connections={self.max_connections}, "
  270. f"max_keepalive_connections={self.max_keepalive_connections}, "
  271. f"keepalive_expiry={self.keepalive_expiry})"
  272. )
  273. class Proxy:
  274. def __init__(
  275. self,
  276. url: URLTypes,
  277. *,
  278. ssl_context: typing.Optional[ssl.SSLContext] = None,
  279. auth: typing.Optional[typing.Tuple[str, str]] = None,
  280. headers: typing.Optional[HeaderTypes] = None,
  281. ) -> None:
  282. url = URL(url)
  283. headers = Headers(headers)
  284. if url.scheme not in ("http", "https", "socks5"):
  285. raise ValueError(f"Unknown scheme for proxy URL {url!r}")
  286. if url.username or url.password:
  287. # Remove any auth credentials from the URL.
  288. auth = (url.username, url.password)
  289. url = url.copy_with(username=None, password=None)
  290. self.url = url
  291. self.auth = auth
  292. self.headers = headers
  293. self.ssl_context = ssl_context
  294. @property
  295. def raw_auth(self) -> typing.Optional[typing.Tuple[bytes, bytes]]:
  296. # The proxy authentication as raw bytes.
  297. return (
  298. None
  299. if self.auth is None
  300. else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8"))
  301. )
  302. def __repr__(self) -> str:
  303. # The authentication is represented with the password component masked.
  304. auth = (self.auth[0], "********") if self.auth else None
  305. # Build a nice concise representation.
  306. url_str = f"{str(self.url)!r}"
  307. auth_str = f", auth={auth!r}" if auth else ""
  308. headers_str = f", headers={dict(self.headers)!r}" if self.headers else ""
  309. return f"Proxy({url_str}{auth_str}{headers_str})"
  310. DEFAULT_TIMEOUT_CONFIG = Timeout(timeout=5.0)
  311. DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20)
  312. DEFAULT_MAX_REDIRECTS = 20