|
| 1 | +import socket |
| 2 | + |
| 3 | +import pytest |
| 4 | + |
| 5 | +from pyhttpd.conf import HttpdConf |
| 6 | + |
| 7 | +# the status as answered by the CGI script, or as written by mod_aptest as a |
| 8 | +# final response of its own |
| 9 | +SCRIPT = "/status_body.py?status={status}&parts={parts}" |
| 10 | +PREBUILT = "/pre?status={status}" |
| 11 | + |
| 12 | +BODY = b"SHOULD-NOT-BE-SENT\n" |
| 13 | + |
| 14 | + |
| 15 | +class TestStatusNoBody: |
| 16 | + |
| 17 | + @pytest.fixture(autouse=True, scope='class') |
| 18 | + def _class_scope(self, env): |
| 19 | + conf = HttpdConf(env, extras={ |
| 20 | + "base": [ |
| 21 | + "<Location /pre>", |
| 22 | + " SetHandler aptest-prebuilt", |
| 23 | + "</Location>", |
| 24 | + ] |
| 25 | + }) |
| 26 | + conf.add_vhost_cgi() |
| 27 | + conf.install() |
| 28 | + assert env.apache_restart() == 0 |
| 29 | + |
| 30 | + @staticmethod |
| 31 | + def _recv_all(sock): |
| 32 | + data = b"" |
| 33 | + while True: |
| 34 | + chunk = sock.recv(65536) |
| 35 | + if not chunk: |
| 36 | + return data |
| 37 | + data += chunk |
| 38 | + |
| 39 | + @staticmethod |
| 40 | + def _split_response(data, method="GET"): |
| 41 | + """Cut the first response off a byte stream the way a client does: |
| 42 | + by status code and Content-Length, never by what the server closes. |
| 43 | + Returns the response and what is left after it.""" |
| 44 | + head, sep, rest = data.partition(b"\r\n\r\n") |
| 45 | + assert sep, f"no complete response header in {data!r}" |
| 46 | + lines = head.decode("latin-1").split("\r\n") |
| 47 | + status = int(lines[0].split(" ")[1]) |
| 48 | + headers = {} |
| 49 | + for line in lines[1:]: |
| 50 | + name, _, value = line.partition(":") |
| 51 | + headers[name.strip().lower()] = value.strip() |
| 52 | + if method == "HEAD" or status in (204, 304) or 100 <= status < 200: |
| 53 | + return status, headers, b"", rest |
| 54 | + if headers.get("transfer-encoding") == "chunked": |
| 55 | + body = b"" |
| 56 | + while True: |
| 57 | + size_line, _, rest = rest.partition(b"\r\n") |
| 58 | + size = int(size_line.split(b";")[0], 16) |
| 59 | + if size == 0: |
| 60 | + # no trailers expected |
| 61 | + assert rest[:2] == b"\r\n", f"trailer: {rest[:20]!r}" |
| 62 | + return status, headers, body, rest[2:] |
| 63 | + body += rest[:size] |
| 64 | + assert rest[size:size + 2] == b"\r\n" |
| 65 | + rest = rest[size + 2:] |
| 66 | + assert "content-length" in headers, \ |
| 67 | + f"body is not delimited, the client reads until close: {headers}" |
| 68 | + n = int(headers["content-length"]) |
| 69 | + assert len(rest) >= n, f"short body: {len(rest)} < {n}" |
| 70 | + return status, headers, rest[:n], rest[n:] |
| 71 | + |
| 72 | + def exchange(self, env, status, method="GET", parts=1, target=SCRIPT): |
| 73 | + """Send two requests on one connection and parse both responses |
| 74 | + from the bytes the server wrote. The second request is a 200 which |
| 75 | + shows the connection is still correctly framed after the first.""" |
| 76 | + host = f"cgi.{env.http_tld}" |
| 77 | + first = target.format(status=status, parts=parts) |
| 78 | + second = target.format(status=200, parts=1) |
| 79 | + req = (f"{method} {first} HTTP/1.1\r\n" |
| 80 | + f"Host: {host}\r\n\r\n" |
| 81 | + f"GET {second} HTTP/1.1\r\n" |
| 82 | + f"Host: {host}\r\nConnection: close\r\n\r\n").encode() |
| 83 | + with socket.create_connection(("127.0.0.1", env.http_port), |
| 84 | + timeout=10) as sock: |
| 85 | + sock.sendall(req) |
| 86 | + data = self._recv_all(sock) |
| 87 | + resp1 = self._split_response(data, method) |
| 88 | + resp2 = self._split_response(resp1[3]) |
| 89 | + assert resp2[3] == b"", f"unexpected data after 2nd response: {resp2[3]!r}" |
| 90 | + return resp1[:3], resp2[:3] |
| 91 | + |
| 92 | + # a 205 carries no content, whatever the handler writes |
| 93 | + def test_core_012_01(self, env): |
| 94 | + (status, headers, body), _ = self.exchange(env, 205) |
| 95 | + assert status == 205 |
| 96 | + assert body == b"" |
| 97 | + |
| 98 | + # without content the response is delimited by Content-Length: 0 |
| 99 | + def test_core_012_02(self, env): |
| 100 | + (status, headers, body), _ = self.exchange(env, 205) |
| 101 | + assert status == 205 |
| 102 | + assert headers.get("content-length") == "0" |
| 103 | + assert "transfer-encoding" not in headers |
| 104 | + |
| 105 | + # the connection stays usable, nothing of the discarded body shows up |
| 106 | + def test_core_012_03(self, env): |
| 107 | + _, (status, headers, body) = self.exchange(env, 205) |
| 108 | + assert status == 200 |
| 109 | + assert body == BODY |
| 110 | + |
| 111 | + # a HEAD request is answered without content and keeps the connection |
| 112 | + def test_core_012_04(self, env): |
| 113 | + (status, headers, body), (status2, _, body2) = \ |
| 114 | + self.exchange(env, 205, method="HEAD") |
| 115 | + assert status == 205 |
| 116 | + assert body == b"" |
| 117 | + assert (status2, body2) == (200, BODY) |
| 118 | + |
| 119 | + # 204 stays without content and without content related headers |
| 120 | + def test_core_012_05(self, env): |
| 121 | + (status, headers, body), (status2, _, body2) = self.exchange(env, 204) |
| 122 | + assert status == 204 |
| 123 | + assert body == b"" |
| 124 | + assert "content-length" not in headers |
| 125 | + assert "transfer-encoding" not in headers |
| 126 | + assert (status2, body2) == (200, BODY) |
| 127 | + |
| 128 | + # 304 stays without content |
| 129 | + def test_core_012_06(self, env): |
| 130 | + (status, headers, body), (status2, _, body2) = self.exchange(env, 304) |
| 131 | + assert status == 304 |
| 132 | + assert body == b"" |
| 133 | + assert (status2, body2) == (200, BODY) |
| 134 | + |
| 135 | + # other 2xx statuses still carry the body |
| 136 | + @pytest.mark.parametrize("code", [200, 201, 202]) |
| 137 | + def test_core_012_07(self, env, code): |
| 138 | + (status, headers, body), _ = self.exchange(env, code) |
| 139 | + assert status == code |
| 140 | + assert body == BODY |
| 141 | + |
| 142 | + # the same when the handler writes its content in several flushed parts |
| 143 | + def test_core_012_08(self, env): |
| 144 | + (status, headers, body), (status2, _, body2) = \ |
| 145 | + self.exchange(env, 205, parts=3) |
| 146 | + assert status == 205 |
| 147 | + assert body == b"" |
| 148 | + assert headers.get("content-length") == "0" |
| 149 | + assert (status2, body2) == (200, BODY) |
| 150 | + |
| 151 | + # the same when the response is a final response bucket written by the |
| 152 | + # handler itself, 204 and 304 included |
| 153 | + @pytest.mark.parametrize("code", [204, 205, 304]) |
| 154 | + def test_core_012_09(self, env, code): |
| 155 | + (status, headers, body), (status2, _, body2) = \ |
| 156 | + self.exchange(env, code, target=PREBUILT) |
| 157 | + assert status == code |
| 158 | + assert body == b"" |
| 159 | + if code == 205: |
| 160 | + assert headers.get("content-length") == "0" |
| 161 | + assert "transfer-encoding" not in headers |
| 162 | + assert (status2, body2) == (200, BODY) |
0 commit comments