Diff options
Diffstat (limited to 'tests/serve-handler-check.py')
| -rw-r--r-- | tests/serve-handler-check.py | 179 |
1 file changed, 179 insertions, 0 deletions
diff --git a/tests/serve-handler-check.py b/tests/serve-handler-check.py new file mode 100644 index 0000000..cc45b45 --- /dev/null +++ b/tests/serve-handler-check.py @@ -0,0 +1,179 @@ +"""Check how tools/serve.py hands a request to the CGI and the answer back. + +The server is started on a free loopback port with a shell script in place of +cgit, which writes back the CGI environment it was given, or fails or stalls +when the query asks it to. Each case sends one request and checks one part of +the way through. Run with the repository root, a scratch directory and the +name of one case. +""" + +import http.client +import importlib.util +import socket +import sys +import threading +from pathlib import Path + +# Loading serve.py would otherwise leave a __pycache__ directory behind in +# tools/, compiled bytecode the repository has no use for. +sys.dont_write_bytecode = True + +STUB = """#!/bin/sh +case "$QUERY_STRING" in +fail) exit 3 ;; +stall) exec sleep 5 ;; +esac +printf 'Status: 200 OK\\nContent-Type: text/plain\\nX-Stub: yes\\n\\n' +printf 'method=%s\\n' "$REQUEST_METHOD" +printf 'script=[%s]\\n' "$SCRIPT_NAME" +printf 'path=%s\\n' "$PATH_INFO" +printf 'query=%s\\n' "$QUERY_STRING" +printf 'cookie=%s\\n' "$HTTP_COOKIE" +printf 'length=%s\\n' "$CONTENT_LENGTH" +printf 'config=%s\\n' "$CGIT_CONFIG" +printf 'body=' +cat +""" + + +def load_serve(root): + path = root + "/tools/serve.py" + spec = importlib.util.spec_from_file_location("serve", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def environment(request, scratch, port): + status, headers, body = request( + "GET", "/foo%2bbar/log/?h=main", headers={"Cookie": "a=b"} + ) + lines = body.decode().splitlines() + return ( + status == 200 + and headers.get("X-Stub") == "yes" + and "method=GET" in lines + and "script=[]" in lines + and "path=/foo+bar/log/" in lines + and "query=h=main" in lines + and "cookie=a=b" in lines + and "config=%s" % (scratch / "cgitrc") in lines + ) + + +def post_body(request, scratch, port): + status, _, body = request("POST", "/p/?login", body=b"user=me") + lines = body.decode().splitlines() + return status == 200 and "length=7" in lines and "body=user=me" in lines + + +def asset(request, scratch, port): + status, headers, body = request("GET", "/cgit.css") + return ( + status == 200 + and headers.get("Content-Type") == "text/css" + and body == b"div#cgit {}\n" + ) + + +def repository_file(request, scratch, port): + status, headers, body = request("GET", "/foo/tree/cgit.css") + return status == 200 and b"path=/foo/tree/cgit.css" in body + + +def asset_link_outside(request, scratch, port): + status, headers, body = request("GET", "/outside.css") + return status == 200 and b"path=/outside.css" in body + + +def failing_cgi(request, scratch, port): + status, _, _ = request("GET", "/?fail") + return status == 500 + + +def stalled_cgi(request, scratch, port): + status, _, _ = request("GET", "/?stall") + return status == 504 + + +def oversized_body(request, scratch, port): + status, _, _ = request("POST", "/", body=b"x" * 17) + return status == 413 + + +# A client drops whatever follows the headers of an answer to HEAD, so this +# one reads the raw answer, which the server ends by closing the connection. +def head(request, scratch, port): + with socket.create_connection(("127.0.0.1", port), timeout=30) as raw: + raw.sendall(b"HEAD /foo/ HTTP/1.0\r\n\r\n") + answer = b"" + while chunk := raw.recv(4096): + answer += chunk + headers, _, body = answer.partition(b"\r\n\r\n") + return ( + headers.startswith(b"HTTP/1.0 200") + and b"X-Stub: yes" in headers + and b"Content-Length: 0" not in headers + and body == b"" + ) + + +CASES = { + "environment": environment, + "post-body": post_body, + "asset": asset, + "repository-file": repository_file, + "asset-link-outside": asset_link_outside, + "failing-cgi": failing_cgi, + "stalled-cgi": stalled_cgi, + "oversized-body": oversized_body, + "head": head, +} + + +def main(): + root, scratch, name = sys.argv[1:] + check = CASES[name] + serve = load_serve(root) + # Small bounds, so the timeout and the body cap are reached quickly, and + # the type main() pins for the stylesheet. + serve.CGI_TIMEOUT = 1 + serve.MAX_BODY_BYTES = 16 + serve.mimetypes.add_type("text/css", ".css") + + scratch = Path(scratch).resolve() + data = scratch / "assets" + data.mkdir() + (data / "cgit.css").write_text("div#cgit {}\n") + (scratch / "outside.css").write_text("not an asset\n") + (data / "outside.css").symlink_to(scratch / "outside.css") + (scratch / "cgitrc").write_text("") + stub = scratch / "cgit" + stub.write_text(STUB) + stub.chmod(0o755) + + server = serve.CgitServer(("127.0.0.1", 0), scratch / "cgitrc", stub, data) + thread = threading.Thread(target=server.serve_forever) + thread.start() + + def request(method, path, body=None, headers={}): + connection = http.client.HTTPConnection( + "127.0.0.1", server.server_port, timeout=30 + ) + try: + connection.request(method, path, body=body, headers=headers) + response = connection.getresponse() + return response.status, dict(response.getheaders()), response.read() + finally: + connection.close() + + try: + ok = check(request, scratch, server.server_port) + finally: + server.shutdown() + server.server_close() + thread.join() + return 0 if ok else 1 + + +sys.exit(main()) |
