diff --git a/open-sse/executors/duckduckgo-web.ts b/open-sse/executors/duckduckgo-web.ts index 05cc4b12cd..e9c6053548 100644 --- a/open-sse/executors/duckduckgo-web.ts +++ b/open-sse/executors/duckduckgo-web.ts @@ -1,6 +1,6 @@ -import { createHash, generateKeyPairSync, randomUUID } from "node:crypto"; +import { generateKeyPairSync, randomUUID } from "node:crypto"; import vm from "node:vm"; -import { parseFragment, serialize } from "parse5"; +import { solveDuckDuckGoChallenge, makeDuckDuckGoFeSignals } from "./duckduckgo-web/challenge.ts"; import { BaseExecutor, type ExecuteInput } from "./base.ts"; import { FETCH_TIMEOUT_MS } from "../config/constants.ts"; import { prepareToolMessages, buildToolAwareResult } from "../translator/webTools.ts"; @@ -82,71 +82,6 @@ type DuckDuckGoChallengeResult = { let durablePublicKey: JsonWebKey | null = null; -const CHALLENGE_STUBS = String.raw` -var __ua = __DDG_REAL_UA__; -var __HTML_LOOKUP = __DDG_HTML_LOOKUP__; -function __makeHtmlElement(tag) { - var state = { _innerHTML: '', _qsaCount: 0, _cssText: '' }; - var el = { - tagName: String(tag).toUpperCase(), nodeName: String(tag).toUpperCase(), nodeType: 1, - children: [], childNodes: [], classList: [], dataset: {}, - offsetWidth: 1, offsetHeight: 1, clientWidth: 1, clientHeight: 1, scrollHeight: 1, scrollWidth: 1, - getBoundingClientRect: function(){ return { x: 0, y: 0, top: 0, left: 0, right: 1, bottom: 1, width: 1, height: 1, toJSON: function(){ return {}; } }; }, - setAttribute: function(){}, removeAttribute: function(){}, - getAttribute: function(a){ if(a==='srcdoc') return state._srcdoc||''; return null; }, - hasAttribute: function(){ return false; }, appendChild: function(c){ return c; }, removeChild: function(c){ return c; }, - addEventListener: function(){}, removeEventListener: function(){}, querySelector: function(){ return null; }, - querySelectorAll: function(s){ if (s === '*') { var arr = []; arr.length = state._qsaCount; return arr; } return []; }, - cloneNode: function(){ return __makeHtmlElement(tag); } - }; - Object.defineProperty(el, 'style', { value: new Proxy({}, { set: function(t, k, v){ t[k] = v; if (k === 'cssText') state._cssText = String(v); return true; }, get: function(t, k){ if (k === 'cssText') return state._cssText; return t[k] || ''; } }), enumerable: true, configurable: true }); - Object.defineProperty(el, 'innerHTML', { get: function(){ return state._innerHTML; }, set: function(v){ var key = String(v); var entry = __HTML_LOOKUP && __HTML_LOOKUP[key]; if (entry) { state._innerHTML = String(entry.html); state._qsaCount = entry.count|0; } else { state._innerHTML = key; state._qsaCount = 0; } }, enumerable: true, configurable: true }); - Object.defineProperty(el, 'outerHTML', { get: function(){ return '<' + tag + '>' + state._innerHTML + ''; }, enumerable: true }); - Object.defineProperty(el, 'srcdoc', { get: function(){ return state._srcdoc||''; }, set: function(v){ state._srcdoc = String(v); }, enumerable: true }); - Object.defineProperty(el, 'contentWindow', { get: function(){ var w = {}; w.document = __ifDoc; w.Proxy = Proxy; w.self = w; w.top = w; w.parent = w; w.window = w; return w; }, enumerable: true }); - Object.defineProperty(el, 'contentDocument', { get: function(){ return __ifDoc; }, enumerable: true }); - return el; -} -function __mkObj(name, base) { - base = base || {}; - return new Proxy(base, { - get: function(t, k) { - if (k in t) return t[k]; - if (k === Symbol.toPrimitive) return function(){ return ''; }; - if (k === Symbol.iterator) return undefined; - if (k === 'then' || k === 'catch' || k === 'finally') return undefined; - if (k === 'constructor') return Object; - if (k === 'toString' || k === 'valueOf') return function(){ return '[object ' + name + ']'; }; - if (k === 'length') return 0; - if (k === 'nodeType') return 1; - if (k === 'tagName' || k === 'nodeName') return 'DIV'; - if (k === 'innerHTML' || k === 'outerHTML' || k === 'textContent' || k === 'innerText' || k === 'value') return ''; - if (k === 'children' || k === 'childNodes' || k === 'classList') return []; - // Real numeric layout values for the DDG challenge DOM probes. - if (k === 'offsetWidth' || k === 'offsetHeight' || k === 'clientWidth' || k === 'clientHeight' || k === 'scrollHeight' || k === 'scrollWidth') return 1; - if (k === 'getBoundingClientRect') return function(){ return { x: 0, y: 0, top: 0, left: 0, right: 1, bottom: 1, width: 1, height: 1, toJSON: function(){ return {}; } }; }; - if (typeof k === 'string' && (k.indexOf('get') === 0 || k.indexOf('query') === 0 || k.indexOf('find') === 0)) return function(){ return k === 'querySelectorAll' || k === 'getElementsByTagName' || k === 'getElementsByClassName' ? [] : null; }; - return function(){ return __mkObj(name + '.' + String(k)); }; - }, - has: function(t, k){ return k in t; }, set: function(t, k, v){ t[k] = v; return true; } - }); -} -function __parseCssDisplay(cssText){ if(!cssText) return ''; var m = String(cssText).match(/(?:^|;)\\s*display\\s*:\\s*([^;]+)/i); return m ? String(m[1]).trim() : ''; } -function __getComputedStyle(el){ var cssText = el && el.style && el.style.cssText || ''; var display = __parseCssDisplay(cssText); return { getPropertyValue: function(name){ if(String(name).toLowerCase()==='display') return display; return ''; }, cssText: cssText, display: display }; } -var __ifMeta = __mkObj('meta', { getAttribute: function(a){ return a==='content' ? "default-src 'none'; script-src 'unsafe-inline';" : null; }, hasAttribute: function(a){ return a==='content'; }, tagName: 'META', nodeName: 'META' }); -var __ifDoc = __mkObj('iframeDoc', { querySelector: function(s){ if (s && s.indexOf('Content-Security-Policy') !== -1) return __ifMeta; if (s === 'meta') return __ifMeta; return null; }, querySelectorAll: function(s){ if (s && s.indexOf('Content-Security-Policy') !== -1) return [__ifMeta]; if (s === 'meta') return [__ifMeta]; return []; }, getElementsByTagName: function(t){ return t && t.toLowerCase()==='meta' ? [__ifMeta] : []; }, body: __mkObj('iframeBody'), head: __mkObj('iframeHead'), documentElement: __mkObj('iframeRoot'), createElement: function(){ return __mkObj('elem', {setAttribute:function(){}, appendChild:function(){}, removeChild:function(){}, getAttribute:function(){return null;}, hasAttribute:function(){return false;}}); }, cookie: '', readyState: 'complete' }); -var __iframeEl = __mkObj('iframe', { contentDocument: __ifDoc, contentWindow: __mkObj('iframeWin', { document: __ifDoc, top: undefined, parent: undefined }), document: __ifDoc, getAttribute: function(a){ if (a==='sandbox') return 'allow-scripts allow-same-origin'; if (a==='srcdoc') return ''; if (a==='id') return 'jsa'; return null; }, hasAttribute: function(a){ return a==='sandbox'||a==='id'; }, tagName: 'IFRAME', nodeName: 'IFRAME', id: 'jsa' }); -var document = __mkObj('document', { querySelector: function(s){ if (s === '#jsa') return __iframeEl; if (s && s.indexOf('Content-Security-Policy') !== -1) return __ifMeta; return null; }, querySelectorAll: function(s){ if (s === '#jsa') return [__iframeEl]; if (s && s.indexOf('Content-Security-Policy') !== -1) return [__ifMeta]; return []; }, getElementById: function(id){ return id==='jsa' ? __iframeEl : null; }, getElementsByTagName: function(t){ if(t&&t.toLowerCase()==='iframe') return [__iframeEl]; return []; }, getElementsByClassName: function(){ return []; }, body: __mkObj('body', {appendChild:function(){}, removeChild:function(){}, querySelector:function(s){return s==='#jsa'?__iframeEl:null;}, querySelectorAll:function(s){return s==='#jsa'?[__iframeEl]:[];}}), head: __mkObj('head'), documentElement: __mkObj('root'), createElement: function(tag){ return __makeHtmlElement(tag||'div'); }, createTextNode: function(t){ return {nodeType:3, nodeValue:String(t||''), textContent:String(t||'')}; }, cookie: '', readyState: 'complete', title: '', addEventListener: function(){}, removeEventListener: function(){} }); - var window = __mkObj('window', { document: document, __DDG_BE_VERSION__: 1, __DDG_FE_CHAT_HASH__: 1, navigator: __mkObj('navigator', { userAgent: __ua, webdriver: false, language: 'en-US', languages: ['en-US','en'], platform: 'Linux x86_64', vendor: 'Google Inc.', appVersion: '5.0 (X11)', cookieEnabled: true, onLine: true, hardwareConcurrency: 8, deviceMemory: 8 }), innerWidth: 1280, innerHeight: 800, outerWidth: 1280, outerHeight: 800, devicePixelRatio: 1, screen: __mkObj('screen', { width:1920, height:1080, availWidth:1920, availHeight:1080, colorDepth:24, pixelDepth:24 }), location: __mkObj('location', { href:'https://duck.ai/', origin:'https://duck.ai', host:'duck.ai', hostname:'duck.ai', protocol:'https:', pathname:'/' }), performance: __mkObj('perf', { now: function(){ return 0; }, timeOrigin: 0 }), history: __mkObj('history', { length: 1, state: null }), addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){return true;}, setTimeout: function(fn){ try{fn();}catch(e){} return 0; }, clearTimeout: function(){}, hasOwnProperty: function(k){ if (k==='__DDG_BE_VERSION__'||k==='__DDG_FE_CHAT_HASH__') return true; return Object.prototype.hasOwnProperty.call(this,k); } }); -window.top = window; window.self = window; window.window = window; window.parent = window; window.globalThis = window; -var top = window, self = window, parent = window, navigator = window.navigator, location = window.location, screen = window.screen, performance = window.performance, history = window.history; -var __R = null, __E = null; -function __HTMLClass(name){ var c = function(){}; c.prototype = __mkObj(name+'.proto'); return c; } -var HTMLElement = __HTMLClass('HTMLElement'), HTMLDivElement = __HTMLClass('HTMLDivElement'), HTMLIFrameElement = __HTMLClass('HTMLIFrameElement'), HTMLDocument = __HTMLClass('HTMLDocument'), Document = __HTMLClass('Document'), Element = __HTMLClass('Element'), Node = __HTMLClass('Node'), Window = __HTMLClass('Window'), Event = __HTMLClass('Event'), MouseEvent = __HTMLClass('MouseEvent'), KeyboardEvent = __HTMLClass('KeyboardEvent'), TouchEvent = __HTMLClass('TouchEvent'), XMLHttpRequest = __HTMLClass('XMLHttpRequest'), WebSocket = __HTMLClass('WebSocket'), Image = __HTMLClass('Image'), FormData = __HTMLClass('FormData'), Blob = __HTMLClass('Blob'), File = __HTMLClass('File'), FileReader = __HTMLClass('FileReader'), URL = __HTMLClass('URL'), URLSearchParams = __HTMLClass('URLSearchParams'), Headers = __HTMLClass('Headers'), Request = __HTMLClass('Request'), Response = __HTMLClass('Response'); -var fetch = function(){ return Promise.resolve(__mkObj('resp', {ok:true, status:200, json:function(){return Promise.resolve({});}, text:function(){return Promise.resolve('');}})); }; -var getComputedStyle = __getComputedStyle; -`; - function extractDuckDuckGoContent(data: unknown): string { if (!data || typeof data !== "object") return ""; const record = data as Record; @@ -247,83 +182,6 @@ function getDuckDuckGoModelCapabilities(model: string): DuckDuckGoModelCapabilit return { reasoningEffort: null }; } -function countHtmlElements(node: unknown): number { - if (!node || typeof node !== "object") return 0; - const record = node as { nodeName?: string; childNodes?: unknown[] }; - const own = record.nodeName && record.nodeName !== "#document-fragment" ? 1 : 0; - let childCount = 0; - for (const child of record.childNodes ?? []) { - childCount += countHtmlElements(child); - } - return own + childCount; -} - -function buildHtmlLookup(js: string): Record { - const lookup: Record = {}; - const seen = new Set(); - const pattern = /(['"])(<[^'"]{1,400}?)\1/g; - for (const match of js.matchAll(pattern)) { - const html = match[2]; - if (seen.has(html)) continue; - seen.add(html); - const fragment = parseFragment(html); - lookup[html] = { - html: serialize(fragment), - count: Math.max(0, countHtmlElements(fragment) - 1), - }; - } - return lookup; -} - -function sha256Base64(value: string): string { - return createHash("sha256").update(value, "utf8").digest("base64"); -} - -async function solveDuckDuckGoChallenge(challenge: string, userAgent: string): Promise { - // SECURITY NOTE: This function executes base64-decoded JavaScript from duck.ai via vm.runInContext. - // The challenge code is upstream-supplied (supply-chain surface). It is sandboxed with a 5s timeout - // to limit DoS risk. This is intentional for the DDG challenge solver to work. - const js = Buffer.from(challenge, "base64").toString("utf8"); - const stubs = CHALLENGE_STUBS.replace("__DDG_REAL_UA__", JSON.stringify(userAgent)).replace( - "__DDG_HTML_LOOKUP__", - JSON.stringify(buildHtmlLookup(js)) - ); - const context = vm.createContext({}); - vm.runInContext(stubs, context, { timeout: 5000 }); - const result = (await vm.runInContext(js, context, { - timeout: 5000, - })) as DuckDuckGoChallengeResult; - const clientHashes = Array.isArray(result.client_hashes) ? result.client_hashes : []; - if (clientHashes.length === 0) - throw new Error("DuckDuckGo challenge returned empty client_hashes"); - clientHashes[0] = userAgent; - result.client_hashes = clientHashes.map((hash) => sha256Base64(String(hash))); - return Buffer.from(JSON.stringify(result), "utf8").toString("base64"); -} - -function makeDuckDuckGoFeSignals(): string { - const start = Date.now() - 3000; - let delta = 80 + Math.floor(Math.random() * 101); - const events: Array> = [{ name: "onboarding_impression_1", delta }]; - delta += 120 + Math.floor(Math.random() * 141); - events.push({ name: "onboarding_impression_2", delta }); - delta += 200 + Math.floor(Math.random() * 301); - events.push({ name: "startNewChat", delta }); - const keyEvents = 6 + Math.floor(Math.random() * 13); - for (let i = 0; i < keyEvents; i++) { - delta += 40 + Math.floor(Math.random() * 141); - events.push({ name: "user_input", delta }); - } - delta += 120 + Math.floor(Math.random() * 231); - events.push({ name: "user_submit", delta }); - const payload = { - start, - events, - end: Math.max(delta + 20 + Math.floor(Math.random() * 71), 3000), - }; - return Buffer.from(JSON.stringify(payload), "utf8").toString("base64"); -} - function extractDuckDuckGoFeVersion(html: string): string | null { return html.match(FE_VERSION_PATTERN)?.[0] ?? null; } diff --git a/open-sse/executors/duckduckgo-web/challenge.ts b/open-sse/executors/duckduckgo-web/challenge.ts new file mode 100644 index 0000000000..4c3f3a6132 --- /dev/null +++ b/open-sse/executors/duckduckgo-web/challenge.ts @@ -0,0 +1,151 @@ +// DuckDuckGo anti-abuse challenge solver + FE signals (pure of module state). +// SECURITY: solveDuckDuckGoChallenge runs upstream-supplied JS in a vm sandbox with a +// 5s timeout (see inline note). Extracted verbatim from duckduckgo-web.ts. +import { createHash } from "node:crypto"; +import vm from "node:vm"; +import { parseFragment, serialize } from "parse5"; + +export const CHALLENGE_STUBS = String.raw` +var __ua = __DDG_REAL_UA__; +var __HTML_LOOKUP = __DDG_HTML_LOOKUP__; +export function __makeHtmlElement(tag) { + var state = { _innerHTML: '', _qsaCount: 0, _cssText: '' }; + var el = { + tagName: String(tag).toUpperCase(), nodeName: String(tag).toUpperCase(), nodeType: 1, + children: [], childNodes: [], classList: [], dataset: {}, + offsetWidth: 1, offsetHeight: 1, clientWidth: 1, clientHeight: 1, scrollHeight: 1, scrollWidth: 1, + getBoundingClientRect: function(){ return { x: 0, y: 0, top: 0, left: 0, right: 1, bottom: 1, width: 1, height: 1, toJSON: function(){ return {}; } }; }, + setAttribute: function(){}, removeAttribute: function(){}, + getAttribute: function(a){ if(a==='srcdoc') return state._srcdoc||''; return null; }, + hasAttribute: function(){ return false; }, appendChild: function(c){ return c; }, removeChild: function(c){ return c; }, + addEventListener: function(){}, removeEventListener: function(){}, querySelector: function(){ return null; }, + querySelectorAll: function(s){ if (s === '*') { var arr = []; arr.length = state._qsaCount; return arr; } return []; }, + cloneNode: function(){ return __makeHtmlElement(tag); } + }; + Object.defineProperty(el, 'style', { value: new Proxy({}, { set: function(t, k, v){ t[k] = v; if (k === 'cssText') state._cssText = String(v); return true; }, get: function(t, k){ if (k === 'cssText') return state._cssText; return t[k] || ''; } }), enumerable: true, configurable: true }); + Object.defineProperty(el, 'innerHTML', { get: function(){ return state._innerHTML; }, set: function(v){ var key = String(v); var entry = __HTML_LOOKUP && __HTML_LOOKUP[key]; if (entry) { state._innerHTML = String(entry.html); state._qsaCount = entry.count|0; } else { state._innerHTML = key; state._qsaCount = 0; } }, enumerable: true, configurable: true }); + Object.defineProperty(el, 'outerHTML', { get: function(){ return '<' + tag + '>' + state._innerHTML + ''; }, enumerable: true }); + Object.defineProperty(el, 'srcdoc', { get: function(){ return state._srcdoc||''; }, set: function(v){ state._srcdoc = String(v); }, enumerable: true }); + Object.defineProperty(el, 'contentWindow', { get: function(){ var w = {}; w.document = __ifDoc; w.Proxy = Proxy; w.self = w; w.top = w; w.parent = w; w.window = w; return w; }, enumerable: true }); + Object.defineProperty(el, 'contentDocument', { get: function(){ return __ifDoc; }, enumerable: true }); + return el; +} +export function __mkObj(name, base) { + base = base || {}; + return new Proxy(base, { + get: function(t, k) { + if (k in t) return t[k]; + if (k === Symbol.toPrimitive) return function(){ return ''; }; + if (k === Symbol.iterator) return undefined; + if (k === 'then' || k === 'catch' || k === 'finally') return undefined; + if (k === 'constructor') return Object; + if (k === 'toString' || k === 'valueOf') return function(){ return '[object ' + name + ']'; }; + if (k === 'length') return 0; + if (k === 'nodeType') return 1; + if (k === 'tagName' || k === 'nodeName') return 'DIV'; + if (k === 'innerHTML' || k === 'outerHTML' || k === 'textContent' || k === 'innerText' || k === 'value') return ''; + if (k === 'children' || k === 'childNodes' || k === 'classList') return []; + // Real numeric layout values for the DDG challenge DOM probes. + if (k === 'offsetWidth' || k === 'offsetHeight' || k === 'clientWidth' || k === 'clientHeight' || k === 'scrollHeight' || k === 'scrollWidth') return 1; + if (k === 'getBoundingClientRect') return function(){ return { x: 0, y: 0, top: 0, left: 0, right: 1, bottom: 1, width: 1, height: 1, toJSON: function(){ return {}; } }; }; + if (typeof k === 'string' && (k.indexOf('get') === 0 || k.indexOf('query') === 0 || k.indexOf('find') === 0)) return function(){ return k === 'querySelectorAll' || k === 'getElementsByTagName' || k === 'getElementsByClassName' ? [] : null; }; + return function(){ return __mkObj(name + '.' + String(k)); }; + }, + has: function(t, k){ return k in t; }, set: function(t, k, v){ t[k] = v; return true; } + }); +} +export function __parseCssDisplay(cssText){ if(!cssText) return ''; var m = String(cssText).match(/(?:^|;)\\s*display\\s*:\\s*([^;]+)/i); return m ? String(m[1]).trim() : ''; } +export function __getComputedStyle(el){ var cssText = el && el.style && el.style.cssText || ''; var display = __parseCssDisplay(cssText); return { getPropertyValue: function(name){ if(String(name).toLowerCase()==='display') return display; return ''; }, cssText: cssText, display: display }; } +var __ifMeta = __mkObj('meta', { getAttribute: function(a){ return a==='content' ? "default-src 'none'; script-src 'unsafe-inline';" : null; }, hasAttribute: function(a){ return a==='content'; }, tagName: 'META', nodeName: 'META' }); +var __ifDoc = __mkObj('iframeDoc', { querySelector: function(s){ if (s && s.indexOf('Content-Security-Policy') !== -1) return __ifMeta; if (s === 'meta') return __ifMeta; return null; }, querySelectorAll: function(s){ if (s && s.indexOf('Content-Security-Policy') !== -1) return [__ifMeta]; if (s === 'meta') return [__ifMeta]; return []; }, getElementsByTagName: function(t){ return t && t.toLowerCase()==='meta' ? [__ifMeta] : []; }, body: __mkObj('iframeBody'), head: __mkObj('iframeHead'), documentElement: __mkObj('iframeRoot'), createElement: function(){ return __mkObj('elem', {setAttribute:function(){}, appendChild:function(){}, removeChild:function(){}, getAttribute:function(){return null;}, hasAttribute:function(){return false;}}); }, cookie: '', readyState: 'complete' }); +var __iframeEl = __mkObj('iframe', { contentDocument: __ifDoc, contentWindow: __mkObj('iframeWin', { document: __ifDoc, top: undefined, parent: undefined }), document: __ifDoc, getAttribute: function(a){ if (a==='sandbox') return 'allow-scripts allow-same-origin'; if (a==='srcdoc') return ''; if (a==='id') return 'jsa'; return null; }, hasAttribute: function(a){ return a==='sandbox'||a==='id'; }, tagName: 'IFRAME', nodeName: 'IFRAME', id: 'jsa' }); +var document = __mkObj('document', { querySelector: function(s){ if (s === '#jsa') return __iframeEl; if (s && s.indexOf('Content-Security-Policy') !== -1) return __ifMeta; return null; }, querySelectorAll: function(s){ if (s === '#jsa') return [__iframeEl]; if (s && s.indexOf('Content-Security-Policy') !== -1) return [__ifMeta]; return []; }, getElementById: function(id){ return id==='jsa' ? __iframeEl : null; }, getElementsByTagName: function(t){ if(t&&t.toLowerCase()==='iframe') return [__iframeEl]; return []; }, getElementsByClassName: function(){ return []; }, body: __mkObj('body', {appendChild:function(){}, removeChild:function(){}, querySelector:function(s){return s==='#jsa'?__iframeEl:null;}, querySelectorAll:function(s){return s==='#jsa'?[__iframeEl]:[];}}), head: __mkObj('head'), documentElement: __mkObj('root'), createElement: function(tag){ return __makeHtmlElement(tag||'div'); }, createTextNode: function(t){ return {nodeType:3, nodeValue:String(t||''), textContent:String(t||'')}; }, cookie: '', readyState: 'complete', title: '', addEventListener: function(){}, removeEventListener: function(){} }); + var window = __mkObj('window', { document: document, __DDG_BE_VERSION__: 1, __DDG_FE_CHAT_HASH__: 1, navigator: __mkObj('navigator', { userAgent: __ua, webdriver: false, language: 'en-US', languages: ['en-US','en'], platform: 'Linux x86_64', vendor: 'Google Inc.', appVersion: '5.0 (X11)', cookieEnabled: true, onLine: true, hardwareConcurrency: 8, deviceMemory: 8 }), innerWidth: 1280, innerHeight: 800, outerWidth: 1280, outerHeight: 800, devicePixelRatio: 1, screen: __mkObj('screen', { width:1920, height:1080, availWidth:1920, availHeight:1080, colorDepth:24, pixelDepth:24 }), location: __mkObj('location', { href:'https://duck.ai/', origin:'https://duck.ai', host:'duck.ai', hostname:'duck.ai', protocol:'https:', pathname:'/' }), performance: __mkObj('perf', { now: function(){ return 0; }, timeOrigin: 0 }), history: __mkObj('history', { length: 1, state: null }), addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){return true;}, setTimeout: function(fn){ try{fn();}catch(e){} return 0; }, clearTimeout: function(){}, hasOwnProperty: function(k){ if (k==='__DDG_BE_VERSION__'||k==='__DDG_FE_CHAT_HASH__') return true; return Object.prototype.hasOwnProperty.call(this,k); } }); +window.top = window; window.self = window; window.window = window; window.parent = window; window.globalThis = window; +var top = window, self = window, parent = window, navigator = window.navigator, location = window.location, screen = window.screen, performance = window.performance, history = window.history; +var __R = null, __E = null; +export function __HTMLClass(name){ var c = function(){}; c.prototype = __mkObj(name+'.proto'); return c; } +var HTMLElement = __HTMLClass('HTMLElement'), HTMLDivElement = __HTMLClass('HTMLDivElement'), HTMLIFrameElement = __HTMLClass('HTMLIFrameElement'), HTMLDocument = __HTMLClass('HTMLDocument'), Document = __HTMLClass('Document'), Element = __HTMLClass('Element'), Node = __HTMLClass('Node'), Window = __HTMLClass('Window'), Event = __HTMLClass('Event'), MouseEvent = __HTMLClass('MouseEvent'), KeyboardEvent = __HTMLClass('KeyboardEvent'), TouchEvent = __HTMLClass('TouchEvent'), XMLHttpRequest = __HTMLClass('XMLHttpRequest'), WebSocket = __HTMLClass('WebSocket'), Image = __HTMLClass('Image'), FormData = __HTMLClass('FormData'), Blob = __HTMLClass('Blob'), File = __HTMLClass('File'), FileReader = __HTMLClass('FileReader'), URL = __HTMLClass('URL'), URLSearchParams = __HTMLClass('URLSearchParams'), Headers = __HTMLClass('Headers'), Request = __HTMLClass('Request'), Response = __HTMLClass('Response'); +var fetch = function(){ return Promise.resolve(__mkObj('resp', {ok:true, status:200, json:function(){return Promise.resolve({});}, text:function(){return Promise.resolve('');}})); }; +var getComputedStyle = __getComputedStyle; +`; + +export function countHtmlElements(node: unknown): number { + if (!node || typeof node !== "object") return 0; + const record = node as { nodeName?: string; childNodes?: unknown[] }; + const own = record.nodeName && record.nodeName !== "#document-fragment" ? 1 : 0; + let childCount = 0; + for (const child of record.childNodes ?? []) { + childCount += countHtmlElements(child); + } + return own + childCount; +} + +export function buildHtmlLookup(js: string): Record { + const lookup: Record = {}; + const seen = new Set(); + const pattern = /(['"])(<[^'"]{1,400}?)\1/g; + for (const match of js.matchAll(pattern)) { + const html = match[2]; + if (seen.has(html)) continue; + seen.add(html); + const fragment = parseFragment(html); + lookup[html] = { + html: serialize(fragment), + count: Math.max(0, countHtmlElements(fragment) - 1), + }; + } + return lookup; +} + +export function sha256Base64(value: string): string { + return createHash("sha256").update(value, "utf8").digest("base64"); +} + +export async function solveDuckDuckGoChallenge( + challenge: string, + userAgent: string +): Promise { + // SECURITY NOTE: This function executes base64-decoded JavaScript from duck.ai via vm.runInContext. + // The challenge code is upstream-supplied (supply-chain surface). It is sandboxed with a 5s timeout + // to limit DoS risk. This is intentional for the DDG challenge solver to work. + const js = Buffer.from(challenge, "base64").toString("utf8"); + const stubs = CHALLENGE_STUBS.replace("__DDG_REAL_UA__", JSON.stringify(userAgent)).replace( + "__DDG_HTML_LOOKUP__", + JSON.stringify(buildHtmlLookup(js)) + ); + const context = vm.createContext({}); + vm.runInContext(stubs, context, { timeout: 5000 }); + const result = (await vm.runInContext(js, context, { + timeout: 5000, + })) as DuckDuckGoChallengeResult; + const clientHashes = Array.isArray(result.client_hashes) ? result.client_hashes : []; + if (clientHashes.length === 0) + throw new Error("DuckDuckGo challenge returned empty client_hashes"); + clientHashes[0] = userAgent; + result.client_hashes = clientHashes.map((hash) => sha256Base64(String(hash))); + return Buffer.from(JSON.stringify(result), "utf8").toString("base64"); +} + +export function makeDuckDuckGoFeSignals(): string { + const start = Date.now() - 3000; + let delta = 80 + Math.floor(Math.random() * 101); + const events: Array> = [{ name: "onboarding_impression_1", delta }]; + delta += 120 + Math.floor(Math.random() * 141); + events.push({ name: "onboarding_impression_2", delta }); + delta += 200 + Math.floor(Math.random() * 301); + events.push({ name: "startNewChat", delta }); + const keyEvents = 6 + Math.floor(Math.random() * 13); + for (let i = 0; i < keyEvents; i++) { + delta += 40 + Math.floor(Math.random() * 141); + events.push({ name: "user_input", delta }); + } + delta += 120 + Math.floor(Math.random() * 231); + events.push({ name: "user_submit", delta }); + const payload = { + start, + events, + end: Math.max(delta + 20 + Math.floor(Math.random() * 71), 3000), + }; + return Buffer.from(JSON.stringify(payload), "utf8").toString("base64"); +} diff --git a/tests/unit/duckduckgo-challenge-split.test.ts b/tests/unit/duckduckgo-challenge-split.test.ts new file mode 100644 index 0000000000..cc4a7bd8a3 --- /dev/null +++ b/tests/unit/duckduckgo-challenge-split.test.ts @@ -0,0 +1,35 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +// Split-guard for the duckduckgo-web challenge-solver extraction. +// The anti-abuse challenge solver + FE signals live in duckduckgo-web/challenge.ts +// (pure of module state; the vm sandbox + 5s timeout are preserved). Host imports back. +const HERE = dirname(fileURLToPath(import.meta.url)); +const EXE = join(HERE, "../../open-sse/executors"); +const HOST = join(EXE, "duckduckgo-web.ts"); +const LEAF = join(EXE, "duckduckgo-web/challenge.ts"); + +test("leaf hosts the solver and does not import the host", () => { + const src = readFileSync(LEAF, "utf8"); + assert.match(src, /export async function solveDuckDuckGoChallenge\b/); + assert.match(src, /export function makeDuckDuckGoFeSignals\b/); + assert.doesNotMatch(src, /from "\.\.\/duckduckgo-web\.ts"/); + // The vm sandbox timeout must survive the move (security invariant). + assert.match(src, /timeout/); +}); + +test("host imports the solver back from the leaf", () => { + const host = readFileSync(HOST, "utf8"); + assert.match(host, /from "\.\/duckduckgo-web\/challenge\.ts"/); +}); + +test("makeDuckDuckGoFeSignals returns a base64 string", async () => { + const { makeDuckDuckGoFeSignals } = + await import("../../open-sse/executors/duckduckgo-web/challenge.ts"); + const out = makeDuckDuckGoFeSignals(); + assert.equal(typeof out, "string"); + assert.ok(out.length > 0); +});