@tapflowio/relay 0.22.0 → 0.23.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/dist/RelayServer.d.ts +18 -0
- package/dist/RelayServer.d.ts.map +1 -1
- package/dist/RelayServer.js +110 -12
- package/dist/RelayServer.js.map +1 -1
- package/dist/SessionManager.d.ts +4 -1
- package/dist/SessionManager.d.ts.map +1 -1
- package/dist/SessionManager.js +7 -0
- package/dist/SessionManager.js.map +1 -1
- package/dist/api/auth.d.ts.map +1 -1
- package/dist/api/auth.js +2 -1
- package/dist/api/auth.js.map +1 -1
- package/dist/api/passwordReset.d.ts +6 -2
- package/dist/api/passwordReset.d.ts.map +1 -1
- package/dist/api/passwordReset.js +21 -6
- package/dist/api/passwordReset.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/clientAddress.d.ts +15 -0
- package/dist/lib/clientAddress.d.ts.map +1 -1
- package/dist/lib/clientAddress.js +21 -0
- package/dist/lib/clientAddress.js.map +1 -1
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +6 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/proxyConfig.d.ts +10 -0
- package/dist/lib/proxyConfig.d.ts.map +1 -1
- package/dist/lib/proxyConfig.js +14 -0
- package/dist/lib/proxyConfig.js.map +1 -1
- package/dist/server.js +3 -1
- package/dist/server.js.map +1 -1
- package/package.json +5 -5
- package/public/assets/AppCenter-OTDuqwSf.js +21 -0
- package/public/assets/AppCenter-OTDuqwSf.js.br +0 -0
- package/public/assets/AppCenter-OTDuqwSf.js.gz +0 -0
- package/public/assets/{Default-2rFTkbo9.js → Default-D9GLpWlN.js} +1 -1
- package/public/assets/Default-D9GLpWlN.js.br +0 -0
- package/public/assets/Default-D9GLpWlN.js.gz +0 -0
- package/public/assets/{Invite-aN4DXJyM.js → Invite-Bhiam8En.js} +1 -1
- package/public/assets/Invite-Bhiam8En.js.br +0 -0
- package/public/assets/Invite-Bhiam8En.js.gz +0 -0
- package/public/assets/{MacResources-D3IlesXU.js → MacResources-DObzVJxQ.js} +1 -1
- package/public/assets/MacResources-DObzVJxQ.js.br +0 -0
- package/public/assets/MacResources-DObzVJxQ.js.gz +0 -0
- package/public/assets/{NotFound-BoDDivK5.js → NotFound-Dz_k_Mzf.js} +1 -1
- package/public/assets/NotFound-Dz_k_Mzf.js.br +0 -0
- package/public/assets/NotFound-Dz_k_Mzf.js.gz +0 -0
- package/public/assets/QASession-2crubOLY.js +150 -0
- package/public/assets/QASession-2crubOLY.js.br +0 -0
- package/public/assets/QASession-2crubOLY.js.gz +0 -0
- package/public/assets/{ResetPassword-DPc5kwQU.js → ResetPassword-EUm12ojo.js} +1 -1
- package/public/assets/ResetPassword-EUm12ojo.js.br +0 -0
- package/public/assets/ResetPassword-EUm12ojo.js.gz +0 -0
- package/public/assets/{Setup-CwPx_e_O.js → Setup-Fc_0RW0a.js} +1 -1
- package/public/assets/Setup-Fc_0RW0a.js.br +0 -0
- package/public/assets/Setup-Fc_0RW0a.js.gz +0 -0
- package/public/assets/Team-Ac5aOGgx.js +6 -0
- package/public/assets/Team-Ac5aOGgx.js.br +0 -0
- package/public/assets/Team-Ac5aOGgx.js.gz +0 -0
- package/public/assets/{Tokens-DdMMke73.js → Tokens-pEnCy-7l.js} +1 -1
- package/public/assets/Tokens-pEnCy-7l.js.br +0 -0
- package/public/assets/Tokens-pEnCy-7l.js.gz +0 -0
- package/public/assets/alert-dialog-DhOTocMh.js +1 -0
- package/public/assets/alert-dialog-DhOTocMh.js.br +0 -0
- package/public/assets/alert-dialog-DhOTocMh.js.gz +0 -0
- package/public/assets/{badge-CMf8d5RJ.js → badge-B4gbViMs.js} +1 -1
- package/public/assets/badge-B4gbViMs.js.br +0 -0
- package/public/assets/badge-B4gbViMs.js.gz +0 -0
- package/public/assets/build-format-DKGajT96.js +11 -0
- package/public/assets/build-format-DKGajT96.js.br +0 -0
- package/public/assets/build-format-DKGajT96.js.gz +0 -0
- package/public/assets/dialog-DEL1xxKJ.js +11 -0
- package/public/assets/dialog-DEL1xxKJ.js.br +0 -0
- package/public/assets/dialog-DEL1xxKJ.js.gz +0 -0
- package/public/assets/index-COIlCLCW.js +218 -0
- package/public/assets/index-COIlCLCW.js.br +0 -0
- package/public/assets/index-COIlCLCW.js.gz +0 -0
- package/public/assets/pencil-CPaoEKFm.js +6 -0
- package/public/assets/pencil-CPaoEKFm.js.br +0 -0
- package/public/assets/pencil-CPaoEKFm.js.gz +0 -0
- package/public/assets/resource-health-Ca4-5Rde.js +1 -0
- package/public/assets/resource-health-Ca4-5Rde.js.br +0 -0
- package/public/assets/resource-health-Ca4-5Rde.js.gz +0 -0
- package/public/assets/{table-CL9AvoTp.js → table-CSeGVnii.js} +1 -1
- package/public/assets/table-CSeGVnii.js.br +0 -0
- package/public/assets/table-CSeGVnii.js.gz +0 -0
- package/public/assets/trash-CD9Y9G-i.js +11 -0
- package/public/assets/trash-CD9Y9G-i.js.br +0 -0
- package/public/assets/trash-CD9Y9G-i.js.gz +0 -0
- package/public/assets/{vendor-charts-8kVeHLwY.js → vendor-charts-YPhPkjLw.js} +1 -1
- package/public/assets/vendor-charts-YPhPkjLw.js.br +0 -0
- package/public/assets/vendor-charts-YPhPkjLw.js.gz +0 -0
- package/public/assets/vendor-react-C5H_1fOn.js +49 -0
- package/public/assets/vendor-react-C5H_1fOn.js.br +0 -0
- package/public/assets/vendor-react-C5H_1fOn.js.gz +0 -0
- package/public/assets/{vendor-router-BFibuab1.js → vendor-router-CnmrRwJF.js} +1 -1
- package/public/assets/vendor-router-CnmrRwJF.js.br +0 -0
- package/public/assets/vendor-router-CnmrRwJF.js.gz +0 -0
- package/public/index.html +3 -3
- package/public/index.html.br +0 -0
- package/public/index.html.gz +0 -0
- package/public/assets/AppCenter-gbsJscjW.js +0 -21
- package/public/assets/AppCenter-gbsJscjW.js.br +0 -0
- package/public/assets/AppCenter-gbsJscjW.js.gz +0 -0
- package/public/assets/Default-2rFTkbo9.js.br +0 -0
- package/public/assets/Default-2rFTkbo9.js.gz +0 -0
- package/public/assets/Invite-aN4DXJyM.js.br +0 -0
- package/public/assets/Invite-aN4DXJyM.js.gz +0 -0
- package/public/assets/MacResources-D3IlesXU.js.br +0 -0
- package/public/assets/MacResources-D3IlesXU.js.gz +0 -0
- package/public/assets/NotFound-BoDDivK5.js.br +0 -0
- package/public/assets/NotFound-BoDDivK5.js.gz +0 -0
- package/public/assets/QASession-U29t9j0E.js +0 -140
- package/public/assets/QASession-U29t9j0E.js.br +0 -0
- package/public/assets/QASession-U29t9j0E.js.gz +0 -0
- package/public/assets/ResetPassword-DPc5kwQU.js.br +0 -0
- package/public/assets/ResetPassword-DPc5kwQU.js.gz +0 -0
- package/public/assets/Setup-CwPx_e_O.js.br +0 -0
- package/public/assets/Setup-CwPx_e_O.js.gz +0 -0
- package/public/assets/Team-DD9li-7M.js +0 -6
- package/public/assets/Team-DD9li-7M.js.br +0 -0
- package/public/assets/Team-DD9li-7M.js.gz +0 -0
- package/public/assets/Tokens-DdMMke73.js.br +0 -0
- package/public/assets/Tokens-DdMMke73.js.gz +0 -0
- package/public/assets/alert-dialog-DADd1jHN.js +0 -1
- package/public/assets/alert-dialog-DADd1jHN.js.br +0 -0
- package/public/assets/alert-dialog-DADd1jHN.js.gz +0 -0
- package/public/assets/badge-CMf8d5RJ.js.br +0 -0
- package/public/assets/badge-CMf8d5RJ.js.gz +0 -0
- package/public/assets/build-format-lp_8kSvq.js +0 -11
- package/public/assets/build-format-lp_8kSvq.js.br +0 -0
- package/public/assets/build-format-lp_8kSvq.js.gz +0 -0
- package/public/assets/dialog-pS8vHXXv.js +0 -11
- package/public/assets/dialog-pS8vHXXv.js.br +0 -0
- package/public/assets/dialog-pS8vHXXv.js.gz +0 -0
- package/public/assets/index-Cajf0T-5.js +0 -198
- package/public/assets/index-Cajf0T-5.js.br +0 -0
- package/public/assets/index-Cajf0T-5.js.gz +0 -0
- package/public/assets/pencil-LD1ga-Kk.js +0 -6
- package/public/assets/pencil-LD1ga-Kk.js.br +0 -0
- package/public/assets/pencil-LD1ga-Kk.js.gz +0 -0
- package/public/assets/resource-health-BScurILF.js +0 -1
- package/public/assets/resource-health-BScurILF.js.br +0 -0
- package/public/assets/resource-health-BScurILF.js.gz +0 -0
- package/public/assets/table-CL9AvoTp.js.br +0 -0
- package/public/assets/table-CL9AvoTp.js.gz +0 -0
- package/public/assets/trash-mvVp4nwq.js +0 -11
- package/public/assets/trash-mvVp4nwq.js.br +0 -0
- package/public/assets/trash-mvVp4nwq.js.gz +0 -0
- package/public/assets/vendor-charts-8kVeHLwY.js.br +0 -0
- package/public/assets/vendor-charts-8kVeHLwY.js.gz +0 -0
- package/public/assets/vendor-react-AJpf4YZ0.js +0 -49
- package/public/assets/vendor-react-AJpf4YZ0.js.br +0 -0
- package/public/assets/vendor-react-AJpf4YZ0.js.gz +0 -0
- package/public/assets/vendor-router-BFibuab1.js.br +0 -0
- package/public/assets/vendor-router-BFibuab1.js.gz +0 -0
package/dist/RelayServer.d.ts
CHANGED
|
@@ -30,6 +30,20 @@ export declare function ownerKeyFor(userId: number | string | undefined, claimed
|
|
|
30
30
|
export declare class RelayServer {
|
|
31
31
|
private readonly options;
|
|
32
32
|
private httpServer;
|
|
33
|
+
/**
|
|
34
|
+
* **The port tunnel clients are pointed at, and the only thing that tells their traffic apart.**
|
|
35
|
+
* rathole's client and `tailscale serve` run on this machine and connect from loopback on behalf of
|
|
36
|
+
* someone on the internet or the tailnet; rathole forwards raw TCP, so no header marks it either. On
|
|
37
|
+
* the main port such a socket is indistinguishable from `tapflow start`'s own agent, which is
|
|
38
|
+
* unauthenticated by design — so every tunnel visitor was too. Everything accepted here is remote
|
|
39
|
+
* (`resolveClientAddress`'s `viaTunnel`), whatever its address.
|
|
40
|
+
*
|
|
41
|
+
* Loopback-only, because only a tunnel client on this machine has any business here; a remote client
|
|
42
|
+
* belongs on the main port, where it is classified by address as before. Same handler, same
|
|
43
|
+
* `WebSocketServer`, same TLS material — rathole carried a TLS relay's own handshake through before
|
|
44
|
+
* this existed, and still does.
|
|
45
|
+
*/
|
|
46
|
+
private tunnelServer;
|
|
33
47
|
private wss;
|
|
34
48
|
private sessions;
|
|
35
49
|
private publicDir;
|
|
@@ -114,12 +128,16 @@ export declare class RelayServer {
|
|
|
114
128
|
};
|
|
115
129
|
agentGraceMs?: number;
|
|
116
130
|
tunnel?: TunnelRuntime;
|
|
131
|
+
tunnelPort?: number;
|
|
117
132
|
});
|
|
118
133
|
private registerRoutes;
|
|
119
134
|
pushLog(msg: string): void;
|
|
120
135
|
start(): Promise<void>;
|
|
136
|
+
private listenTunnel;
|
|
121
137
|
stop(): Promise<void>;
|
|
122
138
|
address(): string | import("net").AddressInfo | null;
|
|
139
|
+
/** The tunnel listener's address, or null when this relay was given no `tunnelPort`. */
|
|
140
|
+
tunnelAddress(): string | import("net").AddressInfo | null;
|
|
123
141
|
private runHeartbeat;
|
|
124
142
|
/**
|
|
125
143
|
* Has this socket answered recently enough to be treated as present?
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelayServer.d.ts","sourceRoot":"","sources":["../src/RelayServer.ts"],"names":[],"mappings":"AAsBA,OAAO,EAA4D,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAwBjH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQvD;AAGD;;gFAEgF;AAChF,eAAO,MAAM,uBAAuB,MAAM,CAAA;AAC1C;;sGAEsG;AACtG,eAAO,MAAM,iCAAiC,MAAM,CAAA;AAqEpD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,EACnC,OAAO,EAAE,MAAM,GAAG,IAAI,GACrB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAUhD;AA8BD,qBAAa,WAAW;
|
|
1
|
+
{"version":3,"file":"RelayServer.d.ts","sourceRoot":"","sources":["../src/RelayServer.ts"],"names":[],"mappings":"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"}
|
package/dist/RelayServer.js
CHANGED
|
@@ -10,7 +10,7 @@ import { directionOf, parseInbound } from '@tapflowio/protocol/validate';
|
|
|
10
10
|
import { Router, json } from './router.js';
|
|
11
11
|
import { requireViewAuth, requireAuth, getAuth, verifyPat } from './middleware/auth.js';
|
|
12
12
|
import { classifyConnection } from './lib/connectionAuth.js';
|
|
13
|
-
import { resolveClientAddress } from './lib/clientAddress.js';
|
|
13
|
+
import { isTunnelIngress, markTunnelIngress, resolveClientAddress } from './lib/clientAddress.js';
|
|
14
14
|
import { BuildTicketStore } from './lib/buildTickets.js';
|
|
15
15
|
import { resolveCorsHeaders } from './lib/cors.js';
|
|
16
16
|
import { isCsrfBlocked } from './lib/csrf.js';
|
|
@@ -156,6 +156,20 @@ function describeRejection(failure, suppressed) {
|
|
|
156
156
|
export class RelayServer {
|
|
157
157
|
options;
|
|
158
158
|
httpServer;
|
|
159
|
+
/**
|
|
160
|
+
* **The port tunnel clients are pointed at, and the only thing that tells their traffic apart.**
|
|
161
|
+
* rathole's client and `tailscale serve` run on this machine and connect from loopback on behalf of
|
|
162
|
+
* someone on the internet or the tailnet; rathole forwards raw TCP, so no header marks it either. On
|
|
163
|
+
* the main port such a socket is indistinguishable from `tapflow start`'s own agent, which is
|
|
164
|
+
* unauthenticated by design — so every tunnel visitor was too. Everything accepted here is remote
|
|
165
|
+
* (`resolveClientAddress`'s `viaTunnel`), whatever its address.
|
|
166
|
+
*
|
|
167
|
+
* Loopback-only, because only a tunnel client on this machine has any business here; a remote client
|
|
168
|
+
* belongs on the main port, where it is classified by address as before. Same handler, same
|
|
169
|
+
* `WebSocketServer`, same TLS material — rathole carried a TLS relay's own handshake through before
|
|
170
|
+
* this existed, and still does.
|
|
171
|
+
*/
|
|
172
|
+
tunnelServer = null;
|
|
159
173
|
wss;
|
|
160
174
|
sessions;
|
|
161
175
|
publicDir;
|
|
@@ -240,6 +254,10 @@ export class RelayServer {
|
|
|
240
254
|
pendingUITrees = new Map();
|
|
241
255
|
constructor(options) {
|
|
242
256
|
this.options = options;
|
|
257
|
+
// 0 on both sides is two ephemeral ports, which is what the tests ask for.
|
|
258
|
+
if (options.tunnelPort !== undefined && options.tunnelPort !== 0 && options.tunnelPort === options.port) {
|
|
259
|
+
throw new Error(`The tunnel port (${options.tunnelPort}) must differ from the relay port. Set TAPFLOW_TUNNEL_PORT to another port.`);
|
|
260
|
+
}
|
|
243
261
|
this.backpressureBytes = options.wsBackpressureBytes ?? DEFAULT_BACKPRESSURE_BYTES;
|
|
244
262
|
this.screenshotTimeoutMs = options.screenshotTimeoutMs ?? 10_000;
|
|
245
263
|
// Longer than the screenshot default: the Android agent's device-side dump
|
|
@@ -280,6 +298,24 @@ export class RelayServer {
|
|
|
280
298
|
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
281
299
|
this.wss.on('connection', (ws, request) => this.handleConnection(ws, request));
|
|
282
300
|
this.wss.on('error', () => { });
|
|
301
|
+
if (options.tunnelPort !== undefined) {
|
|
302
|
+
// Marked before anything reads it: the router hands this same request object to `handleInit`, and
|
|
303
|
+
// `handleUpgrade` passes it on to the `connection` event.
|
|
304
|
+
const tunnelHandler = (req, res) => {
|
|
305
|
+
markTunnelIngress(req);
|
|
306
|
+
return handler(req, res);
|
|
307
|
+
};
|
|
308
|
+
const tunnelServer = options.tls
|
|
309
|
+
? https.createServer({ cert: options.tls.cert, key: options.tls.key }, tunnelHandler)
|
|
310
|
+
: http.createServer(tunnelHandler);
|
|
311
|
+
tunnelServer.on('connection', (socket) => socket.setNoDelay(true));
|
|
312
|
+
// `handleUpgrade` adds the socket to `wss.clients`, so the heartbeat and `stop()` cover it.
|
|
313
|
+
tunnelServer.on('upgrade', (req, socket, head) => {
|
|
314
|
+
markTunnelIngress(req);
|
|
315
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => this.wss.emit('connection', ws, req));
|
|
316
|
+
});
|
|
317
|
+
this.tunnelServer = tunnelServer;
|
|
318
|
+
}
|
|
283
319
|
}
|
|
284
320
|
registerRoutes() {
|
|
285
321
|
const u = this.uploadsDir;
|
|
@@ -327,7 +363,7 @@ export class RelayServer {
|
|
|
327
363
|
this.router.post('/api/v1/team/invite', (req, res) => handleInvite(req, res, this.options.tunnel));
|
|
328
364
|
this.router.patch('/api/v1/team/members/:id', handleUpdateMember);
|
|
329
365
|
this.router.delete('/api/v1/team/members/:id', handleDeleteMember);
|
|
330
|
-
this.router.post('/api/v1/team/members/:id/send-reset', handleSendMemberReset);
|
|
366
|
+
this.router.post('/api/v1/team/members/:id/send-reset', (req, res, params) => handleSendMemberReset(req, res, params, this.options.tunnel));
|
|
331
367
|
// tokens
|
|
332
368
|
this.router.get('/api/v1/tokens', handleListTokens);
|
|
333
369
|
this.router.post('/api/v1/tokens', handleCreateToken);
|
|
@@ -407,7 +443,30 @@ export class RelayServer {
|
|
|
407
443
|
});
|
|
408
444
|
// Bind dual-stack (IPv4 + IPv6). A bare listen(port) binds IPv6-only on some
|
|
409
445
|
// macOS/node setups, so LAN agents connecting over IPv4 (ws://<ipv4>:port) time out.
|
|
410
|
-
this.httpServer.listen({ port: this.options.port, host: '::', ipv6Only: false }, resolve);
|
|
446
|
+
this.httpServer.listen({ port: this.options.port, host: '::', ipv6Only: false }, () => resolve());
|
|
447
|
+
}).then(() => this.listenTunnel());
|
|
448
|
+
}
|
|
449
|
+
listenTunnel() {
|
|
450
|
+
const tunnelServer = this.tunnelServer;
|
|
451
|
+
if (!tunnelServer)
|
|
452
|
+
return Promise.resolve();
|
|
453
|
+
const port = this.options.tunnelPort ?? 0;
|
|
454
|
+
return new Promise((resolve, reject) => {
|
|
455
|
+
tunnelServer.once('error', (err) => reject(err.code === 'EADDRINUSE'
|
|
456
|
+
? new Error(`Tunnel port ${port} is already in use. Stop the process holding it, or set TAPFLOW_TUNNEL_PORT to a free port.`)
|
|
457
|
+
: err));
|
|
458
|
+
// 127.0.0.1, not `localhost`: rathole's client dials that address literally, and `tailscale serve
|
|
459
|
+
// <port>` proxies to it.
|
|
460
|
+
tunnelServer.listen({ port, host: '127.0.0.1' }, () => {
|
|
461
|
+
// The main server's later errors reach `wss`, which swallows them; nothing forwards this one's.
|
|
462
|
+
tunnelServer.on('error', (err) => logger.error(`tunnel listener error: ${String(err)}`));
|
|
463
|
+
resolve();
|
|
464
|
+
});
|
|
465
|
+
}).catch(async (err) => {
|
|
466
|
+
// A relay whose tunnel port failed must not keep serving on the main one: the caller treats this
|
|
467
|
+
// as a failed start and moves on, and the port would stay held by a server nobody stops.
|
|
468
|
+
await this.stop().catch(() => { });
|
|
469
|
+
throw err;
|
|
411
470
|
});
|
|
412
471
|
}
|
|
413
472
|
stop() {
|
|
@@ -445,16 +504,27 @@ export class RelayServer {
|
|
|
445
504
|
for (const requester of this.networkStateRequesters.values())
|
|
446
505
|
requester.dispose();
|
|
447
506
|
this.networkStateRequesters.clear();
|
|
448
|
-
|
|
507
|
+
const closeTunnel = new Promise((resolve, reject) => {
|
|
508
|
+
// Not listening means `start()` never reached it, or its listen failed; `close()` would reject.
|
|
509
|
+
if (!this.tunnelServer?.listening)
|
|
510
|
+
return resolve();
|
|
511
|
+
this.tunnelServer.close((err) => (err ? reject(err) : resolve()));
|
|
512
|
+
});
|
|
513
|
+
const closeMain = new Promise((resolve, reject) => {
|
|
449
514
|
this.wss.clients.forEach((ws) => ws.terminate());
|
|
450
515
|
this.wss.close(() => {
|
|
451
516
|
this.httpServer.close((err) => (err ? reject(err) : resolve()));
|
|
452
517
|
});
|
|
453
518
|
});
|
|
519
|
+
return Promise.all([closeMain, closeTunnel]).then(() => undefined);
|
|
454
520
|
}
|
|
455
521
|
address() {
|
|
456
522
|
return this.httpServer.address();
|
|
457
523
|
}
|
|
524
|
+
/** The tunnel listener's address, or null when this relay was given no `tunnelPort`. */
|
|
525
|
+
tunnelAddress() {
|
|
526
|
+
return this.tunnelServer?.address() ?? null;
|
|
527
|
+
}
|
|
458
528
|
// Terminate sockets that missed the previous pong; ping the rest. Covers all roles via wss.clients.
|
|
459
529
|
runHeartbeat(clients = this.wss.clients) {
|
|
460
530
|
for (const ws of clients) {
|
|
@@ -488,8 +558,10 @@ export class RelayServer {
|
|
|
488
558
|
}
|
|
489
559
|
// 갱신된 cert를 재시작 없이 핫스왑한다(https 종단일 때만 의미 있음).
|
|
490
560
|
updateTlsContext(material) {
|
|
491
|
-
|
|
492
|
-
|
|
561
|
+
for (const server of [this.httpServer, this.tunnelServer]) {
|
|
562
|
+
if (server instanceof https.Server) {
|
|
563
|
+
server.setSecureContext({ cert: material.cert, key: material.key });
|
|
564
|
+
}
|
|
493
565
|
}
|
|
494
566
|
}
|
|
495
567
|
/**
|
|
@@ -604,8 +676,9 @@ export class RelayServer {
|
|
|
604
676
|
const a = socketAddr.replace(/^::ffff:/, '');
|
|
605
677
|
if (a === '::1' || a.startsWith('127.')) {
|
|
606
678
|
logger.warn('Received X-Forwarded-For from a loopback connection but TAPFLOW_TRUSTED_PROXIES is unset. ' +
|
|
607
|
-
'If
|
|
608
|
-
'
|
|
679
|
+
'If a same-host reverse proxy or `tailscale serve` forwards to this port, every client it forwards is ' +
|
|
680
|
+
'treated as localhost (unauthenticated). Point it at the tunnel port instead (TAPFLOW_TUNNEL_PORT, ' +
|
|
681
|
+
'default 4001), or set TAPFLOW_TRUSTED_PROXIES so the real client IP is used.');
|
|
609
682
|
this.warnedProxyMisconfig = true;
|
|
610
683
|
}
|
|
611
684
|
}
|
|
@@ -613,11 +686,15 @@ export class RelayServer {
|
|
|
613
686
|
const socketAddr = this.remoteAddressOf(request);
|
|
614
687
|
const xff = request.headers['x-forwarded-for'];
|
|
615
688
|
const forwardedFor = Array.isArray(xff) ? xff[0] : xff;
|
|
616
|
-
|
|
689
|
+
const viaTunnel = isTunnelIngress(request);
|
|
690
|
+
// A proxy in front of the tunnel port adds the header too, and there it changes nothing about auth.
|
|
691
|
+
if (!viaTunnel)
|
|
692
|
+
this.warnProxyMisconfigOnce(socketAddr, forwardedFor);
|
|
617
693
|
const { addr, isLocal } = resolveClientAddress({
|
|
618
694
|
socketAddr,
|
|
619
695
|
forwardedFor,
|
|
620
696
|
trustedProxies: this.options.trustedProxies ?? [],
|
|
697
|
+
viaTunnel,
|
|
621
698
|
});
|
|
622
699
|
const hasCookieAuth = getAuth(request) !== null;
|
|
623
700
|
// DB lookup — only when the connection can't be classified without it (remote, no cookie).
|
|
@@ -964,15 +1041,32 @@ export class RelayServer {
|
|
|
964
1041
|
// two-member union** while `AgentRegister.platform` is `string`, open so that a third-party
|
|
965
1042
|
// platform can register through `AgentRegistry.register()` (root AGENTS.md, OCP). Validating
|
|
966
1043
|
// this would refuse a platform the repo promises to support — and refusing costs more than a
|
|
967
|
-
// dropped frame
|
|
968
|
-
// empties what the re-join replay reads.
|
|
969
|
-
//
|
|
1044
|
+
// dropped frame: skipping `setChromeData` also
|
|
1045
|
+
// empties what the re-join replay reads. It is no longer true that this arrives once per boot,
|
|
1046
|
+
// either — a foldable re-sends it when the screen changes under the session — so the cost of
|
|
1047
|
+
// a refusal is a viewer stuck with the previous screen's bezel rather than one frame.
|
|
1048
|
+
// The relay never looks inside; the viewer does, and it is where the variants are already
|
|
1049
|
+
// told apart.
|
|
970
1050
|
this.sessions.setChromeData(session.id, raw['payload']);
|
|
971
1051
|
if (session.browserSocket?.readyState === WebSocket.OPEN) {
|
|
972
1052
|
session.browserSocket.send(JSON.stringify(raw));
|
|
973
1053
|
}
|
|
974
1054
|
break;
|
|
975
1055
|
}
|
|
1056
|
+
case 'device:postures': {
|
|
1057
|
+
const session = this.sessions.get(msg.sessionId);
|
|
1058
|
+
if (!session)
|
|
1059
|
+
break;
|
|
1060
|
+
// Cached for the same reason `session:chrome` is: a viewer that re-joins (a tab reload, a
|
|
1061
|
+
// socket blip) gets the session's state replayed, and without this the posture control would
|
|
1062
|
+
// simply be missing until the device was rebooted — with the picture still fine, which is
|
|
1063
|
+
// the hardest kind of gap to diagnose.
|
|
1064
|
+
this.sessions.setPostures(session.id, raw['payload']);
|
|
1065
|
+
if (session.browserSocket?.readyState === WebSocket.OPEN) {
|
|
1066
|
+
session.browserSocket.send(JSON.stringify(raw));
|
|
1067
|
+
}
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
976
1070
|
case 'session:deviceInfo': {
|
|
977
1071
|
const session = this.sessions.get(msg.sessionId);
|
|
978
1072
|
if (!session)
|
|
@@ -1228,6 +1322,7 @@ export class RelayServer {
|
|
|
1228
1322
|
case 'input:pinch:start':
|
|
1229
1323
|
case 'input:pinch:move':
|
|
1230
1324
|
case 'input:rotate':
|
|
1325
|
+
case 'input:posture':
|
|
1231
1326
|
case 'input:keyboard:toggle': {
|
|
1232
1327
|
// No ack, so no correlator to check and nothing to answer. An unowned frame is dropped rather
|
|
1233
1328
|
// than refused for the same reason: there is no waiter to tell.
|
|
@@ -1627,6 +1722,9 @@ export class RelayServer {
|
|
|
1627
1722
|
if (session.deviceInfo) {
|
|
1628
1723
|
this.sendTo(ws, { type: 'session:deviceInfo', sessionId: session.id, payload: session.deviceInfo });
|
|
1629
1724
|
}
|
|
1725
|
+
if (session.postures) {
|
|
1726
|
+
this.sendTo(ws, { type: 'device:postures', sessionId: session.id, payload: session.postures });
|
|
1727
|
+
}
|
|
1630
1728
|
// Replay device:ready only if this session actually announced one (browser WS blip reconnect).
|
|
1631
1729
|
// Not `deviceStatus`: that starts from the agent's `simctl list` snapshot, so a session for a
|
|
1632
1730
|
// simulator that was already running would fire this before the agent had done anything —
|