@statewalker/httpeers-share 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +75 -2
- package/dist/bridge/bridge.d.ts +37 -0
- package/dist/bridge/bridge.d.ts.map +1 -0
- package/dist/bridge/bridge.js +195 -0
- package/dist/bridge/index.d.ts +8 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +7 -0
- package/dist/bridge/serve.d.ts +20 -0
- package/dist/bridge/serve.d.ts.map +1 -0
- package/dist/bridge/serve.js +66 -0
- package/dist/bridge/sockets.d.ts +47 -0
- package/dist/bridge/sockets.d.ts.map +1 -0
- package/dist/bridge/sockets.js +86 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/invite-link.d.ts +21 -0
- package/dist/invite-link.d.ts.map +1 -0
- package/dist/invite-link.js +38 -0
- package/dist/join-outcome.d.ts +16 -0
- package/dist/join-outcome.d.ts.map +1 -0
- package/dist/join-outcome.js +32 -0
- package/dist/lifecycle.d.ts +19 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +28 -0
- package/dist/policy.d.ts +19 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +40 -0
- package/dist/probe.d.ts +20 -0
- package/dist/probe.d.ts.map +1 -0
- package/dist/probe.js +60 -0
- package/dist/service-table.d.ts +33 -0
- package/dist/service-table.d.ts.map +1 -0
- package/dist/service-table.js +59 -0
- package/dist/share-router.d.ts +15 -0
- package/dist/share-router.d.ts.map +1 -0
- package/dist/share-router.js +31 -0
- package/dist/socket-frames.d.ts +30 -0
- package/dist/socket-frames.d.ts.map +1 -0
- package/dist/socket-frames.js +81 -0
- package/dist/socket-tunnel.d.ts +46 -0
- package/dist/socket-tunnel.d.ts.map +1 -0
- package/dist/socket-tunnel.js +157 -0
- package/dist/stamp-guest.d.ts +7 -0
- package/dist/stamp-guest.d.ts.map +1 -0
- package/dist/stamp-guest.js +17 -0
- package/dist/targets.d.ts +18 -0
- package/dist/targets.d.ts.map +1 -0
- package/dist/targets.js +48 -0
- package/dist/upstreams.d.ts +47 -0
- package/dist/upstreams.d.ts.map +1 -0
- package/dist/upstreams.js +141 -0
- package/package.json +59 -3
- package/src/bridge/bridge.ts +229 -0
- package/src/bridge/index.ts +17 -0
- package/src/bridge/serve.ts +88 -0
- package/src/bridge/sockets.ts +119 -0
- package/src/index.ts +50 -0
- package/src/invite-link.ts +48 -0
- package/src/join-outcome.ts +33 -0
- package/src/lifecycle.ts +31 -0
- package/src/policy.ts +52 -0
- package/src/probe.ts +86 -0
- package/src/service-table.ts +91 -0
- package/src/share-router.ts +43 -0
- package/src/socket-frames.ts +83 -0
- package/src/socket-tunnel.ts +167 -0
- package/src/stamp-guest.ts +18 -0
- package/src/targets.ts +51 -0
- package/src/upstreams.ts +149 -0
package/src/upstreams.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream kinds: how the Sharer's tab reaches a Service's local URL.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MESH_CREDENTIAL_HEADERS } from "@statewalker/httpeers-core";
|
|
6
|
+
import { MARKER, type Upstream, urlUpstream } from "@statewalker/webrun-http-proxy";
|
|
7
|
+
|
|
8
|
+
/** Which local URL the Bridge relays to. Set by the Sharer, never by a Guest. */
|
|
9
|
+
export const BRIDGE_TARGET_HEADER = "x-peershare-target";
|
|
10
|
+
/** The Pairing token the Bridge printed at launch. */
|
|
11
|
+
export const BRIDGE_TOKEN_HEADER = "x-peershare-bridge-token";
|
|
12
|
+
/** Marks the Bridge's own responses, so they are never mistaken for the Service's. */
|
|
13
|
+
export const BRIDGE_MARKER = "x-peershare-bridge";
|
|
14
|
+
/**
|
|
15
|
+
* A Service's redirect, restated by the Bridge as a 200 with these two
|
|
16
|
+
* headers: a browser makes every `redirect: "manual"` 3xx opaque (status 0,
|
|
17
|
+
* no `Location`), so a real 3xx would reach the page with nothing in it.
|
|
18
|
+
*/
|
|
19
|
+
export const BRIDGE_REDIRECT_STATUS_HEADER = "x-peershare-redirect-status";
|
|
20
|
+
export const BRIDGE_REDIRECT_LOCATION_HEADER = "x-peershare-redirect-location";
|
|
21
|
+
/**
|
|
22
|
+
* How many cookies the Service tried to set. `Set-Cookie` is a forbidden
|
|
23
|
+
* response header: no page script can read it, so the Bridge says it instead.
|
|
24
|
+
*/
|
|
25
|
+
export const BRIDGE_SET_COOKIE_HEADER = "x-peershare-set-cookie";
|
|
26
|
+
/**
|
|
27
|
+
* Which Guest a request is for, as the Sharer states it from the mesh-proven
|
|
28
|
+
* peer id (rung 16); a Guest's own copy is overwritten. The Bridge keeps one
|
|
29
|
+
* cookie jar per value.
|
|
30
|
+
*/
|
|
31
|
+
export const BRIDGE_GUEST_HEADER = "x-peershare-guest";
|
|
32
|
+
/** Set by a Bridge with cookie jars when it kept the Service's cookies for that Guest. */
|
|
33
|
+
export const BRIDGE_COOKIES_KEPT_HEADER = "x-peershare-cookies";
|
|
34
|
+
/** The Guest id the Probe uses, so a Bridge with cookie jars treats it like any Guest. */
|
|
35
|
+
export const PROBE_GUEST = "peershare-probe";
|
|
36
|
+
|
|
37
|
+
export type UpstreamConfig =
|
|
38
|
+
| { kind: "direct"; target: string }
|
|
39
|
+
| { kind: "bridged"; target: string; bridge: { base: string; token: string } };
|
|
40
|
+
|
|
41
|
+
export function createUpstream(config: UpstreamConfig): Upstream {
|
|
42
|
+
// A Guest's membership token and peer id are consumed by the mesh hop:
|
|
43
|
+
// Dirk's app has no business seeing them, whichever way it is reached.
|
|
44
|
+
// A Guest's copy of the Bridge headers goes too: the target and token are
|
|
45
|
+
// the Sharer's to set, and Direct would otherwise hand them to the app.
|
|
46
|
+
const stripRequestHeaders = [
|
|
47
|
+
...MESH_CREDENTIAL_HEADERS,
|
|
48
|
+
BRIDGE_TARGET_HEADER,
|
|
49
|
+
BRIDGE_TOKEN_HEADER,
|
|
50
|
+
];
|
|
51
|
+
if (config.kind === "direct") {
|
|
52
|
+
return inGuestOrigin(
|
|
53
|
+
urlUpstream({
|
|
54
|
+
base: config.target,
|
|
55
|
+
stripRequestHeaders,
|
|
56
|
+
fetchImpl: directFetch(needsLocalAddressSpace(config.target)),
|
|
57
|
+
}),
|
|
58
|
+
config.target,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
// Bridged is a URL upstream too: re-issued at the Bridge instead of the
|
|
62
|
+
// Service, with the real target and the Pairing token as headers.
|
|
63
|
+
const viaBridge = urlUpstream({
|
|
64
|
+
base: config.bridge.base,
|
|
65
|
+
stripRequestHeaders,
|
|
66
|
+
headers: {
|
|
67
|
+
[BRIDGE_TARGET_HEADER]: config.target,
|
|
68
|
+
[BRIDGE_TOKEN_HEADER]: config.bridge.token,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
return inGuestOrigin(async (request) => {
|
|
72
|
+
const response = await viaBridge(request);
|
|
73
|
+
const redirect = response.headers.get(BRIDGE_REDIRECT_STATUS_HEADER);
|
|
74
|
+
if (redirect != null) {
|
|
75
|
+
// Back to a real redirect for the Guest (see BRIDGE_REDIRECT_STATUS_HEADER).
|
|
76
|
+
const headers = new Headers(response.headers);
|
|
77
|
+
headers.set("location", headers.get(BRIDGE_REDIRECT_LOCATION_HEADER) ?? "/");
|
|
78
|
+
headers.delete(BRIDGE_REDIRECT_STATUS_HEADER);
|
|
79
|
+
headers.delete(BRIDGE_REDIRECT_LOCATION_HEADER);
|
|
80
|
+
return new Response(null, { status: Number(redirect), headers });
|
|
81
|
+
}
|
|
82
|
+
// To `urlUpstream` the Bridge IS the upstream, so its "unreachable" means
|
|
83
|
+
// the Bridge is not running, not that the Service is down.
|
|
84
|
+
if (response.headers.get(MARKER) !== "upstream-unreachable") return response;
|
|
85
|
+
const headers = new Headers(response.headers);
|
|
86
|
+
headers.delete(MARKER);
|
|
87
|
+
headers.set(BRIDGE_MARKER, "bridge-unreachable");
|
|
88
|
+
return new Response(response.body, { status: response.status, headers });
|
|
89
|
+
}, config.target);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A Guest sees the Service at the root of their own isolated origin (rung
|
|
94
|
+
* 08), so a redirect the Service writes absolutely to ITSELF
|
|
95
|
+
* (`https://wiki.example.com/login`, `http://127.0.0.1:5173/login`) must
|
|
96
|
+
* become a path, or the Guest's frame leaves that origin. Redirects anywhere
|
|
97
|
+
* else stay absolute.
|
|
98
|
+
*/
|
|
99
|
+
function inGuestOrigin(upstream: Upstream, target: string): Upstream {
|
|
100
|
+
const own = new URL(target).origin;
|
|
101
|
+
return async (request) => {
|
|
102
|
+
const response = await upstream(request);
|
|
103
|
+
const location = response.headers.get("location");
|
|
104
|
+
if (response.status < 300 || response.status >= 400 || location == null) return response;
|
|
105
|
+
const resolved = new URL(location, target);
|
|
106
|
+
if (resolved.origin !== own) return response;
|
|
107
|
+
const headers = new Headers(response.headers);
|
|
108
|
+
headers.set("location", `${resolved.pathname}${resolved.search}${resolved.hash}`);
|
|
109
|
+
return new Response(response.body, { status: response.status, headers });
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Which hop did not answer, read off an `Upstream`'s response; `undefined` when something did. */
|
|
114
|
+
export function unreachable(response: Response): "service" | "bridge" | undefined {
|
|
115
|
+
const bridge = response.headers.get(BRIDGE_MARKER);
|
|
116
|
+
if (bridge === "bridge-unreachable") return "bridge";
|
|
117
|
+
if (bridge === "target-unreachable") return "service";
|
|
118
|
+
if (response.headers.get(MARKER) === "upstream-unreachable") return "service";
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A plain-http Service on the LAN, called from the https PeerShare page, is
|
|
124
|
+
* mixed content and Chromium blocks it — unless the call says the target is
|
|
125
|
+
* on the local network, which also lets Local Network Access ask the Sharer
|
|
126
|
+
* (measured in rung 03). Loopback needs nothing: it is trusted already.
|
|
127
|
+
*/
|
|
128
|
+
function needsLocalAddressSpace(target: string): boolean {
|
|
129
|
+
const url = new URL(target);
|
|
130
|
+
if (url.protocol !== "http:") return false;
|
|
131
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
132
|
+
return !(host === "localhost" || host === "::1" || /^127\./.test(host));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* How Direct calls the Service. Redirects are FOLLOWED: in the Sharer's
|
|
137
|
+
* browser tab a `redirect: "manual"` response is opaque (status 0, no
|
|
138
|
+
* `Location`), so following is the only way to serve a site that redirects
|
|
139
|
+
* (rung 04). Same everywhere, so Node and the browser agree.
|
|
140
|
+
* `targetAddressSpace` is Chromium's; other runtimes ignore it.
|
|
141
|
+
*/
|
|
142
|
+
function directFetch(local: boolean): typeof fetch {
|
|
143
|
+
return (input, init) =>
|
|
144
|
+
fetch(input, {
|
|
145
|
+
...init,
|
|
146
|
+
redirect: "follow",
|
|
147
|
+
...(local ? { targetAddressSpace: "local" } : {}),
|
|
148
|
+
} as RequestInit);
|
|
149
|
+
}
|