nport 1.0.5 â 2.0.1
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 +228 -33
- package/bin-manager.js +378 -0
- package/index.js +589 -0
- package/package.json +64 -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/README.md
CHANGED
|
@@ -1,68 +1,263 @@
|
|
|
1
1
|
# <img src="https://nport.link/assets/imgs/nport-logo.png" height="30" style="vertical-align: middle;"> NPort
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> đ Free & open source ngrok alternative - Tunnel localhost to the internet via Cloudflare Edge
|
|
4
4
|
|
|
5
5
|
[](https://github.com/tuanngocptn/nport)
|
|
6
6
|
[](https://www.npmjs.com/package/nport)
|
|
7
7
|
[](https://nport.link)
|
|
8
|
+
[](LICENSE)
|
|
8
9
|
|
|
9
10
|
## What is NPort?
|
|
10
11
|
|
|
11
|
-
](https://nport.link)
|
|
12
13
|
|
|
13
|
-
NPort
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
14
|
+
NPort is a powerful, lightweight **ngrok alternative** that creates secure HTTP/HTTPS tunnels from your localhost to public URLs using **Cloudflare's global edge network**. No configuration, no accounts, just instant tunnels with custom subdomains!
|
|
15
|
+
|
|
16
|
+
Perfect for:
|
|
17
|
+
- đ **Development environments** - Share your local work instantly
|
|
18
|
+
- đ **Testing webhooks** - Receive webhooks from GitHub, Stripe, PayPal, etc.
|
|
19
|
+
- đą **Mobile testing** - Test your web app on real devices
|
|
20
|
+
- đ ī¸ **API development** - Debug integrations with external services
|
|
21
|
+
- đĨ **Demo to clients** - Show your progress without deployment
|
|
17
22
|
|
|
18
23
|
## ⨠Features
|
|
19
24
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
+
- ⥠**Instant Setup**: One command to expose your localhost
|
|
26
|
+
- đ **Custom Subdomains**: Choose your own URL (e.g., `myapp.nport.link`)
|
|
27
|
+
- đ **Automatic HTTPS**: SSL/TLS encryption via Cloudflare
|
|
28
|
+
- đ **Global Edge Network**: Fast connections worldwide via Cloudflare
|
|
29
|
+
- đĄ **WebSocket Support**: Full WebSocket and Server-Sent Events support
|
|
30
|
+
- đ¯ **No Configuration**: Works out of the box
|
|
31
|
+
- đģ **Cross-Platform**: Windows, macOS, and Linux support
|
|
32
|
+
- đ **100% Free**: No accounts, no limits, no paywalls
|
|
33
|
+
- đ **Open Source**: MIT licensed
|
|
25
34
|
|
|
26
35
|
## đĻ Installation
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
# Local installation
|
|
31
|
-
npm install nport
|
|
37
|
+
### NPM (Recommended)
|
|
32
38
|
|
|
39
|
+
```bash
|
|
33
40
|
# Global installation
|
|
34
41
|
npm install -g nport
|
|
42
|
+
|
|
43
|
+
# Or use npx without installation
|
|
44
|
+
npx nport 3000 -s myapp
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
# Local installation
|
|
40
|
-
npm install git+https://github.com/tuanngocptn/nport.git
|
|
47
|
+
### From GitHub
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
```bash
|
|
43
50
|
npm install -g git+https://github.com/tuanngocptn/nport.git
|
|
44
51
|
```
|
|
45
52
|
|
|
46
53
|
## đ Quick Start
|
|
47
54
|
|
|
48
55
|
### Basic Usage
|
|
56
|
+
|
|
57
|
+
Expose port 3000 with a random subdomain:
|
|
49
58
|
```bash
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
nport 3000
|
|
60
|
+
```
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
nport -s myapp -p 3000
|
|
62
|
+
Output:
|
|
55
63
|
```
|
|
56
|
-
|
|
64
|
+
ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
65
|
+
NPort - Free & Open Source ngrok Alternative
|
|
66
|
+
ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
67
|
+
đ Website: https://nport.link
|
|
68
|
+
đĻ NPM: https://www.npmjs.com/package/nport
|
|
69
|
+
đģ GitHub: https://github.com/tuanngocptn/nport
|
|
70
|
+
â Support: https://buymeacoffee.com/tuanngocptn
|
|
71
|
+
ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
57
72
|
|
|
58
|
-
|
|
73
|
+
đ Starting Tunnel for port 3000...
|
|
74
|
+
â Tunnel created!
|
|
75
|
+
đ Public URL: https://user-1234.nport.link
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Custom Subdomain
|
|
79
|
+
|
|
80
|
+
Choose your own subdomain:
|
|
81
|
+
```bash
|
|
82
|
+
nport 3000 -s myapp
|
|
83
|
+
# Creates: https://myapp.nport.link
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or using long form:
|
|
59
87
|
```bash
|
|
60
|
-
|
|
61
|
-
npx nport --server https://server.nport.link \
|
|
62
|
-
--subdomain myapp \
|
|
63
|
-
--hostname 127.0.0.1 \
|
|
64
|
-
--port 3000
|
|
88
|
+
nport 3000 --subdomain myapp
|
|
65
89
|
```
|
|
66
90
|
|
|
67
|
-
##
|
|
68
|
-
|
|
91
|
+
## đ Usage Examples
|
|
92
|
+
|
|
93
|
+
### Web Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Next.js
|
|
97
|
+
npm run dev
|
|
98
|
+
nport 3000 -s my-nextjs-app
|
|
99
|
+
|
|
100
|
+
# React (Create React App)
|
|
101
|
+
npm start
|
|
102
|
+
nport 3000 -s my-react-app
|
|
103
|
+
|
|
104
|
+
# Vue.js
|
|
105
|
+
npm run dev
|
|
106
|
+
nport 8080 -s my-vue-app
|
|
107
|
+
|
|
108
|
+
# Express.js
|
|
109
|
+
node server.js
|
|
110
|
+
nport 3000 -s my-api
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Webhook Testing
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Start your local webhook receiver
|
|
117
|
+
node webhook-receiver.js
|
|
118
|
+
|
|
119
|
+
# Expose it to the internet
|
|
120
|
+
nport 4000 -s my-webhooks
|
|
121
|
+
|
|
122
|
+
# Use in GitHub webhook settings:
|
|
123
|
+
# https://my-webhooks.nport.link/webhook
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Mobile Device Testing
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Start your local dev server
|
|
130
|
+
npm run dev
|
|
131
|
+
|
|
132
|
+
# Create tunnel
|
|
133
|
+
nport 3000 -s mobile-test
|
|
134
|
+
|
|
135
|
+
# Open on your phone:
|
|
136
|
+
# https://mobile-test.nport.link
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## đ¯ CLI Options
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
nport <port> [options]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
| Option | Short | Description | Example |
|
|
146
|
+
|--------|-------|-------------|---------|
|
|
147
|
+
| `<port>` | - | Local port to tunnel (default: 8080) | `nport 3000` |
|
|
148
|
+
| `--subdomain` | `-s` | Custom subdomain | `nport 3000 -s myapp` |
|
|
149
|
+
| `-s=value` | - | Alternative format | `nport 3000 -s=myapp` |
|
|
150
|
+
|
|
151
|
+
## đ§ How It Works
|
|
152
|
+
|
|
153
|
+
1. **You run** `nport 3000 -s myapp`
|
|
154
|
+
2. **NPort creates** a Cloudflare Tunnel
|
|
155
|
+
3. **DNS record** is created: `myapp.nport.link` â Cloudflare Edge
|
|
156
|
+
4. **Cloudflared binary** connects your localhost:3000 to Cloudflare
|
|
157
|
+
5. **Traffic flows** through Cloudflare's global network to your machine
|
|
158
|
+
6. **On exit** (Ctrl+C), tunnel and DNS are automatically cleaned up
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
Internet â Cloudflare Edge â Cloudflare Tunnel â Your localhost:3000
|
|
162
|
+
(https://myapp.nport.link)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## đĄī¸ Security
|
|
166
|
+
|
|
167
|
+
- **HTTPS by default**: All tunnels use SSL/TLS encryption
|
|
168
|
+
- **Cloudflare protection**: DDoS protection and security features
|
|
169
|
+
- **Automatic cleanup**: Tunnels are removed when you stop the process
|
|
170
|
+
- **No data logging**: We don't store or log your traffic
|
|
171
|
+
|
|
172
|
+
## đ Comparison with ngrok
|
|
173
|
+
|
|
174
|
+
| Feature | NPort | ngrok |
|
|
175
|
+
|---------|-------|-------|
|
|
176
|
+
| Price | 100% Free | Free tier limited |
|
|
177
|
+
| Custom subdomains | â
Always | â Paid only |
|
|
178
|
+
| HTTPS | â
Always | â
|
|
|
179
|
+
| Account required | â No | â
Yes |
|
|
180
|
+
| Time limits | â None | â ī¸ Free tier limited |
|
|
181
|
+
| Open source | â
MIT | â Proprietary |
|
|
182
|
+
| Global network | â
Cloudflare | â
ngrok Edge |
|
|
183
|
+
|
|
184
|
+
## đ§š Cleanup
|
|
185
|
+
|
|
186
|
+
NPort automatically cleans up resources when you:
|
|
187
|
+
- Press **Ctrl+C** to exit
|
|
188
|
+
- Kill the process
|
|
189
|
+
- Terminal closes
|
|
190
|
+
|
|
191
|
+
The cleanup process:
|
|
192
|
+
1. â
Deletes DNS record (`myapp.nport.link`)
|
|
193
|
+
2. â
Removes Cloudflare Tunnel
|
|
194
|
+
3. â
Stops cloudflared process
|
|
195
|
+
|
|
196
|
+
## đ Troubleshooting
|
|
197
|
+
|
|
198
|
+
### Binary not found
|
|
199
|
+
|
|
200
|
+
If you see "Cloudflared binary not found":
|
|
201
|
+
```bash
|
|
202
|
+
npm install -g nport --force
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Port already in use
|
|
206
|
+
|
|
207
|
+
Make sure your local server is running on the specified port:
|
|
208
|
+
```bash
|
|
209
|
+
# Check if something is listening on port 3000
|
|
210
|
+
lsof -i :3000 # macOS/Linux
|
|
211
|
+
netstat -ano | findstr :3000 # Windows
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Subdomain already taken
|
|
215
|
+
|
|
216
|
+
Choose a different subdomain name:
|
|
217
|
+
```bash
|
|
218
|
+
nport 3000 -s myapp-v2
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Connection issues
|
|
222
|
+
|
|
223
|
+
The `ERR Cannot determine default origin certificate path` warning is harmless and can be ignored. It appears because cloudflared checks for certificate-based authentication (we use token-based instead).
|
|
224
|
+
|
|
225
|
+
## đ¤ Contributing
|
|
226
|
+
|
|
227
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
228
|
+
|
|
229
|
+
1. Fork the repository
|
|
230
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
231
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
232
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
233
|
+
5. Open a Pull Request
|
|
234
|
+
|
|
235
|
+
## đ Support
|
|
236
|
+
|
|
237
|
+
If you find NPort useful, please consider supporting the project:
|
|
238
|
+
|
|
239
|
+
- â [Star on GitHub](https://github.com/tuanngocptn/nport)
|
|
240
|
+
- â [Buy me a coffee](https://buymeacoffee.com/tuanngocptn)
|
|
241
|
+
- đŦ Share with your friends and colleagues
|
|
242
|
+
- đ [Report bugs](https://github.com/tuanngocptn/nport/issues)
|
|
243
|
+
|
|
244
|
+
## đ License
|
|
245
|
+
|
|
246
|
+
[MIT License](LICENSE) - Feel free to use NPort in your projects!
|
|
247
|
+
|
|
248
|
+
## đ Credits
|
|
249
|
+
|
|
250
|
+
- Created by [Nick Pham](https://github.com/tuanngocptn) đģđŗ
|
|
251
|
+
- Powered by [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/)
|
|
252
|
+
- Inspired by [ngrok](https://ngrok.com) and [localtunnel](https://github.com/localtunnel/localtunnel)
|
|
253
|
+
|
|
254
|
+
## đ Links
|
|
255
|
+
|
|
256
|
+
- đ Website: [https://nport.link](https://nport.link)
|
|
257
|
+
- đĻ NPM: [https://www.npmjs.com/package/nport](https://www.npmjs.com/package/nport)
|
|
258
|
+
- đģ GitHub: [https://github.com/tuanngocptn/nport](https://github.com/tuanngocptn/nport)
|
|
259
|
+
- đ§ Email: tuanngocptn@gmail.com
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
Made with â¤ī¸ by [Nick Pham](https://github.com/tuanngocptn)
|
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
|
+
}
|