"""Mock plex.tv + Plex Media Server for exercising Minamp's networking code on a desktop JVM. Serves: https://plex.tv:443 pins, user, resources https://127-0-0-1.abc123.plex.direct:32443 PMS over TLS (wildcard cert, ECDSA leaf, RSA root) http://127.0.0.1:32400 the same PMS in plain HTTP """ import datetime import json import os import ssl import sys import threading import time from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlparse, parse_qs from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.x509.oid import NameOID OUT = sys.argv[1] os.makedirs(OUT, exist_ok=True) NOW = datetime.datetime.now(datetime.timezone.utc) def name(cn): return x509.Name([x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Minamp Test"), x509.NameAttribute(NameOID.COMMON_NAME, cn)]) def cert(subject, issuer, pub, signer, hashalg, ca, sans=None): b = (x509.CertificateBuilder().subject_name(subject).issuer_name(issuer).public_key(pub) .serial_number(x509.random_serial_number()) .not_valid_before(NOW - datetime.timedelta(days=1)).not_valid_after(NOW + datetime.timedelta(days=30)) .add_extension(x509.BasicConstraints(ca=ca, path_length=None), critical=True)) if sans: b = b.add_extension(x509.SubjectAlternativeName([x509.DNSName(s) for s in sans]), critical=False) return b.sign(signer, hashalg) root_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) root = cert(name("Test Root"), name("Test Root"), root_key.public_key(), root_key, hashes.SHA256(), True) inter_key = ec.generate_private_key(ec.SECP384R1()) inter = cert(name("Test ECC Intermediate"), root.subject, inter_key.public_key(), root_key, hashes.SHA256(), True) pms_key = ec.generate_private_key(ec.SECP256R1()) pms = cert(name("*.abc123.plex.direct"), inter.subject, pms_key.public_key(), inter_key, hashes.SHA384(), False, ["*.abc123.plex.direct"]) tv_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) tv = cert(name("plex.tv"), inter.subject, tv_key.public_key(), inter_key, hashes.SHA384(), False, ["plex.tv", "*.plex.tv"]) pem = lambda c: c.public_bytes(serialization.Encoding.PEM) keypem = lambda k: k.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()) open(os.path.join(OUT, "root.pem"), "wb").write(pem(root)) for label, c, k in (("pms", pms, pms_key), ("tv", tv, tv_key)): open(os.path.join(OUT, label + "-chain.pem"), "wb").write(pem(c) + pem(inter)) open(os.path.join(OUT, label + "-key.pem"), "wb").write(keypem(k)) MACHINE = "machine-abc123" TOKEN = "server-token-xyz" ACCOUNT = "account-token-123" pins = {} log = [] TRACKS = [ {"type": "track", "ratingKey": "101", "key": "/library/metadata/101", "title": "Song One", "grandparentTitle": "The Testers", "parentTitle": "First Album", "parentThumb": "/library/metadata/10/thumb/1", "parentRatingKey": "10", "grandparentRatingKey": "1", "index": 1, "duration": 200000, "userRating": 10, "Media": [{"audioCodec": "mp3", "container": "mp3", "bitrate": 192, "Part": [{"key": "/library/parts/501/1/file.mp3", "size": 300000}]}]}, {"type": "track", "ratingKey": "102", "key": "/library/metadata/102", "title": "Song Two (FLAC)", "grandparentTitle": "The Testers", "parentTitle": "First Album", "parentThumb": "/library/metadata/10/thumb/1", "parentRatingKey": "10", "grandparentRatingKey": "1", "index": 2, "duration": 180000, "Media": [{"audioCodec": "flac", "container": "flac", "bitrate": 900, "Part": [{"key": "/library/parts/502/1/file.flac", "size": 9000000}]}]}, ] MP3 = bytes((i * 7) % 256 for i in range(300000)) class H(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def log_message(self, *a): pass def send_json(self, obj, code=200): body = json.dumps(obj).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_POST(self): self.route("POST") def do_PUT(self): self.route("PUT") def do_GET(self): self.route("GET") def do_HEAD(self): self.route("HEAD") def route(self, method): n = int(self.headers.get("Content-Length") or 0) if n: self.rfile.read(n) u = urlparse(self.path) q = parse_qs(u.query) p = u.path host = self.headers.get("Host", "") tok = self.headers.get("X-Plex-Token") or (q.get("X-Plex-Token") or [None])[0] log.append((method, host, self.path, tok, self.headers.get("Range"))) if host.startswith("plex.tv"): return self.tv(method, p, tok) if p == "/identity": return self.send_json({"MediaContainer": {"machineIdentifier": MACHINE, "version": "1.40"}}) if tok != TOKEN: return self.send_json({"error": "unauthorized"}, 401) if p == "/library/sections": return self.send_json({"MediaContainer": {"Directory": [ {"key": "1", "type": "artist", "title": "Music"}, {"key": "2", "type": "movie", "title": "Films"}]}}) if p == "/library/sections/1/all": t = q.get("type", ["8"])[0] if t == "8": start = int(q.get("X-Plex-Container-Start", ["0"])[0]) size = int(q.get("X-Plex-Container-Size", ["50"])[0]) arts = [{"type": "artist", "ratingKey": str(1000 + i), "title": "Artist %03d" % i, "thumb": "/a/%d" % i} for i in range(250)] return self.send_json({"MediaContainer": {"totalSize": 250, "offset": start, "Metadata": arts[start:start + size]}}) if t == "10": if "userRating>>" in q or any(k.endswith(">") for k in q): return self.send_json({"MediaContainer": {"Metadata": TRACKS[:1], "filterSeen": list(q.keys())}}) return self.send_json({"MediaContainer": {"Metadata": TRACKS}}) return self.send_json({"MediaContainer": {"Metadata": [{"type": "album", "ratingKey": "10", "title": "First Album", "parentTitle": "The Testers", "year": 1999, "thumb": "/library/metadata/10/thumb/1"}]}}) if p == "/library/metadata/10/children": return self.send_json({"MediaContainer": {"Metadata": TRACKS}}) if p == "/hubs/search": return self.send_json({"MediaContainer": {"Hub": [ {"type": "artist", "title": "Artists", "Directory": [{"type": "artist", "ratingKey": "1", "title": "The Testers"}]}, {"type": "track", "title": "Tracks", "Metadata": TRACKS[:1]}]}}) if p.startswith("/library/parts/501"): return self.file(method, MP3) if p in ("/music/:/transcode/universal/start.mp3", "/audio/:/transcode/universal/start.mp3"): extra = (q.get("X-Plex-Client-Profile-Extra") or [""])[0] if "scopeName=*" in extra: # pretend this server rejects that variant body = b"