@tapflowio/relay 0.21.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 +20 -0
- package/dist/RelayServer.d.ts.map +1 -1
- package/dist/RelayServer.js +124 -16
- 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/api/team.d.ts +2 -1
- package/dist/api/team.d.ts.map +1 -1
- package/dist/api/team.js +7 -5
- package/dist/api/team.js.map +1 -1
- package/dist/index.d.ts +3 -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/lanAddress.d.ts +10 -0
- package/dist/lib/lanAddress.d.ts.map +1 -1
- package/dist/lib/lanAddress.js +13 -0
- package/dist/lib/lanAddress.js.map +1 -1
- package/dist/lib/proxyConfig.d.ts +14 -2
- package/dist/lib/proxyConfig.d.ts.map +1 -1
- package/dist/lib/proxyConfig.js +36 -13
- package/dist/lib/proxyConfig.js.map +1 -1
- package/dist/lib/publicUrl.d.ts +31 -1
- package/dist/lib/publicUrl.d.ts.map +1 -1
- package/dist/lib/publicUrl.js +51 -9
- package/dist/lib/publicUrl.js.map +1 -1
- package/dist/server.js +10 -2
- 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-D1inA8sU.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-ChaVEWvP.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-DObzVJxQ.js +3 -0
- 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-Ut9BFums.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-CoHwaxo7.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-pEnCy-7l.js +1 -0
- 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-CikXk90N.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-Bxs7CdEi.css +1 -0
- package/public/assets/index-Bxs7CdEi.css.br +0 -0
- package/public/assets/index-Bxs7CdEi.css.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/publicLink-D6zKS_wu.js +1 -0
- package/public/assets/publicLink-D6zKS_wu.js.br +0 -0
- package/public/assets/publicLink-D6zKS_wu.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-dbLmBL_w.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-YPhPkjLw.js +5 -0
- 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 +4 -4
- package/public/index.html.br +0 -0
- package/public/index.html.gz +0 -0
- package/public/assets/AppCenter-BkSQFKRv.js +0 -21
- package/public/assets/AppCenter-BkSQFKRv.js.br +0 -0
- package/public/assets/AppCenter-BkSQFKRv.js.gz +0 -0
- package/public/assets/Default-D1inA8sU.js.br +0 -0
- package/public/assets/Default-D1inA8sU.js.gz +0 -0
- package/public/assets/Invite-ChaVEWvP.js.br +0 -0
- package/public/assets/Invite-ChaVEWvP.js.gz +0 -0
- package/public/assets/MacResources-CLChFyJW.js +0 -1
- package/public/assets/MacResources-CLChFyJW.js.br +0 -0
- package/public/assets/MacResources-CLChFyJW.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-DICynZa0.js +0 -140
- package/public/assets/QASession-DICynZa0.js.br +0 -0
- package/public/assets/QASession-DICynZa0.js.gz +0 -0
- package/public/assets/ResetPassword-Ut9BFums.js.br +0 -0
- package/public/assets/ResetPassword-Ut9BFums.js.gz +0 -0
- package/public/assets/Setup-CoHwaxo7.js.br +0 -0
- package/public/assets/Setup-CoHwaxo7.js.gz +0 -0
- package/public/assets/Team-DaVY1DGm.js +0 -6
- package/public/assets/Team-DaVY1DGm.js.br +0 -0
- package/public/assets/Team-DaVY1DGm.js.gz +0 -0
- package/public/assets/Tokens-_KZJ7AZ9.js +0 -1
- package/public/assets/Tokens-_KZJ7AZ9.js.br +0 -0
- package/public/assets/Tokens-_KZJ7AZ9.js.gz +0 -0
- package/public/assets/alert-dialog-Cc_pQpe6.js +0 -1
- package/public/assets/alert-dialog-Cc_pQpe6.js.br +0 -0
- package/public/assets/alert-dialog-Cc_pQpe6.js.gz +0 -0
- package/public/assets/badge-CikXk90N.js.br +0 -0
- package/public/assets/badge-CikXk90N.js.gz +0 -0
- package/public/assets/build-format-DsApFPHy.js +0 -11
- package/public/assets/build-format-DsApFPHy.js.br +0 -0
- package/public/assets/build-format-DsApFPHy.js.gz +0 -0
- package/public/assets/dialog-B4Wg7TCg.js +0 -11
- package/public/assets/dialog-B4Wg7TCg.js.br +0 -0
- package/public/assets/dialog-B4Wg7TCg.js.gz +0 -0
- package/public/assets/index-CBfjqtE-.js +0 -198
- package/public/assets/index-CBfjqtE-.js.br +0 -0
- package/public/assets/index-CBfjqtE-.js.gz +0 -0
- package/public/assets/index-ClHUQm-F.css +0 -1
- package/public/assets/index-ClHUQm-F.css.br +0 -0
- package/public/assets/index-ClHUQm-F.css.gz +0 -0
- package/public/assets/pencil-DMswV31e.js +0 -6
- package/public/assets/pencil-DMswV31e.js.br +0 -0
- package/public/assets/pencil-DMswV31e.js.gz +0 -0
- package/public/assets/table-dbLmBL_w.js.br +0 -0
- package/public/assets/table-dbLmBL_w.js.gz +0 -0
- package/public/assets/tabs-pPYCRfmT.js +0 -1
- package/public/assets/tabs-pPYCRfmT.js.br +0 -0
- package/public/assets/tabs-pPYCRfmT.js.gz +0 -0
- package/public/assets/trash-VgeHmI8N.js +0 -11
- package/public/assets/trash-VgeHmI8N.js.br +0 -0
- package/public/assets/trash-VgeHmI8N.js.gz +0 -0
- package/public/assets/vendor-charts-DyX5CBcf.js +0 -5
- package/public/assets/vendor-charts-DyX5CBcf.js.br +0 -0
- package/public/assets/vendor-charts-DyX5CBcf.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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TunnelRuntime } from './lib/publicUrl.js';
|
|
1
2
|
export declare function isExternalAddress(addr: string): boolean;
|
|
2
3
|
/** Exported for the re-join test, which pins both edges of this window rather than hardcoding 500 —
|
|
3
4
|
* a test carrying its own copy of the number passes when the constant moves, which is the drift the
|
|
@@ -29,6 +30,20 @@ export declare function ownerKeyFor(userId: number | string | undefined, claimed
|
|
|
29
30
|
export declare class RelayServer {
|
|
30
31
|
private readonly options;
|
|
31
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;
|
|
32
47
|
private wss;
|
|
33
48
|
private sessions;
|
|
34
49
|
private publicDir;
|
|
@@ -112,12 +127,17 @@ export declare class RelayServer {
|
|
|
112
127
|
key: string;
|
|
113
128
|
};
|
|
114
129
|
agentGraceMs?: number;
|
|
130
|
+
tunnel?: TunnelRuntime;
|
|
131
|
+
tunnelPort?: number;
|
|
115
132
|
});
|
|
116
133
|
private registerRoutes;
|
|
117
134
|
pushLog(msg: string): void;
|
|
118
135
|
start(): Promise<void>;
|
|
136
|
+
private listenTunnel;
|
|
119
137
|
stop(): Promise<void>;
|
|
120
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;
|
|
121
141
|
private runHeartbeat;
|
|
122
142
|
/**
|
|
123
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":"
|
|
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;IA6GV,OAAO,CAAC,QAAQ,CAAC,OAAO;IA5GpC,OAAO,CAAC,UAAU,CAA4B;IAC9C;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,YAAY,CAA0C;IAC9D,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,eAAe,CAAsD;IAC7E,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,oBAAoB,CAA8C;IAC1E,OAAO,CAAC,sBAAsB,CAA8C;IAC5E,OAAO,CAAC,gBAAgB,CAA8C;IACtE,OAAO,CAAC,mBAAmB,CAA8C;IACzE,OAAO,CAAC,cAAc,CAA8C;IACpE;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,UAAU,CAAmC;IACrD;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,QAAQ,CAA2E;IAC3F,OAAO,CAAC,YAAY,CAAgC;IAEpD,OAAO,CAAC,iBAAiB,CAAgC;IAEzD,OAAO,CAAC,QAAQ,CAAyC;IAEzD,OAAO,CAAC,aAAa,CAAgC;IACrD;gCAC4B;IAC5B,OAAO,CAAC,sBAAsB,CAAuC;IACrE,OAAO,CAAC,OAAO,CAAuD;IACtE;uGACmG;IACnG,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA+D;IAM5F,OAAO,CAAC,UAAU,CAAsD;IAIxE,OAAO,CAAC,QAAQ,CAAQ;IAGxB,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAQ;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IAEzC,OAAO,CAAC,oBAAoB,CAAQ;IACpC,mGAAmG;IACnG,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAoB;IAC1D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyB;IACtD,OAAO,CAAC,kBAAkB,CAKtB;IACJ,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,cAAc,CAKlB;gBAEyB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,GAAG,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,aAAa,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAkEzW,OAAO,CAAC,cAAc;IA2GtB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAO1B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoCtB,OAAO,CAAC,YAAY;IAyBpB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgCrB,OAAO;IAIP,wFAAwF;IACxF,aAAa;IAKb,OAAO,CAAC,YAAY;IAOpB;;;;;;;;OAQG;IACH,OAAO,CAAC,OAAO;IAYf,gBAAgB,CAAC,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAQ/D;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,kBAAkB;IAS1B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,YAAY;IAiBpB;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,gBAAgB;IAuKxB;;;;;;;;;;OAUG;IACH;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAwClB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,KAAK;IAyZb,OAAO,CAAC,oBAAoB;IAc5B;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM;IAKd;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAWrB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,eAAe;IAkCvB,+EAA+E;IAC/E,OAAO,CAAC,WAAW;IAOnB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IA+BxB,OAAO,CAAC,mBAAmB;IAgG3B;;;;;;4GAMwG;IACxG,OAAO,CAAC,kBAAkB;IAwI1B;wFACoF;IACpF,OAAO,CAAC,YAAY;IAUpB;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;IAYtB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,+BAA+B;IASvC;;;;;;;;;;gFAU4E;IAC5E,OAAO,CAAC,WAAW;IAInB;qGACiG;IACjG,OAAO,CAAC,OAAO;IAIf,2GAA2G;IAC3G,OAAO,CAAC,MAAM;IAId;;;;;;;;;;kDAU8C;IAC9C,OAAO,CAAC,WAAW;IASnB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,WAAW;IAiBnB;;;;uGAImG;IACnG,OAAO,CAAC,cAAc;IActB;;;;;0FAKsF;IACtF,OAAO,CAAC,gBAAgB;IAoCxB;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,eAAe;IAyBvB;;;;;;iDAM6C;IAC7C,OAAO,CAAC,WAAW;IAanB;;;;;;+EAM2E;IAC3E,OAAO,CAAC,uBAAuB;IAqE/B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,mBAAmB;IAS3B,6FAA6F;IAC7F,OAAO,CAAC,sBAAsB;YAoChB,mBAAmB;YAwDnB,eAAe;IAoD7B,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,oBAAoB;IAuB5B,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,oBAAoB;YAed,aAAa;IAoC3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,mBAAmB;IAmC3B,OAAO,CAAC,WAAW;IAmBnB,OAAO,CAAC,WAAW;CAwFpB"}
|
package/dist/RelayServer.js
CHANGED
|
@@ -10,11 +10,13 @@ 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';
|
|
17
|
-
import { pickLanAddress } from './lib/lanAddress.js';
|
|
17
|
+
import { pickLanAddress, runningInContainer } from './lib/lanAddress.js';
|
|
18
|
+
import { config } from './lib/config.js';
|
|
19
|
+
import { forTeammates, resolveAgentRelayUrl, resolvePublicBaseUrl } from './lib/publicUrl.js';
|
|
18
20
|
import { createTrailingRequester, systemTimerScheduler } from './lib/trailingRequester.js';
|
|
19
21
|
import { getDb } from './db.js';
|
|
20
22
|
import { handleLogin, handleLogout, handleMe, handleChangePassword, handleInit, handleAuthStatus } from './api/auth.js';
|
|
@@ -154,6 +156,20 @@ function describeRejection(failure, suppressed) {
|
|
|
154
156
|
export class RelayServer {
|
|
155
157
|
options;
|
|
156
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;
|
|
157
173
|
wss;
|
|
158
174
|
sessions;
|
|
159
175
|
publicDir;
|
|
@@ -238,6 +254,10 @@ export class RelayServer {
|
|
|
238
254
|
pendingUITrees = new Map();
|
|
239
255
|
constructor(options) {
|
|
240
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
|
+
}
|
|
241
261
|
this.backpressureBytes = options.wsBackpressureBytes ?? DEFAULT_BACKPRESSURE_BYTES;
|
|
242
262
|
this.screenshotTimeoutMs = options.screenshotTimeoutMs ?? 10_000;
|
|
243
263
|
// Longer than the screenshot default: the Android agent's device-side dump
|
|
@@ -278,6 +298,24 @@ export class RelayServer {
|
|
|
278
298
|
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
279
299
|
this.wss.on('connection', (ws, request) => this.handleConnection(ws, request));
|
|
280
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
|
+
}
|
|
281
319
|
}
|
|
282
320
|
registerRoutes() {
|
|
283
321
|
const u = this.uploadsDir;
|
|
@@ -322,10 +360,10 @@ export class RelayServer {
|
|
|
322
360
|
this.router.delete('/api/v1/comments/:id', handleDeleteComment);
|
|
323
361
|
// team
|
|
324
362
|
this.router.get('/api/v1/team/members', handleListMembers);
|
|
325
|
-
this.router.post('/api/v1/team/invite', handleInvite);
|
|
363
|
+
this.router.post('/api/v1/team/invite', (req, res) => handleInvite(req, res, this.options.tunnel));
|
|
326
364
|
this.router.patch('/api/v1/team/members/:id', handleUpdateMember);
|
|
327
365
|
this.router.delete('/api/v1/team/members/:id', handleDeleteMember);
|
|
328
|
-
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));
|
|
329
367
|
// tokens
|
|
330
368
|
this.router.get('/api/v1/tokens', handleListTokens);
|
|
331
369
|
this.router.post('/api/v1/tokens', handleCreateToken);
|
|
@@ -346,14 +384,22 @@ export class RelayServer {
|
|
|
346
384
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
347
385
|
res.end(JSON.stringify(this.logBuffer.slice(-lines)));
|
|
348
386
|
});
|
|
349
|
-
//
|
|
387
|
+
// What the dashboard needs to build an address for someone else (#271, #788): a link base for a
|
|
388
|
+
// teammate's browser and an agent address, reported separately because they come from different
|
|
389
|
+
// settings (see lib/publicUrl.ts). Config comes from the singleton, as in the team handlers; whether a
|
|
390
|
+
// tunnel actually came up is known only to the entry point that started it, hence the `tunnel` option.
|
|
350
391
|
this.router.get('/api/v1/relay/host', (req, res) => {
|
|
351
392
|
if (!requireAuth(req, res))
|
|
352
393
|
return;
|
|
353
394
|
const addr = this.httpServer.address();
|
|
354
395
|
const port = typeof addr === 'object' && addr !== null ? addr.port : this.options.port;
|
|
355
396
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
356
|
-
res.end(JSON.stringify({
|
|
397
|
+
res.end(JSON.stringify({
|
|
398
|
+
lanHost: runningInContainer() ? null : pickLanAddress(os.networkInterfaces()),
|
|
399
|
+
port,
|
|
400
|
+
publicBaseUrl: forTeammates(resolvePublicBaseUrl(config, this.options.tunnel)),
|
|
401
|
+
agentRelayUrl: forTeammates(resolveAgentRelayUrl(config)),
|
|
402
|
+
}));
|
|
357
403
|
});
|
|
358
404
|
// agent resources
|
|
359
405
|
this.router.get('/api/v1/agents', handleListAgents);
|
|
@@ -397,7 +443,30 @@ export class RelayServer {
|
|
|
397
443
|
});
|
|
398
444
|
// Bind dual-stack (IPv4 + IPv6). A bare listen(port) binds IPv6-only on some
|
|
399
445
|
// macOS/node setups, so LAN agents connecting over IPv4 (ws://<ipv4>:port) time out.
|
|
400
|
-
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;
|
|
401
470
|
});
|
|
402
471
|
}
|
|
403
472
|
stop() {
|
|
@@ -435,16 +504,27 @@ export class RelayServer {
|
|
|
435
504
|
for (const requester of this.networkStateRequesters.values())
|
|
436
505
|
requester.dispose();
|
|
437
506
|
this.networkStateRequesters.clear();
|
|
438
|
-
|
|
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) => {
|
|
439
514
|
this.wss.clients.forEach((ws) => ws.terminate());
|
|
440
515
|
this.wss.close(() => {
|
|
441
516
|
this.httpServer.close((err) => (err ? reject(err) : resolve()));
|
|
442
517
|
});
|
|
443
518
|
});
|
|
519
|
+
return Promise.all([closeMain, closeTunnel]).then(() => undefined);
|
|
444
520
|
}
|
|
445
521
|
address() {
|
|
446
522
|
return this.httpServer.address();
|
|
447
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
|
+
}
|
|
448
528
|
// Terminate sockets that missed the previous pong; ping the rest. Covers all roles via wss.clients.
|
|
449
529
|
runHeartbeat(clients = this.wss.clients) {
|
|
450
530
|
for (const ws of clients) {
|
|
@@ -478,8 +558,10 @@ export class RelayServer {
|
|
|
478
558
|
}
|
|
479
559
|
// 갱신된 cert를 재시작 없이 핫스왑한다(https 종단일 때만 의미 있음).
|
|
480
560
|
updateTlsContext(material) {
|
|
481
|
-
|
|
482
|
-
|
|
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
|
+
}
|
|
483
565
|
}
|
|
484
566
|
}
|
|
485
567
|
/**
|
|
@@ -594,8 +676,9 @@ export class RelayServer {
|
|
|
594
676
|
const a = socketAddr.replace(/^::ffff:/, '');
|
|
595
677
|
if (a === '::1' || a.startsWith('127.')) {
|
|
596
678
|
logger.warn('Received X-Forwarded-For from a loopback connection but TAPFLOW_TRUSTED_PROXIES is unset. ' +
|
|
597
|
-
'If
|
|
598
|
-
'
|
|
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.');
|
|
599
682
|
this.warnedProxyMisconfig = true;
|
|
600
683
|
}
|
|
601
684
|
}
|
|
@@ -603,11 +686,15 @@ export class RelayServer {
|
|
|
603
686
|
const socketAddr = this.remoteAddressOf(request);
|
|
604
687
|
const xff = request.headers['x-forwarded-for'];
|
|
605
688
|
const forwardedFor = Array.isArray(xff) ? xff[0] : xff;
|
|
606
|
-
|
|
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);
|
|
607
693
|
const { addr, isLocal } = resolveClientAddress({
|
|
608
694
|
socketAddr,
|
|
609
695
|
forwardedFor,
|
|
610
696
|
trustedProxies: this.options.trustedProxies ?? [],
|
|
697
|
+
viaTunnel,
|
|
611
698
|
});
|
|
612
699
|
const hasCookieAuth = getAuth(request) !== null;
|
|
613
700
|
// DB lookup — only when the connection can't be classified without it (remote, no cookie).
|
|
@@ -954,15 +1041,32 @@ export class RelayServer {
|
|
|
954
1041
|
// two-member union** while `AgentRegister.platform` is `string`, open so that a third-party
|
|
955
1042
|
// platform can register through `AgentRegistry.register()` (root AGENTS.md, OCP). Validating
|
|
956
1043
|
// this would refuse a platform the repo promises to support — and refusing costs more than a
|
|
957
|
-
// dropped frame
|
|
958
|
-
// empties what the re-join replay reads.
|
|
959
|
-
//
|
|
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.
|
|
960
1050
|
this.sessions.setChromeData(session.id, raw['payload']);
|
|
961
1051
|
if (session.browserSocket?.readyState === WebSocket.OPEN) {
|
|
962
1052
|
session.browserSocket.send(JSON.stringify(raw));
|
|
963
1053
|
}
|
|
964
1054
|
break;
|
|
965
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
|
+
}
|
|
966
1070
|
case 'session:deviceInfo': {
|
|
967
1071
|
const session = this.sessions.get(msg.sessionId);
|
|
968
1072
|
if (!session)
|
|
@@ -1218,6 +1322,7 @@ export class RelayServer {
|
|
|
1218
1322
|
case 'input:pinch:start':
|
|
1219
1323
|
case 'input:pinch:move':
|
|
1220
1324
|
case 'input:rotate':
|
|
1325
|
+
case 'input:posture':
|
|
1221
1326
|
case 'input:keyboard:toggle': {
|
|
1222
1327
|
// No ack, so no correlator to check and nothing to answer. An unowned frame is dropped rather
|
|
1223
1328
|
// than refused for the same reason: there is no waiter to tell.
|
|
@@ -1617,6 +1722,9 @@ export class RelayServer {
|
|
|
1617
1722
|
if (session.deviceInfo) {
|
|
1618
1723
|
this.sendTo(ws, { type: 'session:deviceInfo', sessionId: session.id, payload: session.deviceInfo });
|
|
1619
1724
|
}
|
|
1725
|
+
if (session.postures) {
|
|
1726
|
+
this.sendTo(ws, { type: 'device:postures', sessionId: session.id, payload: session.postures });
|
|
1727
|
+
}
|
|
1620
1728
|
// Replay device:ready only if this session actually announced one (browser WS blip reconnect).
|
|
1621
1729
|
// Not `deviceStatus`: that starts from the agent's `simctl list` snapshot, so a session for a
|
|
1622
1730
|
// simulator that was already running would fire this before the agent had done anything —
|