@swf2es/desktop 0.0.0-stage → 0.1.14

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 +132 -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 +979 -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 +1146 -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,630 @@
1
+ // Flash's cross-domain policy files, as Adobe's Cross Domain Policy File
2
+ // Specification (version 2.0, 2010) defines them and Flash Player 10 and
3
+ // later applied them: the parser, and what a set of policy files grants a
4
+ // requesting SWF. A URL policy file grants reading a site's data and
5
+ // sending it headers; a socket policy file grants connecting to ports.
6
+ // Pure, without Electron, so that node tests it (tests/desktop/policy.test.ts).
7
+
8
+ export type MetaPolicy = "none" | "master-only" | "by-content-type" | "by-ftp-filename" | "all";
9
+
10
+ const META_POLICIES = new Set<string>([
11
+ "none",
12
+ "master-only",
13
+ "by-content-type",
14
+ "by-ftp-filename",
15
+ "all",
16
+ ]);
17
+
18
+ /** A port, or an inclusive range of them. */
19
+ type Ports = readonly (readonly [from: number, to: number])[];
20
+
21
+ export interface AccessRule {
22
+ domain: string;
23
+ /** The attribute as written; null where left out, its default then the file kind's. */
24
+ secure: boolean | null;
25
+ /** to-ports, for a socket policy; null where it was left out or does not parse. */
26
+ ports: Ports | null;
27
+ }
28
+
29
+ export interface HeaderRule {
30
+ domain: string;
31
+ /** Lower case: a name, a prefix ending in "*", or "*". */
32
+ headers: readonly string[];
33
+ secure: boolean | null;
34
+ }
35
+
36
+ export interface Policy {
37
+ /** site-control's permitted-cross-domain-policies; null where there is none. */
38
+ siteControl: MetaPolicy | null;
39
+ access: readonly AccessRule[];
40
+ headers: readonly HeaderRule[];
41
+ }
42
+
43
+ /** Who asks: a remote SWF by its URL's host and scheme; a local one has neither. */
44
+ export interface Requester {
45
+ /** Lower case, an IPv6 address without its brackets; null for a local SWF. */
46
+ host: string | null;
47
+ /** "http" or "https"; null for a local SWF. */
48
+ scheme: string | null;
49
+ }
50
+
51
+ /** A URL policy file as it came: whence, its Content-Type and X-Permitted-Cross-Domain-Policies. */
52
+ export interface UrlPolicy {
53
+ policy: Policy;
54
+ url: URL;
55
+ /** The media type alone, lower case. */
56
+ contentType: string;
57
+ /** X-Permitted-Cross-Domain-Policies' values, lower case. */
58
+ permitted: readonly string[];
59
+ }
60
+
61
+ /** A socket policy file and the port it was served from. */
62
+ export interface SocketPolicy {
63
+ policy: Policy;
64
+ port: number;
65
+ }
66
+
67
+ const NAME = /^[A-Za-z_:][-A-Za-z0-9_:.]*/;
68
+ const ATTRIBUTE = /^\s+([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:"([^"<]*)"|'([^'<]*)')/;
69
+
70
+ function decodeEntities(text: string): string | null {
71
+ let failed = false;
72
+ const decoded = text.replace(/&([^;&\s]*);?/g, (whole, name: string) => {
73
+ if (!whole.endsWith(";")) {
74
+ failed = true;
75
+ return whole;
76
+ }
77
+
78
+ switch (name) {
79
+ case "amp":
80
+ return "&";
81
+ case "lt":
82
+ return "<";
83
+ case "gt":
84
+ return ">";
85
+ case "quot":
86
+ return '"';
87
+ case "apos":
88
+ return "'";
89
+ }
90
+
91
+ const code = /^#x([0-9a-f]{1,6})$/i.exec(name)
92
+ ? Number.parseInt(name.slice(2), 16)
93
+ : /^#([0-9]{1,7})$/.exec(name)
94
+ ? Number.parseInt(name.slice(1), 10)
95
+ : -1;
96
+ if (code < 0 || code > 0x10ffff) {
97
+ failed = true;
98
+ return whole;
99
+ }
100
+
101
+ return String.fromCodePoint(code);
102
+ });
103
+ return failed ? null : decoded;
104
+ }
105
+
106
+ interface Element {
107
+ name: string;
108
+ attributes: Map<string, string>;
109
+ }
110
+
111
+ /**
112
+ * The root's children, each with its attributes, if `text` is a
113
+ * well-formed document whose root is <cross-domain-policy>; null
114
+ * otherwise. A DOCTYPE is skipped, never read, so no entity of its own
115
+ * is expanded. What the children hold is checked for form and ignored.
116
+ */
117
+ function rootChildren(text: string): Element[] | null {
118
+ let at = text.charCodeAt(0) === 0xfeff ? 1 : 0;
119
+ const open: string[] = [];
120
+ const children: Element[] = [];
121
+ let rooted = false;
122
+
123
+ const skipPast = (end: string): boolean => {
124
+ const found = text.indexOf(end, at);
125
+ if (found < 0) {
126
+ return false;
127
+ }
128
+
129
+ at = found + end.length;
130
+ return true;
131
+ };
132
+
133
+ while (at < text.length) {
134
+ const lt = text.indexOf("<", at);
135
+ const between = text.slice(at, lt < 0 ? text.length : lt);
136
+ // Text only inside the root; and none of it read.
137
+ if (open.length === 0 && between.trim() !== "") {
138
+ return null;
139
+ }
140
+
141
+ if (lt < 0) {
142
+ break;
143
+ }
144
+
145
+ at = lt;
146
+ if (text.startsWith("<?", at)) {
147
+ if (!skipPast("?>")) {
148
+ return null;
149
+ }
150
+ } else if (text.startsWith("<!--", at)) {
151
+ if (!skipPast("-->")) {
152
+ return null;
153
+ }
154
+ } else if (text.startsWith("<![CDATA[", at)) {
155
+ if (open.length === 0 || !skipPast("]]>")) {
156
+ return null;
157
+ }
158
+ } else if (text.startsWith("<!DOCTYPE", at)) {
159
+ if (rooted) {
160
+ return null;
161
+ }
162
+
163
+ // To its '>', past an internal subset in brackets.
164
+ const subset = text.indexOf("[", at);
165
+ const close = text.indexOf(">", at);
166
+ if (close < 0) {
167
+ return null;
168
+ }
169
+
170
+ if (subset >= 0 && subset < close) {
171
+ at = subset;
172
+ if (!skipPast("]") || !/^\s*>/.test(text.slice(at))) {
173
+ return null;
174
+ }
175
+
176
+ at = text.indexOf(">", at) + 1;
177
+ } else {
178
+ at = close + 1;
179
+ }
180
+ } else if (text.startsWith("</", at)) {
181
+ const name = NAME.exec(text.slice(at + 2))?.[0];
182
+ if (!name || open.pop() !== name) {
183
+ return null;
184
+ }
185
+
186
+ at += 2 + name.length;
187
+ const end = /^\s*>/.exec(text.slice(at));
188
+ if (!end) {
189
+ return null;
190
+ }
191
+
192
+ at += end[0].length;
193
+ } else {
194
+ const name = NAME.exec(text.slice(at + 1))?.[0];
195
+ if (!name) {
196
+ return null;
197
+ }
198
+
199
+ at += 1 + name.length;
200
+ const attributes = new Map<string, string>();
201
+ for (;;) {
202
+ const attribute = ATTRIBUTE.exec(text.slice(at));
203
+ if (!attribute) {
204
+ break;
205
+ }
206
+
207
+ const value = decodeEntities(attribute[2] ?? attribute[3]);
208
+ if (value === null || attributes.has(attribute[1])) {
209
+ return null;
210
+ }
211
+
212
+ attributes.set(attribute[1], value);
213
+ at += attribute[0].length;
214
+ }
215
+
216
+ const end = /^\s*(\/?)>/.exec(text.slice(at));
217
+ if (!end) {
218
+ return null;
219
+ }
220
+
221
+ at += end[0].length;
222
+ const empty = end[1] === "/";
223
+ if (open.length === 0) {
224
+ if (rooted || name !== "cross-domain-policy") {
225
+ return null;
226
+ }
227
+
228
+ rooted = true;
229
+ } else if (open.length === 1) {
230
+ children.push({ name, attributes });
231
+ }
232
+
233
+ if (!empty) {
234
+ open.push(name);
235
+ }
236
+ }
237
+ }
238
+
239
+ return rooted && open.length === 0 ? children : null;
240
+ }
241
+
242
+ /** to-ports: "*", or ports and ranges separated by commas; null if any part is not one. */
243
+ export function parsePorts(text: string): Ports | null {
244
+ if (text.trim() === "*") {
245
+ return [[1, 65535]];
246
+ }
247
+
248
+ const ports: [number, number][] = [];
249
+ for (const part of text.split(",")) {
250
+ const range = /^\s*(\d{1,5})\s*(?:-\s*(\d{1,5})\s*)?$/.exec(part);
251
+ if (!range) {
252
+ return null;
253
+ }
254
+
255
+ const from = Number(range[1]);
256
+ const to = range[2] === undefined ? from : Number(range[2]);
257
+ if (from < 1 || to > 65535 || from > to) {
258
+ return null;
259
+ }
260
+
261
+ ports.push([from, to]);
262
+ }
263
+
264
+ return ports;
265
+ }
266
+
267
+ function secureOf(value: string | undefined): boolean | null {
268
+ switch (value?.trim().toLowerCase()) {
269
+ case "true":
270
+ return true;
271
+ case "false":
272
+ return false;
273
+ default:
274
+ return null;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * The policy in `text`, or null where it is not one: not well-formed, or
280
+ * its root not <cross-domain-policy>. A socket server ends its answer with
281
+ * a NUL, which is not part of it. Rules without a domain are dropped.
282
+ */
283
+ export function parsePolicy(text: string): Policy | null {
284
+ const children = rootChildren(text.replace(/\0+$/, ""));
285
+ if (!children) {
286
+ return null;
287
+ }
288
+
289
+ let siteControl: MetaPolicy | null = null;
290
+ const access: AccessRule[] = [];
291
+ const headers: HeaderRule[] = [];
292
+ for (const { name, attributes } of children) {
293
+ const domain = attributes.get("domain")?.trim();
294
+ switch (name) {
295
+ case "site-control": {
296
+ const value = attributes.get("permitted-cross-domain-policies")?.trim().toLowerCase();
297
+ // An unknown value permits nothing more than none would.
298
+ siteControl = value && META_POLICIES.has(value) ? (value as MetaPolicy) : "none";
299
+ break;
300
+ }
301
+ case "allow-access-from": {
302
+ if (domain) {
303
+ const toPorts = attributes.get("to-ports");
304
+ access.push({
305
+ domain,
306
+ secure: secureOf(attributes.get("secure")),
307
+ ports: toPorts === undefined ? null : parsePorts(toPorts),
308
+ });
309
+ }
310
+
311
+ break;
312
+ }
313
+ case "allow-http-request-headers-from": {
314
+ const names = attributes.get("headers");
315
+ if (domain && names !== undefined) {
316
+ headers.push({
317
+ domain,
318
+ headers: names
319
+ .split(",")
320
+ .map((h) => h.trim().toLowerCase())
321
+ .filter((h) => h !== ""),
322
+ secure: secureOf(attributes.get("secure")),
323
+ });
324
+ }
325
+
326
+ break;
327
+ }
328
+ }
329
+ }
330
+
331
+ return { siteControl, access, headers };
332
+ }
333
+
334
+ /**
335
+ * Whether a domain attribute names `requester`, by the specification's
336
+ * matching rules: "*" is everyone, local SWFs among them, which nothing
337
+ * else names; a name or an IP address is itself exactly, an address
338
+ * never its host's name; "*.example.com" is example.com and every name
339
+ * under it, and a wildcard anywhere else matches nothing. "http://" or
340
+ * "https://" before the domain also asks for that scheme.
341
+ */
342
+ export function domainMatches(pattern: string, requester: Requester): boolean {
343
+ let domain = pattern.trim().toLowerCase();
344
+ if (domain === "*") {
345
+ return true;
346
+ }
347
+
348
+ if (requester.host === null) {
349
+ return false;
350
+ }
351
+
352
+ const scheme = /^(https?):\/\//.exec(domain);
353
+ if (scheme) {
354
+ if (scheme[1] !== requester.scheme) {
355
+ return false;
356
+ }
357
+
358
+ domain = domain.slice(scheme[0].length);
359
+ }
360
+
361
+ domain = domain.replace(/^\[(.*)\]$/, "$1");
362
+ if (domain.startsWith("*.")) {
363
+ const suffix = domain.slice(2);
364
+ // A second-level domain at least, and no address.
365
+ if (!suffix.includes(".") || suffix.includes("*") || /^[\d.]+$/.test(suffix)) {
366
+ return false;
367
+ }
368
+
369
+ return requester.host === suffix || requester.host.endsWith(`.${suffix}`);
370
+ }
371
+
372
+ return !domain.includes("*") && domain !== "" && requester.host === domain;
373
+ }
374
+
375
+ /**
376
+ * Whether an HTTPS policy's rule reaches `requester`: secure, its default,
377
+ * grants only a SWF itself loaded over HTTPS, as the specification has it;
378
+ * a local SWF, which no HTTPS brought, is not one. An HTTP policy's rules
379
+ * reach any.
380
+ */
381
+ function secureAllows(policyHttps: boolean, secure: boolean | null, requester: Requester): boolean {
382
+ return !policyHttps || secure === false || requester.scheme === "https";
383
+ }
384
+
385
+ /** The meta-policy restrictiveness order: a lower rank permits less. */
386
+ const RANK: Record<MetaPolicy, number> = {
387
+ none: 0,
388
+ "master-only": 1,
389
+ "by-ftp-filename": 1,
390
+ "by-content-type": 2,
391
+ all: 3,
392
+ };
393
+
394
+ /**
395
+ * The site's meta-policy, from its master policy file: the stricter of
396
+ * its site-control and the X-Permitted-Cross-Domain-Policies it came with,
397
+ * else Flash Player 10's default, master-only. by-ftp-filename is FTP's
398
+ * and permits no other file over HTTP. A site with no master policy file
399
+ * has the default.
400
+ */
401
+ export function urlMetaPolicy(master: UrlPolicy | null): MetaPolicy {
402
+ if (!master) {
403
+ return "master-only";
404
+ }
405
+
406
+ const given: MetaPolicy[] = [];
407
+ if (master.policy.siteControl) {
408
+ given.push(master.policy.siteControl);
409
+ }
410
+
411
+ for (const value of master.permitted) {
412
+ given.push(META_POLICIES.has(value) ? (value as MetaPolicy) : "none");
413
+ }
414
+
415
+ if (given.length === 0) {
416
+ return "master-only";
417
+ }
418
+
419
+ return given.reduce((a, b) => (RANK[b] < RANK[a] ? b : a));
420
+ }
421
+
422
+ /** The Content-Types a URL policy file may come with, as the specification lists them. */
423
+ export function policyContentType(type: string): boolean {
424
+ return type.startsWith("text/") || type === "application/xml" || type === "application/xhtml+xml";
425
+ }
426
+
427
+ /**
428
+ * The policy files of a site that count, by its meta-policy: none for
429
+ * "none", the master alone for master-only, and the others too for "all",
430
+ * or for by-content-type those served as text/x-cross-domain-policy.
431
+ * A file whose X-Permitted-Cross-Domain-Policies says none-this-response
432
+ * is no policy file at all; the caller has dropped it.
433
+ */
434
+ export function usablePolicies(
435
+ master: UrlPolicy | null,
436
+ others: readonly UrlPolicy[],
437
+ ): UrlPolicy[] {
438
+ const meta = urlMetaPolicy(master);
439
+ if (meta === "none") {
440
+ return [];
441
+ }
442
+
443
+ const usable = master ? [master] : [];
444
+ for (const other of others) {
445
+ if (
446
+ meta === "all" ||
447
+ (meta === "by-content-type" && other.contentType === "text/x-cross-domain-policy")
448
+ ) {
449
+ usable.push(other);
450
+ }
451
+ }
452
+
453
+ return usable;
454
+ }
455
+
456
+ /**
457
+ * Whether `policy` covers `target`: a master policy file its whole site,
458
+ * any other its own directory and below. The caller has checked that both
459
+ * are of one origin.
460
+ */
461
+ function covers(policy: UrlPolicy, target: URL): boolean {
462
+ const directory = policy.url.pathname.slice(0, policy.url.pathname.lastIndexOf("/") + 1);
463
+ return target.pathname.startsWith(directory);
464
+ }
465
+
466
+ /** Whether the usable `policies` of `target`'s site let `requester` read `target`. */
467
+ export function allowsAccess(
468
+ policies: readonly UrlPolicy[],
469
+ requester: Requester,
470
+ target: URL,
471
+ ): boolean {
472
+ return policies.some(
473
+ (p) =>
474
+ p.url.origin === target.origin &&
475
+ covers(p, target) &&
476
+ p.policy.access.some(
477
+ (rule) =>
478
+ domainMatches(rule.domain, requester) &&
479
+ secureAllows(p.url.protocol === "https:", rule.secure, requester),
480
+ ),
481
+ );
482
+ }
483
+
484
+ function headerAllowed(allowed: readonly string[], name: string): boolean {
485
+ return allowed.some((h) => (h.endsWith("*") ? name.startsWith(h.slice(0, -1)) : h === name));
486
+ }
487
+
488
+ /** Whether the usable `policies` of `target`'s site let `requester` send it each of `names`. */
489
+ export function allowsHeaders(
490
+ policies: readonly UrlPolicy[],
491
+ requester: Requester,
492
+ target: URL,
493
+ names: readonly string[],
494
+ ): boolean {
495
+ const rules = policies
496
+ .filter((p) => p.url.origin === target.origin && covers(p, target))
497
+ .flatMap((p) =>
498
+ p.policy.headers.filter(
499
+ (rule) =>
500
+ domainMatches(rule.domain, requester) &&
501
+ secureAllows(p.url.protocol === "https:", rule.secure, requester),
502
+ ),
503
+ );
504
+ return names.every((name) =>
505
+ rules.some((rule) => headerAllowed(rule.headers, name.toLowerCase())),
506
+ );
507
+ }
508
+
509
+ /**
510
+ * A socket server's meta-policy, from the policy on its port 843: its
511
+ * site-control, by default "all", so that a policy on any port counts.
512
+ * by-content-type and by-ftp-filename are HTTP's and FTP's, and permit no
513
+ * other socket policy.
514
+ */
515
+ export function socketMetaPolicy(master: SocketPolicy | null): MetaPolicy {
516
+ const meta = master?.policy.siteControl ?? "all";
517
+ return meta === "by-content-type" || meta === "by-ftp-filename" ? "master-only" : meta;
518
+ }
519
+
520
+ /**
521
+ * Whether one socket policy lets `requester` connect to `port`: a rule
522
+ * naming it whose to-ports holds the port, where a policy served from a
523
+ * port of 1024 or above grants only ports of 1024 and above, as Flash
524
+ * Player had it, since any user may serve one there. secure is false by
525
+ * default in a socket policy; true grants only a SWF loaded over HTTPS.
526
+ */
527
+ function socketGrants(policy: SocketPolicy, requester: Requester, port: number): boolean {
528
+ if (policy.port >= 1024 && port < 1024) {
529
+ return false;
530
+ }
531
+
532
+ return policy.policy.access.some(
533
+ (rule) =>
534
+ rule.ports?.some(([from, to]) => port >= from && port <= to) === true &&
535
+ domainMatches(rule.domain, requester) &&
536
+ (rule.secure !== true || requester.scheme === "https"),
537
+ );
538
+ }
539
+
540
+ /**
541
+ * Whether a socket server's policies let `requester` connect to `port`:
542
+ * its master policy, the one on port 843, and, as its meta-policy allows,
543
+ * those on other ports (the one on `port` itself, and those
544
+ * Security.loadPolicyFile named).
545
+ */
546
+ export function allowsSocket(
547
+ master: SocketPolicy | null,
548
+ others: readonly SocketPolicy[],
549
+ requester: Requester,
550
+ port: number,
551
+ ): boolean {
552
+ const meta = socketMetaPolicy(master);
553
+ if (meta === "none") {
554
+ return false;
555
+ }
556
+
557
+ if (master && socketGrants(master, requester, port)) {
558
+ return true;
559
+ }
560
+
561
+ return meta === "all" && others.some((p) => socketGrants(p, requester, port));
562
+ }
563
+
564
+ /**
565
+ * The request headers Flash Player refused a SWF outside AIR's application
566
+ * sandbox (URLRequestHeader), lower case, with "_" read as "-" as Flash
567
+ * read it.
568
+ */
569
+ const FORBIDDEN_HEADERS = new Set([
570
+ "accept-charset",
571
+ "accept-encoding",
572
+ "accept-ranges",
573
+ "age",
574
+ "allow",
575
+ "allowed",
576
+ "authorization",
577
+ "charge-to",
578
+ "connect",
579
+ "connection",
580
+ "content-length",
581
+ "content-location",
582
+ "content-range",
583
+ "cookie",
584
+ "date",
585
+ "delete",
586
+ "etag",
587
+ "expect",
588
+ "get",
589
+ "head",
590
+ "host",
591
+ "if-modified-since",
592
+ "keep-alive",
593
+ "last-modified",
594
+ "location",
595
+ "max-forwards",
596
+ "options",
597
+ "origin",
598
+ "post",
599
+ "proxy-authenticate",
600
+ "proxy-authorization",
601
+ "proxy-connection",
602
+ "public",
603
+ "put",
604
+ "range",
605
+ "referer",
606
+ "request-range",
607
+ "retry-after",
608
+ "server",
609
+ "te",
610
+ "trace",
611
+ "trailer",
612
+ "transfer-encoding",
613
+ "upgrade",
614
+ "uri",
615
+ "user-agent",
616
+ "vary",
617
+ "via",
618
+ "warning",
619
+ "www-authenticate",
620
+ "x-flash-version",
621
+ ]);
622
+
623
+ /**
624
+ * Whether a SWF may not send a header of this name: Flash's list, and
625
+ * what a browser forbids a page beside it (Proxy- and Sec- headers).
626
+ */
627
+ export function forbiddenHeader(name: string): boolean {
628
+ const lower = name.toLowerCase().replaceAll("_", "-");
629
+ return FORBIDDEN_HEADERS.has(lower) || lower.startsWith("proxy-") || lower.startsWith("sec-");
630
+ }