3proxy/.github/wintest/close_test.py
2026-10-11 00:47:32 +03:00

281 lines
7.3 KiB
Python

"""Close behaviour of a 3proxy binary on Windows, for comparing two builds.
Usage: python close_test.py path\\to\\3proxy.exe label
1. upload (#638): a client sends a file through an HTTP CONNECT tunnel and
closes as soon as the last byte is handed to its socket; the server
counts what arrives.
2. download: the server sends a file and closes; the client counts it.
3. reply while uploading: the server reads part of a large upload, answers
and closes; the client counts the answer while still sending.
4. handles: the proxy's handle count before and after many connections
which close with data still queued.
5. timing: many short sequential connections through the proxy.
"""
import socket, subprocess, sys, threading, time, os, tempfile
BIN, LABEL = sys.argv[1], sys.argv[2]
FILE = os.urandom(1580872)
def free_port():
s = socket.socket()
s.bind(("127.0.0.1", 0))
p = s.getsockname()[1]
s.close()
return p
def start_proxy():
port = free_port()
d = tempfile.mkdtemp()
cfg = os.path.join(d, "3proxy.cfg")
with open(cfg, "w") as f:
f.write("auth iponly\nallow *\nproxy -p%d\n" % port)
p = subprocess.Popen([BIN, cfg])
for _ in range(100):
try:
socket.create_connection(("127.0.0.1", port), 1).close()
break
except OSError:
time.sleep(0.05)
time.sleep(0.5)
return p, port
def tunnel(pport, dport):
c = socket.create_connection(("127.0.0.1", pport), 10)
c.sendall(("CONNECT 127.0.0.1:%d HTTP/1.1\r\nHost: 127.0.0.1:%d\r\n\r\n" % (dport, dport)).encode())
head = b""
while b"\r\n\r\n" not in head:
d = c.recv(1)
if not d:
raise OSError("tunnel refused")
head += d
if b" 200" not in head.split(b"\r\n")[0]:
raise OSError("tunnel refused: %r" % head[:40])
return c
def listener():
l = socket.socket()
l.bind(("127.0.0.1", 0))
l.listen(50)
return l, l.getsockname()[1]
def handles(pid):
try:
out = subprocess.check_output(["powershell", "-NoProfile", "-Command",
"(Get-Process -Id %d).HandleCount" % pid])
return int(out.strip())
except Exception:
return -1
def upload(pport, n=20):
full = 0
sizes = []
for _ in range(n):
l, dport = listener()
got = []
def srv():
c, _ = l.accept()
c.settimeout(15)
total = 0
try:
while True:
d = c.recv(65536)
if not d:
break
total += len(d)
except OSError:
pass
got.append(total)
c.close()
t = threading.Thread(target=srv)
t.start()
try:
c = tunnel(pport, dport)
c.sendall(FILE)
c.close()
except OSError:
pass
t.join(20)
l.close()
size = got[0] if got else -1
sizes.append(size)
full += size == len(FILE)
return "upload %d/%d complete, short ones: %s" % (
full, n, [s for s in sizes if s != len(FILE)][:6])
def download(pport, n=20):
full = 0
sizes = []
for _ in range(n):
l, dport = listener()
def srv():
c, _ = l.accept()
try:
c.sendall(FILE)
except OSError:
pass
c.close()
t = threading.Thread(target=srv)
t.start()
total = 0
try:
c = tunnel(pport, dport)
c.settimeout(15)
while True:
d = c.recv(65536)
if not d:
break
total += len(d)
c.close()
except OSError:
pass
t.join(20)
l.close()
sizes.append(total)
full += total == len(FILE)
return "download %d/%d complete, short ones: %s" % (
full, n, [s for s in sizes if s != len(FILE)][:6])
def reply_while_uploading(pport, n=10):
full = 0
errs = set()
for _ in range(n):
l, dport = listener()
reply = b"R" * 65536
def srv():
c, _ = l.accept()
got = 0
while got < 16384:
d = c.recv(16384 - got)
if not d:
break
got += len(d)
c.sendall(reply)
c.shutdown(socket.SHUT_WR)
c.settimeout(10)
try:
while c.recv(65536):
pass
except OSError:
pass
c.close()
t = threading.Thread(target=srv)
t.start()
rx = 0
try:
c = tunnel(pport, dport)
def send():
try:
c.sendall(b"x" * 1500000)
c.shutdown(socket.SHUT_WR)
except OSError as e:
errs.add("send:" + type(e).__name__)
st = threading.Thread(target=send)
st.start()
c.settimeout(15)
try:
while True:
d = c.recv(65536)
if not d:
break
rx += len(d)
except OSError as e:
errs.add("recv:" + type(e).__name__)
st.join(20)
c.close()
except OSError as e:
errs.add("tunnel:" + type(e).__name__)
t.join(20)
l.close()
full += rx == len(reply)
return "reply while uploading %d/%d complete, errors %s" % (full, n, sorted(errs) or "none")
def handle_growth(p, pport, n=300):
l, dport = listener()
stop = []
def srv():
l.settimeout(1)
while not stop:
try:
c, _ = l.accept()
except OSError:
continue
# read a little and close with the rest queued and unread
try:
c.recv(1024)
except OSError:
pass
c.close()
t = threading.Thread(target=srv)
t.start()
before = handles(p.pid)
for _ in range(n):
try:
c = tunnel(pport, dport)
c.setblocking(False)
try:
c.send(b"y" * 262144)
except OSError:
pass
c.close()
except OSError:
pass
time.sleep(8)
after = handles(p.pid)
stop.append(1)
t.join(5)
l.close()
return "handles before %d, after %d connections %d" % (before, n, after)
def timing(pport, n=300):
l, dport = listener()
def srv():
for _ in range(n):
c, _ = l.accept()
d = c.recv(16)
c.sendall(d)
c.close()
t = threading.Thread(target=srv)
t.start()
t0 = time.time()
for _ in range(n):
c = tunnel(pport, dport)
c.sendall(b"ping")
c.recv(16)
c.close()
dt = time.time() - t0
t.join(10)
l.close()
return "timing %d sequential tunnels: %.2fs (%.1f ms each)" % (n, dt, dt * 1000 / n)
p, pport = start_proxy()
try:
for f in (upload, download, reply_while_uploading, timing):
print("[%s] %s" % (LABEL, f(pport)), flush=True)
print("[%s] %s" % (LABEL, handle_growth(p, pport)), flush=True)
finally:
p.kill()