Initializare dupa curatare folder
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
"""End-to-end test: a real tap must be claimable by a web page at any time.
|
||||
|
||||
Simulates the RDM6300 on a pseudo-terminal, starts the actual bridge the way
|
||||
systemd would, then walks the exact flow a web page performs:
|
||||
|
||||
card placed -> page loads -> polls /api/card -> claims it -> hand over once
|
||||
|
||||
Also asserts the two things that matter for security: the tap survives until
|
||||
claimed, and it cannot be claimed twice.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pty
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT = os.path.dirname(HERE)
|
||||
VENV_PY = os.path.join(PROJECT, ".venv", "bin", "python")
|
||||
PYTHON = VENV_PY if os.path.exists(VENV_PY) else sys.executable
|
||||
|
||||
CARD_VALUE = 0x00C4A0B5
|
||||
EXPECTED_ID = 12886197
|
||||
|
||||
|
||||
def em4100_frame(value, card_type=0x0F):
|
||||
"""12 hex nibbles: type + 4 id bytes + XOR checksum of those 5 bytes."""
|
||||
id_bytes = struct.pack(">I", value)
|
||||
checksum = card_type
|
||||
for b in id_bytes:
|
||||
checksum ^= b
|
||||
frame = (bytes([card_type]) + id_bytes + bytes([checksum])).hex().upper()
|
||||
assert len(frame) == 12, frame
|
||||
return frame.encode("ascii")
|
||||
|
||||
|
||||
def main():
|
||||
master, slave = pty.openpty()
|
||||
serial_port = os.ttyname(slave)
|
||||
|
||||
# Port 0 would be cleaner but the CLI needs a real number; pick a free one.
|
||||
import socket
|
||||
probe = socket.socket()
|
||||
probe.bind(("127.0.0.1", 0))
|
||||
http_port = probe.getsockname()[1]
|
||||
probe.close()
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[PYTHON, "-m", "rdm_card_reader", "--serve",
|
||||
"--port", serial_port, "--bridge-port", str(http_port),
|
||||
"--mode", "login", "--tap-ttl", "15",
|
||||
"--allow-origin", "https://kiosk.example"],
|
||||
cwd=PROJECT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, bufsize=1, start_new_session=True,
|
||||
)
|
||||
|
||||
output = []
|
||||
lock = threading.Lock()
|
||||
|
||||
def pump():
|
||||
for line in proc.stdout:
|
||||
with lock:
|
||||
output.append(line.rstrip())
|
||||
|
||||
threading.Thread(target=pump, daemon=True).start()
|
||||
|
||||
base = "http://127.0.0.1:{}".format(http_port)
|
||||
results = {}
|
||||
|
||||
def api(path, method="GET", headers=None):
|
||||
request = urllib.request.Request(
|
||||
base + path, method=method,
|
||||
headers=headers or {},
|
||||
data=b"" if method == "POST" else None,
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=5) as response:
|
||||
body = response.read()
|
||||
return response.status, (json.loads(body) if body else None)
|
||||
|
||||
try:
|
||||
# Wait for the HTTP server to come up.
|
||||
deadline = time.time() + 15
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
api("/api/health")
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.3)
|
||||
|
||||
results["bridge started"] = True
|
||||
|
||||
# The child needs a moment to open the serial port; frames written
|
||||
# before that are discarded, which would look like a reader bug.
|
||||
time.sleep(2.0)
|
||||
|
||||
# --- no card yet -------------------------------------------------
|
||||
_, state = api("/api/card")
|
||||
results["no card -> nothing claimable"] = state["claimable"] is False
|
||||
|
||||
# --- a single tap, as a person would -------------------------------
|
||||
frame = em4100_frame(CARD_VALUE)
|
||||
os.write(master, b"\x02" + frame + b"\x03")
|
||||
time.sleep(0.3)
|
||||
|
||||
# --- a page loads later and polls; the card must still be there ----
|
||||
time.sleep(1.0)
|
||||
_, state = api("/api/card")
|
||||
with lock:
|
||||
print("state after tap: {}".format(json.dumps(state)), flush=True)
|
||||
for line in output[-12:]:
|
||||
print(" " + line)
|
||||
results["card still available later"] = bool(
|
||||
state["claimable"] and state["card"]["id"] == EXPECTED_ID
|
||||
)
|
||||
results["card not consumed yet"] = state["consumed"] is False
|
||||
|
||||
# --- claim it, as the web page does -------------------------------
|
||||
status, claimed = api(
|
||||
"/api/card/claim", method="POST",
|
||||
headers={"Origin": "https://kiosk.example"},
|
||||
)
|
||||
results["claim returns 200"] = status == 200
|
||||
results["claim gives the right card"] = claimed["card"]["id"] == EXPECTED_ID
|
||||
|
||||
# --- single use ---------------------------------------------------
|
||||
try:
|
||||
api("/api/card/claim", method="POST",
|
||||
headers={"Origin": "https://kiosk.example"})
|
||||
results["second claim refused"] = False
|
||||
except urllib.error.HTTPError as exc:
|
||||
results["second claim refused"] = exc.code == 409
|
||||
|
||||
# --- a foreign origin cannot grab it either ------------------------
|
||||
_, state = api("/api/card")
|
||||
results["card marked consumed"] = state["consumed"] is True
|
||||
|
||||
finally:
|
||||
try:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGINT)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
proc.wait(timeout=8)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
try:
|
||||
os.close(master)
|
||||
os.close(slave)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with lock:
|
||||
lines = list(output)
|
||||
|
||||
print("======== bridge output ========")
|
||||
for line in lines[:14]:
|
||||
print(line)
|
||||
print("===============================")
|
||||
print()
|
||||
|
||||
ok = True
|
||||
for name, passed in results.items():
|
||||
print(" [{}] {}".format("PASS" if passed else "FAIL", name))
|
||||
ok = ok and bool(passed)
|
||||
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user