feat: add headless Zoko source checks
This commit is contained in:
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Actual CLI contract checks; --live adds bounded public embed/root probes only."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def snapshot(*roots):
|
||||
return {
|
||||
str(path): path.read_bytes() if path.is_file() else None
|
||||
for root in roots
|
||||
for path in root.rglob("*")
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
executable = Path(sys.argv[1]).resolve(strict=True)
|
||||
live = "--live" in sys.argv[2:]
|
||||
watch = "https://zokoanime.video/stream/ani/1/1/sub"
|
||||
secret = "private-fixture-token"
|
||||
with tempfile.TemporaryDirectory(prefix="terebi-source-check-") as temporary:
|
||||
root = Path(temporary)
|
||||
home, config = root / "home", root / "config"
|
||||
home.mkdir()
|
||||
(config / "terebi").mkdir(parents=True)
|
||||
# A launch would parse this malformed configuration or reject this
|
||||
# executable outside its bundle. A source check must do neither.
|
||||
(config / "terebi" / "config.toml").write_text("not valid TOML = [\n")
|
||||
cli = root / "terebi"
|
||||
shutil.copy2(executable, cli)
|
||||
environment = dict(os.environ, HOME=str(home), CFFIXED_USER_HOME=str(home), XDG_CONFIG_HOME=str(config))
|
||||
before = snapshot(home, config)
|
||||
|
||||
def invoke(arguments):
|
||||
try:
|
||||
result = subprocess.run([str(cli), *arguments], env=environment, capture_output=True, timeout=20)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise AssertionError("source-check CLI exceeded total deadline") from None
|
||||
require(len(result.stdout) <= 128 and len(result.stderr) <= 1024, "bounded CLI output")
|
||||
require(secret.encode() not in result.stdout + result.stderr, "CLI output redacts private input")
|
||||
require(b"https://" not in result.stdout, "no source URLs in JSON")
|
||||
require(snapshot(home, config) == before, "source checking must not write configuration or resume/progress state")
|
||||
return result
|
||||
|
||||
for arguments in [
|
||||
["--check-source"], ["--check-source", watch + "?token=" + secret],
|
||||
["--check-source", "https://" + secret + "@zokoanime.video/stream/ani/1/1/sub"],
|
||||
["--new-instance", "--check-source", watch], ["--check-source", watch, "--no-config"],
|
||||
["--check-source", watch, "--progress-file", str(root / secret)],
|
||||
["--check-source", "https://127.0.0.1/" + secret],
|
||||
]:
|
||||
result = invoke(arguments)
|
||||
require(result.returncode != 0 and not result.stdout, "invalid syntax/unsupported URL must fail without JSON or launch")
|
||||
require(not (root / secret).exists(), "invalid source check must not create progress file")
|
||||
if live:
|
||||
for episode, expected in [(1, "available"), (27, "unavailable")]:
|
||||
result = invoke(["--check-source", f"https://zokoanime.video/stream/ani/1/{episode}/sub"])
|
||||
require(result.returncode == 0, "completed provider checks exit zero")
|
||||
require(result.stdout.endswith(b"\n") and result.stdout.count(b"\n") == 1, "exactly one JSON line")
|
||||
row = json.loads(result.stdout)
|
||||
require(row == {"version": 1, "provider": "zokoanime", "track": "sub", "status": expected},
|
||||
f"live episode {episode} expected {expected}; result is redacted")
|
||||
require(not result.stderr, "successful source checks have no error logs")
|
||||
print("PASS: actual CLI resolves episode 1/root manifest and explicit missing episode 27 headlessly, without state writes")
|
||||
else:
|
||||
print("PASS: actual CLI rejects hostile URLs/flags privately, without configuration reads, state writes or bundle launch")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct SourceCheckChecks {
|
||||
struct Failure: Error {}
|
||||
static func require(_ condition: @autoclosure () -> Bool, _ message: String) throws {
|
||||
guard condition() else {
|
||||
FileHandle.standardError.write(Data("FAIL: \(message)\n".utf8))
|
||||
throw Failure()
|
||||
}
|
||||
}
|
||||
|
||||
static func main() async throws {
|
||||
let watch = "https://zokoanime.video/stream/ani/1/1/sub"
|
||||
guard case .checkSource(let id, let episode) = try PlayerCommand.parse(["--check-source", watch]) else {
|
||||
throw Failure()
|
||||
}
|
||||
try require(id == "1" && episode == 1, "check command is not a launch")
|
||||
let secret = "private-fixture-token"
|
||||
for arguments in [
|
||||
["--check-source"], ["--check-source", watch, watch],
|
||||
["--new-instance", "--check-source", watch], ["--no-config", "--check-source", watch],
|
||||
["--check-source", watch, "--progress-file", "/tmp/" + secret],
|
||||
["--source-file", "/tmp/" + secret, "--check-source", watch],
|
||||
["--check-source", watch, "--help"], ["--check-source", "--check-source"],
|
||||
["--check-source", "https://youtu.be/jNQXAC9IVRw"],
|
||||
["--check-source", "https://barelystarted.miruro.tv/watch/1/show?ep=1"],
|
||||
["--check-source", " " + watch], ["--check-source", watch + "\n"],
|
||||
["--check-source", watch + "?token=" + secret], ["--check-source", watch + "#" + secret],
|
||||
["--check-source", watch.replacingOccurrences(of: "https:", with: "http:")],
|
||||
["--check-source", watch.replacingOccurrences(of: "/1/1/", with: "/01/1/")],
|
||||
["--check-source", watch.replacingOccurrences(of: "/1/sub", with: "/2147483648/sub")],
|
||||
["--check-source", watch.replacingOccurrences(of: "/sub", with: "/dub")],
|
||||
["--check-source", "https://" + secret + "@zokoanime.video/stream/ani/1/1/sub"],
|
||||
["--check-source", "https://zokoanime.video.evil.example/stream/ani/1/1/sub"],
|
||||
["--check-source", "https://zokoanime.video/stream/ani/%31/1/sub"],
|
||||
["--check-source", String(repeating: secret, count: 100)]
|
||||
] {
|
||||
do {
|
||||
_ = try PlayerCommand.parse(arguments)
|
||||
} catch {
|
||||
try require(!error.localizedDescription.contains(secret), "argument errors redact input")
|
||||
continue
|
||||
}
|
||||
throw Failure()
|
||||
}
|
||||
for status in SourceCheck.Status.allCases {
|
||||
let json = status.json
|
||||
let row = try JSONSerialization.jsonObject(with: Data(json.utf8)) as! [String: Any]
|
||||
try require(row.count == 4 && row["version"] as? Int == 1 && row["provider"] as? String == "zokoanime" &&
|
||||
row["track"] as? String == "sub" && row["status"] as? String == status.rawValue,
|
||||
"one bounded result schema without source addresses or error strings")
|
||||
try require(json.utf8.count < 128 && !json.contains("\n"), "bounded single-line JSON")
|
||||
}
|
||||
let root = URL(string: "https://hls.dramahot.top/master.m3u8?token=" + secret)!
|
||||
let master = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000000\nvideo.m3u8\n"
|
||||
let media = "#EXTM3U\n#EXT-X-TARGETDURATION:10\n#EXTINF:10,\nhttps://hls1.drama1.cfd/segment.jpg\n#EXT-X-ENDLIST\n"
|
||||
try MiruroTransport.validateZokoManifest(master, base: root)
|
||||
try MiruroTransport.validateZokoManifest(media, base: root)
|
||||
for malformed in ["<html>" + secret + "</html>", "#EXTM3U", "#EXTM3Ugarbage\n" + master,
|
||||
"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\n", "#EXTM3U\nrandom.txt\n",
|
||||
master.replacingOccurrences(of: "video.m3u8", with: "https://127.0.0.1/private.m3u8"),
|
||||
master.replacingOccurrences(of: "video.m3u8", with: "https://hls1.drama1.cfd/private.m3u8"),
|
||||
media.replacingOccurrences(of: "https://hls1.drama1.cfd/segment.jpg", with: "https://evil.example/" + secret),
|
||||
"#EXTM3U\n#EXT-X-KEY:METHOD=AES-128,URI=\"http://127.0.0.1/" + secret + "\"\n" + media,
|
||||
String(repeating: "x", count: ZokoResolver.bodyLimit + 1)] {
|
||||
do { try MiruroTransport.validateZokoManifest(malformed, base: root) }
|
||||
catch {
|
||||
try require(!error.localizedDescription.contains(secret), "manifest errors redact source details")
|
||||
continue
|
||||
}
|
||||
throw Failure()
|
||||
}
|
||||
// Exercise error classification, not fake provider availability. Live proof uses the actual CLI below.
|
||||
for category in [ZokoError.unavailable, .transport, .protection, .malformed] {
|
||||
let result = await SourceCheck.run(mediaID: "1", episode: 1, resolve: { _, _ in throw category },
|
||||
validate: { _ in throw Failure() })
|
||||
try require(result == (category == .unavailable ? .unavailable : .failed), "only explicit missing episode is unavailable")
|
||||
}
|
||||
let source = ResolvedSource(url: root.absoluteString, headers: "", identity: "anilist:1:1", title: "Fixture",
|
||||
subtitles: [], requirements: .init(zoko: true))
|
||||
let resourceFailure = await SourceCheck.run(mediaID: "1", episode: 1, resolve: { _, _ in source },
|
||||
validate: { _ in throw URLError(.badServerResponse, userInfo: [NSURLErrorFailingURLStringErrorKey: root.absoluteString]) })
|
||||
try require(resourceFailure == .failed && !resourceFailure.json.contains(secret), "resource errors are failed and private")
|
||||
let deadline = await SourceCheck.run(mediaID: "1", episode: 1, timeout: .zero, resolve: { _, _ in
|
||||
try await Task.sleep(for: .seconds(60))
|
||||
return source
|
||||
}, validate: { _ in throw Failure() })
|
||||
try require(deadline == .failed, "total deadline cancels pending work")
|
||||
let cancelled = Task {
|
||||
await SourceCheck.run(mediaID: "1", episode: 1, resolve: { _, _ in
|
||||
try await Task.sleep(for: .seconds(60))
|
||||
return source
|
||||
}, validate: { _ in throw Failure() })
|
||||
}
|
||||
cancelled.cancel()
|
||||
let cancelledResult = await cancelled.value
|
||||
try require(cancelledResult == .failed, "caller cancellation stops source check")
|
||||
print("PASS: source command isolation, redaction, bounded JSON, shared manifest validation, error taxonomy, total deadline and cancellation")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -46,7 +46,7 @@ struct ZokoCheck {
|
||||
let missing = "<span>// Error</span><span>404</span><h1>Not <span>found.</span></h1><p>We couldn't find anything to play here.</p>"
|
||||
try expect(.unavailable) { _ = try ZokoResolver.parse(page: missing, mediaID: "1", episode: 27) }
|
||||
for protected in ["<title>Just a moment</title>", "<script src='/cdn-cgi/challenge-platform/x'></script>",
|
||||
"<h1>Sorry, you have been blocked</h1>"] {
|
||||
"<h1>Sorry, you have been blocked</h1>", missing + "<div>captcha</div>"] {
|
||||
try expect(.protection) { _ = try ZokoResolver.parse(page: protected, mediaID: "1", episode: 1) }
|
||||
}
|
||||
for malformed in ["<html></html>", "window.__P='broken';", try page(config) + page(config), String(repeating: "x", count: ZokoResolver.bodyLimit + 1)] {
|
||||
|
||||
Reference in New Issue
Block a user