stealth-fetch 0.1.2
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.
Potentially problematic release.
This version of stealth-fetch might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/client.d.ts +99 -0
- package/dist/client.js +1117 -0
- package/dist/compat/web.d.ts +15 -0
- package/dist/compat/web.js +31 -0
- package/dist/connection-pool.d.ts +39 -0
- package/dist/connection-pool.js +84 -0
- package/dist/dns-cache.d.ts +25 -0
- package/dist/dns-cache.js +44 -0
- package/dist/http1/chunked.d.ts +35 -0
- package/dist/http1/chunked.js +87 -0
- package/dist/http1/client.d.ts +28 -0
- package/dist/http1/client.js +289 -0
- package/dist/http1/parser.d.ts +29 -0
- package/dist/http1/parser.js +78 -0
- package/dist/http2/client.d.ts +64 -0
- package/dist/http2/client.js +97 -0
- package/dist/http2/connection.d.ts +125 -0
- package/dist/http2/connection.js +666 -0
- package/dist/http2/constants.d.ts +72 -0
- package/dist/http2/constants.js +74 -0
- package/dist/http2/flow-control.d.ts +32 -0
- package/dist/http2/flow-control.js +76 -0
- package/dist/http2/framer.d.ts +47 -0
- package/dist/http2/framer.js +133 -0
- package/dist/http2/hpack.d.ts +54 -0
- package/dist/http2/hpack.js +186 -0
- package/dist/http2/parser.d.ts +35 -0
- package/dist/http2/parser.js +72 -0
- package/dist/http2/stream.d.ts +72 -0
- package/dist/http2/stream.js +252 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +33 -0
- package/dist/protocol-cache.d.ts +14 -0
- package/dist/protocol-cache.js +29 -0
- package/dist/socket/adapter.d.ts +59 -0
- package/dist/socket/adapter.js +145 -0
- package/dist/socket/nat64.d.ts +69 -0
- package/dist/socket/nat64.js +196 -0
- package/dist/socket/tls.d.ts +28 -0
- package/dist/socket/tls.js +33 -0
- package/dist/socket/wasm-pkg/wasm_tls.d.ts +107 -0
- package/dist/socket/wasm-pkg/wasm_tls.js +568 -0
- package/dist/socket/wasm-pkg/wasm_tls_bg.wasm +0 -0
- package/dist/socket/wasm-pkg/wasm_tls_bg.wasm.d.ts +20 -0
- package/dist/socket/wasm-tls-adapter.d.ts +40 -0
- package/dist/socket/wasm-tls-adapter.js +102 -0
- package/dist/socket/wasm-tls-bridge.d.ts +30 -0
- package/dist/socket/wasm-tls-bridge.js +187 -0
- package/dist/utils/headers.d.ts +21 -0
- package/dist/utils/headers.js +36 -0
- package/dist/utils/url.d.ts +16 -0
- package/dist/utils/url.js +12 -0
- package/package.json +87 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Duplex } from "node:stream";
|
|
2
|
+
class CloudflareSocketAdapter extends Duplex {
|
|
3
|
+
reader = null;
|
|
4
|
+
writer = null;
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
cfSocket = null;
|
|
7
|
+
reading = false;
|
|
8
|
+
connected = false;
|
|
9
|
+
hostname;
|
|
10
|
+
port;
|
|
11
|
+
useTls;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
this.hostname = options.hostname;
|
|
15
|
+
this.port = options.port;
|
|
16
|
+
this.useTls = options.tls;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Establish the underlying cloudflare:sockets connection.
|
|
20
|
+
* Must be called before using the stream.
|
|
21
|
+
*/
|
|
22
|
+
async connect() {
|
|
23
|
+
const { connect } = await import("cloudflare:sockets");
|
|
24
|
+
const address = { hostname: this.hostname, port: this.port };
|
|
25
|
+
console.debug(`[socket] connect(${this.hostname}:${this.port} tls=${this.useTls})`);
|
|
26
|
+
this.cfSocket = this.useTls ? connect(address, { secureTransport: "on" }) : connect(address);
|
|
27
|
+
this.writer = this.cfSocket.writable.getWriter();
|
|
28
|
+
this.reader = this.cfSocket.readable.getReader();
|
|
29
|
+
this.connected = true;
|
|
30
|
+
this.cfSocket.closed.then(() => {
|
|
31
|
+
console.debug(`[socket] closed(${this.hostname}:${this.port}) destroyed=${this.destroyed}`);
|
|
32
|
+
this.connected = false;
|
|
33
|
+
if (!this.destroyed) {
|
|
34
|
+
this.push(null);
|
|
35
|
+
this.emit("close");
|
|
36
|
+
}
|
|
37
|
+
}).catch((err) => {
|
|
38
|
+
console.debug(
|
|
39
|
+
`[socket] closed-error(${this.hostname}:${this.port}) destroyed=${this.destroyed} err=${err.message}`
|
|
40
|
+
);
|
|
41
|
+
this.connected = false;
|
|
42
|
+
if (!this.destroyed) {
|
|
43
|
+
this.destroy(err);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
this.emit("connect");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Node.js Duplex _read: pull data from the CF socket's ReadableStream.
|
|
50
|
+
* Starts a read loop that pushes data into the Duplex readable side.
|
|
51
|
+
*/
|
|
52
|
+
_read() {
|
|
53
|
+
if (this.reading || !this.reader) return;
|
|
54
|
+
this.reading = true;
|
|
55
|
+
this.readLoop();
|
|
56
|
+
}
|
|
57
|
+
async readLoop() {
|
|
58
|
+
const reader = this.reader;
|
|
59
|
+
if (!reader) {
|
|
60
|
+
this.reading = false;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
while (this.reading) {
|
|
65
|
+
const { done, value } = await reader.read();
|
|
66
|
+
if (done) {
|
|
67
|
+
this.reading = false;
|
|
68
|
+
this.push(null);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!this.push(value)) {
|
|
72
|
+
this.reading = false;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
this.reading = false;
|
|
78
|
+
if (!this.destroyed) {
|
|
79
|
+
this.destroy(err);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Node.js Duplex _write: write data to the CF socket's WritableStream.
|
|
85
|
+
*/
|
|
86
|
+
_write(chunk, _encoding, callback) {
|
|
87
|
+
if (!this.writer || !this.connected) {
|
|
88
|
+
callback(new Error("Socket not connected"));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const data = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
92
|
+
this.writer.write(data).then(
|
|
93
|
+
() => callback(),
|
|
94
|
+
(err) => callback(err)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Node.js Duplex _final: signal end of writes.
|
|
99
|
+
*/
|
|
100
|
+
_final(callback) {
|
|
101
|
+
if (this.writer) {
|
|
102
|
+
this.writer.close().then(
|
|
103
|
+
() => callback(),
|
|
104
|
+
(err) => callback(err)
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
callback();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Node.js Duplex _destroy: clean up all resources.
|
|
112
|
+
* Uses synchronous close() calls to avoid leaving pending promises
|
|
113
|
+
* that can corrupt CF Workers isolate state.
|
|
114
|
+
*/
|
|
115
|
+
_destroy(error, callback) {
|
|
116
|
+
console.debug(
|
|
117
|
+
`[socket] _destroy(${this.hostname}:${this.port}) error=${error?.message ?? "none"}`
|
|
118
|
+
);
|
|
119
|
+
this.reading = false;
|
|
120
|
+
this.connected = false;
|
|
121
|
+
if (this.reader) {
|
|
122
|
+
this.reader.cancel().catch(() => {
|
|
123
|
+
});
|
|
124
|
+
this.reader = null;
|
|
125
|
+
}
|
|
126
|
+
if (this.writer) {
|
|
127
|
+
this.writer.abort().catch(() => {
|
|
128
|
+
});
|
|
129
|
+
this.writer = null;
|
|
130
|
+
}
|
|
131
|
+
if (this.cfSocket) {
|
|
132
|
+
this.cfSocket.close().catch(() => {
|
|
133
|
+
});
|
|
134
|
+
this.cfSocket = null;
|
|
135
|
+
}
|
|
136
|
+
callback(error);
|
|
137
|
+
}
|
|
138
|
+
/** Whether the socket is currently connected */
|
|
139
|
+
get isConnected() {
|
|
140
|
+
return this.connected;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
CloudflareSocketAdapter
|
|
145
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NAT64 address resolution for bypassing Cloudflare Workers
|
|
3
|
+
* outbound socket restrictions.
|
|
4
|
+
*
|
|
5
|
+
* Converts IPv4 targets to NAT64 IPv6 addresses using public NAT64 gateways.
|
|
6
|
+
* Prefixes verified via RFC 7050 (querying DNS64 servers for ipv4only.arpa).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* NAT64 /96 prefixes, ordered by reliability.
|
|
10
|
+
* Format: full expanded prefix ending with ":" (no trailing "::").
|
|
11
|
+
* The last 32 bits (2 hextets) are filled with the embedded IPv4 address.
|
|
12
|
+
*
|
|
13
|
+
* To convert: prefix + hex(octet1)hex(octet2):hex(octet3)hex(octet4)
|
|
14
|
+
* Example: "2602:fc59:b0:64::" + 108.160.165.8 → [2602:fc59:b0:64::6ca0:a508]
|
|
15
|
+
*/
|
|
16
|
+
declare const NAT64_PREFIXES: string[];
|
|
17
|
+
|
|
18
|
+
interface DnsARecord {
|
|
19
|
+
ipv4: string;
|
|
20
|
+
ttl: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve hostname to IPv4 via DNS-over-HTTPS (Cloudflare 1.1.1.1).
|
|
24
|
+
* Uses fetch() which is always available in CF Workers.
|
|
25
|
+
*/
|
|
26
|
+
declare function resolveIPv4(hostname: string): Promise<DnsARecord | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Convert IPv4 string to NAT64 IPv6 address (bracketed for connect()).
|
|
29
|
+
*
|
|
30
|
+
* Handles two prefix formats:
|
|
31
|
+
* - Ending with "::" (short prefix, e.g. "2602:fc59:b0:64::")
|
|
32
|
+
* → [2602:fc59:b0:64::6ca0:a508]
|
|
33
|
+
* - Ending with ":" (full prefix, e.g. "2a00:1098:2b:0:0:1:")
|
|
34
|
+
* → [2a00:1098:2b:0:0:1:6ca0:a508]
|
|
35
|
+
*/
|
|
36
|
+
declare function ipv4ToNAT64(ipv4: string, prefix: string): string;
|
|
37
|
+
interface BypassResult {
|
|
38
|
+
strategy: "nat64";
|
|
39
|
+
/** The hostname to pass to connect() */
|
|
40
|
+
connectHostname: string;
|
|
41
|
+
/** NAT64 prefix used */
|
|
42
|
+
nat64Prefix: string;
|
|
43
|
+
/** Original IPv4 resolved from DNS */
|
|
44
|
+
resolvedIPv4: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate NAT64 bypass hostname candidates for a given domain.
|
|
48
|
+
* Returns an array of { connectHostname, nat64Prefix } to try in order.
|
|
49
|
+
*/
|
|
50
|
+
declare function generateBypassCandidates(hostname: string): Promise<BypassResult[]>;
|
|
51
|
+
/** Check if an error is a CF Workers network restriction error */
|
|
52
|
+
declare function isCloudflareNetworkError(err: unknown): boolean;
|
|
53
|
+
declare function isCloudflareIPv4(ipv4: string): boolean;
|
|
54
|
+
declare function isCloudflareIPv6(ipv6: string): boolean;
|
|
55
|
+
interface CfCheckResult {
|
|
56
|
+
isCf: boolean;
|
|
57
|
+
ipv4: string | null;
|
|
58
|
+
ipv6: string | null;
|
|
59
|
+
dnsMs: number;
|
|
60
|
+
/** Minimum TTL from A/AAAA records (seconds). 0 if no records found. */
|
|
61
|
+
ttl: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve hostname via DoH (parallel A + AAAA), check if IP is in CF ranges.
|
|
65
|
+
* Used to pre-detect CF CDN targets that need NAT64 bypass.
|
|
66
|
+
*/
|
|
67
|
+
declare function resolveAndCheckCloudflare(hostname: string): Promise<CfCheckResult>;
|
|
68
|
+
|
|
69
|
+
export { type BypassResult, type CfCheckResult, type DnsARecord, NAT64_PREFIXES, generateBypassCandidates, ipv4ToNAT64, isCloudflareIPv4, isCloudflareIPv6, isCloudflareNetworkError, resolveAndCheckCloudflare, resolveIPv4 };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const NAT64_PREFIXES = [
|
|
2
|
+
// === Verified working from CF Workers (deploy tested) ===
|
|
3
|
+
"2602:fc59:b0:64::",
|
|
4
|
+
// ZTVI/ForwardingPlane, Fremont CA, USA — 12ms
|
|
5
|
+
"2602:fc59:11:64::",
|
|
6
|
+
// ZTVI/ForwardingPlane, Chicago, USA — 60ms
|
|
7
|
+
"2a00:1098:2b:0:0:1:",
|
|
8
|
+
// Kasper Dupont (nat64.net), Amsterdam — 150ms
|
|
9
|
+
"2a00:1098:2c:0:0:5:",
|
|
10
|
+
// Kasper Dupont (nat64.net), London — 140ms
|
|
11
|
+
"2a02:898:146:64::",
|
|
12
|
+
// IPng Networks, Netherlands — 155ms
|
|
13
|
+
"2001:67c:2b0:db32::",
|
|
14
|
+
// Trex, Tampere, Finland — 195ms
|
|
15
|
+
// === Additional ZTVI/ForwardingPlane prefixes ===
|
|
16
|
+
"2602:fc59:20::",
|
|
17
|
+
// ZTVI/ForwardingPlane, unknown location
|
|
18
|
+
// === Kasper Dupont (nat64.net) additional locations ===
|
|
19
|
+
"2a00:1098:2c:1::",
|
|
20
|
+
// nat64.net, London (official /96 prefix)
|
|
21
|
+
"2a01:4f8:c2c:123f:64:5:",
|
|
22
|
+
// nat64.net, Nuremberg, Germany
|
|
23
|
+
"2a01:4f8:c2c:123f:64::",
|
|
24
|
+
// nat64.net, Nuremberg (alternate form)
|
|
25
|
+
"2a01:4f9:c010:3f02:64:0:",
|
|
26
|
+
// nat64.net, Helsinki, Finland
|
|
27
|
+
"2a01:4f9:c010:3f02:64::",
|
|
28
|
+
// nat64.net, Helsinki (alternate form)
|
|
29
|
+
// === level66.network ===
|
|
30
|
+
"2001:67c:2960:6464::",
|
|
31
|
+
// level66.network, Germany (Anycast)
|
|
32
|
+
"2a09:11c0:f1:be00::",
|
|
33
|
+
// level66.network, Frankfurt
|
|
34
|
+
// === go6Labs, Slovenia ===
|
|
35
|
+
"2001:67c:27e4:642::",
|
|
36
|
+
// go6Labs, Slovenia
|
|
37
|
+
"2001:67c:27e4:64::",
|
|
38
|
+
// go6Labs, Slovenia
|
|
39
|
+
"2001:67c:27e4:1064::",
|
|
40
|
+
// go6Labs, Slovenia
|
|
41
|
+
"2001:67c:27e4:11::",
|
|
42
|
+
// go6Labs, Slovenia
|
|
43
|
+
// === Other providers ===
|
|
44
|
+
"2a03:7900:6446::",
|
|
45
|
+
// Tuxis, Netherlands
|
|
46
|
+
"2001:67c:2b0:db32:0:1:"
|
|
47
|
+
// Trex, second prefix, Finland
|
|
48
|
+
];
|
|
49
|
+
async function resolveIPv4(hostname) {
|
|
50
|
+
const resp = await fetch(
|
|
51
|
+
`https://1.1.1.1/dns-query?name=${encodeURIComponent(hostname)}&type=A`,
|
|
52
|
+
{ headers: { Accept: "application/dns-json" }, signal: AbortSignal.timeout(3e3) }
|
|
53
|
+
);
|
|
54
|
+
if (!resp.ok) return null;
|
|
55
|
+
const data = await resp.json();
|
|
56
|
+
if (!data.Answer) return null;
|
|
57
|
+
const aRecord = data.Answer.find((r) => r.type === 1);
|
|
58
|
+
if (!aRecord) return null;
|
|
59
|
+
return { ipv4: aRecord.data, ttl: aRecord.TTL };
|
|
60
|
+
}
|
|
61
|
+
function ipv4ToNAT64(ipv4, prefix) {
|
|
62
|
+
const parts = ipv4.split(".");
|
|
63
|
+
if (parts.length !== 4) throw new Error(`Invalid IPv4: ${ipv4}`);
|
|
64
|
+
const hex = parts.map((p) => {
|
|
65
|
+
const n = parseInt(p, 10);
|
|
66
|
+
if (n < 0 || n > 255) throw new Error(`Invalid IPv4 octet: ${p}`);
|
|
67
|
+
return n.toString(16).padStart(2, "0");
|
|
68
|
+
});
|
|
69
|
+
const suffix = `${hex[0]}${hex[1]}:${hex[2]}${hex[3]}`;
|
|
70
|
+
return `[${prefix}${suffix}]`;
|
|
71
|
+
}
|
|
72
|
+
async function generateBypassCandidates(hostname) {
|
|
73
|
+
const dns = await resolveIPv4(hostname);
|
|
74
|
+
if (!dns) return [];
|
|
75
|
+
return NAT64_PREFIXES.map((prefix) => ({
|
|
76
|
+
strategy: "nat64",
|
|
77
|
+
connectHostname: ipv4ToNAT64(dns.ipv4, prefix),
|
|
78
|
+
nat64Prefix: prefix,
|
|
79
|
+
resolvedIPv4: dns.ipv4
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
function isCloudflareNetworkError(err) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
return msg.includes("cannot connect to the specified address") || msg.includes("A network issue was detected") || msg.includes("TCP Loop detected");
|
|
85
|
+
}
|
|
86
|
+
const CF_IPV4_RANGES = (() => {
|
|
87
|
+
const toU32 = (ip) => {
|
|
88
|
+
const p = ip.split(".").map(Number);
|
|
89
|
+
return (p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3]) >>> 0;
|
|
90
|
+
};
|
|
91
|
+
return [
|
|
92
|
+
[toU32("173.245.48.0"), toU32("173.245.63.255")],
|
|
93
|
+
// /20
|
|
94
|
+
[toU32("103.21.244.0"), toU32("103.21.247.255")],
|
|
95
|
+
// /22
|
|
96
|
+
[toU32("103.22.200.0"), toU32("103.22.203.255")],
|
|
97
|
+
// /22
|
|
98
|
+
[toU32("103.31.4.0"), toU32("103.31.7.255")],
|
|
99
|
+
// /22
|
|
100
|
+
[toU32("141.101.64.0"), toU32("141.101.127.255")],
|
|
101
|
+
// /18
|
|
102
|
+
[toU32("108.162.192.0"), toU32("108.162.255.255")],
|
|
103
|
+
// /18
|
|
104
|
+
[toU32("190.93.240.0"), toU32("190.93.255.255")],
|
|
105
|
+
// /20
|
|
106
|
+
[toU32("188.114.96.0"), toU32("188.114.111.255")],
|
|
107
|
+
// /20
|
|
108
|
+
[toU32("197.234.240.0"), toU32("197.234.243.255")],
|
|
109
|
+
// /22
|
|
110
|
+
[toU32("198.41.128.0"), toU32("198.41.255.255")],
|
|
111
|
+
// /17
|
|
112
|
+
[toU32("162.158.0.0"), toU32("162.159.255.255")],
|
|
113
|
+
// /15
|
|
114
|
+
[toU32("104.16.0.0"), toU32("104.27.255.255")],
|
|
115
|
+
// /12
|
|
116
|
+
[toU32("172.64.0.0"), toU32("172.71.255.255")],
|
|
117
|
+
// /13
|
|
118
|
+
[toU32("131.0.72.0"), toU32("131.0.75.255")]
|
|
119
|
+
// /22
|
|
120
|
+
];
|
|
121
|
+
})();
|
|
122
|
+
const CF_IPV6_PREFIXES = [
|
|
123
|
+
"2400:cb00",
|
|
124
|
+
// 2400:cb00::/32
|
|
125
|
+
"2606:4700",
|
|
126
|
+
// 2606:4700::/32
|
|
127
|
+
"2803:f800",
|
|
128
|
+
// 2803:f800::/32
|
|
129
|
+
"2405:8100",
|
|
130
|
+
// 2405:8100::/32
|
|
131
|
+
"2a06:98c0",
|
|
132
|
+
// 2a06:98c0::/29
|
|
133
|
+
"2c0f:f248"
|
|
134
|
+
// 2c0f:f248::/32
|
|
135
|
+
];
|
|
136
|
+
function ipv4ToUint32(ip) {
|
|
137
|
+
const p = ip.split(".").map(Number);
|
|
138
|
+
return (p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3]) >>> 0;
|
|
139
|
+
}
|
|
140
|
+
function isCloudflareIPv4(ipv4) {
|
|
141
|
+
const num = ipv4ToUint32(ipv4);
|
|
142
|
+
return CF_IPV4_RANGES.some(([start, end]) => num >= start && num <= end);
|
|
143
|
+
}
|
|
144
|
+
function isCloudflareIPv6(ipv6) {
|
|
145
|
+
const lower = ipv6.toLowerCase();
|
|
146
|
+
return CF_IPV6_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
147
|
+
}
|
|
148
|
+
async function resolveAndCheckCloudflare(hostname) {
|
|
149
|
+
const t0 = Date.now();
|
|
150
|
+
const dnsSignal = AbortSignal.timeout(3e3);
|
|
151
|
+
const [aResp, aaaaResp] = await Promise.all([
|
|
152
|
+
fetch(`https://1.1.1.1/dns-query?name=${encodeURIComponent(hostname)}&type=A`, {
|
|
153
|
+
headers: { Accept: "application/dns-json" },
|
|
154
|
+
signal: dnsSignal
|
|
155
|
+
}),
|
|
156
|
+
fetch(`https://1.1.1.1/dns-query?name=${encodeURIComponent(hostname)}&type=AAAA`, {
|
|
157
|
+
headers: { Accept: "application/dns-json" },
|
|
158
|
+
signal: dnsSignal
|
|
159
|
+
})
|
|
160
|
+
]);
|
|
161
|
+
const dnsMs = Date.now() - t0;
|
|
162
|
+
let ipv4 = null;
|
|
163
|
+
let ipv6 = null;
|
|
164
|
+
let isCf = false;
|
|
165
|
+
const ttls = [];
|
|
166
|
+
if (aResp.ok) {
|
|
167
|
+
const data = await aResp.json();
|
|
168
|
+
const aRecord = data.Answer?.find((r) => r.type === 1);
|
|
169
|
+
if (aRecord) {
|
|
170
|
+
ipv4 = aRecord.data;
|
|
171
|
+
ttls.push(aRecord.TTL);
|
|
172
|
+
if (isCloudflareIPv4(ipv4)) isCf = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (aaaaResp.ok) {
|
|
176
|
+
const data = await aaaaResp.json();
|
|
177
|
+
const aaaaRecord = data.Answer?.find((r) => r.type === 28);
|
|
178
|
+
if (aaaaRecord) {
|
|
179
|
+
ipv6 = aaaaRecord.data;
|
|
180
|
+
ttls.push(aaaaRecord.TTL);
|
|
181
|
+
if (isCloudflareIPv6(ipv6)) isCf = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const ttl = ttls.length > 0 ? Math.min(...ttls) : 0;
|
|
185
|
+
return { isCf, ipv4, ipv6, dnsMs, ttl };
|
|
186
|
+
}
|
|
187
|
+
export {
|
|
188
|
+
NAT64_PREFIXES,
|
|
189
|
+
generateBypassCandidates,
|
|
190
|
+
ipv4ToNAT64,
|
|
191
|
+
isCloudflareIPv4,
|
|
192
|
+
isCloudflareIPv6,
|
|
193
|
+
isCloudflareNetworkError,
|
|
194
|
+
resolveAndCheckCloudflare,
|
|
195
|
+
resolveIPv4
|
|
196
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CloudflareSocketAdapter } from './adapter.js';
|
|
2
|
+
import { WasmTlsSocketAdapter } from './wasm-tls-adapter.js';
|
|
3
|
+
import 'node:stream';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TLS/TCP connection factory functions.
|
|
7
|
+
* Creates CloudflareSocketAdapter instances with appropriate TLS settings.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Create a TLS-encrypted socket connection (using CF built-in TLS, no ALPN control) */
|
|
11
|
+
declare function createTLSSocket(hostname: string, port?: number): Promise<CloudflareSocketAdapter>;
|
|
12
|
+
/** Create a plain TCP socket connection (no TLS) */
|
|
13
|
+
declare function createPlainSocket(hostname: string, port?: number): Promise<CloudflareSocketAdapter>;
|
|
14
|
+
/** Create a socket with auto TLS based on port/protocol */
|
|
15
|
+
declare function createSocket(hostname: string, port: number, tls: boolean): Promise<CloudflareSocketAdapter>;
|
|
16
|
+
/**
|
|
17
|
+
* Create a WASM TLS socket with full ALPN control.
|
|
18
|
+
* Uses raw TCP (secureTransport: "off") + rustls in WASM for TLS.
|
|
19
|
+
* This allows ALPN negotiation (required for HTTP/2 over TLS).
|
|
20
|
+
* @param hostname - Target hostname (used for TLS SNI)
|
|
21
|
+
* @param port - Target port
|
|
22
|
+
* @param alpnProtocols - ALPN protocol list
|
|
23
|
+
* @param connectHostname - Optional override for TCP connection hostname
|
|
24
|
+
* (e.g. NAT64 IPv6 address). TLS SNI will still use `hostname`.
|
|
25
|
+
*/
|
|
26
|
+
declare function createWasmTLSSocket(hostname: string, port?: number, alpnProtocols?: string[], connectHostname?: string, signal?: AbortSignal): Promise<WasmTlsSocketAdapter>;
|
|
27
|
+
|
|
28
|
+
export { createPlainSocket, createSocket, createTLSSocket, createWasmTLSSocket };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CloudflareSocketAdapter } from "./adapter.js";
|
|
2
|
+
import { WasmTlsSocketAdapter } from "./wasm-tls-adapter.js";
|
|
3
|
+
async function createTLSSocket(hostname, port = 443) {
|
|
4
|
+
const socket = new CloudflareSocketAdapter({ hostname, port, tls: true });
|
|
5
|
+
await socket.connect();
|
|
6
|
+
return socket;
|
|
7
|
+
}
|
|
8
|
+
async function createPlainSocket(hostname, port = 80) {
|
|
9
|
+
const socket = new CloudflareSocketAdapter({ hostname, port, tls: false });
|
|
10
|
+
await socket.connect();
|
|
11
|
+
return socket;
|
|
12
|
+
}
|
|
13
|
+
async function createSocket(hostname, port, tls) {
|
|
14
|
+
const socket = new CloudflareSocketAdapter({ hostname, port, tls });
|
|
15
|
+
await socket.connect();
|
|
16
|
+
return socket;
|
|
17
|
+
}
|
|
18
|
+
async function createWasmTLSSocket(hostname, port = 443, alpnProtocols = ["h2", "http/1.1"], connectHostname, signal) {
|
|
19
|
+
const socket = new WasmTlsSocketAdapter({
|
|
20
|
+
hostname,
|
|
21
|
+
port,
|
|
22
|
+
alpnProtocols,
|
|
23
|
+
connectHostname
|
|
24
|
+
});
|
|
25
|
+
await socket.connect(signal);
|
|
26
|
+
return socket;
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
createPlainSocket,
|
|
30
|
+
createSocket,
|
|
31
|
+
createTLSSocket,
|
|
32
|
+
createWasmTLSSocket
|
|
33
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TLS connection state, exposed to JS via wasm-bindgen.
|
|
6
|
+
* Uses rustls with buffer-based sync IO — JS layer drives socket IO asynchronously.
|
|
7
|
+
*/
|
|
8
|
+
export class TlsConnection {
|
|
9
|
+
free(): void;
|
|
10
|
+
[Symbol.dispose](): void;
|
|
11
|
+
/**
|
|
12
|
+
* Feed ciphertext received from the network into the TLS engine.
|
|
13
|
+
* Returns true if rustls has outgoing data to send (call `flush_outgoing_tls`).
|
|
14
|
+
*/
|
|
15
|
+
feed_ciphertext(data: Uint8Array): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Flush ciphertext produced by rustls (to be sent over the network).
|
|
18
|
+
* Returns the ciphertext bytes as a Vec<u8> (becomes Uint8Array in JS).
|
|
19
|
+
*/
|
|
20
|
+
flush_outgoing_tls(): Uint8Array;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the TLS handshake is still in progress.
|
|
23
|
+
*/
|
|
24
|
+
is_handshaking(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Get the negotiated ALPN protocol (e.g. "h2" or "http/1.1").
|
|
27
|
+
* Returns null if no ALPN was negotiated.
|
|
28
|
+
*/
|
|
29
|
+
negotiated_alpn(): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Create a new TLS client connection.
|
|
32
|
+
* `hostname`: server hostname for SNI
|
|
33
|
+
* `alpn_protocols`: comma-separated ALPN protocol list, e.g. "h2,http/1.1"
|
|
34
|
+
*/
|
|
35
|
+
constructor(hostname: string, alpn_protocols: string);
|
|
36
|
+
/**
|
|
37
|
+
* Send a TLS close_notify alert.
|
|
38
|
+
*/
|
|
39
|
+
send_close_notify(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Take decrypted plaintext data (for the upper layer to consume).
|
|
42
|
+
*/
|
|
43
|
+
take_plaintext(): Uint8Array;
|
|
44
|
+
/**
|
|
45
|
+
* Whether rustls needs more data from the network.
|
|
46
|
+
*/
|
|
47
|
+
wants_read(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Whether rustls has data to write to the network.
|
|
50
|
+
*/
|
|
51
|
+
wants_write(): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Write plaintext data (from the upper layer) into the TLS engine for encryption.
|
|
54
|
+
* Returns true if rustls has outgoing data to send.
|
|
55
|
+
*/
|
|
56
|
+
write_plaintext(data: Uint8Array): boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the library version string (for verification).
|
|
61
|
+
*/
|
|
62
|
+
export function wasm_tls_version(): string;
|
|
63
|
+
|
|
64
|
+
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
65
|
+
|
|
66
|
+
export interface InitOutput {
|
|
67
|
+
readonly memory: WebAssembly.Memory;
|
|
68
|
+
readonly __wbg_tlsconnection_free: (a: number, b: number) => void;
|
|
69
|
+
readonly tlsconnection_feed_ciphertext: (a: number, b: number, c: number, d: number) => void;
|
|
70
|
+
readonly tlsconnection_flush_outgoing_tls: (a: number, b: number) => void;
|
|
71
|
+
readonly tlsconnection_is_handshaking: (a: number) => number;
|
|
72
|
+
readonly tlsconnection_negotiated_alpn: (a: number, b: number) => void;
|
|
73
|
+
readonly tlsconnection_new: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
74
|
+
readonly tlsconnection_send_close_notify: (a: number) => void;
|
|
75
|
+
readonly tlsconnection_take_plaintext: (a: number, b: number) => void;
|
|
76
|
+
readonly tlsconnection_wants_read: (a: number) => number;
|
|
77
|
+
readonly tlsconnection_wants_write: (a: number) => number;
|
|
78
|
+
readonly tlsconnection_write_plaintext: (a: number, b: number, c: number, d: number) => void;
|
|
79
|
+
readonly wasm_tls_version: (a: number) => void;
|
|
80
|
+
readonly __wbindgen_export: (a: number) => void;
|
|
81
|
+
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
82
|
+
readonly __wbindgen_export2: (a: number, b: number) => number;
|
|
83
|
+
readonly __wbindgen_export3: (a: number, b: number, c: number) => void;
|
|
84
|
+
readonly __wbindgen_export4: (a: number, b: number, c: number, d: number) => number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Instantiates the given `module`, which can either be bytes or
|
|
91
|
+
* a precompiled `WebAssembly.Module`.
|
|
92
|
+
*
|
|
93
|
+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
|
94
|
+
*
|
|
95
|
+
* @returns {InitOutput}
|
|
96
|
+
*/
|
|
97
|
+
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
|
101
|
+
* for everything else, calls `WebAssembly.instantiate` directly.
|
|
102
|
+
*
|
|
103
|
+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
|
104
|
+
*
|
|
105
|
+
* @returns {Promise<InitOutput>}
|
|
106
|
+
*/
|
|
107
|
+
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|