Skip to content

Commit

Permalink
Rework IPv6 support (#837)
Browse files Browse the repository at this point in the history
* Revert "Fix server not running with explicit hostname (#827)"

This reverts commit d78394e.

* Revert "Fix reload with ipv6 host (#803)"

This reverts commit 5acaee5.

* Rework IPv6 support

* Fix IPv6 localhost equivalent

Co-authored-by: euri10 <[email protected]>

* Reduce diff size

* More diff size reduction

* Fix: self.host -> config.host

Co-authored-by: euri10 <[email protected]>
  • Loading branch information
florimondmanca and euri10 authored Nov 8, 2020
1 parent 6468b70 commit bdab488
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 64 deletions.
2 changes: 1 addition & 1 deletion docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ equivalent keyword arguments, eg. `uvicorn.run("example:app", port=5000, reload=

## Socket Binding

* `--host <str>` - Bind socket to this host. Use `--host 0.0.0.0` to make the application available on your local network. **Default:** *'127.0.0.1'*.
* `--host <str>` - 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 <int>` - Bind to a socket with this port. **Default:** *8000*.
* `--uds <str>` - Bind to a UNIX domain socket. Useful if you want to run Uvicorn behind a reverse proxy.
* `--fd <int>` - Bind to socket from this file descriptor. Useful if you want to run Uvicorn within a process manager.
Expand Down
40 changes: 12 additions & 28 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@
import threading
import time

import pytest
import requests

from uvicorn.config import Config
from uvicorn.main import Server


def test_run():
@pytest.mark.parametrize(
"host, url",
[
pytest.param(None, "http://127.0.0.1:8000", id="default"),
pytest.param("localhost", "http://127.0.0.1:8000", id="hostname"),
pytest.param("::1", "http://[::1]:8000", id="ipv6"),
],
)
def test_run(host, url):
class App:
def __init__(self, scope):
if scope["type"] != "http":
Expand All @@ -22,38 +31,13 @@ class CustomServer(Server):
def install_signal_handlers(self):
pass

config = Config(app=App, loop="asyncio", limit_max_requests=1)
config = Config(app=App, host=host, loop="asyncio", limit_max_requests=1)
server = CustomServer(config=config)
thread = threading.Thread(target=server.run)
thread.start()
while not server.started:
time.sleep(0.01)
response = requests.get("http://127.0.0.1:8000")
assert response.status_code == 204
thread.join()


def test_run_hostname():
class App:
def __init__(self, scope):
if scope["type"] != "http":
raise Exception()

async def __call__(self, receive, send):
await send({"type": "http.response.start", "status": 204, "headers": []})
await send({"type": "http.response.body", "body": b"", "more_body": False})

class CustomServer(Server):
def install_signal_handlers(self):
pass

config = Config(app=App, host="localhost", loop="asyncio", limit_max_requests=1)
server = CustomServer(config=config)
thread = threading.Thread(target=server.run)
thread.start()
while not server.started:
time.sleep(0.01)
response = requests.get("http://localhost:8000")
response = requests.get(url)
assert response.status_code == 204
thread.join()

Expand Down
37 changes: 15 additions & 22 deletions uvicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,6 @@ def create_ssl_context(
return ctx


def _get_server_start_message(is_ipv6_message: bool = False) -> Tuple[str, str]:
if is_ipv6_message:
ip_repr = "%s://[%s]:%d"
else:
ip_repr = "%s://%s:%d"
message = f"Uvicorn running on {ip_repr} (Press CTRL+C to quit)"
color_message = (
"Uvicorn running on "
+ click.style(ip_repr, bold=True)
+ " (Press CTRL+C to quit)"
)
return message, color_message


class Config:
def __init__(
self,
Expand Down Expand Up @@ -355,10 +341,15 @@ def setup_event_loop(self):
loop_setup()

def bind_socket(self):
family, sockettype, proto, canonname, sockaddr = socket.getaddrinfo(
self.host, self.port, type=socket.SOCK_STREAM
)[0]
sock = socket.socket(family=family, type=sockettype)
family = socket.AF_INET
addr_format = "%s://%s:%d"

if self.host and ":" in self.host:
# 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))
Expand All @@ -367,10 +358,12 @@ def bind_socket(self):
sys.exit(1)
sock.set_inheritable(True)

if family == socket.AddressFamily.AF_INET6:
message, color_message = _get_server_start_message(is_ipv6_message=True)
else:
message, color_message = _get_server_start_message()
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.info(
message,
Expand Down
24 changes: 11 additions & 13 deletions uvicorn/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import time
import typing
from email.utils import formatdate
from ipaddress import IPv4Address, IPv6Address, ip_address

import click

Expand All @@ -25,7 +24,6 @@
SSL_PROTOCOL_VERSION,
WS_PROTOCOLS,
Config,
_get_server_start_message,
)
from uvicorn.supervisors import ChangeReload, Multiprocess

Expand Down Expand Up @@ -506,6 +504,11 @@ def _share_socket(sock: socket) -> socket:

else:
# Standard case. Create a socket from a host/port pair.
addr_format = "%s://%s:%d"
if config.host and ":" in config.host:
# It's an IPv6 address.
addr_format = "%s://[%s]:%d"

try:
server = await loop.create_server(
create_protocol,
Expand All @@ -522,17 +525,12 @@ def _share_socket(sock: socket) -> socket:
if port == 0:
port = server.sockets[0].getsockname()[1]
protocol_name = "https" if config.ssl else "http"
try:
addr = ip_address(config.host)
if isinstance(addr, IPv6Address):
message, color_message = _get_server_start_message(
is_ipv6_message=True
)
elif isinstance(addr, IPv4Address):
message, color_message = _get_server_start_message()
except ValueError:
message, color_message = _get_server_start_message()

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,
Expand Down

0 comments on commit bdab488

Please sign in to comment.