mirror of
https://github.com/3proxy/3proxy.git
synced 2026-10-28 22:05:48 +08:00
The HTTP proxy read each request into a buffer of 64 KB of its own, cleared it with memset, and read the answer's headers into it again; the header lines were copied there one by one from clibuf or srvbuf. Now the header block of a request is read whole into clibuf, and that of an answer into srvbuf, and worked on where it is (hdrread()): - the block is moved to one byte into the buffer, and its lines, as they are kept, are moved down to its start, so the line being worked on, and the 0 written after it, never reach the next one; headers taken out are left behind; - HDRRESERVE is left free after it, for the headers this proxy adds, before whatever came after it, a body or another request, from where the connection is read on; - the buffer is grown only for a block bigger than it, up to the limits there were (512 KB for a request, 64 KB for an answer); - the headers are copied to a buffer of their own only when they need more room than that, when a filter hands back one, when a body is to be read whole into the same buffer for a data filter, and for an ftp listing, which is written where the headers were. Whether buf is the headers' own buffer is kept with it (bufown), not found from where it points: the side's buffer may be a bigger one by then let go of. filterswap() leaves alone a buffer in either side's buffer, which may be one grown by malloc, not only their fixed arrays. What took buf for a buffer of LINESIZE does no longer: the request is logged from a copy of its line, and the rest of a request refused before its headers were all read is drained through a small buffer of its own, keeping track of where its lines begin. Tests: a POST whose body comes in the same packet as its headers, which are rewritten, with a header added and one taken out; 45 KB of request headers; two requests pipelined in one packet; empty lines before a request; an answer with a 30 KB header; a 600 KB header block refused, the proxy answering after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
6.6 KiB
Python
157 lines
6.6 KiB
Python
"""HTTP proxy headers, read whole into the connection's buffer and rewritten
|
|
there: what reaches the server and what comes back are checked byte for
|
|
byte where it matters, for header blocks bigger than the buffer, a body or
|
|
another request in the same packet as the headers, and answers with big
|
|
headers."""
|
|
|
|
import socket
|
|
import threading
|
|
import time
|
|
|
|
|
|
class Origin:
|
|
"""A server recording every request it gets, headers and body, and
|
|
answering each with the answer given, keeping the connection."""
|
|
|
|
def __init__(self, answer):
|
|
self.answer = answer
|
|
self.requests = []
|
|
self.sock = socket.socket()
|
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
self.sock.bind(("127.0.0.1", 0))
|
|
self.sock.listen(16)
|
|
self.port = self.sock.getsockname()[1]
|
|
threading.Thread(target=self.serve, daemon=True).start()
|
|
|
|
def serve(self):
|
|
while True:
|
|
try:
|
|
c, _ = self.sock.accept()
|
|
except OSError:
|
|
return
|
|
threading.Thread(target=self.conn, args=(c,), daemon=True).start()
|
|
|
|
def conn(self, c):
|
|
data = b""
|
|
c.settimeout(10)
|
|
try:
|
|
while True:
|
|
while b"\r\n\r\n" not in data:
|
|
got = c.recv(65536)
|
|
if not got:
|
|
return
|
|
data += got
|
|
head, _, data = data.partition(b"\r\n\r\n")
|
|
length = 0
|
|
for line in head.split(b"\r\n")[1:]:
|
|
name, _, value = line.partition(b":")
|
|
if name.strip().lower() == b"content-length":
|
|
length = int(value.strip())
|
|
while len(data) < length:
|
|
got = c.recv(65536)
|
|
if not got:
|
|
return
|
|
data += got
|
|
body, data = data[:length], data[length:]
|
|
self.requests.append((head, body))
|
|
c.sendall(self.answer)
|
|
except OSError:
|
|
pass
|
|
finally:
|
|
c.close()
|
|
|
|
def close(self):
|
|
self.sock.close()
|
|
|
|
|
|
def session(port, data, until, timeout=5):
|
|
"""Sends data at once and reads until `until` answers have come (each
|
|
ending with the 'ok' body), or the time is up."""
|
|
s = socket.create_connection(("127.0.0.1", port), timeout)
|
|
s.sendall(data)
|
|
got = b""
|
|
deadline = time.time() + timeout
|
|
try:
|
|
while got.count(b"\r\n\r\nok") < until and time.time() < deadline:
|
|
chunk = s.recv(65536)
|
|
if not chunk:
|
|
break
|
|
got += chunk
|
|
except OSError:
|
|
pass
|
|
s.close()
|
|
return got
|
|
|
|
|
|
def run(t):
|
|
ok = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok"
|
|
origin = Origin(ok)
|
|
proxy = t.free_port()
|
|
t.start("proxy_headers", f"""
|
|
log
|
|
auth iponly
|
|
allow *
|
|
proxy -p{proxy}
|
|
""", ports=[proxy])
|
|
base = f"http://127.0.0.1:{origin.port}"
|
|
|
|
def request(path, extra=b"", body=b"", method=b"GET"):
|
|
r = (method + b" " + base.encode() + path + b" HTTP/1.1\r\nHost: 127.0.0.1\r\n" + extra)
|
|
if body:
|
|
r += b"Content-Length: " + str(len(body)).encode() + b"\r\n"
|
|
return r + b"\r\n" + body
|
|
|
|
# --- a body in the same packet as the headers, headers added and taken
|
|
# out on the way ------------------------------------------------------
|
|
origin.requests.clear()
|
|
body = b"0123456789" * 50
|
|
got = session(proxy, request(b"/post", b"Proxy-Connection: keep-alive\r\n", body, b"POST"), 1)
|
|
t.contains(got.decode("latin-1"), "200 OK", "a POST with its body in the headers' packet is answered")
|
|
head, rbody = origin.requests[0] if origin.requests else (b"", b"")
|
|
t.eq(body, rbody, "its body arrives whole, after the headers rewritten in place")
|
|
t.ok("the proxy's Forwarded header is added") if b"\r\nForwarded: for=" in head else \
|
|
t.fail("the proxy's Forwarded header is added", "Forwarded", head[-200:])
|
|
t.ok("and Proxy-Connection taken out") if b"proxy-connection" not in head.lower() else \
|
|
t.fail("and Proxy-Connection taken out", "no Proxy-Connection", head[-200:])
|
|
|
|
# --- a header block bigger than the connection's buffer --------------
|
|
origin.requests.clear()
|
|
big = b"".join(b"X-Big-%d: %s\r\n" % (i, bytes([65 + i]) * 15000) for i in range(3))
|
|
got = session(proxy, request(b"/big", big), 1)
|
|
t.contains(got.decode("latin-1"), "200 OK", "a request with 45 KB of headers is answered")
|
|
head = origin.requests[0][0] if origin.requests else b""
|
|
whole = all((b"X-Big-%d: %s\r\n" % (i, bytes([65 + i]) * 15000)) in head + b"\r\n" for i in range(3))
|
|
t.ok("and every big header arrives whole") if whole else \
|
|
t.fail("and every big header arrives whole", "3 headers of 15000", len(head))
|
|
|
|
# --- two requests in one packet, kept alive ---------------------------
|
|
origin.requests.clear()
|
|
got = session(proxy, request(b"/one", b"Connection: keep-alive\r\n") +
|
|
request(b"/two", b"Connection: keep-alive\r\n"), 2)
|
|
t.eq(2, got.count(b"\r\n\r\nok"), "two pipelined requests are both answered")
|
|
paths = [h.split(b" ")[1] for h, _ in origin.requests]
|
|
t.eq([b"/one", b"/two"], paths, "in order, each with its own path")
|
|
|
|
# --- empty lines before a request -------------------------------------
|
|
got = session(proxy, b"\r\n\r\n" + request(b"/after"), 1)
|
|
t.contains(got.decode("latin-1"), "200 OK", "a request after empty lines is answered")
|
|
|
|
# --- an answer with headers bigger than the connection's buffer -------
|
|
origin.answer = (b"HTTP/1.1 200 OK\r\nX-Long: " + b"L" * 30000 +
|
|
b"\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok")
|
|
got = session(proxy, request(b"/longanswer"), 1)
|
|
t.ok("an answer with a 30 KB header arrives with it whole") if b"X-Long: " + b"L" * 30000 + b"\r\n" in got else \
|
|
t.fail("an answer with a 30 KB header arrives with it whole", "the header", len(got))
|
|
t.ok("and its body after it") if got.endswith(b"\r\n\r\nok") else \
|
|
t.fail("and its body after it", "ok at the end", got[-40:])
|
|
|
|
# --- a request too big for any buffer is refused, and the proxy stays --
|
|
huge = b"".join(b"X-Huge-%d: %s\r\n" % (i, b"H" * 30000) for i in range(20))
|
|
got = session(proxy, request(b"/huge", huge), 1, timeout=3)
|
|
t.ok("a 600 KB header block is refused") if b"200 OK" not in got else \
|
|
t.fail("a 600 KB header block is refused", "no 200", got[:60])
|
|
origin.answer = ok
|
|
got = session(proxy, request(b"/again"), 1)
|
|
t.contains(got.decode("latin-1"), "200 OK", "and the proxy answers after it")
|
|
origin.close()
|