@swf2es/desktop 0.1.19 → 0.1.21
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 +13 -8
- package/dist/main/air-bridge.d.ts +7 -0
- package/dist/main/air-bridge.d.ts.map +1 -0
- package/dist/main/air-bridge.js +166 -0
- package/dist/main/air-bridge.js.map +1 -0
- package/dist/main/dialogs.d.ts +18 -0
- package/dist/main/dialogs.d.ts.map +1 -0
- package/dist/main/dialogs.js +128 -0
- package/dist/main/dialogs.js.map +1 -0
- package/dist/main/main.js +24 -789
- package/dist/main/main.js.map +1 -1
- package/dist/main/menu.d.ts +2 -0
- package/dist/main/menu.d.ts.map +1 -0
- package/dist/main/menu.js +80 -0
- package/dist/main/menu.js.map +1 -0
- package/dist/main/network-rules.d.ts +35 -0
- package/dist/main/network-rules.d.ts.map +1 -0
- package/dist/main/network-rules.js +167 -0
- package/dist/main/network-rules.js.map +1 -0
- package/dist/main/network-types.d.ts +131 -0
- package/dist/main/network-types.d.ts.map +1 -0
- package/dist/main/network-types.js +21 -0
- package/dist/main/network-types.js.map +1 -0
- package/dist/main/network.d.ts +1 -113
- package/dist/main/network.d.ts.map +1 -1
- package/dist/main/network.js +8 -181
- package/dist/main/network.js.map +1 -1
- package/dist/main/open.d.ts +55 -0
- package/dist/main/open.d.ts.map +1 -0
- package/dist/main/open.js +256 -0
- package/dist/main/open.js.map +1 -0
- package/dist/main/permissions.d.ts +17 -0
- package/dist/main/permissions.d.ts.map +1 -0
- package/dist/main/permissions.js +113 -0
- package/dist/main/permissions.js.map +1 -0
- package/dist/main/settings.d.ts +8 -1
- package/dist/main/settings.d.ts.map +1 -1
- package/dist/main/settings.js +9 -4
- package/dist/main/settings.js.map +1 -1
- package/dist/main/state.d.ts +17 -0
- package/dist/main/state.d.ts.map +1 -0
- package/dist/main/state.js +30 -0
- package/dist/main/state.js.map +1 -0
- package/dist/main/switches.d.ts +25 -0
- package/dist/main/switches.d.ts.map +1 -0
- package/dist/main/switches.js +69 -0
- package/dist/main/switches.js.map +1 -0
- package/dist/renderer/index.js +11 -8
- package/dist/renderer/index.js.map +1 -1
- package/package.json +7 -7
- package/src/main/air-bridge.ts +185 -0
- package/src/main/dialogs.ts +147 -0
- package/src/main/main.ts +52 -938
- package/src/main/menu.ts +84 -0
- package/src/main/network-rules.ts +199 -0
- package/src/main/network-types.ts +150 -0
- package/src/main/network.ts +30 -337
- package/src/main/open.ts +302 -0
- package/src/main/permissions.ts +135 -0
- package/src/main/settings.ts +16 -5
- package/src/main/state.ts +44 -0
- package/src/main/switches.ts +87 -0
- package/src/renderer/index.ts +18 -7
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Whether the SWF playing may open a socket or a WebSocket, and where to:
|
|
2
|
+
// the user asked once a server, and a remote SWF's socket policy.
|
|
3
|
+
import { dialog } from "electron";
|
|
4
|
+
import { allowedOnce, current, deniedOnce, networkMovie } from "./open.ts";
|
|
5
|
+
import { sandbox, settings, window } from "./state.ts";
|
|
6
|
+
import { network, proxiedWebSockets, proxy } from "./switches.ts";
|
|
7
|
+
import { hasWebSocket } from "./websocket-connections.ts";
|
|
8
|
+
|
|
9
|
+
/** The prompts on screen, by "host:port": a second connection there waits on the first's answer. */
|
|
10
|
+
const asking = new Map<string, Promise<boolean>>();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether the SWF playing may connect a socket to `host`:`port`, and where
|
|
14
|
+
* to: only with the network, and only as the user allows, once or always
|
|
15
|
+
* for that SWF, when it first asks for that server; and for a remote SWF,
|
|
16
|
+
* as that server's socket policy allows, at the address it came from.
|
|
17
|
+
* Denied where no one can be asked.
|
|
18
|
+
*/
|
|
19
|
+
export async function permitSocket(
|
|
20
|
+
host: string,
|
|
21
|
+
port: number,
|
|
22
|
+
): Promise<{ address?: string } | null> {
|
|
23
|
+
// AIR's application sandbox connects where it likes, unasked, with no socket policy.
|
|
24
|
+
if (sandbox.application) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const playing = current;
|
|
29
|
+
const swf = sandbox.swf;
|
|
30
|
+
const endpoint = `${host}:${port}`;
|
|
31
|
+
if (!playing || !swf || !sandbox.networkAllowed()) {
|
|
32
|
+
process.stderr.write(`swf2es: no socket to ${endpoint}: the SWF has no network\n`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!(await allowedByUser(playing, swf, endpoint))) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (networkMovie?.kind !== "remote") {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
return { address: await network.socketAddress(host, port) };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
process.stderr.write(`swf2es: no socket to ${endpoint}: ${(error as Error).message}\n`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether the user allows the SWF playing a socket to `endpoint`, asked once a server. */
|
|
53
|
+
async function allowedByUser(
|
|
54
|
+
playing: NonNullable<typeof current>,
|
|
55
|
+
swf: string,
|
|
56
|
+
endpoint: string,
|
|
57
|
+
): Promise<boolean> {
|
|
58
|
+
if (settings.socketAllowed(swf, endpoint) || allowedOnce.has(endpoint)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (deniedOnce.has(endpoint)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let answer = asking.get(endpoint);
|
|
67
|
+
if (!answer) {
|
|
68
|
+
answer = askForSocket(playing.movie.name, swf, endpoint).finally(() => asking.delete(endpoint));
|
|
69
|
+
asking.set(endpoint, answer);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (await answer) && current === playing;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function askForSocket(name: string, swf: string, endpoint: string): Promise<boolean> {
|
|
76
|
+
if (!window) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const playing = current;
|
|
81
|
+
let response: number;
|
|
82
|
+
try {
|
|
83
|
+
({ response } = await dialog.showMessageBox(window, {
|
|
84
|
+
type: "question",
|
|
85
|
+
message: `${name} asks to connect to ${endpoint}.`,
|
|
86
|
+
detail:
|
|
87
|
+
"A socket lets the SWF send that server whatever it has. " +
|
|
88
|
+
"Allow it only for a server you trust.",
|
|
89
|
+
buttons: ["Allow Once", "Always Allow for This SWF", "Deny"],
|
|
90
|
+
defaultId: 2,
|
|
91
|
+
cancelId: 2,
|
|
92
|
+
noLink: true,
|
|
93
|
+
}));
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (current !== playing) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switch (response) {
|
|
103
|
+
case 0:
|
|
104
|
+
allowedOnce.add(endpoint);
|
|
105
|
+
return true;
|
|
106
|
+
case 1:
|
|
107
|
+
if (!settings.allowSocket(swf, endpoint)) {
|
|
108
|
+
allowedOnce.add(endpoint);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return true;
|
|
112
|
+
default:
|
|
113
|
+
deniedOnce.add(endpoint);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether what plays may open a WebSocket to `url`: an AIR application,
|
|
120
|
+
* unasked and anywhere, as AIR's application sandbox connects and as its
|
|
121
|
+
* sockets do here. air.net.WebSocket is AIR's alone, so nothing else has one.
|
|
122
|
+
*/
|
|
123
|
+
export function permitWebSocket(url: string): boolean {
|
|
124
|
+
if (sandbox.application && hasWebSocket() && (!proxy || proxiedWebSockets)) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const why = !sandbox.application
|
|
129
|
+
? "only an AIR application has one"
|
|
130
|
+
: !hasWebSocket()
|
|
131
|
+
? "this Electron has no WebSocket client"
|
|
132
|
+
: "this Electron's WebSocket cannot go through the proxy";
|
|
133
|
+
process.stderr.write(`swf2es: no WebSocket to ${url.slice(0, 2048)}: ${why}\n`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
package/src/main/settings.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
// What the app remembers between runs, in settings.json under its user
|
|
2
|
-
// data: a playerglobal.abc the user chose,
|
|
2
|
+
// data: a playerglobal.abc the user chose, the SWFs opened last, and
|
|
3
|
+
// Stage3D turned off or on from the View menu.
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
import type { LibraryName, LibraryState } from "../shared/api.js";
|
|
7
8
|
|
|
8
9
|
export interface Settings {
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Stage3D as the View menu last set it; absent until it does, which is on. Not `stage3D`, the
|
|
12
|
+
* key of the opt-in this replaced: every write saved that one, its `false` along with any other
|
|
13
|
+
* change, so a stored `false` there does not tell a user's choice from the old default.
|
|
14
|
+
*/
|
|
15
|
+
stage3DChoice?: boolean;
|
|
10
16
|
playerglobal?: string;
|
|
11
17
|
/** Most recent first. */
|
|
12
18
|
recent: string[];
|
|
@@ -49,8 +55,13 @@ export class SettingsFile {
|
|
|
49
55
|
return this.update({ [name]: undefined });
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
/** Whether SWFs get a Context3D: on unless the user turned it off. */
|
|
59
|
+
get stage3D(): boolean {
|
|
60
|
+
return this.current.stage3DChoice ?? true;
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
setStage3D(enabled: boolean): boolean {
|
|
53
|
-
return this.update({
|
|
64
|
+
return this.update({ stage3DChoice: enabled });
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
/** `path` opened: first in the recent list. */
|
|
@@ -84,7 +95,7 @@ export class SettingsFile {
|
|
|
84
95
|
const read = JSON.parse(readFileSync(this.path, "utf8"));
|
|
85
96
|
const text = (value: unknown) => (typeof value === "string" ? value : undefined);
|
|
86
97
|
return {
|
|
87
|
-
|
|
98
|
+
stage3DChoice: typeof read.stage3DChoice === "boolean" ? read.stage3DChoice : undefined,
|
|
88
99
|
playerglobal: text(read.playerglobal),
|
|
89
100
|
recent: strings(read.recent),
|
|
90
101
|
sockets: Object.fromEntries(
|
|
@@ -95,7 +106,7 @@ export class SettingsFile {
|
|
|
95
106
|
};
|
|
96
107
|
} catch {
|
|
97
108
|
// None yet, or unreadable: start again rather than refuse to start.
|
|
98
|
-
return {
|
|
109
|
+
return { recent: [], sockets: {} };
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// What the main process's modules share: the window, its page, the
|
|
2
|
+
// settings and the sandbox of what plays. main.ts alone changes the
|
|
3
|
+
// window, the settings and whether the page listens, through the setters
|
|
4
|
+
// here, since a module cannot assign another's bindings; the rest read
|
|
5
|
+
// them as they are now.
|
|
6
|
+
import type { BrowserWindow, IpcMainEvent, IpcMainInvokeEvent } from "electron";
|
|
7
|
+
import { APP_ORIGIN } from "./protocol.ts";
|
|
8
|
+
import { Sandbox } from "./sandbox.ts";
|
|
9
|
+
import type { SettingsFile } from "./settings.ts";
|
|
10
|
+
|
|
11
|
+
export const TITLE = "swf2es";
|
|
12
|
+
|
|
13
|
+
export const sandbox = new Sandbox();
|
|
14
|
+
export let settings: SettingsFile;
|
|
15
|
+
export let window: BrowserWindow | null = null;
|
|
16
|
+
/**
|
|
17
|
+
* The page has asked to start and listens for what opens: until then, start() hands it the movie.
|
|
18
|
+
*/
|
|
19
|
+
export let listening = false;
|
|
20
|
+
|
|
21
|
+
export function setWindow(created: BrowserWindow | null): void {
|
|
22
|
+
window = created;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function setSettings(read: SettingsFile): void {
|
|
26
|
+
settings = read;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function setListening(now: boolean): void {
|
|
30
|
+
listening = now;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether a message comes from the window's own page, on swf2es://app/. */
|
|
34
|
+
export function trusted(event: IpcMainEvent | IpcMainInvokeEvent): boolean {
|
|
35
|
+
const frame = event.senderFrame;
|
|
36
|
+
return (
|
|
37
|
+
window !== null &&
|
|
38
|
+
event.sender === window.webContents &&
|
|
39
|
+
frame !== null &&
|
|
40
|
+
frame.parent === null &&
|
|
41
|
+
// Node's URL gives a scheme it does not know no origin: compare the text.
|
|
42
|
+
frame.url.startsWith(`${APP_ORIGIN}/`)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The command line's switches for this run alone, and what they make:
|
|
2
|
+
// --trace and the network's logs, --ignore-certificate-errors and --proxy,
|
|
3
|
+
// the network judged through them and what opens an AIR application's
|
|
4
|
+
// WebSockets. Each said on stderr as the app starts.
|
|
5
|
+
import { Network } from "./network.ts";
|
|
6
|
+
import { unverifiedHosts } from "./network-rules.ts";
|
|
7
|
+
import { describeProxy, proxyArgument } from "./proxy.ts";
|
|
8
|
+
import {
|
|
9
|
+
dispatchedOpener,
|
|
10
|
+
type Opener,
|
|
11
|
+
proxiedDispatcher,
|
|
12
|
+
unverifiedDispatcher,
|
|
13
|
+
webSocketOpener,
|
|
14
|
+
} from "./websocket-connections.ts";
|
|
15
|
+
|
|
16
|
+
/** --trace: what the page logs, a SWF's trace() among it, goes to the terminal. */
|
|
17
|
+
export const trace = process.argv.includes("--trace");
|
|
18
|
+
// Each request and socket of the SWF's said on stderr: the page's DevTools show none of them.
|
|
19
|
+
// --log-sockets says what each socket sends and receives as well, and so the rest too.
|
|
20
|
+
export const logSockets = process.argv.includes("--log-sockets");
|
|
21
|
+
const logNetwork = logSockets || process.argv.includes("--log-network");
|
|
22
|
+
export const netLog = logNetwork ? (line: string) => process.stderr.write(`${line}\n`) : null;
|
|
23
|
+
/** The SWF's http and https requests, judged as Flash's sandboxes had them. */
|
|
24
|
+
/**
|
|
25
|
+
* --ignore-certificate-errors[=host,...]: HTTPS servers whose certificates go
|
|
26
|
+
* unchecked, for one's own with a certificate it signed itself. For this
|
|
27
|
+
* run alone, never kept, and said aloud: anyone on the way to such a host
|
|
28
|
+
* can read and change what the SWF loads from it.
|
|
29
|
+
*/
|
|
30
|
+
const unverified = unverifiedHosts(process.argv.slice(1));
|
|
31
|
+
if (unverified) {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
unverified.all
|
|
34
|
+
? "swf2es: --ignore-certificate-errors: no HTTPS or WSS server's certificate is verified\n"
|
|
35
|
+
: `swf2es: --ignore-certificate-errors: certificates are not verified for ${unverified.hosts.join(", ") || "no host"}\n`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const unverifiedHost = (host: string) =>
|
|
40
|
+
!!unverified && (unverified.all || unverified.hosts.includes(host.toLowerCase()));
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* --proxy=<url>: what the SWF's requests, sockets and WebSockets go
|
|
44
|
+
* through. For this run alone, never kept, and said aloud, without its
|
|
45
|
+
* credentials. One that does not parse stops the app rather than letting
|
|
46
|
+
* it run without.
|
|
47
|
+
*/
|
|
48
|
+
export const proxied = proxyArgument(process.argv.slice(1));
|
|
49
|
+
if (proxied && "error" in proxied) {
|
|
50
|
+
process.stderr.write(`swf2es: ${proxied.error}\n`);
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const proxy = proxied?.proxy ?? null;
|
|
55
|
+
if (proxy) {
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
`swf2es: --proxy: the SWF's requests, sockets and WebSockets go through ${describeProxy(proxy)}\n`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What --log-network adds to a line of a connection that went through the proxy. */
|
|
62
|
+
export const via = proxy ? " via proxy" : "";
|
|
63
|
+
export const warn = (message: string) => process.stderr.write(`swf2es: ${message}\n`);
|
|
64
|
+
export const network = new Network({ unverified: unverifiedHost, proxy });
|
|
65
|
+
// What opens an AIR application's WebSockets: the global's client, a wss: server's certificate
|
|
66
|
+
// unchecked where the switch names its host; or every one through the proxy.
|
|
67
|
+
const insecureWebSockets = unverified && !proxy ? unverifiedDispatcher() : null;
|
|
68
|
+
if (unverified && !proxy && !insecureWebSockets) {
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
"swf2es: --ignore-certificate-errors: a WebSocket's certificate is verified all the same\n",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const proxiedWebSockets = proxy ? proxiedDispatcher(proxy, unverifiedHost, warn) : null;
|
|
75
|
+
// Never past the proxy: where none can be made, no WebSocket opens.
|
|
76
|
+
export const webSocketOpen: Opener | undefined = proxy
|
|
77
|
+
? proxiedWebSockets
|
|
78
|
+
? dispatchedOpener(proxiedWebSockets)
|
|
79
|
+
: () => {
|
|
80
|
+
throw new Error("no WebSocket through the proxy");
|
|
81
|
+
}
|
|
82
|
+
: insecureWebSockets
|
|
83
|
+
? webSocketOpener(unverifiedHost, insecureWebSockets)
|
|
84
|
+
: undefined;
|
|
85
|
+
if (proxy && !proxiedWebSockets) {
|
|
86
|
+
process.stderr.write("swf2es: --proxy: this Electron's WebSocket cannot go through a proxy\n");
|
|
87
|
+
}
|
package/src/renderer/index.ts
CHANGED
|
@@ -113,7 +113,7 @@ document.addEventListener("drop", (event) => {
|
|
|
113
113
|
}
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
// An open or close
|
|
116
|
+
// An open or close while the page starts takes precedence over the startup snapshot.
|
|
117
117
|
let started = false;
|
|
118
118
|
let pending: OpenedMovie | null | undefined;
|
|
119
119
|
desktop.onOpen((movie) => {
|
|
@@ -133,12 +133,23 @@ desktop.onClose(() => {
|
|
|
133
133
|
const state = await desktop.start();
|
|
134
134
|
configure({ build: state.build ?? undefined });
|
|
135
135
|
if (state.stage3D) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
// The WebGL2 backend loads when a SWF first asks for a context, so that one that never does
|
|
137
|
+
// starts without waiting for it.
|
|
138
|
+
configure({
|
|
139
|
+
stage3D: {
|
|
140
|
+
load: () =>
|
|
141
|
+
import("./stage3d.js").then(
|
|
142
|
+
(backend) => backend.createStage3D,
|
|
143
|
+
(error) => {
|
|
144
|
+
console.warn(
|
|
145
|
+
"swf2es: Stage3D backend could not be loaded; Context3D unavailable.",
|
|
146
|
+
error,
|
|
147
|
+
);
|
|
148
|
+
throw error;
|
|
149
|
+
},
|
|
150
|
+
),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
useLibraries(state.libraries);
|