@swf2es/desktop 0.0.0-stage → 0.1.15

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.
Files changed (132) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +135 -2
  3. package/dist/launch/electron.d.ts +14 -0
  4. package/dist/launch/electron.d.ts.map +1 -0
  5. package/dist/launch/electron.js +40 -0
  6. package/dist/launch/electron.js.map +1 -0
  7. package/dist/launch/launch.d.ts +3 -0
  8. package/dist/launch/launch.d.ts.map +1 -0
  9. package/dist/launch/launch.js +69 -0
  10. package/dist/launch/launch.js.map +1 -0
  11. package/dist/main/addresses.d.ts +7 -0
  12. package/dist/main/addresses.d.ts.map +1 -0
  13. package/dist/main/addresses.js +155 -0
  14. package/dist/main/addresses.js.map +1 -0
  15. package/dist/main/air.d.ts +71 -0
  16. package/dist/main/air.d.ts.map +1 -0
  17. package/dist/main/air.js +216 -0
  18. package/dist/main/air.js.map +1 -0
  19. package/dist/main/connections.d.ts +38 -0
  20. package/dist/main/connections.d.ts.map +1 -0
  21. package/dist/main/connections.js +103 -0
  22. package/dist/main/connections.js.map +1 -0
  23. package/dist/main/decompress.d.ts +51 -0
  24. package/dist/main/decompress.d.ts.map +1 -0
  25. package/dist/main/decompress.js +184 -0
  26. package/dist/main/decompress.js.map +1 -0
  27. package/dist/main/files.d.ts +9 -0
  28. package/dist/main/files.d.ts.map +1 -0
  29. package/dist/main/files.js +120 -0
  30. package/dist/main/files.js.map +1 -0
  31. package/dist/main/main.d.ts +2 -0
  32. package/dist/main/main.d.ts.map +1 -0
  33. package/dist/main/main.js +1009 -0
  34. package/dist/main/main.js.map +1 -0
  35. package/dist/main/network.d.ts +182 -0
  36. package/dist/main/network.d.ts.map +1 -0
  37. package/dist/main/network.js +909 -0
  38. package/dist/main/network.js.map +1 -0
  39. package/dist/main/policy.d.ts +103 -0
  40. package/dist/main/policy.d.ts.map +1 -0
  41. package/dist/main/policy.js +481 -0
  42. package/dist/main/policy.js.map +1 -0
  43. package/dist/main/protocol.d.ts +14 -0
  44. package/dist/main/protocol.d.ts.map +1 -0
  45. package/dist/main/protocol.js +216 -0
  46. package/dist/main/protocol.js.map +1 -0
  47. package/dist/main/sandbox.d.ts +67 -0
  48. package/dist/main/sandbox.d.ts.map +1 -0
  49. package/dist/main/sandbox.js +182 -0
  50. package/dist/main/sandbox.js.map +1 -0
  51. package/dist/main/settings.d.ts +37 -0
  52. package/dist/main/settings.d.ts.map +1 -0
  53. package/dist/main/settings.js +88 -0
  54. package/dist/main/settings.js.map +1 -0
  55. package/dist/main/sockets.d.ts +10 -0
  56. package/dist/main/sockets.d.ts.map +1 -0
  57. package/dist/main/sockets.js +71 -0
  58. package/dist/main/sockets.js.map +1 -0
  59. package/dist/preload/preload.cjs +91 -0
  60. package/dist/preload/preload.cjs.map +1 -0
  61. package/dist/preload/preload.d.cts +2 -0
  62. package/dist/preload/preload.d.cts.map +1 -0
  63. package/dist/preload/prompt.cjs +12 -0
  64. package/dist/preload/prompt.cjs.map +1 -0
  65. package/dist/preload/prompt.d.cts +2 -0
  66. package/dist/preload/prompt.d.cts.map +1 -0
  67. package/dist/renderer/air.d.ts +14 -0
  68. package/dist/renderer/air.d.ts.map +1 -0
  69. package/dist/renderer/air.js +83 -0
  70. package/dist/renderer/air.js.map +1 -0
  71. package/dist/renderer/files.d.ts +4 -0
  72. package/dist/renderer/files.d.ts.map +1 -0
  73. package/dist/renderer/files.js +58 -0
  74. package/dist/renderer/files.js.map +1 -0
  75. package/dist/renderer/index.d.ts +7 -0
  76. package/dist/renderer/index.d.ts.map +1 -0
  77. package/dist/renderer/index.js +126 -0
  78. package/dist/renderer/index.js.map +1 -0
  79. package/dist/renderer/network.d.ts +6 -0
  80. package/dist/renderer/network.d.ts.map +1 -0
  81. package/dist/renderer/network.js +39 -0
  82. package/dist/renderer/network.js.map +1 -0
  83. package/dist/renderer/open-url.d.ts +10 -0
  84. package/dist/renderer/open-url.d.ts.map +1 -0
  85. package/dist/renderer/open-url.js +38 -0
  86. package/dist/renderer/open-url.js.map +1 -0
  87. package/dist/renderer/sockets.d.ts +4 -0
  88. package/dist/renderer/sockets.d.ts.map +1 -0
  89. package/dist/renderer/sockets.js +36 -0
  90. package/dist/renderer/sockets.js.map +1 -0
  91. package/dist/renderer/stage3d.d.ts +3 -0
  92. package/dist/renderer/stage3d.d.ts.map +1 -0
  93. package/dist/renderer/stage3d.js +26 -0
  94. package/dist/renderer/stage3d.js.map +1 -0
  95. package/dist/shared/api.d.ts +205 -0
  96. package/dist/shared/api.d.ts.map +1 -0
  97. package/dist/shared/api.js +5 -0
  98. package/dist/shared/api.js.map +1 -0
  99. package/package.json +46 -3
  100. package/src/launch/electron.ts +44 -0
  101. package/src/launch/launch.ts +72 -0
  102. package/src/launch/tsconfig.json +14 -0
  103. package/src/main/addresses.ts +184 -0
  104. package/src/main/air.ts +275 -0
  105. package/src/main/connections.ts +125 -0
  106. package/src/main/decompress.ts +219 -0
  107. package/src/main/files.ts +147 -0
  108. package/src/main/main.ts +1180 -0
  109. package/src/main/network.ts +1205 -0
  110. package/src/main/policy.ts +630 -0
  111. package/src/main/protocol.ts +237 -0
  112. package/src/main/sandbox.ts +213 -0
  113. package/src/main/settings.ts +117 -0
  114. package/src/main/sockets.ts +81 -0
  115. package/src/main/tsconfig.json +15 -0
  116. package/src/preload/preload.cts +110 -0
  117. package/src/preload/prompt.cts +11 -0
  118. package/src/preload/tsconfig.json +12 -0
  119. package/src/renderer/air.ts +100 -0
  120. package/src/renderer/files.ts +65 -0
  121. package/src/renderer/index.ts +145 -0
  122. package/src/renderer/network.ts +56 -0
  123. package/src/renderer/open-url.ts +50 -0
  124. package/src/renderer/sockets.ts +42 -0
  125. package/src/renderer/stage3d.ts +27 -0
  126. package/src/renderer/tsconfig.json +17 -0
  127. package/src/shared/api.ts +204 -0
  128. package/src/shared/tsconfig.json +11 -0
  129. package/static/icon.png +0 -0
  130. package/static/index.html +22 -0
  131. package/static/open-url.html +21 -0
  132. package/static/style.css +96 -0
