mirror of
https://github.com/3proxy/3proxy.git
synced 2026-11-07 02:05:49 +08:00
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
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.
219 lines
8.3 KiB
Python
219 lines
8.3 KiB
Python
"""Logs: one for each log string, shared by every service naming it, kept
|
|
over a reload while something uses it and closed once nothing does. A log
|
|
which cannot be written passes its lines to standard error."""
|
|
|
|
import os
|
|
import time
|
|
|
|
|
|
def _read(path):
|
|
try:
|
|
with open(path, "rb") as fp:
|
|
return fp.read().decode("utf-8", "replace")
|
|
except OSError:
|
|
return ""
|
|
|
|
|
|
def _proxied(path, mark):
|
|
"""Whether a proxy, not the origin, logged the request marked so."""
|
|
return any(line.startswith("LOG PROXY") and mark in line
|
|
for line in _read(path).splitlines())
|
|
|
|
|
|
def _wait(cond, timeout=10.0):
|
|
deadline = time.time() + timeout
|
|
while time.time() < deadline:
|
|
if cond():
|
|
return True
|
|
time.sleep(0.05)
|
|
return cond()
|
|
|
|
|
|
def _opened(pid, path):
|
|
"""How many times the process has the file open, None without /proc."""
|
|
fds = f"/proc/{pid}/fd"
|
|
if not os.path.isdir(fds):
|
|
return None
|
|
n = 0
|
|
for fd in os.listdir(fds):
|
|
try:
|
|
if os.readlink(os.path.join(fds, fd)) == path:
|
|
n += 1
|
|
except OSError:
|
|
pass
|
|
return n
|
|
|
|
|
|
def _stalled(t):
|
|
"""A log nobody reads: a FIFO whose pipe is full. Clients are not held
|
|
up, the lines past the queue are dropped and counted, and lines queued
|
|
for a service a reload removed are still written as it formatted them."""
|
|
fifo = os.path.join(t.tmpdir, "logs-fifo")
|
|
os.mkfifo(fifo)
|
|
# opened for reading before 3proxy opens it for writing, then not read
|
|
rfd = os.open(fifo, os.O_RDONLY | os.O_NONBLOCK)
|
|
origin, port, adm = t.free_port(), t.free_port(), t.free_port()
|
|
|
|
# lines of 2 KB, made so by the service's format: the pipe's 64 KB fill
|
|
# before the requests are done
|
|
pad = "z" * 2000
|
|
|
|
def conf(proxyline):
|
|
return f"""
|
|
log
|
|
maxlogqueue 8
|
|
auth iponly
|
|
allow *
|
|
http data * /data
|
|
httpsrv -p{origin}
|
|
flush
|
|
admin -p{adm}
|
|
flush
|
|
logformat "LQ {pad} %T"
|
|
auth iponly
|
|
allow *
|
|
{proxyline}
|
|
"""
|
|
|
|
server = t.start("logs_stall", conf(f'proxy -p{port} -l{fifo}'),
|
|
ports=[origin, port, adm])
|
|
began = time.time()
|
|
ok = 0
|
|
for i in range(120):
|
|
r = t.http(f"http://127.0.0.1:{origin}/data?size=10&n{i:04d}",
|
|
proxy=f"127.0.0.1:{port}")
|
|
ok += r.status == 200
|
|
t.eq(120, ok, "a log nobody reads holds no client up")
|
|
t.eq(True, time.time() - began < 60, "and they are not slowed to its pace")
|
|
# the service goes: its lines still queued are written as it made them
|
|
t.write_config("logs_stall", conf("flush"))
|
|
t.http(f"http://127.0.0.1:{adm}/R")
|
|
got = b""
|
|
deadline = time.time() + 15
|
|
while time.time() < deadline and "has its queue full" not in server.output():
|
|
try:
|
|
chunk = os.read(rfd, 65536)
|
|
except BlockingIOError:
|
|
chunk = b""
|
|
got += chunk
|
|
if not chunk:
|
|
time.sleep(0.05)
|
|
t.eq(True, "has its queue full" in server.output(),
|
|
"the lines past its queue are dropped, and said so")
|
|
# what is left in the pipe and the queue, read to the end
|
|
deadline = time.time() + 5
|
|
while time.time() < deadline:
|
|
try:
|
|
chunk = os.read(rfd, 65536)
|
|
except BlockingIOError:
|
|
chunk = b""
|
|
if chunk:
|
|
got += chunk
|
|
deadline = time.time() + 1
|
|
else:
|
|
time.sleep(0.05)
|
|
os.close(rfd)
|
|
lines = got.decode("utf-8", "replace").splitlines()
|
|
t.eq(True, 0 < len(lines) < 120, "some lines are written, not all")
|
|
t.eq(True, all(l.startswith("Q " + pad) for l in lines),
|
|
"each as its service formats it, the service gone or not")
|
|
|
|
|
|
def config(origin, p1, p2, p3, adm, main, shared=None):
|
|
"""p2 and p3 log to shared with -l, if it is given, or are not there."""
|
|
lines = [f"log {main}", 'logformat "LLOG %N %T"', "auth iponly", "allow *",
|
|
"http data * /data", f"httpsrv -p{origin}", f"proxy -p{p1}"]
|
|
if shared:
|
|
lines += [f"proxy -p{p2} -l{shared}", f"proxy -p{p3} -l{shared}"]
|
|
lines.append(f"admin -p{adm}")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def run(t):
|
|
if not t.skip_unless("httpsrv"):
|
|
return
|
|
origin, p1, p2, p3, adm = (t.free_port() for _ in range(5))
|
|
first = os.path.join(t.tmpdir, "logs-first.log")
|
|
second = os.path.join(t.tmpdir, "logs-second.log")
|
|
shared = os.path.join(t.tmpdir, "logs-shared.log")
|
|
|
|
server = t.start("logs", config(origin, p1, p2, p3, adm, first, shared),
|
|
ports=[origin, p1, p2, p3, adm])
|
|
pid = server.proc.pid
|
|
|
|
def get(port, mark):
|
|
return t.http(f"http://127.0.0.1:{origin}/data?size=10&{mark}",
|
|
proxy=f"127.0.0.1:{port}").status
|
|
|
|
# --- the log command and -l ----------------------------------------
|
|
t.eq(200, get(p1, "one"), "a request through the first proxy")
|
|
t.eq(200, get(p2, "two"), "one through the second")
|
|
t.eq(200, get(p3, "three"), "one through the third")
|
|
t.eq(True, _wait(lambda: _proxied(first, "one")), "the log command's file gets its line")
|
|
t.eq(True, _wait(lambda: _proxied(shared, "two") and _proxied(shared, "three")),
|
|
"the two services with the same -l write to the same file")
|
|
t.eq(False, _proxied(first, "two"), "and not to the log command's")
|
|
t.eq(False, _proxied(shared, "one"), "nor the other way round")
|
|
n = _opened(pid, shared)
|
|
if n is not None:
|
|
t.eq(1, n, "the file named by both is opened once")
|
|
t.eq(1, _opened(pid, first), "the log command's file is open")
|
|
|
|
def reload(main, until):
|
|
t.write_config("logs", config(origin, p1, p2, p3, adm, main))
|
|
t.http(f"http://127.0.0.1:{adm}/R")
|
|
return _wait(until)
|
|
|
|
# --- a reload to another file: the old one is closed once unused -----
|
|
t.eq(True, reload(second, lambda: "Accepting connections" in _read(second)),
|
|
"the new configuration logs to the new file")
|
|
t.eq(200, get(p1, "four"), "a request after the reload")
|
|
t.eq(True, _wait(lambda: _proxied(second, "four")), "goes to the new file")
|
|
t.eq(False, _proxied(first, "four"), "not to the old one")
|
|
if n is not None:
|
|
t.eq(True, _wait(lambda: _opened(pid, first) == 0),
|
|
"the old file is closed once its services are gone")
|
|
t.eq(True, _wait(lambda: _opened(pid, shared) == 0),
|
|
"and so is the file of -l nobody names any more")
|
|
t.eq(1, _opened(pid, second), "the new one is open")
|
|
|
|
# --- the same file, rotated daily: reopened under the day's name -----
|
|
dated = second + "." + time.strftime("%Y.%m.%d")
|
|
t.eq(True, reload(second + " D", lambda: os.path.exists(dated)),
|
|
"a rotation given to the log reopens it under the dated name")
|
|
t.eq(200, get(p1, "five"), "a request after that")
|
|
t.eq(True, _wait(lambda: _proxied(dated, "five")), "goes to the dated file")
|
|
t.eq(False, _proxied(second, "five"), "not to the undated one")
|
|
|
|
# --- a log which fails: its lines dropped, and said so -----------------
|
|
if os.path.exists("/dev/full"):
|
|
o2, q = t.free_port(), t.free_port()
|
|
full = t.start("logs_full", f"""
|
|
log /dev/full
|
|
logformat "LLOG %N %T"
|
|
auth iponly
|
|
allow *
|
|
http data * /data
|
|
httpsrv -p{o2}
|
|
proxy -p{q}
|
|
""", ports=[o2, q])
|
|
# a buffer's worth, at least, before writing it fails
|
|
for i in range(80):
|
|
t.http(f"http://127.0.0.1:{o2}/data?size=10&{'x' * 40}{i}",
|
|
proxy=f"127.0.0.1:{q}")
|
|
t.eq(True, _wait(lambda: "Log /dev/full could not be written" in full.output()),
|
|
"a log a full device refuses says so on standard error")
|
|
t.eq(True, "records dropped" in full.output(), "with the records it dropped")
|
|
t.eq(False, "LOG PROXY" in full.output(), "its lines are not written elsewhere")
|
|
|
|
# --- a log which stalls: clients go on, the queue is bounded ----------
|
|
if hasattr(os, "mkfifo"):
|
|
_stalled(t)
|
|
|
|
# --- a log which cannot be opened is an error in the configuration ---
|
|
out = t.run_config("logs_bad", f"""
|
|
log {os.path.join(t.tmpdir, "no", "such", "dir", "x.log")}
|
|
proxy -p{t.free_port()}
|
|
""")
|
|
t.contains(out, "x.log", "a log which cannot be opened is named")
|