summaryrefslogtreecommitdiffstats
path: root/test/modules/http1
diff options
context:
space:
mode:
Diffstat (limited to 'test/modules/http1')
-rw-r--r--test/modules/http1/__init__.py0
-rw-r--r--test/modules/http1/conftest.py36
-rw-r--r--test/modules/http1/env.py81
-rw-r--r--test/modules/http1/htdocs/cgi/files/empty.txt0
-rwxr-xr-xtest/modules/http1/htdocs/cgi/hello.py15
-rw-r--r--test/modules/http1/htdocs/cgi/requestparser.py57
-rwxr-xr-xtest/modules/http1/htdocs/cgi/upload.py55
-rw-r--r--test/modules/http1/mod_h1test/mod_h1test.c129
-rw-r--r--test/modules/http1/mod_h1test/mod_h1test.slo0
-rw-r--r--test/modules/http1/test_001_alive.py20
-rw-r--r--test/modules/http1/test_003_get.py27
-rw-r--r--test/modules/http1/test_004_post.py53
-rw-r--r--test/modules/http1/test_005_trailers.py42
-rw-r--r--test/modules/http1/test_006_unsafe.py134
-rw-r--r--test/modules/http1/test_007_strict.py126
15 files changed, 775 insertions, 0 deletions
diff --git a/test/modules/http1/__init__.py b/test/modules/http1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/modules/http1/__init__.py
diff --git a/test/modules/http1/conftest.py b/test/modules/http1/conftest.py
new file mode 100644
index 0000000..33a16a1
--- /dev/null
+++ b/test/modules/http1/conftest.py
@@ -0,0 +1,36 @@
+import logging
+import os
+
+import pytest
+import sys
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from .env import H1TestEnv
+
+
+def pytest_report_header(config, startdir):
+ env = H1TestEnv()
+ return f"mod_http [apache: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]"
+
+
+def pytest_generate_tests(metafunc):
+ if "repeat" in metafunc.fixturenames:
+ count = int(metafunc.config.getoption("repeat"))
+ metafunc.fixturenames.append('tmp_ct')
+ metafunc.parametrize('repeat', range(count))
+
+
+@pytest.fixture(scope="package")
+def env(pytestconfig) -> H1TestEnv:
+ 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 = H1TestEnv(pytestconfig=pytestconfig)
+ env.setup_httpd()
+ env.apache_access_log_clear()
+ env.httpd_error_log.clear_log()
+ return env
diff --git a/test/modules/http1/env.py b/test/modules/http1/env.py
new file mode 100644
index 0000000..e2df1a5
--- /dev/null
+++ b/test/modules/http1/env.py
@@ -0,0 +1,81 @@
+import inspect
+import logging
+import os
+import subprocess
+from typing import Dict, Any
+
+from pyhttpd.certs import CertificateSpec
+from pyhttpd.conf import HttpdConf
+from pyhttpd.env import HttpdTestEnv, HttpdTestSetup
+
+log = logging.getLogger(__name__)
+
+
+class H1TestSetup(HttpdTestSetup):
+
+ def __init__(self, env: 'HttpdTestEnv'):
+ super().__init__(env=env)
+ self.add_source_dir(os.path.dirname(inspect.getfile(H1TestSetup)))
+ self.add_modules(["cgid", "autoindex", "ssl"])
+
+ def make(self):
+ super().make()
+ self._add_h1test()
+ self._setup_data_1k_1m()
+
+ def _add_h1test(self):
+ local_dir = os.path.dirname(inspect.getfile(H1TestSetup))
+ p = subprocess.run([self.env.apxs, '-c', 'mod_h1test.c'],
+ capture_output=True,
+ cwd=os.path.join(local_dir, 'mod_h1test'))
+ rv = p.returncode
+ if rv != 0:
+ log.error(f"compiling md_h1test failed: {p.stderr}")
+ raise Exception(f"compiling md_h1test failed: {p.stderr}")
+
+ modules_conf = os.path.join(self.env.server_dir, 'conf/modules.conf')
+ with open(modules_conf, 'a') as fd:
+ # load our test module which is not installed
+ fd.write(f"LoadModule h1test_module \"{local_dir}/mod_h1test/.libs/mod_h1test.so\"\n")
+
+ def _setup_data_1k_1m(self):
+ s90 = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678\n"
+ with open(os.path.join(self.env.gen_dir, "data-1k"), 'w') as f:
+ for i in range(10):
+ f.write(f"{i:09d}-{s90}")
+ with open(os.path.join(self.env.gen_dir, "data-10k"), 'w') as f:
+ for i in range(100):
+ f.write(f"{i:09d}-{s90}")
+ with open(os.path.join(self.env.gen_dir, "data-100k"), 'w') as f:
+ for i in range(1000):
+ f.write(f"{i:09d}-{s90}")
+ with open(os.path.join(self.env.gen_dir, "data-1m"), 'w') as f:
+ for i in range(10000):
+ f.write(f"{i:09d}-{s90}")
+
+
+class H1TestEnv(HttpdTestEnv):
+
+ def __init__(self, pytestconfig=None):
+ super().__init__(pytestconfig=pytestconfig)
+ self.add_httpd_log_modules(["http", "core"])
+
+ def setup_httpd(self, setup: HttpdTestSetup = None):
+ super().setup_httpd(setup=H1TestSetup(env=self))
+
+
+class H1Conf(HttpdConf):
+
+ def __init__(self, env: HttpdTestEnv, extras: Dict[str, Any] = None):
+ super().__init__(env=env, extras=HttpdConf.merge_extras(extras, {
+ "base": [
+ "LogLevel http:trace4",
+ ],
+ f"cgi.{env.http_tld}": [
+ "SSLOptions +StdEnvVars",
+ "AddHandler cgi-script .py",
+ "<Location \"/h1test/echo\">",
+ " SetHandler h1test-echo",
+ "</Location>",
+ ]
+ }))
diff --git a/test/modules/http1/htdocs/cgi/files/empty.txt b/test/modules/http1/htdocs/cgi/files/empty.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/modules/http1/htdocs/cgi/files/empty.txt
diff --git a/test/modules/http1/htdocs/cgi/hello.py b/test/modules/http1/htdocs/cgi/hello.py
new file mode 100755
index 0000000..191acb2
--- /dev/null
+++ b/test/modules/http1/htdocs/cgi/hello.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+
+import os
+
+print("Content-Type: application/json")
+print()
+print("{")
+print(" \"https\" : \"%s\"," % (os.getenv('HTTPS', '')))
+print(" \"host\" : \"%s\"," % (os.getenv('SERVER_NAME', '')))
+print(" \"protocol\" : \"%s\"," % (os.getenv('SERVER_PROTOCOL', '')))
+print(" \"ssl_protocol\" : \"%s\"," % (os.getenv('SSL_PROTOCOL', '')))
+print(" \"h2\" : \"%s\"," % (os.getenv('HTTP2', '')))
+print(" \"h2push\" : \"%s\"" % (os.getenv('H2PUSH', '')))
+print("}")
+
diff --git a/test/modules/http1/htdocs/cgi/requestparser.py b/test/modules/http1/htdocs/cgi/requestparser.py
new file mode 100644
index 0000000..c7e0648
--- /dev/null
+++ b/test/modules/http1/htdocs/cgi/requestparser.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+import os
+import sys
+from urllib import parse
+import multipart # https://github.com/andrew-d/python-multipart (`apt install python3-multipart`)
+import shutil
+
+
+try: # Windows needs stdio set for binary mode.
+ import msvcrt
+
+ msvcrt.setmode(0, os.O_BINARY) # stdin = 0
+ msvcrt.setmode(1, os.O_BINARY) # stdout = 1
+except ImportError:
+ pass
+
+
+class FileItem:
+
+ def __init__(self, mparse_item):
+ self.item = mparse_item
+
+ @property
+ def file_name(self):
+ return os.path.basename(self.item.file_name.decode())
+
+ def save_to(self, destpath: str):
+ fsrc = self.item.file_object
+ fsrc.seek(0)
+ with open(destpath, 'wb') as fd:
+ shutil.copyfileobj(fsrc, fd)
+
+
+def get_request_params():
+ oforms = {}
+ ofiles = {}
+ if "REQUEST_URI" in os.environ:
+ qforms = parse.parse_qs(parse.urlsplit(os.environ["REQUEST_URI"]).query)
+ for name, values in qforms.items():
+ oforms[name] = values[0]
+ if "CONTENT_TYPE" in os.environ:
+ ctype = os.environ["CONTENT_TYPE"]
+ if ctype == "application/x-www-form-urlencoded":
+ s = sys.stdin.read()
+ qforms = parse.parse_qs(s)
+ for name, values in qforms.items():
+ oforms[name] = values[0]
+ elif ctype.startswith("multipart/"):
+ def on_field(field):
+ oforms[field.field_name.decode()] = field.value.decode()
+ def on_file(file):
+ ofiles[file.field_name.decode()] = FileItem(file)
+ multipart.parse_form(headers={"Content-Type": ctype},
+ input_stream=sys.stdin.buffer,
+ on_field=on_field, on_file=on_file)
+ return oforms, ofiles
+
diff --git a/test/modules/http1/htdocs/cgi/upload.py b/test/modules/http1/htdocs/cgi/upload.py
new file mode 100755
index 0000000..632b7e9
--- /dev/null
+++ b/test/modules/http1/htdocs/cgi/upload.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+import os
+import sys
+from requestparser import get_request_params
+
+
+forms, files = get_request_params()
+
+status = '200 Ok'
+
+# Test if the file was uploaded
+if 'file' in files:
+ fitem = files['file']
+ # strip leading path from file name to avoid directory traversal attacks
+ fname = fitem.file_name
+ fpath = f'{os.environ["DOCUMENT_ROOT"]}/files/{fname}'
+ fitem.save_to(fpath)
+ message = "The file %s was uploaded successfully" % (fname)
+ print("Status: 201 Created")
+ print("Content-Type: text/html")
+ print("Location: %s://%s/files/%s" % (os.environ["REQUEST_SCHEME"], os.environ["HTTP_HOST"], fname))
+ print("")
+ print("<html><body><p>%s</p></body></html>" % (message))
+
+elif 'remove' in forms:
+ remove = forms['remove']
+ try:
+ fname = os.path.basename(remove)
+ os.remove('./files/' + fname)
+ message = 'The file "' + fname + '" was removed successfully'
+ except OSError as e:
+ message = 'Error removing ' + fname + ': ' + e.strerror
+ status = '404 File Not Found'
+ print("Status: %s" % (status))
+ print("""
+Content-Type: text/html
+
+<html><body>
+<p>%s</p>
+</body></html>""" % (message))
+
+else:
+ message = '''\
+ Upload File<form method="POST" enctype="multipart/form-data">
+ <input type="file" name="file">
+ <button type="submit">Upload</button></form>
+ '''
+ print("Status: %s" % (status))
+ print("""\
+Content-Type: text/html
+
+<html><body>
+<p>%s</p>
+</body></html>""" % (message))
+
diff --git a/test/modules/http1/mod_h1test/mod_h1test.c b/test/modules/http1/mod_h1test/mod_h1test.c
new file mode 100644
index 0000000..cbd87b5
--- /dev/null
+++ b/test/modules/http1/mod_h1test/mod_h1test.c
@@ -0,0 +1,129 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <apr_optional.h>
+#include <apr_optional_hooks.h>
+#include <apr_strings.h>
+#include <apr_cstr.h>
+#include <apr_time.h>
+#include <apr_want.h>
+
+#include <httpd.h>
+#include <http_protocol.h>
+#include <http_request.h>
+#include <http_log.h>
+
+static void h1test_hooks(apr_pool_t *pool);
+
+AP_DECLARE_MODULE(h1test) = {
+ STANDARD20_MODULE_STUFF,
+ NULL, /* func to create per dir config */
+ NULL, /* func to merge per dir config */
+ NULL, /* func to create per server config */
+ NULL, /* func to merge per server config */
+ NULL, /* command handlers */
+ h1test_hooks,
+#if defined(AP_MODULE_FLAG_NONE)
+ AP_MODULE_FLAG_ALWAYS_MERGE
+#endif
+};
+
+
+static int h1test_echo_handler(request_rec *r)
+{
+ conn_rec *c = r->connection;
+ apr_bucket_brigade *bb;
+ apr_bucket *b;
+ apr_status_t rv;
+ char buffer[8192];
+ const char *ct;
+ long l;
+
+ if (strcmp(r->handler, "h1test-echo")) {
+ return DECLINED;
+ }
+ if (r->method_number != M_GET && r->method_number != M_POST) {
+ return DECLINED;
+ }
+
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "echo_handler: processing request");
+ r->status = 200;
+ r->clength = -1;
+ r->chunked = 1;
+ ct = apr_table_get(r->headers_in, "content-type");
+ ap_set_content_type(r, ct? ct : "application/octet-stream");
+
+ bb = apr_brigade_create(r->pool, c->bucket_alloc);
+ /* copy any request body into the response */
+ if ((rv = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK))) goto cleanup;
+ if (ap_should_client_block(r)) {
+ while (0 < (l = ap_get_client_block(r, &buffer[0], sizeof(buffer)))) {
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
+ "echo_handler: copying %ld bytes from request body", l);
+ rv = apr_brigade_write(bb, NULL, NULL, buffer, l);
+ if (APR_SUCCESS != rv) goto cleanup;
+ rv = ap_pass_brigade(r->output_filters, bb);
+ if (APR_SUCCESS != rv) goto cleanup;
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
+ "echo_handler: passed %ld bytes from request body", l);
+ }
+ }
+ /* we are done */
+ b = apr_bucket_eos_create(c->bucket_alloc);
+ APR_BRIGADE_INSERT_TAIL(bb, b);
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "echo_handler: request read");
+
+ if (r->trailers_in && !apr_is_empty_table(r->trailers_in)) {
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
+ "echo_handler: seeing incoming trailers");
+ apr_table_setn(r->trailers_out, "h1test-trailers-in",
+ apr_itoa(r->pool, 1));
+ }
+ if (apr_table_get(r->headers_in, "Add-Trailer")) {
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
+ "echo_handler: seeing incoming Add-Trailer header");
+ apr_table_setn(r->trailers_out, "h1test-add-trailer",
+ apr_table_get(r->headers_in, "Add-Trailer"));
+ }
+
+ rv = ap_pass_brigade(r->output_filters, bb);
+
+cleanup:
+ if (rv == APR_SUCCESS
+ || r->status != HTTP_OK
+ || c->aborted) {
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "echo_handler: request handled");
+ return OK;
+ }
+ else {
+ /* no way to know what type of error occurred */
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "h1test_echo_handler failed");
+ return AP_FILTER_ERROR;
+ }
+ return DECLINED;
+}
+
+
+/* Install this module into the apache2 infrastructure.
+ */
+static void h1test_hooks(apr_pool_t *pool)
+{
+ ap_log_perror(APLOG_MARK, APLOG_TRACE1, 0, pool, "installing hooks and handlers");
+
+ /* test h1 handlers */
+ ap_hook_handler(h1test_echo_handler, NULL, NULL, APR_HOOK_MIDDLE);
+}
+
diff --git a/test/modules/http1/mod_h1test/mod_h1test.slo b/test/modules/http1/mod_h1test/mod_h1test.slo
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/modules/http1/mod_h1test/mod_h1test.slo
diff --git a/test/modules/http1/test_001_alive.py b/test/modules/http1/test_001_alive.py
new file mode 100644
index 0000000..0a1de1d
--- /dev/null
+++ b/test/modules/http1/test_001_alive.py
@@ -0,0 +1,20 @@
+import pytest
+
+from .env import H1Conf
+
+
+class TestBasicAlive:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ H1Conf(env).add_vhost_test1().install()
+ assert env.apache_restart() == 0
+
+ # we expect to see the document from the generic server
+ def test_h1_001_01(self, env):
+ url = env.mkurl("https", "test1", "/alive.json")
+ r = env.curl_get(url, 5)
+ assert r.exit_code == 0, r.stderr + r.stdout
+ assert r.response["json"]
+ assert r.response["json"]["alive"] is True
+ assert r.response["json"]["host"] == "test1"
diff --git a/test/modules/http1/test_003_get.py b/test/modules/http1/test_003_get.py
new file mode 100644
index 0000000..1cd5917
--- /dev/null
+++ b/test/modules/http1/test_003_get.py
@@ -0,0 +1,27 @@
+import socket
+
+import pytest
+
+from .env import H1Conf
+
+
+class TestGet:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ H1Conf(env).add_vhost_cgi(
+ proxy_self=True
+ ).add_vhost_test1(
+ proxy_self=True
+ ).install()
+ assert env.apache_restart() == 0
+
+ # check SSL environment variables from CGI script
+ def test_h1_003_01(self, env):
+ url = env.mkurl("https", "cgi", "/hello.py")
+ r = env.curl_get(url)
+ assert r.response["status"] == 200
+ assert r.response["json"]["protocol"] == "HTTP/1.1"
+ assert r.response["json"]["https"] == "on"
+ tls_version = r.response["json"]["ssl_protocol"]
+ assert tls_version in ["TLSv1.2", "TLSv1.3"]
diff --git a/test/modules/http1/test_004_post.py b/test/modules/http1/test_004_post.py
new file mode 100644
index 0000000..005a8c2
--- /dev/null
+++ b/test/modules/http1/test_004_post.py
@@ -0,0 +1,53 @@
+import difflib
+import email.parser
+import inspect
+import json
+import os
+import sys
+
+import pytest
+
+from .env import H1Conf
+
+
+class TestPost:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ TestPost._local_dir = os.path.dirname(inspect.getfile(TestPost))
+ H1Conf(env).add_vhost_cgi().install()
+ assert env.apache_restart() == 0
+
+ def local_src(self, fname):
+ return os.path.join(TestPost._local_dir, fname)
+
+ # upload and GET again using curl, compare to original content
+ def curl_upload_and_verify(self, env, fname, options=None):
+ url = env.mkurl("https", "cgi", "/upload.py")
+ fpath = os.path.join(env.gen_dir, fname)
+ r = env.curl_upload(url, fpath, options=options)
+ assert r.exit_code == 0, f"{r}"
+ assert 200 <= r.response["status"] < 300
+
+ r2 = env.curl_get(r.response["header"]["location"])
+ assert r2.exit_code == 0
+ assert r2.response["status"] == 200
+ with open(self.local_src(fpath), mode='rb') as file:
+ src = file.read()
+ assert src == r2.response["body"]
+ return r
+
+ def test_h1_004_01(self, env):
+ self.curl_upload_and_verify(env, "data-1k", ["-vvv"])
+
+ def test_h1_004_02(self, env):
+ self.curl_upload_and_verify(env, "data-10k", [])
+
+ def test_h1_004_03(self, env):
+ self.curl_upload_and_verify(env, "data-100k", [])
+
+ def test_h1_004_04(self, env):
+ self.curl_upload_and_verify(env, "data-1m", [])
+
+ def test_h1_004_05(self, env):
+ r = self.curl_upload_and_verify(env, "data-1k", ["-vvv", "-H", "Expect: 100-continue"])
diff --git a/test/modules/http1/test_005_trailers.py b/test/modules/http1/test_005_trailers.py
new file mode 100644
index 0000000..ca717a0
--- /dev/null
+++ b/test/modules/http1/test_005_trailers.py
@@ -0,0 +1,42 @@
+import os
+import pytest
+
+from .env import H1Conf
+
+
+# The trailer tests depend on "nghttp" as no other client seems to be able to send those
+# rare things.
+class TestTrailers:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ H1Conf(env).add_vhost_cgi(proxy_self=True).install()
+ assert env.apache_restart() == 0
+
+ # check that we get a trailer out when telling the handler to add one
+ def test_h1_005_01(self, env):
+ if not env.httpd_is_at_least("2.5.0"):
+ pytest.skip(f'need at least httpd 2.5.0 for this')
+ url = env.mkurl("https", "cgi", "/h1test/echo")
+ host = f"cgi.{env.http_tld}"
+ fpath = os.path.join(env.gen_dir, "data-1k")
+ r = env.curl_upload(url, fpath, options=["--header", "Add-Trailer: 005_01"])
+ assert r.exit_code == 0, f"{r}"
+ assert 200 <= r.response["status"] < 300
+ assert r.response["trailer"], f"no trailers received: {r}"
+ assert "h1test-add-trailer" in r.response["trailer"]
+ assert r.response["trailer"]["h1test-add-trailer"] == "005_01"
+
+ # check that we get out trailers through the proxy
+ def test_h1_005_02(self, env):
+ if not env.httpd_is_at_least("2.5.0"):
+ pytest.skip(f'need at least httpd 2.5.0 for this')
+ url = env.mkurl("https", "cgi", "/proxy/h1test/echo")
+ host = f"cgi.{env.http_tld}"
+ fpath = os.path.join(env.gen_dir, "data-1k")
+ r = env.curl_upload(url, fpath, options=["--header", "Add-Trailer: 005_01"])
+ assert r.exit_code == 0, f"{r}"
+ assert 200 <= r.response["status"] < 300
+ assert r.response["trailer"], f"no trailers received: {r}"
+ assert "h1test-add-trailer" in r.response["trailer"]
+ assert r.response["trailer"]["h1test-add-trailer"] == "005_01"
diff --git a/test/modules/http1/test_006_unsafe.py b/test/modules/http1/test_006_unsafe.py
new file mode 100644
index 0000000..eb83217
--- /dev/null
+++ b/test/modules/http1/test_006_unsafe.py
@@ -0,0 +1,134 @@
+import re
+import socket
+from typing import List, Optional
+
+import pytest
+
+from .env import H1Conf
+
+class TestRequestUnsafe:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = H1Conf(env)
+ conf.add([
+ "HttpProtocolOptions Unsafe",
+ ])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ # unsafe tests from t/apache/http_strict.t
+ # possible expected results:
+ # 0: any HTTP error
+ # 1: any HTTP success
+ # 200-500: specific HTTP status code
+ # None: HTTPD should drop connection without error message
+ @pytest.mark.parametrize(["intext", "status", "lognos"], [
+ ["GET / HTTP/1.0\r\n\r\n", 1, None],
+ ["GET / HTTP/1.0\n\n", 1, None],
+ ["get / HTTP/1.0\r\n\r\n", 501, ["AH00135"]],
+ ["G ET / HTTP/1.0\r\n\r\n", 400, None],
+ ["G\0ET / HTTP/1.0\r\n\r\n", 400, None],
+ ["G/T / HTTP/1.0\r\n\r\n", 501, ["AH00135"]],
+ ["GET /\0 HTTP/1.0\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\0\r\n\r\n", 400, None],
+ ["GET\f/ HTTP/1.0\r\n\r\n", 400, None],
+ ["GET\r/ HTTP/1.0\r\n\r\n", 400, None],
+ ["GET\t/ HTTP/1.0\r\n\r\n", 400, None],
+ ["GET / HTT/1.0\r\n\r\n", 0, None],
+ ["GET / HTTP/1.0\r\nHost: localhost\r\n\r\n", 1, None],
+ ["GET / HTTP/2.0\r\nHost: localhost\r\n\r\n", 1, None],
+ ["GET / HTTP/1.2\r\nHost: localhost\r\n\r\n", 1, None],
+ ["GET / HTTP/1.11\r\nHost: localhost\r\n\r\n", 400, None],
+ ["GET / HTTP/10.0\r\nHost: localhost\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0 \r\nHost: localhost\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0 x\r\nHost: localhost\r\n\r\n", 400, None],
+ ["GET / HTTP/\r\nHost: localhost\r\n\r\n", 0, None],
+ ["GET / HTTP/0.9\r\n\r\n", 0, None],
+ ["GET / HTTP/0.8\r\n\r\n", 0, None],
+ ["GET /\x01 HTTP/1.0\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nFoo: bar\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nFoo:bar\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nFoo: b\0ar\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nFoo: b\x01ar\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nFoo\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nFoo bar\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\n: bar\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nX: bar\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nFoo bar:bash\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nFoo :bar\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\n Foo:bar\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nF\x01o: bar\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nF\ro: bar\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nF\to: bar\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nFo: b\tar\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nFo: bar\r\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\r", None, None],
+ ["GET /\r\n", 0, None],
+ ["GET /#frag HTTP/1.0\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nHost: localhost\r\nHost: localhost\r\n\r\n", 200, None],
+ ["GET http://017700000001/ HTTP/1.0\r\n\r\n", 200, None],
+ ["GET http://0x7f.1/ HTTP/1.0\r\n\r\n", 200, None],
+ ["GET http://127.0.0.1/ HTTP/1.0\r\n\r\n", 200, None],
+ ["GET http://127.01.0.1/ HTTP/1.0\r\n\r\n", 200, None],
+ ["GET http://%3127.0.0.1/ HTTP/1.0\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nHost: localhost:80\r\nHost: localhost:80\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nHost: localhost:80 x\r\n\r", 400, None],
+ ["GET http://localhost:80/ HTTP/1.0\r\n\r\n", 200, None],
+ ["GET http://localhost:80x/ HTTP/1.0\r\n\r\n", 400, None],
+ ["GET http://localhost:80:80/ HTTP/1.0\r\n\r\n", 400, None],
+ ["GET http://localhost::80/ HTTP/1.0\r\n\r\n", 400, None],
+ ["GET http://foo@localhost:80/ HTTP/1.0\r\n\r\n", 200, None],
+ ["GET http://[::1]/ HTTP/1.0\r\n\r\n", 1, None],
+ ["GET http://[::1:2]/ HTTP/1.0\r\n\r\n", 1, None],
+ ["GET http://[4712::abcd]/ HTTP/1.0\r\n\r\n", 1, None],
+ ["GET http://[4712::abcd:1]/ HTTP/1.0\r\n\r\n", 1, None],
+ ["GET http://[4712::abcd::]/ HTTP/1.0\r\n\r\n", 400, None],
+ ["GET http://[4712:abcd::,]/ HTTP/1.0\r\n\r\n", 1, None],
+ ["GET http://[4712::abcd]:8000/ HTTP/1.0\r\n\r\n", 1, None],
+ ["GET http://4713::abcd:8001/ HTTP/1.0\r\n\r\n", 400, None],
+ ["GET / HTTP/1.0\r\nHost: [::1]\r\n\r\n", 1, None],
+ ["GET / HTTP/1.0\r\nHost: [::1:2]\r\n\r\n", 1, None],
+ ["GET / HTTP/1.0\r\nHost: [4711::abcd]\r\n\r\n", 1, None],
+ ["GET / HTTP/1.0\r\nHost: [4711::abcd:1]\r\n\r\n", 1, None],
+ ["GET / HTTP/1.0\r\nHost: [4711:abcd::]\r\n\r\n", 1, None],
+ ["GET / HTTP/1.0\r\nHost: [4711::abcd]:8000\r\n\r\n", 1, None],
+ ["GET / HTTP/1.0\r\nHost: 4714::abcd:8001\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nHost: abc\xa0\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nHost: abc\\foo\r\n\r\n", 400, None],
+ ["GET http://foo/ HTTP/1.0\r\nHost: bar\r\n\r\n", 200, None],
+ ["GET http://foo:81/ HTTP/1.0\r\nHost: bar\r\n\r\n", 200, None],
+ ["GET http://[::1]:81/ HTTP/1.0\r\nHost: bar\r\n\r\n", 200, None],
+ ["GET http://10.0.0.1:81/ HTTP/1.0\r\nHost: bar\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nHost: foo-bar.example.com\r\n\r\n", 200, None],
+ ["GET / HTTP/1.0\r\nHost: foo_bar.example.com\r\n\r\n", 200, None],
+ ["GET http://foo_bar/ HTTP/1.0\r\n\r\n", 200, None],
+ ])
+ def test_h1_006_01(self, env, intext, status: Optional[int], lognos: Optional[List[str]]):
+ with socket.create_connection(('localhost', int(env.http_port))) as sock:
+ # on some OS, the server does not see our connection until there is
+ # something incoming
+ sock.sendall(intext.encode())
+ sock.shutdown(socket.SHUT_WR)
+ buff = sock.recv(1024)
+ msg = buff.decode()
+ if status is None:
+ assert len(msg) == 0, f"unexpected answer: {msg}"
+ else:
+ assert len(msg) > 0, "no answer from server"
+ rlines = msg.splitlines()
+ response = rlines[0]
+ m = re.match(r'^HTTP/1.1 (\d+)\s+(\S+)', response)
+ assert m or status == 0, f"unrecognized response: {rlines}"
+ if status == 1:
+ assert int(m.group(1)) >= 200
+ elif status == 0:
+ # headerless 0.9 response, yuk
+ assert len(rlines) >= 1, f"{rlines}"
+ elif status > 0:
+ assert int(m.group(1)) == status, f"{rlines}"
+ else:
+ assert int(m.group(1)) >= 400, f"{rlines}"
+ #
+ if lognos is not None:
+ env.httpd_error_log.ignore_recent(lognos = lognos)
diff --git a/test/modules/http1/test_007_strict.py b/test/modules/http1/test_007_strict.py
new file mode 100644
index 0000000..7c52f68
--- /dev/null
+++ b/test/modules/http1/test_007_strict.py
@@ -0,0 +1,126 @@
+import re
+import socket
+from typing import List, Optional
+
+import pytest
+
+from .env import H1Conf
+
+
+class TestRequestStrict:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = H1Conf(env)
+ conf.add([
+ "HttpProtocolOptions Strict",
+ ])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ # strict tests from t/apache/http_strict.t
+ # possible expected results:
+ # 0: any HTTP error
+ # 1: any HTTP success
+ # 200-500: specific HTTP status code
+ # undef: HTTPD should drop connection without error message
+ @pytest.mark.parametrize(["intext", "status"], [
+ ["GET / HTTP/1.0\n\n", 400],
+ ["G/T / HTTP/1.0\r\n\r\n", 400],
+ ["GET / HTTP/1.0 \r\nHost: localhost\r\n\r\n", 400],
+ ["GET / HTTP/1.0\r\nFoo: b\x01ar\r\n\r\n", 400],
+ ["GET / HTTP/1.0\r\nF\x01o: bar\r\n\r\n", 400],
+ ["GET / HTTP/1.0\r\r", None],
+ ["GET / HTTP/1.0\r\nHost: localhost\r\nHost: localhost\r\n\r\n", 400],
+ ["GET http://017700000001/ HTTP/1.0\r\n\r\n", 400],
+ ["GET http://0x7f.1/ HTTP/1.0\r\n\r\n", 400],
+ ["GET http://127.01.0.1/ HTTP/1.0\r\n\r\n", 400],
+ ["GET http://%3127.0.0.1/ HTTP/1.0\r\n\r\n", 400],
+ ["GET / HTTP/1.0\r\nHost: localhost:80\r\nHost: localhost:80\r\n\r\n", 400],
+ ["GET http://foo@localhost:80/ HTTP/1.0\r\n\r\n", 400],
+ ["GET / HTTP/1.0\r\nHost: 4714::abcd:8001\r\n\r\n", 400],
+ ["GET / HTTP/1.0\r\nHost: abc\xa0\r\n\r\n", 400],
+ ["GET / HTTP/1.0\r\nHost: foo_bar.example.com\r\n\r\n", 200],
+ ["GET http://foo_bar/ HTTP/1.0\r\n\r\n", 200],
+ ])
+ def test_h1_007_01(self, env, intext, status: Optional[int]):
+ with socket.create_connection(('localhost', int(env.http_port))) as sock:
+ # on some OS, the server does not see our connection until there is
+ # something incoming
+ sock.sendall(intext.encode())
+ sock.shutdown(socket.SHUT_WR)
+ buff = sock.recv(1024)
+ msg = buff.decode()
+ if status is None:
+ assert len(msg) == 0, f"unexpected answer: {msg}"
+ else:
+ assert len(msg) > 0, "no answer from server"
+ rlines = msg.splitlines()
+ response = rlines[0]
+ m = re.match(r'^HTTP/1.1 (\d+)\s+(\S+)', response)
+ assert m, f"unrecognized response: {rlines}"
+ if status == 1:
+ assert int(m.group(1)) >= 200
+ elif status == 90:
+ assert len(rlines) >= 1, f"{rlines}"
+ elif status > 0:
+ assert int(m.group(1)) == status, f"{rlines}"
+ else:
+ assert int(m.group(1)) >= 400, f"{rlines}"
+
+ @pytest.mark.parametrize(["hvalue", "expvalue", "status", "lognos"], [
+ ['"123"', '123', 200, None],
+ ['"123 "', '123 ', 200, None], # trailing space stays
+ ['"123\t"', '123\t', 200, None], # trailing tab stays
+ ['" 123"', '123', 200, None], # leading space is stripped
+ ['" 123"', '123', 200, None], # leading spaces are stripped
+ ['"\t123"', '123', 200, None], # leading tab is stripped
+ ['"expr=%{unescape:123%0A 123}"', '', 500, ["AH02430"]], # illegal char
+ ['" \t "', '', 200, None], # just ws
+ ])
+ def test_h1_007_02(self, env, hvalue, expvalue, status, lognos: Optional[List[str]]):
+ hname = 'ap-test-007'
+ conf = H1Conf(env, extras={
+ f'test1.{env.http_tld}': [
+ '<Location /index.html>',
+ f'Header add {hname} {hvalue}',
+ '</Location>',
+ ]
+ })
+ conf.add_vhost_test1(proxy_self=True)
+ conf.install()
+ assert env.apache_restart() == 0
+ url = env.mkurl("https", "test1", "/index.html")
+ r = env.curl_get(url, options=['--http1.1'])
+ assert r.response["status"] == status
+ if int(status) < 400:
+ assert r.response["header"][hname] == expvalue
+ #
+ if lognos is not None:
+ env.httpd_error_log.ignore_recent(lognos = lognos)
+
+ @pytest.mark.parametrize(["hvalue", "expvalue"], [
+ ['123', '123'],
+ ['123 ', '123'], # trailing space is stripped
+ ['123\t', '123'], # trailing tab is stripped
+ [' 123', '123'], # leading space is stripped
+ [' 123', '123'], # leading spaces are stripped
+ ['\t123', '123'], # leading tab is stripped
+ ])
+ def test_h1_007_03(self, env, hvalue, expvalue):
+ # same as 007_02, but http1 proxied
+ hname = 'ap-test-007'
+ conf = H1Conf(env, extras={
+ f'test1.{env.http_tld}': [
+ '<Location /index.html>',
+ f'Header add {hname} "{hvalue}"',
+ '</Location>',
+ ]
+ })
+ conf.add_vhost_test1(proxy_self=True)
+ conf.install()
+ assert env.apache_restart() == 0
+ url = env.mkurl("https", "test1", "/proxy/index.html")
+ r = env.curl_get(url, options=['--http1.1'])
+ assert r.response["status"] == 200
+ assert r.response["header"][hname] == expvalue