mirror of
https://github.com/3proxy/3proxy.git
synced 2026-09-02 21:05:49 +08:00
196 lines
9.3 KiB
Python
196 lines
9.3 KiB
Python
"""The operations that serve a filesystem: file, cache, redir and rewrite.
|
|
|
|
A rule maps a request onto a path with a template, where $1 upwards stand for
|
|
what the stars or the groups of a regular expression matched.
|
|
"""
|
|
|
|
import os
|
|
|
|
|
|
def run(t):
|
|
root = os.path.join(t.tmpdir, "web")
|
|
os.makedirs(os.path.join(root, "picts", "set"), exist_ok=True)
|
|
with open(os.path.join(root, "a.html"), "w") as fp:
|
|
fp.write("<h1>hello</h1>")
|
|
with open(os.path.join(root, "big.bin"), "wb") as fp:
|
|
fp.write(b"x" * 300000) # past a single send, and past the cache limit
|
|
with open(os.path.join(root, "picts", "set", "dog.gif"), "wb") as fp:
|
|
fp.write(b"GIF89a-pretend")
|
|
|
|
with open(os.path.join(root, "b.webp"), "wb") as fp:
|
|
fp.write(b"RIFF-pretend")
|
|
|
|
port = t.free_port()
|
|
t.start("httpsrv_files", f"""
|
|
log
|
|
auth iponly
|
|
allow *
|
|
http rewrite * /alias/** "/w/$1"
|
|
http file * /w/*.html "{root}/$1.html"
|
|
http file * /big {root}/big.bin
|
|
http cache * /c/*.html "{root}/$1.html"
|
|
http cache * "pcre:^/(.*)/pic/(.*)\\.(gif|jpeg)$" "{root}/picts/$1/$2.$3"
|
|
http redir * /old/** 301 "https://example.org/$1"
|
|
http redir * /moved /w/a.html
|
|
http file * /rel/*.html "web/$1.html"
|
|
http echo * /echo
|
|
|
|
http_content_type .webp image/webp
|
|
http_content_type dat application/x-mydata
|
|
http file * /ct/*.webp "{root}/$1.webp"
|
|
http file * /named/*.html "{root}/$1.html" text/x-named
|
|
http file * /star/*.html "{root}/$1.html" *
|
|
http cache * /ctc/*.webp "{root}/$1.webp"
|
|
|
|
http file * /aged/*.html "{root}/$1.html" * 3600
|
|
http cache * /aged2/*.html "{root}/$1.html" * 60
|
|
http file * /extra/*.html "{root}/$1.html" * * "X-One: 1\\nX-Two: two words"
|
|
http file * /err/*.html "{root}/$1.html" * * "X-Served: static" 404
|
|
http reply * /ok**
|
|
http reply * /nobody** 204
|
|
http reply * /down** 503 "Retry-After: 30"
|
|
http cache * /held/*.html "{root}/$1.html" * 30
|
|
httpsrv -p{port}
|
|
""", ports=[port])
|
|
|
|
url = f"http://127.0.0.1:{port}"
|
|
|
|
# --- file -------------------------------------------------------------
|
|
r = t.http(url + "/w/a.html")
|
|
t.eq(200, r.status, "a file is served")
|
|
t.contains(r, "<h1>hello</h1>", "with its content")
|
|
t.eq("text/html", r.header("Content-Type"), "and a type taken from the name")
|
|
t.eq(str(len("<h1>hello</h1>")), r.header("Content-Length"), "and its length")
|
|
|
|
t.eq(404, t.http(url + "/w/nosuch.html").status, "a missing file is not found")
|
|
big = t.http(url + "/big")
|
|
t.eq(300000, big.length, "a large file arrives whole")
|
|
t.eq("300000", big.header("Content-Length"), "and is announced by its length")
|
|
t.eq(None, big.header("Transfer-Encoding"),
|
|
"a file is never sent chunked")
|
|
t.eq("300000", t.http(url + "/big", method="HEAD").header("Content-Length"),
|
|
"HEAD gives the length without the body")
|
|
t.eq(200, t.http(url + "/w/a.html", method="HEAD").status, "HEAD is answered")
|
|
t.eq(0, t.http(url + "/w/a.html", method="HEAD").length, "HEAD carries no body")
|
|
|
|
# --- cache ------------------------------------------------------------
|
|
first = t.http(url + "/c/a.html")
|
|
second = t.http(url + "/c/a.html")
|
|
t.eq(200, first.status, "a cached file is served")
|
|
t.eq(first.text, second.text, "and the same on the next request")
|
|
t.contains(second, "<h1>hello</h1>", "from memory this time")
|
|
|
|
# a file changed on disk is noticed rather than served from before
|
|
with open(os.path.join(root, "a.html"), "w") as fp:
|
|
fp.write("<h1>changed</h1>")
|
|
t.contains(t.http(url + "/c/a.html"), "changed",
|
|
"a file replaced on disk is read again")
|
|
|
|
# --- what the stars stand for -----------------------------------------
|
|
r = t.http(url + "/set/pic/dog.gif")
|
|
t.eq(200, r.status, "a regular expression maps a request onto a path")
|
|
t.contains(r, "GIF89a", "and the file is served")
|
|
t.eq("image/gif", r.header("Content-Type"), "with the type of that name")
|
|
|
|
# --- redir ------------------------------------------------------------
|
|
r = t.http(url + "/old/thing")
|
|
t.eq(301, r.status, "a redirect uses the status it was given")
|
|
t.eq("https://example.org/thing", r.header("Location"),
|
|
"and a location built from the request")
|
|
t.eq(302, t.http(url + "/moved").status, "without a status it is 302")
|
|
|
|
# --- rewrite ----------------------------------------------------------
|
|
r = t.http(url + "/alias/a.html")
|
|
t.eq(200, r.status, "a rewritten request reaches the rule after it")
|
|
t.contains(r, "changed", "and is served from the path it was rewritten to")
|
|
|
|
# --- the type a reply carries -------------------------------------------
|
|
# Worked out from the name, using what the configuration has registered
|
|
# on top of what is built in, unless the rule says otherwise.
|
|
t.eq("image/webp", t.http(url + "/ct/b.webp").header("Content-Type"),
|
|
"a registered extension names the type")
|
|
t.eq("image/webp", t.http(url + "/ctc/b.webp").header("Content-Type"),
|
|
"and a cached file is answered the same way")
|
|
t.eq("text/x-named", t.http(url + "/named/a.html").header("Content-Type"),
|
|
"a rule may name the type itself")
|
|
t.eq("text/html", t.http(url + "/star/a.html").header("Content-Type"),
|
|
"and a star there leaves it to the name of the file")
|
|
|
|
# --- what a rule adds to the answer ------------------------------------
|
|
r = t.http(url + "/aged/a.html")
|
|
t.eq("max-age=3600", r.header("Cache-Control"), "a rule may describe caching")
|
|
t.eq("max-age=60", t.http(url + "/aged2/a.html").header("Cache-Control"),
|
|
"a cached file is answered the same way")
|
|
t.eq(None, t.http(url + "/w/a.html").header("Cache-Control"),
|
|
"and a rule which says nothing sends nothing")
|
|
|
|
r = t.http(url + "/extra/a.html")
|
|
t.eq("1", r.header("X-One"), "a rule may add headers")
|
|
t.eq("two words", r.header("X-Two"),
|
|
"the second of them arrives whole, spaces and all")
|
|
|
|
r = t.http(url + "/err/a.html")
|
|
t.eq(404, r.status, "a rule may answer with the status it names")
|
|
t.contains(r, "<h1>", "and the file is still the body")
|
|
t.eq("static", r.header("X-Served"),
|
|
"what the rule adds goes with the status the rule asked for")
|
|
|
|
# a refusal the server decided on is its own answer
|
|
r = t.http(url + "/err/nosuch.html")
|
|
t.eq(404, r.status, "a missing file is still not found")
|
|
t.eq(None, r.header("X-Served"), "and carries none of the rule's headers")
|
|
t.eq(None, t.http(url + "/aged/nosuch.html").header("Cache-Control"),
|
|
"nor what it said about caching")
|
|
|
|
# --- reply --------------------------------------------------------------
|
|
r = t.http(url + "/ok")
|
|
t.eq(200, r.status, "reply answers with 200 by default")
|
|
t.eq("0", r.header("Content-Length"), "with a length of zero")
|
|
t.eq(0, r.length, "and no body")
|
|
|
|
r = t.http(url + "/nobody")
|
|
t.eq(204, r.status, "reply answers with the status it was given")
|
|
t.eq(None, r.header("Content-Length"),
|
|
"and a status carrying no body is sent without a length")
|
|
|
|
r = t.http(url + "/down")
|
|
t.eq(503, r.status, "reply serves a refusal the configuration decided on")
|
|
t.eq("30", r.header("Retry-After"), "with the headers that go with it")
|
|
|
|
# --- a client which has the file already --------------------------------
|
|
r = t.http(url + "/w/a.html")
|
|
stamp = r.header("Last-Modified")
|
|
t.ne(None, stamp, "a file is answered with the time it was last changed")
|
|
|
|
r = t.http(url + "/w/a.html", headers={"If-Modified-Since": stamp})
|
|
t.eq(304, r.status, "and an unchanged file is answered 304")
|
|
t.eq(0, r.length, "which carries no body")
|
|
t.eq(None, r.header("Content-Length"), "and no length")
|
|
t.eq(stamp, r.header("Last-Modified"), "but still says when the file changed")
|
|
|
|
t.eq(200, t.http(url + "/w/a.html",
|
|
headers={"If-Modified-Since": "Sun, 06 Nov 1994 08:49:37 GMT"}).status,
|
|
"an older date is answered with the file")
|
|
t.eq(200, t.http(url + "/w/a.html",
|
|
headers={"If-Modified-Since": "not a date at all"}).status,
|
|
"and a date which cannot be read is treated as none")
|
|
t.eq(304, t.http(url + "/c/a.html", headers={"If-Modified-Since": stamp}).status,
|
|
"a file answered from memory is conditional in the same way")
|
|
t.eq(404, t.http(url + "/err/a.html", headers={"If-Modified-Since": stamp}).status,
|
|
"a rule with a status of its own is not turned into a 304")
|
|
|
|
# --- a rule which says how long its copy may be held --------------------
|
|
t.contains(t.http(url + "/held/a.html"), "changed", "a held file is served")
|
|
with open(os.path.join(root, "a.html"), "w") as fp:
|
|
fp.write("<h1>replaced</h1>")
|
|
t.not_contains(t.http(url + "/held/a.html"), "replaced",
|
|
"and within its max-age the disk is not looked at again")
|
|
t.contains(t.http(url + "/c/a.html"), "replaced",
|
|
"while a rule without one notices the change at once")
|
|
|
|
# --- the paths a rule may not build ------------------------------------
|
|
t.eq(403, t.http(url + "/rel/a.html").status,
|
|
"a relative target is refused")
|
|
t.ne(200, t.http(url + "/w/../etc/passwd").status,
|
|
"a request climbing out of the tree is refused")
|