feat: add headless Zoko source checks
This commit is contained in:
@@ -189,6 +189,25 @@ final class MiruroTransport: @unchecked Sendable {
|
||||
throw MiruroPlaybackError()
|
||||
}
|
||||
}
|
||||
// Headless probe: no listener, relay token, native player or subtitle fetch.
|
||||
// The same HTTP reader and manifest walker are used by actual playback.
|
||||
static func checkZokoRoot(_ source: ResolvedSource) async throws {
|
||||
guard source.requirements.zoko, let url = URL(string: source.url),
|
||||
ZokoResolver.validResource(url, playlist: true) else { throw ZokoError.malformed }
|
||||
let (data, response) = try await MiruroMediaHTTP.read(url, headers: ZokoResolver.referer,
|
||||
limit: ZokoResolver.bodyLimit)
|
||||
guard response.statusCode == 200, response.url == url,
|
||||
let text = String(data: data, encoding: .utf8) else { throw ZokoError.malformed }
|
||||
try validateZokoManifest(text, base: url)
|
||||
}
|
||||
|
||||
static func validateZokoManifest(_ text: String, base: URL) throws {
|
||||
guard ZokoResolver.validResource(base, playlist: true) else { throw ZokoError.malformed }
|
||||
_ = try rewrite(text, base: base, strict: true) { url, kind in
|
||||
guard ZokoResolver.validResource(url, playlist: kind == .playlist) else { throw ZokoError.malformed }
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
static func subtitle(_ track: ResolvedSubtitle) async throws -> ResolvedSubtitle {
|
||||
guard let proxy = track.proxy, let url = URL(string: track.url) else { return track }
|
||||
@@ -375,7 +394,9 @@ final class MiruroTransport: @unchecked Sendable {
|
||||
} 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)
|
||||
return (try Self.rewrite(text, base: url, strict: requirements.zoko) { target, kind in
|
||||
try self.register(target, kind: kind, depth: resource.depth + 1)
|
||||
}, "application/vnd.apple.mpegurl", nil)
|
||||
}
|
||||
guard embed == nil || response.statusCode == 200 else { throw MiruroPlaybackError() }
|
||||
let data = try embed?.resourceData(raw, url: url) ?? raw
|
||||
@@ -431,30 +452,40 @@ final class MiruroTransport: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
private func rewrite(_ text: String, base: URL, depth: Int) throws -> Data {
|
||||
private static func rewrite(_ text: String, base: URL, strict: Bool,
|
||||
register: (URL, Kind) throws -> URL) throws -> Data {
|
||||
guard text.utf8.count <= 2 * 1024 * 1024, text.hasPrefix("#EXTM3U") else { throw MiruroPlaybackError() }
|
||||
let regex = try NSRegularExpression(pattern: #"URI=\"([^\"]+)\""#)
|
||||
var nextPlaylist = false
|
||||
var hasMedia = false
|
||||
var hasReference = false
|
||||
var lines: [String] = []
|
||||
for raw in text.components(separatedBy: .newlines) {
|
||||
try Task.checkCancellation()
|
||||
var line = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if line.hasPrefix("#") {
|
||||
if strict, lines.isEmpty, line != "#EXTM3U" { throw ZokoError.malformed }
|
||||
if line.hasPrefix("#EXTINF:") { hasMedia = true }
|
||||
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)
|
||||
let replacement = try register(url, kind)
|
||||
if kind == .playlist { hasReference = true }
|
||||
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
|
||||
if nextPlaylist || hasMedia { hasReference = true }
|
||||
line = try register(url, kind).absoluteString
|
||||
nextPlaylist = false
|
||||
}
|
||||
lines.append(line)
|
||||
}
|
||||
if strict, nextPlaylist || !hasReference { throw ZokoError.malformed }
|
||||
return Data(lines.joined(separator: "\n").utf8)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user