devtunnel-cli 3.0.45 ā 3.1.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/README.md +18 -7
- package/package.json +89 -86
- package/src/core/RUN.js +64 -64
- package/src/core/index.js +374 -374
- package/src/core/proxy-server.js +105 -105
- package/src/core/setup-cloudflared.js +427 -427
- package/src/core/start.js +2 -2
- package/src/core/static-server.js +109 -109
- package/src/utils/folder-picker.js +140 -140
- package/src/utils/pages/index.html +1 -1
- package/src/utils/tunnel-helpers.js +35 -35
|
@@ -1,427 +1,427 @@
|
|
|
1
|
-
import { spawn, exec } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import https from 'https';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { dirname } from 'path';
|
|
7
|
-
import { promisify } from 'util';
|
|
8
|
-
|
|
9
|
-
const execAsync = promisify(exec);
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = dirname(__filename);
|
|
12
|
-
|
|
13
|
-
const BIN_DIR = path.join(__dirname, '../../bin');
|
|
14
|
-
const CLOUDFLARED_VERSION = '2024.8.2'; // Latest stable version
|
|
15
|
-
|
|
16
|
-
// Binary URLs with multiple mirrors for reliability
|
|
17
|
-
const DOWNLOAD_URLS = {
|
|
18
|
-
win32: [
|
|
19
|
-
`https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
|
|
20
|
-
`https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`
|
|
21
|
-
],
|
|
22
|
-
darwin: [
|
|
23
|
-
`https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`,
|
|
24
|
-
`https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`
|
|
25
|
-
],
|
|
26
|
-
linux: [
|
|
27
|
-
`https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
|
|
28
|
-
`https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`
|
|
29
|
-
]
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Get platform display name
|
|
33
|
-
function getPlatformName() {
|
|
34
|
-
const platform = process.platform;
|
|
35
|
-
return platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'macOS' : 'Linux';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function getBinaryPath() {
|
|
39
|
-
const platform = process.platform;
|
|
40
|
-
const binName = platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
|
|
41
|
-
return path.join(BIN_DIR, platform, binName);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function hasEnoughDiskSpace() {
|
|
45
|
-
try {
|
|
46
|
-
const stats = fs.statfsSync ? fs.statfsSync(BIN_DIR) : null;
|
|
47
|
-
if (stats) {
|
|
48
|
-
const availableSpace = stats.bavail * stats.bsize;
|
|
49
|
-
const requiredSpace = 50 * 1024 * 1024;
|
|
50
|
-
return availableSpace > requiredSpace;
|
|
51
|
-
}
|
|
52
|
-
return true;
|
|
53
|
-
} catch {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function safeUnlink(filePath) {
|
|
59
|
-
try {
|
|
60
|
-
if (fs.existsSync(filePath)) {
|
|
61
|
-
fs.unlinkSync(filePath);
|
|
62
|
-
}
|
|
63
|
-
} catch (err) {
|
|
64
|
-
// Ignore permission errors - file might be locked or in use
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function isAdmin() {
|
|
69
|
-
if (process.platform !== 'win32') {
|
|
70
|
-
return process.getuid && process.getuid() === 0;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const { stdout } = await execAsync('net session');
|
|
75
|
-
return stdout.length > 0;
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function showPermissionSolutions(dirPath) {
|
|
82
|
-
console.log('\nš” Solutions:');
|
|
83
|
-
if (process.platform === 'win32') {
|
|
84
|
-
console.log(' 1. Run terminal as Administrator (Right-click ā Run as administrator)');
|
|
85
|
-
console.log(' 2. DevTunnel will automatically request admin privileges if needed');
|
|
86
|
-
} else {
|
|
87
|
-
console.log(' 1. Run with sudo: sudo npm i -g devtunnel-cli');
|
|
88
|
-
}
|
|
89
|
-
console.log(' 2. Check if antivirus is blocking file writes');
|
|
90
|
-
console.log(' 3. Check folder permissions for:', dirPath);
|
|
91
|
-
console.log(' 4. Try installing manually: https://github.com/cloudflare/cloudflared/releases\n');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function downloadFile(url, dest, retryCount = 0) {
|
|
95
|
-
return new Promise((resolve, reject) => {
|
|
96
|
-
const dir = path.dirname(dest);
|
|
97
|
-
try {
|
|
98
|
-
if (!fs.existsSync(dir)) {
|
|
99
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
-
}
|
|
101
|
-
} catch (err) {
|
|
102
|
-
reject(new Error(`Cannot create directory: ${err.message}. Try running as administrator or choose a different location.`));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const tempDest = dest + '.download';
|
|
107
|
-
|
|
108
|
-
// Clean up any existing temp file first
|
|
109
|
-
safeUnlink(tempDest);
|
|
110
|
-
|
|
111
|
-
let file;
|
|
112
|
-
try {
|
|
113
|
-
file = fs.createWriteStream(tempDest);
|
|
114
|
-
} catch (err) {
|
|
115
|
-
if (err.code === 'EPERM' || err.code === 'EACCES') {
|
|
116
|
-
reject(new Error(`Permission denied: Cannot write to ${dir}. Try running as administrator or check antivirus settings.`));
|
|
117
|
-
} else {
|
|
118
|
-
reject(new Error(`Cannot create download file: ${err.message}`));
|
|
119
|
-
}
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const request = https.get(url, {
|
|
124
|
-
headers: {
|
|
125
|
-
'User-Agent': 'DevTunnel/3.0',
|
|
126
|
-
'Accept': '*/*'
|
|
127
|
-
},
|
|
128
|
-
timeout: 30000 // 30 second timeout
|
|
129
|
-
}, (response) => {
|
|
130
|
-
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
131
|
-
file.close();
|
|
132
|
-
safeUnlink(tempDest);
|
|
133
|
-
downloadFile(response.headers.location, dest, retryCount)
|
|
134
|
-
.then(resolve)
|
|
135
|
-
.catch(reject);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (response.statusCode !== 200) {
|
|
140
|
-
file.close();
|
|
141
|
-
safeUnlink(tempDest);
|
|
142
|
-
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
147
|
-
let downloaded = 0;
|
|
148
|
-
let lastPercent = 0;
|
|
149
|
-
|
|
150
|
-
response.on('data', (chunk) => {
|
|
151
|
-
downloaded += chunk.length;
|
|
152
|
-
if (totalSize) {
|
|
153
|
-
const percent = Math.round((downloaded / totalSize) * 100);
|
|
154
|
-
if (percent !== lastPercent && percent % 5 === 0) {
|
|
155
|
-
const mb = (downloaded / 1024 / 1024).toFixed(1);
|
|
156
|
-
const totalMb = (totalSize / 1024 / 1024).toFixed(1);
|
|
157
|
-
process.stdout.write(`\rā³ Downloading: ${percent}% (${mb}/${totalMb} MB)`);
|
|
158
|
-
lastPercent = percent;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
response.pipe(file);
|
|
164
|
-
|
|
165
|
-
file.on('finish', () => {
|
|
166
|
-
file.close(() => {
|
|
167
|
-
// Move temp file to final destination
|
|
168
|
-
try {
|
|
169
|
-
if (fs.existsSync(dest)) {
|
|
170
|
-
fs.unlinkSync(dest);
|
|
171
|
-
}
|
|
172
|
-
fs.renameSync(tempDest, dest);
|
|
173
|
-
|
|
174
|
-
console.log('\nā
Download complete');
|
|
175
|
-
|
|
176
|
-
// Make executable on Unix-like systems
|
|
177
|
-
if (process.platform !== 'win32') {
|
|
178
|
-
try {
|
|
179
|
-
fs.chmodSync(dest, 0o755);
|
|
180
|
-
console.log('ā
Permissions set (executable)');
|
|
181
|
-
} catch (err) {
|
|
182
|
-
console.log('ā ļø Warning: Could not set executable permissions');
|
|
183
|
-
console.log(' Run: chmod +x ' + dest);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Verify file size
|
|
188
|
-
const stats = fs.statSync(dest);
|
|
189
|
-
if (stats.size < 1000000) { // Less than 1MB is suspicious
|
|
190
|
-
fs.unlinkSync(dest);
|
|
191
|
-
reject(new Error('Downloaded file is too small (corrupted)'));
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
resolve();
|
|
196
|
-
} catch (err) {
|
|
197
|
-
reject(new Error(`Cannot finalize download: ${err.message}`));
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
request.on('timeout', () => {
|
|
204
|
-
request.destroy();
|
|
205
|
-
file.close();
|
|
206
|
-
safeUnlink(tempDest);
|
|
207
|
-
reject(new Error('Download timeout (30 seconds)'));
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
request.on('error', (err) => {
|
|
211
|
-
file.close();
|
|
212
|
-
safeUnlink(tempDest);
|
|
213
|
-
reject(err);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
file.on('error', (err) => {
|
|
217
|
-
file.close();
|
|
218
|
-
safeUnlink(tempDest);
|
|
219
|
-
if (err.code === 'EPERM' || err.code === 'EACCES') {
|
|
220
|
-
reject(new Error(`Permission denied: Cannot write to ${tempDest}. Try running as administrator or check antivirus settings.`));
|
|
221
|
-
} else {
|
|
222
|
-
reject(new Error(`File write error: ${err.message}`));
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Try downloading from multiple URLs with retries
|
|
229
|
-
async function downloadWithRetry(urls, dest, maxRetries = 3) {
|
|
230
|
-
for (let urlIndex = 0; urlIndex < urls.length; urlIndex++) {
|
|
231
|
-
const url = urls[urlIndex];
|
|
232
|
-
console.log(`š„ Source: ${urlIndex === 0 ? 'GitHub' : 'Mirror'} (${urlIndex + 1}/${urls.length})`);
|
|
233
|
-
|
|
234
|
-
for (let retry = 0; retry < maxRetries; retry++) {
|
|
235
|
-
try {
|
|
236
|
-
if (retry > 0) {
|
|
237
|
-
console.log(`š Retry ${retry}/${maxRetries - 1}...`);
|
|
238
|
-
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s before retry
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
await downloadFile(url, dest, retry);
|
|
242
|
-
return true; // Success!
|
|
243
|
-
|
|
244
|
-
} catch (err) {
|
|
245
|
-
const isLastRetry = retry === maxRetries - 1;
|
|
246
|
-
const isLastUrl = urlIndex === urls.length - 1;
|
|
247
|
-
|
|
248
|
-
if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
|
|
249
|
-
console.log(`\nā Permission Error: ${err.message}`);
|
|
250
|
-
|
|
251
|
-
if (process.platform === 'win32' && retry === 0) {
|
|
252
|
-
const admin = await isAdmin();
|
|
253
|
-
if (!admin) {
|
|
254
|
-
console.log('\nš Attempting to request administrator privileges...');
|
|
255
|
-
console.log(' Please click "Yes" on the UAC prompt\n');
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
const nodePath = process.execPath;
|
|
259
|
-
const scriptPath = process.argv[1];
|
|
260
|
-
const proc = spawn('powershell', [
|
|
261
|
-
'-NoProfile',
|
|
262
|
-
'-NonInteractive',
|
|
263
|
-
'-ExecutionPolicy', 'Bypass',
|
|
264
|
-
'-Command', `Start-Process -FilePath "${nodePath}" -ArgumentList "${scriptPath}" -Verb RunAs -Wait`
|
|
265
|
-
], {
|
|
266
|
-
stdio: 'inherit',
|
|
267
|
-
shell: false
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
proc.on('close', () => {
|
|
271
|
-
process.exit(0);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
proc.on('error', () => {
|
|
275
|
-
console.log('\nā ļø Could not elevate privileges automatically');
|
|
276
|
-
showPermissionSolutions(path.dirname(dest));
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
return false;
|
|
280
|
-
} catch {
|
|
281
|
-
showPermissionSolutions(path.dirname(dest));
|
|
282
|
-
throw err;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
showPermissionSolutions(path.dirname(dest));
|
|
288
|
-
throw err;
|
|
289
|
-
} else if (err.message.includes('ENOTFOUND') || err.message.includes('ECONNREFUSED')) {
|
|
290
|
-
console.log(`\nā Network error: ${err.message}`);
|
|
291
|
-
} else if (err.message.includes('timeout')) {
|
|
292
|
-
console.log(`\nā Download timeout`);
|
|
293
|
-
} else {
|
|
294
|
-
console.log(`\nā Error: ${err.message}`);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (isLastRetry && isLastUrl) {
|
|
298
|
-
throw new Error(`All download attempts failed: ${err.message}`);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (isLastRetry) {
|
|
302
|
-
console.log('š” Trying alternative source...\n');
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
throw new Error('All download sources failed');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export async function setupCloudflared() {
|
|
313
|
-
const platform = process.platform;
|
|
314
|
-
const binaryPath = getBinaryPath();
|
|
315
|
-
|
|
316
|
-
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
317
|
-
console.log('ā š¦ Cloudflare Setup (First Run) ā');
|
|
318
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
319
|
-
|
|
320
|
-
// Check if binary already exists
|
|
321
|
-
if (fs.existsSync(binaryPath)) {
|
|
322
|
-
try {
|
|
323
|
-
// Verify it works
|
|
324
|
-
const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
|
|
325
|
-
const works = await new Promise((resolve) => {
|
|
326
|
-
testProc.on('close', (code) => resolve(code === 0));
|
|
327
|
-
testProc.on('error', () => resolve(false));
|
|
328
|
-
setTimeout(() => resolve(false), 5000);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
if (works) {
|
|
332
|
-
console.log('ā
Cloudflare already installed and working\n');
|
|
333
|
-
return binaryPath;
|
|
334
|
-
} else {
|
|
335
|
-
console.log('ā ļø Existing binary not working, re-downloading...\n');
|
|
336
|
-
safeUnlink(binaryPath);
|
|
337
|
-
}
|
|
338
|
-
} catch {
|
|
339
|
-
console.log('ā ļø Existing binary corrupted, re-downloading...\n');
|
|
340
|
-
safeUnlink(binaryPath);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const urls = DOWNLOAD_URLS[platform];
|
|
345
|
-
if (!urls) {
|
|
346
|
-
console.error(`ā ERROR: Platform "${platform}" not supported`);
|
|
347
|
-
console.error(' Supported: Windows, macOS, Linux\n');
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
console.log(`š„ļø Platform: ${getPlatformName()}`);
|
|
352
|
-
console.log(`š Install to: ${binaryPath}`);
|
|
353
|
-
console.log(`š Size: ~40 MB\n`);
|
|
354
|
-
|
|
355
|
-
// Check disk space
|
|
356
|
-
if (!hasEnoughDiskSpace()) {
|
|
357
|
-
console.error('ā ERROR: Not enough disk space (need 50+ MB)\n');
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Check write permissions
|
|
362
|
-
try {
|
|
363
|
-
const dir = path.dirname(binaryPath);
|
|
364
|
-
if (!fs.existsSync(dir)) {
|
|
365
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
366
|
-
}
|
|
367
|
-
const testFile = path.join(dir, '.write-test');
|
|
368
|
-
fs.writeFileSync(testFile, 'test');
|
|
369
|
-
fs.unlinkSync(testFile);
|
|
370
|
-
} catch (err) {
|
|
371
|
-
console.error('ā ERROR: Cannot write to installation directory');
|
|
372
|
-
console.error(` Location: ${path.dirname(binaryPath)}`);
|
|
373
|
-
console.error(` Reason: ${err.message}\n`);
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
console.log('š„ Starting download...\n');
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
await downloadWithRetry(urls, binaryPath);
|
|
381
|
-
|
|
382
|
-
// Final verification
|
|
383
|
-
console.log('\nš Verifying installation...');
|
|
384
|
-
const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
|
|
385
|
-
const works = await new Promise((resolve) => {
|
|
386
|
-
testProc.on('close', (code) => resolve(code === 0));
|
|
387
|
-
testProc.on('error', () => resolve(false));
|
|
388
|
-
setTimeout(() => resolve(false), 5000);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
if (works) {
|
|
392
|
-
console.log('ā
Verification successful!');
|
|
393
|
-
console.log('ā
Cloudflare ready to use\n');
|
|
394
|
-
return binaryPath;
|
|
395
|
-
} else {
|
|
396
|
-
console.error('ā Downloaded binary not working properly');
|
|
397
|
-
safeUnlink(binaryPath);
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
} catch (err) {
|
|
402
|
-
console.error('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
403
|
-
console.error('ā ā Installation Failed ā');
|
|
404
|
-
console.error('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
405
|
-
console.error(`Reason: ${err.message}\n`);
|
|
406
|
-
|
|
407
|
-
if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
|
|
408
|
-
showPermissionSolutions(path.dirname(binaryPath));
|
|
409
|
-
} else {
|
|
410
|
-
console.log('š” Troubleshooting:');
|
|
411
|
-
console.log(' 1. Check internet connection');
|
|
412
|
-
console.log(' 2. Check firewall/antivirus settings');
|
|
413
|
-
console.log(' 3. Try running as administrator');
|
|
414
|
-
console.log(' 4. Install manually: https://github.com/cloudflare/cloudflared/releases\n');
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
console.log('š DevTunnel will use fallback tunnels (Ngrok/LocalTunnel)\n');
|
|
418
|
-
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Check if bundled cloudflared exists and is working
|
|
424
|
-
export function hasBundledCloudflared() {
|
|
425
|
-
const binaryPath = getBinaryPath();
|
|
426
|
-
return fs.existsSync(binaryPath);
|
|
427
|
-
}
|
|
1
|
+
import { spawn, exec } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const BIN_DIR = path.join(__dirname, '../../bin');
|
|
14
|
+
const CLOUDFLARED_VERSION = '2024.8.2'; // Latest stable version
|
|
15
|
+
|
|
16
|
+
// Binary URLs with multiple mirrors for reliability
|
|
17
|
+
const DOWNLOAD_URLS = {
|
|
18
|
+
win32: [
|
|
19
|
+
`https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
|
|
20
|
+
`https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`
|
|
21
|
+
],
|
|
22
|
+
darwin: [
|
|
23
|
+
`https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`,
|
|
24
|
+
`https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`
|
|
25
|
+
],
|
|
26
|
+
linux: [
|
|
27
|
+
`https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
|
|
28
|
+
`https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Get platform display name
|
|
33
|
+
function getPlatformName() {
|
|
34
|
+
const platform = process.platform;
|
|
35
|
+
return platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'macOS' : 'Linux';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getBinaryPath() {
|
|
39
|
+
const platform = process.platform;
|
|
40
|
+
const binName = platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
|
|
41
|
+
return path.join(BIN_DIR, platform, binName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasEnoughDiskSpace() {
|
|
45
|
+
try {
|
|
46
|
+
const stats = fs.statfsSync ? fs.statfsSync(BIN_DIR) : null;
|
|
47
|
+
if (stats) {
|
|
48
|
+
const availableSpace = stats.bavail * stats.bsize;
|
|
49
|
+
const requiredSpace = 50 * 1024 * 1024;
|
|
50
|
+
return availableSpace > requiredSpace;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function safeUnlink(filePath) {
|
|
59
|
+
try {
|
|
60
|
+
if (fs.existsSync(filePath)) {
|
|
61
|
+
fs.unlinkSync(filePath);
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// Ignore permission errors - file might be locked or in use
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function isAdmin() {
|
|
69
|
+
if (process.platform !== 'win32') {
|
|
70
|
+
return process.getuid && process.getuid() === 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const { stdout } = await execAsync('net session');
|
|
75
|
+
return stdout.length > 0;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function showPermissionSolutions(dirPath) {
|
|
82
|
+
console.log('\nš” Solutions:');
|
|
83
|
+
if (process.platform === 'win32') {
|
|
84
|
+
console.log(' 1. Run terminal as Administrator (Right-click ā Run as administrator)');
|
|
85
|
+
console.log(' 2. DevTunnel will automatically request admin privileges if needed');
|
|
86
|
+
} else {
|
|
87
|
+
console.log(' 1. Run with sudo: sudo npm i -g devtunnel-cli');
|
|
88
|
+
}
|
|
89
|
+
console.log(' 2. Check if antivirus is blocking file writes');
|
|
90
|
+
console.log(' 3. Check folder permissions for:', dirPath);
|
|
91
|
+
console.log(' 4. Try installing manually: https://github.com/cloudflare/cloudflared/releases\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function downloadFile(url, dest, retryCount = 0) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const dir = path.dirname(dest);
|
|
97
|
+
try {
|
|
98
|
+
if (!fs.existsSync(dir)) {
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
reject(new Error(`Cannot create directory: ${err.message}. Try running as administrator or choose a different location.`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const tempDest = dest + '.download';
|
|
107
|
+
|
|
108
|
+
// Clean up any existing temp file first
|
|
109
|
+
safeUnlink(tempDest);
|
|
110
|
+
|
|
111
|
+
let file;
|
|
112
|
+
try {
|
|
113
|
+
file = fs.createWriteStream(tempDest);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (err.code === 'EPERM' || err.code === 'EACCES') {
|
|
116
|
+
reject(new Error(`Permission denied: Cannot write to ${dir}. Try running as administrator or check antivirus settings.`));
|
|
117
|
+
} else {
|
|
118
|
+
reject(new Error(`Cannot create download file: ${err.message}`));
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const request = https.get(url, {
|
|
124
|
+
headers: {
|
|
125
|
+
'User-Agent': 'DevTunnel/3.0',
|
|
126
|
+
'Accept': '*/*'
|
|
127
|
+
},
|
|
128
|
+
timeout: 30000 // 30 second timeout
|
|
129
|
+
}, (response) => {
|
|
130
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
131
|
+
file.close();
|
|
132
|
+
safeUnlink(tempDest);
|
|
133
|
+
downloadFile(response.headers.location, dest, retryCount)
|
|
134
|
+
.then(resolve)
|
|
135
|
+
.catch(reject);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (response.statusCode !== 200) {
|
|
140
|
+
file.close();
|
|
141
|
+
safeUnlink(tempDest);
|
|
142
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
147
|
+
let downloaded = 0;
|
|
148
|
+
let lastPercent = 0;
|
|
149
|
+
|
|
150
|
+
response.on('data', (chunk) => {
|
|
151
|
+
downloaded += chunk.length;
|
|
152
|
+
if (totalSize) {
|
|
153
|
+
const percent = Math.round((downloaded / totalSize) * 100);
|
|
154
|
+
if (percent !== lastPercent && percent % 5 === 0) {
|
|
155
|
+
const mb = (downloaded / 1024 / 1024).toFixed(1);
|
|
156
|
+
const totalMb = (totalSize / 1024 / 1024).toFixed(1);
|
|
157
|
+
process.stdout.write(`\rā³ Downloading: ${percent}% (${mb}/${totalMb} MB)`);
|
|
158
|
+
lastPercent = percent;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
response.pipe(file);
|
|
164
|
+
|
|
165
|
+
file.on('finish', () => {
|
|
166
|
+
file.close(() => {
|
|
167
|
+
// Move temp file to final destination
|
|
168
|
+
try {
|
|
169
|
+
if (fs.existsSync(dest)) {
|
|
170
|
+
fs.unlinkSync(dest);
|
|
171
|
+
}
|
|
172
|
+
fs.renameSync(tempDest, dest);
|
|
173
|
+
|
|
174
|
+
console.log('\nā
Download complete');
|
|
175
|
+
|
|
176
|
+
// Make executable on Unix-like systems
|
|
177
|
+
if (process.platform !== 'win32') {
|
|
178
|
+
try {
|
|
179
|
+
fs.chmodSync(dest, 0o755);
|
|
180
|
+
console.log('ā
Permissions set (executable)');
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.log('ā ļø Warning: Could not set executable permissions');
|
|
183
|
+
console.log(' Run: chmod +x ' + dest);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Verify file size
|
|
188
|
+
const stats = fs.statSync(dest);
|
|
189
|
+
if (stats.size < 1000000) { // Less than 1MB is suspicious
|
|
190
|
+
fs.unlinkSync(dest);
|
|
191
|
+
reject(new Error('Downloaded file is too small (corrupted)'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
resolve();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
reject(new Error(`Cannot finalize download: ${err.message}`));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
request.on('timeout', () => {
|
|
204
|
+
request.destroy();
|
|
205
|
+
file.close();
|
|
206
|
+
safeUnlink(tempDest);
|
|
207
|
+
reject(new Error('Download timeout (30 seconds)'));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
request.on('error', (err) => {
|
|
211
|
+
file.close();
|
|
212
|
+
safeUnlink(tempDest);
|
|
213
|
+
reject(err);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
file.on('error', (err) => {
|
|
217
|
+
file.close();
|
|
218
|
+
safeUnlink(tempDest);
|
|
219
|
+
if (err.code === 'EPERM' || err.code === 'EACCES') {
|
|
220
|
+
reject(new Error(`Permission denied: Cannot write to ${tempDest}. Try running as administrator or check antivirus settings.`));
|
|
221
|
+
} else {
|
|
222
|
+
reject(new Error(`File write error: ${err.message}`));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Try downloading from multiple URLs with retries
|
|
229
|
+
async function downloadWithRetry(urls, dest, maxRetries = 3) {
|
|
230
|
+
for (let urlIndex = 0; urlIndex < urls.length; urlIndex++) {
|
|
231
|
+
const url = urls[urlIndex];
|
|
232
|
+
console.log(`š„ Source: ${urlIndex === 0 ? 'GitHub' : 'Mirror'} (${urlIndex + 1}/${urls.length})`);
|
|
233
|
+
|
|
234
|
+
for (let retry = 0; retry < maxRetries; retry++) {
|
|
235
|
+
try {
|
|
236
|
+
if (retry > 0) {
|
|
237
|
+
console.log(`š Retry ${retry}/${maxRetries - 1}...`);
|
|
238
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s before retry
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await downloadFile(url, dest, retry);
|
|
242
|
+
return true; // Success!
|
|
243
|
+
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const isLastRetry = retry === maxRetries - 1;
|
|
246
|
+
const isLastUrl = urlIndex === urls.length - 1;
|
|
247
|
+
|
|
248
|
+
if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
|
|
249
|
+
console.log(`\nā Permission Error: ${err.message}`);
|
|
250
|
+
|
|
251
|
+
if (process.platform === 'win32' && retry === 0) {
|
|
252
|
+
const admin = await isAdmin();
|
|
253
|
+
if (!admin) {
|
|
254
|
+
console.log('\nš Attempting to request administrator privileges...');
|
|
255
|
+
console.log(' Please click "Yes" on the UAC prompt\n');
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const nodePath = process.execPath;
|
|
259
|
+
const scriptPath = process.argv[1];
|
|
260
|
+
const proc = spawn('powershell', [
|
|
261
|
+
'-NoProfile',
|
|
262
|
+
'-NonInteractive',
|
|
263
|
+
'-ExecutionPolicy', 'Bypass',
|
|
264
|
+
'-Command', `Start-Process -FilePath "${nodePath}" -ArgumentList "${scriptPath}" -Verb RunAs -Wait`
|
|
265
|
+
], {
|
|
266
|
+
stdio: 'inherit',
|
|
267
|
+
shell: false
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
proc.on('close', () => {
|
|
271
|
+
process.exit(0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
proc.on('error', () => {
|
|
275
|
+
console.log('\nā ļø Could not elevate privileges automatically');
|
|
276
|
+
showPermissionSolutions(path.dirname(dest));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return false;
|
|
280
|
+
} catch {
|
|
281
|
+
showPermissionSolutions(path.dirname(dest));
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
showPermissionSolutions(path.dirname(dest));
|
|
288
|
+
throw err;
|
|
289
|
+
} else if (err.message.includes('ENOTFOUND') || err.message.includes('ECONNREFUSED')) {
|
|
290
|
+
console.log(`\nā Network error: ${err.message}`);
|
|
291
|
+
} else if (err.message.includes('timeout')) {
|
|
292
|
+
console.log(`\nā Download timeout`);
|
|
293
|
+
} else {
|
|
294
|
+
console.log(`\nā Error: ${err.message}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (isLastRetry && isLastUrl) {
|
|
298
|
+
throw new Error(`All download attempts failed: ${err.message}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isLastRetry) {
|
|
302
|
+
console.log('š” Trying alternative source...\n');
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
throw new Error('All download sources failed');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function setupCloudflared() {
|
|
313
|
+
const platform = process.platform;
|
|
314
|
+
const binaryPath = getBinaryPath();
|
|
315
|
+
|
|
316
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
317
|
+
console.log('ā š¦ Cloudflare Setup (First Run) ā');
|
|
318
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
319
|
+
|
|
320
|
+
// Check if binary already exists
|
|
321
|
+
if (fs.existsSync(binaryPath)) {
|
|
322
|
+
try {
|
|
323
|
+
// Verify it works
|
|
324
|
+
const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
|
|
325
|
+
const works = await new Promise((resolve) => {
|
|
326
|
+
testProc.on('close', (code) => resolve(code === 0));
|
|
327
|
+
testProc.on('error', () => resolve(false));
|
|
328
|
+
setTimeout(() => resolve(false), 5000);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (works) {
|
|
332
|
+
console.log('ā
Cloudflare already installed and working\n');
|
|
333
|
+
return binaryPath;
|
|
334
|
+
} else {
|
|
335
|
+
console.log('ā ļø Existing binary not working, re-downloading...\n');
|
|
336
|
+
safeUnlink(binaryPath);
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
console.log('ā ļø Existing binary corrupted, re-downloading...\n');
|
|
340
|
+
safeUnlink(binaryPath);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const urls = DOWNLOAD_URLS[platform];
|
|
345
|
+
if (!urls) {
|
|
346
|
+
console.error(`ā ERROR: Platform "${platform}" not supported`);
|
|
347
|
+
console.error(' Supported: Windows, macOS, Linux\n');
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.log(`š„ļø Platform: ${getPlatformName()}`);
|
|
352
|
+
console.log(`š Install to: ${binaryPath}`);
|
|
353
|
+
console.log(`š Size: ~40 MB\n`);
|
|
354
|
+
|
|
355
|
+
// Check disk space
|
|
356
|
+
if (!hasEnoughDiskSpace()) {
|
|
357
|
+
console.error('ā ERROR: Not enough disk space (need 50+ MB)\n');
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check write permissions
|
|
362
|
+
try {
|
|
363
|
+
const dir = path.dirname(binaryPath);
|
|
364
|
+
if (!fs.existsSync(dir)) {
|
|
365
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
366
|
+
}
|
|
367
|
+
const testFile = path.join(dir, '.write-test');
|
|
368
|
+
fs.writeFileSync(testFile, 'test');
|
|
369
|
+
fs.unlinkSync(testFile);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
console.error('ā ERROR: Cannot write to installation directory');
|
|
372
|
+
console.error(` Location: ${path.dirname(binaryPath)}`);
|
|
373
|
+
console.error(` Reason: ${err.message}\n`);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
console.log('š„ Starting download...\n');
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await downloadWithRetry(urls, binaryPath);
|
|
381
|
+
|
|
382
|
+
// Final verification
|
|
383
|
+
console.log('\nš Verifying installation...');
|
|
384
|
+
const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
|
|
385
|
+
const works = await new Promise((resolve) => {
|
|
386
|
+
testProc.on('close', (code) => resolve(code === 0));
|
|
387
|
+
testProc.on('error', () => resolve(false));
|
|
388
|
+
setTimeout(() => resolve(false), 5000);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (works) {
|
|
392
|
+
console.log('ā
Verification successful!');
|
|
393
|
+
console.log('ā
Cloudflare ready to use\n');
|
|
394
|
+
return binaryPath;
|
|
395
|
+
} else {
|
|
396
|
+
console.error('ā Downloaded binary not working properly');
|
|
397
|
+
safeUnlink(binaryPath);
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.error('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
403
|
+
console.error('ā ā Installation Failed ā');
|
|
404
|
+
console.error('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
405
|
+
console.error(`Reason: ${err.message}\n`);
|
|
406
|
+
|
|
407
|
+
if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
|
|
408
|
+
showPermissionSolutions(path.dirname(binaryPath));
|
|
409
|
+
} else {
|
|
410
|
+
console.log('š” Troubleshooting:');
|
|
411
|
+
console.log(' 1. Check internet connection');
|
|
412
|
+
console.log(' 2. Check firewall/antivirus settings');
|
|
413
|
+
console.log(' 3. Try running as administrator');
|
|
414
|
+
console.log(' 4. Install manually: https://github.com/cloudflare/cloudflared/releases\n');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log('š DevTunnel will use fallback tunnels (Ngrok/LocalTunnel)\n');
|
|
418
|
+
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Check if bundled cloudflared exists and is working
|
|
424
|
+
export function hasBundledCloudflared() {
|
|
425
|
+
const binaryPath = getBinaryPath();
|
|
426
|
+
return fs.existsSync(binaryPath);
|
|
427
|
+
}
|