From 6b5e7d4588b98810788ed4a5ff73f2cd14ed7835 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Wed, 27 Apr 2022 11:48:36 +0000 Subject: *) mod_md: added support for managing certificates via a local tailscale demon for users of that secure networking. This gives trusted certificates for tailscale assigned domain names in the *.ts.net space. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1900313 13f79535-47bb-0310-9956-ffa450edef68 --- test/modules/md/md_env.py | 6 ++ test/modules/md/test_780_tailscale.py | 186 ++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 test/modules/md/test_780_tailscale.py (limited to 'test') diff --git a/test/modules/md/md_env.py b/test/modules/md/md_env.py index 718e5d1742..ca07f96937 100755 --- a/test/modules/md/md_env.py +++ b/test/modules/md/md_env.py @@ -111,6 +111,7 @@ class MDTestEnv(HttpdTestEnv): self._a2md_bin = os.path.join(self.bin_dir, 'a2md') self._default_domain = f"test1.{self.http_tld}" + self._tailscale_domain = "test.headless-chicken.ts.net" self._store_dir = "./md" self.set_store_dir_default() @@ -119,6 +120,7 @@ class MDTestEnv(HttpdTestEnv): valid_from=timedelta(days=-100), valid_to=timedelta(days=-10)), CertificateSpec(domains=["localhost"], key_type='rsa2048'), + CertificateSpec(domains=[self._tailscale_domain]), ]) def setup_httpd(self, setup: HttpdTestSetup = None): @@ -168,6 +170,10 @@ class MDTestEnv(HttpdTestEnv): def store_dir(self): return self._store_dir + @property + def tailscale_domain(self): + return self._tailscale_domain + def get_request_domain(self, request): name = request.node.originalname if request.node.originalname else request.node.name return "%s-%s" % (re.sub(r'[_]', '-', name), MDTestEnv.DOMAIN_SUFFIX) diff --git a/test/modules/md/test_780_tailscale.py b/test/modules/md/test_780_tailscale.py new file mode 100644 index 0000000000..84a266b2eb --- /dev/null +++ b/test/modules/md/test_780_tailscale.py @@ -0,0 +1,186 @@ +import os +import re +import socket +import sys +from threading import Thread + +import pytest + +from .md_conf import MDConf + + +class TailscaleFaker: + + def __init__(self, env, path): + self.env = env + self._uds_path = path + self._done = False + + def start(self): + def process(self): + self._socket.listen(1) + self._process() + + try: + os.unlink(self._uds_path) + except OSError: + if os.path.exists(self._uds_path): + raise + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.bind(self._uds_path) + self._thread = Thread(target=process, daemon=True, args=[self]) + self._thread.start() + + def stop(self): + self._done = True + self._socket.close() + + def send_error(self, c, status, reason): + c.sendall(f"""HTTP/1.1 {status} {reason}\r +Server: TailscaleFaker\r +Content-Length: 0\r +Connection: close\r +\r +""".encode()) + + def send_data(self, c, ctype: str, data: bytes): + c.sendall(f"""HTTP/1.1 200 OK\r +Server: TailscaleFaker\r +Content-Type: {ctype}\r +Content-Length: {len(data)}\r +Connection: close\r +\r +""".encode() + data) + + def _process(self): + # a http server written on a sunny afternooon + while self._done is False: + try: + c, client_address = self._socket.accept() + try: + data = c.recv(1024) + lines = data.decode().splitlines() + m = re.match(r'^(?P\w+)\s+(?P\S+)\s+HTTP/1.1', lines[0]) + if m is None: + self.send_error(c, 400, "Bad Request") + continue + uri = m.group('uri') + m = re.match(r'/localapi/v0/cert/(?P\S+)\?type=(?P\w+)', uri) + if m is None: + self.send_error(c, 404, "Not Found") + continue + domain = m.group('domain') + cred_type = m.group('type') + creds = self.env.get_credentials_for_name(domain) + sys.stderr.write(f"lookup domain={domain}, type={cred_type} -> {creds}\n") + if creds is None or len(creds) == 0: + self.send_error(c, 404, "Not Found") + continue + if cred_type == 'crt': + self.send_data(c, "text/plain", creds[0].cert_pem) + pass + elif cred_type == 'key': + self.send_data(c, "text/plain", creds[0].pkey_pem) + else: + self.send_error(c, 404, "Not Found") + continue + finally: + c.close() + + except ConnectionAbortedError: + self._done = True + + +class TestTailscale: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env, acme): + UDS_PATH = f"{env.gen_dir}/tailscale.sock" + TestTailscale.UDS_PATH = UDS_PATH + faker = TailscaleFaker(env=env, path=UDS_PATH) + faker.start() + env.APACHE_CONF_SRC = "data/test_auto" + acme.start(config='default') + env.clear_store() + MDConf(env).install() + assert env.apache_restart() == 0 + yield + faker.stop() + + @pytest.fixture(autouse=True, scope='function') + def _method_scope(self, env, request): + env.clear_store() + self.test_domain = env.get_request_domain(request) + + def _write_res_file(self, doc_root, name, content): + if not os.path.exists(doc_root): + os.makedirs(doc_root) + open(os.path.join(doc_root, name), "w").write(content) + + # create a MD using `tailscale` as protocol, wrong path + def test_md_780_001(self, env): + domain = env.tailscale_domain + # generate config with one MD + domains = [domain] + socket_path = '/xxx' + conf = MDConf(env, admin="admin@" + domain) + conf.start_md(domains) + conf.add([ + "MDCertificateProtocol tailscale", + f"MDCertificateAuthority file://{socket_path}", + ]) + conf.end_md() + conf.add_vhost(domains) + conf.install() + # restart and watch it fail due to wrong tailscale unix socket path + assert env.apache_restart() == 0 + md = env.await_error(domain) + assert md + assert md['renewal']['errors'] > 0 + assert md['renewal']['last']['status-description'] == 'No such file or directory' + assert md['renewal']['last']['detail'] == \ + f"tailscale socket not available, may not be up: {socket_path}" + + # create a MD using `tailscale` as protocol, path to faker, should succeed + def test_md_780_002(self, env): + domain = env.tailscale_domain + # generate config with one MD + domains = [domain] + socket_path = '/xxx' + conf = MDConf(env, admin="admin@" + domain) + conf.start_md(domains) + conf.add([ + "MDCertificateProtocol tailscale", + f"MDCertificateAuthority file://{self.UDS_PATH}", + ]) + conf.end_md() + conf.add_vhost(domains) + conf.install() + # restart and watch it fail due to wrong tailscale unix socket path + assert env.apache_restart() == 0 + assert env.await_completion(domains) + assert env.apache_restart() == 0 + env.check_md_complete(domain) + + # create a MD using `tailscale` as protocol, but domain name not assigned by tailscale + def test_md_780_003(self, env): + domain = "test.not-correct.ts.net" + # generate config with one MD + domains = [domain] + socket_path = '/xxx' + conf = MDConf(env, admin="admin@" + domain) + conf.start_md(domains) + conf.add([ + "MDCertificateProtocol tailscale", + f"MDCertificateAuthority file://{self.UDS_PATH}", + ]) + conf.end_md() + conf.add_vhost(domains) + conf.install() + # restart and watch it fail due to wrong tailscale unix socket path + assert env.apache_restart() == 0 + md = env.await_error(domain) + assert md + assert md['renewal']['errors'] > 0 + assert md['renewal']['last']['status-description'] == 'No such file or directory' + assert md['renewal']['last']['detail'] == "retrieving certificate from tailscale" -- cgit v1.2.1