nport 1.0.6 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +227 -32
- package/analytics.js +259 -0
- package/bin-manager.js +378 -0
- package/index.js +635 -0
- package/package.json +65 -33
- package/LICENSE +0 -21
- package/bin/client +0 -43
- package/bin/server +0 -28
- package/client.js +0 -69
- package/examples/api.js +0 -9
- package/lib/api.js +0 -22
- package/nginx.conf.sample +0 -33
- package/server.js +0 -200
package/bin-manager.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import https from "https";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Configuration & Constants
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
// Fix for __dirname in ES modules
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Binary configuration
|
|
17
|
+
const BIN_DIR = path.join(__dirname, "bin");
|
|
18
|
+
const BINARY_NAME = "cloudflared";
|
|
19
|
+
const COMPRESSED_SUFFIX = ".tgz";
|
|
20
|
+
const TEMP_ARCHIVE_NAME = "cloudflared.tgz";
|
|
21
|
+
|
|
22
|
+
// Platform detection
|
|
23
|
+
const PLATFORM = os.platform();
|
|
24
|
+
const ARCH = os.arch();
|
|
25
|
+
const IS_WINDOWS = PLATFORM === "win32";
|
|
26
|
+
const IS_MACOS = PLATFORM === "darwin";
|
|
27
|
+
const IS_LINUX = PLATFORM === "linux";
|
|
28
|
+
|
|
29
|
+
// Binary paths
|
|
30
|
+
const BIN_NAME = IS_WINDOWS ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
31
|
+
const BIN_PATH = path.join(BIN_DIR, BIN_NAME);
|
|
32
|
+
|
|
33
|
+
// Download configuration
|
|
34
|
+
const GITHUB_BASE_URL = "https://github.com/cloudflare/cloudflared/releases/latest/download";
|
|
35
|
+
const REDIRECT_CODES = [301, 302];
|
|
36
|
+
const SUCCESS_CODE = 200;
|
|
37
|
+
const UNIX_EXECUTABLE_MODE = "755";
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Platform Detection
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Platform and architecture mapping for cloudflared releases
|
|
45
|
+
*/
|
|
46
|
+
const PLATFORM_MAPPINGS = {
|
|
47
|
+
darwin: {
|
|
48
|
+
amd64: "cloudflared-darwin-amd64.tgz",
|
|
49
|
+
arm64: "cloudflared-darwin-amd64.tgz", // macOS uses universal binary
|
|
50
|
+
},
|
|
51
|
+
win32: {
|
|
52
|
+
x64: "cloudflared-windows-amd64.exe",
|
|
53
|
+
ia32: "cloudflared-windows-386.exe",
|
|
54
|
+
},
|
|
55
|
+
linux: {
|
|
56
|
+
x64: "cloudflared-linux-amd64",
|
|
57
|
+
arm64: "cloudflared-linux-arm64",
|
|
58
|
+
arm: "cloudflared-linux-arm",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalizes architecture name for mapping lookup
|
|
64
|
+
* @param {string} arch - Raw architecture from os.arch()
|
|
65
|
+
* @returns {string} Normalized architecture name
|
|
66
|
+
*/
|
|
67
|
+
function normalizeArch(arch) {
|
|
68
|
+
const archMap = {
|
|
69
|
+
x64: "x64",
|
|
70
|
+
amd64: "amd64",
|
|
71
|
+
arm64: "arm64",
|
|
72
|
+
ia32: "ia32",
|
|
73
|
+
arm: "arm",
|
|
74
|
+
};
|
|
75
|
+
return archMap[arch] || arch;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Determines the download URL based on current platform and architecture
|
|
80
|
+
* @returns {string} Download URL for cloudflared binary
|
|
81
|
+
* @throws {Error} If platform/architecture combination is not supported
|
|
82
|
+
*/
|
|
83
|
+
function getDownloadUrl() {
|
|
84
|
+
const normalizedArch = normalizeArch(ARCH);
|
|
85
|
+
const platformMapping = PLATFORM_MAPPINGS[PLATFORM];
|
|
86
|
+
|
|
87
|
+
if (!platformMapping) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Unsupported platform: ${PLATFORM}. Supported platforms: darwin, win32, linux`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const binaryName = platformMapping[normalizedArch];
|
|
94
|
+
|
|
95
|
+
if (!binaryName) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Unsupported architecture: ${ARCH} for platform ${PLATFORM}. ` +
|
|
98
|
+
`Supported architectures: ${Object.keys(platformMapping).join(", ")}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return `${GITHUB_BASE_URL}/${binaryName}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Checks if the download URL points to a compressed archive
|
|
107
|
+
* @param {string} url - Download URL
|
|
108
|
+
* @returns {boolean} True if URL is for a compressed file
|
|
109
|
+
*/
|
|
110
|
+
function isCompressedArchive(url) {
|
|
111
|
+
return url.endsWith(COMPRESSED_SUFFIX);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// File System Utilities
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Ensures directory exists, creates it if it doesn't
|
|
120
|
+
* @param {string} dirPath - Directory path to ensure
|
|
121
|
+
*/
|
|
122
|
+
function ensureDirectory(dirPath) {
|
|
123
|
+
if (!fs.existsSync(dirPath)) {
|
|
124
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Safely removes a file if it exists
|
|
130
|
+
* @param {string} filePath - Path to file to remove
|
|
131
|
+
*/
|
|
132
|
+
function safeUnlink(filePath) {
|
|
133
|
+
try {
|
|
134
|
+
if (fs.existsSync(filePath)) {
|
|
135
|
+
fs.unlinkSync(filePath);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
// Ignore errors during cleanup
|
|
139
|
+
console.warn(`Warning: Could not remove ${filePath}:`, err.message);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Sets executable permissions on Unix-like systems
|
|
145
|
+
* @param {string} filePath - Path to file
|
|
146
|
+
* @param {string} mode - Permission mode (e.g., "755")
|
|
147
|
+
*/
|
|
148
|
+
function setExecutablePermissions(filePath, mode = UNIX_EXECUTABLE_MODE) {
|
|
149
|
+
if (!IS_WINDOWS) {
|
|
150
|
+
fs.chmodSync(filePath, mode);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validates that a file exists at the given path
|
|
156
|
+
* @param {string} filePath - Path to validate
|
|
157
|
+
* @param {string} errorMessage - Error message if file doesn't exist
|
|
158
|
+
* @throws {Error} If file doesn't exist
|
|
159
|
+
*/
|
|
160
|
+
function validateFileExists(filePath, errorMessage) {
|
|
161
|
+
if (!fs.existsSync(filePath)) {
|
|
162
|
+
throw new Error(errorMessage || `File not found: ${filePath}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Download Utilities
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Downloads a file from a URL with automatic redirect handling
|
|
172
|
+
* @param {string} url - URL to download from
|
|
173
|
+
* @param {string} dest - Destination file path
|
|
174
|
+
* @returns {Promise<string>} Resolves with destination path on success
|
|
175
|
+
*/
|
|
176
|
+
async function downloadFile(url, dest) {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const file = fs.createWriteStream(dest);
|
|
179
|
+
|
|
180
|
+
https
|
|
181
|
+
.get(url, (response) => {
|
|
182
|
+
// Handle redirects
|
|
183
|
+
if (REDIRECT_CODES.includes(response.statusCode)) {
|
|
184
|
+
file.close();
|
|
185
|
+
safeUnlink(dest);
|
|
186
|
+
downloadFile(response.headers.location, dest)
|
|
187
|
+
.then(resolve)
|
|
188
|
+
.catch(reject);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle non-success status codes
|
|
193
|
+
if (response.statusCode !== SUCCESS_CODE) {
|
|
194
|
+
file.close();
|
|
195
|
+
safeUnlink(dest);
|
|
196
|
+
reject(
|
|
197
|
+
new Error(
|
|
198
|
+
`Download failed with status code ${response.statusCode} from ${url}`
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Stream response to file
|
|
205
|
+
response.pipe(file);
|
|
206
|
+
|
|
207
|
+
file.on("finish", () => {
|
|
208
|
+
file.close(() => resolve(dest));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
file.on("error", (err) => {
|
|
212
|
+
file.close();
|
|
213
|
+
safeUnlink(dest);
|
|
214
|
+
reject(err);
|
|
215
|
+
});
|
|
216
|
+
})
|
|
217
|
+
.on("error", (err) => {
|
|
218
|
+
file.close();
|
|
219
|
+
safeUnlink(dest);
|
|
220
|
+
reject(new Error(`Network error: ${err.message}`));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// Archive Extraction
|
|
227
|
+
// ============================================================================
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extracts a .tgz archive to a directory
|
|
231
|
+
* @param {string} archivePath - Path to .tgz file
|
|
232
|
+
* @param {string} targetDir - Directory to extract to
|
|
233
|
+
* @throws {Error} If extraction fails
|
|
234
|
+
*/
|
|
235
|
+
function extractTarGz(archivePath, targetDir) {
|
|
236
|
+
try {
|
|
237
|
+
execSync(`tar -xzf "${archivePath}" -C "${targetDir}"`, {
|
|
238
|
+
stdio: "pipe", // Suppress output
|
|
239
|
+
});
|
|
240
|
+
} catch (err) {
|
|
241
|
+
throw new Error(`Extraction failed: ${err.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ============================================================================
|
|
246
|
+
// Logging Utilities
|
|
247
|
+
// ============================================================================
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Console logging with consistent formatting
|
|
251
|
+
*/
|
|
252
|
+
const logger = {
|
|
253
|
+
info: (msg) => console.log(`ℹ️ ${msg}`),
|
|
254
|
+
success: (msg) => console.log(`✅ ${msg}`),
|
|
255
|
+
warn: (msg) => console.warn(`⚠️ ${msg}`),
|
|
256
|
+
error: (msg) => console.error(`❌ ${msg}`),
|
|
257
|
+
progress: (msg) => console.log(`🚧 ${msg}`),
|
|
258
|
+
extract: (msg) => console.log(`📦 ${msg}`),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Core Binary Management
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Downloads and installs the cloudflared binary
|
|
267
|
+
* @returns {Promise<string>} Path to installed binary
|
|
268
|
+
*/
|
|
269
|
+
async function installBinary() {
|
|
270
|
+
logger.progress(
|
|
271
|
+
"Cloudflared binary not found. Downloading... (This happens only once)"
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const url = getDownloadUrl();
|
|
275
|
+
const isArchive = isCompressedArchive(url);
|
|
276
|
+
const downloadDest = isArchive
|
|
277
|
+
? path.join(BIN_DIR, TEMP_ARCHIVE_NAME)
|
|
278
|
+
: BIN_PATH;
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Download binary or archive
|
|
282
|
+
await downloadFile(url, downloadDest);
|
|
283
|
+
|
|
284
|
+
// Extract if it's an archive (macOS)
|
|
285
|
+
if (isArchive) {
|
|
286
|
+
logger.extract("Extracting binary...");
|
|
287
|
+
extractTarGz(downloadDest, BIN_DIR);
|
|
288
|
+
|
|
289
|
+
// Clean up archive
|
|
290
|
+
safeUnlink(downloadDest);
|
|
291
|
+
|
|
292
|
+
// Validate extraction succeeded
|
|
293
|
+
validateFileExists(
|
|
294
|
+
BIN_PATH,
|
|
295
|
+
"Extraction failed: Binary not found after extraction"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Set executable permissions (Unix-like systems)
|
|
300
|
+
setExecutablePermissions(BIN_PATH);
|
|
301
|
+
|
|
302
|
+
logger.success("Download complete.");
|
|
303
|
+
return BIN_PATH;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
// Clean up any partial downloads
|
|
306
|
+
safeUnlink(downloadDest);
|
|
307
|
+
safeUnlink(BIN_PATH);
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Ensures cloudflared binary is available, downloading if necessary
|
|
314
|
+
* @returns {Promise<string>} Path to cloudflared binary
|
|
315
|
+
*/
|
|
316
|
+
export async function ensureCloudflared() {
|
|
317
|
+
// Ensure bin directory exists
|
|
318
|
+
ensureDirectory(BIN_DIR);
|
|
319
|
+
|
|
320
|
+
// Check if binary already exists
|
|
321
|
+
if (fs.existsSync(BIN_PATH)) {
|
|
322
|
+
// Always ensure permissions are set correctly (in case they were lost)
|
|
323
|
+
setExecutablePermissions(BIN_PATH);
|
|
324
|
+
return BIN_PATH;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Download and install
|
|
328
|
+
try {
|
|
329
|
+
return await installBinary();
|
|
330
|
+
} catch (error) {
|
|
331
|
+
logger.error(`Installation failed: ${error.message}`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// CLI Entry Point
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Checks if we're running in a CI environment
|
|
342
|
+
* @returns {boolean} True if in CI environment
|
|
343
|
+
*/
|
|
344
|
+
function isCI() {
|
|
345
|
+
return !!(
|
|
346
|
+
process.env.CI || // Generic CI flag
|
|
347
|
+
process.env.GITHUB_ACTIONS || // GitHub Actions
|
|
348
|
+
process.env.GITLAB_CI || // GitLab CI
|
|
349
|
+
process.env.CIRCLECI || // CircleCI
|
|
350
|
+
process.env.TRAVIS || // Travis CI
|
|
351
|
+
process.env.JENKINS_URL || // Jenkins
|
|
352
|
+
process.env.BUILDKITE // Buildkite
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Main function when run directly from command line
|
|
358
|
+
*/
|
|
359
|
+
async function main() {
|
|
360
|
+
// Skip binary download in CI environments to keep package lightweight
|
|
361
|
+
if (isCI()) {
|
|
362
|
+
logger.info("Running in CI environment - skipping binary download");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const binaryPath = await ensureCloudflared();
|
|
368
|
+
logger.success(`Cloudflared binary is ready at: ${binaryPath}`);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
logger.error(error.message);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Run if executed directly
|
|
376
|
+
if (process.argv[1] === __filename) {
|
|
377
|
+
main();
|
|
378
|
+
}
|