diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-05 10:00:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-05 10:00:10 +0000 |
commit | 3204e211a1e248154ff95b90b6a7e29cfa92069c (patch) | |
tree | 79f901498145b63bf34e9981a013f3d9b52eafc2 /test/modules/http1 | |
parent | Adding upstream version 2.4.61. (diff) | |
download | apache2-upstream.tar.xz apache2-upstream.zip |
Adding upstream version 2.4.62.upstream/2.4.62upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/modules/http1')
-rw-r--r-- | test/modules/http1/__init__.py | 0 | ||||
-rw-r--r-- | test/modules/http1/conftest.py | 36 | ||||
-rw-r--r-- | test/modules/http1/env.py | 81 | ||||
-rw-r--r-- | test/modules/http1/htdocs/cgi/files/empty.txt | 0 | ||||
-rwxr-xr-x | test/modules/http1/htdocs/cgi/hello.py | 15 | ||||
-rw-r--r-- | test/modules/http1/htdocs/cgi/requestparser.py | 57 | ||||
-rwxr-xr-x | test/modules/http1/htdocs/cgi/upload.py | 55 | ||||
-rw-r--r-- | test/modules/http1/mod_h1test/mod_h1test.c | 129 | ||||
-rw-r--r-- | test/modules/http1/mod_h1test/mod_h1test.slo | 0 | ||||
-rw-r--r-- | test/modules/http1/test_001_alive.py | 20 | ||||
-rw-r--r-- | test/modules/http1/test_003_get.py | 27 | ||||
-rw-r--r-- | test/modules/http1/test_004_post.py | 53 | ||||
-rw-r--r-- | test/modules/http1/test_005_trailers.py | 42 | ||||
-rw-r--r-- | test/modules/http1/test_006_unsafe.py | 134 | ||||
-rw-r--r-- | test/modules/http1/test_007_strict.py | 126 |
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 |