diff --git a/datasette/cli.py b/datasette/cli.py index 25d6acea..0a355edb 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -408,6 +408,14 @@ def uninstall(packages, yes): is_flag=True, help="Create database files if they do not exist", ) +@click.option( + "--ssl-keyfile", + help="SSL key file", +) +@click.option( + "--ssl-certfile", + help="SSL certificate file", +) def serve( files, immutable, @@ -432,6 +440,8 @@ def serve( pdb, open_browser, create, + ssl_keyfile, + ssl_certfile, return_instance=False, ): """Serve up specified SQLite database files with a web UI""" @@ -546,9 +556,14 @@ def serve( ) url = f"http://{host}:{port}{path}" webbrowser.open(url) - uvicorn.run( - ds.app(), host=host, port=port, log_level="info", lifespan="on", workers=1 + uvicorn_kwargs = dict( + host=host, port=port, log_level="info", lifespan="on", workers=1 ) + if ssl_keyfile: + uvicorn_kwargs["ssl_keyfile"] = ssl_keyfile + if ssl_certfile: + uvicorn_kwargs["ssl_certfile"] = ssl_certfile + uvicorn.run(ds.app(), **uvicorn_kwargs) async def check_databases(ds): diff --git a/docs/datasette-serve-help.txt b/docs/datasette-serve-help.txt index 257d38c6..62d59794 100644 --- a/docs/datasette-serve-help.txt +++ b/docs/datasette-serve-help.txt @@ -41,4 +41,6 @@ Options: --pdb Launch debugger on any errors -o, --open Open Datasette in your web browser --create Create database files if they do not exist + --ssl-keyfile TEXT SSL key file + --ssl-certfile TEXT SSL certificate file --help Show this message and exit. diff --git a/setup.py b/setup.py index be94c1c6..34b6b396 100644 --- a/setup.py +++ b/setup.py @@ -73,6 +73,7 @@ setup( "beautifulsoup4>=4.8.1,<4.10.0", "black==20.8b1", "pytest-timeout>=1.4.2,<1.5", + "trustme>=0.7,<0.8", ], }, tests_require=["datasette[test]"], diff --git a/tests/conftest.py b/tests/conftest.py index a963a4fd..b00ea006 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,11 @@ import os import pathlib import pytest import re +import subprocess +import tempfile +import time +import trustme + try: import pysqlite3 as sqlite3 @@ -91,3 +96,58 @@ def check_permission_actions_are_documented(): pm.add_hookcall_monitoring( before=before, after=lambda outcome, hook_name, hook_impls, kwargs: None ) + + +@pytest.fixture(scope="session") +def ds_localhost_http_server(): + ds_proc = subprocess.Popen( + ["datasette", "--memory", "-p", "8041"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + # Avoid FileNotFoundError: [Errno 2] No such file or directory: + cwd=tempfile.gettempdir(), + ) + # Give the server time to start + time.sleep(1.5) + # Check it started successfully + assert not ds_proc.poll(), ds_proc.stdout.read().decode("utf-8") + yield ds_proc + # Shut it down at the end of the pytest session + ds_proc.terminate() + + +@pytest.fixture(scope="session") +def ds_localhost_https_server(tmp_path_factory): + cert_directory = tmp_path_factory.mktemp("certs") + ca = trustme.CA() + server_cert = ca.issue_cert("localhost") + keyfile = str(cert_directory / "server.key") + certfile = str(cert_directory / "server.pem") + client_cert = str(cert_directory / "client.pem") + server_cert.private_key_pem.write_to_path(path=keyfile) + for blob in server_cert.cert_chain_pems: + blob.write_to_path(path=certfile, append=True) + ca.cert_pem.write_to_path(path=client_cert) + + ds_proc = subprocess.Popen( + [ + "datasette", + "--memory", + "-p", + "8042", + "--ssl-keyfile", + keyfile, + "--ssl-certfile", + certfile, + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=tempfile.gettempdir(), + ) + # Give the server time to start + time.sleep(1.5) + # Check it started successfully + assert not ds_proc.poll(), ds_proc.stdout.read().decode("utf-8") + yield ds_proc, client_cert + # Shut it down at the end of the pytest session + ds_proc.terminate() diff --git a/tests/test_cli.py b/tests/test_cli.py index 33c2c8ac..2ca500cf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -149,6 +149,8 @@ def test_metadata_yaml(): pdb=False, open_browser=False, create=False, + ssl_keyfile=None, + ssl_certfile=None, return_instance=True, ) client = _TestClient(ds)