mirror of
https://github.com/3proxy/3proxy.git
synced 2026-10-17 08:55:49 +08:00
Temporary Windows close behaviour test
This commit is contained in:
parent
fa95270d7c
commit
0d064d111b
280
.github/wintest/close_test.py
vendored
Normal file
280
.github/wintest/close_test.py
vendored
Normal file
@ -0,0 +1,280 @@
|
||||
"""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()
|
||||
81
.github/wintest/rsttest.py
vendored
Normal file
81
.github/wintest/rsttest.py
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
"""Does closing with unread data reset the connection, and is the reply lost?
|
||||
|
||||
The server reads only part of a large upload, answers, and closes in one of
|
||||
three ways while the client is still sending. The client reports how much of
|
||||
the reply arrived and what errors it saw.
|
||||
"""
|
||||
import socket, sys, threading, time
|
||||
|
||||
REPLY = b"R" * 65536
|
||||
UPLOAD = 1500000
|
||||
|
||||
|
||||
def server(lsock, variant):
|
||||
c, _ = lsock.accept()
|
||||
got = 0
|
||||
while got < 16384:
|
||||
d = c.recv(16384 - got)
|
||||
if not d:
|
||||
break
|
||||
got += len(d)
|
||||
c.sendall(REPLY)
|
||||
if variant == "close":
|
||||
c.close()
|
||||
elif variant == "shutrdwr":
|
||||
c.shutdown(socket.SHUT_RDWR)
|
||||
c.close()
|
||||
elif variant == "drain":
|
||||
c.shutdown(socket.SHUT_WR)
|
||||
c.settimeout(5)
|
||||
try:
|
||||
while c.recv(65536):
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
c.close()
|
||||
|
||||
|
||||
def run(variant):
|
||||
lsock = socket.socket()
|
||||
lsock.bind(("127.0.0.1", 0))
|
||||
lsock.listen(1)
|
||||
port = lsock.getsockname()[1]
|
||||
t = threading.Thread(target=server, args=(lsock, variant))
|
||||
t.start()
|
||||
cl = socket.create_connection(("127.0.0.1", port))
|
||||
errs = {}
|
||||
|
||||
def send():
|
||||
try:
|
||||
cl.sendall(b"x" * UPLOAD)
|
||||
cl.shutdown(socket.SHUT_WR)
|
||||
except OSError as e:
|
||||
errs["send"] = type(e).__name__
|
||||
|
||||
st = threading.Thread(target=send)
|
||||
st.start()
|
||||
# read slowly at first, as a client busy uploading would
|
||||
time.sleep(0.3)
|
||||
rx = 0
|
||||
cl.settimeout(5)
|
||||
try:
|
||||
while True:
|
||||
d = cl.recv(65536)
|
||||
if not d:
|
||||
break
|
||||
rx += len(d)
|
||||
except OSError as e:
|
||||
errs["recv"] = type(e).__name__
|
||||
st.join()
|
||||
t.join()
|
||||
cl.close()
|
||||
lsock.close()
|
||||
return rx, errs
|
||||
|
||||
|
||||
for variant in sys.argv[1:] or ["close", "shutrdwr", "drain"]:
|
||||
results = [run(variant) for _ in range(5)]
|
||||
full = sum(1 for rx, _ in results if rx == len(REPLY))
|
||||
errs = sorted({k + ":" + v for _, e in results for k, v in e.items()})
|
||||
print("%-9s reply complete %d/5, received %s, errors %s"
|
||||
% (variant, full, [rx for rx, _ in results], errs or "none"))
|
||||
43
.github/workflows/windows-close-test.yml
vendored
Normal file
43
.github/workflows/windows-close-test.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
# Temporary: compares close behaviour of this branch with its base on Windows.
|
||||
name: Windows close test (temporary)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'test/windows-close' ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
close:
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: install Windows libraries
|
||||
run: vcpkg install pcre2:x64-windows wolfssl:x64-windows-static
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
- name: build the change and its base (MSVC)
|
||||
shell: cmd
|
||||
run: |
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cd /d "%GITHUB_WORKSPACE%"
|
||||
set "LIB=%LIB%;c:/vcpkg/installed/x64-windows-static/lib;c:/vcpkg/installed/x64-windows/lib"
|
||||
set "INCLUDE=%INCLUDE%;c:/vcpkg/installed/x64-windows-static/include;c:/vcpkg/installed/x64-windows/include"
|
||||
nmake /F Makefile.msvc WOLFSSL=1 || exit /b 1
|
||||
git worktree add basesrc HEAD~2 || exit /b 1
|
||||
cd basesrc
|
||||
nmake /F Makefile.msvc WOLFSSL=1 || exit /b 1
|
||||
- name: socket close semantics (no proxy)
|
||||
run: python .github/wintest/rsttest.py
|
||||
- name: close behaviour, base
|
||||
run: |
|
||||
$env:PATH = "c:\vcpkg\installed\x64-windows\bin;$env:PATH"
|
||||
python .github/wintest/close_test.py basesrc\bin\3proxy.exe base
|
||||
- name: close behaviour, change
|
||||
run: |
|
||||
$env:PATH = "c:\vcpkg\installed\x64-windows\bin;$env:PATH"
|
||||
python .github/wintest/close_test.py bin\3proxy.exe change
|
||||
Loading…
Reference in New Issue
Block a user