"""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())