From 5d357391b0e04786fdf286b994f549a5e001ae1b Mon Sep 17 00:00:00 2001 From: vvanglro Date: Thu, 17 Oct 2024 11:41:26 +0800 Subject: [PATCH 1/5] feat: support for setting ipv6 via cli --- docs/settings.md | 3 +-- tests/test_config.py | 31 ++++++++++++----------- uvicorn/config.py | 59 +++++++++++++++++++++++++------------------- uvicorn/main.py | 19 +++++++------- uvicorn/server.py | 40 +++++++++++++++--------------- 5 files changed, 82 insertions(+), 70 deletions(-) diff --git a/docs/settings.md b/docs/settings.md index a4439c3d0..901fbbba5 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -22,8 +22,7 @@ For example, in case you want to run the app on port `5000`, just set the enviro ## Socket Binding -* `--host ` - Bind socket to this host. Use `--host 0.0.0.0` to make the application available on your local network. IPv6 addresses are supported, for example: `--host '::'`. **Default:** *'127.0.0.1'*. -* `--port ` - Bind to a socket with this port. **Default:** *8000*. +* `--host ` - Bind socket to this host. May be used multiple times. If unused, then by default 127.0.0.1. IPv6 addresses are supported, for example: `--host '::'`, when using ipv6 only, if ipv4 is available it will work at the same time. * `--uds ` - Bind to a UNIX domain socket, for example `--uds /tmp/uvicorn.sock`. Useful if you want to run Uvicorn behind a reverse proxy. * `--fd ` - Bind to socket from this file descriptor. Useful if you want to run Uvicorn within a process manager. diff --git a/tests/test_config.py b/tests/test_config.py index e16cc5d56..20cf58cff 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -262,9 +262,10 @@ def test_concrete_http_class() -> None: def test_socket_bind() -> None: config = Config(app=asgi_app) config.load() - sock = config.bind_socket() - assert isinstance(sock, socket.socket) - sock.close() + sockets = config.bind_socket() + for sock in sockets: + assert isinstance(sock, socket.socket) + sock.close() def test_ssl_config( @@ -493,11 +494,12 @@ def test_bind_unix_socket_works_with_reload_or_workers( ): # pragma: py-win32 config = Config(app=asgi_app, uds=short_socket_name, reload=reload, workers=workers) config.load() - sock = config.bind_socket() - assert isinstance(sock, socket.socket) - assert sock.family == socket.AF_UNIX - assert sock.getsockname() == short_socket_name - sock.close() + sockets = config.bind_socket() + for sock in sockets: + assert isinstance(sock, socket.socket) + assert sock.family == socket.AF_UNIX + assert sock.getsockname() == short_socket_name + sock.close() @pytest.mark.parametrize( @@ -514,12 +516,13 @@ def test_bind_fd_works_with_reload_or_workers(reload: bool, workers: int): # pr fd = fdsock.fileno() config = Config(app=asgi_app, fd=fd, reload=reload, workers=workers) config.load() - sock = config.bind_socket() - assert isinstance(sock, socket.socket) - assert sock.family == socket.AF_UNIX - assert sock.getsockname() == "" - sock.close() - fdsock.close() + sockets = config.bind_socket() + for sock in sockets: + assert isinstance(sock, socket.socket) + assert sock.family == socket.AF_UNIX + assert sock.getsockname() == "" + sock.close() + fdsock.close() @pytest.mark.parametrize( diff --git a/uvicorn/config.py b/uvicorn/config.py index 65dfe651e..6b2d122d7 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -176,7 +176,7 @@ class Config: def __init__( self, app: ASGIApplication | Callable[..., Any] | str, - host: str = "127.0.0.1", + host: list[str] | str | None = "127.0.0.1", port: int = 8000, uds: str | None = None, fd: int | None = None, @@ -225,6 +225,8 @@ def __init__( h11_max_incomplete_event_size: int | None = None, ): self.app = app + if host and isinstance(host, str): + host = [host] self.host = host self.port = port self.uds = uds @@ -476,7 +478,8 @@ def setup_event_loop(self) -> None: if loop_setup is not None: loop_setup(use_subprocess=self.use_subprocess) - def bind_socket(self) -> socket.socket: + def bind_socket(self) -> list[socket.socket]: + sockets: list[socket.socket] = [] logger_args: list[str | int] if self.uds: # pragma: py-win32 path = self.uds @@ -489,40 +492,46 @@ def bind_socket(self) -> socket.socket: logger.error(exc) sys.exit(1) + sockets.append(sock) message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)" sock_name_format = "%s" color_message = "Uvicorn running on " + click.style(sock_name_format, bold=True) + " (Press CTRL+C to quit)" logger_args = [self.uds] + logger.info(message, *logger_args, extra={"color_message": color_message}) elif self.fd: # pragma: py-win32 sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM) + sockets.append(sock) message = "Uvicorn running on socket %s (Press CTRL+C to quit)" fd_name_format = "%s" color_message = "Uvicorn running on " + click.style(fd_name_format, bold=True) + " (Press CTRL+C to quit)" logger_args = [sock.getsockname()] + logger.info(message, *logger_args, extra={"color_message": color_message}) else: - family = socket.AF_INET - addr_format = "%s://%s:%d" - - if self.host and ":" in self.host: # pragma: full coverage - # It's an IPv6 address. - family = socket.AF_INET6 - addr_format = "%s://[%s]:%d" - - sock = socket.socket(family=family) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - sock.bind((self.host, self.port)) - except OSError as exc: # pragma: full coverage - logger.error(exc) - sys.exit(1) - - message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)" - color_message = "Uvicorn running on " + click.style(addr_format, bold=True) + " (Press CTRL+C to quit)" - protocol_name = "https" if self.is_ssl else "http" - logger_args = [protocol_name, self.host, sock.getsockname()[1]] - logger.info(message, *logger_args, extra={"color_message": color_message}) - sock.set_inheritable(True) - return sock + for host in self.host: + family = socket.AF_INET + addr_format = "%s://%s:%d" + + if ":" in host: # pragma: full coverage + # It's an IPv6 address. + family = socket.AF_INET6 + addr_format = "%s://[%s]:%d" + + sock = socket.socket(family=family) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind((host, self.port)) + except OSError as exc: # pragma: full coverage + logger.error(exc) + sys.exit(1) + sockets.append(sock) + message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)" + color_message = "Uvicorn running on " + click.style(addr_format, bold=True) + " (Press CTRL+C to quit)" + protocol_name = "https" if self.is_ssl else "http" + logger_args = [protocol_name, host, sock.getsockname()[1]] + logger.info(message, *logger_args, extra={"color_message": color_message}) + for sock in sockets: + sock.set_inheritable(True) + return sockets @property def should_reload(self) -> bool: diff --git a/uvicorn/main.py b/uvicorn/main.py index 96a10d538..a62b375e3 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -62,10 +62,11 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No @click.argument("app", envvar="UVICORN_APP") @click.option( "--host", - type=str, - default="127.0.0.1", + multiple=True, help="Bind socket to this host.", - show_default=True, + default=[ + "127.0.0.1", + ], ) @click.option( "--port", @@ -362,7 +363,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No ) def main( app: str, - host: str, + host: list[str], port: int, uds: str, fd: int, @@ -463,7 +464,7 @@ def main( def run( app: ASGIApplication | Callable[..., Any] | str, *, - host: str = "127.0.0.1", + host: list[str] | str | None = "127.0.0.1", port: int = 8000, uds: str | None = None, fd: int | None = None, @@ -570,11 +571,11 @@ def run( try: if config.should_reload: - sock = config.bind_socket() - ChangeReload(config, target=server.run, sockets=[sock]).run() + sockets = config.bind_socket() + ChangeReload(config, target=server.run, sockets=sockets).run() elif config.workers > 1: - sock = config.bind_socket() - Multiprocess(config, target=server.run, sockets=[sock]).run() + sockets = config.bind_socket() + Multiprocess(config, target=server.run, sockets=sockets).run() else: server.run() except KeyboardInterrupt: diff --git a/uvicorn/server.py b/uvicorn/server.py index f14026f16..a40461269 100644 --- a/uvicorn/server.py +++ b/uvicorn/server.py @@ -198,26 +198,26 @@ def _log_started_message(self, listeners: Sequence[socket.SocketType]) -> None: logger.info("Uvicorn running on unix socket %s (Press CTRL+C to quit)", config.uds) else: - addr_format = "%s://%s:%d" - host = "0.0.0.0" if config.host is None else config.host - if ":" in host: - # It's an IPv6 address. - addr_format = "%s://[%s]:%d" - - port = config.port - if port == 0: - port = listeners[0].getsockname()[1] - - protocol_name = "https" if config.ssl else "http" - message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)" - color_message = "Uvicorn running on " + click.style(addr_format, bold=True) + " (Press CTRL+C to quit)" - logger.info( - message, - protocol_name, - host, - port, - extra={"color_message": color_message}, - ) + for host in config.host: + addr_format = "%s://%s:%d" + if ":" in host: + # It's an IPv6 address. + addr_format = "%s://[%s]:%d" + + port = config.port + if port == 0: + port = listeners[0].getsockname()[1] + + protocol_name = "https" if config.ssl else "http" + message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)" + color_message = "Uvicorn running on " + click.style(addr_format, bold=True) + " (Press CTRL+C to quit)" + logger.info( + message, + protocol_name, + host, + port, + extra={"color_message": color_message}, + ) async def main_loop(self) -> None: counter = 0 From 7de29b940a79a6128ca4e9979ca75a9a4aea49cb Mon Sep 17 00:00:00 2001 From: vvanglro Date: Thu, 17 Oct 2024 12:07:55 +0800 Subject: [PATCH 2/5] fix(config): remove None as default value for host parameter --- uvicorn/config.py | 2 +- uvicorn/main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uvicorn/config.py b/uvicorn/config.py index 6b2d122d7..c155ba066 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -176,7 +176,7 @@ class Config: def __init__( self, app: ASGIApplication | Callable[..., Any] | str, - host: list[str] | str | None = "127.0.0.1", + host: list[str] | str = "127.0.0.1", port: int = 8000, uds: str | None = None, fd: int | None = None, diff --git a/uvicorn/main.py b/uvicorn/main.py index a62b375e3..ce27232c1 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -464,7 +464,7 @@ def main( def run( app: ASGIApplication | Callable[..., Any] | str, *, - host: list[str] | str | None = "127.0.0.1", + host: list[str] | str = "127.0.0.1", port: int = 8000, uds: str | None = None, fd: int | None = None, From fec719c81e33e2d58c4a6e69073f94bb1873d4ab Mon Sep 17 00:00:00 2001 From: vvanglro Date: Thu, 17 Oct 2024 12:22:14 +0800 Subject: [PATCH 3/5] fix docs --- docs/deployment.md | 3 +-- docs/index.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index d69fcf88e..9efdf90be 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -31,8 +31,7 @@ $ uvicorn --help Usage: uvicorn [OPTIONS] APP Options: - --host TEXT Bind socket to this host. [default: - 127.0.0.1] + --host TEXT Bind socket to this host. --port INTEGER Bind socket to this port. If 0, an available port will be picked. [default: 8000] --uds TEXT Bind to a UNIX domain socket. diff --git a/docs/index.md b/docs/index.md index bb6fc321a..ac42c2c13 100644 --- a/docs/index.md +++ b/docs/index.md @@ -101,8 +101,7 @@ $ uvicorn --help Usage: uvicorn [OPTIONS] APP Options: - --host TEXT Bind socket to this host. [default: - 127.0.0.1] + --host TEXT Bind socket to this host. --port INTEGER Bind socket to this port. If 0, an available port will be picked. [default: 8000] --uds TEXT Bind to a UNIX domain socket. From ab622098934372377e039b1c22f9fbebc995d53d Mon Sep 17 00:00:00 2001 From: vvanglro Date: Thu, 17 Oct 2024 14:06:37 +0800 Subject: [PATCH 4/5] fix(config): host validation --- uvicorn/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uvicorn/config.py b/uvicorn/config.py index c155ba066..d709dff6f 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -225,7 +225,7 @@ def __init__( h11_max_incomplete_event_size: int | None = None, ): self.app = app - if host and isinstance(host, str): + if isinstance(host, str): host = [host] self.host = host self.port = port From 23c0473165f49cb4dc2732cc82111dcb29af34c9 Mon Sep 17 00:00:00 2001 From: vvanglro Date: Thu, 17 Oct 2024 15:37:34 +0800 Subject: [PATCH 5/5] docs: show default host value in usage message --- docs/deployment.md | 3 ++- docs/index.md | 3 ++- uvicorn/main.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 9efdf90be..d69fcf88e 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -31,7 +31,8 @@ $ uvicorn --help Usage: uvicorn [OPTIONS] APP Options: - --host TEXT Bind socket to this host. + --host TEXT Bind socket to this host. [default: + 127.0.0.1] --port INTEGER Bind socket to this port. If 0, an available port will be picked. [default: 8000] --uds TEXT Bind to a UNIX domain socket. diff --git a/docs/index.md b/docs/index.md index ac42c2c13..bb6fc321a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -101,7 +101,8 @@ $ uvicorn --help Usage: uvicorn [OPTIONS] APP Options: - --host TEXT Bind socket to this host. + --host TEXT Bind socket to this host. [default: + 127.0.0.1] --port INTEGER Bind socket to this port. If 0, an available port will be picked. [default: 8000] --uds TEXT Bind to a UNIX domain socket. diff --git a/uvicorn/main.py b/uvicorn/main.py index ce27232c1..0fdb6ba9b 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -67,6 +67,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No default=[ "127.0.0.1", ], + show_default=True, ) @click.option( "--port",