@swf2es/desktop 0.1.16 → 0.1.17
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/README.md +41 -1
- package/dist/main/air.d.ts +3 -3
- package/dist/main/air.d.ts.map +1 -1
- package/dist/main/air.js +17 -8
- package/dist/main/air.js.map +1 -1
- package/dist/main/case-paths.d.ts +12 -0
- package/dist/main/case-paths.d.ts.map +1 -0
- package/dist/main/case-paths.js +84 -0
- package/dist/main/case-paths.js.map +1 -0
- package/dist/main/connections.d.ts +20 -1
- package/dist/main/connections.d.ts.map +1 -1
- package/dist/main/connections.js +71 -8
- package/dist/main/connections.js.map +1 -1
- package/dist/main/main.js +80 -14
- package/dist/main/main.js.map +1 -1
- package/dist/main/network.d.ts +10 -0
- package/dist/main/network.d.ts.map +1 -1
- package/dist/main/network.js +111 -35
- package/dist/main/network.js.map +1 -1
- package/dist/main/proxy.d.ts +48 -0
- package/dist/main/proxy.d.ts.map +1 -0
- package/dist/main/proxy.js +402 -0
- package/dist/main/proxy.js.map +1 -0
- package/dist/main/sandbox.d.ts.map +1 -1
- package/dist/main/sandbox.js +2 -1
- package/dist/main/sandbox.js.map +1 -1
- package/dist/main/settings.d.ts +2 -0
- package/dist/main/settings.d.ts.map +1 -1
- package/dist/main/settings.js +4 -0
- package/dist/main/settings.js.map +1 -1
- package/dist/main/sockets.d.ts +4 -2
- package/dist/main/sockets.d.ts.map +1 -1
- package/dist/main/sockets.js +6 -4
- package/dist/main/sockets.js.map +1 -1
- package/dist/main/websocket-connections.d.ts +11 -0
- package/dist/main/websocket-connections.d.ts.map +1 -1
- package/dist/main/websocket-connections.js +72 -4
- package/dist/main/websocket-connections.js.map +1 -1
- package/dist/main/websockets.d.ts +3 -2
- package/dist/main/websockets.d.ts.map +1 -1
- package/dist/main/websockets.js +4 -3
- package/dist/main/websockets.js.map +1 -1
- package/package.json +7 -7
- package/src/main/air.ts +18 -8
- package/src/main/case-paths.ts +97 -0
- package/src/main/connections.ts +90 -8
- package/src/main/main.ts +105 -20
- package/src/main/network.ts +140 -37
- package/src/main/proxy.ts +497 -0
- package/src/main/sandbox.ts +2 -1
- package/src/main/settings.ts +5 -0
- package/src/main/sockets.ts +7 -4
- package/src/main/websocket-connections.ts +84 -4
- package/src/main/websockets.ts +4 -2
package/src/main/main.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// electron apps/desktop [--trace] app.xml [root-dir] [-- arguments...]
|
|
7
7
|
//
|
|
8
8
|
// --ignore-certificate-errors[=host,...] leaves HTTPS and WSS certificates unchecked.
|
|
9
|
+
// --proxy=socks5://[user:pass@]host:port (or http://...) sends the SWF's network through a proxy.
|
|
9
10
|
//
|
|
10
11
|
// The second runs an AIR application by its descriptor, as adl does.
|
|
11
12
|
import { statSync } from "node:fs";
|
|
@@ -49,14 +50,23 @@ import {
|
|
|
49
50
|
statOf,
|
|
50
51
|
writeOf,
|
|
51
52
|
} from "./air.ts";
|
|
53
|
+
import { resolveCase } from "./case-paths.ts";
|
|
52
54
|
import { bridgeFiles } from "./files.ts";
|
|
53
55
|
import { failureLine, requestLine } from "./netlog.ts";
|
|
54
56
|
import { type Movie, Network, Refused, unverifiedHosts } from "./network.ts";
|
|
55
57
|
import { APP_ORIGIN, registerScheme, serve } from "./protocol.ts";
|
|
58
|
+
import { describeProxy, proxyArgument, sameProxy } from "./proxy.ts";
|
|
56
59
|
import { Sandbox } from "./sandbox.ts";
|
|
57
60
|
import { SettingsFile } from "./settings.ts";
|
|
58
61
|
import { bridgeSockets } from "./sockets.ts";
|
|
59
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
dispatchedOpener,
|
|
64
|
+
hasWebSocket,
|
|
65
|
+
type Opener,
|
|
66
|
+
proxiedDispatcher,
|
|
67
|
+
unverifiedDispatcher,
|
|
68
|
+
webSocketOpener,
|
|
69
|
+
} from "./websocket-connections.ts";
|
|
60
70
|
import { bridgeWebSockets } from "./websockets.ts";
|
|
61
71
|
|
|
62
72
|
const preload = fileURLToPath(new URL("../preload/preload.cjs", import.meta.url));
|
|
@@ -106,16 +116,54 @@ if (unverified) {
|
|
|
106
116
|
|
|
107
117
|
const unverifiedHost = (host: string) =>
|
|
108
118
|
!!unverified && (unverified.all || unverified.hosts.includes(host.toLowerCase()));
|
|
109
|
-
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* --proxy=<url>: what the SWF's requests, sockets and WebSockets go
|
|
122
|
+
* through. For this run alone, never kept, and said aloud, without its
|
|
123
|
+
* credentials. One that does not parse stops the app rather than letting
|
|
124
|
+
* it run without.
|
|
125
|
+
*/
|
|
126
|
+
const proxied = proxyArgument(process.argv.slice(1));
|
|
127
|
+
if (proxied && "error" in proxied) {
|
|
128
|
+
process.stderr.write(`swf2es: ${proxied.error}\n`);
|
|
129
|
+
process.exit(2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const proxy = proxied?.proxy ?? null;
|
|
133
|
+
if (proxy) {
|
|
134
|
+
process.stderr.write(
|
|
135
|
+
`swf2es: --proxy: the SWF's requests, sockets and WebSockets go through ${describeProxy(proxy)}\n`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** What --log-network adds to a line of a connection that went through the proxy. */
|
|
140
|
+
const via = proxy ? " via proxy" : "";
|
|
141
|
+
const warn = (message: string) => process.stderr.write(`swf2es: ${message}\n`);
|
|
142
|
+
const network = new Network({ unverified: unverifiedHost, proxy });
|
|
110
143
|
// What opens an AIR application's WebSockets: the global's client, a wss: server's certificate
|
|
111
|
-
// unchecked where the switch names its host.
|
|
112
|
-
const insecureWebSockets = unverified ? unverifiedDispatcher() : null;
|
|
113
|
-
if (unverified && !insecureWebSockets) {
|
|
144
|
+
// unchecked where the switch names its host; or every one through the proxy.
|
|
145
|
+
const insecureWebSockets = unverified && !proxy ? unverifiedDispatcher() : null;
|
|
146
|
+
if (unverified && !proxy && !insecureWebSockets) {
|
|
114
147
|
process.stderr.write(
|
|
115
148
|
"swf2es: --ignore-certificate-errors: a WebSocket's certificate is verified all the same\n",
|
|
116
149
|
);
|
|
117
150
|
}
|
|
118
151
|
|
|
152
|
+
const proxiedWebSockets = proxy ? proxiedDispatcher(proxy, unverifiedHost, warn) : null;
|
|
153
|
+
// Never past the proxy: where none can be made, no WebSocket opens.
|
|
154
|
+
const webSocketOpen: Opener | undefined = proxy
|
|
155
|
+
? proxiedWebSockets
|
|
156
|
+
? dispatchedOpener(proxiedWebSockets)
|
|
157
|
+
: () => {
|
|
158
|
+
throw new Error("no WebSocket through the proxy");
|
|
159
|
+
}
|
|
160
|
+
: insecureWebSockets
|
|
161
|
+
? webSocketOpener(unverifiedHost, insecureWebSockets)
|
|
162
|
+
: undefined;
|
|
163
|
+
if (proxy && !proxiedWebSockets) {
|
|
164
|
+
process.stderr.write("swf2es: --proxy: this Electron's WebSocket cannot go through a proxy\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
119
167
|
/** What the page's requests under way abort with, by the preload's number. */
|
|
120
168
|
const requests = new Map<number, AbortController>();
|
|
121
169
|
let settings: SettingsFile;
|
|
@@ -584,13 +632,15 @@ async function askForSocket(name: string, swf: string, endpoint: string): Promis
|
|
|
584
632
|
* sockets do here. air.net.WebSocket is AIR's alone, so nothing else has one.
|
|
585
633
|
*/
|
|
586
634
|
function permitWebSocket(url: string): boolean {
|
|
587
|
-
if (sandbox.application && hasWebSocket()) {
|
|
635
|
+
if (sandbox.application && hasWebSocket() && (!proxy || proxiedWebSockets)) {
|
|
588
636
|
return true;
|
|
589
637
|
}
|
|
590
638
|
|
|
591
|
-
const why = sandbox.application
|
|
592
|
-
? "
|
|
593
|
-
:
|
|
639
|
+
const why = !sandbox.application
|
|
640
|
+
? "only an AIR application has one"
|
|
641
|
+
: !hasWebSocket()
|
|
642
|
+
? "this Electron has no WebSocket client"
|
|
643
|
+
: "this Electron's WebSocket cannot go through the proxy";
|
|
594
644
|
process.stderr.write(`swf2es: no WebSocket to ${url.slice(0, 2048)}: ${why}\n`);
|
|
595
645
|
return false;
|
|
596
646
|
}
|
|
@@ -643,10 +693,22 @@ async function chooseLibrary(name: LibraryName): Promise<void> {
|
|
|
643
693
|
refuse("The settings could not be saved.", `swf2es uses ${path} until it quits.`);
|
|
644
694
|
}
|
|
645
695
|
|
|
696
|
+
buildMenu();
|
|
697
|
+
|
|
646
698
|
// The page fetched the libraries once; a fresh page fetches the new one.
|
|
647
699
|
window.webContents.reload();
|
|
648
700
|
}
|
|
649
701
|
|
|
702
|
+
/** Play on the library the app ships again, in place of one the user chose. */
|
|
703
|
+
function resetLibrary(name: LibraryName): void {
|
|
704
|
+
if (!settings.clearLibrary(name)) {
|
|
705
|
+
refuse("The settings could not be saved.", `swf2es uses its own ${name} until it quits.`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
buildMenu();
|
|
709
|
+
window?.webContents.reload();
|
|
710
|
+
}
|
|
711
|
+
|
|
650
712
|
function buildMenu(): void {
|
|
651
713
|
const recent = settings.settings.recent;
|
|
652
714
|
const template: MenuItemConstructorOptions[] = [
|
|
@@ -681,6 +743,11 @@ function buildMenu(): void {
|
|
|
681
743
|
},
|
|
682
744
|
{ type: "separator" },
|
|
683
745
|
{ label: "Choose playerglobal.abc…", click: () => void chooseLibrary("playerglobal") },
|
|
746
|
+
{
|
|
747
|
+
label: "Use the App's playerglobal",
|
|
748
|
+
enabled: !!settings.settings.playerglobal,
|
|
749
|
+
click: () => resetLibrary("playerglobal"),
|
|
750
|
+
},
|
|
684
751
|
{ type: "separator" },
|
|
685
752
|
{ role: "quit" },
|
|
686
753
|
],
|
|
@@ -913,14 +980,8 @@ function listen(): void {
|
|
|
913
980
|
void chooseLibrary(name);
|
|
914
981
|
}
|
|
915
982
|
});
|
|
916
|
-
bridgeSockets(trusted, permitSocket, netLog, logSockets);
|
|
917
|
-
bridgeWebSockets(
|
|
918
|
-
trusted,
|
|
919
|
-
permitWebSocket,
|
|
920
|
-
insecureWebSockets ? webSocketOpener(unverifiedHost, insecureWebSockets) : undefined,
|
|
921
|
-
netLog,
|
|
922
|
-
logSockets,
|
|
923
|
-
);
|
|
983
|
+
bridgeSockets(trusted, permitSocket, netLog, logSockets, { proxy, warn });
|
|
984
|
+
bridgeWebSockets(trusted, permitWebSocket, webSocketOpen, netLog, logSockets, via);
|
|
924
985
|
ipcMain.handle("net:fetch", (event, id: unknown, request: unknown) => {
|
|
925
986
|
if (!trusted(event) || typeof id !== "number") {
|
|
926
987
|
throw new Error("not the app's page");
|
|
@@ -1052,7 +1113,7 @@ function listenForAir(): void {
|
|
|
1052
1113
|
}
|
|
1053
1114
|
|
|
1054
1115
|
try {
|
|
1055
|
-
const real = await realpath(path);
|
|
1116
|
+
const real = await realpath(resolveCase(path));
|
|
1056
1117
|
return sandbox.startApplication(real) ? new Uint8Array(await readFile(real)) : null;
|
|
1057
1118
|
} catch {
|
|
1058
1119
|
return null;
|
|
@@ -1155,7 +1216,7 @@ async function fetchForPage(
|
|
|
1155
1216
|
response.status,
|
|
1156
1217
|
response.bytes?.length ?? null,
|
|
1157
1218
|
performance.now() - started,
|
|
1158
|
-
),
|
|
1219
|
+
) + via,
|
|
1159
1220
|
);
|
|
1160
1221
|
return response;
|
|
1161
1222
|
} catch (error) {
|
|
@@ -1164,7 +1225,7 @@ async function fetchForPage(
|
|
|
1164
1225
|
const why = error instanceof Refused ? error.message : `failed: ${error}`;
|
|
1165
1226
|
process.stderr.write(`swf2es: not loading ${shown}: ${why}\n`);
|
|
1166
1227
|
} else {
|
|
1167
|
-
netLog?.(failureLine(method, url, "aborted", performance.now() - started));
|
|
1228
|
+
netLog?.(failureLine(method, url, "aborted", performance.now() - started) + via);
|
|
1168
1229
|
}
|
|
1169
1230
|
|
|
1170
1231
|
throw error;
|
|
@@ -1178,10 +1239,25 @@ async function fetchForPage(
|
|
|
1178
1239
|
// One app to a user data directory: a second start hands its SWF to the first and quits.
|
|
1179
1240
|
const first = app.requestSingleInstanceLock();
|
|
1180
1241
|
if (!first) {
|
|
1242
|
+
if (proxied) {
|
|
1243
|
+
process.stderr.write(
|
|
1244
|
+
"swf2es: handed to the app already open, which opens it only if started with the same --proxy\n",
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1181
1248
|
app.quit();
|
|
1182
1249
|
}
|
|
1183
1250
|
|
|
1184
1251
|
app.on("second-instance", (_event, argv, workingDirectory) => {
|
|
1252
|
+
// What it names would go through another proxy than this app's, or past it.
|
|
1253
|
+
const asked = proxyArgument(argv.slice(1));
|
|
1254
|
+
if (asked && !("proxy" in asked && sameProxy(asked.proxy, proxy))) {
|
|
1255
|
+
process.stderr.write(
|
|
1256
|
+
"swf2es: not opening what a second start named: its --proxy is not this app's\n",
|
|
1257
|
+
);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1185
1261
|
if (window) {
|
|
1186
1262
|
if (window.isMinimized()) {
|
|
1187
1263
|
window.restore();
|
|
@@ -1231,6 +1307,15 @@ void app.whenReady().then(() => {
|
|
|
1231
1307
|
}
|
|
1232
1308
|
|
|
1233
1309
|
settings = new SettingsFile(app.getPath("userData"));
|
|
1310
|
+
// A playerglobal chosen once is kept across versions of the app: said at each start, as a
|
|
1311
|
+
// library older or newer than the app's own makes SWFs fail in ways nothing else explains.
|
|
1312
|
+
const chosen = settings.libraries().playerglobal;
|
|
1313
|
+
if (chosen) {
|
|
1314
|
+
process.stderr.write(
|
|
1315
|
+
`swf2es: playing on the playerglobal chosen in File, ${chosen}, not the app's own\n`,
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1234
1319
|
serve(session.defaultSession, sandbox, () => settings.libraries());
|
|
1235
1320
|
// The page never goes to the network itself: a SWF's requests go through
|
|
1236
1321
|
// the main process (network.ts), which sends them without this session.
|
package/src/main/network.ts
CHANGED
|
@@ -21,12 +21,17 @@
|
|
|
21
21
|
// that changes its answer (rebinding) changes nothing.
|
|
22
22
|
// - Requests are bounded: methods, headers, sizes, redirects, time, and
|
|
23
23
|
// how many run at once; and each movie's are aborted as the next opens.
|
|
24
|
+
// - Through a proxy (--proxy, proxy.ts), each connection is a tunnel to
|
|
25
|
+
// the address its host was pinned to, judged as without one: the proxy
|
|
26
|
+
// is asked for that address, not the name, so a proxy's DNS that answers
|
|
27
|
+
// otherwise than this machine's reaches nothing the judgement did not.
|
|
24
28
|
//
|
|
25
29
|
// Without Electron, so that node tests it (tests/desktop/network.test.ts).
|
|
26
30
|
import { lookup } from "node:dns/promises";
|
|
27
31
|
import { type ClientRequest, request as httpRequest, type IncomingMessage } from "node:http";
|
|
28
32
|
import { request as httpsRequest } from "node:https";
|
|
29
|
-
import { connect, isIP, isIPv4 } from "node:net";
|
|
33
|
+
import { connect, isIP, isIPv4, type Socket } from "node:net";
|
|
34
|
+
import type { Duplex } from "node:stream";
|
|
30
35
|
import * as tls from "node:tls";
|
|
31
36
|
import { type AddressClass, addressClass, badPort, RANK } from "./addresses.ts";
|
|
32
37
|
import { DECODED, Decompressor } from "./decompress.ts";
|
|
@@ -44,6 +49,7 @@ import {
|
|
|
44
49
|
urlMetaPolicy,
|
|
45
50
|
usablePolicies,
|
|
46
51
|
} from "./policy.ts";
|
|
52
|
+
import { connectThrough, type ProxyConfig, ProxyError } from "./proxy.ts";
|
|
47
53
|
|
|
48
54
|
export type Purpose = "movie" | "content" | "data" | "send";
|
|
49
55
|
|
|
@@ -145,6 +151,8 @@ export interface NetworkOptions {
|
|
|
145
151
|
limits?: Partial<Limits>;
|
|
146
152
|
/** Where a socket server keeps its master policy: 843. */
|
|
147
153
|
socketPolicyPort?: number;
|
|
154
|
+
/** What every connection goes through (--proxy); none by default. */
|
|
155
|
+
proxy?: ProxyConfig | null;
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
/** What Flash Player 32 reported, sent as it sent it. */
|
|
@@ -239,6 +247,7 @@ export class Network {
|
|
|
239
247
|
private readonly ca: string | string[] | undefined;
|
|
240
248
|
private readonly unverified: (host: string) => boolean;
|
|
241
249
|
private readonly socketPolicyPort: number;
|
|
250
|
+
private readonly proxy: ProxyConfig | null;
|
|
242
251
|
private session: Session | null = null;
|
|
243
252
|
private running = 0;
|
|
244
253
|
private readonly waiting: (() => void)[] = [];
|
|
@@ -253,6 +262,7 @@ export class Network {
|
|
|
253
262
|
this.ca = options.ca ?? defaultCa();
|
|
254
263
|
this.unverified = options.unverified ?? (() => false);
|
|
255
264
|
this.socketPolicyPort = options.socketPolicyPort ?? 843;
|
|
265
|
+
this.proxy = options.proxy ?? null;
|
|
256
266
|
}
|
|
257
267
|
|
|
258
268
|
/**
|
|
@@ -753,7 +763,13 @@ export class Network {
|
|
|
753
763
|
timeout: this.limits.policyTimeout,
|
|
754
764
|
deadline: this.limits.policyTimeout,
|
|
755
765
|
});
|
|
756
|
-
} catch {
|
|
766
|
+
} catch (error) {
|
|
767
|
+
// A proxy that failed is said, as the request that needed the policy fails;
|
|
768
|
+
// a server the proxy did not reach has no policy, as one that is not there.
|
|
769
|
+
if (proxyFault(error)) {
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
|
|
757
773
|
return null;
|
|
758
774
|
}
|
|
759
775
|
|
|
@@ -790,15 +806,27 @@ export class Network {
|
|
|
790
806
|
timeout: number,
|
|
791
807
|
signal: AbortSignal,
|
|
792
808
|
): Promise<SocketPolicy | null> {
|
|
793
|
-
return new Promise((answer) => {
|
|
809
|
+
return new Promise((answer, fail) => {
|
|
794
810
|
const chunks: Buffer[] = [];
|
|
795
811
|
let length = 0;
|
|
796
|
-
|
|
797
|
-
|
|
812
|
+
let socket: Socket | null = null;
|
|
813
|
+
let finished = false;
|
|
814
|
+
const opening = new AbortController();
|
|
815
|
+
const finish = (policy: SocketPolicy | null, error?: Error) => {
|
|
816
|
+
if (finished) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
finished = true;
|
|
798
821
|
clearTimeout(timer);
|
|
799
822
|
signal.removeEventListener("abort", abort);
|
|
800
|
-
|
|
801
|
-
|
|
823
|
+
opening.abort();
|
|
824
|
+
socket?.destroy();
|
|
825
|
+
if (error) {
|
|
826
|
+
fail(error);
|
|
827
|
+
} else {
|
|
828
|
+
answer(policy);
|
|
829
|
+
}
|
|
802
830
|
};
|
|
803
831
|
const read = () => {
|
|
804
832
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
@@ -808,24 +836,70 @@ export class Network {
|
|
|
808
836
|
const abort = () => finish(null);
|
|
809
837
|
const timer = setTimeout(abort, timeout);
|
|
810
838
|
signal.addEventListener("abort", abort, { once: true });
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
839
|
+
const talk = (connected: Socket) => {
|
|
840
|
+
socket = connected;
|
|
841
|
+
connected.on("data", (chunk: Buffer) => {
|
|
842
|
+
length += chunk.length;
|
|
843
|
+
if (length > this.limits.policy) {
|
|
844
|
+
finish(null);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
818
847
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
848
|
+
chunks.push(chunk);
|
|
849
|
+
if (chunk.includes(0)) {
|
|
850
|
+
read();
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
connected.on("end", read);
|
|
854
|
+
connected.on("error", () => finish(null));
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
if (!this.proxy) {
|
|
858
|
+
const direct = connect({ host: address, port });
|
|
859
|
+
talk(direct);
|
|
860
|
+
direct.on("connect", () => direct.write("<policy-file-request/>\0"));
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
connectThrough(this.proxy, address, port, { signal: opening.signal }).then(
|
|
865
|
+
(tunnel) => {
|
|
866
|
+
if (finished) {
|
|
867
|
+
tunnel.destroy();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
talk(tunnel);
|
|
872
|
+
tunnel.write("<policy-file-request/>\0");
|
|
873
|
+
},
|
|
874
|
+
(error: ProxyError) =>
|
|
875
|
+
// A server the proxy did not reach has no policy; a proxy that failed is said.
|
|
876
|
+
finish(null, error.fault ? new Refused(error.message, { cause: error }) : undefined),
|
|
877
|
+
);
|
|
826
878
|
});
|
|
827
879
|
}
|
|
828
880
|
|
|
881
|
+
/**
|
|
882
|
+
* A request's createConnection: a tunnel through `proxy` to `address`,
|
|
883
|
+
* with TLS over it where `secure` says how, as Node's callback form takes
|
|
884
|
+
* it. A proxy's failure is a refusal saying why, the ProxyError its cause.
|
|
885
|
+
*/
|
|
886
|
+
private tunnel(
|
|
887
|
+
proxy: ProxyConfig,
|
|
888
|
+
address: string,
|
|
889
|
+
port: number,
|
|
890
|
+
secure: tls.ConnectionOptions | false,
|
|
891
|
+
signal: AbortSignal,
|
|
892
|
+
) {
|
|
893
|
+
return (_options: unknown, done: (error: Error | null, socket: Duplex) => void) => {
|
|
894
|
+
connectThrough(proxy, address, port, { signal }).then(
|
|
895
|
+
(tunnel) => done(null, secure ? tls.connect({ ...secure, socket: tunnel }) : tunnel),
|
|
896
|
+
(error: ProxyError) =>
|
|
897
|
+
done(new Refused(error.message, { cause: error }), undefined as unknown as Duplex),
|
|
898
|
+
);
|
|
899
|
+
return undefined;
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
829
903
|
/**
|
|
830
904
|
* One HTTP exchange with `address`, the URL's host pinned to it: its
|
|
831
905
|
* status and headers, and its body if `read` and the status is a
|
|
@@ -877,28 +951,41 @@ export class Network {
|
|
|
877
951
|
|
|
878
952
|
const https = url.protocol === "https:";
|
|
879
953
|
const family = isIP(address);
|
|
954
|
+
const port = Number(url.port || (https ? 443 : 80));
|
|
955
|
+
const secure: tls.ConnectionOptions = {
|
|
956
|
+
servername: isIP(host) ? undefined : host,
|
|
957
|
+
ca: this.ca,
|
|
958
|
+
// Only where the user asked for this host: its certificate goes unchecked.
|
|
959
|
+
...(this.unverified(host) ? { rejectUnauthorized: false } : {}),
|
|
960
|
+
};
|
|
880
961
|
let request: ClientRequest;
|
|
881
962
|
try {
|
|
882
963
|
request = (https ? httpsRequest : httpRequest)({
|
|
883
964
|
host,
|
|
884
|
-
port
|
|
965
|
+
port,
|
|
885
966
|
path: `${url.pathname}${url.search}`,
|
|
886
967
|
method,
|
|
887
968
|
headers: outgoing,
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
969
|
+
...(this.proxy
|
|
970
|
+
? // Through the proxy to the pinned address, TLS over the tunnel for https; the
|
|
971
|
+
// request's own timers and signal bound the proxy's part too. No agent: the
|
|
972
|
+
// connection is this one's.
|
|
973
|
+
{ createConnection: this.tunnel(this.proxy, address, port, https && secure, signal) }
|
|
974
|
+
: {
|
|
975
|
+
agent: false,
|
|
976
|
+
// The address pinned for this request, whatever the resolver would say now.
|
|
977
|
+
lookup: (_name, opts, callback) => {
|
|
978
|
+
if ((opts as { all?: boolean }).all) {
|
|
979
|
+
(callback as (e: null, a: { address: string; family: number }[]) => void)(
|
|
980
|
+
null,
|
|
981
|
+
[{ address, family }],
|
|
982
|
+
);
|
|
983
|
+
} else {
|
|
984
|
+
(callback as (e: null, a: string, f: number) => void)(null, address, family);
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
...(https ? secure : {}),
|
|
988
|
+
}),
|
|
902
989
|
});
|
|
903
990
|
} catch (error) {
|
|
904
991
|
// What check() let through and Node still refuses: a refusal like any other.
|
|
@@ -931,7 +1018,10 @@ export class Network {
|
|
|
931
1018
|
const abort = () => stop(new Refused("aborted"));
|
|
932
1019
|
signal.addEventListener("abort", abort, { once: true });
|
|
933
1020
|
arm();
|
|
934
|
-
|
|
1021
|
+
// A proxy's refusal comes as it was made, with its cause.
|
|
1022
|
+
request.on("error", (error) =>
|
|
1023
|
+
stop(error instanceof Refused ? error : new Refused(error.message)),
|
|
1024
|
+
);
|
|
935
1025
|
request.on("response", (response: IncomingMessage) => {
|
|
936
1026
|
const status = response.statusCode ?? 0;
|
|
937
1027
|
const received: [string, string][] = [];
|
|
@@ -1195,11 +1285,24 @@ function cached<T>(
|
|
|
1195
1285
|
|
|
1196
1286
|
value = make();
|
|
1197
1287
|
cache.set(key, value);
|
|
1288
|
+
// A failure, a proxy's, is not kept: the next request asks again.
|
|
1289
|
+
const made = value;
|
|
1290
|
+
made.catch(() => {
|
|
1291
|
+
if (cache.get(key) === made) {
|
|
1292
|
+
cache.delete(key);
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1198
1295
|
}
|
|
1199
1296
|
|
|
1200
1297
|
return value;
|
|
1201
1298
|
}
|
|
1202
1299
|
|
|
1300
|
+
/** Whether `error` is a refusal for a proxy that failed, not a server it did not reach. */
|
|
1301
|
+
function proxyFault(error: unknown): boolean {
|
|
1302
|
+
const cause = (error as Error | null)?.cause;
|
|
1303
|
+
return cause instanceof ProxyError && cause.fault;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1203
1306
|
function describe(requester: Requester): string {
|
|
1204
1307
|
return requester.host === null ? 'a local SWF (domain="*")' : requester.host;
|
|
1205
1308
|
}
|