mirror of
https://github.com/3proxy/3proxy.git
synced 2026-09-02 21:05:49 +08:00
Make pcre_extend work, and cover PCRE filtering with tests
The filter walked its list of access rules but tested the first entry each time round, so anything pcre_extend appended was never consulted and the command did nothing at all. Test the entry the loop is on. pcre_extend takes an ACE and no FILTER_ACTION - the rule keeps the action it was given - so correct the manual, which documented an argument the command does not read. The tests cover matching and denial, rule order, client headers, matching options, the access rule a pcre rule carries, pcre_extend, and rewriting a reply header and reply data. Request rewriting is covered through an HTTP parent, which is the path where it reaches the wire.
This commit is contained in:
parent
fc544c4dff
commit
cdbd47dc5b
@ -1435,7 +1435,7 @@ Apply a rule for matching regular expression.
|
|||||||
Match and replace with rewrite expression.
|
Match and replace with rewrite expression.
|
||||||
.br
|
.br
|
||||||
.BR pcre_extend
|
.BR pcre_extend
|
||||||
\fIFILTER_ACTION [ACE]\fR
|
\fIACE\fR
|
||||||
.br
|
.br
|
||||||
Extend the ACL of the last pcre or pcre_rewrite command by adding an additional ACE.
|
Extend the ACL of the last pcre or pcre_rewrite command by adding an additional ACE.
|
||||||
.br
|
.br
|
||||||
|
|||||||
@ -277,7 +277,7 @@ static FILTER_ACTION pcre_filter_buffer(void *fc, struct clientparam *param, uns
|
|||||||
#define pcrefd ((struct pcre_filter_data *)fc)
|
#define pcrefd ((struct pcre_filter_data *)fc)
|
||||||
|
|
||||||
for(acl = pcrefd->acl; acl; acl=acl->next){
|
for(acl = pcrefd->acl; acl; acl=acl->next){
|
||||||
if(pl->ACLMatches(pcrefd->acl, param)){
|
if(pl->ACLMatches(acl, param)){
|
||||||
match = 1;
|
match = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
113
tests/cases/pcre.py
Normal file
113
tests/cases/pcre.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
"""PCRE filtering: matching, rewriting, options and rule scope.
|
||||||
|
|
||||||
|
Request rewriting only reaches the wire through an HTTP parent. On a direct
|
||||||
|
connection the request has already been parsed and converted to origin form
|
||||||
|
by the time the filter runs, so the rewrite shows up in the log and nowhere
|
||||||
|
else; that path is left alone here rather than pinned down as correct.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _has_pcre(t):
|
||||||
|
"""Whether this build accepts the pcre commands at all.
|
||||||
|
|
||||||
|
The last line is nonsense on purpose: it makes 3proxy report and exit
|
||||||
|
instead of waiting, and what it says about the line above is the answer.
|
||||||
|
"""
|
||||||
|
out = t.run_config("pcre_probe",
|
||||||
|
'log\npcre request deny "x"\nnot_a_command\n')
|
||||||
|
return "'pcre'" not in out
|
||||||
|
|
||||||
|
|
||||||
|
def run(t):
|
||||||
|
if not _has_pcre(t):
|
||||||
|
t.skip("PCRE (this build has no PCRE support)")
|
||||||
|
return
|
||||||
|
|
||||||
|
origin = t.free_port()
|
||||||
|
t.start("pcre_origin", f"""
|
||||||
|
log
|
||||||
|
auth iponly
|
||||||
|
allow *
|
||||||
|
http * /echo* echo
|
||||||
|
http * /secret* echo
|
||||||
|
http * /data data
|
||||||
|
httpsrv -p{origin}
|
||||||
|
""", ports=[origin])
|
||||||
|
|
||||||
|
url = f"http://127.0.0.1:{origin}"
|
||||||
|
|
||||||
|
def proxy_with(name, *rules):
|
||||||
|
port = t.free_port()
|
||||||
|
t.start(name, "\n".join([
|
||||||
|
"log", "flush", "auth iponly", "allow *", *rules, f"proxy -p{port}"]),
|
||||||
|
ports=[port])
|
||||||
|
return f"127.0.0.1:{port}"
|
||||||
|
|
||||||
|
# --- matching and denial ---------------------------------------------
|
||||||
|
p = proxy_with("deny", 'pcre request deny "/secret"')
|
||||||
|
t.eq(200, t.http(url + "/echo", proxy=p).status, "an unmatched request passes")
|
||||||
|
t.ne(200, t.http(url + "/secret/page", proxy=p).status, "a matched request is denied")
|
||||||
|
|
||||||
|
# the rules are ordered, and the first decision wins
|
||||||
|
p = proxy_with("allow_first", 'pcre request allow "/echo"', 'pcre request deny "/"')
|
||||||
|
t.eq(200, t.http(url + "/echo", proxy=p).status, "allow short-circuits a later deny")
|
||||||
|
p = proxy_with("deny_first", 'pcre request deny "/"', 'pcre request allow "/echo"')
|
||||||
|
t.ne(200, t.http(url + "/echo", proxy=p).status, "deny short-circuits a later allow")
|
||||||
|
|
||||||
|
# --- what the pattern is matched against ------------------------------
|
||||||
|
p = proxy_with("cliheader", 'pcre cliheader deny "BadBot"')
|
||||||
|
t.eq(200, t.http(url + "/echo", proxy=p).status, "a header rule ignores other requests")
|
||||||
|
t.ne(200, t.http(url + "/echo", proxy=p, headers={"User-Agent": "BadBot/1.0"}).status,
|
||||||
|
"a client header can be matched")
|
||||||
|
|
||||||
|
# --- options ------------------------------------------------------------
|
||||||
|
p = proxy_with("caseless", "pcre_options PCRE2_CASELESS",
|
||||||
|
'pcre request deny "/SECRET"')
|
||||||
|
t.ne(200, t.http(url + "/secret/page", proxy=p).status,
|
||||||
|
"PCRE2_CASELESS makes the match case-insensitive")
|
||||||
|
p = proxy_with("cased", 'pcre request deny "/SECRET"')
|
||||||
|
t.eq(200, t.http(url + "/secret/page", proxy=p).status,
|
||||||
|
"without it the match is case-sensitive")
|
||||||
|
|
||||||
|
# --- the access rule a pcre rule carries --------------------------------
|
||||||
|
p = proxy_with("ace_here", f'pcre request deny "/echo" * * * {origin}')
|
||||||
|
t.ne(200, t.http(url + "/echo", proxy=p).status,
|
||||||
|
"a rule applies where its access rule matches")
|
||||||
|
p = proxy_with("ace_elsewhere", 'pcre request deny "/echo" * * * 1')
|
||||||
|
t.eq(200, t.http(url + "/echo", proxy=p).status,
|
||||||
|
"and not where it does not")
|
||||||
|
|
||||||
|
# pcre_extend appends another access rule to the one just defined
|
||||||
|
p = proxy_with("extend", 'pcre request deny "/echo" * * * 1',
|
||||||
|
f"pcre_extend * * * {origin}")
|
||||||
|
t.ne(200, t.http(url + "/echo", proxy=p).status,
|
||||||
|
"pcre_extend widens the rule to another destination")
|
||||||
|
p = proxy_with("extend_other", 'pcre request deny "/echo" * * * 1',
|
||||||
|
"pcre_extend * * * 2")
|
||||||
|
t.eq(200, t.http(url + "/echo", proxy=p).status,
|
||||||
|
"an extension that matches nothing changes nothing")
|
||||||
|
|
||||||
|
# --- rewriting the reply ------------------------------------------------
|
||||||
|
p = proxy_with("rewrite_srv",
|
||||||
|
'pcre_rewrite srvheader dunno "text/plain" "text/rewritten"',
|
||||||
|
'pcre_rewrite srvdata dunno "peer.addr" "PEER.ADDR"')
|
||||||
|
r = t.http(url + "/echo", proxy=p)
|
||||||
|
t.eq(200, r.status, "a rewritten reply still arrives")
|
||||||
|
t.eq("text/rewritten", r.header("Content-Type"), "a reply header can be rewritten")
|
||||||
|
t.contains(r, "PEER.ADDR", "reply data can be rewritten")
|
||||||
|
t.not_contains(r, "peer.addr", "the original text is gone")
|
||||||
|
|
||||||
|
# --- rewriting the request, which needs an HTTP parent -------------------
|
||||||
|
parent = t.free_port()
|
||||||
|
t.start("pcre_parent", f"""
|
||||||
|
log
|
||||||
|
flush
|
||||||
|
auth iponly
|
||||||
|
allow *
|
||||||
|
proxy -p{parent}
|
||||||
|
""", ports=[parent])
|
||||||
|
p = proxy_with("rewrite_req", 'pcre_rewrite request dunno "/echo/old" "/echo/new"',
|
||||||
|
f"parent 1000 http 127.0.0.1 {parent}")
|
||||||
|
r = t.http(url + "/echo/old", proxy=p)
|
||||||
|
t.eq(200, r.status, "a rewritten request still arrives")
|
||||||
|
t.contains(r, "path=/echo/new", "the origin sees the rewritten request")
|
||||||
Loading…
Reference in New Issue
Block a user