Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vhost support using multiple TLS certificates #2270

Merged
merged 40 commits into from Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
379b1a2
Initial support for using multiple SSL certificates.
Tronic Oct 15, 2021
978a905
Also list IP address subjectAltNames on log.
Tronic Oct 15, 2021
e769733
Use Python 3.7+ way of specifying TLSv1.2 as the minimum version. Lin…
Tronic Oct 15, 2021
13fbaa5
isort
Tronic Oct 15, 2021
794c350
Cleanup, store server name for later use. Add RSA ciphers. Log reject…
Tronic Oct 16, 2021
afb9bd4
Cleanup, linter.
Tronic Oct 16, 2021
643fd54
Alter the order of initial log messages and handling. In particular, …
Tronic Oct 16, 2021
497d58d
Store server name (SNI) to conn_info.
Tronic Oct 16, 2021
69a7e91
Update test with new error message.
Tronic Oct 16, 2021
1bd8c23
Refactor for readability.
Tronic Oct 16, 2021
098b167
Cleanup
Tronic Oct 17, 2021
b82c3f0
Replace old expired test cert with new ones and a script for regenera…
Tronic Oct 17, 2021
01299bf
Refactor TLS tests to a separate file.
Tronic Oct 17, 2021
b5bde3c
Add cryptography to dev deps for rebuilding TLS certs.
Tronic Oct 17, 2021
1760755
Minor adjustment to messages.
Tronic Oct 17, 2021
f59006a
Tests added for new TLS code.
Tronic Oct 17, 2021
da22609
Find the correct log row before testing for message. The order was di…
Tronic Oct 17, 2021
8c9eb42
More log message order fixup. The tests do not account for the logo b…
Tronic Oct 17, 2021
1c3353c
Another attempt at log message indexing fixup.
Tronic Oct 17, 2021
8244f3f
Major TLS refactoring.
Tronic Oct 19, 2021
e3861ab
Remove a problematic logger test that apparently was not adding any c…
Tronic Oct 19, 2021
320361c
Revert accidental commit of uvloop disable.
Tronic Oct 19, 2021
2a8d7e0
Typing fixes / refactoring.
Tronic Oct 19, 2021
32f2f03
Additional test for cert selection. Certs recreated without DNS:local…
Tronic Oct 19, 2021
14f4e8b
Add tests for single certificate path shorthand and SNI information.
Tronic Oct 19, 2021
62a1c36
Move TLS dict processing to CertSimple, make the names field optional…
Tronic Oct 19, 2021
f790e27
Sanic CLI options --tls and --tls-strict-host to use the new features.
Tronic Oct 24, 2021
76f9573
SSL argument typing updated
Tronic Oct 24, 2021
60cfd72
Use ValueError for internal message passing to avoid CertificateError…
Tronic Oct 24, 2021
5d25739
Linter
Tronic Oct 24, 2021
888aee4
Test CLI TLS options.
Tronic Oct 24, 2021
72f6b9d
Merge branch 'main' into tls-vhosts
Tronic Oct 24, 2021
eeea1f0
Merge branch 'main' into tls-vhosts
Tronic Oct 24, 2021
4b28ebb
Merge branch 'main' into tls-vhosts
Tronic Oct 24, 2021
6f7fcfd
Maybe the right codeclimate option now...
Tronic Oct 24, 2021
b7c20b2
Merge branch 'main' into tls-vhosts
ahopkins Oct 27, 2021
ee00e42
Merge remote-tracking branch 'origin/main' into tls-vhosts
Tronic Oct 27, 2021
3991b60
Improved TLS argument help, removed support for combining --cert/--ke…
Tronic Oct 27, 2021
66a07c1
Removed support for strict checking without any certs, black forced f…
Tronic Oct 27, 2021
bb793fa
Update CLI tests for stricter TLS options.
Tronic Oct 27, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .codeclimate.yml
Expand Up @@ -21,5 +21,7 @@ checks:
config:
threshold: 40
complex-logic:
enabled: false
method-complexity:
config:
threshold: 10
42 changes: 33 additions & 9 deletions sanic/__main__.py
Expand Up @@ -4,7 +4,7 @@
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Union

from sanic_routing import __version__ as __routing_version__ # type: ignore

Expand Down Expand Up @@ -79,10 +79,30 @@ def main():
help="location of unix socket\n ",
)
parser.add_argument(
"--cert", dest="cert", type=str, help="Location of certificate for SSL"
"--cert",
dest="cert",
type=str,
help="Location of certificate (instead of --tls)",
)
parser.add_argument(
"--key",
dest="key",
type=str,
help="Location of keyfile (instead of --tls)",
)
parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL\n "
"--tls",
metavar="DIR",
type=str,
action="append",
help="TLS certificate folder with fullchain.pem and privkey.pem.\n"
"May be specified multiple times to choose of multiple certificates.",
)
parser.add_argument(
"--tls-strict-host",
dest="tlshost",
action="store_true",
help="Only allow clients that send an SNI matching server certs.\n ",
)
Tronic marked this conversation as resolved.
Show resolved Hide resolved
parser.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
Expand Down Expand Up @@ -149,14 +169,18 @@ def main():
f"Module is not a Sanic app, it is a {app_type_name}. "
f"Perhaps you meant {args.module}.app?"
)
ssl: Union[None, dict, str, list] = []
if args.tlshost:
ssl.append(None)
if args.cert is not None or args.key is not None:
ssl: Optional[Dict[str, Any]] = {
"cert": args.cert,
"key": args.key,
}
else:
ssl.append(dict(cert=args.cert, key=args.key))
if args.tls:
ssl += args.tls
if not ssl:
ssl = None

elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector.
ssl = ssl[0]
kwargs = {
"host": args.host,
"port": args.port,
Expand Down
78 changes: 34 additions & 44 deletions sanic/app.py
Expand Up @@ -19,7 +19,7 @@
from inspect import isawaitable
from pathlib import Path
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from ssl import SSLContext
from traceback import format_exc
from types import SimpleNamespace
from typing import (
Expand Down Expand Up @@ -78,6 +78,7 @@
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.websockets.impl import ConnectionClosed
from sanic.signals import Signal, SignalRouter
from sanic.tls import process_to_context
from sanic.touchup import TouchUp, TouchUpMeta


Expand Down Expand Up @@ -952,7 +953,7 @@ def run(
*,
debug: bool = False,
auto_reload: Optional[bool] = None,
ssl: Union[Dict[str, str], SSLContext, None] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
Expand All @@ -978,7 +979,7 @@ def run(
:type auto_relaod: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
:type ssl: str, dict, SSLContext or list
:param sock: Socket for the server to accept connections from
:type sock: socket
:param workers: Number of processes received before it is respected
Expand Down Expand Up @@ -1082,7 +1083,7 @@ async def create_server(
port: Optional[int] = None,
*,
debug: bool = False,
ssl: Union[Dict[str, str], SSLContext, None] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
Expand Down Expand Up @@ -1266,16 +1267,6 @@ def _helper(
auto_reload=False,
):
"""Helper function used by `run` and `create_server`."""

if isinstance(ssl, dict):
# try common aliaseses
cert = ssl.get("cert") or ssl.get("certificate")
key = ssl.get("key") or ssl.get("keyfile")
if cert is None or key is None:
raise ValueError("SSLContext or certificate and key required.")
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
context.load_cert_chain(cert, keyfile=key)
ssl = context
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError(
"PROXIES_COUNT cannot be negative. "
Expand All @@ -1285,6 +1276,35 @@ def _helper(

self.error_handler.debug = debug
self.debug = debug
if self.configure_logging and debug:
logger.setLevel(logging.DEBUG)
if (
self.config.LOGO
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
):
logger.debug(
self.config.LOGO
if isinstance(self.config.LOGO, str)
else BASE_LOGO
)
# Serve
if host and port:
proto = "http"
if ssl is not None:
proto = "https"
if unix:
logger.info(f"Goin' Fast @ {unix} {proto}://...")
else:
# colon(:) is legal for a host only in an ipv6 address
display_host = f"[{host}]" if ":" in host else host
logger.info(f"Goin' Fast @ {proto}://{display_host}:{port}")

debug_mode = "enabled" if self.debug else "disabled"
reload_mode = "enabled" if auto_reload else "disabled"
logger.debug(f"Sanic auto-reload: {reload_mode}")
logger.debug(f"Sanic debug mode: {debug_mode}")

ssl = process_to_context(ssl)

server_settings = {
"protocol": protocol,
Expand Down Expand Up @@ -1313,39 +1333,9 @@ def _helper(
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners

if self.configure_logging and debug:
logger.setLevel(logging.DEBUG)

if (
self.config.LOGO
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
):
logger.debug(
self.config.LOGO
if isinstance(self.config.LOGO, str)
else BASE_LOGO
)

if run_async:
server_settings["run_async"] = True

# Serve
if host and port:
proto = "http"
if ssl is not None:
proto = "https"
if unix:
logger.info(f"Goin' Fast @ {unix} {proto}://...")
else:
# colon(:) is legal for a host only in an ipv6 address
display_host = f"[{host}]" if ":" in host else host
logger.info(f"Goin' Fast @ {proto}://{display_host}:{port}")

debug_mode = "enabled" if self.debug else "disabled"
reload_mode = "enabled" if auto_reload else "disabled"
logger.debug(f"Sanic auto-reload: {reload_mode}")
logger.debug(f"Sanic debug mode: {debug_mode}")

return server_settings

def _build_endpoint_name(self, *parts):
Expand Down
16 changes: 14 additions & 2 deletions sanic/models/server_types.py
@@ -1,4 +1,6 @@
from ssl import SSLObject
from types import SimpleNamespace
from typing import Optional

from sanic.models.protocol_types import TransportProtocol

Expand All @@ -20,8 +22,10 @@ class ConnInfo:
"peername",
"server_port",
"server",
"server_name",
"sockname",
"ssl",
"cert",
)

def __init__(self, transport: TransportProtocol, unix=None):
Expand All @@ -31,8 +35,16 @@ def __init__(self, transport: TransportProtocol, unix=None):
self.server_port = self.client_port = 0
self.client_ip = ""
self.sockname = addr = transport.get_extra_info("sockname")
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))

self.ssl = False
self.server_name = ""
self.cert = {}
sslobj: Optional[SSLObject] = transport.get_extra_info(
"ssl_object"
) # type: ignore
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
if sslobj:
self.ssl = True
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
self.cert = getattr(sslobj.context, "sanic", {})
if isinstance(addr, str): # UNIX socket
self.server = unix or addr
return
Expand Down