@astralyn/sash 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +17 -5
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/api.js +14 -45
- package/dist/app-state.js +99 -0
- package/dist/autostart/command.js +24 -0
- package/dist/autostart/context.js +43 -0
- package/dist/autostart/files.js +60 -0
- package/dist/autostart/installation.js +48 -0
- package/dist/autostart/start.js +47 -0
- package/dist/autostart/windows-registry.js +88 -0
- package/dist/autostart/windows.js +59 -0
- package/dist/autostart-contract.js +31 -0
- package/dist/autostart-entry.js +10 -0
- package/dist/autostart.js +69 -0
- package/dist/cli.js +11 -11
- package/dist/commands/auto.js +15 -0
- package/dist/commands/lifecycle.js +5 -15
- package/dist/commands/logs.js +11 -4
- package/dist/commands/shared.js +2 -4
- package/dist/commands/status.js +5 -1
- package/dist/commands/update.js +7 -3
- package/dist/commands/web.js +14 -50
- package/dist/contracts.js +168 -348
- package/dist/core-config-validation.js +22 -38
- package/dist/core-update.js +126 -762
- package/dist/core.js +20 -46
- package/dist/daemon/app.js +129 -117
- package/dist/daemon/context.js +13 -7
- package/dist/daemon/entry.js +21 -38
- package/dist/daemon/errors.js +3 -0
- package/dist/daemon/handlers/autostart.js +18 -0
- package/dist/daemon/handlers/core.js +32 -9
- package/dist/daemon/handlers/daemon.js +17 -6
- package/dist/daemon/handlers/profiles.js +12 -2
- package/dist/daemon/handlers/settings.js +9 -36
- package/dist/daemon/router.js +26 -21
- package/dist/daemon/scheduler.js +3 -1
- package/dist/daemon/server.js +5 -2
- package/dist/daemon-client.js +13 -3
- package/dist/daemon-http.js +6 -0
- package/dist/daemon-lifecycle.js +19 -196
- package/dist/github.js +7 -2
- package/dist/http.js +7 -5
- package/dist/mihomo-config.js +2 -1
- package/dist/paths.js +5 -7
- package/dist/profile-model.js +87 -0
- package/dist/profile-service.js +250 -587
- package/dist/profiles.js +43 -171
- package/dist/runtime-lifecycle.js +112 -194
- package/dist/runtime-owner.js +37 -76
- package/dist/sash-client.js +40 -19
- package/dist/settings-service.js +29 -224
- package/dist/settings.js +61 -289
- package/dist/status.js +24 -16
- package/dist/supervisor.js +1 -14
- package/dist/sysproxy/common.js +5 -45
- package/dist/sysproxy/factory.js +24 -65
- package/dist/sysproxy/snapshot.js +24 -257
- package/dist/sysproxy.js +1 -4
- package/dist/system-proxy-manager.js +37 -35
- package/dist/ui/assets/{ConnectionsView-CfaOG2JV.js → ConnectionsView-BgXQM6Z4.js} +1 -1
- package/dist/ui/assets/{LogsView-BDiRATXN.js → LogsView-2f542P8z.js} +1 -1
- package/dist/ui/assets/{PaginationFooter-DTytr1iu.js → PaginationFooter-cj1tcZej.js} +1 -1
- package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
- package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
- package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
- package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
- package/dist/ui/assets/{RulesView-CjEyCN1A.js → RulesView-CtKvlckZ.js} +1 -1
- package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
- package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
- package/dist/ui/assets/index-CBInDvdJ.js +19 -0
- package/dist/ui/assets/index-mOLy7fkB.css +1 -0
- package/dist/ui/assets/{theme-BwDBMsKO.js → theme-D40MF8cQ.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/docs/architecture-proposal.md +109 -0
- package/docs/autostart.md +101 -0
- package/docs/backend.md +92 -224
- package/docs/frontend.md +60 -106
- package/docs/usage.md +92 -164
- package/package.json +3 -3
- package/dist/commands/upgrade.js +0 -43
- package/dist/core-install-transaction.js +0 -114
- package/dist/core-update-coordination.js +0 -105
- package/dist/core-update-service.js +0 -245
- package/dist/managed-state-transaction.js +0 -375
- package/dist/offline-mutation.js +0 -58
- package/dist/profile-migration.js +0 -101
- package/dist/runtime-recovery.js +0 -31
- package/dist/sysproxy/darwin.js +0 -287
- package/dist/sysproxy/gnome.js +0 -181
- package/dist/sysproxy/legacy.js +0 -58
- package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
- package/dist/ui/assets/CodeEditorModal-D1zn-jRS.js +0 -14
- package/dist/ui/assets/ProfileEditorDialog-CyyP6OF1.js +0 -1
- package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
- package/dist/ui/assets/ProfilesView-hFa7-O72.js +0 -2
- package/dist/ui/assets/SettingsFileDialog-CjBSyH4K.js +0 -1
- package/dist/ui/assets/SettingsView-D5QvJId6.css +0 -1
- package/dist/ui/assets/SettingsView-DsRntRfn.js +0 -2
- package/dist/ui/assets/index-CxQTf_P9.css +0 -1
- package/dist/ui/assets/index-_mb4OfYp.js +0 -19
package/dist/settings.js
CHANGED
|
@@ -1,305 +1,29 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import { atomicWriteFileSync } from "./fs-atomic.js";
|
|
4
2
|
import { isPlainObject } from "./json-shape.js";
|
|
5
|
-
|
|
6
|
-
import { acquireStateLockSync } from "./state-lock.js";
|
|
7
|
-
const PUBLIC_SETTINGS_KEYS = [
|
|
8
|
-
"mixedPort",
|
|
9
|
-
"controller",
|
|
10
|
-
"tun",
|
|
11
|
-
"allowLan",
|
|
12
|
-
"daemonPort",
|
|
13
|
-
"systemProxy",
|
|
14
|
-
];
|
|
15
|
-
function exhaustiveKeys() {
|
|
16
|
-
return (keys) => keys;
|
|
17
|
-
}
|
|
18
|
-
const CANONICAL_SETTINGS_KEYS = exhaustiveKeys()([
|
|
19
|
-
"schemaVersion",
|
|
20
|
-
"subscriptionUrl",
|
|
21
|
-
"mixedPort",
|
|
22
|
-
"controller",
|
|
23
|
-
"secret",
|
|
24
|
-
"tun",
|
|
25
|
-
"allowLan",
|
|
26
|
-
"daemonPort",
|
|
27
|
-
"daemonSecret",
|
|
28
|
-
"systemProxy",
|
|
29
|
-
]);
|
|
30
|
-
const LEGACY_SETTINGS_KEYS = ["coreVersion", "uiVersion"];
|
|
31
|
-
const ACCEPTED_SETTINGS_KEYS = new Set([
|
|
32
|
-
...CANONICAL_SETTINGS_KEYS,
|
|
33
|
-
...LEGACY_SETTINGS_KEYS,
|
|
34
|
-
]);
|
|
35
|
-
export function publicSettings(settings) {
|
|
36
|
-
return {
|
|
37
|
-
mixedPort: settings.mixedPort,
|
|
38
|
-
controller: settings.controller,
|
|
39
|
-
tun: settings.tun,
|
|
40
|
-
allowLan: settings.allowLan,
|
|
41
|
-
daemonPort: settings.daemonPort,
|
|
42
|
-
systemProxy: settings.systemProxy,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
export function sameSettings(a, b) {
|
|
46
|
-
return CANONICAL_SETTINGS_KEYS.every((key) => a[key] === b[key]);
|
|
47
|
-
}
|
|
3
|
+
const development = process.env.SASH_DEVELOPMENT === "1";
|
|
48
4
|
export const DEFAULT_SETTINGS = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
controller: "127.0.0.1:9090",
|
|
5
|
+
mixedPort: development ? 18890 : 7890,
|
|
6
|
+
controller: development ? "127.0.0.1:18990" : "127.0.0.1:9090",
|
|
52
7
|
secret: "",
|
|
53
|
-
tun: false,
|
|
54
8
|
allowLan: false,
|
|
55
|
-
daemonPort: 19090,
|
|
9
|
+
daemonPort: development ? 28990 : 19090,
|
|
56
10
|
daemonSecret: "",
|
|
57
11
|
systemProxy: false,
|
|
58
12
|
};
|
|
59
|
-
function
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
function invalidSettings(file, message) {
|
|
63
|
-
return new Error(`Settings are invalid: ${file}: ${message}`);
|
|
64
|
-
}
|
|
65
|
-
function readRequiredField(source, key, defaultValue, allowMissing, file, parse) {
|
|
66
|
-
if (!hasOwn(source, key)) {
|
|
67
|
-
if (!allowMissing)
|
|
68
|
-
throw invalidSettings(file, `${key} is required`);
|
|
69
|
-
return { value: defaultValue, missing: true };
|
|
70
|
-
}
|
|
71
|
-
const value = source[key];
|
|
72
|
-
if (value === null)
|
|
73
|
-
throw invalidSettings(file, `${key} must not be null`);
|
|
74
|
-
return { value: parse(value, key, file), missing: false };
|
|
75
|
-
}
|
|
76
|
-
function readOptionalString(source, key, file) {
|
|
77
|
-
if (!hasOwn(source, key))
|
|
78
|
-
return undefined;
|
|
79
|
-
const value = source[key];
|
|
80
|
-
if (value === null)
|
|
81
|
-
throw invalidSettings(file, `${key} must not be null`);
|
|
82
|
-
return parseString(value, key, file);
|
|
83
|
-
}
|
|
84
|
-
function parseString(value, key, file) {
|
|
85
|
-
if (typeof value !== "string")
|
|
86
|
-
throw invalidSettings(file, `${key} must be a string`);
|
|
87
|
-
return value;
|
|
88
|
-
}
|
|
89
|
-
function hasSecretControlCharacter(secret) {
|
|
90
|
-
for (const char of secret) {
|
|
91
|
-
const code = char.charCodeAt(0);
|
|
92
|
-
if (code <= 31 || code === 127)
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
function parseSecret(value, key, file) {
|
|
98
|
-
const secret = parseString(value, key, file);
|
|
99
|
-
if (!secret.trim())
|
|
100
|
-
throw invalidSettings(file, `${key} must not be blank`);
|
|
101
|
-
if (hasSecretControlCharacter(secret)) {
|
|
102
|
-
throw invalidSettings(file, `${key} must not contain control characters`);
|
|
103
|
-
}
|
|
104
|
-
return secret;
|
|
105
|
-
}
|
|
106
|
-
function parseBoolean(value, key, file) {
|
|
107
|
-
if (typeof value !== "boolean")
|
|
108
|
-
throw invalidSettings(file, `${key} must be a boolean`);
|
|
109
|
-
return value;
|
|
110
|
-
}
|
|
111
|
-
function parsePort(value, key, file) {
|
|
112
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 65_535) {
|
|
113
|
-
throw invalidSettings(file, `${key} must be an integer from 1 to 65535`);
|
|
114
|
-
}
|
|
115
|
-
return value;
|
|
116
|
-
}
|
|
117
|
-
function parseController(value, key, file) {
|
|
118
|
-
const controller = parseString(value, key, file);
|
|
119
|
-
const address = parseControllerAddress(controller);
|
|
120
|
-
if (!address) {
|
|
121
|
-
throw invalidSettings(file, `${key} must be a loopback host:port controller address`);
|
|
122
|
-
}
|
|
123
|
-
return address.canonical;
|
|
124
|
-
}
|
|
125
|
-
function validateSettingsRelations(settings, file) {
|
|
126
|
-
const controller = parseControllerAddress(settings.controller);
|
|
127
|
-
if (!controller) {
|
|
128
|
-
throw invalidSettings(file, "controller must be a loopback host:port controller address");
|
|
129
|
-
}
|
|
130
|
-
const ports = [
|
|
131
|
-
["mixedPort", settings.mixedPort],
|
|
132
|
-
["daemonPort", settings.daemonPort],
|
|
133
|
-
["controller", controller.port],
|
|
134
|
-
];
|
|
135
|
-
for (let i = 0; i < ports.length; i++) {
|
|
136
|
-
for (let j = i + 1; j < ports.length; j++) {
|
|
137
|
-
if (ports[i]?.[1] === ports[j]?.[1]) {
|
|
138
|
-
throw invalidSettings(file, `${ports[i]?.[0]} and ${ports[j]?.[0]} must use different ports`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
function parseSchemaVersion(source, allowMissing, file) {
|
|
144
|
-
if (!hasOwn(source, "schemaVersion")) {
|
|
145
|
-
if (!allowMissing)
|
|
146
|
-
throw invalidSettings(file, "schemaVersion is required");
|
|
147
|
-
return { value: 0, missing: true };
|
|
148
|
-
}
|
|
149
|
-
const value = source.schemaVersion;
|
|
150
|
-
if (value === null)
|
|
151
|
-
throw invalidSettings(file, "schemaVersion must not be null");
|
|
152
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
153
|
-
throw invalidSettings(file, "schemaVersion must be an integer");
|
|
154
|
-
}
|
|
155
|
-
if (value === 0 || value === 1)
|
|
156
|
-
return { value, missing: false };
|
|
157
|
-
if (value > 1) {
|
|
158
|
-
throw invalidSettings(file, `schemaVersion ${value} is from a future version`);
|
|
159
|
-
}
|
|
160
|
-
throw invalidSettings(file, "schemaVersion must be 0 or 1");
|
|
161
|
-
}
|
|
162
|
-
function parseSettings(document, file, allowMissing) {
|
|
163
|
-
if (!isPlainObject(document)) {
|
|
164
|
-
throw invalidSettings(file, "JSON root must be a plain object");
|
|
165
|
-
}
|
|
166
|
-
for (const key of Object.keys(document)) {
|
|
167
|
-
if (!ACCEPTED_SETTINGS_KEYS.has(key)) {
|
|
168
|
-
throw invalidSettings(file, `unknown field ${JSON.stringify(key)}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
const schemaVersion = parseSchemaVersion(document, allowMissing, file);
|
|
172
|
-
const subscriptionUrl = readOptionalString(document, "subscriptionUrl", file);
|
|
173
|
-
const mixedPort = readRequiredField(document, "mixedPort", DEFAULT_SETTINGS.mixedPort, allowMissing, file, parsePort);
|
|
174
|
-
const controller = readRequiredField(document, "controller", DEFAULT_SETTINGS.controller, allowMissing, file, parseController);
|
|
175
|
-
const secret = readRequiredField(document, "secret", DEFAULT_SETTINGS.secret, allowMissing, file, parseSecret);
|
|
176
|
-
const tun = readRequiredField(document, "tun", DEFAULT_SETTINGS.tun, allowMissing, file, parseBoolean);
|
|
177
|
-
const allowLan = readRequiredField(document, "allowLan", DEFAULT_SETTINGS.allowLan, allowMissing, file, parseBoolean);
|
|
178
|
-
if (tun.value && !allowMissing) {
|
|
179
|
-
throw invalidSettings(file, "TUN is unavailable in this release; tun must be false");
|
|
180
|
-
}
|
|
181
|
-
const daemonPort = readRequiredField(document, "daemonPort", DEFAULT_SETTINGS.daemonPort, allowMissing, file, parsePort);
|
|
182
|
-
const daemonSecret = readRequiredField(document, "daemonSecret", DEFAULT_SETTINGS.daemonSecret, allowMissing, file, parseSecret);
|
|
183
|
-
const systemProxy = readRequiredField(document, "systemProxy", DEFAULT_SETTINGS.systemProxy, allowMissing, file, parseBoolean);
|
|
184
|
-
let needsRewrite = schemaVersion.missing ||
|
|
185
|
-
schemaVersion.value === 0 ||
|
|
186
|
-
mixedPort.missing ||
|
|
187
|
-
controller.missing ||
|
|
188
|
-
controller.value !== document.controller ||
|
|
189
|
-
secret.missing ||
|
|
190
|
-
tun.missing ||
|
|
191
|
-
tun.value ||
|
|
192
|
-
allowLan.missing ||
|
|
193
|
-
daemonPort.missing ||
|
|
194
|
-
daemonSecret.missing ||
|
|
195
|
-
systemProxy.missing;
|
|
196
|
-
for (const key of LEGACY_SETTINGS_KEYS) {
|
|
197
|
-
if (!hasOwn(document, key))
|
|
198
|
-
continue;
|
|
199
|
-
const value = document[key];
|
|
200
|
-
if (value === null)
|
|
201
|
-
throw invalidSettings(file, `${key} must not be null`);
|
|
202
|
-
parseString(value, key, file);
|
|
203
|
-
needsRewrite = true;
|
|
204
|
-
}
|
|
205
|
-
const normalizedSecret = secret.missing ? generateSecret() : secret.value;
|
|
206
|
-
const normalizedDaemonSecret = daemonSecret.missing ? generateSecret() : daemonSecret.value;
|
|
207
|
-
const settings = {
|
|
208
|
-
schemaVersion: 1,
|
|
209
|
-
mixedPort: mixedPort.value,
|
|
210
|
-
controller: controller.value,
|
|
211
|
-
secret: normalizedSecret,
|
|
212
|
-
tun: false,
|
|
213
|
-
allowLan: allowLan.value,
|
|
214
|
-
daemonPort: daemonPort.value,
|
|
215
|
-
daemonSecret: normalizedDaemonSecret,
|
|
216
|
-
systemProxy: systemProxy.value,
|
|
217
|
-
};
|
|
218
|
-
if (subscriptionUrl !== undefined)
|
|
219
|
-
settings.subscriptionUrl = subscriptionUrl;
|
|
220
|
-
validateSettingsRelations(settings, file);
|
|
221
|
-
return { settings, needsRewrite };
|
|
13
|
+
export function generateSecret() {
|
|
14
|
+
return crypto.randomBytes(24).toString("hex");
|
|
222
15
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
text = fs.readFileSync(file, "utf8");
|
|
227
|
-
}
|
|
228
|
-
catch (err) {
|
|
229
|
-
if (err.code === "ENOENT") {
|
|
230
|
-
return { exists: false, document: {} };
|
|
231
|
-
}
|
|
232
|
-
throw err;
|
|
233
|
-
}
|
|
234
|
-
try {
|
|
235
|
-
return { exists: true, document: JSON.parse(text) };
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
throw new Error(`Settings file is invalid JSON: ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
239
|
-
}
|
|
16
|
+
export function initialSettings() {
|
|
17
|
+
return { ...DEFAULT_SETTINGS, secret: generateSecret(), daemonSecret: generateSecret() };
|
|
240
18
|
}
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
schemaVersion: 1,
|
|
19
|
+
export function publicSettings(settings) {
|
|
20
|
+
return {
|
|
244
21
|
mixedPort: settings.mixedPort,
|
|
245
22
|
controller: settings.controller,
|
|
246
|
-
secret: settings.secret,
|
|
247
|
-
tun: settings.tun,
|
|
248
23
|
allowLan: settings.allowLan,
|
|
249
24
|
daemonPort: settings.daemonPort,
|
|
250
|
-
daemonSecret: settings.daemonSecret,
|
|
251
25
|
systemProxy: settings.systemProxy,
|
|
252
26
|
};
|
|
253
|
-
if (settings.subscriptionUrl !== undefined)
|
|
254
|
-
canonical.subscriptionUrl = settings.subscriptionUrl;
|
|
255
|
-
return canonical;
|
|
256
|
-
}
|
|
257
|
-
function writeCanonicalSettings(settings, layout) {
|
|
258
|
-
atomicWriteFileSync(layout.settingsFile, `${JSON.stringify(canonicalSettings(settings), null, 2)}\n`);
|
|
259
|
-
}
|
|
260
|
-
function loadSettingsUnlocked(layout) {
|
|
261
|
-
const source = readSettingsFile(layout.settingsFile);
|
|
262
|
-
const parsed = parseSettings(source.document, layout.settingsFile, true);
|
|
263
|
-
if (!source.exists || parsed.needsRewrite) {
|
|
264
|
-
writeCanonicalSettings(parsed.settings, layout);
|
|
265
|
-
}
|
|
266
|
-
return parsed.settings;
|
|
267
|
-
}
|
|
268
|
-
export function generateSecret() {
|
|
269
|
-
return crypto.randomBytes(24).toString("hex");
|
|
270
|
-
}
|
|
271
|
-
export function loadSettings(layout = sashLayout()) {
|
|
272
|
-
const lease = acquireStateLockSync(layout.settingsLockFile, { purpose: "settings" });
|
|
273
|
-
try {
|
|
274
|
-
return loadSettingsUnlocked(layout);
|
|
275
|
-
}
|
|
276
|
-
finally {
|
|
277
|
-
lease.release();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
export function saveSettings(settings, layout = sashLayout()) {
|
|
281
|
-
const lease = acquireStateLockSync(layout.settingsLockFile, { purpose: "settings" });
|
|
282
|
-
try {
|
|
283
|
-
const parsed = parseSettings(settings, layout.settingsFile, false);
|
|
284
|
-
writeCanonicalSettings(parsed.settings, layout);
|
|
285
|
-
return parsed.settings;
|
|
286
|
-
}
|
|
287
|
-
finally {
|
|
288
|
-
lease.release();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
export function parseSettingsText(text, file = "sash.json") {
|
|
292
|
-
let document;
|
|
293
|
-
try {
|
|
294
|
-
document = JSON.parse(text);
|
|
295
|
-
}
|
|
296
|
-
catch (err) {
|
|
297
|
-
throw new Error(`Settings file is invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
298
|
-
}
|
|
299
|
-
return parseSettings(document, file, false).settings;
|
|
300
|
-
}
|
|
301
|
-
export function validateSettingsCandidate(candidate) {
|
|
302
|
-
return parseSettings(candidate, "candidate settings", false).settings;
|
|
303
27
|
}
|
|
304
28
|
export function parseControllerAddress(value) {
|
|
305
29
|
const match = value.trim().match(/^(127\.0\.0\.1|localhost|\[::1\]):(\d+)$/i);
|
|
@@ -307,12 +31,60 @@ export function parseControllerAddress(value) {
|
|
|
307
31
|
const rawPort = match?.[2];
|
|
308
32
|
if (!rawHost || !rawPort)
|
|
309
33
|
return undefined;
|
|
310
|
-
const port = Number
|
|
34
|
+
const port = Number(rawPort);
|
|
311
35
|
if (port < 1 || port > 65_535 || String(port) !== rawPort)
|
|
312
36
|
return undefined;
|
|
313
|
-
if (rawHost === "[::1]")
|
|
37
|
+
if (rawHost === "[::1]")
|
|
314
38
|
return { host: "::1", port, canonical: `[::1]:${port}` };
|
|
315
|
-
}
|
|
316
39
|
const host = rawHost === "localhost" ? "localhost" : "127.0.0.1";
|
|
317
40
|
return { host, port, canonical: `${host}:${port}` };
|
|
318
41
|
}
|
|
42
|
+
export function validateSettingsCandidate(value) {
|
|
43
|
+
if (!isPlainObject(value))
|
|
44
|
+
throw new Error("Settings must be a plain object");
|
|
45
|
+
const keys = Object.keys(DEFAULT_SETTINGS);
|
|
46
|
+
if (Object.keys(value).some((key) => !keys.includes(key))) {
|
|
47
|
+
throw new Error("Settings contain an unknown field");
|
|
48
|
+
}
|
|
49
|
+
for (const key of keys) {
|
|
50
|
+
if (!Object.hasOwn(value, key))
|
|
51
|
+
throw new Error(`Settings field ${key} is required`);
|
|
52
|
+
}
|
|
53
|
+
const port = (key) => {
|
|
54
|
+
const number = value[key];
|
|
55
|
+
if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > 65_535) {
|
|
56
|
+
throw new Error(`${key} must be an integer from 1 to 65535`);
|
|
57
|
+
}
|
|
58
|
+
return number;
|
|
59
|
+
};
|
|
60
|
+
const secret = (key) => {
|
|
61
|
+
const text = value[key];
|
|
62
|
+
if (typeof text !== "string" || !text.trim())
|
|
63
|
+
throw new Error(`${key} must not be blank`);
|
|
64
|
+
if ([...text].some((char) => char.charCodeAt(0) <= 31 || char.charCodeAt(0) === 127)) {
|
|
65
|
+
throw new Error(`${key} must not contain control characters`);
|
|
66
|
+
}
|
|
67
|
+
return text;
|
|
68
|
+
};
|
|
69
|
+
const boolean = (key) => {
|
|
70
|
+
if (typeof value[key] !== "boolean")
|
|
71
|
+
throw new Error(`${key} must be a boolean`);
|
|
72
|
+
return value[key];
|
|
73
|
+
};
|
|
74
|
+
const controller = typeof value.controller === "string" && parseControllerAddress(value.controller);
|
|
75
|
+
if (!controller)
|
|
76
|
+
throw new Error("controller must be a loopback host:port address");
|
|
77
|
+
const result = {
|
|
78
|
+
mixedPort: port("mixedPort"),
|
|
79
|
+
controller: controller.canonical,
|
|
80
|
+
secret: secret("secret"),
|
|
81
|
+
allowLan: boolean("allowLan"),
|
|
82
|
+
daemonPort: port("daemonPort"),
|
|
83
|
+
daemonSecret: secret("daemonSecret"),
|
|
84
|
+
systemProxy: boolean("systemProxy"),
|
|
85
|
+
};
|
|
86
|
+
if (new Set([result.mixedPort, result.daemonPort, controller.port]).size !== 3) {
|
|
87
|
+
throw new Error("mixedPort, daemonPort and controller must use different ports");
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
package/dist/status.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { AutostartService } from "./autostart.js";
|
|
1
2
|
import { currentCoreVersion } from "./core.js";
|
|
2
3
|
import { SashDaemonClient } from "./daemon-client.js";
|
|
3
4
|
import { evaluateDaemon, } from "./daemon-lifecycle.js";
|
|
4
|
-
import {
|
|
5
|
+
import { getActiveProfile, loadProfiles } from "./profiles.js";
|
|
5
6
|
import { SystemProxyManager } from "./system-proxy-manager.js";
|
|
6
7
|
import { uiInstalled } from "./webui.js";
|
|
7
|
-
export const CLI_STATUS_SCHEMA_VERSION =
|
|
8
|
+
export const CLI_STATUS_SCHEMA_VERSION = 2;
|
|
8
9
|
function errorText(err) {
|
|
9
10
|
const message = err instanceof Error ? err.message : String(err);
|
|
10
11
|
return (message
|
|
@@ -106,16 +107,13 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
|
|
|
106
107
|
: currentCoreVersion(context.layout);
|
|
107
108
|
const profile = dependencies.activeProfile
|
|
108
109
|
? dependencies.activeProfile(context)
|
|
109
|
-
:
|
|
110
|
-
layout: context.layout,
|
|
111
|
-
settings: () => context.settings,
|
|
112
|
-
}).active();
|
|
110
|
+
: getActiveProfile(loadProfiles(context.layout));
|
|
113
111
|
let coreRunning = daemonState.running ? null : false;
|
|
114
112
|
let coreHealthy = daemonState.running ? null : false;
|
|
115
113
|
let corePid = null;
|
|
116
114
|
let coreVersion = null;
|
|
117
|
-
let tunActive = daemonState.running ? null : false;
|
|
118
115
|
let desiredProxy = context.settings.systemProxy;
|
|
116
|
+
let mixedEndpoint = `127.0.0.1:${context.settings.mixedPort}`;
|
|
119
117
|
let proxySource;
|
|
120
118
|
let queriedDaemon = false;
|
|
121
119
|
if (daemonState.kind === "healthy") {
|
|
@@ -124,6 +122,11 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
|
|
|
124
122
|
queriedDaemon = true;
|
|
125
123
|
daemon = daemonObservation(daemonState, "healthy");
|
|
126
124
|
desiredProxy = status.systemProxy.desired;
|
|
125
|
+
mixedEndpoint = status.core.running
|
|
126
|
+
? status.configuration.appliedSettings
|
|
127
|
+
? `127.0.0.1:${status.configuration.appliedSettings.mixedPort}`
|
|
128
|
+
: "unknown"
|
|
129
|
+
: `127.0.0.1:${status.settings.mixedPort}`;
|
|
127
130
|
proxySource = {
|
|
128
131
|
applied: status.systemProxy.applied,
|
|
129
132
|
appliedKnown: status.systemProxy.appliedKnown,
|
|
@@ -137,7 +140,6 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
|
|
|
137
140
|
else if (!status.core.running) {
|
|
138
141
|
coreRunning = false;
|
|
139
142
|
coreHealthy = false;
|
|
140
|
-
tunActive = false;
|
|
141
143
|
}
|
|
142
144
|
else {
|
|
143
145
|
coreRunning = true;
|
|
@@ -147,7 +149,6 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
|
|
|
147
149
|
typeof status.core.version === "string" && status.core.version
|
|
148
150
|
? status.core.version
|
|
149
151
|
: null;
|
|
150
|
-
tunActive = typeof status.core.tunActive === "boolean" ? status.core.tunActive : null;
|
|
151
152
|
if (coreHealthy === null)
|
|
152
153
|
addError(errors, "Core health is unavailable");
|
|
153
154
|
else if (!coreHealthy)
|
|
@@ -156,8 +157,6 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
|
|
|
156
157
|
addError(errors, "Core PID is unavailable");
|
|
157
158
|
if (coreVersion === null)
|
|
158
159
|
addError(errors, "Core runtime version is unavailable");
|
|
159
|
-
if (tunActive === null)
|
|
160
|
-
addError(errors, "Core TUN state is unavailable");
|
|
161
160
|
}
|
|
162
161
|
}
|
|
163
162
|
catch (err) {
|
|
@@ -179,11 +178,24 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
|
|
|
179
178
|
: null;
|
|
180
179
|
const activeProfile = profile ? { id: profile.id, name: profile.name, url: profile.url } : null;
|
|
181
180
|
const daemonPort = daemon.port || context.settings.daemonPort;
|
|
181
|
+
let autostart;
|
|
182
|
+
try {
|
|
183
|
+
autostart = await (dependencies.inspectAutostart
|
|
184
|
+
? dependencies.inspectAutostart(context)
|
|
185
|
+
: new AutostartService({ layout: context.layout }).inspect());
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
autostart = { state: "unknown", canEnable: false, reason: errorText(error) };
|
|
189
|
+
}
|
|
190
|
+
if (autostart.state === "unknown") {
|
|
191
|
+
addError(errors, `Autostart query failed: ${autostart.reason ?? "unknown error"}`);
|
|
192
|
+
}
|
|
182
193
|
return {
|
|
183
194
|
schemaVersion: CLI_STATUS_SCHEMA_VERSION,
|
|
184
195
|
complete: errors.length === 0,
|
|
185
196
|
healthy,
|
|
186
197
|
queryError: errors.length > 0 ? errors.join("; ") : null,
|
|
198
|
+
autostart,
|
|
187
199
|
daemon: { ...daemon, port: daemonPort },
|
|
188
200
|
core: {
|
|
189
201
|
running: coreRunning,
|
|
@@ -199,16 +211,12 @@ export async function collectRuntimeStatus(context, dependencies = {}) {
|
|
|
199
211
|
},
|
|
200
212
|
uiInstalled: dependencies.hasUi ? dependencies.hasUi(context) : uiInstalled(context.layout),
|
|
201
213
|
endpoints: {
|
|
202
|
-
mixedProxy:
|
|
214
|
+
mixedProxy: mixedEndpoint,
|
|
203
215
|
controller: context.settings.controller,
|
|
204
216
|
daemonApi: `http://127.0.0.1:${daemonPort}`,
|
|
205
217
|
dashboard: `http://127.0.0.1:${daemonPort}/ui/`,
|
|
206
218
|
},
|
|
207
219
|
activeProfile,
|
|
208
|
-
tun: {
|
|
209
|
-
desired: context.settings.tun,
|
|
210
|
-
active: tunActive,
|
|
211
|
-
},
|
|
212
220
|
paths: {
|
|
213
221
|
root: context.layout.root,
|
|
214
222
|
config: context.layout.configFile,
|
package/dist/supervisor.js
CHANGED
|
@@ -79,14 +79,6 @@ export class CoreSupervisor {
|
|
|
79
79
|
}
|
|
80
80
|
return terminated;
|
|
81
81
|
}
|
|
82
|
-
async probeTunActive(api) {
|
|
83
|
-
try {
|
|
84
|
-
return await api.getTunActive();
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
82
|
async start() {
|
|
91
83
|
if (this.child && this.isAlive(this.child.pid ?? -1)) {
|
|
92
84
|
throw new Error(`Core is already running (PID=${this.child.pid})`);
|
|
@@ -166,12 +158,10 @@ export class CoreSupervisor {
|
|
|
166
158
|
}
|
|
167
159
|
healthyProbes++;
|
|
168
160
|
if (healthyProbes >= 2 && this.isAlive(pid)) {
|
|
169
|
-
const tunActive = await this.probeTunActive(api);
|
|
170
161
|
if (this.isAlive(pid)) {
|
|
171
162
|
return {
|
|
172
163
|
pid,
|
|
173
164
|
version,
|
|
174
|
-
...(tunActive !== undefined ? { tunActive } : {}),
|
|
175
165
|
};
|
|
176
166
|
}
|
|
177
167
|
}
|
|
@@ -241,13 +231,11 @@ export class CoreSupervisor {
|
|
|
241
231
|
const api = new MihomoApi(settings.controller, settings.secret);
|
|
242
232
|
let healthy = false;
|
|
243
233
|
let version;
|
|
244
|
-
let tunActive;
|
|
245
234
|
try {
|
|
246
|
-
version = await api.version();
|
|
235
|
+
version = await api.version({ deadlineMs: 1000, attempts: 1 });
|
|
247
236
|
healthy = true;
|
|
248
237
|
if (!this.ownsCore(ownership))
|
|
249
238
|
return { running: false };
|
|
250
|
-
tunActive = await this.probeTunActive(api);
|
|
251
239
|
}
|
|
252
240
|
catch {
|
|
253
241
|
healthy = false;
|
|
@@ -260,7 +248,6 @@ export class CoreSupervisor {
|
|
|
260
248
|
startedAt: this.childStartedAt,
|
|
261
249
|
healthy,
|
|
262
250
|
version,
|
|
263
|
-
...(tunActive !== undefined ? { tunActive } : {}),
|
|
264
251
|
};
|
|
265
252
|
}
|
|
266
253
|
isRunning() {
|
package/dist/sysproxy/common.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { errorMessage } from "../error-utils.js";
|
|
2
2
|
import { isPlainObject } from "../json-shape.js";
|
|
3
|
-
import {
|
|
3
|
+
import { runSanitizedCommandAsync, windowsSystemExecutable } from "../process.js";
|
|
4
4
|
import { DEFAULT_BYPASS_LIST } from "./types.js";
|
|
5
5
|
const MAX_PROXY_STRING_LENGTH = 4096;
|
|
6
|
-
const MAX_SERVICE_NAME_LENGTH = 512;
|
|
7
6
|
export { errorMessage, isPlainObject };
|
|
8
7
|
export function parseProxyString(value, label, allowEmpty = true) {
|
|
9
8
|
if (typeof value !== "string") {
|
|
@@ -20,19 +19,6 @@ export function parseProxyString(value, label, allowEmpty = true) {
|
|
|
20
19
|
}
|
|
21
20
|
return value;
|
|
22
21
|
}
|
|
23
|
-
export function parseServiceName(value, label) {
|
|
24
|
-
const service = parseProxyString(value, label, false);
|
|
25
|
-
if (service.length > MAX_SERVICE_NAME_LENGTH) {
|
|
26
|
-
throw new Error(`Invalid system proxy snapshot: ${label} has an invalid length`);
|
|
27
|
-
}
|
|
28
|
-
return service;
|
|
29
|
-
}
|
|
30
|
-
export function parseSnapshotPort(value, label) {
|
|
31
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 65_535) {
|
|
32
|
-
throw new Error(`Invalid system proxy snapshot: ${label} must be an integer from 0 to 65535`);
|
|
33
|
-
}
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
22
|
export function parseProxyEnable(value) {
|
|
37
23
|
if (value === null)
|
|
38
24
|
return null;
|
|
@@ -41,37 +27,8 @@ export function parseProxyEnable(value) {
|
|
|
41
27
|
}
|
|
42
28
|
return value;
|
|
43
29
|
}
|
|
44
|
-
export function compareStrings(a, b) {
|
|
45
|
-
if (a < b)
|
|
46
|
-
return -1;
|
|
47
|
-
if (a > b)
|
|
48
|
-
return 1;
|
|
49
|
-
return 0;
|
|
50
|
-
}
|
|
51
|
-
export function isSystemProxySupported(platform = process.platform) {
|
|
52
|
-
if (platform === "win32" || platform === "darwin")
|
|
53
|
-
return true;
|
|
54
|
-
if (platform !== "linux")
|
|
55
|
-
return false;
|
|
56
|
-
return findExecutableOnPath("gsettings") !== undefined;
|
|
57
|
-
}
|
|
58
|
-
function resolveSystemProxyCommand(command) {
|
|
59
|
-
if (process.platform === "win32" && command.toLowerCase() === "reg.exe") {
|
|
60
|
-
return windowsSystemExecutable("reg.exe");
|
|
61
|
-
}
|
|
62
|
-
if (process.platform === "darwin" && command === "networksetup") {
|
|
63
|
-
return "/usr/sbin/networksetup";
|
|
64
|
-
}
|
|
65
|
-
if (process.platform === "linux" && command === "gsettings") {
|
|
66
|
-
const resolved = findExecutableOnPath(command);
|
|
67
|
-
if (!resolved)
|
|
68
|
-
throw new Error("gsettings is not available on this system");
|
|
69
|
-
return resolved;
|
|
70
|
-
}
|
|
71
|
-
return command;
|
|
72
|
-
}
|
|
73
30
|
export function runCmd(cmd, args, timeoutMs = 5000) {
|
|
74
|
-
return runSanitizedCommandAsync(
|
|
31
|
+
return runSanitizedCommandAsync(cmd.toLowerCase() === "reg.exe" ? windowsSystemExecutable("reg.exe") : cmd, args, { timeoutMs });
|
|
75
32
|
}
|
|
76
33
|
export function normalizeEnableOptions(opts) {
|
|
77
34
|
if (!isPlainObject(opts)) {
|
|
@@ -97,3 +54,6 @@ export function normalizeEnableOptions(opts) {
|
|
|
97
54
|
export function formatHostPort(host, port) {
|
|
98
55
|
return host.includes(":") && !host.startsWith("[") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
99
56
|
}
|
|
57
|
+
export function isSystemProxySupported(platform = process.platform) {
|
|
58
|
+
return platform === "win32";
|
|
59
|
+
}
|