profile-manager-secure 1.0.0
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/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/cli.js.map +1 -0
- package/dist/frontend/assets/index-D0IIAYWB.css +1 -0
- package/dist/frontend/assets/index-D9Y0_s3b.js +273 -0
- package/dist/frontend/index.html +26 -0
- package/dist/middleware/auth.middleware.d.ts +8 -0
- package/dist/middleware/auth.middleware.js +51 -0
- package/dist/middleware/auth.middleware.js.map +1 -0
- package/dist/middleware/error.middleware.d.ts +2 -0
- package/dist/middleware/error.middleware.js +11 -0
- package/dist/middleware/error.middleware.js.map +1 -0
- package/dist/modules/auth/auth.controller.d.ts +1 -0
- package/dist/modules/auth/auth.controller.js +175 -0
- package/dist/modules/auth/auth.controller.js.map +1 -0
- package/dist/modules/credential/credential.controller.d.ts +1 -0
- package/dist/modules/credential/credential.controller.js +263 -0
- package/dist/modules/credential/credential.controller.js.map +1 -0
- package/dist/modules/proxy/proxy.controller.d.ts +1 -0
- package/dist/modules/proxy/proxy.controller.js +270 -0
- package/dist/modules/proxy/proxy.controller.js.map +1 -0
- package/dist/modules/proxy/proxy.service.d.ts +28 -0
- package/dist/modules/proxy/proxy.service.js +185 -0
- package/dist/modules/proxy/proxy.service.js.map +1 -0
- package/dist/modules/tailscale/tailscale.controller.d.ts +1 -0
- package/dist/modules/tailscale/tailscale.controller.js +149 -0
- package/dist/modules/tailscale/tailscale.controller.js.map +1 -0
- package/dist/modules/tailscale/tailscale.service.d.ts +96 -0
- package/dist/modules/tailscale/tailscale.service.js +561 -0
- package/dist/modules/tailscale/tailscale.service.js.map +1 -0
- package/dist/modules/totp/totp.controller.d.ts +1 -0
- package/dist/modules/totp/totp.controller.js +41 -0
- package/dist/modules/totp/totp.controller.js.map +1 -0
- package/dist/modules/totp/totp.service.d.ts +26 -0
- package/dist/modules/totp/totp.service.js +96 -0
- package/dist/modules/totp/totp.service.js.map +1 -0
- package/dist/modules/totp/totp.service.test.d.ts +1 -0
- package/dist/modules/totp/totp.service.test.js +41 -0
- package/dist/modules/totp/totp.service.test.js.map +1 -0
- package/dist/modules/vault/apikey.service.d.ts +27 -0
- package/dist/modules/vault/apikey.service.js +87 -0
- package/dist/modules/vault/apikey.service.js.map +1 -0
- package/dist/modules/vault/constants.d.ts +2 -0
- package/dist/modules/vault/constants.js +24 -0
- package/dist/modules/vault/constants.js.map +1 -0
- package/dist/modules/vault/types.d.ts +73 -0
- package/dist/modules/vault/types.js +2 -0
- package/dist/modules/vault/types.js.map +1 -0
- package/dist/modules/vault/vault-migration.service.d.ts +21 -0
- package/dist/modules/vault/vault-migration.service.js +99 -0
- package/dist/modules/vault/vault-migration.service.js.map +1 -0
- package/dist/modules/vault/vault-storage.service.d.ts +22 -0
- package/dist/modules/vault/vault-storage.service.js +76 -0
- package/dist/modules/vault/vault-storage.service.js.map +1 -0
- package/dist/modules/vault/vault.service.d.ts +66 -0
- package/dist/modules/vault/vault.service.js +229 -0
- package/dist/modules/vault/vault.service.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +67 -0
- package/dist/server.js.map +1 -0
- package/dist/utils/crypto.d.ts +20 -0
- package/dist/utils/crypto.js +54 -0
- package/dist/utils/crypto.js.map +1 -0
- package/dist/utils/crypto.test.d.ts +1 -0
- package/dist/utils/crypto.test.js +66 -0
- package/dist/utils/crypto.test.js.map +1 -0
- package/dist/utils/logger.d.ts +5 -0
- package/dist/utils/logger.js +12 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tailscale.controller.js","sourceRoot":"","sources":["../../../src/modules/tailscale/tailscale.controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export interface TailscaleStatus {
|
|
2
|
+
installed: boolean;
|
|
3
|
+
running: boolean;
|
|
4
|
+
backendState: string;
|
|
5
|
+
ip4?: string;
|
|
6
|
+
ip6?: string;
|
|
7
|
+
deviceName?: string;
|
|
8
|
+
tailnet?: string;
|
|
9
|
+
peersCount?: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
isInstalling?: boolean;
|
|
12
|
+
installProgress?: number;
|
|
13
|
+
installStatus?: string;
|
|
14
|
+
hasCert?: boolean;
|
|
15
|
+
certDomain?: string;
|
|
16
|
+
dnsName?: string;
|
|
17
|
+
magicDNSSuffix?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare class TailscaleService {
|
|
20
|
+
private cliPath;
|
|
21
|
+
private appDataDir;
|
|
22
|
+
private tailscaleDir;
|
|
23
|
+
private socketPath;
|
|
24
|
+
private statusCache;
|
|
25
|
+
private cacheTimestamp;
|
|
26
|
+
private cacheTTL;
|
|
27
|
+
private isRefreshingStatus;
|
|
28
|
+
private watchdogInterval;
|
|
29
|
+
private desiredStateEnabled;
|
|
30
|
+
private isInstalling;
|
|
31
|
+
private installProgress;
|
|
32
|
+
private installStatus;
|
|
33
|
+
constructor();
|
|
34
|
+
private detectCli;
|
|
35
|
+
private runCli;
|
|
36
|
+
isInstalled(): Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Get cached Tailscale status to protect Event Loop
|
|
39
|
+
*/
|
|
40
|
+
getStatus(): Promise<TailscaleStatus>;
|
|
41
|
+
private refreshStatusCache;
|
|
42
|
+
/**
|
|
43
|
+
* Spawn tailscaled in userspace mode if standard daemon is unavailable
|
|
44
|
+
*/
|
|
45
|
+
startUserspaceDaemon(): Promise<boolean>;
|
|
46
|
+
up(options: {
|
|
47
|
+
authKey?: string;
|
|
48
|
+
acceptDns?: boolean;
|
|
49
|
+
exitNode?: string;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
success: boolean;
|
|
52
|
+
message: string;
|
|
53
|
+
}>;
|
|
54
|
+
down(): Promise<{
|
|
55
|
+
success: boolean;
|
|
56
|
+
message: string;
|
|
57
|
+
}>;
|
|
58
|
+
logout(): Promise<{
|
|
59
|
+
success: boolean;
|
|
60
|
+
message: string;
|
|
61
|
+
}>;
|
|
62
|
+
getFunnelStatus(): Promise<{
|
|
63
|
+
funnelActive: boolean;
|
|
64
|
+
serveActive: boolean;
|
|
65
|
+
funnelUrl?: string;
|
|
66
|
+
serveUrl?: string;
|
|
67
|
+
}>;
|
|
68
|
+
toggleFunnel(active: boolean): Promise<{
|
|
69
|
+
success: boolean;
|
|
70
|
+
message: string;
|
|
71
|
+
}>;
|
|
72
|
+
toggleServe(active: boolean): Promise<{
|
|
73
|
+
success: boolean;
|
|
74
|
+
message: string;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Provision TLS Certificate from Tailscale for secure HTTPS API access
|
|
78
|
+
*/
|
|
79
|
+
provisionCert(): Promise<{
|
|
80
|
+
success: boolean;
|
|
81
|
+
message: string;
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* Download and Install Tailscale CLI silently on Windows
|
|
85
|
+
*/
|
|
86
|
+
installTailscale(): Promise<{
|
|
87
|
+
success: boolean;
|
|
88
|
+
message: string;
|
|
89
|
+
}>;
|
|
90
|
+
private downloadMsi;
|
|
91
|
+
/**
|
|
92
|
+
* Watchdog checking connection health every 30 seconds
|
|
93
|
+
*/
|
|
94
|
+
private startWatchdog;
|
|
95
|
+
}
|
|
96
|
+
export declare const tailscaleService: TailscaleService;
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import https from 'https';
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
7
|
+
export class TailscaleService {
|
|
8
|
+
cliPath = null;
|
|
9
|
+
appDataDir;
|
|
10
|
+
tailscaleDir;
|
|
11
|
+
socketPath = null;
|
|
12
|
+
// Cache & Watchdog variables
|
|
13
|
+
statusCache = null;
|
|
14
|
+
cacheTimestamp = 0;
|
|
15
|
+
cacheTTL = 10000; // 10 seconds
|
|
16
|
+
isRefreshingStatus = false;
|
|
17
|
+
watchdogInterval = null;
|
|
18
|
+
desiredStateEnabled = false;
|
|
19
|
+
// Installer state
|
|
20
|
+
isInstalling = false;
|
|
21
|
+
installProgress = 0;
|
|
22
|
+
installStatus = '';
|
|
23
|
+
constructor() {
|
|
24
|
+
this.appDataDir = path.join(os.homedir(), '.account-manager');
|
|
25
|
+
this.tailscaleDir = path.join(this.appDataDir, 'tailscale');
|
|
26
|
+
// Create directories if they do not exist
|
|
27
|
+
if (!fs.existsSync(this.appDataDir)) {
|
|
28
|
+
fs.mkdirSync(this.appDataDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
if (!fs.existsSync(this.tailscaleDir)) {
|
|
31
|
+
fs.mkdirSync(this.tailscaleDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
this.detectCli();
|
|
34
|
+
this.startWatchdog();
|
|
35
|
+
}
|
|
36
|
+
detectCli() {
|
|
37
|
+
const commonPaths = [
|
|
38
|
+
'C:\\Program Files\\Tailscale\\tailscale.exe',
|
|
39
|
+
'C:\\Program Files (x86)\\Tailscale\\tailscale.exe'
|
|
40
|
+
];
|
|
41
|
+
for (const p of commonPaths) {
|
|
42
|
+
if (fs.existsSync(p)) {
|
|
43
|
+
this.cliPath = p;
|
|
44
|
+
logger.info(`Tailscale CLI detected at: ${p}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.cliPath = 'tailscale';
|
|
49
|
+
}
|
|
50
|
+
runCli(args, options = {}) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const bin = this.cliPath || 'tailscale';
|
|
53
|
+
const finalArgs = [...args];
|
|
54
|
+
if (this.socketPath) {
|
|
55
|
+
finalArgs.unshift(`--socket=${this.socketPath}`);
|
|
56
|
+
}
|
|
57
|
+
// Mask sensitive info (like authkey) in log output
|
|
58
|
+
const loggedArgs = finalArgs.map(arg => {
|
|
59
|
+
if (arg.startsWith('--authkey=')) {
|
|
60
|
+
return '--authkey=********';
|
|
61
|
+
}
|
|
62
|
+
return arg;
|
|
63
|
+
});
|
|
64
|
+
logger.info(`Running Tailscale CLI command: ${bin} ${loggedArgs.join(' ')}`);
|
|
65
|
+
const child = spawn(bin, finalArgs, {
|
|
66
|
+
timeout: options.timeout || 30000,
|
|
67
|
+
windowsHide: true
|
|
68
|
+
});
|
|
69
|
+
let stdout = '';
|
|
70
|
+
let stderr = '';
|
|
71
|
+
child.stdout?.on('data', (data) => {
|
|
72
|
+
stdout += data.toString();
|
|
73
|
+
});
|
|
74
|
+
child.stderr?.on('data', (data) => {
|
|
75
|
+
stderr += data.toString();
|
|
76
|
+
});
|
|
77
|
+
child.on('close', (code) => {
|
|
78
|
+
if (code === 0) {
|
|
79
|
+
resolve({ stdout, stderr });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const error = new Error(`Command failed with exit code ${code}`);
|
|
83
|
+
error.stdout = stdout;
|
|
84
|
+
error.stderr = stderr;
|
|
85
|
+
error.code = code;
|
|
86
|
+
reject(error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
child.on('error', (err) => {
|
|
90
|
+
reject(err);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async isInstalled() {
|
|
95
|
+
try {
|
|
96
|
+
if (this.cliPath && this.cliPath !== 'tailscale') {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
await new Promise((resolve, reject) => {
|
|
100
|
+
const child = spawn(this.cliPath || 'tailscale', ['--version'], { windowsHide: true });
|
|
101
|
+
child.on('close', (code) => code === 0 ? resolve() : reject());
|
|
102
|
+
child.on('error', reject);
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get cached Tailscale status to protect Event Loop
|
|
112
|
+
*/
|
|
113
|
+
async getStatus() {
|
|
114
|
+
// If installing, return installation status
|
|
115
|
+
if (this.isInstalling) {
|
|
116
|
+
return {
|
|
117
|
+
installed: false,
|
|
118
|
+
running: false,
|
|
119
|
+
backendState: 'Installing',
|
|
120
|
+
isInstalling: true,
|
|
121
|
+
installProgress: this.installProgress,
|
|
122
|
+
installStatus: this.installStatus
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
const isCacheStale = now - this.cacheTimestamp > this.cacheTTL;
|
|
127
|
+
if (this.statusCache && !isCacheStale) {
|
|
128
|
+
return this.statusCache;
|
|
129
|
+
}
|
|
130
|
+
if (this.isRefreshingStatus) {
|
|
131
|
+
return this.statusCache || { installed: false, running: false, backendState: 'Querying' };
|
|
132
|
+
}
|
|
133
|
+
// Refresh cache in background or wait if no cache exists
|
|
134
|
+
if (!this.statusCache) {
|
|
135
|
+
await this.refreshStatusCache();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.refreshStatusCache().catch((err) => logger.error('Background status refresh failed:', err));
|
|
139
|
+
}
|
|
140
|
+
return this.statusCache;
|
|
141
|
+
}
|
|
142
|
+
async refreshStatusCache() {
|
|
143
|
+
this.isRefreshingStatus = true;
|
|
144
|
+
try {
|
|
145
|
+
const installed = await this.isInstalled();
|
|
146
|
+
if (!installed) {
|
|
147
|
+
this.statusCache = { installed: false, running: false, backendState: 'NotInstalled' };
|
|
148
|
+
this.cacheTimestamp = Date.now();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Check if SSL certs exist in our certs folder
|
|
152
|
+
const certsDir = path.join(this.appDataDir, 'certs');
|
|
153
|
+
let hasCert = false;
|
|
154
|
+
let certDomain = '';
|
|
155
|
+
if (fs.existsSync(certsDir)) {
|
|
156
|
+
const files = fs.readdirSync(certsDir);
|
|
157
|
+
const crtFile = files.find(f => f.endsWith('.crt'));
|
|
158
|
+
if (crtFile) {
|
|
159
|
+
hasCert = true;
|
|
160
|
+
certDomain = crtFile.replace('.crt', '');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const { stdout } = await this.runCli(['status', '--json'], { timeout: 4000 });
|
|
165
|
+
const statusData = JSON.parse(stdout);
|
|
166
|
+
const self = statusData.Self;
|
|
167
|
+
const backendState = statusData.BackendState;
|
|
168
|
+
// If running, mark desired state as enabled to let watchdog keep it alive
|
|
169
|
+
if (backendState === 'Running') {
|
|
170
|
+
this.desiredStateEnabled = true;
|
|
171
|
+
}
|
|
172
|
+
this.statusCache = {
|
|
173
|
+
installed: true,
|
|
174
|
+
running: true,
|
|
175
|
+
backendState,
|
|
176
|
+
ip4: self?.TailscaleIPs?.[0] || undefined,
|
|
177
|
+
ip6: self?.TailscaleIPs?.[1] || undefined,
|
|
178
|
+
deviceName: self?.HostName || undefined,
|
|
179
|
+
tailnet: statusData.CurrentTailnet?.Name || undefined,
|
|
180
|
+
dnsName: self?.DNSName ? self.DNSName.replace(/\.$/, '') : undefined,
|
|
181
|
+
magicDNSSuffix: statusData.MagicDNSSuffix || statusData.CurrentTailnet?.MagicDNSSuffix || undefined,
|
|
182
|
+
peersCount: statusData.Peer ? Object.keys(statusData.Peer).length : 0,
|
|
183
|
+
hasCert,
|
|
184
|
+
certDomain
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const errMsg = error.message || '';
|
|
189
|
+
// Handle case where tailscaled daemon is not running
|
|
190
|
+
if (errMsg.includes('connect') || errMsg.includes('daemon') || errMsg.includes('tailscaled')) {
|
|
191
|
+
this.statusCache = {
|
|
192
|
+
installed: true,
|
|
193
|
+
running: false,
|
|
194
|
+
backendState: 'Stopped',
|
|
195
|
+
error: 'Tailscale daemon is not running.',
|
|
196
|
+
hasCert,
|
|
197
|
+
certDomain
|
|
198
|
+
};
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Try reading output if tailscale status failed but returned json (e.g. logged out state)
|
|
202
|
+
try {
|
|
203
|
+
if (error.stdout) {
|
|
204
|
+
const statusData = JSON.parse(error.stdout);
|
|
205
|
+
const self = statusData.Self;
|
|
206
|
+
this.statusCache = {
|
|
207
|
+
installed: true,
|
|
208
|
+
running: true,
|
|
209
|
+
backendState: statusData.BackendState || 'NeedsLogin',
|
|
210
|
+
ip4: self?.TailscaleIPs?.[0] || undefined,
|
|
211
|
+
ip6: self?.TailscaleIPs?.[1] || undefined,
|
|
212
|
+
deviceName: self?.HostName || undefined,
|
|
213
|
+
tailnet: statusData.CurrentTailnet?.Name || undefined,
|
|
214
|
+
dnsName: self?.DNSName ? self.DNSName.replace(/\.$/, '') : undefined,
|
|
215
|
+
magicDNSSuffix: statusData.MagicDNSSuffix || statusData.CurrentTailnet?.MagicDNSSuffix || undefined,
|
|
216
|
+
hasCert,
|
|
217
|
+
certDomain
|
|
218
|
+
};
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
223
|
+
this.statusCache = {
|
|
224
|
+
installed: true,
|
|
225
|
+
running: true,
|
|
226
|
+
backendState: 'NeedsLogin',
|
|
227
|
+
error: errMsg || 'Tailscale requires authentication.',
|
|
228
|
+
hasCert,
|
|
229
|
+
certDomain
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
this.cacheTimestamp = Date.now();
|
|
235
|
+
this.isRefreshingStatus = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Spawn tailscaled in userspace mode if standard daemon is unavailable
|
|
240
|
+
*/
|
|
241
|
+
async startUserspaceDaemon() {
|
|
242
|
+
if (os.platform() !== 'win32')
|
|
243
|
+
return false; // Windows focus
|
|
244
|
+
const binPath = this.cliPath && this.cliPath !== 'tailscale'
|
|
245
|
+
? path.join(path.dirname(this.cliPath), 'tailscaled.exe')
|
|
246
|
+
: 'tailscaled';
|
|
247
|
+
this.socketPath = path.join(this.tailscaleDir, 'tailscaled.sock');
|
|
248
|
+
logger.info(`Starting local Userspace Tailscale daemon: ${binPath}`);
|
|
249
|
+
// Kill any existing userspace tailscaled processes first
|
|
250
|
+
try {
|
|
251
|
+
await new Promise((resolve) => {
|
|
252
|
+
const child = spawn('taskkill', ['/f', '/im', 'tailscaled.exe'], { windowsHide: true });
|
|
253
|
+
child.on('close', () => resolve());
|
|
254
|
+
child.on('error', () => resolve());
|
|
255
|
+
}).catch(() => { });
|
|
256
|
+
}
|
|
257
|
+
catch { }
|
|
258
|
+
const daemonArgs = [
|
|
259
|
+
`--socket=${this.socketPath}`,
|
|
260
|
+
`--statedir=${this.tailscaleDir}`,
|
|
261
|
+
`--no-tun`
|
|
262
|
+
];
|
|
263
|
+
const child = spawn(binPath, daemonArgs, {
|
|
264
|
+
detached: true,
|
|
265
|
+
stdio: 'ignore',
|
|
266
|
+
windowsHide: true
|
|
267
|
+
});
|
|
268
|
+
child.unref();
|
|
269
|
+
// Wait 2 seconds for socket initialization
|
|
270
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
271
|
+
// Verify it started by checking status
|
|
272
|
+
try {
|
|
273
|
+
const status = await this.getStatus();
|
|
274
|
+
return status.running;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async up(options) {
|
|
281
|
+
try {
|
|
282
|
+
this.desiredStateEnabled = true;
|
|
283
|
+
let status = await this.getStatus();
|
|
284
|
+
// If daemon is not running, try starting in userspace mode
|
|
285
|
+
if (!status.running) {
|
|
286
|
+
const daemonStarted = await this.startUserspaceDaemon();
|
|
287
|
+
if (!daemonStarted) {
|
|
288
|
+
return { success: false, message: 'Không thể khởi động dịch vụ Tailscale. Hãy đảm bảo bạn đã chạy app hoặc cài đặt dịch vụ.' };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const cmdArgs = ['up'];
|
|
292
|
+
if (options.authKey) {
|
|
293
|
+
cmdArgs.push(`--authkey=${options.authKey}`);
|
|
294
|
+
}
|
|
295
|
+
if (options.acceptDns !== undefined) {
|
|
296
|
+
cmdArgs.push(`--accept-dns=${options.acceptDns}`);
|
|
297
|
+
}
|
|
298
|
+
if (options.exitNode) {
|
|
299
|
+
cmdArgs.push(`--exit-node=${options.exitNode}`);
|
|
300
|
+
}
|
|
301
|
+
cmdArgs.push('--reset');
|
|
302
|
+
await this.runCli(cmdArgs, { timeout: 30000 });
|
|
303
|
+
this.cacheTimestamp = 0; // Invalidate cache
|
|
304
|
+
return { success: true, message: 'Connected to Tailscale successfully' };
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
logger.error('Failed to connect Tailscale:', error);
|
|
308
|
+
return { success: false, message: error.message || 'Failed to connect' };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async down() {
|
|
312
|
+
try {
|
|
313
|
+
this.desiredStateEnabled = false;
|
|
314
|
+
await this.runCli(['down']);
|
|
315
|
+
this.cacheTimestamp = 0; // Invalidate cache
|
|
316
|
+
return { success: true, message: 'Disconnected from Tailscale' };
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
logger.error('Failed to disconnect Tailscale:', error);
|
|
320
|
+
return { success: false, message: error.message || 'Failed to disconnect' };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async logout() {
|
|
324
|
+
try {
|
|
325
|
+
this.desiredStateEnabled = false;
|
|
326
|
+
await this.runCli(['logout']);
|
|
327
|
+
// Clean up userspace socket if running
|
|
328
|
+
if (this.socketPath && fs.existsSync(this.socketPath)) {
|
|
329
|
+
try {
|
|
330
|
+
fs.unlinkSync(this.socketPath);
|
|
331
|
+
}
|
|
332
|
+
catch { }
|
|
333
|
+
this.socketPath = null;
|
|
334
|
+
}
|
|
335
|
+
this.cacheTimestamp = 0; // Invalidate cache
|
|
336
|
+
return { success: true, message: 'Logged out of Tailscale' };
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
logger.error('Failed to logout Tailscale:', error);
|
|
340
|
+
return { success: false, message: error.message || 'Failed to logout' };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async getFunnelStatus() {
|
|
344
|
+
try {
|
|
345
|
+
const installed = await this.isInstalled();
|
|
346
|
+
if (!installed) {
|
|
347
|
+
return { funnelActive: false, serveActive: false };
|
|
348
|
+
}
|
|
349
|
+
// 1. Run tailscale serve status --json
|
|
350
|
+
const { stdout: serveStdout } = await this.runCli(['serve', 'status', '--json']).catch(() => ({ stdout: '{}' }));
|
|
351
|
+
const serveData = JSON.parse(serveStdout || '{}');
|
|
352
|
+
// 2. Run tailscale funnel status --json
|
|
353
|
+
const { stdout: funnelStdout } = await this.runCli(['funnel', 'status', '--json']).catch(() => ({ stdout: '{}' }));
|
|
354
|
+
const funnelData = JSON.parse(funnelStdout || '{}');
|
|
355
|
+
// Fetch DNSName from status to get stable URL (avoids suffix conflicts)
|
|
356
|
+
const status = await this.getStatus();
|
|
357
|
+
const host = status.dnsName || (status.ip4 ? (status.deviceName + '.' + (status.tailnet || '')).replace(/\.$/, '') : '');
|
|
358
|
+
const serveActive = serveData.Web && Object.keys(serveData.Web).length > 0;
|
|
359
|
+
const funnelActive = funnelData.AllowFunnel && Object.keys(funnelData.AllowFunnel).length > 0;
|
|
360
|
+
return {
|
|
361
|
+
funnelActive: !!funnelActive,
|
|
362
|
+
serveActive: !!serveActive,
|
|
363
|
+
funnelUrl: funnelActive && host ? `https://${host}` : undefined,
|
|
364
|
+
serveUrl: serveActive && host ? `https://${host}` : undefined
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
logger.error('Failed to get funnel/serve status:', error);
|
|
369
|
+
return { funnelActive: false, serveActive: false };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async toggleFunnel(active) {
|
|
373
|
+
try {
|
|
374
|
+
if (active) {
|
|
375
|
+
// Expose port 18823 in the background
|
|
376
|
+
await this.runCli(['funnel', '--bg', '--yes', '18823']);
|
|
377
|
+
return { success: true, message: 'Public Tunnel (Tailscale Funnel) enabled' };
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
await this.runCli(['funnel', 'reset']);
|
|
381
|
+
return { success: true, message: 'Public Tunnel disabled' };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
logger.error('Failed to toggle Funnel:', error);
|
|
386
|
+
return { success: false, message: error.message || 'Failed to toggle Funnel' };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async toggleServe(active) {
|
|
390
|
+
try {
|
|
391
|
+
if (active) {
|
|
392
|
+
// Expose port 18823 in the background inside Tailnet
|
|
393
|
+
await this.runCli(['serve', '--bg', '--yes', '18823']);
|
|
394
|
+
return { success: true, message: 'Tailnet HTTPS Serve enabled' };
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
await this.runCli(['serve', 'reset']);
|
|
398
|
+
return { success: true, message: 'Tailnet HTTPS Serve disabled' };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
logger.error('Failed to toggle Serve:', error);
|
|
403
|
+
return { success: false, message: error.message || 'Failed to toggle Serve' };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Provision TLS Certificate from Tailscale for secure HTTPS API access
|
|
408
|
+
*/
|
|
409
|
+
async provisionCert() {
|
|
410
|
+
const status = await this.getStatus();
|
|
411
|
+
const domain = status.dnsName || (status.deviceName && status.magicDNSSuffix ? `${status.deviceName}.${status.magicDNSSuffix}` : '');
|
|
412
|
+
if (!status.running || !domain) {
|
|
413
|
+
return { success: false, message: 'Tailscale VPN phải hoạt động và có tên miền hợp lệ để cấp chứng chỉ TLS.' };
|
|
414
|
+
}
|
|
415
|
+
const certsDir = path.join(this.appDataDir, 'certs');
|
|
416
|
+
if (!fs.existsSync(certsDir)) {
|
|
417
|
+
fs.mkdirSync(certsDir, { recursive: true });
|
|
418
|
+
}
|
|
419
|
+
const certFile = path.join(certsDir, `${domain}.crt`);
|
|
420
|
+
const keyFile = path.join(certsDir, `${domain}.key`);
|
|
421
|
+
try {
|
|
422
|
+
logger.info(`Provisioning TLS cert for ${domain}...`);
|
|
423
|
+
await this.runCli(['cert', '--cert-file', certFile, '--key-file', keyFile, domain], { timeout: 45000 });
|
|
424
|
+
this.cacheTimestamp = 0; // Invalidate cache
|
|
425
|
+
return { success: true, message: `Cấp chứng chỉ TLS thành công cho tên miền: ${domain}` };
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
logger.error('Failed to provision TLS certificate:', error);
|
|
429
|
+
return { success: false, message: `Lỗi cấp chứng chỉ: ${error.message || 'Unknown error'}` };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Download and Install Tailscale CLI silently on Windows
|
|
434
|
+
*/
|
|
435
|
+
async installTailscale() {
|
|
436
|
+
if (this.isInstalling) {
|
|
437
|
+
return { success: false, message: 'Quá trình cài đặt đang diễn ra.' };
|
|
438
|
+
}
|
|
439
|
+
if (os.platform() !== 'win32') {
|
|
440
|
+
return { success: false, message: 'Tính năng cài đặt tự động hiện tại chỉ khả dụng trên Windows.' };
|
|
441
|
+
}
|
|
442
|
+
this.isInstalling = true;
|
|
443
|
+
this.installProgress = 0;
|
|
444
|
+
this.installStatus = 'Đang chuẩn bị tải xuống...';
|
|
445
|
+
const msiUrl = 'https://pkgs.tailscale.com/stable/tailscale-setup-latest-amd64.msi';
|
|
446
|
+
const msiPath = path.join(os.tmpdir(), 'tailscale-setup.msi');
|
|
447
|
+
// Run async install process
|
|
448
|
+
(async () => {
|
|
449
|
+
try {
|
|
450
|
+
this.installStatus = 'Đang tải bộ cài đặt Tailscale MSI (khoảng 30MB)...';
|
|
451
|
+
await this.downloadMsi(msiUrl, msiPath, (pct) => {
|
|
452
|
+
this.installProgress = Math.round(pct * 0.7); // 70% progress for downloading
|
|
453
|
+
});
|
|
454
|
+
this.installStatus = 'Đang thực thi cài đặt ngầm. Vui lòng cấp quyền Administrator (UAC) nếu được hỏi...';
|
|
455
|
+
this.installProgress = 80;
|
|
456
|
+
const msiArgs = `'/i','${msiPath}','TS_NOLAUNCH=true','/quiet','/norestart'`;
|
|
457
|
+
const cmd = `Start-Process msiexec -ArgumentList ${msiArgs} -Verb RunAs -Wait`;
|
|
458
|
+
await new Promise((resolve, reject) => {
|
|
459
|
+
const child = spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', cmd], {
|
|
460
|
+
timeout: 60000,
|
|
461
|
+
windowsHide: true
|
|
462
|
+
});
|
|
463
|
+
let stderr = '';
|
|
464
|
+
child.stderr?.on('data', (d) => stderr += d.toString());
|
|
465
|
+
child.on('close', (code) => {
|
|
466
|
+
if (code === 0)
|
|
467
|
+
resolve();
|
|
468
|
+
else
|
|
469
|
+
reject(new Error(`Powershell installation exited with code ${code}. Stderr: ${stderr}`));
|
|
470
|
+
});
|
|
471
|
+
child.on('error', reject);
|
|
472
|
+
});
|
|
473
|
+
this.installStatus = 'Đang kiểm tra kết quả cài đặt...';
|
|
474
|
+
this.installProgress = 95;
|
|
475
|
+
// Poll for 5 seconds to wait for file system update
|
|
476
|
+
let found = false;
|
|
477
|
+
for (let i = 0; i < 5; i++) {
|
|
478
|
+
this.detectCli();
|
|
479
|
+
if (this.cliPath && this.cliPath !== 'tailscale') {
|
|
480
|
+
found = true;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
484
|
+
}
|
|
485
|
+
if (!found) {
|
|
486
|
+
throw new Error('Đã cài đặt xong nhưng không tìm thấy file tailscale.exe');
|
|
487
|
+
}
|
|
488
|
+
this.installProgress = 100;
|
|
489
|
+
this.installStatus = 'Cài đặt Tailscale hoàn tất thành công!';
|
|
490
|
+
logger.info('Tailscale silent install completed successfully.');
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
logger.error('Tailscale silent install failed:', error);
|
|
494
|
+
this.installStatus = `Cài đặt thất bại: ${error.message || 'Unknown error'}`;
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
this.isInstalling = false;
|
|
498
|
+
try {
|
|
499
|
+
if (fs.existsSync(msiPath)) {
|
|
500
|
+
fs.unlinkSync(msiPath);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch { }
|
|
504
|
+
}
|
|
505
|
+
})();
|
|
506
|
+
return { success: true, message: 'Bắt đầu quá trình tải và cài đặt Tailscale ngầm.' };
|
|
507
|
+
}
|
|
508
|
+
downloadMsi(url, dest, onProgress) {
|
|
509
|
+
return new Promise((resolve, reject) => {
|
|
510
|
+
const file = fs.createWriteStream(dest);
|
|
511
|
+
https.get(url, (res) => {
|
|
512
|
+
if (res.statusCode !== 200) {
|
|
513
|
+
reject(new Error(`Tải xuống thất bại với HTTP code: ${res.statusCode}`));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const total = parseInt(res.headers['content-length'] || '0', 10);
|
|
517
|
+
let downloaded = 0;
|
|
518
|
+
res.on('data', (chunk) => {
|
|
519
|
+
downloaded += chunk.length;
|
|
520
|
+
if (total > 0) {
|
|
521
|
+
onProgress((downloaded / total) * 100);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
res.pipe(file);
|
|
525
|
+
file.on('finish', () => {
|
|
526
|
+
file.close();
|
|
527
|
+
resolve();
|
|
528
|
+
});
|
|
529
|
+
}).on('error', (err) => {
|
|
530
|
+
fs.unlink(dest, () => { });
|
|
531
|
+
reject(err);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Watchdog checking connection health every 30 seconds
|
|
537
|
+
*/
|
|
538
|
+
startWatchdog() {
|
|
539
|
+
if (this.watchdogInterval)
|
|
540
|
+
return;
|
|
541
|
+
this.watchdogInterval = setInterval(async () => {
|
|
542
|
+
if (!this.desiredStateEnabled || this.isInstalling)
|
|
543
|
+
return;
|
|
544
|
+
try {
|
|
545
|
+
const status = await this.getStatus();
|
|
546
|
+
// If it is stopped unexpectedly, try to bring it up in userspace
|
|
547
|
+
if (status.installed && status.backendState === 'Stopped') {
|
|
548
|
+
logger.warn('[Tailscale Watchdog] Service stopped unexpectedly, attempting self-healing...');
|
|
549
|
+
await this.up({ acceptDns: true });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
logger.error('[Tailscale Watchdog] Error checking connection state:', err);
|
|
554
|
+
}
|
|
555
|
+
}, 30000);
|
|
556
|
+
// Make sure interval doesn't hold the process open
|
|
557
|
+
this.watchdogInterval.unref();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
export const tailscaleService = new TailscaleService();
|
|
561
|
+
//# sourceMappingURL=tailscale.service.js.map
|