import Foundation import Network import Darwin private struct MiruroPlaybackError: LocalizedError { var errorDescription: String? { "Could not prepare this Miruro provider. Private source details are redacted." } } private final class MiruroMediaRedirectPolicy: NSObject, URLSessionTaskDelegate, Sendable { func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) } } // No cookies, redirects or credentials inherited from the app/browser session. enum MiruroMediaHTTP { private static let session: URLSession = { let configuration = URLSessionConfiguration.ephemeral configuration.httpShouldSetCookies = false configuration.httpCookieStorage = nil configuration.urlCredentialStorage = nil configuration.urlCache = nil configuration.timeoutIntervalForRequest = 20 configuration.timeoutIntervalForResource = 30 return URLSession(configuration: configuration, delegate: MiruroMediaRedirectPolicy(), delegateQueue: nil) }() static func read(_ url: URL, headers: String = "", limit: Int = 2 * 1024 * 1024, range: String? = nil) async throws -> (Data, HTTPURLResponse) { _ = try YouTubeResolver.validateURL(url.absoluteString) var request = URLRequest(url: url) for line in headers.components(separatedBy: "\r\n") where !line.isEmpty { guard let colon = line.firstIndex(of: ":") else { throw MiruroPlaybackError() } request.setValue(String(line[line.index(after: colon)...]).trimmingCharacters(in: .whitespaces), forHTTPHeaderField: String(line[.. 0 { data.reserveCapacity(Int(response.expectedContentLength)) } for try await byte in bytes { try Task.checkCancellation() guard data.count < limit else { throw MiruroPlaybackError() } data.append(byte) } return (data, response) } } struct MiruroProxyDeployment: Sendable { let base: URL let key: [UInt8] var alternate: URL? = nil static func decode(_ html: String) throws -> Self { let pattern = #"env:\s*(\{[^\n]*\})"# guard let match = html.range(of: pattern, options: .regularExpression), let start = html[match].firstIndex(of: "{"), let data = String(html[start.. URL { guard [base, alternate].compactMap({ $0 }).contains(where: { $0.scheme == url.scheme && $0.host == url.host && $0.port == url.port }) else { return url } guard let encoded = url.path.split(separator: "/").first?.split(separator: "~").first else { throw MiruroPlaybackError() } var text = encoded.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") text += String(repeating: "=", count: (4 - text.count % 4) % 4) guard let data = Data(base64Encoded: text) else { throw MiruroPlaybackError() } var bytes = Array(data) if !key.isEmpty { for index in bytes.indices { bytes[index] ^= key[index % key.count] } } guard let target = String(bytes: bytes, encoding: .utf8), let result = URL(string: target), ["http", "https"].contains(result.scheme), result.user == nil, result.password == nil else { throw MiruroPlaybackError() } return result } func url(_ target: URL, referer: String?, proxy: MiruroProxy, suffix: String) throws -> URL { func encode(_ text: String) -> String { var bytes = Array(text.utf8) if !key.isEmpty { for index in bytes.indices { bytes[index] ^= key[index % key.count] } } return Data(bytes).base64EncodedString().replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") } var path = encode(target.absoluteString) if let referer, !referer.isEmpty { path += "~" + encode(referer) } else if proxy.rotate { path += "~" } if proxy.rotate { path += "~anon" } guard let url = URL(string: base.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + "/" + path + suffix) else { throw MiruroPlaybackError() } return url } } private actor MiruroDeploymentCache { static let shared = MiruroDeploymentCache() private var deployment: MiruroProxyDeployment? func get() async throws -> MiruroProxyDeployment { if let deployment { return deployment } let (data, _) = try await MiruroMediaHTTP.read(URL(string: "https://barelystarted.miruro.tv/")!) guard let html = String(data: data, encoding: .utf8) else { throw MiruroPlaybackError() } let result = try MiruroProxyDeployment.decode(html) deployment = result return result } } // Only registered resources are reachable: no caller-supplied upstream URLs, and // no private headers ever cross the loopback native/Cast handoff. // ponytail: buffer up to eight 64-MiB resources; stream the relay if that ceiling becomes limiting. final class MiruroTransport: @unchecked Sendable { private enum Kind: String { case playlist, segment, resource } private struct Resource { let url: URL; let kind: Kind; let depth: Int } private let listener: NWListener private let queue = DispatchQueue(label: "terebi.miruro.transport") private let lock = NSLock() private var resources: [String: Resource] = [:] private var paths: [String: String] = [:] private var active = 0 private var preparedMaster: (Data, String, String?)? private let token = UUID().uuidString private let sourceURL: URL private let headers: String private let requirements: MiruroPlaybackRequirements private let deployment: MiruroProxyDeployment? private let embed: MiruroEmbed? private init(source: ResolvedSource, deployment: MiruroProxyDeployment?, embed: MiruroEmbed?) throws { guard let sourceURL = URL(string: source.url) else { throw MiruroPlaybackError() } self.sourceURL = embed?.url ?? sourceURL headers = embed?.headers ?? source.headers requirements = source.requirements self.deployment = deployment self.embed = embed let parameters = NWParameters.tcp parameters.requiredLocalEndpoint = .hostPort(host: .ipv4(.loopback), port: .any) listener = try NWListener(using: parameters) } static func prepare(_ source: ResolvedSource, deployment override: MiruroProxyDeployment? = nil) async throws -> ResolvedSource { let rules = source.requirements guard rules.embed || rules.proxy != nil || rules.segmentOrigin != nil || !rules.playlistQuery.isEmpty else { return source } do { let deployment: MiruroProxyDeployment? if let override { deployment = override } else if rules.proxy != nil { deployment = try await MiruroDeploymentCache.shared.get() } else { deployment = nil } let embed = try await rules.embed ? MiruroEmbed.resolve(url: URL(string: source.url)!) : nil let owner = try MiruroTransport(source: source, deployment: deployment, embed: embed) try await owner.start() let url = try owner.register(owner.sourceURL, kind: .playlist, depth: 0) // Exercise the master before replacing acknowledged playback. A failed // provider switch must leave the current decoder and position intact. let master = try await owner.load(Resource(url: owner.sourceURL, kind: .playlist, depth: 0), range: nil) owner.lock.withLock { owner.preparedMaster = master } var prepared = ResolvedSource(url: url.absoluteString, headers: "", identity: source.identity, title: source.title, subtitles: source.subtitles) prepared.playbackLifetime = owner return prepared } catch { if Task.isCancelled || error is CancellationError { throw CancellationError() } throw MiruroPlaybackError() } } static func subtitle(_ track: ResolvedSubtitle) async throws -> ResolvedSubtitle { guard let proxy = track.proxy, let url = URL(string: track.url) else { return track } do { let deployment = try await MiruroDeploymentCache.shared.get() let target = try deployment.url(url, referer: header("Referer", in: track.headers), proxy: proxy, suffix: "/sub.vtt") return ResolvedSubtitle(url: target.absoluteString, format: track.format, language: track.language, label: track.label, isDefault: track.isDefault, headers: [:]) } catch { if Task.isCancelled || error is CancellationError { throw CancellationError() } throw MiruroPlaybackError() } } private static func header(_ name: String, in headers: String) -> String? { for line in headers.components(separatedBy: "\r\n") { guard let colon = line.firstIndex(of: ":"), line[..) in listener.stateUpdateHandler = { [weak self] state in switch state { case .ready: self?.listener.stateUpdateHandler = nil continuation.resume() case .failed, .cancelled: self?.listener.stateUpdateHandler = nil continuation.resume(throwing: MiruroPlaybackError()) default: break } } listener.newConnectionHandler = { [weak self] connection in self?.accept(connection) } listener.start(queue: queue) } try Task.checkCancellation() } deinit { listener.cancel() } private func register(_ url: URL, kind: Kind, depth: Int) throws -> URL { guard depth <= 12, ["http", "https"].contains(url.scheme), url.user == nil, url.password == nil, url.fragment == nil, url.absoluteString.utf8.count <= 32768, let port = listener.port else { throw MiruroPlaybackError() } return try lock.withLock { let key = kind.rawValue + ":" + url.absoluteString let path: String if let existing = paths[key] { path = existing } else { guard resources.count < 32768 else { throw MiruroPlaybackError() } path = "/" + token + "/" + String(resources.count) + (kind == .playlist ? ".m3u8" : ".bin") resources[path] = Resource(url: url, kind: kind, depth: depth) paths[key] = path } return URL(string: "http://127.0.0.1:\(port.rawValue)\(path)")! } } private func accept(_ connection: NWConnection) { let admitted = lock.withLock { () -> Bool in guard active < 8 else { return false } active += 1 return true } guard admitted else { connection.cancel(); return } connection.start(queue: queue) queue.asyncAfter(deadline: .now() + 35) { connection.cancel() } receive(connection, data: Data()) } private func receive(_ connection: NWConnection, data: Data) { connection.receive(minimumIncompleteLength: 1, maximumLength: 16384) { [weak self] next, _, complete, error in guard let self else { connection.cancel(); return } var data = data if let next { data.append(next) } guard error == nil, data.count <= 16384 else { self.finish(connection); return } guard data.range(of: Data("\r\n\r\n".utf8)) != nil else { if complete { self.finish(connection) } else { self.receive(connection, data: data) } return } Task { [self] in defer { finish(connection) } do { guard let request = String(data: data, encoding: .utf8) else { throw MiruroPlaybackError() } let lines = request.components(separatedBy: "\r\n") let first = lines[0].split(separator: " ") guard first.count == 3, first[0] == "GET" || first[0] == "HEAD", let resource = lock.withLock({ resources[String(first[1])] }) else { throw MiruroPlaybackError() } var range: String? for line in lines.dropFirst() where line.lowercased().hasPrefix("range:") { let value = String(line.dropFirst(6)).trimmingCharacters(in: .whitespaces) guard value.range(of: #"^bytes=[0-9]+-[0-9]*$"#, options: .regularExpression) != nil, range == nil else { throw MiruroPlaybackError() } range = value } let (body, type, contentRange) = try await load(resource, range: range) let status = contentRange == nil ? "200 OK" : "206 Partial Content" var response = "HTTP/1.1 \(status)\r\nContent-Type: \(type)\r\nContent-Length: \(body.count)\r\nConnection: close\r\n" if let contentRange { response += "Content-Range: \(contentRange)\r\n" } try await send(Data((response + "\r\n").utf8), connection: connection) if first[0] == "GET" { try await send(body, connection: connection) } } catch { try? await send(Data("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".utf8), connection: connection) } } } } private func finish(_ connection: NWConnection) { connection.cancel() lock.withLock { active -= 1 } } private func send(_ data: Data, connection: NWConnection) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in connection.send(content: data, completion: .contentProcessed { error in if let error { continuation.resume(throwing: error) } else { continuation.resume() } }) } } private func load(_ resource: Resource, range: String?) async throws -> (Data, String, String?) { if resource.depth == 0, resource.kind == .playlist, let master = lock.withLock({ () -> (Data, String, String?)? in defer { preparedMaster = nil } return preparedMaster }) { return master } let original = try deployment?.originalURL(resource.url) ?? resource.url var url = try embed?.resourceURL(original) ?? original if resource.kind == .playlist, !requirements.playlistQuery.isEmpty { var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! var query = components.queryItems ?? [] query.removeAll { requirements.playlistQuery[$0.name] != nil } query += requirements.playlistQuery.sorted { $0.key < $1.key }.map { URLQueryItem(name: $0.key, value: $0.value) } components.queryItems = query guard let result = components.url else { throw MiruroPlaybackError() } url = result } if resource.kind == .segment, let origin = requirements.segmentOrigin, let replacement = URLComponents(string: origin), var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { components.scheme = replacement.scheme components.host = replacement.host components.port = replacement.port guard let result = components.url else { throw MiruroPlaybackError() } url = result } func fetch(_ url: URL) async throws -> (Data, HTTPURLResponse) { // Same-origin fixture/source access remains allowed. A manifest or // decrypted token cannot turn the relay into a local-network proxy. if !Self.sameOrigin(url, sourceURL) { try Self.requirePublicDestination(url) } let target: URL let requestHeaders: String if let proxy = requirements.proxy, let deployment, resource.kind == .playlist || proxy.segments { target = try deployment.url(url, referer: Self.header("Referer", in: headers), proxy: proxy, suffix: resource.kind == .playlist ? "/pl.m3u8" : "/seg.jpg") requestHeaders = "" } else { target = url // Only the embed's fixed public Referer has cross-origin scope. requestHeaders = embed?.headers ?? (Self.sameOrigin(url, sourceURL) ? headers : "") } return try await MiruroMediaHTTP.read(target, headers: requestHeaders, limit: resource.kind == .playlist ? 2 * 1024 * 1024 : 64 * 1024 * 1024, range: resource.kind == .playlist || embed != nil ? nil : range) } let raw: Data let response: HTTPURLResponse if resource.kind == .playlist, let skip = requirements.playlistQuery["skip"], !skip.isEmpty, var components = URLComponents(url: url, resolvingAgainstBaseURL: false), components.queryItems?.contains(where: { $0.name == "token" }) == true { components.queryItems?.removeAll { $0.name == "token" } do { (raw, response) = try await fetch(components.url!) } catch { try Task.checkCancellation() (raw, response) = try await fetch(url) } } else { (raw, response) = try await fetch(url) } if resource.kind == .playlist { guard response.statusCode == 200, let text = String(data: raw, encoding: .utf8), text.hasPrefix("#EXTM3U") else { throw MiruroPlaybackError() } return (try rewrite(text, base: url, depth: resource.depth), "application/vnd.apple.mpegurl", nil) } guard embed == nil || response.statusCode == 200 else { throw MiruroPlaybackError() } let data = try embed?.resourceData(raw, url: url) ?? raw if embed != nil, let range { let offsets = range.dropFirst(6).split(separator: "-", omittingEmptySubsequences: false) guard let start = Int(offsets[0]), start < data.count else { throw MiruroPlaybackError() } let end = offsets[1].isEmpty ? data.count - 1 : min(Int(offsets[1]) ?? -1, data.count - 1) guard end >= start else { throw MiruroPlaybackError() } return (data.subdata(in: start..<(end + 1)), "application/octet-stream", "bytes \(start)-\(end)/\(data.count)") } let contentRange = response.statusCode == 206 ? response.value(forHTTPHeaderField: "Content-Range") : nil if response.statusCode == 206 { guard let contentRange, contentRange.range(of: #"^bytes [0-9]+-[0-9]+/([0-9]+|\*)$"#, options: .regularExpression) != nil else { throw MiruroPlaybackError() } } return (data, "application/octet-stream", contentRange) } private static func sameOrigin(_ lhs: URL, _ rhs: URL) -> Bool { lhs.scheme?.lowercased() == rhs.scheme?.lowercased() && lhs.host?.lowercased() == rhs.host?.lowercased() && (lhs.port ?? (lhs.scheme == "https" ? 443 : 80)) == (rhs.port ?? (rhs.scheme == "https" ? 443 : 80)) } private static func requirePublicDestination(_ url: URL) throws { guard let host = url.host else { throw MiruroPlaybackError() } var hints = addrinfo() hints.ai_family = AF_UNSPEC hints.ai_socktype = SOCK_STREAM var addresses: UnsafeMutablePointer? guard getaddrinfo(host, nil, &hints, &addresses) == 0, let first = addresses else { throw MiruroPlaybackError() } defer { freeaddrinfo(first) } var current: UnsafeMutablePointer? = first while let entry = current { let value = entry.pointee if value.ai_family == AF_INET { let address = value.ai_addr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { UInt32(bigEndian: $0.pointee.sin_addr.s_addr) } let a = address >> 24, b = (address >> 16) & 255 guard a != 0, a != 10, a != 127, a < 224, !(a == 169 && b == 254), !(a == 172 && (16...31).contains(b)), !(a == 192 && b == 168), !(a == 100 && (64...127).contains(b)), !(a == 198 && (18...19).contains(b)) else { throw MiruroPlaybackError() } } else if value.ai_family == AF_INET6 { let bytes = value.ai_addr.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { withUnsafeBytes(of: $0.pointee.sin6_addr) { Array($0) } } // Only globally routed unicast; excludes loopback, link-local, // unique-local, multicast and IPv4-mapped local destinations. guard bytes[0] & 0xe0 == 0x20 else { throw MiruroPlaybackError() } } else { throw MiruroPlaybackError() } current = value.ai_next } } private func rewrite(_ text: String, base: URL, depth: Int) throws -> Data { let regex = try NSRegularExpression(pattern: #"URI=\"([^\"]+)\""#) var nextPlaylist = false var lines: [String] = [] for raw in text.components(separatedBy: .newlines) { var line = raw.trimmingCharacters(in: .whitespacesAndNewlines) if line.hasPrefix("#") { let kind: Kind = line.hasPrefix("#EXT-X-MAP:") ? .segment : (line.hasPrefix("#EXT-X-MEDIA:") || line.hasPrefix("#EXT-X-I-FRAME-STREAM-INF:") || line.hasPrefix("#EXT-X-RENDITION-REPORT:")) ? .playlist : .resource for match in regex.matches(in: line, range: NSRange(line.startIndex..., in: line)).reversed() { guard let range = Range(match.range(at: 1), in: line), let url = URL(string: String(line[range]), relativeTo: base)?.absoluteURL else { throw MiruroPlaybackError() } let replacement = try register(url, kind: kind, depth: depth + 1) line.replaceSubrange(range, with: replacement.absoluteString) } if line.hasPrefix("#EXT-X-STREAM-INF:") { nextPlaylist = true } } else if !line.isEmpty { guard let url = URL(string: line, relativeTo: base)?.absoluteURL else { throw MiruroPlaybackError() } let kind: Kind = nextPlaylist || url.path.lowercased().hasSuffix(".m3u8") ? .playlist : .segment line = try register(url, kind: kind, depth: depth + 1).absoluteString nextPlaylist = false } lines.append(line) } return Data(lines.joined(separator: "\n").utf8) } }