mirror of
https://github.com/3proxy/3proxy.git
synced 2026-10-17 08:55:49 +08:00
UDP sessions check for a reload in udpsockmap() as TCP connections do in sockmap(): unless noforce, a session goes by the newest service on its listener, re-authenticated as its client, or is dropped. A request on a connection that outlives a reload, an HTTP one kept alive, is authorized by clientauth(): it moves the connection to the newest service on its listener first if a reload came since, then authorizes as the service whose rules apply (PSRV). The proxy uses it for every request, so a request of a user the new configuration removed is answered 407 rather than authorized by the old rules and cut while relayed. reattach() takes whether to authenticate: sockmap() and udpsockmap() move and authenticate at once, clientauth() moves and then authorizes the request. A test case covers the drops: with one slot free in the new service one of two downloads moves on and the other is cut (27; a service keeps one of its maxconn slots for the next connection it accepts), and a proxy download is cut when a socks service takes its port (26). The reload case checks the kept-alive request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
5.1 KiB
Python
126 lines
5.1 KiB
Python
"""Reloading the configuration: a file with an error leaves the configuration
|
|
in force, services and users, as it was; a good one replaces it, while the
|
|
connections of the old services finish undisturbed."""
|
|
|
|
import threading
|
|
import time
|
|
|
|
|
|
def config(origin, proxy, adm, user, extra=""):
|
|
return f"""
|
|
log
|
|
auth iponly
|
|
allow *
|
|
http data * /data
|
|
httpsrv -p{origin}
|
|
|
|
flush
|
|
users {user}
|
|
auth strong
|
|
allow *
|
|
proxy -p{proxy}
|
|
|
|
flush
|
|
auth iponly
|
|
allow *
|
|
admin -p{adm}
|
|
{extra}
|
|
"""
|
|
|
|
|
|
def run(t):
|
|
origin = t.free_port()
|
|
first = t.free_port()
|
|
second = t.free_port()
|
|
adm = t.free_port()
|
|
|
|
server = t.start("reload", config(origin, first, adm, "alice:CL:one"),
|
|
ports=[origin, first, adm])
|
|
url = f"http://127.0.0.1:{origin}/data?size=1000"
|
|
proxy = f"127.0.0.1:{first}"
|
|
|
|
def as_user(user, password, through=proxy):
|
|
return t.http(url, proxy=through, proxy_auth=(user, password)).status
|
|
|
|
t.eq(200, as_user("alice", "one"), "the first configuration lets alice in")
|
|
|
|
def reload_and_wait(expect_failure=False):
|
|
before = server.output()
|
|
accepting = before.count("Accepting connections")
|
|
t.http(f"http://127.0.0.1:{adm}/R")
|
|
deadline = time.time() + 10
|
|
while time.time() < deadline:
|
|
out = server.output()
|
|
if expect_failure and "Unknown command" in out[len(before):]:
|
|
return out
|
|
if not expect_failure and out.count("Accepting connections") >= accepting + 3:
|
|
return out
|
|
time.sleep(0.05)
|
|
return server.output()
|
|
|
|
# --- a file with an error: nothing changes --------------------------
|
|
t.write_config("reload", config(origin, first, adm, "bob:CL:two", "nosuchcommand"))
|
|
out = reload_and_wait(expect_failure=True)
|
|
t.contains(out, "Unknown command", "the broken configuration is refused")
|
|
time.sleep(1.5) # what a switch would take: the old services stop within a second
|
|
t.eq(200, as_user("alice", "one"), "after a failed reload alice still gets in")
|
|
t.ne(200, as_user("bob", "two"), "and bob, from the refused file, does not")
|
|
|
|
# --- a good file: the new configuration takes over, and the connections
|
|
# of the old services go by its rules from then on ---------------------
|
|
def download(result):
|
|
# about two seconds: 2 MB in 64 KB blocks, 60 ms apart
|
|
r = t.http(f"http://127.0.0.1:{origin}/data?size=2000000&block=65536&delay=60",
|
|
proxy=proxy, proxy_auth=("alice", "one"))
|
|
result["status"], result["length"] = r.status, r.length
|
|
|
|
def during_reload(users, port):
|
|
result = {}
|
|
th = threading.Thread(target=download, args=(result,))
|
|
th.start()
|
|
time.sleep(0.3)
|
|
t.write_config("reload", config(origin, port, adm, users))
|
|
reload_and_wait()
|
|
return th, result
|
|
|
|
# alice stays: her download goes on under the new service
|
|
th, kept = during_reload("alice:CL:one bob:CL:two", first)
|
|
t.eq(200, as_user("bob", "two"), "the new configuration lets bob in")
|
|
th.join(20)
|
|
t.eq(2000000, kept.get("length"),
|
|
"a download of a user the new configuration keeps finishes whole")
|
|
|
|
# a connection kept alive: each request is authorized by the rules in
|
|
# force, so alice's next request after she is removed is refused
|
|
keep = t.connection("127.0.0.1", origin, proxy=proxy)
|
|
t.eq(200, t.http(url, proxy=proxy, proxy_auth=("alice", "one"), conn=keep).status,
|
|
"a kept-alive connection serves alice")
|
|
t.write_config("reload", config(origin, first, adm, "bob:CL:two"))
|
|
reload_and_wait()
|
|
r = t.http(url, proxy=proxy, proxy_auth=("alice", "one"), conn=keep)
|
|
t.eq(407, r.status, "after she is removed, her next request on it is refused "
|
|
"with a request for credentials, authorized by the new rules")
|
|
t.write_config("reload", config(origin, first, adm, "alice:CL:one bob:CL:two"))
|
|
reload_and_wait()
|
|
|
|
# alice is removed: her download is cut, being re-authenticated
|
|
th, cut = during_reload("bob:CL:two", first)
|
|
th.join(20)
|
|
t.ok("a download of a user the new configuration removes is cut") \
|
|
if cut.get("length", 0) < 2000000 else \
|
|
t.fail("a download of a user the new configuration removes is cut",
|
|
"less than 2000000 bytes", cut.get("length"))
|
|
t.ne(200, as_user("alice", "one"), "and alice cannot come back")
|
|
|
|
# the service moves to another port: the old one closes
|
|
t.write_config("reload", config(origin, second, adm, "bob:CL:two"))
|
|
reload_and_wait()
|
|
t.ok("the moved service listens on its new port") if t.wait_port(second) else \
|
|
t.fail("the moved service listens on its new port", f"port {second} open", "closed")
|
|
t.eq(200, as_user("bob", "two", f"127.0.0.1:{second}"), "bob gets in there")
|
|
deadline = time.time() + 5
|
|
while time.time() < deadline and t.wait_port(first, timeout=0.2):
|
|
time.sleep(0.2)
|
|
t.ok("the old port is closed") if not t.wait_port(first, timeout=0.2) else \
|
|
t.fail("the old port is closed", f"port {first} closed", "open")
|