@@ -0,0 +1,909 @@
1
+ // The network a SWF reaches through the desktop app: every http and https
2
+ // request it makes, sent by the main process rather than the page, and
3
+ // judged as Flash Player judged it, by its sandbox and the policy files
4
+ // of the sites it reaches, not by CORS; and the socket policies a remote
5
+ // SWF's sockets need. So a SWF reaches http: and servers that never heard
6
+ // of CORS, as it did in Flash, and nothing it should not.
7
+ //
8
+ // - A remote SWF's own origin is open to it. Another origin's data it
9
+ // reads as that origin's policy files grant its domain; content (a
10
+ // Loader's, a Sound's) and what sendToURL sends go anywhere. A
11
+ // local-with-networking SWF has no domain: only a policy granting "*"
12
+ // lets it read data.
13
+ // - Nothing is sent with the user's credentials: no cookies, no
14
+ // authentication, no client certificate. So all a SWF gains by asking
15
+ // from here rather than its own server is this machine's place on the
16
+ // network, and that is the one thing guarded beyond Flash's rules:
17
+ // a request to an address more private than the SWF's own (addresses.ts)
18
+ // needs a policy from that address granting it, whatever its purpose,
19
+ // same origin or not. Each request resolves its host once and connects
20
+ // to that address, its policy file fetched from the same one, so DNS
21
+ // that changes its answer (rebinding) changes nothing.
22
+ // - Requests are bounded: methods, headers, sizes, redirects, time, and
23
+ // how many run at once; and each movie's are aborted as the next opens.
24
+ //
25
+ // Without Electron, so that node tests it (tests/desktop/network.test.ts).
26
+ import { lookup } from "node:dns/promises";
27
+ import { request as httpRequest } from "node:http";
28
+ import { request as httpsRequest } from "node:https";
29
+ import { connect, isIP, isIPv4 } from "node:net";
30
+ import * as tls from "node:tls";
31
+ import { addressClass, badPort, RANK } from "./addresses.js";
32
+ import { DECODED, Decompressor } from "./decompress.js";
33
+ import { allowsAccess, allowsHeaders, allowsSocket, forbiddenHeader, parsePolicy, policyContentType, socketMetaPolicy, urlMetaPolicy, usablePolicies, } from "./policy.js";
34
+ /** A request refused, with why, for the terminal; the SWF sees a failed load. */
35
+ export class Refused extends Error {
36
+ }
37
+ const LIMITS = {
38
+ url: 64 * 1024,
39
+ headers: 32,
40
+ headerBytes: 8 * 1024,
41
+ body: 32 * 1024 * 1024,
42
+ response: 128 * 1024 * 1024,
43
+ policy: 1024 * 1024,
44
+ redirects: 10,
45
+ timeout: 30_000,
46
+ deadline: 300_000,
47
+ policyTimeout: 10_000,
48
+ masterSocketTimeout: 3_000,
49
+ buffered: 256 * 1024 * 1024,
50
+ concurrent: 16,
51
+ queue: 1024,
52
+ policyFiles: 64,
53
+ };
54
+ /** What Flash Player 32 reported, sent as it sent it. */
55
+ const FLASH_VERSION = "32,0,0,465";
56
+ const USER_AGENT = "Mozilla/5.0 (compatible; swf2es) Shockwave Flash";
57
+ const REDIRECTS = new Set([301, 302, 303, 307, 308]);
58
+ const TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
59
+ /** The system's trusted roots beside Node's, where this Node can tell them. */
60
+ function defaultCa() {
61
+ const get = tls.getCACertificates;
62
+ if (typeof get !== "function") {
63
+ return undefined;
64
+ }
65
+ try {
66
+ return [...new Set([...get("default"), ...get("system")])];
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ /**
73
+ * The hosts --ignore-certificate-errors names, of the app's arguments up to
74
+ * an AIR application's own after "--": null without the switch; with it
75
+ * alone, every host; with `=a.test,b.test`, those, whatever their case.
76
+ * A host is a name or an address, with no port: every port of it.
77
+ */
78
+ export function unverifiedHosts(argv) {
79
+ const end = argv.indexOf("--");
80
+ const own = end < 0 ? argv : argv.slice(0, end);
81
+ let named = false;
82
+ let all = false;
83
+ const hosts = [];
84
+ for (const arg of own) {
85
+ if (arg === "--ignore-certificate-errors") {
86
+ named = true;
87
+ all = true;
88
+ }
89
+ else if (arg.startsWith("--ignore-certificate-errors=")) {
90
+ // The switch with nothing named ignores nothing, rather than everything by a slip.
91
+ named = true;
92
+ for (const host of arg.slice(arg.indexOf("=") + 1).split(",")) {
93
+ if (host.trim() !== "") {
94
+ hosts.push(host.trim().toLowerCase());
95
+ }
96
+ }
97
+ }
98
+ }
99
+ return named ? { all, hosts } : null;
100
+ }
101
+ export class Network {
102
+ limits;
103
+ resolveHost;
104
+ classify;
105
+ ca;
106
+ unverified;
107
+ socketPolicyPort;
108
+ session = null;
109
+ running = 0;
110
+ waiting = [];
111
+ constructor(options = {}) {
112
+ this.limits = { ...LIMITS, ...options.limits };
113
+ this.resolveHost =
114
+ options.resolve ??
115
+ (async (host) => (await lookup(host, { all: true, verbatim: true })).map(({ address }) => address));
116
+ this.classify = options.classify ?? addressClass;
117
+ this.ca = options.ca ?? defaultCa();
118
+ this.unverified = options.unverified ?? (() => false);
119
+ this.socketPolicyPort = options.socketPolicyPort ?? 843;
120
+ }
121
+ /**
122
+ * Judge requests for `movie` from now on, or for none: what the last
123
+ * movie had under way is aborted, and its policy files forgotten.
124
+ */
125
+ play(movie) {
126
+ this.session?.abort.abort();
127
+ this.session = movie
128
+ ? {
129
+ movie,
130
+ origin: null,
131
+ abort: new AbortController(),
132
+ policies: new Map(),
133
+ socketPolicies: new Map(),
134
+ urlPolicyFiles: [],
135
+ socketPolicyFiles: [],
136
+ buffered: 0,
137
+ }
138
+ : null;
139
+ }
140
+ /** The movie's URL after its redirects, once it has come; null before, and for a local movie. */
141
+ get origin() {
142
+ return this.session?.origin?.url.href ?? null;
143
+ }
144
+ /**
145
+ * A policy file the movie named with Security.loadPolicyFile: an http(s)
146
+ * URL, or "xmlsocket://host:port" for a socket policy. It is fetched
147
+ * only when a request needs it.
148
+ */
149
+ addPolicyFile(text) {
150
+ const session = this.session;
151
+ if (!session ||
152
+ typeof text !== "string" ||
153
+ text.length > this.limits.url ||
154
+ session.urlPolicyFiles.length + session.socketPolicyFiles.length >= this.limits.policyFiles) {
155
+ return;
156
+ }
157
+ let url;
158
+ try {
159
+ url = new URL(text);
160
+ }
161
+ catch {
162
+ return;
163
+ }
164
+ if (url.protocol === "xmlsocket:") {
165
+ const port = Number(url.port);
166
+ if (url.hostname && Number.isInteger(port) && port > 0) {
167
+ // Lower case by hand: the URL standard keeps an unknown scheme's host as written.
168
+ session.socketPolicyFiles.push({ host: bare(url.hostname).toLowerCase(), port });
169
+ }
170
+ }
171
+ else if ((url.protocol === "http:" || url.protocol === "https:") &&
172
+ !url.username &&
173
+ !url.password &&
174
+ !session.urlPolicyFiles.some((u) => u.href === url.href)) {
175
+ url.hash = "";
176
+ session.urlPolicyFiles.push(url);
177
+ }
178
+ }
179
+ /**
180
+ * Send what the page asks, `request` as it came over IPC, checked here,
181
+ * if the movie may; `signal` aborts it. Refused, it throws a Refused
182
+ * saying why. `deliver` is handed the response while its bytes still
183
+ * count against the movie's budget, for a caller that copies them away
184
+ * (over IPC) before they are given back.
185
+ */
186
+ async fetch(request, signal, deliver) {
187
+ const session = this.session;
188
+ if (!session) {
189
+ throw new Refused("no SWF plays");
190
+ }
191
+ const checked = this.check(request);
192
+ const aborted = AbortSignal.any([signal, session.abort.signal]);
193
+ await this.turn(aborted);
194
+ // All told, from its turn: its redirects and the policy files it waits on included.
195
+ const deadline = AbortSignal.timeout(this.limits.deadline);
196
+ const bounded = AbortSignal.any([aborted, deadline]);
197
+ const budget = { session, taken: 0 };
198
+ try {
199
+ const response = checked.purpose === "movie"
200
+ ? await this.fetchMovie(session, checked, bounded, budget)
201
+ : await this.fetchFor(session, checked, bounded, budget);
202
+ deliver?.(response);
203
+ return response;
204
+ }
205
+ catch (error) {
206
+ throw deadline.aborted && !aborted.aborted ? new Refused("it took too long") : error;
207
+ }
208
+ finally {
209
+ session.buffered -= budget.taken;
210
+ this.done();
211
+ }
212
+ }
213
+ /**
214
+ * The address the movie's socket to `host`:`port` connects to, once its
215
+ * socket policy allows it, as Flash Player 9.0.124 and later asked of
216
+ * every socket a remote SWF opened, its own server's included: the
217
+ * master policy on port 843, then, as its meta-policy allows, those
218
+ * Security.loadPolicyFile named and the one on the port itself. Each
219
+ * fetched from the address the socket then connects to.
220
+ */
221
+ async socketAddress(host, port) {
222
+ const session = this.session;
223
+ const origin = session?.origin;
224
+ if (!session || !origin) {
225
+ throw new Refused("the SWF has not loaded");
226
+ }
227
+ // All told, its DNS and the policies of every port it tries included,
228
+ // each of which it stops waiting on when time is up.
229
+ const deadline = AbortSignal.timeout(this.limits.deadline);
230
+ try {
231
+ return await this.socketPolicyAddress(session, origin, host, port, deadline);
232
+ }
233
+ catch (error) {
234
+ throw deadline.aborted ? new Refused("it took too long") : error;
235
+ }
236
+ }
237
+ async socketPolicyAddress(session, origin, host, port, deadline) {
238
+ const target = await this.pin(host, deadline);
239
+ const policy = (at, timeout) => abortable(cached(session.socketPolicies, `${target.address}|${at}`, () => this.socketPolicy(target.address, at, timeout, session.abort.signal)), deadline);
240
+ const master = await policy(this.socketPolicyPort, this.limits.masterSocketTimeout);
241
+ if (allowsSocket(master, [], origin.requester, port)) {
242
+ return target.address;
243
+ }
244
+ if (socketMetaPolicy(master) === "all") {
245
+ const ports = [
246
+ ...session.socketPolicyFiles
247
+ .filter((f) => f.host === bare(host).toLowerCase())
248
+ .map((f) => f.port),
249
+ port,
250
+ ];
251
+ for (const at of new Set(ports)) {
252
+ if (at === this.socketPolicyPort) {
253
+ continue;
254
+ }
255
+ const other = await policy(at, this.limits.policyTimeout);
256
+ if (other && allowsSocket(master, [other], origin.requester, port)) {
257
+ return target.address;
258
+ }
259
+ }
260
+ }
261
+ throw new Refused(`no socket policy on ${host} grants ${describe(origin.requester)}`);
262
+ }
263
+ /** `request` checked and made plain: only what Flash Player let a SWF send. */
264
+ check(request) {
265
+ const r = request;
266
+ if (typeof r !== "object" || r === null || typeof r.url !== "string") {
267
+ throw new Refused("not a request");
268
+ }
269
+ if (r.url.length > this.limits.url) {
270
+ throw new Refused("its URL is too long");
271
+ }
272
+ let target;
273
+ try {
274
+ target = new URL(r.url);
275
+ }
276
+ catch {
277
+ throw new Refused("not a URL");
278
+ }
279
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
280
+ throw new Refused(`no ${target.protocol} URL is fetched for a SWF`);
281
+ }
282
+ if (target.username || target.password) {
283
+ throw new Refused("a URL with credentials");
284
+ }
285
+ target.hash = "";
286
+ const purpose = r.purpose;
287
+ if (purpose !== "movie" && purpose !== "content" && purpose !== "data" && purpose !== "send") {
288
+ throw new Refused("no purpose");
289
+ }
290
+ // Flash Player outside AIR sent GET and POST only.
291
+ const method = typeof r.method === "string" ? r.method.toUpperCase() : "";
292
+ if (method !== "GET" && method !== "POST") {
293
+ throw new Refused(`no ${String(r.method)} request is sent for a SWF`);
294
+ }
295
+ const body = method === "POST" && r.body instanceof Uint8Array ? r.body : null;
296
+ if (body && body.length > this.limits.body) {
297
+ throw new Refused("its body is too large");
298
+ }
299
+ const headers = [];
300
+ let bytes = 0;
301
+ if (!Array.isArray(r.headers) || r.headers.length > this.limits.headers) {
302
+ throw new Refused("too many headers");
303
+ }
304
+ for (const header of r.headers) {
305
+ if (!Array.isArray(header) || header.length !== 2) {
306
+ throw new Refused("not a header");
307
+ }
308
+ const [name, value] = header;
309
+ if (typeof name !== "string" || typeof value !== "string" || !TOKEN.test(name)) {
310
+ throw new Refused("not a header");
311
+ }
312
+ // No control characters, line breaks among them, which would start a header of
313
+ // their own, and nothing HTTP's Latin-1 does not hold: what Node would throw for.
314
+ if (/[^\t\x20-\x7e\x80-\xff]/.test(value)) {
315
+ throw new Refused(`the header ${name} has a character no header may hold`);
316
+ }
317
+ if (forbiddenHeader(name)) {
318
+ throw new Refused(`a SWF may not send the header ${name}`);
319
+ }
320
+ bytes += name.length + value.length;
321
+ headers.push([name, value]);
322
+ }
323
+ if (bytes > this.limits.headerBytes) {
324
+ throw new Refused("its headers are too long");
325
+ }
326
+ return { url: target.href, target, method, headers, body, purpose };
327
+ }
328
+ /** Wait for a turn among the requests under way: at most `concurrent` at once. */
329
+ async turn(signal) {
330
+ if (this.running < this.limits.concurrent) {
331
+ this.running++;
332
+ return;
333
+ }
334
+ if (this.waiting.length >= this.limits.queue) {
335
+ throw new Refused("too many requests at once");
336
+ }
337
+ await new Promise((go, fail) => {
338
+ const start = () => {
339
+ signal.removeEventListener("abort", stop);
340
+ go();
341
+ };
342
+ const stop = () => {
343
+ const at = this.waiting.indexOf(start);
344
+ if (at >= 0) {
345
+ this.waiting.splice(at, 1);
346
+ }
347
+ fail(new Refused("aborted"));
348
+ };
349
+ if (signal.aborted) {
350
+ stop();
351
+ return;
352
+ }
353
+ signal.addEventListener("abort", stop, { once: true });
354
+ this.waiting.push(start);
355
+ });
356
+ this.running++;
357
+ }
358
+ done() {
359
+ this.running--;
360
+ this.waiting.shift()?.();
361
+ }
362
+ /** `host`'s address, its resolver's first, and how private; refused where none is reached. */
363
+ async pin(host, signal) {
364
+ const name = bare(host);
365
+ let address;
366
+ if (isIP(name)) {
367
+ address = name;
368
+ }
369
+ else {
370
+ let addresses;
371
+ try {
372
+ addresses = await (signal
373
+ ? abortable(this.resolveHost(name), signal)
374
+ : this.resolveHost(name));
375
+ }
376
+ catch {
377
+ throw new Refused(signal?.aborted ? "aborted" : `${name} has no address`);
378
+ }
379
+ if (addresses.length === 0) {
380
+ throw new Refused(`${name} has no address`);
381
+ }
382
+ address = addresses[0];
383
+ }
384
+ const place = this.classify(address);
385
+ if (place === null) {
386
+ throw new Refused(`${name} is at ${address}, which no request reaches`);
387
+ }
388
+ return { address, rank: RANK[place] };
389
+ }
390
+ /** The movie's SWF, from wherever the user asked; no redirect takes it somewhere more private. */
391
+ async fetchMovie(session, request, signal, budget) {
392
+ const movie = session.movie;
393
+ if (movie.kind !== "remote" || request.url !== new URL(movie.url).href) {
394
+ throw new Refused("not the SWF that was opened");
395
+ }
396
+ let url = request.target;
397
+ let first = null;
398
+ for (let hop = 0; hop <= this.limits.redirects; hop++) {
399
+ const target = await this.pin(url.hostname, signal);
400
+ checkPort(url);
401
+ first ??= target.rank;
402
+ if (target.rank > first) {
403
+ throw new Refused(`a redirect to ${url.href}, somewhere more private`);
404
+ }
405
+ const response = await this.send(url, target.address, "GET", [], null, signal, {
406
+ read: true,
407
+ max: this.limits.response,
408
+ referer: null,
409
+ budget,
410
+ });
411
+ const next = redirectOf(url, response);
412
+ if (next) {
413
+ url = next;
414
+ continue;
415
+ }
416
+ if (response.bytes) {
417
+ session.origin = {
418
+ url,
419
+ requester: { host: bare(url.hostname).toLowerCase(), scheme: url.protocol.slice(0, -1) },
420
+ rank: target.rank,
421
+ };
422
+ }
423
+ return { ...response, ...(url.href === request.url ? {} : { url: url.href }) };
424
+ }
425
+ throw new Refused("too many redirects");
426
+ }
427
+ /** A request of the SWF's, each hop of its redirects judged as if asked for itself. */
428
+ async fetchFor(session, request, signal, budget) {
429
+ let requester;
430
+ let rank;
431
+ let origin;
432
+ const application = session.movie.kind === "application";
433
+ if (application) {
434
+ requester = { host: null, scheme: null };
435
+ // Nothing is more private than an application: it reaches its own machine too.
436
+ rank = Number.POSITIVE_INFINITY;
437
+ origin = null;
438
+ }
439
+ else if (session.movie.kind === "local") {
440
+ requester = { host: null, scheme: null };
441
+ // A local SWF stands for nothing on the network: it reaches no private address of itself.
442
+ rank = RANK.public;
443
+ origin = null;
444
+ }
445
+ else if (session.origin) {
446
+ ({ requester, rank } = session.origin);
447
+ origin = session.origin.url.origin;
448
+ }
449
+ else {
450
+ throw new Refused("the SWF has not loaded");
451
+ }
452
+ let { method, headers, body } = request;
453
+ let url = request.target;
454
+ for (let hop = 0; hop <= this.limits.redirects; hop++) {
455
+ checkPort(url);
456
+ const target = await this.pin(url.hostname, signal);
457
+ const same = origin === url.origin;
458
+ const morePrivate = target.rank > rank;
459
+ const custom = headers.map(([n]) => n).filter((n) => n.toLowerCase() !== "content-type");
460
+ const needsAccess = !application && (morePrivate || (!same && request.purpose === "data"));
461
+ const needsHeaders = !application && !same && custom.length > 0;
462
+ if (needsAccess || needsHeaders) {
463
+ const policies = await abortable(this.policiesFor(session, url, target.address), signal);
464
+ if (needsAccess && !allowsAccess(policies, requester, url)) {
465
+ throw new Refused(morePrivate
466
+ ? `${url.host} is at ${target.address}, more private than the SWF, ` +
467
+ `and no policy file there grants ${describe(requester)}`
468
+ : `no policy file on ${url.origin} grants ${describe(requester)}`);
469
+ }
470
+ if (needsHeaders && !allowsHeaders(policies, requester, url, custom)) {
471
+ throw new Refused(`no policy file on ${url.origin} lets it send ${custom.join(", ")}`);
472
+ }
473
+ }
474
+ const response = await this.send(url, target.address, method, headers, body, signal, {
475
+ // sendToURL's answer is no one's to read.
476
+ read: request.purpose !== "send",
477
+ max: this.limits.response,
478
+ budget,
479
+ referer: refererFor(session.origin?.url ?? null, url),
480
+ });
481
+ const next = redirectOf(url, response);
482
+ if (!next) {
483
+ return { ...response, ...(url.href === request.url ? {} : { url: url.href }) };
484
+ }
485
+ // As browsers do: a 303, and a 301 or 302 after a POST, go on as a GET without the body.
486
+ if (response.status === 303 || (method === "POST" && response.status <= 302)) {
487
+ method = "GET";
488
+ body = null;
489
+ headers = [];
490
+ }
491
+ url = next;
492
+ }
493
+ throw new Refused("too many redirects");
494
+ }
495
+ /**
496
+ * The policy files of `url`'s site that count, fetched from `address`:
497
+ * its master policy file, and those the movie named under it as the
498
+ * master's meta-policy allows. Each is fetched once a movie and address,
499
+ * and aborted only with the movie, not with the request that first asked:
500
+ * the next finds it as it came.
501
+ */
502
+ async policiesFor(session, url, address) {
503
+ const get = (policyUrl) => cached(session.policies, `${policyUrl.href}|${address}`, () => this.urlPolicy(policyUrl, address, session.abort.signal));
504
+ const master = await get(new URL("/crossdomain.xml", url.origin));
505
+ const meta = urlMetaPolicy(master);
506
+ const others = [];
507
+ if (meta === "all" || meta === "by-content-type") {
508
+ for (const named of session.urlPolicyFiles) {
509
+ const directory = named.pathname.slice(0, named.pathname.lastIndexOf("/") + 1);
510
+ if (named.origin === url.origin &&
511
+ named.pathname !== "/crossdomain.xml" &&
512
+ url.pathname.startsWith(directory)) {
513
+ const other = await get(named);
514
+ if (other) {
515
+ others.push(other);
516
+ }
517
+ }
518
+ }
519
+ }
520
+ return usablePolicies(master, others);
521
+ }
522
+ /** A URL policy file, or null: missing, redirected, mistyped, refusing to be one, or none. */
523
+ async urlPolicy(url, address, signal) {
524
+ let response;
525
+ try {
526
+ response = await this.send(url, address, "GET", [], null, signal, {
527
+ read: true,
528
+ max: this.limits.policy,
529
+ referer: null,
530
+ timeout: this.limits.policyTimeout,
531
+ deadline: this.limits.policyTimeout,
532
+ });
533
+ }
534
+ catch {
535
+ return null;
536
+ }
537
+ // Redirects are not followed: the specification ignores one to another
538
+ // domain, and one within a site would move the file's scope.
539
+ if (response.status !== 200 || !response.bytes) {
540
+ return null;
541
+ }
542
+ const header = (name) => response.headers
543
+ .filter(([n]) => n.toLowerCase() === name)
544
+ .flatMap(([, v]) => v.split(","))
545
+ .map((v) => v.trim().toLowerCase())
546
+ .filter((v) => v !== "");
547
+ const contentType = (header("content-type")[0] ?? "").split(";")[0].trim();
548
+ const permitted = header("x-permitted-cross-domain-policies");
549
+ if (!policyContentType(contentType) || permitted.includes("none-this-response")) {
550
+ return null;
551
+ }
552
+ const policy = parsePolicy(new TextDecoder().decode(response.bytes));
553
+ return policy && { policy, url, contentType, permitted };
554
+ }
555
+ /**
556
+ * A socket policy: a connection to `address`:`port` that asks for one
557
+ * with <policy-file-request/> and its NUL, and reads the answer up to
558
+ * its NUL or its close; null where none comes in `timeout`.
559
+ */
560
+ socketPolicy(address, port, timeout, signal) {
561
+ return new Promise((answer) => {
562
+ const chunks = [];
563
+ let length = 0;
564
+ const socket = connect({ host: address, port });
565
+ const finish = (policy) => {
566
+ clearTimeout(timer);
567
+ signal.removeEventListener("abort", abort);
568
+ socket.destroy();
569
+ answer(policy);
570
+ };
571
+ const read = () => {
572
+ const text = Buffer.concat(chunks).toString("utf8");
573
+ const policy = parsePolicy(text.split("\0")[0]);
574
+ finish(policy && { policy, port });
575
+ };
576
+ const abort = () => finish(null);
577
+ const timer = setTimeout(abort, timeout);
578
+ signal.addEventListener("abort", abort, { once: true });
579
+ socket.on("connect", () => socket.write("<policy-file-request/>\0"));
580
+ socket.on("data", (chunk) => {
581
+ length += chunk.length;
582
+ if (length > this.limits.policy) {
583
+ finish(null);
584
+ return;
585
+ }
586
+ chunks.push(chunk);
587
+ if (chunk.includes(0)) {
588
+ read();
589
+ }
590
+ });
591
+ socket.on("end", read);
592
+ socket.on("error", () => finish(null));
593
+ });
594
+ }
595
+ /**
596
+ * One HTTP exchange with `address`, the URL's host pinned to it: its
597
+ * status and headers, and its body if `read` and the status is a
598
+ * success, up to `max` bytes. No cookies, no credentials, no
599
+ * compression asked for, no connection kept.
600
+ */
601
+ send(url, address, method, headers, body, signal, options) {
602
+ const timeout = options.timeout ?? this.limits.timeout;
603
+ const deadline = options.deadline ?? this.limits.deadline;
604
+ return new Promise((answer, fail) => {
605
+ if (signal.aborted) {
606
+ fail(new Refused("aborted"));
607
+ return;
608
+ }
609
+ const host = bare(url.hostname);
610
+ const outgoing = {
611
+ "User-Agent": USER_AGENT,
612
+ Accept: "*/*",
613
+ "x-flash-version": FLASH_VERSION,
614
+ };
615
+ if (options.referer) {
616
+ outgoing.Referer = options.referer;
617
+ }
618
+ for (const [name, value] of headers) {
619
+ outgoing[name] = value;
620
+ }
621
+ if (body) {
622
+ outgoing["Content-Length"] = String(body.length);
623
+ }
624
+ const https = url.protocol === "https:";
625
+ const family = isIP(address);
626
+ let request;
627
+ try {
628
+ request = (https ? httpsRequest : httpRequest)({
629
+ host,
630
+ port: url.port || (https ? 443 : 80),
631
+ path: `${url.pathname}${url.search}`,
632
+ method,
633
+ headers: outgoing,
634
+ agent: false,
635
+ // The address pinned for this request, whatever the resolver would say now.
636
+ lookup: (_name, opts, callback) => {
637
+ if (opts.all) {
638
+ callback(null, [
639
+ { address, family },
640
+ ]);
641
+ }
642
+ else {
643
+ callback(null, address, family);
644
+ }
645
+ },
646
+ ...(https ? { servername: isIP(host) ? undefined : host, ca: this.ca } : {}),
647
+ // Only where the user asked for this host: its certificate goes unchecked.
648
+ ...(https && this.unverified(host) ? { rejectUnauthorized: false } : {}),
649
+ });
650
+ }
651
+ catch (error) {
652
+ // What check() let through and Node still refuses: a refusal like any other.
653
+ fail(new Refused(error.message));
654
+ return;
655
+ }
656
+ let timer;
657
+ const overall = setTimeout(() => stop(new Refused("it took too long")), deadline);
658
+ let finished = false;
659
+ /** What else a stop takes down: a decompressor. */
660
+ const stopping = [];
661
+ const stop = (error) => {
662
+ if (!finished) {
663
+ finished = true;
664
+ for (const down of stopping) {
665
+ down();
666
+ }
667
+ clearTimeout(timer);
668
+ clearTimeout(overall);
669
+ signal.removeEventListener("abort", abort);
670
+ request.destroy();
671
+ fail(error);
672
+ }
673
+ };
674
+ const arm = () => {
675
+ clearTimeout(timer);
676
+ timer = setTimeout(() => stop(new Refused("it timed out")), timeout);
677
+ };
678
+ const abort = () => stop(new Refused("aborted"));
679
+ signal.addEventListener("abort", abort, { once: true });
680
+ arm();
681
+ request.on("error", (error) => stop(new Refused(error.message)));
682
+ request.on("response", (response) => {
683
+ const status = response.statusCode ?? 0;
684
+ const received = [];
685
+ const raw = response.rawHeaders;
686
+ for (let i = 0; i + 1 < raw.length; i += 2) {
687
+ // The SWF has no cookie jar to keep them in.
688
+ if (!/^set-cookie2?$/i.test(raw[i])) {
689
+ received.push([raw[i], raw[i + 1]]);
690
+ }
691
+ }
692
+ const ok = status >= 200 && status < 300;
693
+ const end = (bytes) => {
694
+ if (!finished) {
695
+ finished = true;
696
+ clearTimeout(timer);
697
+ clearTimeout(overall);
698
+ signal.removeEventListener("abort", abort);
699
+ request.destroy();
700
+ answer({ bytes, status, headers: received });
701
+ }
702
+ };
703
+ if (!ok || !options.read) {
704
+ end(null);
705
+ return;
706
+ }
707
+ const declared = Number(response.headers["content-length"]);
708
+ if (declared > options.max) {
709
+ stop(new Refused(`its response is larger than ${options.max} bytes`));
710
+ return;
711
+ }
712
+ // A body a server compressed though no one asked: decompressed as it
713
+ // comes, the limits counting what comes out. One coding is undone;
714
+ // more than one, which no server needs, is refused.
715
+ const codings = String(response.headers["content-encoding"] ?? "")
716
+ .split(",")
717
+ .map((c) => c.trim().toLowerCase())
718
+ .filter((c) => c !== "" && c !== "identity");
719
+ if (codings.length > 1) {
720
+ stop(new Refused(`its response is encoded ${codings.length} times`));
721
+ return;
722
+ }
723
+ const coding = codings[0] ?? "";
724
+ const coded = DECODED.has(coding);
725
+ if (coded) {
726
+ const named = (n) => /^content-(encoding|length)$/i.test(n);
727
+ received.splice(0, received.length, ...received.filter(([n]) => !named(n)));
728
+ }
729
+ // The body, copied as it comes into one array that doubles as it
730
+ // fills: never a list of the chunks, which for a body sent a byte at
731
+ // a time would hold hundreds of times its size. It starts at the
732
+ // declared length, where that is believable, and so needs no copy at
733
+ // the end.
734
+ const budget = options.budget;
735
+ const exact = !coded && Number.isSafeInteger(declared) && declared >= 0;
736
+ let body = new Uint8Array(0);
737
+ let length = 0;
738
+ const reserve = (capacity) => {
739
+ const more = capacity - body.length;
740
+ if (budget) {
741
+ if (budget.session.buffered + more > this.limits.buffered) {
742
+ stop(new Refused("the SWF's responses under way hold too much at once"));
743
+ return false;
744
+ }
745
+ budget.session.buffered += more;
746
+ budget.taken += more;
747
+ }
748
+ const grown = new Uint8Array(capacity);
749
+ grown.set(body.subarray(0, length));
750
+ body = grown;
751
+ return true;
752
+ };
753
+ if (exact && declared > 0 && !reserve(declared)) {
754
+ return;
755
+ }
756
+ const take = (chunk) => {
757
+ if (finished) {
758
+ return;
759
+ }
760
+ if (length + chunk.length > options.max) {
761
+ stop(new Refused(`its response is larger than ${options.max} bytes`));
762
+ return;
763
+ }
764
+ if (length + chunk.length > body.length) {
765
+ const capacity = Math.min(options.max, Math.max(length + chunk.length, body.length * 2));
766
+ if (!reserve(capacity)) {
767
+ return;
768
+ }
769
+ }
770
+ body.set(chunk, length);
771
+ length += chunk.length;
772
+ arm();
773
+ };
774
+ // Bytes of their own, not a view of a larger array, whose rest IPC
775
+ // would carry to the page with them; the larger array let go of at
776
+ // once, so that it may be collected before IPC copies the rest.
777
+ const done = () => {
778
+ const bytes = length === body.length ? body : body.slice(0, length);
779
+ body = new Uint8Array(0);
780
+ end(bytes);
781
+ };
782
+ arm();
783
+ if (!coded) {
784
+ response.on("data", take);
785
+ response.on("end", done);
786
+ }
787
+ else {
788
+ const decompressor = new Decompressor(coding, take, done, () => stop(new Refused("its response does not decompress")));
789
+ stopping.push(() => decompressor.destroy());
790
+ decompressor.onDrain = () => response.resume();
791
+ response.on("data", (chunk) => {
792
+ arm();
793
+ if (!decompressor.write(chunk)) {
794
+ response.pause();
795
+ }
796
+ });
797
+ response.on("end", () => decompressor.end());
798
+ }
799
+ response.on("error", (error) => stop(new Refused(error.message)));
800
+ response.on("aborted", () => stop(new Refused("the response broke off")));
801
+ });
802
+ request.end(body ?? undefined);
803
+ });
804
+ }
805
+ }
806
+ /**
807
+ * Whether `url` is potentially trustworthy, as the Secure Contexts
808
+ * specification has it for a Referer: https, or this machine by its
809
+ * loopback address or a name under localhost.
810
+ */
811
+ function trustworthy(url) {
812
+ if (url.protocol === "https:") {
813
+ return true;
814
+ }
815
+ // A name with its root's dot is the same name; of IPv6 addresses the
816
+ // specification names ::1 alone, not an IPv4 loopback address inside one.
817
+ const host = bare(url.hostname).toLowerCase().replace(/\.$/, "");
818
+ return (host === "localhost" ||
819
+ host.endsWith(".localhost") ||
820
+ host === "::1" ||
821
+ (isIPv4(host) && addressClass(host) === "loopback"));
822
+ }
823
+ /** The longest Referer sent whole, as the Referrer Policy specification has it. */
824
+ const MAX_REFERER = 4096;
825
+ /**
826
+ * The Referer a request from the SWF at `swf` to `target` carries, as
827
+ * browsers' strict-origin-when-cross-origin has it: the SWF's URL to its
828
+ * own origin, its origin alone to another, and nothing from https to what
829
+ * is not potentially trustworthy; a URL longer than 4096 characters goes
830
+ * as its origin.
831
+ */
832
+ export function refererFor(swf, target) {
833
+ if (!swf || (swf.protocol === "https:" && !trustworthy(target))) {
834
+ return null;
835
+ }
836
+ const origin = `${swf.origin}/`;
837
+ if (swf.origin !== target.origin || swf.href.length > MAX_REFERER) {
838
+ return origin.length > MAX_REFERER ? null : origin;
839
+ }
840
+ return swf.href;
841
+ }
842
+ /** `promise`, or a Refused once `signal` aborts: a request stops waiting on a shared policy. */
843
+ function abortable(promise, signal) {
844
+ if (signal.aborted) {
845
+ return Promise.reject(new Refused("aborted"));
846
+ }
847
+ return new Promise((resolve, reject) => {
848
+ const stop = () => reject(new Refused("aborted"));
849
+ signal.addEventListener("abort", stop, { once: true });
850
+ promise.then((value) => {
851
+ signal.removeEventListener("abort", stop);
852
+ resolve(value);
853
+ }, (error) => {
854
+ signal.removeEventListener("abort", stop);
855
+ reject(error);
856
+ });
857
+ });
858
+ }
859
+ /** A host name without an IPv6 address's brackets. */
860
+ function bare(host) {
861
+ return host.replace(/^\[(.*)\]$/, "$1");
862
+ }
863
+ function checkPort(url) {
864
+ const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
865
+ if (badPort(port)) {
866
+ throw new Refused(`port ${port} is not for HTTP`);
867
+ }
868
+ }
869
+ /** Where a redirect goes, if `response` is one, to http or https; one elsewhere is refused. */
870
+ function redirectOf(url, response) {
871
+ if (!REDIRECTS.has(response.status)) {
872
+ return null;
873
+ }
874
+ const location = response.headers.find(([n]) => n.toLowerCase() === "location")?.[1];
875
+ if (location === undefined) {
876
+ return null;
877
+ }
878
+ let next;
879
+ try {
880
+ next = new URL(location, url);
881
+ }
882
+ catch {
883
+ throw new Refused("a redirect to no URL");
884
+ }
885
+ if (next.protocol !== "http:" && next.protocol !== "https:") {
886
+ throw new Refused(`a redirect to ${next.protocol}`);
887
+ }
888
+ if (next.username || next.password) {
889
+ throw new Refused("a redirect to a URL with credentials");
890
+ }
891
+ next.hash = "";
892
+ return next;
893
+ }
894
+ /** `key`'s value in `cache`, made once with `make`; a cache grown past its bound starts again. */
895
+ function cached(cache, key, make) {
896
+ let value = cache.get(key);
897
+ if (!value) {
898
+ if (cache.size >= 512) {
899
+ cache.clear();
900
+ }
901
+ value = make();
902
+ cache.set(key, value);
903
+ }
904
+ return value;
905
+ }
906
+ function describe(requester) {
907
+ return requester.host === null ? 'a local SWF (domain="*")' : requester.host;
908
+ }
909
+ //# sourceMappingURL=network.js.map