3proxy/tests/cases/httpsrv_keepalive.py
Vladimir Dubrovin 629a978c00
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled
help: the features of the build listed, and the tests skip what they need
3proxy --help (and the usage message of a bad invocation) now lists
what the build was made with: TLS and its backend, RADIUS, PCRE, IPv6,
UNIX sockets, the built-in HTTP server, transparency, ODBC and the
optional proxy services. The test runner reads the list before the
first case and the cases state the features they need with
t.skip_unless(), so a build without a feature skips its cases instead
of failing them; a binary too old to report anything skips the cases
with needs and runs the rest.
2026-10-30 14:24:46 +03:00

112 lines
5.0 KiB
Python

"""Keep-alive: which answers may be followed by another request.
The next request begins where the last answer ended, so a connection is only
kept when the length of what was sent is known exactly and the body of the
request was read to its end. Everything else closes, which is the safe way to
be wrong.
"""
import os
def run(t):
if not t.skip_unless("httpsrv"):
return
root = os.path.join(t.tmpdir, "ka")
os.makedirs(root, exist_ok=True)
with open(os.path.join(root, "a.html"), "w") as fp:
fp.write("<h1>hello</h1>")
port = t.free_port()
t.start("httpsrv_keepalive", f"""
log
auth iponly
allow *
http file * /w/*.html "{root}/$1.html"
http reply * /ok** 200
http echo * /echo**
http data * /chunked** size=100&chunked=1
httpsrv -p{port}
""", ports=[port])
def req(path, version="1.1", extra="", body=""):
head = (f"GET {path} HTTP/{version}\r\nHost: t\r\n{extra}\r\n")
if body:
head = head.replace("GET", "POST", 1)
return head + body
def session(*requests, quiet=0.5):
text, closed = t.raw_session(port, "".join(requests), quiet=quiet)
return text, closed, text.count("HTTP/1.")
# --- what keeps the connection ---------------------------------------
text, closed, n = session(req("/w/a.html"), req("/ok"),
req("/w/a.html", extra="Connection: close\r\n"))
t.eq(3, n, "three 1.1 requests are answered on one connection")
t.eq(True, closed, "and the one asking to close ends it")
t.contains(text, "Connection: keep-alive", "the answers say the connection is kept")
t.eq(2, text.count("<h1>hello</h1>"), "each file arrives whole")
text, closed, n = session(req("/w/a.html", version="1.0"), req("/ok", version="1.0"))
t.eq(1, n, "a 1.0 request without the header is answered once")
t.eq(True, closed, "and the connection ends")
t.contains(text, "Connection: close", "which the answer says")
text, closed, n = session(req("/w/a.html", version="1.0",
extra="Connection: keep-alive\r\n"),
req("/ok", version="1.0",
extra="Connection: close\r\n"))
t.eq(2, n, "a 1.0 client asking for keep-alive gets it")
# a request carrying a body: the next one begins after it
text, closed, n = session(req("/echo", extra="Content-Length: 5\r\n", body="hello"),
req("/ok", extra="Connection: close\r\n"))
t.eq(2, n, "a body which was read to its end leaves the stream in place")
t.contains(text, "content.length=5", "and the body was seen")
# --- what ends it -----------------------------------------------------
text, closed, n = session(req("/echo", extra="Transfer-Encoding: chunked\r\n"),
req("/ok"))
t.eq(1, n, "a request body this server cannot frame ends the connection")
t.eq(True, closed, "the connection is closed rather than left mid-body")
# A body longer than the server is willing to read leaves the rest of it
# in the stream, so the connection cannot carry another request. The send
# may not even finish - the server answers and closes part way through -
# which is the same answer from the other side.
big = "x" * 1500000
text, closed, n = session(req("/echo", extra="Content-Length: 1500000\r\n", body=big),
quiet=2)
t.eq(1, n, "a body past what the server will read is answered once")
t.contains(text, "Connection: close",
"and the answer ends the connection rather than leaving the rest to be read")
# --- answers of other shapes -----------------------------------------
text, closed, n = session(req("/chunked"), req("/ok", extra="Connection: close\r\n"))
t.eq(2, n, "a chunked answer may be followed by another request")
text, closed, n = session(req("/chunked", version="1.0"), req("/ok", version="1.0"))
t.eq(1, n, "but not for a client which has no chunked encoding to read")
stamp = t.http(f"http://127.0.0.1:{port}/w/a.html").header("Last-Modified")
text, closed, n = session(req("/w/a.html", extra=f"If-Modified-Since: {stamp}\r\n"),
req("/ok", extra="Connection: close\r\n"))
t.eq(2, n, "a 304 carries no body and the next request follows it")
t.contains(text, "304", "and it is a 304")
# --- the administration pages always close ---------------------------
aport = t.free_port()
t.start("httpsrv_keepalive_admin", f"""
log
auth iponly
allow *
http file * /w/*.html "{root}/$1.html"
admin -p{aport}
""", ports=[aport])
text, closed = t.raw_session(aport,
f"GET /w/a.html HTTP/1.1\r\nHost: t\r\n\r\nGET /C HTTP/1.1\r\nHost: t\r\n\r\n"
f"GET /w/a.html HTTP/1.1\r\nHost: t\r\n\r\n")
t.eq(2, text.count("HTTP/1."), "an administration page is the last thing on a connection")
t.eq(True, closed, "which the server closes, since the page states no length")