dsh-mobile 0.1.0-alpha.2 → 0.1.0-alpha.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.en.md +123 -0
- package/README.md +86 -116
- package/SECURITY.md +5 -3
- package/assets/brand/app-icon-master.png +0 -0
- package/assets/brand/repository-hero.png +0 -0
- package/cordis.patch.yml +10 -0
- package/lib/cli.js +317 -80
- package/lib/client.js +623 -206
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +31 -1
- package/lib/index.mjs +716 -23
- package/lib/mobile-layout.js +227 -0
- package/lib/mobile-layout.js.map +1 -0
- package/package.json +19 -13
- package/README.zh.md +0 -206
package/lib/cli.js
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir, networkInterfaces } from "node:os";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { X509Certificate, createPrivateKey, createPublicKey } from "node:crypto";
|
|
6
8
|
import { generate } from "selfsigned";
|
|
7
|
-
//#region src/
|
|
9
|
+
//#region src/managed-setup.ts
|
|
10
|
+
const execFile$2 = promisify(execFile);
|
|
11
|
+
const VIRTUAL_INTERFACE_MARKERS = [
|
|
12
|
+
"bridge",
|
|
13
|
+
"docker",
|
|
14
|
+
"hyper-v",
|
|
15
|
+
"mihomo",
|
|
16
|
+
"radmin",
|
|
17
|
+
"tailscale",
|
|
18
|
+
"tap",
|
|
19
|
+
"tun",
|
|
20
|
+
"utun",
|
|
21
|
+
"vbox",
|
|
22
|
+
"veth",
|
|
23
|
+
"virtual",
|
|
24
|
+
"vmware",
|
|
25
|
+
"vpn",
|
|
26
|
+
"vethernet",
|
|
27
|
+
"wsl",
|
|
28
|
+
"zerotier"
|
|
29
|
+
];
|
|
8
30
|
function privateIpv4(value) {
|
|
9
31
|
const parts = value.split(".").map(Number);
|
|
10
32
|
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) && (parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168);
|
|
@@ -19,93 +41,201 @@ function networkCidr(address, cidr) {
|
|
|
19
41
|
0
|
|
20
42
|
].map((shift) => network >>> shift & 255).join(".")}/${String(prefix)}`;
|
|
21
43
|
}
|
|
22
|
-
|
|
23
|
-
|
|
44
|
+
/** List current private IPv4 candidates with their interface identity. */
|
|
45
|
+
function availableLanNetworks(table = networkInterfaces()) {
|
|
46
|
+
const candidates = Object.entries(table).flatMap(([name, entries]) => (entries ?? []).filter((entry) => entry.family === "IPv4" && !entry.internal && privateIpv4(entry.address) && entry.cidr !== null).map((entry) => ({
|
|
47
|
+
name,
|
|
24
48
|
address: entry.address,
|
|
25
49
|
cidr: networkCidr(entry.address, entry.cidr)
|
|
26
|
-
}));
|
|
27
|
-
return [...new Map(candidates.map((entry) => [entry.address
|
|
50
|
+
})));
|
|
51
|
+
return [...new Map(candidates.map((entry) => [`${entry.name}\0${entry.address}`, entry])).values()];
|
|
28
52
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
function likelyVirtualInterface(name) {
|
|
54
|
+
const normalized = name.toLowerCase().replaceAll(/[^a-z0-9]+/gu, " ");
|
|
55
|
+
return VIRTUAL_INTERFACE_MARKERS.some((marker) => normalized.includes(marker.replaceAll("-", " "))) || /^(?:br|wg)\d*\b/u.test(normalized);
|
|
56
|
+
}
|
|
57
|
+
async function runRouteCommand(file, args) {
|
|
58
|
+
return (await execFile$2(file, [...args], {
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
windowsHide: true
|
|
61
|
+
})).stdout;
|
|
62
|
+
}
|
|
63
|
+
function uniqueLines(output) {
|
|
64
|
+
return [...new Set(output.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean))];
|
|
65
|
+
}
|
|
66
|
+
/** Return operating-system default-route interfaces in routing preference order. */
|
|
67
|
+
async function preferredLanInterfaceNames(platform = process.platform, run = runRouteCommand) {
|
|
68
|
+
try {
|
|
69
|
+
if (platform === "win32") return uniqueLines(await run("powershell.exe", [
|
|
70
|
+
"-NoProfile",
|
|
71
|
+
"-NonInteractive",
|
|
72
|
+
"-Command",
|
|
73
|
+
[
|
|
74
|
+
"$routes = Get-NetRoute -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop",
|
|
75
|
+
"$ranked = $routes | Where-Object { $_.State -eq 'Alive' -and $_.NextHop -ne '0.0.0.0' } | ForEach-Object {",
|
|
76
|
+
" $route = $_",
|
|
77
|
+
" $adapter = Get-NetAdapter -InterfaceIndex $route.InterfaceIndex -ErrorAction SilentlyContinue",
|
|
78
|
+
" $ip = Get-NetIPInterface -AddressFamily IPv4 -InterfaceIndex $route.InterfaceIndex -ErrorAction SilentlyContinue",
|
|
79
|
+
" if ($adapter -and $ip -and $adapter.Status -eq 'Up' -and $adapter.HardwareInterface -eq $true -and $adapter.Virtual -ne $true) {",
|
|
80
|
+
" [pscustomobject]@{ Name = $route.InterfaceAlias; Metric = [int]$route.RouteMetric + [int]$ip.InterfaceMetric }",
|
|
81
|
+
" }",
|
|
82
|
+
"}",
|
|
83
|
+
"$ranked | Sort-Object Metric | Select-Object -ExpandProperty Name -Unique"
|
|
84
|
+
].join("; ")
|
|
85
|
+
]));
|
|
86
|
+
if (platform === "linux") {
|
|
87
|
+
const routes = uniqueLines(await run("ip", [
|
|
88
|
+
"-o",
|
|
89
|
+
"-4",
|
|
90
|
+
"route",
|
|
91
|
+
"show",
|
|
92
|
+
"default"
|
|
93
|
+
])).map((line) => ({
|
|
94
|
+
name: /(?:^|\s)dev\s+(\S+)/u.exec(line)?.[1],
|
|
95
|
+
metric: Number(/(?:^|\s)metric\s+(\d+)/u.exec(line)?.[1] ?? 0)
|
|
96
|
+
})).filter((route) => route.name !== void 0 && !likelyVirtualInterface(route.name)).sort((left, right) => left.metric - right.metric);
|
|
97
|
+
return [...new Set(routes.map((route) => route.name))];
|
|
45
98
|
}
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
99
|
+
if (platform === "darwin") {
|
|
100
|
+
const name = /^\s*interface:\s*(\S+)\s*$/mu.exec(await run("route", [
|
|
101
|
+
"-n",
|
|
102
|
+
"get",
|
|
103
|
+
"default"
|
|
104
|
+
]))?.[1];
|
|
105
|
+
return name === void 0 || likelyVirtualInterface(name) ? [] : [name];
|
|
50
106
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!Number.isSafeInteger(port) || port < 1024 || port > 65535) throw new Error("--port must be from 1024 through 65535");
|
|
54
|
-
if (!Number.isSafeInteger(dshPort) || dshPort < 1024 || dshPort > 65535) throw new Error("--dsh-port must be from 1024 through 65535");
|
|
55
|
-
if (address !== void 0 && !privateIpv4(address)) throw new Error("--address must be a private IPv4 address");
|
|
56
|
-
return {
|
|
57
|
-
...address === void 0 ? {} : { address },
|
|
58
|
-
port,
|
|
59
|
-
dshPort
|
|
60
|
-
};
|
|
107
|
+
} catch {}
|
|
108
|
+
return [];
|
|
61
109
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
110
|
+
/** Select an active LAN, optionally by address or by a previously saved interface name. */
|
|
111
|
+
function selectLanNetwork(requestedAddress, requestedInterface, table, preferredInterfaces = []) {
|
|
112
|
+
const candidates = availableLanNetworks(table);
|
|
113
|
+
if (requestedAddress !== void 0) {
|
|
114
|
+
const match = candidates.find((candidate) => candidate.address === requestedAddress);
|
|
115
|
+
if (match === void 0) throw new Error(`--address ${requestedAddress} is not an active private LAN address`);
|
|
67
116
|
return match;
|
|
68
117
|
}
|
|
118
|
+
if (requestedInterface !== void 0) {
|
|
119
|
+
const matches = candidates.filter((candidate) => candidate.name === requestedInterface);
|
|
120
|
+
if (matches.length === 1) return matches[0];
|
|
121
|
+
if (matches.length === 0) throw new Error(`saved LAN interface ${JSON.stringify(requestedInterface)} is not connected`);
|
|
122
|
+
throw new Error(`saved LAN interface ${JSON.stringify(requestedInterface)} has more than one private IPv4 address`);
|
|
123
|
+
}
|
|
69
124
|
if (candidates.length === 1) return candidates[0];
|
|
70
125
|
if (candidates.length === 0) throw new Error("no active private LAN address was found; connect to Wi-Fi or Ethernet");
|
|
71
|
-
|
|
126
|
+
for (const name of preferredInterfaces) {
|
|
127
|
+
const matches = candidates.filter((candidate) => candidate.name === name);
|
|
128
|
+
if (matches.length === 1) return matches[0];
|
|
129
|
+
}
|
|
130
|
+
const physicalCandidates = candidates.filter((candidate) => !likelyVirtualInterface(candidate.name));
|
|
131
|
+
if (physicalCandidates.length === 1) return physicalCandidates[0];
|
|
132
|
+
throw new Error(`more than one LAN address is active; rerun with --address and one of: ${candidates.map((entry) => `${entry.name}=${entry.address}`).join(", ")}`);
|
|
72
133
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
134
|
+
function assertMatchingCa(certPem, keyPem) {
|
|
135
|
+
const certificate = new X509Certificate(certPem);
|
|
136
|
+
if (!certificate.ca || certificate.subject !== certificate.issuer || !certificate.verify(certificate.publicKey)) throw new Error("managed TLS CA must be a self-signed CA certificate");
|
|
137
|
+
const privatePublic = createPublicKey(createPrivateKey(keyPem)).export({
|
|
138
|
+
format: "der",
|
|
139
|
+
type: "spki"
|
|
140
|
+
});
|
|
141
|
+
const certificatePublic = certificate.publicKey.export({
|
|
142
|
+
format: "der",
|
|
143
|
+
type: "spki"
|
|
144
|
+
});
|
|
145
|
+
if (!privatePublic.equals(certificatePublic)) throw new Error("managed TLS CA certificate and key do not match");
|
|
146
|
+
if (Date.parse(certificate.validFrom) > Date.now() || Date.parse(certificate.validTo) <= Date.now()) throw new Error("managed TLS CA certificate is not currently valid");
|
|
147
|
+
return certificate;
|
|
75
148
|
}
|
|
76
|
-
async function
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const home = dshHome();
|
|
80
|
-
const directory = join(home, "mobile-access");
|
|
81
|
-
const tls = join(directory, "tls");
|
|
82
|
-
await mkdir(tls, {
|
|
149
|
+
async function atomicWrite(file, contents) {
|
|
150
|
+
const directory = dirname(file);
|
|
151
|
+
await mkdir(directory, {
|
|
83
152
|
recursive: true,
|
|
84
153
|
mode: 448
|
|
85
154
|
});
|
|
155
|
+
const temporary = join(directory, `.${basename(file)}.${process.pid}.tmp`);
|
|
156
|
+
await writeFile(temporary, contents, { mode: 384 });
|
|
157
|
+
await chmod(temporary, 384);
|
|
158
|
+
await rename(temporary, file);
|
|
159
|
+
}
|
|
160
|
+
/** Create a long-lived CA or migrate the legacy self-signed server certificate as that CA. */
|
|
161
|
+
async function ensureManagedCa(setup, legacy) {
|
|
162
|
+
let certPem;
|
|
163
|
+
let keyPem;
|
|
164
|
+
try {
|
|
165
|
+
[certPem, keyPem] = await Promise.all([readFile(setup.caCertFile, "utf8"), readFile(setup.caKeyFile, "utf8")]);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error.code !== "ENOENT") throw error;
|
|
168
|
+
let migrated = false;
|
|
169
|
+
if (legacy !== void 0) try {
|
|
170
|
+
[certPem, keyPem] = await Promise.all([readFile(legacy.certFile, "utf8"), readFile(legacy.keyFile, "utf8")]);
|
|
171
|
+
assertMatchingCa(certPem, keyPem);
|
|
172
|
+
migrated = true;
|
|
173
|
+
} catch (legacyError) {
|
|
174
|
+
if (legacyError.code !== "ENOENT") throw legacyError;
|
|
175
|
+
}
|
|
176
|
+
if (!migrated) {
|
|
177
|
+
const now = /* @__PURE__ */ new Date();
|
|
178
|
+
const notAfter = new Date(now);
|
|
179
|
+
notAfter.setFullYear(notAfter.getFullYear() + 5);
|
|
180
|
+
const generated = await generate([{
|
|
181
|
+
name: "commonName",
|
|
182
|
+
value: "DeepSeek Harness Mobile CA"
|
|
183
|
+
}], {
|
|
184
|
+
keyType: "ec",
|
|
185
|
+
curve: "P-256",
|
|
186
|
+
algorithm: "sha256",
|
|
187
|
+
notBeforeDate: /* @__PURE__ */ new Date(now.getTime() - 3e5),
|
|
188
|
+
notAfterDate: notAfter,
|
|
189
|
+
extensions: [{
|
|
190
|
+
name: "basicConstraints",
|
|
191
|
+
cA: true,
|
|
192
|
+
critical: true
|
|
193
|
+
}, {
|
|
194
|
+
name: "keyUsage",
|
|
195
|
+
digitalSignature: true,
|
|
196
|
+
keyCertSign: true,
|
|
197
|
+
cRLSign: true,
|
|
198
|
+
critical: true
|
|
199
|
+
}]
|
|
200
|
+
});
|
|
201
|
+
certPem = generated.cert;
|
|
202
|
+
keyPem = generated.private;
|
|
203
|
+
}
|
|
204
|
+
if (certPem === void 0 || keyPem === void 0) throw new Error("managed TLS CA creation did not produce key material");
|
|
205
|
+
await Promise.all([atomicWrite(setup.caCertFile, certPem), atomicWrite(setup.caKeyFile, keyPem)]);
|
|
206
|
+
}
|
|
207
|
+
if (certPem === void 0 || keyPem === void 0) throw new Error("managed TLS CA creation did not produce key material");
|
|
208
|
+
return assertMatchingCa(certPem, keyPem);
|
|
209
|
+
}
|
|
210
|
+
/** Sign and atomically install a server leaf for the interface's current address. */
|
|
211
|
+
async function refreshManagedServerCertificate(setup, address) {
|
|
212
|
+
const [caCert, caKey] = await Promise.all([readFile(setup.tls.caCertFile, "utf8"), readFile(setup.tls.caKeyFile, "utf8")]);
|
|
213
|
+
assertMatchingCa(caCert, caKey);
|
|
86
214
|
const now = /* @__PURE__ */ new Date();
|
|
87
215
|
const notAfter = new Date(now);
|
|
88
216
|
notAfter.setDate(notAfter.getDate() + 397);
|
|
89
|
-
const
|
|
217
|
+
const server = await generate([{
|
|
90
218
|
name: "commonName",
|
|
91
|
-
value:
|
|
219
|
+
value: "DeepSeek Harness Mobile"
|
|
92
220
|
}], {
|
|
93
221
|
keyType: "ec",
|
|
94
222
|
curve: "P-256",
|
|
95
223
|
algorithm: "sha256",
|
|
96
224
|
notBeforeDate: /* @__PURE__ */ new Date(now.getTime() - 3e5),
|
|
97
225
|
notAfterDate: notAfter,
|
|
226
|
+
ca: {
|
|
227
|
+
cert: caCert,
|
|
228
|
+
key: caKey
|
|
229
|
+
},
|
|
98
230
|
extensions: [
|
|
99
231
|
{
|
|
100
232
|
name: "basicConstraints",
|
|
101
|
-
cA:
|
|
233
|
+
cA: false,
|
|
102
234
|
critical: true
|
|
103
235
|
},
|
|
104
236
|
{
|
|
105
237
|
name: "keyUsage",
|
|
106
238
|
digitalSignature: true,
|
|
107
|
-
keyCertSign: true,
|
|
108
|
-
cRLSign: true,
|
|
109
239
|
critical: true
|
|
110
240
|
},
|
|
111
241
|
{
|
|
@@ -116,24 +246,138 @@ async function setup(args) {
|
|
|
116
246
|
name: "subjectAltName",
|
|
117
247
|
altNames: [{
|
|
118
248
|
type: 7,
|
|
119
|
-
ip:
|
|
249
|
+
ip: address
|
|
120
250
|
}]
|
|
121
251
|
}
|
|
122
252
|
]
|
|
123
253
|
});
|
|
124
|
-
|
|
125
|
-
|
|
254
|
+
await Promise.all([atomicWrite(setup.tls.certFile, server.cert), atomicWrite(setup.tls.keyFile, server.private)]);
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/cli.ts
|
|
258
|
+
const execFile$1 = promisify(execFile);
|
|
259
|
+
const FIREWALL_TCP_RULE = "DSH Mobile HTTPS";
|
|
260
|
+
const FIREWALL_UDP_RULE = "DSH Mobile Discovery";
|
|
261
|
+
function parseOptions(args) {
|
|
262
|
+
let address;
|
|
263
|
+
let port = 3443;
|
|
264
|
+
let dshPort = 3080;
|
|
265
|
+
let configureFirewall = true;
|
|
266
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
267
|
+
const name = args[index];
|
|
268
|
+
const value = args[index + 1];
|
|
269
|
+
if (name === "--address" && value !== void 0) {
|
|
270
|
+
address = value;
|
|
271
|
+
index += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (name === "--port" && value !== void 0) {
|
|
275
|
+
port = Number(value);
|
|
276
|
+
index += 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (name === "--dsh-port" && value !== void 0) {
|
|
280
|
+
dshPort = Number(value);
|
|
281
|
+
index += 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (name === "--no-firewall") {
|
|
285
|
+
configureFirewall = false;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
throw new Error(`unknown setup option: ${name ?? ""}`);
|
|
289
|
+
}
|
|
290
|
+
if (!Number.isSafeInteger(port) || port < 1024 || port > 65535) throw new Error("--port must be from 1024 through 65535");
|
|
291
|
+
if (!Number.isSafeInteger(dshPort) || dshPort < 1024 || dshPort > 65535) throw new Error("--dsh-port must be from 1024 through 65535");
|
|
292
|
+
return {
|
|
293
|
+
...address === void 0 ? {} : { address },
|
|
294
|
+
port,
|
|
295
|
+
dshPort,
|
|
296
|
+
configureFirewall
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function dshHome() {
|
|
300
|
+
return resolve(process.env.DSH_HOME ?? join(homedir(), ".dsh"));
|
|
301
|
+
}
|
|
302
|
+
async function runElevatedPowerShell(script) {
|
|
303
|
+
const launch = [
|
|
304
|
+
"$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
|
|
305
|
+
` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(script, "utf16le").toString("base64")}')`,
|
|
306
|
+
"; exit $process.ExitCode"
|
|
307
|
+
].join(" ");
|
|
308
|
+
await execFile$1("powershell.exe", [
|
|
309
|
+
"-NoProfile",
|
|
310
|
+
"-NonInteractive",
|
|
311
|
+
"-Command",
|
|
312
|
+
launch
|
|
313
|
+
], { windowsHide: true });
|
|
314
|
+
}
|
|
315
|
+
async function configureWindowsFirewall(port) {
|
|
316
|
+
if (process.platform !== "win32") return;
|
|
317
|
+
const script = [
|
|
318
|
+
"$ErrorActionPreference = 'Stop'",
|
|
319
|
+
`Get-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
|
|
320
|
+
`Get-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
|
|
321
|
+
`New-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`,
|
|
322
|
+
`New-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -Direction Inbound -Action Allow -Protocol UDP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`
|
|
323
|
+
].join("; ");
|
|
324
|
+
console.log("Windows will request administrator approval for two LAN-only firewall rules.");
|
|
325
|
+
await runElevatedPowerShell(script);
|
|
326
|
+
}
|
|
327
|
+
async function removeWindowsFirewall() {
|
|
328
|
+
if (process.platform !== "win32") return;
|
|
329
|
+
await runElevatedPowerShell([
|
|
330
|
+
"$ErrorActionPreference = 'Stop'",
|
|
331
|
+
`Get-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
|
|
332
|
+
`Get-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`
|
|
333
|
+
].join("; "));
|
|
334
|
+
}
|
|
335
|
+
async function setup(args) {
|
|
336
|
+
const options = parseOptions(args);
|
|
337
|
+
const preferredInterfaces = options.address === void 0 ? await preferredLanInterfaceNames() : [];
|
|
338
|
+
const network = selectLanNetwork(options.address, void 0, void 0, preferredInterfaces);
|
|
339
|
+
const home = dshHome();
|
|
340
|
+
const directory = join(home, "mobile-access");
|
|
341
|
+
const tls = join(directory, "tls");
|
|
342
|
+
await mkdir(tls, {
|
|
343
|
+
recursive: true,
|
|
344
|
+
mode: 448
|
|
345
|
+
});
|
|
346
|
+
const legacyCertFile = join(tls, "cert.pem");
|
|
347
|
+
const legacyKeyFile = join(tls, "key.pem");
|
|
348
|
+
const certFile = join(tls, "server-cert.pem");
|
|
349
|
+
const keyFile = join(tls, "server-key.pem");
|
|
350
|
+
const caCertFile = join(tls, "ca.pem");
|
|
351
|
+
const caKeyFile = join(tls, "ca-key.pem");
|
|
126
352
|
const androidCertificate = join(tls, "dsh-mobile-ca.cer");
|
|
353
|
+
const managedTls = {
|
|
354
|
+
mode: "managed",
|
|
355
|
+
caCertFile,
|
|
356
|
+
caKeyFile,
|
|
357
|
+
certFile,
|
|
358
|
+
keyFile
|
|
359
|
+
};
|
|
360
|
+
const ca = await ensureManagedCa(managedTls, {
|
|
361
|
+
certFile: legacyCertFile,
|
|
362
|
+
keyFile: legacyKeyFile
|
|
363
|
+
});
|
|
364
|
+
const managedSetup = {
|
|
365
|
+
version: 2,
|
|
366
|
+
networkInterface: network.name,
|
|
367
|
+
listenPort: options.port,
|
|
368
|
+
upstreamOrigin: `http://127.0.0.1:${String(options.dshPort)}`,
|
|
369
|
+
tls: managedTls
|
|
370
|
+
};
|
|
371
|
+
await refreshManagedServerCertificate(managedSetup, network.address);
|
|
372
|
+
await writeFile(androidCertificate, ca.raw, { mode: 384 });
|
|
127
373
|
await Promise.all([
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
writeFile(androidCertificate, new X509Certificate(certificate.cert).raw, { mode: 384 })
|
|
131
|
-
]);
|
|
132
|
-
await Promise.all([
|
|
374
|
+
chmod(caCertFile, 384),
|
|
375
|
+
chmod(caKeyFile, 384),
|
|
133
376
|
chmod(certFile, 384),
|
|
134
377
|
chmod(keyFile, 384),
|
|
135
378
|
chmod(androidCertificate, 384)
|
|
136
379
|
]);
|
|
380
|
+
if (options.configureFirewall) await configureWindowsFirewall(options.port);
|
|
137
381
|
const customCss = join(directory, "mobile.css");
|
|
138
382
|
try {
|
|
139
383
|
await readFile(customCss);
|
|
@@ -165,22 +409,14 @@ async function setup(args) {
|
|
|
165
409
|
}
|
|
166
410
|
const origin = `https://${network.address}:${String(options.port)}`;
|
|
167
411
|
await Promise.all([writeFile(join(directory, "setup.json"), `${JSON.stringify({
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
listenHost: network.address,
|
|
171
|
-
upstreamOrigin: `http://127.0.0.1:${String(options.dshPort)}`,
|
|
172
|
-
allowedCidrs: [network.cidr],
|
|
173
|
-
tls: {
|
|
174
|
-
mode: "provided",
|
|
175
|
-
certFile: certFile.replaceAll("\\", "/"),
|
|
176
|
-
keyFile: keyFile.replaceAll("\\", "/")
|
|
177
|
-
}
|
|
412
|
+
...managedSetup,
|
|
413
|
+
tls: Object.fromEntries(Object.entries(managedSetup.tls).map(([key, value]) => [key, typeof value === "string" ? value.replaceAll("\\", "/") : value]))
|
|
178
414
|
}, null, 2)}\n`, { mode: 384 }), writeFile(join(directory, "control.json"), "{\"version\":1,\"enabled\":true}\n", { mode: 384 })]);
|
|
179
|
-
console.log(`DSH Mobile is configured for ${origin}`);
|
|
180
|
-
console.log(`Install this certificate on Android
|
|
415
|
+
console.log(`DSH Mobile follows ${network.name} and is currently configured for ${origin}`);
|
|
416
|
+
console.log(`Install this CA certificate on Android once: ${androidCertificate}`);
|
|
181
417
|
console.log(`Ask DSH to customize the mobile Web UI and features in: ${customCss} and ${customScript}`);
|
|
182
418
|
console.log("Start DSH with: dsh --profile web");
|
|
183
|
-
console.log("Then open the Mobile card in the lower-left corner and create a pairing
|
|
419
|
+
console.log("Then open the Mobile card in the lower-left corner and create a pairing key.");
|
|
184
420
|
}
|
|
185
421
|
async function purge(args) {
|
|
186
422
|
if (args.length !== 1 || args[0] !== "--yes") throw new Error("purge requires --yes");
|
|
@@ -189,11 +425,12 @@ async function purge(args) {
|
|
|
189
425
|
recursive: true,
|
|
190
426
|
force: true
|
|
191
427
|
});
|
|
428
|
+
await removeWindowsFirewall();
|
|
192
429
|
console.log("Removed DSH Mobile certificates, devices, preferences, and custom Web files.");
|
|
193
430
|
}
|
|
194
431
|
function help() {
|
|
195
432
|
console.log([
|
|
196
|
-
"dsh-mobile setup [--address 192.168.x.x] [--port 3443] [--dsh-port 3080]",
|
|
433
|
+
"dsh-mobile setup [--address 192.168.x.x] [--port 3443] [--dsh-port 3080] [--no-firewall]",
|
|
197
434
|
"dsh-mobile purge --yes",
|
|
198
435
|
"",
|
|
199
436
|
"Run through the DSH profile:",
|