termbeam 1.2.6 → 1.2.8
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 +6 -4
- package/package.json +1 -1
- package/src/auth.js +16 -1
- package/src/cli.js +24 -7
- package/src/routes.js +2 -2
- package/src/server.js +15 -10
package/README.md
CHANGED
|
@@ -104,27 +104,29 @@ Persisted tunnels save a tunnel ID to `~/.termbeam/tunnel.json` so the URL stays
|
|
|
104
104
|
```bash
|
|
105
105
|
termbeam [shell] [args...] # start with a specific shell (default: auto-detect)
|
|
106
106
|
termbeam --port 8080 # custom port (default: 3456)
|
|
107
|
-
termbeam --host
|
|
107
|
+
termbeam --host 0.0.0.0 # allow LAN access (default: 127.0.0.1)
|
|
108
|
+
termbeam --lan # shortcut for --host 0.0.0.0
|
|
108
109
|
```
|
|
109
110
|
|
|
110
111
|
| Flag | Description | Default |
|
|
111
112
|
| --------------------- | ---------------------------------------------------- | -------------- |
|
|
112
113
|
| `--password <pw>` | Set access password (also accepts `--password=<pw>`) | Auto-generated |
|
|
113
|
-
| `--no-password` | Disable password
|
|
114
|
+
| `--no-password` | Disable password (cannot combine with `--public`) | — |
|
|
114
115
|
| `--generate-password` | Auto-generate a secure password | On |
|
|
115
116
|
| `--tunnel` | Create an ephemeral devtunnel URL (private) | On |
|
|
116
117
|
| `--no-tunnel` | Disable tunnel (LAN-only) | — |
|
|
117
118
|
| `--persisted-tunnel` | Create a reusable devtunnel URL | Off |
|
|
118
119
|
| `--public` | Allow public tunnel access | Off |
|
|
119
120
|
| `--port <port>` | Server port | `3456` |
|
|
120
|
-
| `--host <addr>` | Bind address | `
|
|
121
|
+
| `--host <addr>` | Bind address | `127.0.0.1` |
|
|
122
|
+
| `--lan` | Bind to all interfaces (LAN access) | Off |
|
|
121
123
|
| `--log-level <level>` | Log verbosity (error/warn/info/debug) | `info` |
|
|
122
124
|
|
|
123
125
|
Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `TERMBEAM_LOG_LEVEL`, `SHELL` (Unix fallback), `COMSPEC` (Windows fallback). See [Configuration docs](https://dorlugasigal.github.io/TermBeam/configuration/).
|
|
124
126
|
|
|
125
127
|
## Security
|
|
126
128
|
|
|
127
|
-
TermBeam auto-generates a password and creates a tunnel by default, so your terminal is protected out of the box.
|
|
129
|
+
TermBeam auto-generates a password and creates a tunnel by default, so your terminal is protected out of the box. By default, the server binds to `127.0.0.1` (localhost only). Use `--lan` or `--host 0.0.0.0` to allow LAN access, or `--no-tunnel` to disable the tunnel.
|
|
128
130
|
|
|
129
131
|
Auth uses secure httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, and security headers (X-Frame-Options, X-Content-Type-Options, etc.) are set on all responses. The QR code on startup embeds a share token for password-free login — the token is reusable within its 5-minute validity window, which handles tunnel proxy retries and link preview services. API clients that can't use cookies can authenticate with an `Authorization: Bearer <password>` header. See the [Security Guide](https://dorlugasigal.github.io/TermBeam/security/) for more.
|
|
130
132
|
|
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -129,7 +129,22 @@ function createAuth(password) {
|
|
|
129
129
|
if (!password) return next();
|
|
130
130
|
if (req.cookies.pty_token && validateToken(req.cookies.pty_token)) return next();
|
|
131
131
|
const authHeader = req.headers.authorization;
|
|
132
|
-
if (authHeader
|
|
132
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
133
|
+
const ip = req.ip || req.socket.remoteAddress;
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const window = 60 * 1000;
|
|
136
|
+
const maxAttempts = 5;
|
|
137
|
+
const attempts = authAttempts.get(ip) || [];
|
|
138
|
+
const recent = attempts.filter((t) => now - t < window);
|
|
139
|
+
if (recent.length >= maxAttempts) {
|
|
140
|
+
log.warn(`Auth: rate limit exceeded for ${ip}`);
|
|
141
|
+
return res.status(429).json({ error: 'Too many attempts. Try again later.' });
|
|
142
|
+
}
|
|
143
|
+
if (authHeader === `Bearer ${password}`) return next();
|
|
144
|
+
recent.push(now);
|
|
145
|
+
authAttempts.set(ip, recent);
|
|
146
|
+
return res.status(401).json({ error: 'unauthorized' });
|
|
147
|
+
}
|
|
133
148
|
if (req.accepts('html')) return res.redirect('/login');
|
|
134
149
|
res.status(401).json({ error: 'unauthorized' });
|
|
135
150
|
}
|
package/src/cli.js
CHANGED
|
@@ -20,7 +20,8 @@ Options:
|
|
|
20
20
|
--persisted-tunnel Create a reusable devtunnel URL (stable across restarts)
|
|
21
21
|
--public Allow public tunnel access (default: private, owner-only)
|
|
22
22
|
--port <port> Set port (default: 3456, or PORT env var)
|
|
23
|
-
--host <addr> Bind address (default:
|
|
23
|
+
--host <addr> Bind address (default: 127.0.0.1)
|
|
24
|
+
--lan Bind to 0.0.0.0 (allow LAN access, default: localhost only)
|
|
24
25
|
--log-level <level> Set log verbosity: error, warn, info, debug (default: info)
|
|
25
26
|
-h, --help Show this help
|
|
26
27
|
-v, --version Show version
|
|
@@ -206,7 +207,7 @@ function getDefaultShell() {
|
|
|
206
207
|
|
|
207
208
|
function parseArgs() {
|
|
208
209
|
let port = parseInt(process.env.PORT || '3456', 10);
|
|
209
|
-
let host = '
|
|
210
|
+
let host = '127.0.0.1';
|
|
210
211
|
|
|
211
212
|
// Resolve log level early (env + args) so shell detection logs are visible
|
|
212
213
|
let logLevel = process.env.TERMBEAM_LOG_LEVEL || 'info';
|
|
@@ -230,7 +231,7 @@ function parseArgs() {
|
|
|
230
231
|
let useTunnel = true;
|
|
231
232
|
let noTunnel = false;
|
|
232
233
|
let persistedTunnel = false;
|
|
233
|
-
let
|
|
234
|
+
let publicTunnel = false;
|
|
234
235
|
let explicitPassword = !!password;
|
|
235
236
|
|
|
236
237
|
const args = process.argv.slice(2);
|
|
@@ -248,7 +249,7 @@ function parseArgs() {
|
|
|
248
249
|
useTunnel = true;
|
|
249
250
|
persistedTunnel = true;
|
|
250
251
|
} else if (args[i] === '--public') {
|
|
251
|
-
|
|
252
|
+
publicTunnel = true;
|
|
252
253
|
} else if (args[i].startsWith('--password=')) {
|
|
253
254
|
password = args[i].split('=')[1];
|
|
254
255
|
explicitPassword = true;
|
|
@@ -267,6 +268,8 @@ function parseArgs() {
|
|
|
267
268
|
explicitPassword = true;
|
|
268
269
|
} else if (args[i] === '--port' && args[i + 1]) {
|
|
269
270
|
port = parseInt(args[++i], 10);
|
|
271
|
+
} else if (args[i] === '--lan') {
|
|
272
|
+
host = '0.0.0.0';
|
|
270
273
|
} else if (args[i] === '--host' && args[i + 1]) {
|
|
271
274
|
host = args[++i];
|
|
272
275
|
} else if (args[i] === '--log-level' && args[i + 1]) {
|
|
@@ -285,8 +288,22 @@ function parseArgs() {
|
|
|
285
288
|
if (noTunnel) useTunnel = false;
|
|
286
289
|
|
|
287
290
|
// --public requires a tunnel
|
|
288
|
-
if (
|
|
289
|
-
|
|
291
|
+
if (publicTunnel && !useTunnel) {
|
|
292
|
+
const rd = '\x1b[31m';
|
|
293
|
+
const rs = '\x1b[0m';
|
|
294
|
+
console.error(
|
|
295
|
+
`${rd}Error: --public requires a tunnel. Remove --no-tunnel or remove --public.${rs}`,
|
|
296
|
+
);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --public requires password authentication
|
|
301
|
+
if (publicTunnel && !password) {
|
|
302
|
+
const rd = '\x1b[31m';
|
|
303
|
+
const rs = '\x1b[0m';
|
|
304
|
+
console.error(
|
|
305
|
+
`${rd}Error: Public tunnels require password authentication. Remove --no-password or remove --public.${rs}`,
|
|
306
|
+
);
|
|
290
307
|
process.exit(1);
|
|
291
308
|
}
|
|
292
309
|
|
|
@@ -302,7 +319,7 @@ function parseArgs() {
|
|
|
302
319
|
password,
|
|
303
320
|
useTunnel,
|
|
304
321
|
persistedTunnel,
|
|
305
|
-
|
|
322
|
+
publicTunnel,
|
|
306
323
|
shell,
|
|
307
324
|
shellArgs,
|
|
308
325
|
cwd,
|
package/src/routes.js
CHANGED
|
@@ -28,7 +28,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
28
28
|
httpOnly: true,
|
|
29
29
|
sameSite: 'lax',
|
|
30
30
|
maxAge: 24 * 60 * 60 * 1000,
|
|
31
|
-
secure:
|
|
31
|
+
secure: req.secure,
|
|
32
32
|
});
|
|
33
33
|
log.info(`Auth: login success from ${req.ip}`);
|
|
34
34
|
res.json({ ok: true });
|
|
@@ -58,7 +58,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
58
58
|
httpOnly: true,
|
|
59
59
|
sameSite: 'lax',
|
|
60
60
|
maxAge: 24 * 60 * 60 * 1000,
|
|
61
|
-
secure:
|
|
61
|
+
secure: req.secure,
|
|
62
62
|
});
|
|
63
63
|
log.info(`Auth: share-token auto-login from ${req.ip}`);
|
|
64
64
|
// Redirect to the same path without ?ott= to keep the URL clean
|
package/src/server.js
CHANGED
|
@@ -27,10 +27,10 @@ function getLocalIP() {
|
|
|
27
27
|
return '127.0.0.1';
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function
|
|
30
|
+
function confirmPublicTunnel() {
|
|
31
31
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
32
|
return new Promise((resolve) => {
|
|
33
|
-
rl.question(' Do you want to continue with
|
|
33
|
+
rl.question(' Do you want to continue with public access? (y/N): ', (answer) => {
|
|
34
34
|
rl.close();
|
|
35
35
|
resolve(answer.trim().toLowerCase() === 'y');
|
|
36
36
|
});
|
|
@@ -110,8 +110,8 @@ function createTermBeamServer(overrides = {}) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
// Warn and require consent for
|
|
114
|
-
if (config.useTunnel && config.
|
|
113
|
+
// Warn and require consent for public tunnel access
|
|
114
|
+
if (config.useTunnel && config.publicTunnel) {
|
|
115
115
|
const rd = '\x1b[31m';
|
|
116
116
|
const yl = '\x1b[33m';
|
|
117
117
|
const rs = '\x1b[0m';
|
|
@@ -123,7 +123,7 @@ function createTermBeamServer(overrides = {}) {
|
|
|
123
123
|
console.log(` ${yl}No Microsoft login will be required to reach the tunnel.${rs}`);
|
|
124
124
|
console.log(` ${yl}Only the TermBeam password will protect your terminal.${rs}`);
|
|
125
125
|
console.log('');
|
|
126
|
-
const confirmed = await
|
|
126
|
+
const confirmed = await confirmPublicTunnel();
|
|
127
127
|
if (!confirmed) {
|
|
128
128
|
console.log('');
|
|
129
129
|
console.log(' Aborted. Restart without --public for private access.');
|
|
@@ -174,11 +174,13 @@ function createTermBeamServer(overrides = {}) {
|
|
|
174
174
|
const gn = '\x1b[38;5;114m'; // green
|
|
175
175
|
const dm = '\x1b[2m'; // dim
|
|
176
176
|
|
|
177
|
+
const bl = '\x1b[38;5;75m'; // light blue
|
|
178
|
+
|
|
177
179
|
let publicUrl = null;
|
|
178
180
|
if (config.useTunnel) {
|
|
179
181
|
const tunnel = await startTunnel(config.port, {
|
|
180
182
|
persisted: config.persistedTunnel,
|
|
181
|
-
anonymous: config.
|
|
183
|
+
anonymous: config.publicTunnel,
|
|
182
184
|
});
|
|
183
185
|
if (tunnel) {
|
|
184
186
|
publicUrl = tunnel.url;
|
|
@@ -191,10 +193,15 @@ function createTermBeamServer(overrides = {}) {
|
|
|
191
193
|
console.log(` Shell: ${config.shell}`);
|
|
192
194
|
console.log(` Session: ${defaultId}`);
|
|
193
195
|
console.log(` Auth: ${config.password ? `${gn}🔒 password${rs}` : '🔓 none'}`);
|
|
196
|
+
if (isLanReachable) {
|
|
197
|
+
console.log(` Bind: ${config.host} (LAN accessible)`);
|
|
198
|
+
} else {
|
|
199
|
+
console.log(` Bind: ${config.host} (localhost only)`);
|
|
200
|
+
}
|
|
194
201
|
console.log('');
|
|
195
202
|
|
|
196
203
|
if (publicUrl) {
|
|
197
|
-
console.log(`
|
|
204
|
+
console.log(` Public: ${bl}${publicUrl}${rs}`);
|
|
198
205
|
}
|
|
199
206
|
console.log(` Local: http://localhost:${config.port}`);
|
|
200
207
|
if (isLanReachable) {
|
|
@@ -205,8 +212,6 @@ function createTermBeamServer(overrides = {}) {
|
|
|
205
212
|
const qrDisplayUrl = qrUrl; // clean URL shown in console text
|
|
206
213
|
const qrCodeUrl = config.password ? `${qrUrl}?ott=${auth.generateShareToken()}` : qrUrl;
|
|
207
214
|
console.log('');
|
|
208
|
-
console.log(` ${dm}📋 Clipboard requires HTTPS — use the Public or localhost URL${rs}`);
|
|
209
|
-
console.log('');
|
|
210
215
|
try {
|
|
211
216
|
const qr = await QRCode.toString(qrCodeUrl, { type: 'terminal', small: true });
|
|
212
217
|
console.log(qr);
|
|
@@ -214,7 +219,7 @@ function createTermBeamServer(overrides = {}) {
|
|
|
214
219
|
/* ignore */
|
|
215
220
|
}
|
|
216
221
|
|
|
217
|
-
console.log(` Scan the QR code or open: ${qrDisplayUrl}`);
|
|
222
|
+
console.log(` Scan the QR code or open: ${bl}${qrDisplayUrl}${rs}`);
|
|
218
223
|
if (config.password) console.log(` Password: ${gn}${config.password}${rs}`);
|
|
219
224
|
console.log('');
|
|
220
225
|
|