From 42f403ac0cba9754ed827addff59fbbfaf0e6116 Mon Sep 17 00:00:00 2001 From: Yong Jian Ming Date: Mon, 11 May 2026 15:38:31 +0800 Subject: [PATCH 1/2] fix(en.comix): fix fetch token --- sources/en.comix/res/source.json | 2 +- sources/en.comix/src/web.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sources/en.comix/res/source.json b/sources/en.comix/res/source.json index c248491b..c71fd169 100644 --- a/sources/en.comix/res/source.json +++ b/sources/en.comix/res/source.json @@ -2,7 +2,7 @@ "info": { "id": "en.comix", "name": "Comix", - "version": 10, + "version": 11, "url": "https://comix.to", "contentRating": 1, "languages": ["en"] diff --git a/sources/en.comix/src/web.rs b/sources/en.comix/src/web.rs index 9d5af9a6..7b975656 100644 --- a/sources/en.comix/src/web.rs +++ b/sources/en.comix/src/web.rs @@ -20,10 +20,10 @@ pub fn get_token(web_view: &WebView, path: &str) -> Result { try {{ const vmKey = Object.keys(window).find(key => key.startsWith('vm')); const vmObj = window[vmKey]; - if (!vmObj || typeof vmObj.Qi !== 'function') {{ + if (!vmObj || typeof vmObj.qi !== 'function') {{ return ''; }} - return vmObj.Qi('{path}'); + return vmObj.qi('{path}'); }} catch(e) {{ return ''; }} @@ -41,7 +41,7 @@ pub fn decode_response(web_view: &WebView, url: &str, encoded_res: &str) -> Resu try {{ const vmKey = Object.keys(window).find(key => key.startsWith('vm')); const vmObj = window[vmKey]; - if (!vmObj || typeof vmObj.Qi !== 'function') {{ + if (!vmObj || typeof vmObj.qi !== 'function') {{ return ''; }} var captured = {{ req: null, res: null }}; From 1c98dfd52c4b6aa797baeaf72938898102b8b055 Mon Sep 17 00:00:00 2001 From: Yong Jian Ming Date: Mon, 11 May 2026 20:29:33 +0800 Subject: [PATCH 2/2] fix(en.comix): use a dynamic function search in case the function name keep changing --- sources/en.comix/src/lib.rs | 10 ++-- sources/en.comix/src/web.rs | 106 ++++++++++++++++++++++++++++++++---- 2 files changed, 102 insertions(+), 14 deletions(-) diff --git a/sources/en.comix/src/lib.rs b/sources/en.comix/src/lib.rs index 272371fe..9f0a107a 100644 --- a/sources/en.comix/src/lib.rs +++ b/sources/en.comix/src/lib.rs @@ -205,7 +205,8 @@ impl Source for Comix { let web_view = web::create_web_view()?; let path = format!("/manga/{}/chapters", manga.key); - let token = web::get_token(&web_view, &path)?; + let js_function = web::probe_for_function(&web_view, &path)?; + let token = web::get_token(&web_view, &path, &js_function)?; loop { let url = format!( @@ -217,7 +218,7 @@ impl Source for Comix { ); let encoded_res = Request::get(&url)?.string()?; - let result = web::decode_response(&web_view, &url, &encoded_res)?; + let result = web::decode_response(&web_view, &url, &encoded_res, &js_function)?; let res = serde_json::from_str::(&result)?; let items = res.result.items; @@ -266,10 +267,11 @@ impl Source for Comix { fn get_page_list(&self, _manga: Manga, chapter: Chapter) -> Result> { let web_view = web::create_web_view()?; let path = format!("/chapters/{}", chapter.key); - let token = web::get_token(&web_view, &path)?; + let js_function = web::probe_for_function(&web_view, &path)?; + let token = web::get_token(&web_view, &path, &js_function)?; let url = format!("{API_URL}{path}?_={token}"); let encoded_res = Request::get(&url)?.string()?; - let result = web::decode_response(&web_view, &url, &encoded_res)?; + let result = web::decode_response(&web_view, &url, &encoded_res, &js_function)?; let json: ChapterResponse = serde_json::from_str(&result)?; let Some(result) = json.result else { diff --git a/sources/en.comix/src/web.rs b/sources/en.comix/src/web.rs index 7b975656..4523dade 100644 --- a/sources/en.comix/src/web.rs +++ b/sources/en.comix/src/web.rs @@ -13,17 +13,103 @@ pub fn create_web_view() -> Result { Ok(web_view) } +pub fn probe_for_function(web_view: &WebView, path: &str) -> Result { + let result = web_view.eval(&format!( + "(() => {{ + try {{ + const probe = '{path}'; + const tokenRe = /^[A-Za-z0-9_-]{{40,200}}$/; + const shortRe = /^[A-Za-z]{{1,3}}$/; + const nameRe = /^vm[A-Za-z]_/; + + function tryProbe(ns, topName) {{ + var sig = '', inst = ''; + var fnames; + try {{ fnames = Object.keys(ns); }} catch (e) {{ return null; }} + for (var j = 0; j < fnames.length; j++) {{ + var fn = ns[fnames[j]]; + if (typeof fn !== 'function') continue; + var ref = fnames[j]; + if (!sig) {{ + try {{ + var out = fn(probe); + if (typeof out === 'string' && out !== probe && tokenRe.test(out)) {{ + sig = ref; + }} + }} catch (e) {{}} + }} + if (!inst) {{ + try {{ + var got = false; + fn({{ + interceptors: {{ + request: {{ use: function() {{}} }}, + response: {{ use: function() {{ got = true; }} }} + }}, + defaults: {{ headers: {{ common: {{}} }}, transformRequest: [], transformResponse: [] }} + }}); + if (got) inst = ref; + }} catch (e) {{}} + }} + if (sig && inst) return {{ topName: topName, sig: sig, inst: inst }}; + }} + return null; + }} + + var keys = Object.keys(window); + + // Fast path: matches every observed deploy. + for (var i = 0; i < keys.length; i++) {{ + var topName = keys[i]; + if (!nameRe.test(topName)) continue; + var ns = window[topName]; + if (!ns || typeof ns !== 'object' || ns === window) continue; + var hit = tryProbe(ns, topName); + if (hit) return JSON.stringify(hit); + }} + + // Fallback: structural fingerprint, no name constraint. + for (var i = 0; i < keys.length; i++) {{ + var topName = keys[i]; + if (nameRe.test(topName)) continue; // already tried + var ns = window[topName]; + if (!ns || typeof ns !== 'object' || ns === window) continue; + var fnames; + try {{ fnames = Object.keys(ns); }} catch (e) {{ continue; }} + if (fnames.length < 5) continue; + var shortAlpha = 0; + for (var s = 0; s < fnames.length; s++) {{ + if (shortRe.test(fnames[s])) shortAlpha++; + }} + if (shortAlpha < 3) continue; + var hit = tryProbe(ns, topName); + if (hit) return JSON.stringify(hit); + }} + + // This probably won't happen but just in case + return ''; + }} catch(e) {{ + return ''; + }} + }})()" + ))?; + if result.is_empty() { + bail!("Failed to fetch token") + } + Ok(result) +} + /// * `path`: API path, e.g. "/manga/some-hash/chapters" -pub fn get_token(web_view: &WebView, path: &str) -> Result { +pub fn get_token(web_view: &WebView, path: &str, js_function: &str) -> Result { let token = web_view.eval(&format!( "(() => {{ try {{ - const vmKey = Object.keys(window).find(key => key.startsWith('vm')); - const vmObj = window[vmKey]; - if (!vmObj || typeof vmObj.qi !== 'function') {{ + const vmFnName = JSON.parse('{js_function}'); + const vmObj = window[vmFnName.topName]; + if (!vmObj || typeof vmObj[vmFnName.sig] !== 'function') {{ return ''; }} - return vmObj.qi('{path}'); + return vmObj[vmFnName.sig]('{path}'); }} catch(e) {{ return ''; }} @@ -35,13 +121,13 @@ pub fn get_token(web_view: &WebView, path: &str) -> Result { Ok(token) } -pub fn decode_response(web_view: &WebView, url: &str, encoded_res: &str) -> Result { +pub fn decode_response(web_view: &WebView, url: &str, encoded_res: &str, js_function: &str) -> Result { let result = web_view.eval(&format!( "(() => {{ try {{ - const vmKey = Object.keys(window).find(key => key.startsWith('vm')); - const vmObj = window[vmKey]; - if (!vmObj || typeof vmObj.qi !== 'function') {{ + const vmFnName = JSON.parse('{js_function}'); + const vmObj = window[vmFnName.topName]; + if (!vmObj || typeof vmObj[vmFnName.sig] !== 'function') {{ return ''; }} var captured = {{ req: null, res: null }}; @@ -64,7 +150,7 @@ pub fn decode_response(web_view: &WebView, url: &str, encoded_res: &str) -> Resu transformResponse: [], }}, }}; - vmObj.v(fakeAxios); + vmObj[vmFnName.inst](fakeAxios); var raw = JSON.parse('{encoded_res}'); var bodyOut;