config.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. from __future__ import annotations
  2. import asyncio
  3. import inspect
  4. import json
  5. import logging
  6. import logging.config
  7. import os
  8. import socket
  9. import ssl
  10. import sys
  11. from pathlib import Path
  12. from typing import Any, Awaitable, Callable, Literal
  13. import click
  14. from uvicorn._types import ASGIApplication
  15. from uvicorn.importer import ImportFromStringError, import_from_string
  16. from uvicorn.logging import TRACE_LOG_LEVEL
  17. from uvicorn.middleware.asgi2 import ASGI2Middleware
  18. from uvicorn.middleware.message_logger import MessageLoggerMiddleware
  19. from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
  20. from uvicorn.middleware.wsgi import WSGIMiddleware
  21. HTTPProtocolType = Literal["auto", "h11", "httptools"]
  22. WSProtocolType = Literal["auto", "none", "websockets", "wsproto"]
  23. LifespanType = Literal["auto", "on", "off"]
  24. LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"]
  25. InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"]
  26. LOG_LEVELS: dict[str, int] = {
  27. "critical": logging.CRITICAL,
  28. "error": logging.ERROR,
  29. "warning": logging.WARNING,
  30. "info": logging.INFO,
  31. "debug": logging.DEBUG,
  32. "trace": TRACE_LOG_LEVEL,
  33. }
  34. HTTP_PROTOCOLS: dict[HTTPProtocolType, str] = {
  35. "auto": "uvicorn.protocols.http.auto:AutoHTTPProtocol",
  36. "h11": "uvicorn.protocols.http.h11_impl:H11Protocol",
  37. "httptools": "uvicorn.protocols.http.httptools_impl:HttpToolsProtocol",
  38. }
  39. WS_PROTOCOLS: dict[WSProtocolType, str | None] = {
  40. "auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol",
  41. "none": None,
  42. "websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
  43. "wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
  44. }
  45. LIFESPAN: dict[LifespanType, str] = {
  46. "auto": "uvicorn.lifespan.on:LifespanOn",
  47. "on": "uvicorn.lifespan.on:LifespanOn",
  48. "off": "uvicorn.lifespan.off:LifespanOff",
  49. }
  50. LOOP_SETUPS: dict[LoopSetupType, str | None] = {
  51. "none": None,
  52. "auto": "uvicorn.loops.auto:auto_loop_setup",
  53. "asyncio": "uvicorn.loops.asyncio:asyncio_setup",
  54. "uvloop": "uvicorn.loops.uvloop:uvloop_setup",
  55. }
  56. INTERFACES: list[InterfaceType] = ["auto", "asgi3", "asgi2", "wsgi"]
  57. SSL_PROTOCOL_VERSION: int = ssl.PROTOCOL_TLS_SERVER
  58. LOGGING_CONFIG: dict[str, Any] = {
  59. "version": 1,
  60. "disable_existing_loggers": False,
  61. "formatters": {
  62. "default": {
  63. "()": "uvicorn.logging.DefaultFormatter",
  64. "fmt": "%(levelprefix)s %(message)s",
  65. "use_colors": None,
  66. },
  67. "access": {
  68. "()": "uvicorn.logging.AccessFormatter",
  69. "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
  70. },
  71. },
  72. "handlers": {
  73. "default": {
  74. "formatter": "default",
  75. "class": "logging.StreamHandler",
  76. "stream": "ext://sys.stderr",
  77. },
  78. "access": {
  79. "formatter": "access",
  80. "class": "logging.StreamHandler",
  81. "stream": "ext://sys.stdout",
  82. },
  83. },
  84. "loggers": {
  85. "uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
  86. "uvicorn.error": {"level": "INFO"},
  87. "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
  88. },
  89. }
  90. logger = logging.getLogger("uvicorn.error")
  91. def create_ssl_context(
  92. certfile: str | os.PathLike[str],
  93. keyfile: str | os.PathLike[str] | None,
  94. password: str | None,
  95. ssl_version: int,
  96. cert_reqs: int,
  97. ca_certs: str | os.PathLike[str] | None,
  98. ciphers: str | None,
  99. ) -> ssl.SSLContext:
  100. ctx = ssl.SSLContext(ssl_version)
  101. get_password = (lambda: password) if password else None
  102. ctx.load_cert_chain(certfile, keyfile, get_password)
  103. ctx.verify_mode = ssl.VerifyMode(cert_reqs)
  104. if ca_certs:
  105. ctx.load_verify_locations(ca_certs)
  106. if ciphers:
  107. ctx.set_ciphers(ciphers)
  108. return ctx
  109. def is_dir(path: Path) -> bool:
  110. try:
  111. if not path.is_absolute():
  112. path = path.resolve()
  113. return path.is_dir()
  114. except OSError:
  115. return False
  116. def resolve_reload_patterns(
  117. patterns_list: list[str], directories_list: list[str]
  118. ) -> tuple[list[str], list[Path]]:
  119. directories: list[Path] = list(set(map(Path, directories_list.copy())))
  120. patterns: list[str] = patterns_list.copy()
  121. current_working_directory = Path.cwd()
  122. for pattern in patterns_list:
  123. # Special case for the .* pattern, otherwise this would only match
  124. # hidden directories which is probably undesired
  125. if pattern == ".*":
  126. continue
  127. patterns.append(pattern)
  128. if is_dir(Path(pattern)):
  129. directories.append(Path(pattern))
  130. else:
  131. for match in current_working_directory.glob(pattern):
  132. if is_dir(match):
  133. directories.append(match)
  134. directories = list(set(directories))
  135. directories = list(map(Path, directories))
  136. directories = list(map(lambda x: x.resolve(), directories))
  137. directories = list(
  138. {reload_path for reload_path in directories if is_dir(reload_path)}
  139. )
  140. children = []
  141. for j in range(len(directories)):
  142. for k in range(j + 1, len(directories)):
  143. if directories[j] in directories[k].parents:
  144. children.append(directories[k]) # pragma: py-darwin
  145. elif directories[k] in directories[j].parents:
  146. children.append(directories[j])
  147. directories = list(set(directories).difference(set(children)))
  148. return list(set(patterns)), directories
  149. def _normalize_dirs(dirs: list[str] | str | None) -> list[str]:
  150. if dirs is None:
  151. return []
  152. if isinstance(dirs, str):
  153. return [dirs]
  154. return list(set(dirs))
  155. class Config:
  156. def __init__(
  157. self,
  158. app: ASGIApplication | Callable[..., Any] | str,
  159. host: str = "127.0.0.1",
  160. port: int = 8000,
  161. uds: str | None = None,
  162. fd: int | None = None,
  163. loop: LoopSetupType = "auto",
  164. http: type[asyncio.Protocol] | HTTPProtocolType = "auto",
  165. ws: type[asyncio.Protocol] | WSProtocolType = "auto",
  166. ws_max_size: int = 16 * 1024 * 1024,
  167. ws_max_queue: int = 32,
  168. ws_ping_interval: float | None = 20.0,
  169. ws_ping_timeout: float | None = 20.0,
  170. ws_per_message_deflate: bool = True,
  171. lifespan: LifespanType = "auto",
  172. env_file: str | os.PathLike[str] | None = None,
  173. log_config: dict[str, Any] | str | None = LOGGING_CONFIG,
  174. log_level: str | int | None = None,
  175. access_log: bool = True,
  176. use_colors: bool | None = None,
  177. interface: InterfaceType = "auto",
  178. reload: bool = False,
  179. reload_dirs: list[str] | str | None = None,
  180. reload_delay: float = 0.25,
  181. reload_includes: list[str] | str | None = None,
  182. reload_excludes: list[str] | str | None = None,
  183. workers: int | None = None,
  184. proxy_headers: bool = True,
  185. server_header: bool = True,
  186. date_header: bool = True,
  187. forwarded_allow_ips: list[str] | str | None = None,
  188. root_path: str = "",
  189. limit_concurrency: int | None = None,
  190. limit_max_requests: int | None = None,
  191. backlog: int = 2048,
  192. timeout_keep_alive: int = 5,
  193. timeout_notify: int = 30,
  194. timeout_graceful_shutdown: int | None = None,
  195. callback_notify: Callable[..., Awaitable[None]] | None = None,
  196. ssl_keyfile: str | None = None,
  197. ssl_certfile: str | os.PathLike[str] | None = None,
  198. ssl_keyfile_password: str | None = None,
  199. ssl_version: int = SSL_PROTOCOL_VERSION,
  200. ssl_cert_reqs: int = ssl.CERT_NONE,
  201. ssl_ca_certs: str | None = None,
  202. ssl_ciphers: str = "TLSv1",
  203. headers: list[tuple[str, str]] | None = None,
  204. factory: bool = False,
  205. h11_max_incomplete_event_size: int | None = None,
  206. ):
  207. self.app = app
  208. self.host = host
  209. self.port = port
  210. self.uds = uds
  211. self.fd = fd
  212. self.loop = loop
  213. self.http = http
  214. self.ws = ws
  215. self.ws_max_size = ws_max_size
  216. self.ws_max_queue = ws_max_queue
  217. self.ws_ping_interval = ws_ping_interval
  218. self.ws_ping_timeout = ws_ping_timeout
  219. self.ws_per_message_deflate = ws_per_message_deflate
  220. self.lifespan = lifespan
  221. self.log_config = log_config
  222. self.log_level = log_level
  223. self.access_log = access_log
  224. self.use_colors = use_colors
  225. self.interface = interface
  226. self.reload = reload
  227. self.reload_delay = reload_delay
  228. self.workers = workers or 1
  229. self.proxy_headers = proxy_headers
  230. self.server_header = server_header
  231. self.date_header = date_header
  232. self.root_path = root_path
  233. self.limit_concurrency = limit_concurrency
  234. self.limit_max_requests = limit_max_requests
  235. self.backlog = backlog
  236. self.timeout_keep_alive = timeout_keep_alive
  237. self.timeout_notify = timeout_notify
  238. self.timeout_graceful_shutdown = timeout_graceful_shutdown
  239. self.callback_notify = callback_notify
  240. self.ssl_keyfile = ssl_keyfile
  241. self.ssl_certfile = ssl_certfile
  242. self.ssl_keyfile_password = ssl_keyfile_password
  243. self.ssl_version = ssl_version
  244. self.ssl_cert_reqs = ssl_cert_reqs
  245. self.ssl_ca_certs = ssl_ca_certs
  246. self.ssl_ciphers = ssl_ciphers
  247. self.headers: list[tuple[str, str]] = headers or []
  248. self.encoded_headers: list[tuple[bytes, bytes]] = []
  249. self.factory = factory
  250. self.h11_max_incomplete_event_size = h11_max_incomplete_event_size
  251. self.loaded = False
  252. self.configure_logging()
  253. self.reload_dirs: list[Path] = []
  254. self.reload_dirs_excludes: list[Path] = []
  255. self.reload_includes: list[str] = []
  256. self.reload_excludes: list[str] = []
  257. if (
  258. reload_dirs or reload_includes or reload_excludes
  259. ) and not self.should_reload:
  260. logger.warning(
  261. "Current configuration will not reload as not all conditions are met, "
  262. "please refer to documentation."
  263. )
  264. if self.should_reload:
  265. reload_dirs = _normalize_dirs(reload_dirs)
  266. reload_includes = _normalize_dirs(reload_includes)
  267. reload_excludes = _normalize_dirs(reload_excludes)
  268. self.reload_includes, self.reload_dirs = resolve_reload_patterns(
  269. reload_includes, reload_dirs
  270. )
  271. self.reload_excludes, self.reload_dirs_excludes = resolve_reload_patterns(
  272. reload_excludes, []
  273. )
  274. reload_dirs_tmp = self.reload_dirs.copy()
  275. for directory in self.reload_dirs_excludes:
  276. for reload_directory in reload_dirs_tmp:
  277. if (
  278. directory == reload_directory
  279. or directory in reload_directory.parents
  280. ):
  281. try:
  282. self.reload_dirs.remove(reload_directory)
  283. except ValueError:
  284. pass
  285. for pattern in self.reload_excludes:
  286. if pattern in self.reload_includes:
  287. self.reload_includes.remove(pattern)
  288. if not self.reload_dirs:
  289. if reload_dirs:
  290. logger.warning(
  291. "Provided reload directories %s did not contain valid "
  292. + "directories, watching current working directory.",
  293. reload_dirs,
  294. )
  295. self.reload_dirs = [Path(os.getcwd())]
  296. logger.info(
  297. "Will watch for changes in these directories: %s",
  298. sorted(list(map(str, self.reload_dirs))),
  299. )
  300. if env_file is not None:
  301. from dotenv import load_dotenv
  302. logger.info("Loading environment from '%s'", env_file)
  303. load_dotenv(dotenv_path=env_file)
  304. if workers is None and "WEB_CONCURRENCY" in os.environ:
  305. self.workers = int(os.environ["WEB_CONCURRENCY"])
  306. self.forwarded_allow_ips: list[str] | str
  307. if forwarded_allow_ips is None:
  308. self.forwarded_allow_ips = os.environ.get(
  309. "FORWARDED_ALLOW_IPS", "127.0.0.1"
  310. )
  311. else:
  312. self.forwarded_allow_ips = forwarded_allow_ips
  313. if self.reload and self.workers > 1:
  314. logger.warning('"workers" flag is ignored when reloading is enabled.')
  315. @property
  316. def asgi_version(self) -> Literal["2.0", "3.0"]:
  317. mapping: dict[str, Literal["2.0", "3.0"]] = {
  318. "asgi2": "2.0",
  319. "asgi3": "3.0",
  320. "wsgi": "3.0",
  321. }
  322. return mapping[self.interface]
  323. @property
  324. def is_ssl(self) -> bool:
  325. return bool(self.ssl_keyfile or self.ssl_certfile)
  326. @property
  327. def use_subprocess(self) -> bool:
  328. return bool(self.reload or self.workers > 1)
  329. def configure_logging(self) -> None:
  330. logging.addLevelName(TRACE_LOG_LEVEL, "TRACE")
  331. if self.log_config is not None:
  332. if isinstance(self.log_config, dict):
  333. if self.use_colors in (True, False):
  334. self.log_config["formatters"]["default"][
  335. "use_colors"
  336. ] = self.use_colors
  337. self.log_config["formatters"]["access"][
  338. "use_colors"
  339. ] = self.use_colors
  340. logging.config.dictConfig(self.log_config)
  341. elif self.log_config.endswith(".json"):
  342. with open(self.log_config) as file:
  343. loaded_config = json.load(file)
  344. logging.config.dictConfig(loaded_config)
  345. elif self.log_config.endswith((".yaml", ".yml")):
  346. # Install the PyYAML package or the uvicorn[standard] optional
  347. # dependencies to enable this functionality.
  348. import yaml
  349. with open(self.log_config) as file:
  350. loaded_config = yaml.safe_load(file)
  351. logging.config.dictConfig(loaded_config)
  352. else:
  353. # See the note about fileConfig() here:
  354. # https://docs.python.org/3/library/logging.config.html#configuration-file-format
  355. logging.config.fileConfig(
  356. self.log_config, disable_existing_loggers=False
  357. )
  358. if self.log_level is not None:
  359. if isinstance(self.log_level, str):
  360. log_level = LOG_LEVELS[self.log_level]
  361. else:
  362. log_level = self.log_level
  363. logging.getLogger("uvicorn.error").setLevel(log_level)
  364. logging.getLogger("uvicorn.access").setLevel(log_level)
  365. logging.getLogger("uvicorn.asgi").setLevel(log_level)
  366. if self.access_log is False:
  367. logging.getLogger("uvicorn.access").handlers = []
  368. logging.getLogger("uvicorn.access").propagate = False
  369. def load(self) -> None:
  370. assert not self.loaded
  371. if self.is_ssl:
  372. assert self.ssl_certfile
  373. self.ssl: ssl.SSLContext | None = create_ssl_context(
  374. keyfile=self.ssl_keyfile,
  375. certfile=self.ssl_certfile,
  376. password=self.ssl_keyfile_password,
  377. ssl_version=self.ssl_version,
  378. cert_reqs=self.ssl_cert_reqs,
  379. ca_certs=self.ssl_ca_certs,
  380. ciphers=self.ssl_ciphers,
  381. )
  382. else:
  383. self.ssl = None
  384. encoded_headers = [
  385. (key.lower().encode("latin1"), value.encode("latin1"))
  386. for key, value in self.headers
  387. ]
  388. self.encoded_headers = (
  389. [(b"server", b"uvicorn")] + encoded_headers
  390. if b"server" not in dict(encoded_headers) and self.server_header
  391. else encoded_headers
  392. )
  393. if isinstance(self.http, str):
  394. http_protocol_class = import_from_string(HTTP_PROTOCOLS[self.http])
  395. self.http_protocol_class: type[asyncio.Protocol] = http_protocol_class
  396. else:
  397. self.http_protocol_class = self.http
  398. if isinstance(self.ws, str):
  399. ws_protocol_class = import_from_string(WS_PROTOCOLS[self.ws])
  400. self.ws_protocol_class: type[asyncio.Protocol] | None = ws_protocol_class
  401. else:
  402. self.ws_protocol_class = self.ws
  403. self.lifespan_class = import_from_string(LIFESPAN[self.lifespan])
  404. try:
  405. self.loaded_app = import_from_string(self.app)
  406. except ImportFromStringError as exc:
  407. logger.error("Error loading ASGI app. %s" % exc)
  408. sys.exit(1)
  409. try:
  410. self.loaded_app = self.loaded_app()
  411. except TypeError as exc:
  412. if self.factory:
  413. logger.error("Error loading ASGI app factory: %s", exc)
  414. sys.exit(1)
  415. else:
  416. if not self.factory:
  417. logger.warning(
  418. "ASGI app factory detected. Using it, "
  419. "but please consider setting the --factory flag explicitly."
  420. )
  421. if self.interface == "auto":
  422. if inspect.isclass(self.loaded_app):
  423. use_asgi_3 = hasattr(self.loaded_app, "__await__")
  424. elif inspect.isfunction(self.loaded_app):
  425. use_asgi_3 = asyncio.iscoroutinefunction(self.loaded_app)
  426. else:
  427. call = getattr(self.loaded_app, "__call__", None)
  428. use_asgi_3 = asyncio.iscoroutinefunction(call)
  429. self.interface = "asgi3" if use_asgi_3 else "asgi2"
  430. if self.interface == "wsgi":
  431. self.loaded_app = WSGIMiddleware(self.loaded_app)
  432. self.ws_protocol_class = None
  433. elif self.interface == "asgi2":
  434. self.loaded_app = ASGI2Middleware(self.loaded_app)
  435. if logger.getEffectiveLevel() <= TRACE_LOG_LEVEL:
  436. self.loaded_app = MessageLoggerMiddleware(self.loaded_app)
  437. if self.proxy_headers:
  438. self.loaded_app = ProxyHeadersMiddleware(
  439. self.loaded_app, trusted_hosts=self.forwarded_allow_ips
  440. )
  441. self.loaded = True
  442. def setup_event_loop(self) -> None:
  443. loop_setup: Callable | None = import_from_string(LOOP_SETUPS[self.loop])
  444. if loop_setup is not None:
  445. loop_setup(use_subprocess=self.use_subprocess)
  446. def bind_socket(self) -> socket.socket:
  447. logger_args: list[str | int]
  448. if self.uds: # pragma: py-win32
  449. path = self.uds
  450. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  451. try:
  452. sock.bind(path)
  453. uds_perms = 0o666
  454. os.chmod(self.uds, uds_perms)
  455. except OSError as exc:
  456. logger.error(exc)
  457. sys.exit(1)
  458. message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)"
  459. sock_name_format = "%s"
  460. color_message = (
  461. "Uvicorn running on "
  462. + click.style(sock_name_format, bold=True)
  463. + " (Press CTRL+C to quit)"
  464. )
  465. logger_args = [self.uds]
  466. elif self.fd: # pragma: py-win32
  467. sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM)
  468. message = "Uvicorn running on socket %s (Press CTRL+C to quit)"
  469. fd_name_format = "%s"
  470. color_message = (
  471. "Uvicorn running on "
  472. + click.style(fd_name_format, bold=True)
  473. + " (Press CTRL+C to quit)"
  474. )
  475. logger_args = [sock.getsockname()]
  476. else:
  477. family = socket.AF_INET
  478. addr_format = "%s://%s:%d"
  479. if self.host and ":" in self.host: # pragma: py-win32
  480. # It's an IPv6 address.
  481. family = socket.AF_INET6
  482. addr_format = "%s://[%s]:%d"
  483. sock = socket.socket(family=family)
  484. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  485. try:
  486. sock.bind((self.host, self.port))
  487. except OSError as exc:
  488. logger.error(exc)
  489. sys.exit(1)
  490. message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
  491. color_message = (
  492. "Uvicorn running on "
  493. + click.style(addr_format, bold=True)
  494. + " (Press CTRL+C to quit)"
  495. )
  496. protocol_name = "https" if self.is_ssl else "http"
  497. logger_args = [protocol_name, self.host, sock.getsockname()[1]]
  498. logger.info(message, *logger_args, extra={"color_message": color_message})
  499. sock.set_inheritable(True)
  500. return sock
  501. @property
  502. def should_reload(self) -> bool:
  503. return isinstance(self.app, str) and self.reload