vpn-split 21.0.16 → 21.0.19

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 (52) hide show
  1. package/browser/fesm2022/vpn-split-browser.mjs +2 -3
  2. package/browser/fesm2022/vpn-split-browser.mjs.map +1 -1
  3. package/browser/package.json +1 -1
  4. package/browser-prod/fesm2022/vpn-split-browser.mjs +2 -2
  5. package/browser-prod/fesm2022/vpn-split-browser.mjs.map +1 -1
  6. package/browser-prod/package.json +1 -1
  7. package/lib/build-info._auto-generated_.d.ts +1 -1
  8. package/lib/build-info._auto-generated_.js +1 -1
  9. package/lib/models.js +1 -2
  10. package/lib/models.js.map +1 -1
  11. package/lib/package.json +1 -1
  12. package/lib-prod/build-info._auto-generated_.js +14 -0
  13. package/lib-prod/env/env.angular-node-app.js +130 -0
  14. package/lib-prod/env/env.docs-webapp.js +130 -0
  15. package/lib-prod/env/env.electron-app.js +130 -0
  16. package/lib-prod/env/env.mobile-app.js +130 -0
  17. package/lib-prod/env/env.npm-lib-and-cli-tool.js +130 -0
  18. package/lib-prod/env/env.vscode-plugin.js +130 -0
  19. package/lib-prod/env/index.js +6 -0
  20. package/lib-prod/{hostile.backend.ts → hostile.backend.js} +21 -31
  21. package/lib-prod/index._auto-generated_.js +0 -0
  22. package/lib-prod/index.js +2 -0
  23. package/lib-prod/migrations/index.js +1 -0
  24. package/lib-prod/migrations/migrations_index._auto-generated_.js +0 -0
  25. package/lib-prod/models.js +165 -0
  26. package/lib-prod/package.json +1 -1
  27. package/lib-prod/start.backend.js +0 -0
  28. package/lib-prod/vpn-split.backend.js +700 -0
  29. package/package.json +1 -1
  30. package/websql/fesm2022/vpn-split-websql.mjs +2 -3
  31. package/websql/fesm2022/vpn-split-websql.mjs.map +1 -1
  32. package/websql/package.json +1 -1
  33. package/websql-prod/fesm2022/vpn-split-websql.mjs +2 -2
  34. package/websql-prod/fesm2022/vpn-split-websql.mjs.map +1 -1
  35. package/websql-prod/package.json +1 -1
  36. package/lib-prod/build-info._auto-generated_.ts +0 -27
  37. package/lib-prod/env/env.angular-node-app.ts +0 -66
  38. package/lib-prod/env/env.docs-webapp.ts +0 -66
  39. package/lib-prod/env/env.electron-app.ts +0 -66
  40. package/lib-prod/env/env.mobile-app.ts +0 -66
  41. package/lib-prod/env/env.npm-lib-and-cli-tool.ts +0 -66
  42. package/lib-prod/env/env.vscode-plugin.ts +0 -66
  43. package/lib-prod/env/index.ts +0 -6
  44. package/lib-prod/index._auto-generated_.ts +0 -5
  45. package/lib-prod/index.ts +0 -5
  46. package/lib-prod/lib-info.md +0 -8
  47. package/lib-prod/migrations/index.ts +0 -2
  48. package/lib-prod/migrations/migrations-info.md +0 -6
  49. package/lib-prod/migrations/migrations_index._auto-generated_.ts +0 -5
  50. package/lib-prod/models.ts +0 -250
  51. package/lib-prod/start.backend.ts +0 -28
  52. package/lib-prod/vpn-split.backend.ts +0 -1039
