// The relay a player's requests take (native-sink ยง4.4), with the reference package's floor as // the origin's presentation: its media playlist sealed by its receipt, its fragments named by // their SHA-256. What the relay presents, it presents byte for byte; a fragment changed at the // origin is answered 502 and never handed on; and the session's bundle verifies. import CryptoKit import XCTest import WaiSink final class RelayTests: XCTestCase { private func fetch(_ url: URL) -> (Int, Data) { let done = expectation(description: url.absoluteString) var out: (Int, Data) = (0, Data()) URLSession(configuration: .ephemeral).dataTask(with: url) { d, r, _ in out = ((r as? HTTPURLResponse)?.statusCode ?? 0, d ?? Data()) done.fulfill() }.resume() wait(for: [done], timeout: 20) return out } /// The signed case of the corpus, served from an origin on loopback: playlist, receipt, init /// and fragments, the third fragment changed when `tamper`. private func origin(tamper: Bool) throws -> (TestOrigin, [String: Any]) { let doc = try corpus() let objects = doc["objects"] as! [String: String] let o = TestOrigin() try o.start() let c = (doc["cases"] as! [[String: Any]]).first { $0["name"] as? String == "signed-vod" }! for call in c["calls"] as! [[String: Any]] { let d = call["do"] as! [String: Any] if d["call"] as? String == "playlist", d["receipt"] is String { let pl = hexData(objects[d["body"] as! String]!) o.put("/title/v/v.m3u8", pl, type: "application/vnd.apple.mpegurl") o.put("/title/v/" + WaiSink.receiptName(pl), hexData(objects[d["receipt"] as! String]!), type: "application/json") } if d["call"] as? String == "object", let m = d["meta"] as? [String: Any], let u = URL(string: m["url"] as! String), u.path.hasPrefix("/title/v/"), u.lastPathComponent != "v.m3u8" { var b = hexData(objects[d["body"] as! String]!) if tamper && d["body"] as? String == "f2" { b[b.count - 1] ^= 1 } o.put(u.path, b, type: u.pathExtension == "m4s" ? "video/iso.segment" : "video/mp4") } } return (o, doc) } func testTheRelayPresentsWhatVerifiesAndWithholdsWhatDoesNot() throws { for tamper in [false, true] { let (o, doc) = try origin(tamper: tamper) defer { o.stop() } let sink = try WaiSink(config: .init(channel: "film-library", owner: doc["owner"] as! String)) let relay = WaiRelay(sink: sink) try relay.start() defer { relay.stop() } let (st, pl) = fetch(relay.relayURL(for: o.url("/title/v/v.m3u8"))) XCTAssertEqual(st, 200) let text = String(decoding: pl, as: UTF8.self) let uris = text.components(separatedBy: "\n").filter { !$0.isEmpty && !$0.hasPrefix("#") } XCTAssertEqual(uris.count, 5) XCTAssertTrue(uris.allSatisfy { $0.hasPrefix("http://127.0.0.1:\(relay.port)/s/") }, "every URI points at the relay") XCTAssertTrue(text.contains("URI=\"http://127.0.0.1:\(relay.port)/s/"), "the map points at the relay") XCTAssertEqual(relay.events.first { $0.url.hasSuffix("v.m3u8") }?.listing, "signed") var refused = 0 for u in uris { let (s, b) = fetch(URL(string: u)!) let name = URL(string: u)!.lastPathComponent if s == 200 { XCTAssertEqual(SHA.hex(b) + ".m4s", name, "presented bytes are the bytes the name states") } else { XCTAssertEqual(s, 502) XCTAssertTrue(b.isEmpty, "a refused fragment is never handed on") refused += 1 } } XCTAssertEqual(refused, tamper ? 1 : 0) let ev = relay.events.filter { $0.url.hasSuffix(".m4s") } XCTAssertTrue(ev.allSatisfy { $0.listing == "signed" && $0.bound == "sha256" }) XCTAssertEqual(ev.compactMap(\.refused), tamper ? ["object_digest_mismatch"] : []) print("WAI-SINK relay\(tamper ? " (a fragment changed at the origin)" : ""): playlist \(relay.events.first { $0.url.hasSuffix("v.m3u8") }?.listing ?? ""), fragments \(ev.map { "\($0.status)" }.joined(separator: " "))") let bundle = try sink.end(tamper ? .error : .ended, atUs: WaiSink.nowUs) let out = try WaiSink.verifyBundle(bundle, owner: hexData(doc["owner"] as! String), channels: doc["channels"] as! [String]) XCTAssertEqual(out["outcome"] as? String, "accept", "\(out)") // Every fragment received is acknowledged, the refused one too. let delivery = out["delivery"] as! [[String: Any]] XCTAssertEqual(delivery.first { $0["class"] as? String == "fetch" }?["objects"] as? Int, 5) } } func testARequestOutsideTheSessionIsNotAnswered() throws { let sink = try WaiSink(config: .init(channel: "film-library", owner: String(repeating: "0", count: 64))) let relay = WaiRelay(sink: sink) try relay.start() defer { relay.stop() } let (st, _) = fetch(URL(string: "http://127.0.0.1:\(relay.port)/s/00000000000000000000000000000000/aHR0cDovL2EuZXhhbXBsZS94/x")!) XCTAssertEqual(st, 404) } } /// SHA-256, for the tests' own checks: the platform's. enum SHA { static func hex(_ d: Data) -> String { SHA256.hash(data: d).map { String(format: "%02x", $0) }.joined() } }