You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by ic...@apache.org on 2021/11/30 16:30:26 UTC
svn commit: r1895433 [1/2] - in /httpd/httpd/trunk/test/modules/tls: ./ htdocs/ htdocs/a.mod-tls.test/ htdocs/b.mod-tls.test/ htdocs/b.mod-tls.test/dir1/
Author: icing
Date: Tue Nov 30 16:30:26 2021
New Revision: 1895433
URL: http://svn.apache.org/viewvc?rev=1895433&view=rev
Log:
* test suite: adding modules/tls, the test suite for the
new mod_tls module to be run via pytest.
Integration into travis TBD.
Added:
httpd/httpd/trunk/test/modules/tls/
httpd/httpd/trunk/test/modules/tls/__init__.py
httpd/httpd/trunk/test/modules/tls/conf.py
httpd/httpd/trunk/test/modules/tls/conftest.py
httpd/httpd/trunk/test/modules/tls/env.py
httpd/httpd/trunk/test/modules/tls/htdocs/
httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/
httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/index.json
httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/vars.py (with props)
httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/
httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/dir1/
httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py (with props)
httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/index.json
httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py (with props)
httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/vars.py (with props)
httpd/httpd/trunk/test/modules/tls/htdocs/index.html
httpd/httpd/trunk/test/modules/tls/htdocs/index.json
httpd/httpd/trunk/test/modules/tls/load_test.py
httpd/httpd/trunk/test/modules/tls/test_01_apache.py
httpd/httpd/trunk/test/modules/tls/test_02_conf.py
httpd/httpd/trunk/test/modules/tls/test_03_sni.py
httpd/httpd/trunk/test/modules/tls/test_04_get.py
httpd/httpd/trunk/test/modules/tls/test_05_proto.py
httpd/httpd/trunk/test/modules/tls/test_06_ciphers.py
httpd/httpd/trunk/test/modules/tls/test_07_alpn.py
httpd/httpd/trunk/test/modules/tls/test_08_vars.py
httpd/httpd/trunk/test/modules/tls/test_09_timeout.py
httpd/httpd/trunk/test/modules/tls/test_10_session_id.py
httpd/httpd/trunk/test/modules/tls/test_11_md.py
httpd/httpd/trunk/test/modules/tls/test_12_cauth.py
httpd/httpd/trunk/test/modules/tls/test_13_proxy.py
httpd/httpd/trunk/test/modules/tls/test_14_proxy_ssl.py
httpd/httpd/trunk/test/modules/tls/test_15_proxy_tls.py
httpd/httpd/trunk/test/modules/tls/test_16_proxy_mixed.py
httpd/httpd/trunk/test/modules/tls/test_17_proxy_machine_cert.py
Added: httpd/httpd/trunk/test/modules/tls/__init__.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/__init__.py?rev=1895433&view=auto
==============================================================================
(empty)
Added: httpd/httpd/trunk/test/modules/tls/conf.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/conf.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/conf.py (added)
+++ httpd/httpd/trunk/test/modules/tls/conf.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,61 @@
+import os
+from typing import List, Dict, Any
+
+from pyhttpd.conf import HttpdConf
+from pyhttpd.env import HttpdTestEnv
+
+
+class TlsTestConf(HttpdConf):
+
+ def __init__(self, env: HttpdTestEnv, extras: Dict[str, Any] = None):
+ extras = extras if extras is not None else {}
+ super().__init__(env=env, extras=extras)
+
+ def start_tls_vhost(self, domains: List[str], port=None, ssl_module=None):
+ if ssl_module is None:
+ ssl_module = 'mod_tls'
+ super().start_vhost(domains=domains, port=port, doc_root=f"htdocs/{domains[0]}", ssl_module=ssl_module)
+
+ def end_tls_vhost(self):
+ self.end_vhost()
+
+ def add_tls_vhosts(self, domains: List[str], port=None, ssl_module=None):
+ for domain in domains:
+ self.start_tls_vhost(domains=[domain], port=port, ssl_module=ssl_module)
+ self.end_tls_vhost()
+
+ def add_md_vhosts(self, domains: List[str], port = None):
+ self.add([
+ f"LoadModule md_module {self.env.libexec_dir}/mod_md.so",
+ "LogLevel md:debug",
+ ])
+ for domain in domains:
+ self.add(f"<MDomain {domain}>")
+ for cred in self.env.ca.get_credentials_for_name(domain):
+ cert_file = os.path.relpath(cred.cert_file, self.env.server_dir)
+ pkey_file = os.path.relpath(cred.pkey_file, self.env.server_dir) if cred.pkey_file else cert_file
+ self.add([
+ f" MDCertificateFile {cert_file}",
+ f" MDCertificateKeyFile {pkey_file}",
+ ])
+ self.add("</MDomain>")
+ super().add_vhost(domains=[domain], port=port, doc_root=f"htdocs/{domain}",
+ with_ssl=True, with_certificates=False, ssl_module='mod_tls')
+
+ def add_md_base(self, domain: str):
+ self.add([
+ f"LoadModule md_module {self.env.libexec_dir}/mod_md.so",
+ "LogLevel md:debug",
+ f"ServerName {domain}",
+ "MDBaseServer on",
+ ])
+ self.add(f"TLSEngine {self.env.https_port}")
+ self.add(f"<MDomain {domain}>")
+ for cred in self.env.ca.get_credentials_for_name(domain):
+ cert_file = os.path.relpath(cred.cert_file, self.env.server_dir)
+ pkey_file = os.path.relpath(cred.pkey_file, self.env.server_dir) if cred.pkey_file else cert_file
+ self.add([
+ f"MDCertificateFile {cert_file}",
+ f"MDCertificateKeyFile {pkey_file}",
+ ])
+ self.add("</MDomain>")
Added: httpd/httpd/trunk/test/modules/tls/conftest.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/conftest.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/conftest.py (added)
+++ httpd/httpd/trunk/test/modules/tls/conftest.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,39 @@
+import logging
+import os
+import sys
+import pytest
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from .env import TlsTestEnv
+
+
+def pytest_report_header(config, startdir):
+ _x = config
+ _x = startdir
+ env = TlsTestEnv()
+ return "mod_tls [apache: {aversion}({prefix})]".format(
+ prefix=env.prefix,
+ aversion=env.get_httpd_version()
+ )
+
+
+@pytest.fixture(scope="package")
+def env(pytestconfig) -> TlsTestEnv:
+ level = logging.INFO
+ console = logging.StreamHandler()
+ console.setLevel(level)
+ console.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
+ logging.getLogger('').addHandler(console)
+ logging.getLogger('').setLevel(level=level)
+ env = TlsTestEnv(pytestconfig=pytestconfig)
+ env.setup_httpd()
+ env.apache_access_log_clear()
+ env.httpd_error_log.clear_log()
+ return env
+
+
+@pytest.fixture(autouse=True, scope="package")
+def _session_scope(env):
+ yield
+ assert env.apache_stop() == 0
Added: httpd/httpd/trunk/test/modules/tls/env.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/env.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/env.py (added)
+++ httpd/httpd/trunk/test/modules/tls/env.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,186 @@
+import inspect
+import logging
+import os
+import re
+import subprocess
+import sys
+import time
+
+from datetime import timedelta, datetime
+from http.client import HTTPConnection
+from typing import List, Optional, Dict, Tuple, Union
+from urllib.parse import urlparse
+
+from pyhttpd.certs import CertificateSpec
+from pyhttpd.env import HttpdTestEnv, HttpdTestSetup
+from pyhttpd.result import ExecResult
+
+log = logging.getLogger(__name__)
+
+
+class TlsTestSetup(HttpdTestSetup):
+
+ def __init__(self, env: 'HttpdTestEnv'):
+ super().__init__(env=env)
+ self.add_source_dir(os.path.dirname(inspect.getfile(TlsTestSetup)))
+ self.add_modules(["tls", "http2", "cgid", "watchdog", "proxy_http2"])
+
+
+class TlsCipher:
+
+ def __init__(self, id: int, name: str, flavour: str,
+ min_version: float, max_version: float = None,
+ openssl: str = None):
+ self.id = id
+ self.name = name
+ self.flavour = flavour
+ self.min_version = min_version
+ self.max_version = max_version if max_version is not None else self.min_version
+ if openssl is None:
+ if name.startswith('TLS13_'):
+ openssl = re.sub(r'^TLS13_', 'TLS_', name)
+ else:
+ openssl = re.sub(r'^TLS_', '', name)
+ openssl = re.sub(r'_WITH_([^_]+)_', r'_\1_', openssl)
+ openssl = re.sub(r'_AES_(\d+)', r'_AES\1', openssl)
+ openssl = re.sub(r'(_POLY1305)_\S+$', r'\1', openssl)
+ openssl = re.sub(r'_', '-', openssl)
+ self.openssl_name = openssl
+ self.id_name = "TLS_CIPHER_0x{0:04x}".format(self.id)
+
+ def __repr__(self):
+ return self.name
+
+ def __str__(self):
+ return self.name
+
+
+class TlsTestEnv(HttpdTestEnv):
+
+ # current rustls supported ciphers in their order of preference
+ # used to test cipher selection, see test_06_ciphers.py
+ RUSTLS_CIPHERS = [
+ TlsCipher(0x1303, "TLS13_CHACHA20_POLY1305_SHA256", "CHACHA", 1.3),
+ TlsCipher(0x1302, "TLS13_AES_256_GCM_SHA384", "AES", 1.3),
+ TlsCipher(0x1301, "TLS13_AES_128_GCM_SHA256", "AES", 1.3),
+ TlsCipher(0xcca9, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "ECDSA", 1.2),
+ TlsCipher(0xcca8, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "RSA", 1.2),
+ TlsCipher(0xc02c, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "ECDSA", 1.2),
+ TlsCipher(0xc02b, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "ECDSA", 1.2),
+ TlsCipher(0xc030, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "RSA", 1.2),
+ TlsCipher(0xc02f, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "RSA", 1.2),
+ ]
+
+ def __init__(self, pytestconfig=None):
+ super().__init__(pytestconfig=pytestconfig)
+ self._domain_a = "a.mod-tls.test"
+ self._domain_b = "b.mod-tls.test"
+ self.add_httpd_conf([
+ f'<Directory "{self.server_dir}/htdocs/{self.domain_a}">',
+ ' AllowOverride None',
+ ' Require all granted',
+ ' AddHandler cgi-script .py',
+ ' Options +ExecCGI',
+ '</Directory>',
+ f'<Directory "{self.server_dir}/htdocs/{self.domain_b}">',
+ ' AllowOverride None',
+ ' Require all granted',
+ ' AddHandler cgi-script .py',
+ ' Options +ExecCGI',
+ '</Directory>',
+ f'<VirtualHost *:{self.http_port}>',
+ ' ServerName localhost',
+ ' DocumentRoot "htdocs"',
+ '</VirtualHost>',
+ f'<VirtualHost *:{self.http_port}>',
+ f' ServerName {self.domain_a}',
+ ' DocumentRoot "htdocs/a.mod-tls.test"',
+ '</VirtualHost>',
+ f'<VirtualHost *:{self.http_port}>',
+ f' ServerName {self.domain_b}',
+ ' DocumentRoot "htdocs/b.mod-tls.test"',
+ '</VirtualHost>',
+ ])
+ self.add_cert_specs([
+ CertificateSpec(domains=[self.domain_a]),
+ CertificateSpec(domains=[self.domain_b], key_type='secp256r1', single_file=True),
+ CertificateSpec(domains=[self.domain_b], key_type='rsa4096'),
+ CertificateSpec(name="clientsX", sub_specs=[
+ CertificateSpec(name="user1", client=True, single_file=True),
+ CertificateSpec(name="user2", client=True, single_file=True),
+ CertificateSpec(name="user_expired", client=True,
+ single_file=True, valid_from=timedelta(days=-91),
+ valid_to=timedelta(days=-1)),
+ ]),
+ CertificateSpec(name="clientsY", sub_specs=[
+ CertificateSpec(name="user1", client=True, single_file=True),
+ ]),
+ CertificateSpec(name="user1", client=True, single_file=True),
+ ])
+ self.add_httpd_log_modules(['tls'])
+
+
+ def setup_httpd(self, setup: TlsTestSetup = None):
+ if setup is None:
+ setup = TlsTestSetup(env=self)
+ super().setup_httpd(setup=setup)
+
+ @property
+ def domain_a(self) -> str:
+ return self._domain_a
+
+ @property
+ def domain_b(self) -> str:
+ return self._domain_b
+
+ def tls_get(self, domain, paths: Union[str, List[str]], options: List[str] = None) -> ExecResult:
+ if isinstance(paths, str):
+ paths = [paths]
+ urls = [f"https://{domain}:{self.https_port}{path}" for path in paths]
+ return self.curl_raw(urls=urls, options=options)
+
+ def tls_get_json(self, domain: str, path: str, options=None):
+ r = self.tls_get(domain=domain, paths=path, options=options)
+ return r.json
+
+ def run_diff(self, fleft: str, fright: str) -> ExecResult:
+ return self.run(['diff', '-u', fleft, fright])
+
+ def openssl(self, args: List[str]) -> ExecResult:
+ return self.run(['openssl'] + args)
+
+ def openssl_client(self, domain, extra_args: List[str] = None) -> ExecResult:
+ args = ["s_client", "-CAfile", self.ca.cert_file, "-servername", domain,
+ "-connect", "localhost:{port}".format(
+ port=self.https_port
+ )]
+ if extra_args:
+ args.extend(extra_args)
+ args.extend([])
+ return self.openssl(args)
+
+ CURL_SUPPORTS_TLS_1_3 = None
+
+ def curl_supports_tls_1_3(self) -> bool:
+ if self.CURL_SUPPORTS_TLS_1_3 is None:
+ r = self.tls_get(self.domain_a, "/index.json", options=["--tlsv1.3"])
+ self.CURL_SUPPORTS_TLS_1_3 = r.exit_code == 0
+ return self.CURL_SUPPORTS_TLS_1_3
+
+ OPENSSL_SUPPORTED_PROTOCOLS = None
+
+ @staticmethod
+ def openssl_supports_tls_1_3() -> bool:
+ if TlsTestEnv.OPENSSL_SUPPORTED_PROTOCOLS is None:
+ env = TlsTestEnv()
+ r = env.openssl(args=["ciphers", "-v"])
+ protos = set()
+ ciphers = set()
+ for line in r.stdout.splitlines():
+ m = re.match(r'^(\S+)\s+(\S+)\s+(.*)$', line)
+ if m:
+ ciphers.add(m.group(1))
+ protos.add(m.group(2))
+ TlsTestEnv.OPENSSL_SUPPORTED_PROTOCOLS = protos
+ TlsTestEnv.OPENSSL_SUPPORTED_CIPHERS = ciphers
+ return "TLSv1.3" in TlsTestEnv.OPENSSL_SUPPORTED_PROTOCOLS
Added: httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/index.json
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/index.json?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/index.json (added)
+++ httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/index.json Tue Nov 30 16:30:26 2021
@@ -0,0 +1,3 @@
+{
+ "domain": "a.mod-tls.test"
+}
\ No newline at end of file
Added: httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/vars.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/vars.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/vars.py (added)
+++ httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/vars.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+import json
+import os, cgi
+import re
+
+jenc = json.JSONEncoder()
+
+def get_var(name: str, def_val: str = ""):
+ if name in os.environ:
+ return os.environ[name]
+ return def_val
+
+def get_json_var(name: str, def_val: str = ""):
+ var = get_var(name, def_val=def_val)
+ return jenc.encode(var)
+
+
+name = None
+try:
+ form = cgi.FieldStorage()
+ if 'name' in form:
+ name = str(form['name'].value)
+except Exception:
+ pass
+
+print("Content-Type: application/json\n")
+if name:
+ print(f"""{{ "{name}" : {get_json_var(name, '')}}}""")
+else:
+ print(f"""{{ "https" : {get_json_var('HTTPS', '')},
+ "host" : {get_json_var('SERVER_NAME', '')},
+ "protocol" : {get_json_var('SERVER_PROTOCOL', '')},
+ "ssl_protocol" : {get_json_var('SSL_PROTOCOL', '')},
+ "ssl_cipher" : {get_json_var('SSL_CIPHER', '')}
+}}""")
+
Propchange: httpd/httpd/trunk/test/modules/tls/htdocs/a.mod-tls.test/vars.py
------------------------------------------------------------------------------
svn:executable = *
Added: httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py (added)
+++ httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+import os
+
+def get_var(name: str, def_val: str = ""):
+ if name in os.environ:
+ return os.environ[name]
+ return def_val
+
+print("Content-Type: application/json")
+print()
+print("""{{ "https" : "{https}",
+ "host" : "{server_name}",
+ "protocol" : "{protocol}",
+ "ssl_protocol" : "{ssl_protocol}",
+ "ssl_cipher" : "{ssl_cipher}"
+}}""".format(
+ https=get_var('HTTPS', ''),
+ server_name=get_var('SERVER_NAME', ''),
+ protocol=get_var('SERVER_PROTOCOL', ''),
+ ssl_protocol=get_var('SSL_PROTOCOL', ''),
+ ssl_cipher=get_var('SSL_CIPHER', ''),
+))
+
Propchange: httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py
------------------------------------------------------------------------------
svn:executable = *
Added: httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/index.json
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/index.json?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/index.json (added)
+++ httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/index.json Tue Nov 30 16:30:26 2021
@@ -0,0 +1,3 @@
+{
+ "domain": "b.mod-tls.test"
+}
\ No newline at end of file
Added: httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py (added)
+++ httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+import random
+import sys
+import time
+from datetime import timedelta
+
+random.seed()
+to_write = total_len = random.randint(1, 10*1024*1024)
+
+sys.stdout.write("Content-Type: application/octet-stream\n")
+sys.stdout.write(f"Content-Length: {total_len}\n")
+sys.stdout.write("\n")
+sys.stdout.flush()
+
+while to_write > 0:
+ len = random.randint(1, 1024*1024)
+ len = min(len, to_write)
+ sys.stdout.buffer.write(random.randbytes(len))
+ to_write -= len
+ delay = timedelta(seconds=random.uniform(0.0, 0.5))
+ time.sleep(delay.total_seconds())
+sys.stdout.flush()
+
Propchange: httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py
------------------------------------------------------------------------------
svn:executable = *
Added: httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/vars.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/vars.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/vars.py (added)
+++ httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/vars.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+import json
+import os, cgi
+
+jenc = json.JSONEncoder()
+
+def get_var(name: str, def_val: str = ""):
+ if name in os.environ:
+ return os.environ[name]
+ return def_val
+
+def get_json_var(name: str, def_val: str = ""):
+ var = get_var(name, def_val=def_val)
+ return jenc.encode(var)
+
+
+name = None
+try:
+ form = cgi.FieldStorage()
+ if 'name' in form:
+ name = str(form['name'].value)
+except Exception:
+ pass
+
+print("Content-Type: application/json\n")
+if name:
+ print(f"""{{ "{name}" : {get_json_var(name, '')}}}""")
+else:
+ print(f"""{{ "https" : {get_json_var('HTTPS', '')},
+ "host" : {get_json_var('SERVER_NAME', '')},
+ "protocol" : {get_json_var('SERVER_PROTOCOL', '')},
+ "ssl_protocol" : {get_json_var('SSL_PROTOCOL', '')},
+ "ssl_cipher" : {get_json_var('SSL_CIPHER', '')}
+}}""")
+
Propchange: httpd/httpd/trunk/test/modules/tls/htdocs/b.mod-tls.test/vars.py
------------------------------------------------------------------------------
svn:executable = *
Added: httpd/httpd/trunk/test/modules/tls/htdocs/index.html
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/htdocs/index.html?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/htdocs/index.html (added)
+++ httpd/httpd/trunk/test/modules/tls/htdocs/index.html Tue Nov 30 16:30:26 2021
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <title>mod_h2 test site generic</title>
+ </head>
+ <body>
+ <h1>mod_h2 test site generic</h1>
+ </body>
+</html>
+
Added: httpd/httpd/trunk/test/modules/tls/htdocs/index.json
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/htdocs/index.json?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/htdocs/index.json (added)
+++ httpd/httpd/trunk/test/modules/tls/htdocs/index.json Tue Nov 30 16:30:26 2021
@@ -0,0 +1,3 @@
+{
+ "domain": "localhost"
+}
\ No newline at end of file
Added: httpd/httpd/trunk/test/modules/tls/load_test.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/load_test.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/load_test.py (added)
+++ httpd/httpd/trunk/test/modules/tls/load_test.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,882 @@
+import argparse
+import logging
+import multiprocessing
+import os
+import re
+import sys
+import time
+from datetime import timedelta, datetime
+from threading import Thread
+from tqdm import tqdm # type: ignore
+from typing import Dict, Iterable, List, Tuple, Optional
+
+sys.path.append(os.path.dirname(__file__))
+sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from conf import TlsTestConf
+from env import TlsTestEnv, TlsTestSetup
+from pyhttpd.result import ExecResult
+
+log = logging.getLogger(__name__)
+
+
+class LoadTestException(Exception):
+ pass
+
+
+class H2LoadLogSummary:
+
+ @staticmethod
+ def from_file(fpath: str, title: str, duration: timedelta) -> 'H2LoadLogSummary':
+ with open(fpath) as fd:
+ return H2LoadLogSummary.from_lines(fd.readlines(), title=title, duration=duration)
+
+ @staticmethod
+ def from_lines(lines: Iterable[str], title: str, duration: timedelta) -> 'H2LoadLogSummary':
+ stati = {}
+ count = 0
+ all_durations = timedelta(milliseconds=0)
+ for line in lines:
+ parts = re.split(r'\s+', line) # start(us), status(int), duration(ms), tbd.
+ if len(parts) >= 3 and parts[0] and parts[1] and parts[2]:
+ count += 1
+ status = int(parts[1])
+ if status in stati:
+ stati[status] += 1
+ else:
+ stati[status] = 1
+ all_durations += timedelta(microseconds=int(parts[2]))
+ else:
+ sys.stderr.write("unrecognize log line: {0}".format(line))
+ return H2LoadLogSummary(title=title, total=count, stati=stati,
+ duration=duration, all_durations=all_durations)
+
+ def __init__(self, title: str, total: int, stati: Dict[int, int],
+ duration: timedelta, all_durations: timedelta):
+ self._title = title
+ self._total = total
+ self._stati = stati
+ self._duration = duration
+ self._all_durations = all_durations
+ self._transfered_mb = 0.0
+ self._exec_result = None
+ self._expected_responses = 0
+
+ @property
+ def title(self) -> str:
+ return self._title
+
+ @property
+ def response_count(self) -> int:
+ return self._total
+
+ @property
+ def duration(self) -> timedelta:
+ return self._duration
+
+ @property
+ def response_durations(self) -> timedelta:
+ return self._all_durations
+
+ @property
+ def response_stati(self) -> Dict[int, int]:
+ return self._stati
+
+ @property
+ def expected_responses(self) -> int:
+ return self._expected_responses
+
+ @property
+ def execution(self) -> ExecResult:
+ return self._exec_result
+
+ def all_200(self) -> bool:
+ non_200s = [n for n in self._stati.keys() if n != 200]
+ return len(non_200s) == 0
+
+ @property
+ def throughput_mb(self) -> float:
+ if self._transfered_mb > 0.0:
+ return self._transfered_mb / self.duration.total_seconds()
+ return 0.0
+
+ def set_transfered_mb(self, mb: float) -> None:
+ self._transfered_mb = mb
+
+ def set_exec_result(self, result: ExecResult):
+ self._exec_result = result
+
+ def set_expected_responses(self, n: int):
+ self._expected_responses = n
+
+ def get_footnote(self) -> Optional[str]:
+ note = ""
+ if 0 < self.expected_responses != self.response_count:
+ note += "{0}/{1} missing".format(
+ self.expected_responses - self.response_count,
+ self.expected_responses
+ )
+ if not self.all_200():
+ note += ", non 200s:"
+ for status in [n for n in self.response_stati.keys() if n != 200]:
+ note += " {0}={1}".format(status, self.response_stati[status])
+ return note if len(note) else None
+
+
+class H2LoadMonitor:
+
+ def __init__(self, fpath: str, expected: int, title: str):
+ self._fpath = fpath
+ self._expected = expected
+ self._title = title
+ self._tqdm = tqdm(desc=title, total=expected, unit="request", leave=False)
+ self._running = False
+ self._lines = ()
+ self._tail = None
+
+ def start(self):
+ self._tail = Thread(target=self._collect, kwargs={'self': self})
+ self._running = True
+ self._tail.start()
+
+ def get_summary(self, duration: timedelta) -> H2LoadLogSummary:
+ self._running = False
+ self._tail.join()
+ return H2LoadLogSummary.from_file(self._fpath, title=self._title, duration=duration)
+
+ def stop(self):
+ self._running = False
+
+ @staticmethod
+ def _collect(self) -> None:
+ first_call = True
+ while self._running:
+ try:
+ with open(self._fpath) as fd:
+ if first_call:
+ fd.seek(0, 2)
+ first_call = False
+ latest_data = fd.read()
+ while self._running:
+ if '\n' not in latest_data:
+ latest_data += fd.read()
+ if '\n' not in latest_data:
+ if not os.path.isfile(self._fpath):
+ break
+ time.sleep(0.1)
+ continue
+ lines = latest_data.split('\n')
+ if lines[-1] != '\n':
+ latest_data = lines[-1]
+ lines = lines[:-1]
+ else:
+ latest_data = None
+ self._tqdm.update(n=len(lines))
+ if latest_data is None:
+ latest_data = fd.read()
+ except IOError:
+ time.sleep(0.1)
+ self._tqdm.close()
+
+
+def mk_text_file(fpath: str, lines: int):
+ t110 = ""
+ for _ in range(11):
+ t110 += "0123456789"
+ with open(fpath, "w") as fd:
+ for i in range(lines):
+ fd.write("{0:015d}: ".format(i)) # total 128 bytes per line
+ fd.write(t110)
+ fd.write("\n")
+
+
+class LoadTestCase:
+
+ @staticmethod
+ def from_scenario(scenario: Dict, env: TlsTestEnv) -> 'SingleFileLoadTest':
+ raise NotImplemented
+
+ def run(self) -> H2LoadLogSummary:
+ raise NotImplemented
+
+ def format_result(self, summary: H2LoadLogSummary) -> str:
+ raise NotImplemented
+
+ @staticmethod
+ def setup_base_conf(env: TlsTestEnv, worker_count: int = 5000, extras=None) -> TlsTestConf:
+ conf = TlsTestConf(env=env, extras=extras)
+ # ylavic's formula
+ process_count = int(max(10, min(100, int(worker_count / 100))))
+ thread_count = int(max(25, int(worker_count / process_count)))
+ conf.add(f"""
+ StartServers 1
+ ServerLimit {int(process_count * 2.5)}
+ ThreadLimit {thread_count}
+ ThreadsPerChild {thread_count}
+ MinSpareThreads {thread_count}
+ MaxSpareThreads {int(worker_count / 2)}
+ MaxRequestWorkers {worker_count}
+ MaxConnectionsPerChild 0
+ KeepAliveTimeout 60
+ MaxKeepAliveRequests 0
+ """)
+ return conf
+
+ @staticmethod
+ def start_server(env: TlsTestEnv, cd: timedelta = None):
+ if cd:
+ with tqdm(desc="connection cooldown", total=int(cd.total_seconds()), unit="s", leave=False) as t:
+ end = datetime.now() + cd
+ while datetime.now() < end:
+ time.sleep(1)
+ t.update()
+ assert env.apache_restart() == 0
+
+ @staticmethod
+ def server_setup(env: TlsTestEnv, ssl_module: str):
+ if 'mod_tls' == ssl_module:
+ extras = {
+ 'base': [
+ "Protocols h2 http/1.1",
+ "ProxyPreserveHost on",
+ f"TLSProxyCA {env.ca.cert_file}",
+ f"<Proxy https://127.0.0.1:{env.https_port}/>",
+ " TLSProxyEngine on",
+ "</Proxy>",
+ f"<Proxy h2://127.0.0.1:{env.https_port}/>",
+ " TLSProxyEngine on",
+ "</Proxy>",
+ ],
+ env.domain_a: [
+ f"ProxyPass /proxy-h1/ https://127.0.0.1:{env.https_port}/",
+ f"ProxyPass /proxy-h2/ h2://127.0.0.1:{env.https_port}/",
+ f"TLSOptions +StdEnvVars",
+ ],
+ }
+ elif 'mod_ssl' == ssl_module:
+ extras = {
+ 'base': [
+ "Protocols h2 http/1.1",
+ "ProxyPreserveHost on",
+ "SSLProxyVerify require",
+ f"SSLProxyCACertificateFile {env.ca.cert_file}",
+ f"<Proxy https://127.0.0.1:{env.https_port}/>",
+ " SSLProxyEngine on",
+ "</Proxy>",
+ f"<Proxy h2://127.0.0.1:{env.https_port}/>",
+ " SSLProxyEngine on",
+ "</Proxy>",
+ ],
+ env.domain_a: [
+ f"ProxyPass /proxy-h1/ https://127.0.0.1:{env.https_port}/",
+ f"ProxyPass /proxy-h2/ h2://127.0.0.1:{env.https_port}/",
+ "TLSOptions +StdEnvVars",
+ ],
+ }
+ elif 'mod_gnutls' == ssl_module:
+ extras = {
+ 'base': [
+ "Protocols h2 http/1.1",
+ "ProxyPreserveHost on",
+ f"GnuTLSProxyCAFile {env.ca.cert_file}",
+ f"<Proxy https://127.0.0.1:{env.https_port}/>",
+ " GnuTLSProxyEngine on",
+ "</Proxy>",
+ f"<Proxy h2://127.0.0.1:{env.https_port}/>",
+ " GnuTLSProxyEngine on",
+ "</Proxy>",
+ ],
+ env.domain_a: [
+ f"ProxyPass /proxy-h1/ https://127.0.0.1:{env.https_port}/",
+ f"ProxyPass /proxy-h2/ h2://127.0.0.1:{env.https_port}/",
+ ],
+ }
+ else:
+ raise LoadTestException("tests for module: {0}".format(ssl_module))
+ conf = LoadTestCase.setup_base_conf(env=env, extras=extras)
+ conf.add_tls_vhosts(domains=[env.domain_a], ssl_module=ssl_module)
+ conf.install()
+
+
+class SingleFileLoadTest(LoadTestCase):
+
+ def __init__(self, env: TlsTestEnv, location: str,
+ clients: int, requests: int, resource_kb: int,
+ ssl_module: str = 'mod_tls', protocol: str = 'h2',
+ threads: int = None):
+ self.env = env
+ self._location = location
+ self._clients = clients
+ self._requests = requests
+ self._resource_kb = resource_kb
+ self._ssl_module = ssl_module
+ self._protocol = protocol
+ self._threads = threads if threads is not None else min(multiprocessing.cpu_count() / 2, self._clients)
+
+ @staticmethod
+ def from_scenario(scenario: Dict, env: TlsTestEnv) -> 'SingleFileLoadTest':
+ return SingleFileLoadTest(
+ env=env,
+ location=scenario['location'],
+ clients=scenario['clients'], requests=scenario['requests'],
+ ssl_module=scenario['module'], resource_kb=scenario['rsize'],
+ protocol=scenario['protocol'] if 'protocol' in scenario else 'h2'
+ )
+
+ def _setup(self) -> str:
+ LoadTestCase.server_setup(env=self.env, ssl_module=self._ssl_module)
+ docs_a = os.path.join(self.env.server_docs_dir, self.env.domain_a)
+ fname = "{0}k.txt".format(self._resource_kb)
+ mk_text_file(os.path.join(docs_a, fname), 8 * self._resource_kb)
+ self.start_server(env=self.env)
+ return fname
+
+ def _teardown(self):
+ pass
+
+ def run_test(self, mode: str, path: str) -> H2LoadLogSummary:
+ monitor = None
+ try:
+ log_file = "{gen_dir}/h2load.log".format(gen_dir=self.env.gen_dir)
+ if os.path.isfile(log_file):
+ os.remove(log_file)
+ monitor = H2LoadMonitor(log_file, expected=self._requests,
+ title=f"{self._ssl_module}/{self._protocol}/"
+ f"{self._clients}c/{self._resource_kb / 1024}MB[{mode}]")
+ monitor.start()
+ args = [
+ 'h2load',
+ '--clients={0}'.format(self._clients),
+ '--threads={0}'.format(self._threads),
+ '--requests={0}'.format(self._requests),
+ '--log-file={0}'.format(log_file),
+ '--connect-to=localhost:{0}'.format(self.env.https_port)
+ ]
+ if self._protocol == 'h1' or self._protocol == 'http/1.1':
+ args.append('--h1')
+ elif self._protocol == 'h2':
+ args.extend(['-m', "6"])
+ else:
+ raise Exception(f"unknown protocol: {self._protocol}")
+ r = self.env.run(args + [
+ f'https://{self.env.domain_a}:{self.env.https_port}{self._location}{path}'
+ ])
+ if r.exit_code != 0:
+ raise LoadTestException("h2load returned {0}: {1}".format(r.exit_code, r.stderr))
+ summary = monitor.get_summary(duration=r.duration)
+ summary.set_expected_responses(self._requests)
+ summary.set_exec_result(r)
+ summary.set_transfered_mb(self._requests * self._resource_kb / 1024)
+ return summary
+ finally:
+ if monitor is not None:
+ monitor.stop()
+
+ def run(self) -> H2LoadLogSummary:
+ path = self._setup()
+ try:
+ self.run_test(mode="warmup", path=path)
+ return self.run_test(mode="measure", path=path)
+ finally:
+ self._teardown()
+
+ def format_result(self, summary: H2LoadLogSummary) -> Tuple[str, Optional[List[str]]]:
+ return "{0:.0f}".format(summary.throughput_mb), summary.get_footnote()
+
+
+class MultiFileLoadTest(LoadTestCase):
+ SETUP_DONE = False
+
+ def __init__(self, env: TlsTestEnv, location: str,
+ clients: int, requests: int, file_count: int,
+ file_sizes: List[int],
+ ssl_module: str = 'mod_tls', protocol: str = 'h2',
+ threads: int = None, ):
+ self.env = env
+ self._location = location
+ self._clients = clients
+ self._requests = requests
+ self._file_count = file_count
+ self._file_sizes = file_sizes
+ self._ssl_module = ssl_module
+ self._protocol = protocol
+ self._threads = threads if threads is not None else \
+ min(multiprocessing.cpu_count() / 2, self._clients)
+ self._url_file = "{gen_dir}/h2load-urls.txt".format(gen_dir=self.env.gen_dir)
+
+ @staticmethod
+ def from_scenario(scenario: Dict, env: TlsTestEnv) -> 'MultiFileLoadTest':
+ return MultiFileLoadTest(
+ env=env,
+ location=scenario['location'],
+ clients=scenario['clients'], requests=scenario['requests'],
+ file_sizes=scenario['file_sizes'], file_count=scenario['file_count'],
+ ssl_module=scenario['module'], protocol=scenario['protocol']
+ )
+
+ def _setup(self, cls):
+ LoadTestCase.server_setup(env=self.env, ssl_module=self._ssl_module)
+ if not cls.SETUP_DONE:
+ with tqdm(desc="setup resources", total=self._file_count, unit="file", leave=False) as t:
+ docs_a = os.path.join(self.env.server_docs_dir, self.env.domain_a)
+ uris = []
+ for i in range(self._file_count):
+ fsize = self._file_sizes[i % len(self._file_sizes)]
+ if fsize is None:
+ raise Exception("file sizes?: {0} {1}".format(i, fsize))
+ fname = "{0}-{1}k.txt".format(i, fsize)
+ mk_text_file(os.path.join(docs_a, fname), 8 * fsize)
+ uris.append(f"{self._location}{fname}")
+ t.update()
+ with open(self._url_file, 'w') as fd:
+ fd.write("\n".join(uris))
+ fd.write("\n")
+ cls.SETUP_DONE = True
+ self.start_server(env=self.env)
+
+ def _teardown(self):
+ pass
+
+ def run_test(self, mode: str, path: str) -> H2LoadLogSummary:
+ _path = path
+ monitor = None
+ try:
+ log_file = "{gen_dir}/h2load.log".format(gen_dir=self.env.gen_dir)
+ if os.path.isfile(log_file):
+ os.remove(log_file)
+ monitor = H2LoadMonitor(log_file, expected=self._requests,
+ title=f"{self._ssl_module}/{self._protocol}/"
+ f"{self._file_count / 1024}f/{self._clients}c[{mode}]")
+ monitor.start()
+ args = [
+ 'h2load',
+ '--clients={0}'.format(self._clients),
+ '--requests={0}'.format(self._requests),
+ '--input-file={0}'.format(self._url_file),
+ '--log-file={0}'.format(log_file),
+ '--connect-to=localhost:{0}'.format(self.env.https_port)
+ ]
+ if self._protocol == 'h1' or self._protocol == 'http/1.1':
+ args.append('--h1')
+ elif self._protocol == 'h2':
+ args.extend(['-m', "6"])
+ else:
+ raise Exception(f"unknown protocol: {self._protocol}")
+ r = self.env.run(args + [
+ f'--base-uri=https://{self.env.domain_a}:{self.env.https_port}{self._location}'
+ ])
+ if r.exit_code != 0:
+ raise LoadTestException("h2load returned {0}: {1}".format(r.exit_code, r.stderr))
+ summary = monitor.get_summary(duration=r.duration)
+ summary.set_expected_responses(self._requests)
+ summary.set_exec_result(r)
+ return summary
+ finally:
+ if monitor is not None:
+ monitor.stop()
+
+ def run(self) -> H2LoadLogSummary:
+ path = self._setup(self.__class__)
+ try:
+ time.sleep(1)
+ self.run_test(mode="warmup", path=path)
+ return self.run_test(mode="measure", path=path)
+ finally:
+ self._teardown()
+
+ def format_result(self, summary: H2LoadLogSummary) -> Tuple[str, Optional[List[str]]]:
+ return "{0:.0f}".format(
+ summary.response_count / summary.duration.total_seconds()
+ ), summary.get_footnote()
+
+
+class ConnectionLoadTest(LoadTestCase):
+ SETUP_DONE = False
+
+ def __init__(self, env: TlsTestEnv, location: str,
+ clients: int, requests: int, duration: timedelta,
+ file_count: int, file_sizes: List[int], cooldown: timedelta,
+ ssl_module: str = 'mod_tls', protocol: str = 'h2'):
+ self.env = env
+ self._location = location
+ self._clients = clients
+ self._requests = requests
+ self._duration = duration
+ self._file_count = file_count
+ self._file_sizes = file_sizes
+ self._ssl_module = ssl_module
+ self._protocol = protocol
+ self._url_file = "{gen_dir}/h2load-urls.txt".format(gen_dir=self.env.gen_dir)
+ self._cd = cooldown
+
+ @staticmethod
+ def from_scenario(scenario: Dict, env: TlsTestEnv) -> 'ConnectionLoadTest':
+ return ConnectionLoadTest(
+ env=env,
+ location=scenario['location'],
+ clients=scenario['clients'], requests=scenario['requests'],
+ duration=scenario['duration'], cooldown=scenario['cooldown'],
+ file_sizes=scenario['file_sizes'], file_count=scenario['file_count'],
+ ssl_module=scenario['module'], protocol=scenario['protocol']
+ )
+
+ def _setup(self):
+ LoadTestCase.server_setup(env=self.env, ssl_module=self._ssl_module)
+ if not ConnectionLoadTest.SETUP_DONE:
+ with tqdm(desc="setup resources", total=self._file_count, unit="file", leave=False) as t:
+ docs_a = os.path.join(self.env.server_docs_dir, self.env.domain_a)
+ uris = []
+ for i in range(self._file_count):
+ fsize = self._file_sizes[i % len(self._file_sizes)]
+ if fsize is None:
+ raise Exception("file sizes?: {0} {1}".format(i, fsize))
+ fname = "{0}-{1}k.txt".format(i, fsize)
+ mk_text_file(os.path.join(docs_a, fname), 8 * fsize)
+ uris.append(f"{self._location}{fname}")
+ t.update()
+ with open(self._url_file, 'w') as fd:
+ fd.write("\n".join(uris))
+ fd.write("\n")
+ ConnectionLoadTest.SETUP_DONE = True
+ self.start_server(env=self.env, cd=self._cd)
+
+ def _teardown(self):
+ pass
+
+ def run_test(self, mode: str, path: str) -> H2LoadLogSummary:
+ _mode = mode
+ _path = path
+ monitor = None
+ try:
+ log_file = "{gen_dir}/h2load.log".format(gen_dir=self.env.gen_dir)
+ if os.path.isfile(log_file):
+ os.remove(log_file)
+ monitor = H2LoadMonitor(log_file, expected=0,
+ title=f"{self._ssl_module}/{self._protocol}/"
+ f"{self._clients}c/{self._duration.total_seconds()}s")
+ monitor.start()
+ args = [
+ 'h2load',
+ '--clients={0}'.format(self._clients),
+ '--requests={0}'.format(self._requests * self._clients),
+ '--input-file={0}'.format(self._url_file),
+ '--log-file={0}'.format(log_file),
+ '--connect-to=localhost:{0}'.format(self.env.https_port)
+ ]
+ if self._protocol == 'h1' or self._protocol == 'http/1.1':
+ args.append('--h1')
+ elif self._protocol == 'h2':
+ args.extend(['-m', "6"])
+ else:
+ raise Exception(f"unknown protocol: {self._protocol}")
+ args += [
+ f'--base-uri=https://{self.env.domain_a}:{self.env.https_port}{self._location}'
+ ]
+ end = datetime.now() + self._duration
+ r = None
+ while datetime.now() < end:
+ r = self.env.run(args)
+ if r.exit_code != 0:
+ raise LoadTestException("h2load returned {0}: {1}".format(r.exit_code, r.stderr))
+ summary = monitor.get_summary(duration=self._duration)
+ summary.set_exec_result(r)
+ return summary
+ finally:
+ if monitor is not None:
+ monitor.stop()
+
+ def run(self) -> H2LoadLogSummary:
+ path = self._setup()
+ try:
+ return self.run_test(mode="measure", path=path)
+ finally:
+ self._teardown()
+
+ def format_result(self, summary: H2LoadLogSummary) -> Tuple[str, Optional[List[str]]]:
+ return "{0:.0f}".format(
+ summary.response_count / summary.duration.total_seconds() / self._requests
+ ), summary.get_footnote()
+
+
+class LoadTest:
+
+ @staticmethod
+ def print_table(table: List[List[str]], foot_notes: List[str] = None):
+ col_widths = []
+ col_sep = " "
+ for row in table[1:]:
+ for idx, cell in enumerate(row):
+ if idx >= len(col_widths):
+ col_widths.append(len(cell))
+ else:
+ col_widths[idx] = max(len(cell), col_widths[idx])
+ row_len = sum(col_widths) + (len(col_widths) * len(col_sep))
+ print(f"{' '.join(table[0]):^{row_len}}")
+ for row in table[1:]:
+ line = ""
+ for idx, cell in enumerate(row):
+ line += f"{col_sep if idx > 0 else ''}{cell:>{col_widths[idx]}}"
+ print(line)
+ if foot_notes is not None:
+ for idx, note in enumerate(foot_notes):
+ print("{0:3d}) {1}".format(idx + 1, note))
+
+ @staticmethod
+ def scenario_with(base: Dict, updates: Dict) -> Dict:
+ scenario = base.copy()
+ scenario.update(updates)
+ return scenario
+
+ @classmethod
+ def main(cls):
+ parser = argparse.ArgumentParser(prog='load_h1', description="""
+ Run a range of load tests against the test Apache setup.
+ """)
+ parser.add_argument("-m", "--module", type=str, default=None,
+ help="which module to test, defaults to all")
+ parser.add_argument("-p", "--protocol", type=str, default=None,
+ help="which protocols to test, defaults to all")
+ parser.add_argument("-v", "--verbose", action='count', default=0,
+ help="log more output on stderr")
+ parser.add_argument("names", nargs='*', help="Name(s) of scenarios to run")
+ args = parser.parse_args()
+
+ if args.verbose > 0:
+ console = logging.StreamHandler()
+ console.setLevel(logging.INFO)
+ console.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
+ logging.getLogger('').addHandler(console)
+
+ rv = 0
+ env = TlsTestEnv()
+
+ try:
+ log.debug("starting tests")
+
+ scenario_sf = {
+ "title": "sizes and throughput (MB/s)",
+ "class": SingleFileLoadTest,
+ "location": "/",
+ "clients": 0,
+ "row0_title": "module protocol",
+ "row_title": "{module} {protocol}",
+ "rows": [
+ {"module": "mod_ssl", "protocol": 'h1'},
+ {"module": "mod_tls", "protocol": 'h1'},
+ {"module": "mod_ssl", "protocol": 'h2'},
+ {"module": "mod_tls", "protocol": 'h2'},
+ ],
+ "col_title": "{rsize}KB",
+ "columns": [],
+ }
+ scenario_mf = {
+ "title": "connections and throughput (MB/s)",
+ "class": MultiFileLoadTest,
+ "location": "/",
+ "file_count": 1024,
+ "file_sizes": [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 10000],
+ "requests": 10000,
+ "row0_title": "module protocol",
+ "row_title": "{module} {protocol}",
+ "rows": [
+ {"module": "mod_ssl", "protocol": 'h1'},
+ {"module": "mod_tls", "protocol": 'h1'},
+ {"module": "mod_ssl", "protocol": 'h2'},
+ {"module": "mod_tls", "protocol": 'h2'},
+ ],
+ "col_title": "{clients}c",
+ "columns": [],
+ }
+ scenario_conn = {
+ "title": "connections",
+ "class": ConnectionLoadTest,
+ "location": "/",
+ "duration": timedelta(seconds=10),
+ "cooldown": timedelta(seconds=5),
+ "file_count": 12,
+ "file_sizes": [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 10000],
+ "requests": 1,
+ "clients": 1,
+ "row0_title": "module protocol",
+ "row_title": "{module} {protocol}",
+ "rows": [
+ {"module": "mod_ssl", "protocol": 'h1'},
+ {"module": "mod_tls", "protocol": 'h1'},
+ {"module": "mod_ssl", "protocol": 'h2'},
+ {"module": "mod_tls", "protocol": 'h2'},
+ ],
+ "col_title": "{clients}c",
+ "columns": [],
+ }
+
+ scenarios = {
+ "1c-throughput": cls.scenario_with(scenario_sf, {
+ "title": "1 conn, 1k-10k requests, *sizes, throughput (MB/s)",
+ "clients": 1,
+ "columns": [
+ {"requests": 10000, "rsize": 10},
+ {"requests": 6000, "rsize": 100},
+ {"requests": 3000, "rsize": 1024},
+ {"requests": 1000, "rsize": 10 * 1024},
+ ],
+ }),
+ "10c-throughput": cls.scenario_with(scenario_sf, {
+ "title": "10 conn, 5k-50k requests, *sizes, throughput (MB/s)",
+ "clients": 10,
+ "columns": [
+ {"requests": 50000, "rsize": 10},
+ {"requests": 25000, "rsize": 100},
+ {"requests": 10000, "rsize": 1024},
+ {"requests": 5000, "rsize": 10 * 1024},
+ ],
+ }),
+ "20c-throughput": cls.scenario_with(scenario_sf, {
+ "title": "20 conn, 5k-50k requests, *sizes, throughput (MB/s)",
+ "clients": 20,
+ "columns": [
+ {"requests": 50000, "rsize": 10},
+ {"requests": 25000, "rsize": 100},
+ {"requests": 10000, "rsize": 1024},
+ {"requests": 5000, "rsize": 10 * 1024},
+ ],
+ }),
+ "50c-throughput": cls.scenario_with(scenario_sf, {
+ "title": "50 conn, 10k-100k requests, *sizes, throughput (MB/s)",
+ "clients": 50,
+ "columns": [
+ {"requests": 100000, "rsize": 10},
+ {"requests": 50000, "rsize": 100},
+ {"requests": 10000, "rsize": 1024},
+ {"requests": 5000, "rsize": 10 * 1024},
+ ],
+ }),
+ "1k-files": cls.scenario_with(scenario_mf, {
+ "title": "1k files, 1k-10MB, *conn, 10k req, (req/s)",
+ "clients": 1,
+ "columns": [
+ {"clients": 1},
+ {"clients": 2},
+ {"clients": 4},
+ {"clients": 8},
+ {"clients": 16},
+ {"clients": 32},
+ {"clients": 64},
+ ],
+ }),
+ "1k-files-proxy-h1": cls.scenario_with(scenario_mf, {
+ "location": "/proxy-h1/",
+ "title": "1k files, h1 proxy, 1k-10MB, *conn, 10k req, (req/s)",
+ "clients": 1,
+ "columns": [
+ {"clients": 1},
+ {"clients": 2},
+ {"clients": 4},
+ {"clients": 8},
+ {"clients": 16},
+ {"clients": 32},
+ {"clients": 64},
+ ],
+ }),
+ "1k-files-proxy-h2": cls.scenario_with(scenario_mf, {
+ "location": "/proxy-h2/",
+ "title": "1k files, h2 proxy, 1k-10MB, *conn, 10k req, (req/s)",
+ "clients": 1,
+ "columns": [
+ {"clients": 1},
+ {"clients": 2},
+ {"clients": 4},
+ {"clients": 8},
+ {"clients": 16},
+ {"clients": 32},
+ {"clients": 64},
+ ],
+ }),
+ "1m-reqs": cls.scenario_with(scenario_mf, {
+ "title": "1m requests, 1k files, 1k-10MB, (req/s)",
+ "clients": 1,
+ "requests": 1000000,
+ "columns": [
+ {"clients": 1},
+ {"clients": 4},
+ {"clients": 16},
+ {"clients": 64},
+ ],
+ }),
+ "conn-scale": cls.scenario_with(scenario_conn, {
+ "title": "c parallel clients, 1 req/c (conn/s)",
+ "requests": 1,
+ "duration": timedelta(seconds=30),
+ "cooldown": timedelta(seconds=10),
+ "columns": [
+ {"clients": 1},
+ {"clients": 2},
+ {"clients": 4},
+ {"clients": 8},
+ {"clients": 16},
+ {"clients": 32},
+ ],
+ }),
+ "conn-limits": cls.scenario_with(scenario_conn, {
+ "title": "c parallel clients, 1 req/c (conn/s)",
+ "requests": 1,
+ "duration": timedelta(seconds=10),
+ "cooldown": timedelta(seconds=30),
+ "columns": [
+ {"clients": 64},
+ {"clients": 128},
+ {"clients": 256},
+ {"clients": 512},
+ ],
+ }),
+ }
+ for name in args.names:
+ if name not in scenarios:
+ raise LoadTestException(f"scenario unknown: '{name}'")
+ names = args.names if len(args.names) else sorted(scenarios.keys())
+
+ setup = TlsTestSetup(env=env)
+ env.setup_httpd(setup=setup)
+
+ for name in names:
+ scenario = scenarios[name]
+ table = [
+ [scenario['title']],
+ ]
+ foot_notes = []
+ headers = [scenario['row0_title']]
+ for col in scenario['columns']:
+ headers.append(scenario['col_title'].format(**col))
+ table.append(headers)
+ cls.print_table(table)
+ for row in scenario['rows']:
+ if args.module is not None and row['module'] != args.module:
+ continue
+ if args.protocol is not None and row['protocol'] != args.protocol:
+ continue
+ row_line = [scenario['row_title'].format(**row)]
+ table.append(row_line)
+ for col in scenario['columns']:
+ t = scenario.copy()
+ t.update(row)
+ t.update(col)
+ test = scenario['class'].from_scenario(t, env=env)
+ env.httpd_error_log.clear_log()
+ summary = test.run()
+ result, fnote = test.format_result(summary)
+ if fnote:
+ foot_notes.append(fnote)
+ row_line.append("{0}{1}".format(result,
+ f"[{len(foot_notes)}]" if fnote else ""))
+ cls.print_table(table, foot_notes)
+ except KeyboardInterrupt:
+ rv = 1
+ except LoadTestException as ex:
+ sys.stderr.write(f"ERROR: {str(ex)}\n")
+ rv = 1
+
+ env.apache_stop()
+ sys.exit(rv)
+
+
+if __name__ == "__main__":
+ LoadTest.main()
Added: httpd/httpd/trunk/test/modules/tls/test_01_apache.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/test_01_apache.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/test_01_apache.py (added)
+++ httpd/httpd/trunk/test/modules/tls/test_01_apache.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,14 @@
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestApache:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ TlsTestConf(env=env).install()
+ assert env.apache_restart() == 0
+
+ def test_01_apache_http(self, env):
+ assert env.is_live(env.http_base_url)
Added: httpd/httpd/trunk/test/modules/tls/test_02_conf.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/test_02_conf.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/test_02_conf.py (added)
+++ httpd/httpd/trunk/test/modules/tls/test_02_conf.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,138 @@
+import os
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestConf:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ TlsTestConf(env=env).install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ if env.is_live(timeout=timedelta(milliseconds=100)):
+ assert env.apache_stop() == 0
+
+ def test_02_conf_cert_args_missing(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCertificate")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ def test_02_conf_cert_single_arg(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCertificate cert.pem")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ def test_02_conf_cert_file_missing(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCertificate cert.pem key.pem")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ def test_02_conf_cert_file_exist(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCertificate test-02-cert.pem test-02-key.pem")
+ conf.install()
+ for name in ["test-02-cert.pem", "test-02-key.pem"]:
+ with open(os.path.join(env.server_dir, name), "w") as fd:
+ fd.write("")
+ assert env.apache_fail() == 0
+
+ def test_02_conf_cert_listen_missing(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSEngine")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ def test_02_conf_cert_listen_wrong(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSEngine ^^^^^")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ @pytest.mark.parametrize("listen", [
+ "443",
+ "129.168.178.188:443",
+ "[::]:443",
+ ])
+ def test_02_conf_cert_listen_valid(self, env, listen: str):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSEngine {listen}".format(listen=listen))
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_02_conf_cert_listen_cert(self, env):
+ domain = env.domain_a
+ conf = TlsTestConf(env=env)
+ conf.add_tls_vhosts(domains=[domain])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_02_conf_proto_wrong(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSProtocol wrong")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ @pytest.mark.parametrize("proto", [
+ "default",
+ "TLSv1.2+",
+ "TLSv1.3+",
+ "TLSv0x0303+",
+ ])
+ def test_02_conf_proto_valid(self, env, proto):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSProtocol {proto}".format(proto=proto))
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_02_conf_honor_wrong(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSHonorClientOrder wrong")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ @pytest.mark.parametrize("honor", [
+ "on",
+ "OfF",
+ ])
+ def test_02_conf_honor_valid(self, env, honor: str):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSHonorClientOrder {honor}".format(honor=honor))
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.mark.parametrize("cipher", [
+ "default",
+ "TLS13_AES_128_GCM_SHA256:TLS13_AES_256_GCM_SHA384:TLS13_CHACHA20_POLY1305_SHA256",
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:"
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:"
+ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+ """TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 \\
+ TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\\
+ TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"""
+ ])
+ def test_02_conf_cipher_valid(self, env, cipher):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCiphersPrefer {cipher}".format(cipher=cipher))
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.mark.parametrize("cipher", [
+ "wrong",
+ "YOLO",
+ "TLS_NULL_WITH_NULL_NULLX", # not supported
+ "TLS_DHE_RSA_WITH_AES128_GCM_SHA256", # not supported
+ ])
+ def test_02_conf_cipher_wrong(self, env, cipher):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCiphersPrefer {cipher}".format(cipher=cipher))
+ conf.install()
+ assert env.apache_fail() == 0
Added: httpd/httpd/trunk/test/modules/tls/test_03_sni.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/test_03_sni.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/test_03_sni.py (added)
+++ httpd/httpd/trunk/test/modules/tls/test_03_sni.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,73 @@
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestSni:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ env.curl_supports_tls_1_3() # init
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ def test_03_sni_get_a(self, env):
+ # do we see the correct json for the domain_a?
+ data = env.tls_get_json(env.domain_a, "/index.json")
+ assert data == {'domain': env.domain_a}
+
+ def test_03_sni_get_b(self, env):
+ # do we see the correct json for the domain_a?
+ data = env.tls_get_json(env.domain_b, "/index.json")
+ assert data == {'domain': env.domain_b}
+
+ def test_03_sni_unknown(self, env):
+ # connection will be denied as cert does not cover this domain
+ domain_unknown = "unknown.test"
+ r = env.tls_get(domain_unknown, "/index.json")
+ assert r.exit_code != 0
+
+ def test_03_sni_request_other_same_config(self, env):
+ # do we see the first vhost respone for another domain with different certs?
+ r = env.tls_get(env.domain_a, "/index.json", options=[
+ "-vvvv", "--header", "Host: {0}".format(env.domain_b)
+ ])
+ # request is marked as misdirected
+ assert r.exit_code == 0
+ assert r.json is None
+ assert r.response['status'] == 421
+
+ def test_03_sni_request_other_other_honor(self, env):
+ if env.curl_supports_tls_1_3():
+ # can't do this test then
+ return
+ # do we see the first vhost respone for an unknown domain?
+ conf = TlsTestConf(env=env, extras={
+ env.domain_a: "TLSProtocol TLSv1.2+",
+ env.domain_b: "TLSProtocol TLSv1.3+"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.tls_get(env.domain_a, "/index.json", options=[
+ "-vvvv", "--header", "Host: {0}".format(env.domain_b)
+ ])
+ # request denied
+ assert r.exit_code == 0
+ assert r.json is None
+
+ def test_03_sni_bad_hostname(self, env):
+ # curl checks hostnames we give it, but the openssl client
+ # does not. Good for us, since we need to test it.
+ r = env.openssl(["s_client", "-connect",
+ "localhost:{0}".format(env.https_port),
+ "-servername", b'x\x2f.y'.decode()])
+ assert r.exit_code == 1, r.stderr
Added: httpd/httpd/trunk/test/modules/tls/test_04_get.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/test_04_get.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/test_04_get.py (added)
+++ httpd/httpd/trunk/test/modules/tls/test_04_get.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,67 @@
+import os
+import time
+from datetime import timedelta
+
+import pytest
+
+from .env import TlsTestEnv
+from .conf import TlsTestConf
+
+
+def mk_text_file(fpath: str, lines: int):
+ t110 = 11 * "0123456789"
+ with open(fpath, "w") as fd:
+ for i in range(lines):
+ fd.write("{0:015d}: ".format(i)) # total 128 bytes per line
+ fd.write(t110)
+ fd.write("\n")
+
+
+class TestGet:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ docs_a = os.path.join(env.server_docs_dir, env.domain_a)
+ mk_text_file(os.path.join(docs_a, "1k.txt"), 8)
+ mk_text_file(os.path.join(docs_a, "10k.txt"), 80)
+ mk_text_file(os.path.join(docs_a, "100k.txt"), 800)
+ mk_text_file(os.path.join(docs_a, "1m.txt"), 8000)
+ mk_text_file(os.path.join(docs_a, "10m.txt"), 80000)
+ assert env.apache_restart() == 0
+
+ @pytest.mark.parametrize("fname, flen", [
+ ("1k.txt", 1024),
+ ("10k.txt", 10*1024),
+ ("100k.txt", 100 * 1024),
+ ("1m.txt", 1000 * 1024),
+ ("10m.txt", 10000 * 1024),
+ ])
+ def test_04_get(self, env, fname, flen):
+ # do we see the correct json for the domain_a?
+ docs_a = os.path.join(env.server_docs_dir, env.domain_a)
+ r = env.tls_get(env.domain_a, "/{0}".format(fname))
+ assert r.exit_code == 0
+ assert len(r.stdout) == flen
+ pref = os.path.join(docs_a, fname)
+ pout = os.path.join(docs_a, "{0}.out".format(fname))
+ with open(pout, 'w') as fd:
+ fd.write(r.stdout)
+ dr = env.run_diff(pref, pout)
+ assert dr.exit_code == 0, "differences found:\n{0}".format(dr.stdout)
+
+ @pytest.mark.parametrize("fname, flen", [
+ ("1k.txt", 1024),
+ ])
+ def test_04_double_get(self, env, fname, flen):
+ # we'd like to check that we can do >1 requests on the same connection
+ # however curl hides that from us, unless we analyze its verbose output
+ docs_a = os.path.join(env.server_docs_dir, env.domain_a)
+ r = env.tls_get(env.domain_a, paths=[
+ "/{0}".format(fname),
+ "/{0}".format(fname)
+ ])
+ assert r.exit_code == 0
+ assert len(r.stdout) == 2*flen
Added: httpd/httpd/trunk/test/modules/tls/test_05_proto.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/test_05_proto.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/test_05_proto.py (added)
+++ httpd/httpd/trunk/test/modules/tls/test_05_proto.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,67 @@
+import time
+from datetime import timedelta
+import socket
+from threading import Thread
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestProto:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_a: "TLSProtocol TLSv1.3+",
+ env.domain_b: [
+ "# the commonly used name",
+ "TLSProtocol TLSv1.2+",
+ "# the numeric one (yes, this is 1.2)",
+ "TLSProtocol TLSv0x0303+",
+ ],
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ CURL_SUPPORTS_TLS_1_3 = None
+
+ def test_05_proto_1_2(self, env):
+ r = env.tls_get(env.domain_b, "/index.json", options=["--tlsv1.2"])
+ assert r.exit_code == 0, r.stderr
+ if env.curl_supports_tls_1_3():
+ r = env.tls_get(env.domain_b, "/index.json", options=["--tlsv1.3"])
+ assert r.exit_code == 0, r.stderr
+
+ def test_05_proto_1_3(self, env):
+ r = env.tls_get(env.domain_a, "/index.json", options=["--tlsv1.3"])
+ if env.curl_supports_tls_1_3():
+ assert r.exit_code == 0, r.stderr
+ else:
+ assert r.exit_code == 4, r.stderr
+
+ def test_05_proto_close(self, env):
+ s = socket.create_connection(('localhost', env.https_port))
+ time.sleep(0.1)
+ s.close()
+
+ def test_05_proto_ssl_close(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': "LogLevel ssl:debug",
+ env.domain_a: "SSLProtocol TLSv1.3",
+ env.domain_b: "SSLProtocol TLSv1.2",
+ })
+ for d in [env.domain_a, env.domain_b]:
+ conf.add_vhost(domains=[d], port=env.https_port)
+ conf.install()
+ assert env.apache_restart() == 0
+ s = socket.create_connection(('localhost', env.https_port))
+ time.sleep(0.1)
+ s.close()
+
+
Added: httpd/httpd/trunk/test/modules/tls/test_06_ciphers.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/test_06_ciphers.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/test_06_ciphers.py (added)
+++ httpd/httpd/trunk/test/modules/tls/test_06_ciphers.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,209 @@
+import re
+from datetime import timedelta
+
+import pytest
+
+from .env import TlsTestEnv
+from .conf import TlsTestConf
+
+
+class TestCiphers:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': "TLSHonorClientOrder off",
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ def _get_protocol_cipher(self, output: str):
+ protocol = None
+ cipher = None
+ for line in output.splitlines():
+ m = re.match(r'^\s+Protocol\s*:\s*(\S+)$', line)
+ if m:
+ protocol = m.group(1)
+ continue
+ m = re.match(r'^\s+Cipher\s*:\s*(\S+)$', line)
+ if m:
+ cipher = m.group(1)
+ return protocol, cipher
+
+ def test_06_ciphers_ecdsa(self, env):
+ ecdsa_1_2 = [c for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'ECDSA'][0]
+ # client speaks only this cipher, see that it gets it
+ r = env.openssl_client(env.domain_b, extra_args=[
+ "-cipher", ecdsa_1_2.openssl_name, "-tls1_2"
+ ])
+ protocol, cipher = self._get_protocol_cipher(r.stdout)
+ assert protocol == "TLSv1.2", r.stdout
+ assert cipher == ecdsa_1_2.openssl_name, r.stdout
+
+ def test_06_ciphers_rsa(self, env):
+ rsa_1_2 = [c for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'RSA'][0]
+ # client speaks only this cipher, see that it gets it
+ r = env.openssl_client(env.domain_b, extra_args=[
+ "-cipher", rsa_1_2.openssl_name, "-tls1_2"
+ ])
+ protocol, cipher = self._get_protocol_cipher(r.stdout)
+ assert protocol == "TLSv1.2", r.stdout
+ assert cipher == rsa_1_2.openssl_name, r.stdout
+
+ @pytest.mark.parametrize("cipher", [
+ c for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'ECDSA'
+ ], ids=[
+ c.name for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'ECDSA'
+ ])
+ def test_06_ciphers_server_prefer_ecdsa(self, env, cipher):
+ # Select a ECSDA ciphers as preference and suppress all RSA ciphers.
+ # The last is not strictly necessary since rustls prefers ECSDA anyway
+ suppress_names = [c.name for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'RSA']
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSHonorClientOrder off",
+ f"TLSCiphersPrefer {cipher.name}",
+ f"TLSCiphersSuppress {':'.join(suppress_names)}",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.openssl_client(env.domain_b, extra_args=["-tls1_2"])
+ client_proto, client_cipher = self._get_protocol_cipher(r.stdout)
+ assert client_proto == "TLSv1.2", r.stdout
+ assert client_cipher == cipher.openssl_name, r.stdout
+
+ @pytest.mark.skip(reason="Wrong certified key selected by rustls")
+ # see <https://github.com/rustls/rustls-ffi/issues/236>
+ @pytest.mark.parametrize("cipher", [
+ c for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ], ids=[
+ c.name for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ])
+ def test_06_ciphers_server_prefer_rsa(self, env, cipher):
+ # Select a RSA ciphers as preference and suppress all ECDSA ciphers.
+ # The last is necessary since rustls prefers ECSDA and openssl leaks that it can.
+ suppress_names = [c.name for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'ECDSA']
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSHonorClientOrder off",
+ f"TLSCiphersPrefer {cipher.name}",
+ f"TLSCiphersSuppress {':'.join(suppress_names)}",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.openssl_client(env.domain_b, extra_args=["-tls1_2"])
+ client_proto, client_cipher = self._get_protocol_cipher(r.stdout)
+ assert client_proto == "TLSv1.2", r.stdout
+ assert client_cipher == cipher.openssl_name, r.stdout
+
+ @pytest.mark.skip(reason="Wrong certified key selected by rustls")
+ # see <https://github.com/rustls/rustls-ffi/issues/236>
+ @pytest.mark.parametrize("cipher", [
+ c for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ], ids=[
+ c.openssl_name for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ])
+ def test_06_ciphers_server_prefer_rsa_alias(self, env, cipher):
+ # same as above, but using openssl names for ciphers
+ suppress_names = [c.openssl_name for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'ECDSA']
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSHonorClientOrder off",
+ f"TLSCiphersPrefer {cipher.openssl_name}",
+ f"TLSCiphersSuppress {':'.join(suppress_names)}",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.openssl_client(env.domain_b, extra_args=["-tls1_2"])
+ client_proto, client_cipher = self._get_protocol_cipher(r.stdout)
+ assert client_proto == "TLSv1.2", r.stdout
+ assert client_cipher == cipher.openssl_name, r.stdout
+
+ @pytest.mark.skip(reason="Wrong certified key selected by rustls")
+ # see <https://github.com/rustls/rustls-ffi/issues/236>
+ @pytest.mark.parametrize("cipher", [
+ c for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ], ids=[
+ c.id_name for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ])
+ def test_06_ciphers_server_prefer_rsa_id(self, env, cipher):
+ # same as above, but using openssl names for ciphers
+ suppress_names = [c.id_name for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'ECDSA']
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSHonorClientOrder off",
+ f"TLSCiphersPrefer {cipher.id_name}",
+ f"TLSCiphersSuppress {':'.join(suppress_names)}",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.openssl_client(env.domain_b, extra_args=["-tls1_2"])
+ client_proto, client_cipher = self._get_protocol_cipher(r.stdout)
+ assert client_proto == "TLSv1.2", r.stdout
+ assert client_cipher == cipher.openssl_name, r.stdout
+
+ def test_06_ciphers_pref_unknown(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "TLSCiphersPrefer TLS_MY_SUPER_CIPHER:SSL_WHAT_NOT"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() != 0
+ # get a working config again, so that subsequent test cases do not stumble
+ conf = TlsTestConf(env=env)
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ env.apache_restart()
+
+ def test_06_ciphers_pref_unsupported(self, env):
+ # a warning on prefering a known, but not supported cipher
+ env.httpd_error_log.ignore_recent()
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "TLSCiphersPrefer TLS_NULL_WITH_NULL_NULL"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ (errors, warnings) = env.httpd_error_log.get_recent_count()
+ assert errors == 0
+ assert warnings == 2 # once on dry run, once on start
+
+ def test_06_ciphers_supp_unknown(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "TLSCiphersSuppress TLS_MY_SUPER_CIPHER:SSL_WHAT_NOT"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() != 0
+
+ def test_06_ciphers_supp_unsupported(self, env):
+ # no warnings on suppressing known, but not supported ciphers
+ env.httpd_error_log.ignore_recent()
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "TLSCiphersSuppress TLS_NULL_WITH_NULL_NULL"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ (errors, warnings) = env.httpd_error_log.get_recent_count()
+ assert errors == 0
+ assert warnings == 0
Added: httpd/httpd/trunk/test/modules/tls/test_07_alpn.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/test_07_alpn.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/test_07_alpn.py (added)
+++ httpd/httpd/trunk/test/modules/tls/test_07_alpn.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,43 @@
+import re
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestAlpn:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "Protocols h2 http/1.1"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ def _get_protocol(self, output: str):
+ for line in output.splitlines():
+ m = re.match(r'^\*\s+ALPN, server accepted to use\s+(.*)$', line)
+ if m:
+ return m.group(1)
+ return None
+
+ def test_07_alpn_get_a(self, env):
+ # do we see the correct json for the domain_a?
+ r = env.tls_get(env.domain_a, "/index.json", options=["-vvvvvv"])
+ assert r.exit_code == 0, r.stderr
+ protocol = self._get_protocol(r.stderr)
+ assert protocol == "http/1.1", r.stderr
+
+ def test_07_alpn_get_b(self, env):
+ # do we see the correct json for the domain_a?
+ r = env.tls_get(env.domain_b, "/index.json", options=["-vvvvvv"])
+ assert r.exit_code == 0, r.stderr
+ protocol = self._get_protocol(r.stderr)
+ assert protocol == "h2", r.stderr
Added: httpd/httpd/trunk/test/modules/tls/test_08_vars.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/test_08_vars.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/test_08_vars.py (added)
+++ httpd/httpd/trunk/test/modules/tls/test_08_vars.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,63 @@
+import re
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestVars:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': [
+ "TLSHonorClientOrder off",
+ "TLSOptions +StdEnvVars",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_08_vars_root(self, env):
+ # in domain_b root, the StdEnvVars is switch on
+ if env.curl_supports_tls_1_3():
+ exp_proto = "TLSv1.3"
+ exp_cipher = "TLS_AES_256_GCM_SHA384"
+ else:
+ exp_proto = "TLSv1.2"
+ exp_cipher = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
+ r = env.tls_get(env.domain_b, "/vars.py")
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {
+ 'https': 'on',
+ 'host': 'b.mod-tls.test',
+ 'protocol': 'HTTP/1.1',
+ 'ssl_protocol': exp_proto,
+ # this will vary by client potentially
+ 'ssl_cipher': exp_cipher,
+ }, r.stdout
+
+ @pytest.mark.parametrize("name, value", [
+ ("SERVER_NAME", "b.mod-tls.test"),
+ ("SSL_SESSION_RESUMED", "Initial"),
+ ("SSL_SECURE_RENEG", "false"),
+ ("SSL_COMPRESS_METHOD", "NULL"),
+ ("SSL_CIPHER_EXPORT", "false"),
+ ("SSL_CLIENT_VERIFY", "NONE"),
+ ])
+ def test_08_vars_const(self, env, name: str, value: str):
+ r = env.tls_get(env.domain_b, f"/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {name: value}, r.stdout
+
+ @pytest.mark.parametrize("name, pattern", [
+ ("SSL_VERSION_INTERFACE", r'mod_tls/\d+\.\d+\.\d+'),
+ ("SSL_VERSION_LIBRARY", r'rustls-ffi/\d+\.\d+\.\d+/rustls/\d+\.\d+\.\d+'),
+ ])
+ def test_08_vars_match(self, env, name: str, pattern: str):
+ r = env.tls_get(env.domain_b, f"/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert name in r.json
+ assert re.match(pattern, r.json[name]), r.json
Added: httpd/httpd/trunk/test/modules/tls/test_09_timeout.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/tls/test_09_timeout.py?rev=1895433&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/tls/test_09_timeout.py (added)
+++ httpd/httpd/trunk/test/modules/tls/test_09_timeout.py Tue Nov 30 16:30:26 2021
@@ -0,0 +1,43 @@
+import socket
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestTimeout:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': "RequestReadTimeout handshake=1",
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ def test_09_timeout_handshake(self, env):
+ # in domain_b root, the StdEnvVars is switch on
+ s = socket.create_connection(('localhost', env.https_port))
+ s.send(b'1234')
+ s.settimeout(0.0)
+ try:
+ s.recv(1024)
+ assert False, "able to recv() on a TLS connection before we sent a hello"
+ except BlockingIOError:
+ pass
+ s.settimeout(3.0)
+ try:
+ while True:
+ buf = s.recv(1024)
+ if not buf:
+ break
+ print("recv() -> {0}".format(buf))
+ except (socket.timeout, BlockingIOError):
+ assert False, "socket not closed as handshake timeout should trigger"
+ s.close()