cli-tunnel 1.2.0-beta.10 → 1.2.0-beta.11
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/index.js +36 -2
- package/package.json +1 -1
- package/remote-ui/app.js +20 -10
package/dist/index.js
CHANGED
|
@@ -228,7 +228,7 @@ setInterval(() => {
|
|
|
228
228
|
ticketRateLimits.delete(ip);
|
|
229
229
|
}
|
|
230
230
|
}, 60000);
|
|
231
|
-
const server = http.createServer((req, res) => {
|
|
231
|
+
const server = http.createServer(async (req, res) => {
|
|
232
232
|
const clientIp = req.socket.remoteAddress || 'unknown';
|
|
233
233
|
// F-8: Rate limiting for HTTP endpoints
|
|
234
234
|
if (req.url?.startsWith('/api/')) {
|
|
@@ -274,6 +274,40 @@ const server = http.createServer((req, res) => {
|
|
|
274
274
|
return;
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
+
// Hub ticket proxy — fetch ticket from local session on behalf of grid client
|
|
278
|
+
if (hubMode && req.url?.startsWith('/api/proxy/ticket/') && req.method === 'POST') {
|
|
279
|
+
const targetPort = parseInt(req.url.replace('/api/proxy/ticket/', ''), 10);
|
|
280
|
+
if (!Number.isFinite(targetPort) || targetPort < 1 || targetPort > 65535) {
|
|
281
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
282
|
+
res.end(JSON.stringify({ error: 'Invalid port' }));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Find token for this port from session files
|
|
286
|
+
const localSessions = readLocalSessions();
|
|
287
|
+
const session = localSessions.find(s => s.port === targetPort);
|
|
288
|
+
if (!session) {
|
|
289
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
290
|
+
res.end(JSON.stringify({ error: 'Session not found' }));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const ticketResp = await fetch(`http://127.0.0.1:${targetPort}/api/auth/ticket`, {
|
|
295
|
+
method: 'POST', headers: { 'Authorization': `Bearer ${session.token}` },
|
|
296
|
+
signal: AbortSignal.timeout(3000),
|
|
297
|
+
});
|
|
298
|
+
if (!ticketResp.ok)
|
|
299
|
+
throw new Error('Ticket request failed');
|
|
300
|
+
const ticketData = await ticketResp.json();
|
|
301
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
302
|
+
res.end(JSON.stringify({ ticket: ticketData.ticket, port: targetPort }));
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
306
|
+
res.end(JSON.stringify({ error: 'Session unreachable' }));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
277
311
|
// Sessions API
|
|
278
312
|
if ((req.url === '/api/sessions' || req.url?.startsWith('/api/sessions?')) && req.method === 'GET') {
|
|
279
313
|
try {
|
|
@@ -383,7 +417,7 @@ const server = http.createServer((req, res) => {
|
|
|
383
417
|
'Content-Type': mimes[ext] || 'application/octet-stream',
|
|
384
418
|
'X-Frame-Options': 'DENY',
|
|
385
419
|
'X-Content-Type-Options': 'nosniff',
|
|
386
|
-
'Content-Security-Policy': "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; connect-src 'self' ws://localhost:* wss://*.devtunnels.ms;",
|
|
420
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; connect-src 'self' ws://localhost:* ws://127.0.0.1:* http://127.0.0.1:* wss://*.devtunnels.ms https://*.devtunnels.ms;",
|
|
387
421
|
'Referrer-Policy': 'no-referrer',
|
|
388
422
|
'Cache-Control': 'no-store',
|
|
389
423
|
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
package/package.json
CHANGED
package/remote-ui/app.js
CHANGED
|
@@ -117,7 +117,9 @@
|
|
|
117
117
|
|
|
118
118
|
async function loadSessions() {
|
|
119
119
|
try {
|
|
120
|
-
const
|
|
120
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
121
|
+
const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
|
|
122
|
+
const resp = await fetch('/api/sessions', { headers });
|
|
121
123
|
const data = await resp.json();
|
|
122
124
|
renderDashboard(data.sessions || []);
|
|
123
125
|
} catch (err) {
|
|
@@ -187,17 +189,21 @@
|
|
|
187
189
|
};
|
|
188
190
|
|
|
189
191
|
window.cleanOffline = async () => {
|
|
190
|
-
const
|
|
192
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
193
|
+
const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
|
|
194
|
+
const resp = await fetch('/api/sessions', { headers });
|
|
191
195
|
const data = await resp.json();
|
|
192
196
|
const offline = (data.sessions || []).filter(s => !s.online);
|
|
193
197
|
for (const s of offline) {
|
|
194
|
-
await fetch('/api/sessions/' + s.id, { method: 'DELETE' });
|
|
198
|
+
await fetch('/api/sessions/' + s.id, { method: 'DELETE', headers });
|
|
195
199
|
}
|
|
196
200
|
loadSessions();
|
|
197
201
|
};
|
|
198
202
|
|
|
199
203
|
window.deleteSession = async (id) => {
|
|
200
|
-
|
|
204
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
205
|
+
const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
|
|
206
|
+
await fetch('/api/sessions/' + id, { method: 'DELETE', headers });
|
|
201
207
|
loadSessions();
|
|
202
208
|
};
|
|
203
209
|
|
|
@@ -265,20 +271,24 @@
|
|
|
265
271
|
// Delay fit to ensure container has size
|
|
266
272
|
setTimeout(function() { panelFit.fit(); }, 100);
|
|
267
273
|
|
|
268
|
-
// Connect WebSocket to this session
|
|
274
|
+
// Connect WebSocket to this session
|
|
269
275
|
var statusDot = header.querySelector('.grid-panel-status');
|
|
270
276
|
var panelWs = null;
|
|
271
277
|
|
|
272
278
|
(function connectPanel() {
|
|
273
|
-
|
|
274
|
-
|
|
279
|
+
// Use hub's proxy endpoint to get a ticket for the session
|
|
280
|
+
var tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
281
|
+
var proxyUrl = '/api/proxy/ticket/' + s.port;
|
|
282
|
+
var wsBase = s.isLocal ? 'ws://127.0.0.1:' + s.port : s.url.replace('https://', 'wss://');
|
|
283
|
+
|
|
284
|
+
fetch(proxyUrl, {
|
|
275
285
|
method: 'POST',
|
|
276
|
-
headers: { 'Authorization': 'Bearer ' +
|
|
286
|
+
headers: { 'Authorization': 'Bearer ' + tokenParam }
|
|
277
287
|
}).then(function(resp) {
|
|
278
288
|
if (!resp.ok) throw new Error('Auth failed');
|
|
279
289
|
return resp.json();
|
|
280
290
|
}).then(function(data) {
|
|
281
|
-
panelWs = new WebSocket(
|
|
291
|
+
panelWs = new WebSocket(wsBase + '?ticket=' + encodeURIComponent(data.ticket));
|
|
282
292
|
|
|
283
293
|
panelWs.onopen = function() {
|
|
284
294
|
if (statusDot) { statusDot.style.color = 'var(--green)'; statusDot.title = 'Connected'; }
|
|
@@ -731,7 +741,7 @@
|
|
|
731
741
|
// F-5: Event delegation for key-bar buttons (no inline onclick)
|
|
732
742
|
const keyBar = document.getElementById('key-bar');
|
|
733
743
|
if (keyBar) {
|
|
734
|
-
|
|
744
|
+
var keyMap = {
|
|
735
745
|
'\\x1b[A': '\x1b[A', '\\x1b[B': '\x1b[B', '\\x1b[C': '\x1b[C', '\\x1b[D': '\x1b[D',
|
|
736
746
|
'\\t': '\t', '\\r': '\r', '\\x1b': '\x1b', '\\x03': '\x03', ' ': ' ', '\\x7f': '\x7f',
|
|
737
747
|
};
|