@240xu/dsh-websearch 2.8.4 → 2.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/backends/ddg.js +6 -5
- package/lib/util/egress-fetch.js +66 -1
- package/package.json +1 -1
package/lib/backends/ddg.js
CHANGED
|
@@ -30,20 +30,21 @@ export const ddgBackend = {
|
|
|
30
30
|
throwIfSearchAborted(signal);
|
|
31
31
|
const params = new URLSearchParams({ q: request.query, b: Math.random().toString().slice(2) });
|
|
32
32
|
applyDdgFilters(params, normalizeFilters(request.filters));
|
|
33
|
-
const
|
|
33
|
+
const target = `${DDG_ENDPOINT}?${params.toString()}`;
|
|
34
34
|
hooks.recordRequest?.({ endpoint: DDG_ENDPOINT, toolName: "html", body: { q: request.query }, headers: {} });
|
|
35
35
|
|
|
36
36
|
let text;
|
|
37
37
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// GET form: DDG's html endpoint still serves the same results page, and
|
|
39
|
+
// unlike the POST form it is not rejected by the endpoint's bot wall
|
|
40
|
+
// for non-browser clients (POST is JA3 + method double-gated).
|
|
41
|
+
const res = await egressFetch(target, {
|
|
42
|
+
method: "GET",
|
|
40
43
|
redirect: "error",
|
|
41
44
|
headers: {
|
|
42
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
43
45
|
"Accept": "text/html",
|
|
44
46
|
"User-Agent": "dsh-websearch/2.3",
|
|
45
47
|
},
|
|
46
|
-
body,
|
|
47
48
|
...(signal !== undefined ? { signal } : {}),
|
|
48
49
|
});
|
|
49
50
|
if (!res.ok) {
|
package/lib/util/egress-fetch.js
CHANGED
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
//
|
|
11
11
|
// When no matching proxy env is set, or the target host sits in NO_PROXY, it
|
|
12
12
|
// defers to the native fetch, so behavior everywhere else is unchanged.
|
|
13
|
+
//
|
|
14
|
+
// Optional TLS-egress forwarder: when DSH_EGRESS_FORWARDER is set (a local
|
|
15
|
+
// sidecar exposing a POST /fwd API backed by a Chromium TLS stack), requests
|
|
16
|
+
// are relayed through it with the upstream proxy carried in the sidecar spec
|
|
17
|
+
// (DSH_EGRESS_FORWARDER_PROXY, defaulting to HTTPS_PROXY). This exists because
|
|
18
|
+
// some upstreams (Exa, DuckDuckGo) gate on the client TLS fingerprint (JA3)
|
|
19
|
+
// rather than on the exit IP; the Node TLS fingerprint is rejected where a
|
|
20
|
+
// real browser's is accepted. Unset the env and behavior is byte-identical.
|
|
13
21
|
|
|
14
22
|
import http from "node:http";
|
|
15
23
|
import https from "node:https";
|
|
@@ -17,13 +25,33 @@ import https from "node:https";
|
|
|
17
25
|
const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
|
|
18
26
|
const MAX_REDIRECTS = 5;
|
|
19
27
|
|
|
20
|
-
/** fetch-compatible entry:
|
|
28
|
+
/** fetch-compatible entry: forwarder first, then HTTPS_PROXY/HTTP_PROXY tunnel. */
|
|
21
29
|
export function egressFetch(url, init = {}) {
|
|
30
|
+
const forwarder = forwarderFor(url);
|
|
31
|
+
if (forwarder !== undefined) return forwarderFetch(String(url), init, forwarder);
|
|
22
32
|
const proxy = proxyFor(url);
|
|
23
33
|
if (proxy === undefined) return fetch(url, init);
|
|
24
34
|
return tunneledFetch(String(url), init, proxy, 0);
|
|
25
35
|
}
|
|
26
36
|
|
|
37
|
+
/** Resolve the TLS-egress forwarder for a request target (NO_PROXY aware). */
|
|
38
|
+
function forwarderFor(url) {
|
|
39
|
+
let target;
|
|
40
|
+
try {
|
|
41
|
+
target = new URL(String(url));
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (target.protocol !== "https:" && target.protocol !== "http:") return undefined;
|
|
46
|
+
const env = process.env;
|
|
47
|
+
const endpoint = env.DSH_EGRESS_FORWARDER;
|
|
48
|
+
if (endpoint === undefined || endpoint === "") return undefined;
|
|
49
|
+
if (inNoProxy(target, env.NO_PROXY || env.no_proxy)) return undefined;
|
|
50
|
+
const proxy = env.DSH_EGRESS_FORWARDER_PROXY
|
|
51
|
+
|| env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || "";
|
|
52
|
+
return { endpoint, proxy: proxy === "" ? undefined : proxy };
|
|
53
|
+
}
|
|
54
|
+
|
|
27
55
|
/** Resolve the proxy URL for a request target (NO_PROXY aware). */
|
|
28
56
|
function proxyFor(url) {
|
|
29
57
|
let target;
|
|
@@ -110,6 +138,43 @@ async function tunneledFetch(url, init, proxy, depth) {
|
|
|
110
138
|
return response;
|
|
111
139
|
}
|
|
112
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Relay through the TLS-egress sidecar: POST /fwd with an
|
|
143
|
+
* x-sidecar-spec base64 JSON header; the sidecar performs the upstream
|
|
144
|
+
* request with its own (Chromium) TLS stack and streams the upstream
|
|
145
|
+
* status/headers/body back verbatim. Upstream dial failures surface as
|
|
146
|
+
* 502 + x-sidecar-error, which we rethrow as fetch-style TypeError.
|
|
147
|
+
*/
|
|
148
|
+
async function forwarderFetch(url, init, forwarder) {
|
|
149
|
+
const method = String(init.method ?? "GET").toUpperCase();
|
|
150
|
+
const headers = normalizeHeaders(init.headers);
|
|
151
|
+
const body = normalizeBody(init.body);
|
|
152
|
+
const spec = { url, method, headers, timeout_ms: 90000 };
|
|
153
|
+
if (forwarder.proxy !== undefined) spec.proxy = forwarder.proxy;
|
|
154
|
+
if (init.redirect !== undefined) spec.redirect = init.redirect;
|
|
155
|
+
let response;
|
|
156
|
+
try {
|
|
157
|
+
response = await fetch(forwarder.endpoint, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: {
|
|
160
|
+
"content-type": "application/octet-stream",
|
|
161
|
+
"x-sidecar-spec": Buffer.from(JSON.stringify(spec)).toString("base64"),
|
|
162
|
+
},
|
|
163
|
+
...(body !== undefined ? { body } : {}),
|
|
164
|
+
...(init.signal !== undefined ? { signal: init.signal } : {}),
|
|
165
|
+
});
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new TypeError(`fetch failed: forwarder unreachable (${sanitize(error)})`, { cause: error });
|
|
168
|
+
}
|
|
169
|
+
const marker = response.headers.get("x-sidecar-error");
|
|
170
|
+
if (response.status === 502 && marker !== null) {
|
|
171
|
+
let detail = "";
|
|
172
|
+
try { detail = (await response.text()).slice(0, 200); } catch {}
|
|
173
|
+
throw new TypeError(`fetch failed: forwarder ${marker}${detail !== "" ? ` (${detail})` : ""}`);
|
|
174
|
+
}
|
|
175
|
+
return response;
|
|
176
|
+
}
|
|
177
|
+
|
|
113
178
|
/* ------------------------------------------------------------------ */
|
|
114
179
|
/* transports */
|
|
115
180
|
/* ------------------------------------------------------------------ */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@240xu/dsh-websearch",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.5",
|
|
4
4
|
"description": "Unified web search provider for DeepSeek Harness (ctx.web): 11 backends — Exa + Parallel + DDG + SearXNG (keyless), DeepSeek + Anthropic + OpenAI + Brave + Tavily + Serper + Mojeek (key-gated via Settings credentials). Registers provider id \"unified\" for zero-config web_search. 统一网页搜索聚合:11 后端并发扇出、去重合并,零依赖。See CHANGELOG.md for version history.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|