Handle interrupted pairing, artwork retries and revoked access reliably
This commit is contained in:
@@ -61,7 +61,7 @@ for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridg
|
||||
proxy = add("test-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1, remoteGlobalIDString=uid("MusicBridgetarget"), remoteInfo="MusicBridge")
|
||||
deps = [add("test-dependency", "PBXTargetDependency", target=uid("MusicBridgetarget"), targetProxy=proxy)]
|
||||
else:
|
||||
settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="3", MARKETING_VERSION="0.2.0")
|
||||
settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="4", MARKETING_VERSION="0.3.0")
|
||||
targets.append(add(name + "target", "PBXNativeTarget", name=name, productName=name, productReference=product,
|
||||
productType="com.apple.product-type.bundle.unit-test" if is_test else "com.apple.product-type.application",
|
||||
buildConfigurationList=configurations(name, settings), buildPhases=phases, buildRules=[], dependencies=deps))
|
||||
|
||||
+83
-9
@@ -11,6 +11,8 @@ import ssl
|
||||
import struct
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import zlib
|
||||
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
work = root / "build/tls"
|
||||
@@ -34,12 +36,83 @@ extendedKeyUsage = serverAuth
|
||||
subjectAltName = DNS:localhost,IP:127.0.0.1
|
||||
""")
|
||||
state = json.loads((root / "MusicBridgeTests/state-v1.json").read_text())
|
||||
counts = {}
|
||||
counts_lock = threading.Lock()
|
||||
|
||||
|
||||
def count(token, path):
|
||||
with counts_lock:
|
||||
key = (token, path)
|
||||
counts[key] = counts.get(key, 0) + 1
|
||||
return counts[key]
|
||||
|
||||
|
||||
def png(size):
|
||||
def chunk(kind, data):
|
||||
return struct.pack("!I", len(data)) + kind + data + struct.pack("!I", zlib.crc32(kind + data))
|
||||
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack("!IIBBBBB", size, size, 8, 2, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress((b"\x00" + b"\x00\x80\xff" * size) * size)) + chunk(b"IEND", b""))
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def reply(self, status, body):
|
||||
payload = json.dumps(body).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def snapshot(self, token):
|
||||
result = dict(state)
|
||||
result["artworkId"] = "A" * 64 if token in ("artwork", "artwork-fail", "late-art") else None
|
||||
return result
|
||||
|
||||
def do_POST(self):
|
||||
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", "0"))))
|
||||
if self.path == "/v1/pair":
|
||||
code = body.get("code")
|
||||
if code == "33333333":
|
||||
time.sleep(0.5)
|
||||
self.reply(200, {"success": True, "code": "ok", "token": "A" * 64})
|
||||
else:
|
||||
error = "pairing_closed" if code == "22222222" else "invalid_code"
|
||||
self.reply(403, {"success": False, "code": error})
|
||||
else:
|
||||
self.reply(200, {"success": False, "code": "unsupported", "message": "Плеер не поддерживает эту команду."})
|
||||
|
||||
def do_GET(self):
|
||||
token = self.headers.get("Authorization", "").removeprefix("Bearer ")
|
||||
number = count(token, self.path)
|
||||
if token == "revoked":
|
||||
self.reply(401, {})
|
||||
return
|
||||
if token == "redirect" and self.path == "/v1/state":
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/redirect-target")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
return
|
||||
if token == "recover" and self.path == "/v1/state" and number == 1:
|
||||
self.reply(503, {})
|
||||
return
|
||||
if self.path.startswith("/v1/artwork/"):
|
||||
if token == "artwork-fail" or (token == "artwork" and number == 1):
|
||||
self.reply(404, {})
|
||||
return
|
||||
size = 1
|
||||
if token == "late-art":
|
||||
if self.path.endswith("A" * 64): time.sleep(0.5)
|
||||
else: size = 2
|
||||
payload = png(size)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "image/png")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
return
|
||||
if self.path == "/v1/events":
|
||||
accept = base64.b64encode(hashlib.sha1(
|
||||
(self.headers["Sec-WebSocket-Key"] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest()).decode()
|
||||
@@ -48,16 +121,17 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_header("Connection", "Upgrade")
|
||||
self.send_header("Sec-WebSocket-Accept", accept)
|
||||
self.end_headers()
|
||||
payload = json.dumps({"type": "state", "state": state}).encode()
|
||||
self.wfile.write(b"\x81\x7e" + struct.pack("!H", len(payload)) + payload)
|
||||
self.wfile.flush()
|
||||
snapshot = self.snapshot(token)
|
||||
if token == "late-art":
|
||||
time.sleep(0.1)
|
||||
snapshot["artworkId"] = "B" * 64
|
||||
for _ in range(30 if token == "command-reject" else 1):
|
||||
payload = json.dumps({"type": "state", "state": snapshot}).encode()
|
||||
self.wfile.write(b"\x81\x7e" + struct.pack("!H", len(payload)) + payload)
|
||||
self.wfile.flush()
|
||||
if token == "command-reject": time.sleep(0.05)
|
||||
else:
|
||||
payload = json.dumps(state).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
self.reply(200, self.snapshot(token))
|
||||
|
||||
|
||||
fixtures = {}
|
||||
|
||||
Reference in New Issue
Block a user