@@ -0,0 +1,700 @@
1
+ import { execSync } from "child_process";
2
+ import * as dgram from "dgram";
3
+ import * as http from "http";
4
+ import axios from "axios";
5
+ import * as express from "express";
6
+ import * as httpProxy from "http-proxy";
7
+ import { Log, Level } from "ng2-logger/lib-prod";
8
+ import { config } from "tnp-core/lib-prod";
9
+ import { path, fse, https, isElevated, crossPlatformPath, ___NS__includes, ___NS__isArray, ___NS__keys, ___NS__merge, ___NS__values, UtilsNetwork__NS__getEtcHostsPath, UtilsNetwork__NS__getFirstIpV4LocalActiveIpAddress, UtilsNetwork__NS__getLocalIpAddresses, UtilsOs__NS__commandExistsSync, UtilsOs__NS__getRealHomeDir, UtilsOs__NS__isRunningInWindowsPowerShell } from "tnp-core/lib-prod";
10
+ import { CoreModels__NS__localhostDomain, CoreModels__NS__localhostIp127 } from "tnp-core/lib-prod";
11
+ import { Helpers__NS__error, Helpers__NS__exists, Helpers__NS__info, Helpers__NS__killProcessByPort, Helpers__NS__run, Helpers__NS__writeFile, HelpersTaon__NS__arrays__NS__from } from "tnp-helpers/lib-prod";
12
+ import { Hostile } from "./hostile.backend";
13
+ import { HostForServer } from "./models";
14
+ const HOST_FILE_PATH = UtilsNetwork__NS__getEtcHostsPath();
15
+ const log = Log.create("vpn-split", Level.INFO);
16
+ const GENERATED = "#GENERATED_BY_CLI#";
17
+ const EOL = process.platform === "win32" ? "\r\n" : "\n";
18
+ const SERVERS_PATH = "/$$$$servers$$$$";
19
+ const HOST_FILE_PATHUSER = crossPlatformPath([
20
+ UtilsOs__NS__getRealHomeDir(),
21
+ "hosts-file__vpn-split"
22
+ ]);
23
+ const from = HostForServer.From;
24
+ const DefaultEtcHosts = {
25
+ "localhost alias": from({
26
+ ipOrDomain: "127.0.0.1",
27
+ aliases: "localhost",
28
+ isDefault: true
29
+ }),
30
+ broadcasthost: from({
31
+ ipOrDomain: "255.255.255.255",
32
+ aliases: "broadcasthost",
33
+ isDefault: true
34
+ }),
35
+ "localhost alias ipv6": from({
36
+ ipOrDomain: "::1",
37
+ aliases: "localhost",
38
+ isDefault: true
39
+ })
40
+ };
41
+ class VpnSplit {
42
+ constructor(portsToPass, hosts, cwd) {
43
+ this.portsToPass = portsToPass;
44
+ this.hosts = hosts;
45
+ this.cwd = cwd;
46
+ this.__hostile = new Hostile();
47
+ }
48
+ //#region getters
49
+ get hostsArr() {
50
+ const hosts = this.hosts;
51
+ return ___NS__keys(hosts).map((hostName) => {
52
+ const v = hosts[hostName];
53
+ v.name = hostName;
54
+ return v;
55
+ });
56
+ }
57
+ get hostsArrWithoutDefault() {
58
+ return this.hostsArr.filter((f) => !f.isDefault);
59
+ }
60
+ get serveKeyName() {
61
+ return "tmp-" + config.file.server_key;
62
+ }
63
+ get serveKeyPath() {
64
+ return path.join(this.cwd, this.serveKeyName);
65
+ }
66
+ get serveCertName() {
67
+ return "tmp-" + config.file.server_cert;
68
+ }
69
+ get serveCertPath() {
70
+ return path.join(this.cwd, this.serveCertName);
71
+ }
72
+ get serveCertChainName() {
73
+ return "tmp-" + config.file.server_chain_cert;
74
+ }
75
+ get serveCertChainPath() {
76
+ return path.join(this.cwd, this.serveCertChainName);
77
+ }
78
+ //#endregion
79
+ //#region fields
80
+ __hostile;
81
+ //#endregion
82
+ //#region singleton
83
+ static _instances = {};
84
+ static async Instance({
85
+ ports = [80, 443, 4443, 22, 2222, 8180, 8080, 4407, 7999, 9443],
86
+ additionalDefaultHosts = {},
87
+ cwd = process.cwd(),
88
+ allowNotSudo = false
89
+ } = {}) {
90
+ if (!await isElevated() && !allowNotSudo) {
91
+ Helpers__NS__error(
92
+ `[vpn-split] Please run this program as sudo (or admin on windows)`,
93
+ false,
94
+ true
95
+ );
96
+ }
97
+ if (!VpnSplit._instances[cwd]) {
98
+ VpnSplit._instances[cwd] = new VpnSplit(
99
+ ports,
100
+ ___NS__merge(DefaultEtcHosts, additionalDefaultHosts),
101
+ cwd
102
+ );
103
+ }
104
+ return VpnSplit._instances[cwd];
105
+ }
106
+ //#endregion
107
+ //#region start server
108
+ async startServer(saveHostInUserFolder = false) {
109
+ this.createCertificateIfNotExists();
110
+ const hostsForCert = this.hosts;
111
+ saveHosts(hostsForCert, { saveHostInUserFolder });
112
+ for (const portToPassthrough of this.portsToPass) {
113
+ await this.serverPassthrough(portToPassthrough);
114
+ }
115
+ for (const portToPassthrough of this.portsToPass) {
116
+ await this.serverUdpPassthrough(portToPassthrough);
117
+ }
118
+ Helpers__NS__info(`Activated (server).`);
119
+ }
120
+ //#endregion
121
+ //#region apply hosts
122
+ applyHosts(hosts) {
123
+ saveHosts(hosts);
124
+ }
125
+ applyHostsLocal(hosts) {
126
+ saveHostsLocal(hosts);
127
+ }
128
+ //#endregion
129
+ //#region start client
130
+ async startClient(vpnServerTargets, options) {
131
+ options = options || {};
132
+ const { saveHostInUserFolder } = options;
133
+ if (!Array.isArray(vpnServerTargets)) {
134
+ vpnServerTargets = [vpnServerTargets];
135
+ }
136
+ for (const vpnServerTarget of vpnServerTargets) {
137
+ await this.preventBadTargetForClient(vpnServerTarget);
138
+ }
139
+ this.createCertificateIfNotExists();
140
+ const hosts = [];
141
+ for (const vpnServerTarget of vpnServerTargets) {
142
+ const newHosts = await this.getRemoteHosts(vpnServerTarget);
143
+ for (const host of newHosts) {
144
+ host.originHostname = vpnServerTarget.hostname;
145
+ hosts.push(host);
146
+ }
147
+ }
148
+ const originalHosts = this.hostsArr;
149
+ const combinedHostsObj = ___NS__values(
150
+ [
151
+ ...originalHosts,
152
+ ...hosts.map(
153
+ (h) => HostForServer.From(
154
+ {
155
+ aliases: h.alias,
156
+ ipOrDomain: h.ip,
157
+ originHostname: h.originHostname
158
+ },
159
+ `external host ${h.alias} ${h.ip}`
160
+ )
161
+ )
162
+ ].map((c) => {
163
+ let copy = c.clone();
164
+ if (!copy.isDefault) {
165
+ copy.ip = `127.0.0.1`;
166
+ }
167
+ return copy;
168
+ }).reduce((prev, curr) => {
169
+ return ___NS__merge(prev, {
170
+ [curr.aliases.join(" ")]: curr
171
+ });
172
+ }, {})
173
+ );
174
+ const hostsForCert = options.useHost ? options.useHost : combinedHostsObj;
175
+ saveHosts(hostsForCert, {
176
+ saveHostInUserFolder
177
+ });
178
+ for (const portToPassthrough of this.portsToPass) {
179
+ await this.clientPassthrough(
180
+ portToPassthrough,
181
+ vpnServerTargets,
182
+ combinedHostsObj
183
+ );
184
+ }
185
+ for (const portToPassthrough of this.portsToPass) {
186
+ await this.clientUdpPassthrough(
187
+ portToPassthrough,
188
+ vpnServerTargets,
189
+ combinedHostsObj
190
+ );
191
+ }
192
+ Helpers__NS__info(`Client activated`);
193
+ }
194
+ //#endregion
195
+ //#region private methods / get remote hosts
196
+ async getRemoteHosts(vpnServerTarget) {
197
+ try {
198
+ const url = `http://${vpnServerTarget.hostname}${SERVERS_PATH}`;
199
+ const response = await axios({ url, method: "GET" });
200
+ return response.data;
201
+ } catch (err) {
202
+ Helpers__NS__error(
203
+ `Remote server: ${vpnServerTarget.hostname} may be inactive...`,
204
+ true,
205
+ true
206
+ );
207
+ return [];
208
+ }
209
+ }
210
+ //#endregion
211
+ //#region private methods / create certificate
212
+ createCertificateIfNotExists() {
213
+ if (!Helpers__NS__exists(this.serveKeyPath) || !Helpers__NS__exists(this.serveCertPath)) {
214
+ Helpers__NS__info(
215
+ `[vpn-split] Generating new self-signed certificate for localhost...`
216
+ );
217
+ const commandGen = UtilsOs__NS__isRunningInWindowsPowerShell() ? `powershell -Command "& 'C:\\Program Files\\Git\\usr\\bin\\openssl.exe' req -nodes -new -x509 -keyout ${this.serveKeyName} -out ${this.serveCertName}"` : `openssl req -nodes -new -x509 -keyout ${this.serveKeyName} -out ${this.serveCertName}`;
218
+ Helpers__NS__run(commandGen, { cwd: this.cwd, output: true }).sync();
219
+ }
220
+ }
221
+ //#endregion
222
+ //#region private methods / TCP & HTTPS passthrough
223
+ getTarget({
224
+ req,
225
+ res,
226
+ port,
227
+ hostname
228
+ }) {
229
+ return `${req.protocol}://${hostname}:${port}`;
230
+ }
231
+ getProxyConfig({
232
+ req,
233
+ res,
234
+ port,
235
+ hostname,
236
+ isHttps
237
+ }) {
238
+ const serverPassthrough = !!hostname;
239
+ const target = this.getTarget({
240
+ req,
241
+ res,
242
+ port,
243
+ hostname: serverPassthrough ? hostname : req.hostname
244
+ });
245
+ return isHttps ? {
246
+ target,
247
+ ssl: {
248
+ key: fse.readFileSync(this.serveKeyPath),
249
+ cert: fse.readFileSync(this.serveCertPath)
250
+ },
251
+ // agent: new https.Agent({
252
+ // secureOptions: crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION,
253
+ // }),
254
+ // changeOrigin: true,
255
+ secure: true
256
+ } : { target };
257
+ }
258
+ getNotFoundMsg(req, res, port, type) {
259
+ return `[vpn-split] You are requesting a URL that is not in proxy reach [${type}]
260
+ Protocol: ${req.protocol}
261
+ Hostname: ${req.hostname}
262
+ OriginalUrl: ${req.originalUrl}
263
+ Req.method: ${req.method}
264
+ Port: ${port}
265
+ SERVERS_PATH: ${SERVERS_PATH}`;
266
+ }
267
+ getMaybeChangeOriginTrueMsg(req, res, port, type) {
268
+ return `[vpn-split] Possibly need changeOrigin: true in your proxy config
269
+ Protocol: ${req.protocol}
270
+ Hostname: ${req.hostname}
271
+ OriginalUrl: ${req.originalUrl}
272
+ Req.method: ${req.method}
273
+ Port: ${port}`;
274
+ }
275
+ filterHeaders(req, res) {
276
+ const headersToRemove = [
277
+ // 'Strict-Transport-Security',
278
+ // 'Content-Security-Policy',
279
+ // ...
280
+ ];
281
+ headersToRemove.forEach((headerName) => {
282
+ delete req.headers[headerName];
283
+ res.setHeader(headerName, "");
284
+ });
285
+ }
286
+ isHttpsPort(port) {
287
+ return [443, 4443, 9443].includes(port);
288
+ }
289
+ //#region server passthrough (TCP/HTTPS)
290
+ async serverPassthrough(portToPassthrough) {
291
+ const isHttps = this.isHttpsPort(portToPassthrough);
292
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
293
+ const app = express();
294
+ const proxy = httpProxy.createProxyServer({});
295
+ const localIp = await UtilsNetwork__NS__getLocalIpAddresses();
296
+ const currentLocalIps = [
297
+ CoreModels__NS__localhostDomain,
298
+ CoreModels__NS__localhostIp127,
299
+ ...localIp.map((a) => a.address)
300
+ ];
301
+ app.use(
302
+ (req, res, next) => {
303
+ this.filterHeaders(req, res);
304
+ if (currentLocalIps.includes(req.hostname)) {
305
+ if (req.method === "GET" && req.originalUrl === SERVERS_PATH) {
306
+ res.send(
307
+ JSON.stringify(
308
+ this.hostsArrWithoutDefault.map((h) => ({
309
+ ip: h.ip,
310
+ alias: HelpersTaon__NS__arrays__NS__from(h.aliases).join(" ")
311
+ }))
312
+ )
313
+ );
314
+ } else {
315
+ const msg = this.getNotFoundMsg(
316
+ req,
317
+ res,
318
+ portToPassthrough,
319
+ "server"
320
+ );
321
+ log.d(msg);
322
+ res.send(msg);
323
+ }
324
+ next();
325
+ } else {
326
+ proxy.web(
327
+ req,
328
+ res,
329
+ this.getProxyConfig({ req, res, port: portToPassthrough, isHttps }),
330
+ next
331
+ );
332
+ }
333
+ }
334
+ );
335
+ const server = isHttps ? https.createServer(
336
+ {
337
+ key: fse.readFileSync(this.serveKeyPath),
338
+ cert: fse.readFileSync(this.serveCertPath)
339
+ },
340
+ app
341
+ ) : http.createServer(app);
342
+ await Helpers__NS__killProcessByPort(portToPassthrough, { silent: true });
343
+ await new Promise((resolve, reject) => {
344
+ server.listen(portToPassthrough, () => {
345
+ console.log(
346
+ `TCP/HTTPS server listening on port ${portToPassthrough} (secure=${isHttps})`
347
+ );
348
+ resolve();
349
+ });
350
+ });
351
+ }
352
+ //#endregion
353
+ //#region client passthrough (TCP/HTTPS)
354
+ async clientPassthrough(portToPassthrough, vpnServerTargets, hostsArr) {
355
+ const aliasToOriginHostname = {};
356
+ for (const h of hostsArr) {
357
+ for (const alias of h.aliases) {
358
+ aliasToOriginHostname[alias] = h.originHostname;
359
+ }
360
+ }
361
+ delete aliasToOriginHostname["localhost"];
362
+ delete aliasToOriginHostname["broadcasthost"];
363
+ const originToUrlMap = {};
364
+ for (const url of vpnServerTargets) {
365
+ originToUrlMap[url.hostname] = url;
366
+ }
367
+ const isHttps = this.isHttpsPort(portToPassthrough);
368
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
369
+ const app = express();
370
+ const proxy = httpProxy.createProxyServer({});
371
+ app.use(
372
+ (req, res, next) => {
373
+ this.filterHeaders(req, res);
374
+ const originHostname = aliasToOriginHostname[req.hostname];
375
+ if (!originHostname) {
376
+ const msg = this.getMaybeChangeOriginTrueMsg(
377
+ req,
378
+ res,
379
+ portToPassthrough,
380
+ "client"
381
+ );
382
+ res.send(msg);
383
+ next();
384
+ return;
385
+ }
386
+ const targetUrlObj = originToUrlMap[originHostname];
387
+ if (!targetUrlObj) {
388
+ const notFoundMsg = this.getNotFoundMsg(
389
+ req,
390
+ res,
391
+ portToPassthrough,
392
+ "client"
393
+ );
394
+ res.send(notFoundMsg);
395
+ next();
396
+ return;
397
+ }
398
+ proxy.web(
399
+ req,
400
+ res,
401
+ this.getProxyConfig({
402
+ req,
403
+ res,
404
+ port: portToPassthrough,
405
+ isHttps,
406
+ hostname: targetUrlObj.hostname
407
+ }),
408
+ next
409
+ );
410
+ }
411
+ );
412
+ const server = isHttps ? https.createServer(
413
+ {
414
+ key: fse.readFileSync(this.serveKeyPath),
415
+ cert: fse.readFileSync(this.serveCertPath)
416
+ },
417
+ app
418
+ ) : http.createServer(app);
419
+ await Helpers__NS__killProcessByPort(portToPassthrough, { silent: true });
420
+ await new Promise((resolve, reject) => {
421
+ server.listen(portToPassthrough, () => {
422
+ log.i(
423
+ `TCP/HTTPS client listening on port ${portToPassthrough} (secure=${isHttps})`
424
+ );
425
+ resolve();
426
+ });
427
+ });
428
+ }
429
+ //#endregion
430
+ //#region UDP passthrough
431
+ /**
432
+ * Start a UDP socket for “server” mode on a given port.
433
+ * This example forwards inbound messages right back to the sender,
434
+ * or you can forward them to an external IP:port if desired.
435
+ */
436
+ async serverUdpPassthrough(port) {
437
+ return;
438
+ const socket = dgram.createSocket("udp4");
439
+ socket.on("message", (msg, rinfo) => {
440
+ socket.send(msg, 0, msg.length, rinfo.port, rinfo.address, (err) => {
441
+ if (err) {
442
+ log.er(`UDP server send error: ${err}`);
443
+ }
444
+ });
445
+ });
446
+ socket.on("listening", () => {
447
+ const address = socket.address();
448
+ log.i(`UDP server listening at ${address.address}:${address.port}`);
449
+ });
450
+ socket.bind(port);
451
+ }
452
+ /**
453
+ * Start a UDP socket for “client” mode on a given port.
454
+ * This example also just does a trivial pass-through or echo,
455
+ * but you can adapt to forward to a remote server.
456
+ */
457
+ async clientUdpPassthrough(port, vpnServerTargets, hostsArr) {
458
+ return;
459
+ const socket = dgram.createSocket("udp4");
460
+ const primaryTarget = vpnServerTargets[0];
461
+ const targetHost = primaryTarget.hostname;
462
+ const targetPort = port;
463
+ const clientMap = /* @__PURE__ */ new Map();
464
+ socket.on("message", (msg, rinfo) => {
465
+ const isFromLocal = !___NS__includes(
466
+ hostsArr.map((h) => h.ipOrDomain),
467
+ rinfo.address
468
+ );
469
+ if (isFromLocal) {
470
+ const key = `local-${rinfo.address}:${rinfo.port}`;
471
+ clientMap.set(key, rinfo);
472
+ socket.send(msg, 0, msg.length, targetPort, targetHost, (err) => {
473
+ if (err) {
474
+ log.er(`UDP client forward error: ${err}`);
475
+ }
476
+ });
477
+ } else {
478
+ clientMap.forEach((localRinfo, key) => {
479
+ socket.send(
480
+ msg,
481
+ 0,
482
+ msg.length,
483
+ localRinfo.port,
484
+ localRinfo.address,
485
+ (err) => {
486
+ if (err) {
487
+ log.er(`UDP client re-send error: ${err}`);
488
+ }
489
+ }
490
+ );
491
+ });
492
+ }
493
+ });
494
+ socket.on("listening", () => {
495
+ const address = socket.address();
496
+ log.i(
497
+ `UDP client listening at ${address.address}:${address.port}, forwarding to ${targetHost}:${targetPort}`
498
+ );
499
+ });
500
+ socket.bind(port);
501
+ }
502
+ //#endregion
503
+ //#region private methods / get port from request
504
+ getPortFromRequest(req) {
505
+ const host = req.headers.host;
506
+ const protocol = req.protocol;
507
+ if (host) {
508
+ const hostParts = host.split(":");
509
+ if (hostParts.length === 2) {
510
+ return Number(hostParts[1]);
511
+ } else {
512
+ return protocol === "https" ? 443 : 80;
513
+ }
514
+ }
515
+ return 80;
516
+ }
517
+ //#endregion
518
+ // Put this method in your VpnSplit class (or outside as a static helper)
519
+ ensureMkcertCertificate(domains) {
520
+ if (!UtilsOs__NS__commandExistsSync("mkcert")) {
521
+ Helpers__NS__error("[vpn-split] mkcert is not installed.", false, true);
522
+ }
523
+ const certPath = this.serveCertPath;
524
+ const keyPath = this.serveKeyPath;
525
+ const allNames = [
526
+ "localhost",
527
+ "127.0.0.1",
528
+ // '::1',
529
+ // optional: common local networks so the cert also works for raw IP access
530
+ // '10.0.0.0/8',
531
+ // '172.16.0.0/12',
532
+ // '192.168.0.0/16',
533
+ ...domains
534
+ // <-- your 120+ domains here
535
+ ];
536
+ const runMkcert = (args) => {
537
+ try {
538
+ return execSync(`mkcert ${args}`, { stdio: "pipe" }).toString().trim();
539
+ } catch (err) {
540
+ const stderr = err.stderr?.toString() || "";
541
+ if (stderr.includes("command not found")) {
542
+ Helpers__NS__error(
543
+ "[vpn-split] mkcert is not installed. Install it first: https://github.com/FiloSottile/mkcert",
544
+ false,
545
+ true
546
+ );
547
+ }
548
+ throw err;
549
+ }
550
+ };
551
+ let caInstalled = false;
552
+ try {
553
+ const output = runMkcert("-CAROOT");
554
+ const rootDir = crossPlatformPath(output.trim());
555
+ if (rootDir && fse.pathExistsSync(crossPlatformPath([rootDir, "rootCA.pem"]))) {
556
+ Helpers__NS__info("[vpn-split] mkcert local CA is already installed");
557
+ caInstalled = true;
558
+ } else {
559
+ Helpers__NS__info("[vpn-split] mkcert local CA is NOT installed");
560
+ }
561
+ } catch {
562
+ Helpers__NS__info("[vpn-split] mkcert error checking local CA installation");
563
+ }
564
+ if (!caInstalled) {
565
+ Helpers__NS__info(
566
+ "[vpn-split] Installing mkcert local CA (requires sudo once)..."
567
+ );
568
+ try {
569
+ execSync("mkcert -install", { stdio: "inherit" });
570
+ Helpers__NS__info("[vpn-split] Local CA installed and trusted system-wide");
571
+ } catch (err) {
572
+ Helpers__NS__error(
573
+ '[vpn-split] Failed to run "mkcert -install". Run it manually with sudo.',
574
+ false,
575
+ true
576
+ );
577
+ }
578
+ }
579
+ const isWindows = process.platform === "win32";
580
+ let tempCertFiles = [];
581
+ let tempKeyFiles = [];
582
+ if (allNames.length <= 20 || !isWindows) {
583
+ const namesArg = allNames.map((d) => `"${d}"`).join(" ");
584
+ const mkcertCmd = `-key-file "${keyPath}" -cert-file "${certPath}" ${namesArg}`;
585
+ fse.ensureDirSync(path.dirname(certPath));
586
+ runMkcert(mkcertCmd);
587
+ } else {
588
+ const chunkSize = 20;
589
+ fse.ensureDirSync(path.dirname(certPath));
590
+ for (let i = 0; i < allNames.length; i += chunkSize) {
591
+ const chunk = allNames.slice(i, i + chunkSize);
592
+ const tempCert = `${certPath}.${i}.pem`;
593
+ const tempKey = `${keyPath}.${i}.pem`;
594
+ const namesArg = chunk.map((d) => `"${d}"`).join(" ");
595
+ const mkcertCmd = `-key-file "${tempKey}" -cert-file "${tempCert}" ${namesArg}`;
596
+ Helpers__NS__info(
597
+ `[vpn-split] Generating batch ${Math.floor(i / chunkSize) + 1}/${Math.ceil(allNames.length / chunkSize)}...`
598
+ );
599
+ runMkcert(mkcertCmd);
600
+ tempCertFiles.push(tempCert);
601
+ tempKeyFiles.push(tempKey);
602
+ }
603
+ Helpers__NS__info("[vpn-split] Merging batches into final cert...");
604
+ const firstKey = tempKeyFiles[0];
605
+ fse.copySync(firstKey, keyPath);
606
+ const mergedCertContent = tempCertFiles.map((f) => fse.readFileSync(f, "utf8")).join("\n");
607
+ fse.writeFileSync(certPath, mergedCertContent);
608
+ tempCertFiles.forEach((f) => fse.removeSync(f));
609
+ tempKeyFiles.slice(1).forEach((f) => fse.removeSync(f));
610
+ Helpers__NS__info("[vpn-split] Merged cert ready!");
611
+ }
612
+ if (!fse.existsSync(certPath) || !fse.existsSync(keyPath)) {
613
+ Helpers__NS__error("[vpn-split] Files missing after generation!", false, true);
614
+ } else {
615
+ Helpers__NS__info(
616
+ `[vpn-split] Trusted cert ready: ${allNames.length} hostnames covered`
617
+ );
618
+ }
619
+ }
620
+ //#region private methods / prevent bad target for client
621
+ async preventBadTargetForClient(vpnServerTarget) {
622
+ if (!vpnServerTarget) {
623
+ const currentLocalIp = await UtilsNetwork__NS__getFirstIpV4LocalActiveIpAddress();
624
+ Helpers__NS__error(
625
+ `[vpn-server] Please provide a correct target server.
626
+ Example:
627
+ vpn-server ${currentLocalIp}
628
+
629
+ Your local IP is: ${currentLocalIp}`,
630
+ false,
631
+ true
632
+ );
633
+ }
634
+ }
635
+ //#endregion
636
+ }
637
+ const genMsg = `
638
+ ################################################
639
+ ## This file is generated #####################
640
+ ################################################
641
+ `.trim() + EOL;
642
+ function saveHosts(hosts, options) {
643
+ const { saveHostInUserFolder } = options || {};
644
+ if (___NS__isArray(hosts)) {
645
+ hosts = hosts.reduce((prev, curr) => {
646
+ return ___NS__merge(prev, {
647
+ [curr.name]: curr
648
+ });
649
+ }, {});
650
+ }
651
+ const toSave = parseHost(hosts, !!saveHostInUserFolder);
652
+ if (saveHostInUserFolder) {
653
+ Helpers__NS__writeFile(HOST_FILE_PATHUSER, toSave);
654
+ } else {
655
+ Helpers__NS__writeFile(HOST_FILE_PATH, toSave);
656
+ }
657
+ }
658
+ function saveHostsLocal(hosts, options) {
659
+ const { saveHostInUserFolder } = options || {};
660
+ if (___NS__isArray(hosts)) {
661
+ hosts = hosts.reduce((prev, curr) => {
662
+ return ___NS__merge(prev, {
663
+ [curr.name]: curr
664
+ });
665
+ }, {});
666
+ }
667
+ const toSave = parseHost(hosts, !!saveHostInUserFolder, true);
668
+ if (saveHostInUserFolder) {
669
+ Helpers__NS__writeFile(HOST_FILE_PATHUSER, toSave);
670
+ } else {
671
+ Helpers__NS__writeFile(HOST_FILE_PATH, toSave);
672
+ }
673
+ }
674
+ function parseHost(hosts, saveHostInUserFolder, useLocal = false) {
675
+ hosts = {
676
+ ...DefaultEtcHosts,
677
+ ...hosts
678
+ };
679
+ ___NS__keys(hosts).forEach((hostName) => {
680
+ const v = hosts[hostName];
681
+ v.name = hostName;
682
+ });
683
+ return genMsg + EOL + ___NS__keys(hosts).map((hostName) => {
684
+ const v = hosts[hostName];
685
+ if (v.skipUpdateOfServerEtcHosts) {
686
+ console.warn(
687
+ `[vpn-split] Skip saving host: ${v.name} (${v.ipOrDomain})`
688
+ );
689
+ return `# SKIPPING HOST ${v.ipOrDomain} ${v.aliases.join(" ")} ${GENERATED}`;
690
+ }
691
+ const aliasesStr = v.aliases.join(" ");
692
+ if (saveHostInUserFolder) {
693
+ return useLocal ? `127.0.0.1 ${aliasesStr}` : `${v.disabled ? "#" : ""}${v.ipOrDomain} ${aliasesStr}`;
694
+ }
695
+ return useLocal ? `127.0.0.1 ${aliasesStr}` : `${v.disabled ? "#" : ""}${v.ipOrDomain} ${aliasesStr} # ${v.name} ${GENERATED}`;
696
+ }).join(EOL) + EOL + EOL + genMsg;
697
+ }
698
+ export {
699
+ VpnSplit
700
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vpn-split",
3
- "version": "21.0.16",
3
+ "version": "21.0.19",
4
4
  "scripts": {
5
5
  "taon init": "taon init",
6
6
  "taon start": "taon start",