From bda0fea1934c264df144aa6d468013eaf20394e9 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sun, 24 May 2026 11:53:12 -0700 Subject: [PATCH] deobfuscate: soft-fail sig_fn/nsig_fn extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When YouTube rotates player.js to a shape our six sig/nsig regex patterns don't recognise (eg. c2f7551f, May 2026), the whole player path used to die at extract_fns even for clients that don't need the sig fn at all (iOS, Android, Tv all get pre-signed stream URLs). Now sig_fn / nsig_fn extraction is best-effort. Only the signature timestamp is required — every `needs_deobf` client needs sts in the request payload, but the actual deobfuscation functions are only consumed by map_url when a stream URL carries `&s=` or `&n=`. On failure we log a warning and store an empty string; Deobfuscator then skips the JS eval, and any deobfuscate_sig/deobfuscate_nsig call will fail loudly with "sig fn unavailable" instead of crashing the player. Keeps the Tv fallback alive even when sig deobf regex breaks. --- src/deobfuscate.rs | 52 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/deobfuscate.rs b/src/deobfuscate.rs index d08a6e1..54b5201 100644 --- a/src/deobfuscate.rs +++ b/src/deobfuscate.rs @@ -61,10 +61,34 @@ impl DeobfData { } pub fn extract_fns(js_url: &str, player_js: &str) -> Result { - let sig_fn = get_sig_fn(player_js)?; - let nsig_fn = get_nsig_fn(player_js)?; + // The signature timestamp is the only piece every "needs_deobf" client + // actually requires in its request payload — without it, those clients + // get an error back. So we hard-fail on sts extraction. let sts = get_sts(player_js)?; + // sig_fn and nsig_fn are needed only when YouTube returns stream URLs + // containing the &s= cipher / &n= throttling params. Most clients + // (iOS, Android, Tv) get pre-signed URLs and never touch these. + // Tolerate extraction failures here so a single rotated player.js + // shape doesn't bring down the whole player path for those clients. + // The dead-code fallback is preserved: if a stream URL DOES need + // deobfuscation, `Deobfuscator::deobfuscate_sig` will fail with a + // clear "sig fn unavailable" error instead of crashing the player. + let sig_fn = match get_sig_fn(player_js) { + Ok(f) => f, + Err(e) => { + tracing::warn!("could not extract sig deobf fn (sig deobfuscation disabled until YT rotates player.js again): {}", e); + String::new() + } + }; + let nsig_fn = match get_nsig_fn(player_js) { + Ok(f) => f, + Err(e) => { + tracing::warn!("could not extract nsig deobf fn (throttling parameter deobf disabled until YT rotates player.js again): {}", e); + String::new() + } + }; + Ok(Self { js_url: js_url.to_owned(), sig_fn, @@ -79,13 +103,23 @@ impl Deobfuscator { pub fn new(data: &DeobfData) -> Result { let rt = Runtime::new()?; let ctx = Context::full(&rt)?; - ctx.with(|ctx| { - let mut opts = rquickjs::context::EvalOptions::default(); - opts.strict = false; - ctx.eval_with_options::<(), _>(data.sig_fn.as_bytes(), opts)?; - let mut opts = rquickjs::context::EvalOptions::default(); - opts.strict = false; - ctx.eval_with_options::<(), _>(data.nsig_fn.as_bytes(), opts) + ctx.with(|ctx| -> Result<(), rquickjs::Error> { + // Skip JS eval for any deobf fn we couldn't extract. The matching + // `deobfuscate_sig` / `deobfuscate_nsig` calls will then return an + // Err naturally because the global won't be defined — and that + // only matters if a stream actually has obfuscated params, which + // shouldn't happen on the iOS/Android/Tv InnerTube paths. + if !data.sig_fn.is_empty() { + let mut opts = rquickjs::context::EvalOptions::default(); + opts.strict = false; + ctx.eval_with_options::<(), _>(data.sig_fn.as_bytes(), opts)?; + } + if !data.nsig_fn.is_empty() { + let mut opts = rquickjs::context::EvalOptions::default(); + opts.strict = false; + ctx.eval_with_options::<(), _>(data.nsig_fn.as_bytes(), opts)?; + } + Ok(()) })?; Ok(Self { ctx }) }