@tapflowio/relay 0.27.0 → 0.27.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/dist/RelayServer.d.ts +20 -0
- package/dist/RelayServer.d.ts.map +1 -1
- package/dist/RelayServer.js +98 -6
- package/dist/RelayServer.js.map +1 -1
- package/dist/api/builds.d.ts.map +1 -1
- package/dist/api/builds.js +13 -8
- package/dist/api/builds.js.map +1 -1
- package/dist/api/comments.d.ts.map +1 -1
- package/dist/api/comments.js +5 -7
- package/dist/api/comments.js.map +1 -1
- package/dist/api/invitations.d.ts.map +1 -1
- package/dist/api/invitations.js +3 -0
- package/dist/api/invitations.js.map +1 -1
- package/dist/api/profile.d.ts.map +1 -1
- package/dist/api/profile.js +28 -9
- package/dist/api/profile.js.map +1 -1
- package/dist/api/recordings.d.ts.map +1 -1
- package/dist/api/recordings.js +5 -6
- package/dist/api/recordings.js.map +1 -1
- package/dist/api/settings.d.ts.map +1 -1
- package/dist/api/settings.js +28 -9
- package/dist/api/settings.js.map +1 -1
- package/dist/lib/clientAddress.d.ts +12 -0
- package/dist/lib/clientAddress.d.ts.map +1 -1
- package/dist/lib/clientAddress.js +22 -0
- package/dist/lib/clientAddress.js.map +1 -1
- package/dist/lib/uploads.d.ts +13 -0
- package/dist/lib/uploads.d.ts.map +1 -1
- package/dist/lib/uploads.js +41 -0
- package/dist/lib/uploads.js.map +1 -1
- package/package.json +3 -3
- package/public/assets/{AppCenter-BMdYtQbV.js → AppCenter-CeLjUbnR.js} +1 -1
- package/public/assets/AppCenter-CeLjUbnR.js.br +0 -0
- package/public/assets/AppCenter-CeLjUbnR.js.gz +0 -0
- package/public/assets/{Default-0DHHVGaN.js → Default-UB0Pa3lN.js} +1 -1
- package/public/assets/Default-UB0Pa3lN.js.br +0 -0
- package/public/assets/Default-UB0Pa3lN.js.gz +0 -0
- package/public/assets/{Invite-C0ZB_HiU.js → Invite-Dv3omu3x.js} +1 -1
- package/public/assets/Invite-Dv3omu3x.js.br +0 -0
- package/public/assets/Invite-Dv3omu3x.js.gz +0 -0
- package/public/assets/{MacResources-DmY5pRV2.js → MacResources-CY4-OdGt.js} +1 -1
- package/public/assets/MacResources-CY4-OdGt.js.br +0 -0
- package/public/assets/MacResources-CY4-OdGt.js.gz +0 -0
- package/public/assets/QASession-H5eRSwZv.js +150 -0
- package/public/assets/QASession-H5eRSwZv.js.br +0 -0
- package/public/assets/QASession-H5eRSwZv.js.gz +0 -0
- package/public/assets/{ResetPassword-jNRuertx.js → ResetPassword-nFTigYt2.js} +1 -1
- package/public/assets/ResetPassword-nFTigYt2.js.br +0 -0
- package/public/assets/ResetPassword-nFTigYt2.js.gz +0 -0
- package/public/assets/{Setup-CF_FN9Dt.js → Setup-UmLtF2PF.js} +1 -1
- package/public/assets/Setup-UmLtF2PF.js.br +0 -0
- package/public/assets/Setup-UmLtF2PF.js.gz +0 -0
- package/public/assets/{Team-BbiyOGau.js → Team-DW40kt3H.js} +1 -1
- package/public/assets/Team-DW40kt3H.js.br +0 -0
- package/public/assets/Team-DW40kt3H.js.gz +0 -0
- package/public/assets/{Tokens-CZo4sPTx.js → Tokens-B1a4ChJK.js} +1 -1
- package/public/assets/Tokens-B1a4ChJK.js.br +0 -0
- package/public/assets/Tokens-B1a4ChJK.js.gz +0 -0
- package/public/assets/{alert-dialog-DFz8G_zu.js → alert-dialog-aoqYdAwx.js} +1 -1
- package/public/assets/alert-dialog-aoqYdAwx.js.br +0 -0
- package/public/assets/alert-dialog-aoqYdAwx.js.gz +0 -0
- package/public/assets/{badge-B3vaBVIQ.js → badge-DNaS0Mu3.js} +1 -1
- package/public/assets/badge-DNaS0Mu3.js.br +0 -0
- package/public/assets/badge-DNaS0Mu3.js.gz +0 -0
- package/public/assets/{build-format-dbQPU3A4.js → build-format-DF5aFcag.js} +1 -1
- package/public/assets/build-format-DF5aFcag.js.br +0 -0
- package/public/assets/build-format-DF5aFcag.js.gz +0 -0
- package/public/assets/{dialog-DeBPk-I3.js → dialog-DE2WUMuX.js} +1 -1
- package/public/assets/dialog-DE2WUMuX.js.br +0 -0
- package/public/assets/dialog-DE2WUMuX.js.gz +0 -0
- package/public/assets/{field-hint-CMKmbVpy.js → field-hint-Csb40Gzp.js} +1 -1
- package/public/assets/field-hint-Csb40Gzp.js.br +2 -0
- package/public/assets/field-hint-Csb40Gzp.js.gz +0 -0
- package/public/assets/index-BbCkTtjO.css +1 -0
- package/public/assets/index-BbCkTtjO.css.br +0 -0
- package/public/assets/index-BbCkTtjO.css.gz +0 -0
- package/public/assets/{index-B4w4Lh3e.js → index-DSW9h8XX.js} +2 -2
- package/public/assets/index-DSW9h8XX.js.br +0 -0
- package/public/assets/index-DSW9h8XX.js.gz +0 -0
- package/public/assets/{list-view-BvINA5t4.js → list-view-C0w97ltJ.js} +1 -1
- package/public/assets/list-view-C0w97ltJ.js.br +0 -0
- package/public/assets/list-view-C0w97ltJ.js.gz +0 -0
- package/public/assets/{pencil-CycAOoC_.js → pencil-BN18R4oa.js} +1 -1
- package/public/assets/pencil-BN18R4oa.js.br +0 -0
- package/public/assets/pencil-BN18R4oa.js.gz +0 -0
- package/public/assets/{resource-health-C1qSG3MK.js → resource-health-Bi20EUDs.js} +1 -1
- package/public/assets/resource-health-Bi20EUDs.js.br +0 -0
- package/public/assets/resource-health-Bi20EUDs.js.gz +0 -0
- package/public/assets/{trash-BXQnOI5U.js → trash-D1l1Fm-Z.js} +1 -1
- package/public/assets/trash-D1l1Fm-Z.js.br +0 -0
- package/public/assets/trash-D1l1Fm-Z.js.gz +0 -0
- package/public/index.html +2 -2
- package/public/index.html.br +0 -0
- package/public/index.html.gz +0 -0
- package/public/assets/AppCenter-BMdYtQbV.js.br +0 -0
- package/public/assets/AppCenter-BMdYtQbV.js.gz +0 -0
- package/public/assets/Default-0DHHVGaN.js.br +0 -0
- package/public/assets/Default-0DHHVGaN.js.gz +0 -0
- package/public/assets/Invite-C0ZB_HiU.js.br +0 -0
- package/public/assets/Invite-C0ZB_HiU.js.gz +0 -0
- package/public/assets/MacResources-DmY5pRV2.js.br +0 -0
- package/public/assets/MacResources-DmY5pRV2.js.gz +0 -0
- package/public/assets/QASession-2QogWEX7.js +0 -150
- package/public/assets/QASession-2QogWEX7.js.br +0 -0
- package/public/assets/QASession-2QogWEX7.js.gz +0 -0
- package/public/assets/ResetPassword-jNRuertx.js.br +0 -0
- package/public/assets/ResetPassword-jNRuertx.js.gz +0 -0
- package/public/assets/Setup-CF_FN9Dt.js.br +0 -0
- package/public/assets/Setup-CF_FN9Dt.js.gz +0 -0
- package/public/assets/Team-BbiyOGau.js.br +0 -0
- package/public/assets/Team-BbiyOGau.js.gz +0 -0
- package/public/assets/Tokens-CZo4sPTx.js.br +0 -0
- package/public/assets/Tokens-CZo4sPTx.js.gz +0 -0
- package/public/assets/alert-dialog-DFz8G_zu.js.br +0 -0
- package/public/assets/alert-dialog-DFz8G_zu.js.gz +0 -0
- package/public/assets/badge-B3vaBVIQ.js.br +0 -0
- package/public/assets/badge-B3vaBVIQ.js.gz +0 -0
- package/public/assets/build-format-dbQPU3A4.js.br +0 -0
- package/public/assets/build-format-dbQPU3A4.js.gz +0 -0
- package/public/assets/dialog-DeBPk-I3.js.br +0 -0
- package/public/assets/dialog-DeBPk-I3.js.gz +0 -0
- package/public/assets/field-hint-CMKmbVpy.js.br +0 -0
- package/public/assets/field-hint-CMKmbVpy.js.gz +0 -0
- package/public/assets/index-B4w4Lh3e.js.br +0 -0
- package/public/assets/index-B4w4Lh3e.js.gz +0 -0
- package/public/assets/index-CDWu0GQL.css +0 -1
- package/public/assets/index-CDWu0GQL.css.br +0 -0
- package/public/assets/index-CDWu0GQL.css.gz +0 -0
- package/public/assets/list-view-BvINA5t4.js.br +0 -0
- package/public/assets/list-view-BvINA5t4.js.gz +0 -0
- package/public/assets/pencil-CycAOoC_.js.br +0 -0
- package/public/assets/pencil-CycAOoC_.js.gz +0 -0
- package/public/assets/resource-health-C1qSG3MK.js.br +0 -0
- package/public/assets/resource-health-C1qSG3MK.js.gz +0 -0
- package/public/assets/trash-BXQnOI5U.js.br +0 -0
- package/public/assets/trash-BXQnOI5U.js.gz +0 -0
package/dist/RelayServer.d.ts
CHANGED
|
@@ -64,6 +64,15 @@ export declare class RelayServer {
|
|
|
64
64
|
private purgeBuildsTimer;
|
|
65
65
|
private flushResourcesTimer;
|
|
66
66
|
private heartbeatTimer;
|
|
67
|
+
/** When the last heartbeat sweep ran, by the same clock `isAlive` judges with. */
|
|
68
|
+
private lastSweepAt;
|
|
69
|
+
/** Names agents give at registration, kept apart from the session map so an agent with no sessions
|
|
70
|
+
* can still be named when its socket goes. */
|
|
71
|
+
private agentNames;
|
|
72
|
+
private releaseStallWatch;
|
|
73
|
+
/** Agent sockets whose close was already explained — ended by a sweep, or replaced by the same agent
|
|
74
|
+
* registering again — so the close handler does not log them as a fresh drop. */
|
|
75
|
+
private explainedCloses;
|
|
67
76
|
/**
|
|
68
77
|
* When each socket last answered a ping. WeakMap → no manual cleanup on close (GC handles it).
|
|
69
78
|
*
|
|
@@ -133,6 +142,7 @@ export declare class RelayServer {
|
|
|
133
142
|
private readonly pendingBootTtlMs;
|
|
134
143
|
private readonly corsAllowed;
|
|
135
144
|
private warnedProxyMisconfig;
|
|
145
|
+
private warnedTrustedProxyWithoutXff;
|
|
136
146
|
/** Agent identities already warned about a missing `build-download`. See `warnLegacyInstaller`. */
|
|
137
147
|
private readonly warnedLegacyInstaller;
|
|
138
148
|
private readonly buildTickets;
|
|
@@ -212,6 +222,10 @@ export declare class RelayServer {
|
|
|
212
222
|
*/
|
|
213
223
|
private warnOrphanedAgentTokens;
|
|
214
224
|
private runHeartbeat;
|
|
225
|
+
/** A sweep that runs late means this process was held, or the machine slept: the pongs it judges were
|
|
226
|
+
* never read, so its terminations say nothing about the clients. Same clock as `isAlive`. */
|
|
227
|
+
private noteLateSweep;
|
|
228
|
+
private pongAgeSeconds;
|
|
215
229
|
/**
|
|
216
230
|
* Injected into the handlers whose write can take access away — member update and removal, token
|
|
217
231
|
* revocation, a password reset or change — and called after the write commits. The sweep re-derives
|
|
@@ -302,6 +316,12 @@ export declare class RelayServer {
|
|
|
302
316
|
*/
|
|
303
317
|
private networkStateRequester;
|
|
304
318
|
private remoteAddressOf;
|
|
319
|
+
/**
|
|
320
|
+
* Called on both doors — the WebSocket upgrade and every HTTP request — since `auth/init` and the logs
|
|
321
|
+
* route decide locality on the HTTP side. Not on the tunnel listener, where every request is remote
|
|
322
|
+
* whatever it carries.
|
|
323
|
+
*/
|
|
324
|
+
private warnTrustedProxyWithoutXffOnce;
|
|
305
325
|
private warnProxyMisconfigOnce;
|
|
306
326
|
private handleConnection;
|
|
307
327
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelayServer.d.ts","sourceRoot":"","sources":["../src/RelayServer.ts"],"names":[],"mappings":"AAuBA,OAAO,EAA4D,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAA;
|
|
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, touchPat, findPat, findUser } from './middleware/auth.js';
|
|
12
12
|
import { AGENT_SCOPE, WS_ACCESS_CHANGED_REASON, WS_AGENT_OWNER_REASON, WS_SCOPE_REASON, classifyConnection, isAffectedBy, revalidatePrincipal } from './lib/connectionAuth.js';
|
|
13
|
-
import { isTunnelIngress, markTunnelIngress, resolveClientAddress, resolveRequestClient } from './lib/clientAddress.js';
|
|
13
|
+
import { isTunnelIngress, markTunnelIngress, proxiedWithoutForwardedFor, resolveClientAddress, resolveRequestClient } from './lib/clientAddress.js';
|
|
14
14
|
import { BuildTicketStore } from './lib/buildTickets.js';
|
|
15
15
|
import { resolveBuildFile } from './lib/buildFiles.js';
|
|
16
16
|
import { resolveCorsHeaders } from './lib/cors.js';
|
|
@@ -23,6 +23,7 @@ import { getDb } from './db.js';
|
|
|
23
23
|
import { handleLogin, handleLogout, handleMe, handleChangePassword, handleInit, handleAuthStatus } from './api/auth.js';
|
|
24
24
|
import { handleVerify, handleAccept } from './api/invitations.js';
|
|
25
25
|
import { createLogger, hasCapability } from '@tapflowio/agent-core';
|
|
26
|
+
import { acquireLoopStallWatch } from '@tapflowio/agent-core/utils';
|
|
26
27
|
import { createKeyframeAwareSender, createRateLimitedDropWarn, sendAudioYieldingToVideo, DEFAULT_BACKPRESSURE_BYTES, hasEnvelope, patchRelayedAt, readEnvelopeFlags, CODEC_JPEG, CODEC_AUDIO, } from '@tapflowio/agent-core/utils';
|
|
27
28
|
const logger = createLogger('relay');
|
|
28
29
|
// True when a remote IP is public — i.e. not loopback, private LAN, or link-local. Used to pick the
|
|
@@ -214,6 +215,15 @@ export class RelayServer {
|
|
|
214
215
|
purgeBuildsTimer = null;
|
|
215
216
|
flushResourcesTimer = null;
|
|
216
217
|
heartbeatTimer = null;
|
|
218
|
+
/** When the last heartbeat sweep ran, by the same clock `isAlive` judges with. */
|
|
219
|
+
lastSweepAt = 0;
|
|
220
|
+
/** Names agents give at registration, kept apart from the session map so an agent with no sessions
|
|
221
|
+
* can still be named when its socket goes. */
|
|
222
|
+
agentNames = new WeakMap();
|
|
223
|
+
releaseStallWatch = null;
|
|
224
|
+
/** Agent sockets whose close was already explained — ended by a sweep, or replaced by the same agent
|
|
225
|
+
* registering again — so the close handler does not log them as a fresh drop. */
|
|
226
|
+
explainedCloses = new WeakMap();
|
|
217
227
|
/**
|
|
218
228
|
* When each socket last answered a ping. WeakMap → no manual cleanup on close (GC handles it).
|
|
219
229
|
*
|
|
@@ -297,6 +307,7 @@ export class RelayServer {
|
|
|
297
307
|
corsAllowed;
|
|
298
308
|
// One-shot warning when XFF arrives on a loopback socket but TAPFLOW_TRUSTED_PROXIES is unset.
|
|
299
309
|
warnedProxyMisconfig = false;
|
|
310
|
+
warnedTrustedProxyWithoutXff = false;
|
|
300
311
|
/** Agent identities already warned about a missing `build-download`. See `warnLegacyInstaller`. */
|
|
301
312
|
warnedLegacyInstaller = new Set();
|
|
302
313
|
buildTickets = new BuildTicketStore();
|
|
@@ -526,8 +537,17 @@ export class RelayServer {
|
|
|
526
537
|
this.purgeBuildsTimer.unref();
|
|
527
538
|
this.flushResourcesTimer = setInterval(() => this.flushResourceBuffers(), 60_000);
|
|
528
539
|
this.flushResourcesTimer.unref();
|
|
529
|
-
this.
|
|
540
|
+
this.lastSweepAt = Date.now();
|
|
541
|
+
this.heartbeatTimer = setInterval(() => { this.noteLateSweep(); this.runHeartbeat(); }, HEARTBEAT_MS);
|
|
530
542
|
this.heartbeatTimer.unref();
|
|
543
|
+
this.releaseStallWatch ??= acquireLoopStallWatch();
|
|
544
|
+
// The rule a proxy has to follow, said once to whoever set the list: the case it covers (a proxy that
|
|
545
|
+
// sends no forwarding header at all) is one no request-time check can see.
|
|
546
|
+
const trustedProxies = this.options.trustedProxies ?? [];
|
|
547
|
+
if (trustedProxies.length > 0) {
|
|
548
|
+
logger.info(`Trusted proxies: ${trustedProxies.join(', ')}. A request from one of these without ` +
|
|
549
|
+
'X-Forwarded-For is treated as coming from that address itself, so the proxy must send X-Forwarded-For.');
|
|
550
|
+
}
|
|
531
551
|
return new Promise((resolve, reject) => {
|
|
532
552
|
this.httpServer.once('error', (err) => {
|
|
533
553
|
if (err.code === 'EADDRINUSE') {
|
|
@@ -586,6 +606,8 @@ export class RelayServer {
|
|
|
586
606
|
clearInterval(this.heartbeatTimer);
|
|
587
607
|
this.heartbeatTimer = null;
|
|
588
608
|
}
|
|
609
|
+
this.releaseStallWatch?.();
|
|
610
|
+
this.releaseStallWatch = null;
|
|
589
611
|
// `unref()` would not do: the test runner's process outlives the server, so an un-cleared hold
|
|
590
612
|
// still fires — against a server that has already stopped.
|
|
591
613
|
this.stopping = true;
|
|
@@ -664,14 +686,42 @@ export class RelayServer {
|
|
|
664
686
|
// interval, whatever path the change took. `onAuthChanged` is the same check run early.
|
|
665
687
|
runHeartbeat(clients = this.wss.clients) {
|
|
666
688
|
this.revalidateSockets(clients);
|
|
689
|
+
// Agents one line each; everything else summed, because a relay waking from sleep finds every socket
|
|
690
|
+
// stale at once and one line per browser tab would push the agent lines out of the log buffer.
|
|
691
|
+
const others = new Map();
|
|
667
692
|
for (const ws of clients) {
|
|
668
693
|
if (!this.isAlive(ws)) {
|
|
694
|
+
const role = this.wsRoles.get(ws);
|
|
695
|
+
if (role === 'agent') {
|
|
696
|
+
this.pushLog(`heartbeat terminated agent ${this.agentNames.get(ws) ?? 'unknown'} — last pong ${this.pongAgeSeconds(ws)}s ago`);
|
|
697
|
+
this.explainedCloses.set(ws, 'terminated');
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
const key = role ?? 'unregistered';
|
|
701
|
+
others.set(key, (others.get(key) ?? 0) + 1);
|
|
702
|
+
}
|
|
669
703
|
ws.terminate();
|
|
670
704
|
continue;
|
|
671
705
|
}
|
|
672
706
|
if (ws.readyState === WebSocket.OPEN)
|
|
673
707
|
ws.ping();
|
|
674
708
|
}
|
|
709
|
+
if (others.size > 0) {
|
|
710
|
+
this.pushLog(`heartbeat terminated ${[...others].map(([role, n]) => `${n} ${role}`).join(', ')} socket(s)`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/** A sweep that runs late means this process was held, or the machine slept: the pongs it judges were
|
|
714
|
+
* never read, so its terminations say nothing about the clients. Same clock as `isAlive`. */
|
|
715
|
+
noteLateSweep(now = Date.now()) {
|
|
716
|
+
const late = now - this.lastSweepAt - HEARTBEAT_MS;
|
|
717
|
+
if (late > HEARTBEAT_MS / 2) {
|
|
718
|
+
this.pushLog(`heartbeat sweep ran ${Math.round(late / 1000)}s late — the relay was stalled or the system slept; this sweep may end sockets that were alive`);
|
|
719
|
+
}
|
|
720
|
+
this.lastSweepAt = now;
|
|
721
|
+
}
|
|
722
|
+
pongAgeSeconds(ws) {
|
|
723
|
+
const at = this.lastPongAt.get(ws);
|
|
724
|
+
return at === undefined ? '?' : Math.round((Date.now() - at) / 1000);
|
|
675
725
|
}
|
|
676
726
|
/**
|
|
677
727
|
* Injected into the handlers whose write can take access away — member update and removal, token
|
|
@@ -897,6 +947,24 @@ export class RelayServer {
|
|
|
897
947
|
remoteAddressOf(request) {
|
|
898
948
|
return request.socket.remoteAddress ?? '';
|
|
899
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Called on both doors — the WebSocket upgrade and every HTTP request — since `auth/init` and the logs
|
|
952
|
+
* route decide locality on the HTTP side. Not on the tunnel listener, where every request is remote
|
|
953
|
+
* whatever it carries.
|
|
954
|
+
*/
|
|
955
|
+
warnTrustedProxyWithoutXffOnce(request) {
|
|
956
|
+
if (this.warnedTrustedProxyWithoutXff || isTunnelIngress(request))
|
|
957
|
+
return;
|
|
958
|
+
const header = proxiedWithoutForwardedFor(this.remoteAddressOf(request), request.headers, this.options.trustedProxies ?? []);
|
|
959
|
+
if (!header)
|
|
960
|
+
return;
|
|
961
|
+
logger.warn(`A request from a trusted proxy (TAPFLOW_TRUSTED_PROXIES) carried ${header} but no X-Forwarded-For. ` +
|
|
962
|
+
'Without that header the relay cannot see the real client, so it treats the request as coming from the ' +
|
|
963
|
+
'proxy itself: local, and so unauthenticated, when that address is loopback. Configure the proxy to ' +
|
|
964
|
+
'send X-Forwarded-For. A proxy on this host can instead use the tunnel port, which is open only when a ' +
|
|
965
|
+
'tunnel is configured or TAPFLOW_TUNNEL_PORT names a port.');
|
|
966
|
+
this.warnedTrustedProxyWithoutXff = true;
|
|
967
|
+
}
|
|
900
968
|
warnProxyMisconfigOnce(socketAddr, forwardedFor) {
|
|
901
969
|
if (this.warnedProxyMisconfig)
|
|
902
970
|
return;
|
|
@@ -906,12 +974,19 @@ export class RelayServer {
|
|
|
906
974
|
if (a === '::1' || a.startsWith('127.')) {
|
|
907
975
|
logger.warn('Received X-Forwarded-For from a loopback connection but TAPFLOW_TRUSTED_PROXIES is unset. ' +
|
|
908
976
|
'If a same-host reverse proxy or `tailscale serve` forwards to this port, every client it forwards is ' +
|
|
909
|
-
'treated as localhost (unauthenticated). Point it at the tunnel port instead (
|
|
910
|
-
'
|
|
977
|
+
'treated as localhost (unauthenticated). Point it at the tunnel port instead (open only when a tunnel ' +
|
|
978
|
+
'is configured or TAPFLOW_TUNNEL_PORT names a port), or set TAPFLOW_TRUSTED_PROXIES so the real ' +
|
|
979
|
+
'client IP is used.');
|
|
911
980
|
this.warnedProxyMisconfig = true;
|
|
912
981
|
}
|
|
913
982
|
}
|
|
914
983
|
handleConnection(ws, request) {
|
|
984
|
+
// **First, before anything can return.** A frame that breaks the protocol makes `ws` emit `'error'` on
|
|
985
|
+
// this socket (a reset it handles itself), and an EventEmitter with no `'error'` listener throws: from
|
|
986
|
+
// inside `ws`'s receiver, with nothing above it to catch. One client could end the relay for everyone,
|
|
987
|
+
// signed in or not. `ws` closes the socket itself after emitting, so the `'close'` handler below still runs.
|
|
988
|
+
// Debug, not warn: any client can produce this at will, and the close is already logged where it matters.
|
|
989
|
+
ws.on('error', (err) => logger.debug(`WebSocket error: ${err.message}`));
|
|
915
990
|
const socketAddr = this.remoteAddressOf(request);
|
|
916
991
|
const xff = request.headers['x-forwarded-for'];
|
|
917
992
|
const forwardedFor = Array.isArray(xff) ? xff[0] : xff;
|
|
@@ -919,6 +994,7 @@ export class RelayServer {
|
|
|
919
994
|
// A proxy in front of the tunnel port adds the header too, and there it changes nothing about auth.
|
|
920
995
|
if (!viaTunnel)
|
|
921
996
|
this.warnProxyMisconfigOnce(socketAddr, forwardedFor);
|
|
997
|
+
this.warnTrustedProxyWithoutXffOnce(request);
|
|
922
998
|
const { addr, isLocal } = resolveClientAddress({
|
|
923
999
|
socketAddr,
|
|
924
1000
|
forwardedFor,
|
|
@@ -1085,7 +1161,19 @@ export class RelayServer {
|
|
|
1085
1161
|
logger.error(`route failed for ${inbound.msg.type}:`, e);
|
|
1086
1162
|
}
|
|
1087
1163
|
});
|
|
1088
|
-
ws.on('close', () => {
|
|
1164
|
+
ws.on('close', (code, reason) => {
|
|
1165
|
+
// Not while stopping: the relay ending every socket itself says nothing about the agents.
|
|
1166
|
+
if (this.wsRoles.get(ws) === 'agent' && !this.stopping) {
|
|
1167
|
+
const name = this.agentNames.get(ws) ?? 'unknown';
|
|
1168
|
+
const explained = this.explainedCloses.get(ws);
|
|
1169
|
+
if (explained === 'replaced') {
|
|
1170
|
+
this.pushLog(`agent socket replaced: ${name} registered again on a new connection`);
|
|
1171
|
+
}
|
|
1172
|
+
else if (!explained) {
|
|
1173
|
+
const why = reason.length > 0 ? ` ${reason.toString()}` : '';
|
|
1174
|
+
this.pushLog(`agent socket closed: ${name} (code ${code}${why}, last pong ${this.pongAgeSeconds(ws)}s ago)`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1089
1177
|
this.wsRoles.delete(ws);
|
|
1090
1178
|
this.wsExternal.delete(ws);
|
|
1091
1179
|
this.forgetShutdownRequesters(ws);
|
|
@@ -2040,6 +2128,7 @@ export class RelayServer {
|
|
|
2040
2128
|
// gone and its in-flight screenshots would be undiscoverable — reject them here instead.
|
|
2041
2129
|
// The rebound sessions have already moved off `old`, so this no longer covers them.
|
|
2042
2130
|
this.evictAgentSocket(old, 'replaced');
|
|
2131
|
+
this.explainedCloses.set(old, 'replaced');
|
|
2043
2132
|
old.terminate();
|
|
2044
2133
|
}
|
|
2045
2134
|
}
|
|
@@ -2097,7 +2186,9 @@ export class RelayServer {
|
|
|
2097
2186
|
// transition, matching the disconnect line in evictAgentSocket.
|
|
2098
2187
|
// `||`, not `??`: the schema defaults both of these to `''` for an agent that omits them, so `??`
|
|
2099
2188
|
// would print an empty name and an empty platform where this used to print `unknown`.
|
|
2100
|
-
|
|
2189
|
+
this.agentNames.set(ws, msg.agentName || msg.agentId || 'unknown');
|
|
2190
|
+
// Through `pushLog` too, so `tapflow logs` shows an agent coming back after the line about it leaving.
|
|
2191
|
+
this.pushLog(`agent connected: ${msg.agentName || msg.agentId || 'unknown'} (${msg.platform || 'unknown'}) — ${registeredSessions.length} device(s)`);
|
|
2101
2192
|
}
|
|
2102
2193
|
/** The only producer of `error` — all five exits below, and nothing else in the repo sends that message.
|
|
2103
2194
|
*
|
|
@@ -2791,6 +2882,7 @@ export class RelayServer {
|
|
|
2791
2882
|
this.resourceBuffers.clear();
|
|
2792
2883
|
}
|
|
2793
2884
|
async handleRequest(req, res) {
|
|
2885
|
+
this.warnTrustedProxyWithoutXffOnce(req);
|
|
2794
2886
|
const corsHeaders = resolveCorsHeaders(req.headers.origin, this.corsAllowed);
|
|
2795
2887
|
if (corsHeaders) {
|
|
2796
2888
|
for (const [k, v] of Object.entries(corsHeaders))
|