@tesser-sh/gateway 0.53.2 → 0.55.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/index.js +225 -162
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -43,10 +43,8 @@ li .name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
|
43
43
|
.ports a { color: inherit; text-decoration: none; }
|
|
44
44
|
.ports a:hover { color: var(--bone); text-decoration: underline; text-decoration-color: var(--remote); text-underline-offset: 3px; }
|
|
45
45
|
tesser-use { color: var(--faint); }
|
|
46
|
-
tesser-wake { display: block; margin-top: 24px; }
|
|
47
46
|
.bad { margin-top: 24px; color: var(--bad); font-size: 13px; }
|
|
48
47
|
.open { margin-top: 24px; padding: 9px 16px; background: var(--remote); color: #171717; border: 0; font: 500 14px var(--sans); cursor: pointer; }
|
|
49
|
-
.open:disabled { background: var(--rule); color: var(--dim); cursor: default; }
|
|
50
48
|
a.open { display: inline-block; text-decoration: none; }
|
|
51
49
|
.sub { margin-top: 8px; color: var(--dim); }
|
|
52
50
|
.steps { list-style: none; padding: 0; margin-top: 24px; }
|
|
@@ -98,9 +96,11 @@ document.addEventListener("click", (event) => {
|
|
|
98
96
|
// ../front/src/viewer.ts
|
|
99
97
|
var STATUS = {
|
|
100
98
|
waking: 503,
|
|
99
|
+
asleep: 503,
|
|
101
100
|
booting: 503,
|
|
102
101
|
"not-answering": 504,
|
|
103
102
|
stopped: 502,
|
|
103
|
+
idle: 502,
|
|
104
104
|
"wake-failed": 503,
|
|
105
105
|
"no-port": 404,
|
|
106
106
|
"not-shared": 404,
|
|
@@ -113,12 +113,16 @@ function viewerText(state) {
|
|
|
113
113
|
switch (state.kind) {
|
|
114
114
|
case "waking":
|
|
115
115
|
return `${state.box.name} is waking up; retry shortly`;
|
|
116
|
+
case "asleep":
|
|
117
|
+
return `${state.box.name} is asleep; open it in a browser to wake it`;
|
|
116
118
|
case "booting":
|
|
117
119
|
return `${state.box.name} is booting; retry shortly`;
|
|
118
120
|
case "not-answering":
|
|
119
121
|
return `${state.box.name} isn't answering on port ${state.port}`;
|
|
120
122
|
case "stopped":
|
|
121
123
|
return `${state.box.name} has stopped`;
|
|
124
|
+
case "idle":
|
|
125
|
+
return `nothing is running on ${state.box.name}`;
|
|
122
126
|
case "wake-failed":
|
|
123
127
|
return `${state.box.name} didn't wake up: ${state.reason}`;
|
|
124
128
|
case "no-port":
|
|
@@ -143,7 +147,10 @@ function steps(stage, port, seconds) {
|
|
|
143
147
|
function who(box) {
|
|
144
148
|
return box.sharedBy === null ? "" : `<p class="who">Shared by ${escapeHtml(box.sharedBy)}</p>`;
|
|
145
149
|
}
|
|
146
|
-
function
|
|
150
|
+
function cmd(command) {
|
|
151
|
+
return `<div class="cmd"><code>${escapeHtml(command)}</code><button type="button" data-copy="${escapeHtml(command)}">Copy</button></div>`;
|
|
152
|
+
}
|
|
153
|
+
function body(state, owner) {
|
|
147
154
|
switch (state.kind) {
|
|
148
155
|
case "waking":
|
|
149
156
|
return {
|
|
@@ -152,6 +159,13 @@ function body(state) {
|
|
|
152
159
|
retry: !0,
|
|
153
160
|
rest: `${who(state.box)}${steps(state.stage, state.port, state.seconds)}<p class="hint">Usually about 30 seconds. This page opens the app by itself.</p>`
|
|
154
161
|
};
|
|
162
|
+
case "asleep":
|
|
163
|
+
return {
|
|
164
|
+
headline: `${state.box.name} is asleep`,
|
|
165
|
+
spinner: !1,
|
|
166
|
+
retry: !1,
|
|
167
|
+
rest: `${who(state.box)}<p class="sub">It went to sleep while nobody was using it.</p><a class="open" href="">Wake it</a>`
|
|
168
|
+
};
|
|
155
169
|
case "booting":
|
|
156
170
|
return { headline: `${state.box.name} is booting`, spinner: !0, retry: !0, rest: `${who(state.box)}<p class="hint">A fresh machine takes a minute or two. This page opens the app by itself.</p>` };
|
|
157
171
|
case "not-answering":
|
|
@@ -159,21 +173,28 @@ function body(state) {
|
|
|
159
173
|
headline: `${state.box.name} isn't answering on port ${state.port}`,
|
|
160
174
|
spinner: !0,
|
|
161
175
|
retry: !0,
|
|
162
|
-
rest: `${who(state.box)}<p class="hint">The box is up but the app hasn't answered yet. Still trying.</p
|
|
176
|
+
rest: `${who(state.box)}<p class="hint">The box is up but the app hasn't answered yet. Still trying.</p>${owner ? cmd(`tesser logs ${state.box.boxId} -f`) : ""}`
|
|
163
177
|
};
|
|
164
178
|
case "stopped":
|
|
165
179
|
return {
|
|
166
180
|
headline: `${state.box.name} has stopped`,
|
|
167
181
|
spinner: !1,
|
|
168
182
|
retry: !0,
|
|
169
|
-
rest: `${who(state.box)}<p class="sub">The app on this box exited. ${state.box.sharedBy === null ? "Ask whoever shared it" : "Ask them"} to restart it.</p>`
|
|
183
|
+
rest: owner ? cmd(`tesser logs ${state.box.boxId}`) : `${who(state.box)}<p class="sub">The app on this box exited. ${state.box.sharedBy === null ? "Ask whoever shared it" : "Ask them"} to restart it.</p>`
|
|
184
|
+
};
|
|
185
|
+
case "idle":
|
|
186
|
+
return {
|
|
187
|
+
headline: `Nothing is running on ${state.box.name}`,
|
|
188
|
+
spinner: !1,
|
|
189
|
+
retry: !0,
|
|
190
|
+
rest: owner ? cmd("tesser dev") : `${who(state.box)}<p class="sub">${state.box.sharedBy === null ? "Ask whoever shared it" : "Ask them"} to start the app.</p>`
|
|
170
191
|
};
|
|
171
192
|
case "wake-failed":
|
|
172
193
|
return {
|
|
173
194
|
headline: `${state.box.name} didn't wake up`,
|
|
174
195
|
spinner: !1,
|
|
175
196
|
retry: !1,
|
|
176
|
-
rest: `${who(state.box)}<p class="bad">${escapeHtml(state.reason)}</p><
|
|
197
|
+
rest: `${who(state.box)}<p class="bad">${escapeHtml(state.reason)}</p><a class="open" href="">Try again</a>`
|
|
177
198
|
};
|
|
178
199
|
case "no-port":
|
|
179
200
|
return {
|
|
@@ -188,12 +209,12 @@ function body(state) {
|
|
|
188
209
|
return { headline: "Can't check what's shared", spinner: !0, retry: !0, rest: `<p class="sub">This gateway can't reach tesser right now. Retrying.</p>` };
|
|
189
210
|
}
|
|
190
211
|
}
|
|
191
|
-
function viewerPage(state) {
|
|
192
|
-
let { headline, spinner, retry, rest } = body(state), main = `<main>
|
|
212
|
+
function viewerPage(state, owner = !1) {
|
|
213
|
+
let { headline, spinner, retry, rest } = body(state, owner), main = `<main>
|
|
193
214
|
<h1>${spinner ? '<span class="sq"></span>' : ""}${escapeHtml(headline)}</h1>
|
|
194
215
|
${rest}
|
|
195
216
|
</main>`;
|
|
196
|
-
return shell(`${headline} · tesser`, main, retry ? RETRY : "",
|
|
217
|
+
return shell(`${headline} · tesser`, main, retry ? RETRY : "", owner);
|
|
197
218
|
}
|
|
198
219
|
|
|
199
220
|
// ../protocol/src/table.ts
|
|
@@ -320,7 +341,7 @@ function decodeFrame(buf) {
|
|
|
320
341
|
}
|
|
321
342
|
|
|
322
343
|
// src/cloudflare.ts
|
|
323
|
-
import { Effect as
|
|
344
|
+
import { Effect as Effect5, Schema as Schema3 } from "effect";
|
|
324
345
|
|
|
325
346
|
// src/jwt.ts
|
|
326
347
|
import { Clock, Effect, Option } from "effect";
|
|
@@ -382,7 +403,120 @@ function audienceIncludes(aud, expected) {
|
|
|
382
403
|
|
|
383
404
|
// src/relay.ts
|
|
384
405
|
import { DurableObject } from "cloudflare:workers";
|
|
385
|
-
import { Clock as Clock3, Duration as Duration2, Effect as
|
|
406
|
+
import { Clock as Clock3, Duration as Duration2, Effect as Effect4, Layer as Layer2, Schema as Schema2 } from "effect";
|
|
407
|
+
|
|
408
|
+
// ../front/src/http.ts
|
|
409
|
+
var HOP_BY_HOP = [
|
|
410
|
+
"connection",
|
|
411
|
+
"keep-alive",
|
|
412
|
+
"proxy-authenticate",
|
|
413
|
+
"proxy-authorization",
|
|
414
|
+
"proxy-connection",
|
|
415
|
+
"te",
|
|
416
|
+
"trailer",
|
|
417
|
+
"transfer-encoding",
|
|
418
|
+
"upgrade"
|
|
419
|
+
];
|
|
420
|
+
function withoutHopByHop(source) {
|
|
421
|
+
let headers = new Headers(source), connectionHeaders = headers.get("connection")?.split(",").map((name) => name.trim()).filter(Boolean) ?? [];
|
|
422
|
+
for (let name of [...HOP_BY_HOP, ...connectionHeaders])
|
|
423
|
+
headers.delete(name);
|
|
424
|
+
return headers;
|
|
425
|
+
}
|
|
426
|
+
function wantsDocument(request) {
|
|
427
|
+
return request.method === "GET" && (request.headers.get("sec-fetch-mode") === "navigate" || request.headers.get("accept")?.toLowerCase().includes("text/html") === !0);
|
|
428
|
+
}
|
|
429
|
+
function userNavigation(request) {
|
|
430
|
+
return request.headers.get("sec-fetch-mode") === "navigate" && request.headers.get("sec-fetch-user") === "?1";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ../proxy/src/core.ts
|
|
434
|
+
import { Clock as Clock2, Context, Duration, Effect as Effect2, Schedule, Schema } from "effect";
|
|
435
|
+
|
|
436
|
+
class BoxNotShared extends Schema.TaggedError()("BoxNotShared", { boxId: Schema.String }) {
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
class DirectoryUnavailable extends Schema.TaggedError()("DirectoryUnavailable", { reason: Schema.String }) {
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
class BoxAsleep extends Schema.TaggedError()("BoxAsleep", { boxId: Schema.String }) {
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
class WakeFailed extends Schema.TaggedError()("WakeFailed", { boxId: Schema.String, reason: Schema.String }) {
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
class UpstreamRefused extends Schema.TaggedError()("UpstreamRefused", { boxId: Schema.String, port: Schema.Number }) {
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
class Resolver extends Context.Service()("@tesser/proxy/Resolver") {
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
class Waker extends Context.Service()("@tesser/proxy/Waker") {
|
|
455
|
+
}
|
|
456
|
+
var WAKING_WINDOW = Duration.minutes(5), startingRetry = { schedule: Schedule.spaced(Duration.seconds(1)), times: 60 }, respond = (response) => ({ _tag: "response", response });
|
|
457
|
+
function viewerResponse(request, state, owner = !1) {
|
|
458
|
+
let status = viewerStatus(state);
|
|
459
|
+
if (wantsDocument(request))
|
|
460
|
+
return new Response(viewerPage(state, owner), { status, headers: STATUS_HEADERS });
|
|
461
|
+
return new Response(`${viewerText(state)}
|
|
462
|
+
`, { status, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", "x-tesser-status": "1" } });
|
|
463
|
+
}
|
|
464
|
+
var page = (request, state) => Effect2.map(Resolver, ({ owner }) => respond(viewerResponse(request, state, owner)));
|
|
465
|
+
function viewerBox(box) {
|
|
466
|
+
return { boxId: box.boxId, name: box.name, sharedBy: box.sharedBy };
|
|
467
|
+
}
|
|
468
|
+
var wakingPage = Effect2.fn("Proxy.wakingPage")(function* (request, box, port, stage, since) {
|
|
469
|
+
let now = yield* Clock2.currentTimeMillis;
|
|
470
|
+
return yield* page(request, { kind: "waking", box: viewerBox(box), stage, port, seconds: Math.max(0, Math.round((now - since) / 1000)) });
|
|
471
|
+
}), refused = Effect2.fn("Proxy.refused")(function* (request, box, port) {
|
|
472
|
+
let status = yield* (yield* Waker).status(box.boxId), now = yield* Clock2.currentTimeMillis;
|
|
473
|
+
if (box.app === "exited")
|
|
474
|
+
return yield* page(request, { kind: "stopped", box: viewerBox(box) });
|
|
475
|
+
if (status !== null && now - status.since < Duration.toMillis(WAKING_WINDOW))
|
|
476
|
+
return yield* wakingPage(request, box, port, box.app === "running" ? "port" : "app", status.since);
|
|
477
|
+
if (box.app === "none")
|
|
478
|
+
return yield* page(request, { kind: "idle", box: viewerBox(box) });
|
|
479
|
+
return yield* page(request, { kind: "not-answering", box: viewerBox(box), port });
|
|
480
|
+
}), serve = Effect2.fn("Proxy.serve")(function* (request, route, box) {
|
|
481
|
+
let { port, path, headers } = route;
|
|
482
|
+
if (request.headers.get("upgrade")?.toLowerCase() === "websocket")
|
|
483
|
+
return { _tag: "websocket", box, port, path, headers };
|
|
484
|
+
let replayable = request.method === "GET" || request.method === "HEAD", document = wantsDocument(request), attempt = Effect2.suspend(() => route.fetch(box, new Request(`http://${headers.get("host")}${path}`, {
|
|
485
|
+
method: request.method,
|
|
486
|
+
headers,
|
|
487
|
+
body: replayable ? void 0 : request.body,
|
|
488
|
+
redirect: "manual",
|
|
489
|
+
signal: request.signal,
|
|
490
|
+
...replayable ? {} : { duplex: "half" }
|
|
491
|
+
})));
|
|
492
|
+
return yield* (document || !replayable ? attempt : attempt.pipe(Effect2.retry(startingRetry))).pipe(Effect2.flatMap((response) => Effect2.map(route.present(box, response), respond)), Effect2.catchTag("UpstreamRefused", () => document ? refused(request, box, port) : page(request, { kind: "not-answering", box: viewerBox(box), port })));
|
|
493
|
+
}), handle = Effect2.fn("Proxy.handle")(function* (request) {
|
|
494
|
+
let resolved = yield* (yield* Resolver).resolve(request);
|
|
495
|
+
if (resolved instanceof Response)
|
|
496
|
+
return respond(resolved);
|
|
497
|
+
let { box, port } = resolved, waker = yield* Waker;
|
|
498
|
+
if (box.power !== "awake") {
|
|
499
|
+
if (box.power === "asleep" && userNavigation(request))
|
|
500
|
+
yield* waker.wake(box.boxId);
|
|
501
|
+
let status = yield* waker.status(box.boxId);
|
|
502
|
+
if (box.power === "asleep" && status?.failure)
|
|
503
|
+
return yield* page(request, { kind: "wake-failed", box: viewerBox(box), reason: status.failure });
|
|
504
|
+
if (box.power === "asleep" && status === null)
|
|
505
|
+
return yield* page(request, { kind: "asleep", box: viewerBox(box) });
|
|
506
|
+
if (wantsDocument(request)) {
|
|
507
|
+
if (box.power === "launching" && status === null)
|
|
508
|
+
return yield* page(request, { kind: "booting", box: viewerBox(box) });
|
|
509
|
+
return yield* wakingPage(request, box, port, "machine", status?.since ?? (yield* Clock2.currentTimeMillis));
|
|
510
|
+
}
|
|
511
|
+
return yield* serve(request, resolved, yield* waker.awake(box.boxId));
|
|
512
|
+
}
|
|
513
|
+
return yield* serve(request, resolved, box);
|
|
514
|
+
}, (effect, request) => effect.pipe(Effect2.catchTags({
|
|
515
|
+
BoxAsleep: (e) => page(request, { kind: "asleep", box: { boxId: e.boxId, name: e.boxId, sharedBy: null } }),
|
|
516
|
+
BoxNotShared: () => page(request, { kind: "not-shared" }),
|
|
517
|
+
DirectoryUnavailable: () => page(request, { kind: "unavailable" }),
|
|
518
|
+
WakeFailed: (e) => page(request, { kind: "wake-failed", box: { boxId: e.boxId, name: e.boxId, sharedBy: null }, reason: e.reason })
|
|
519
|
+
})));
|
|
386
520
|
|
|
387
521
|
// ../front/src/csp.ts
|
|
388
522
|
var SCRIPT_DIRECTIVES = ["connect-src", "script-src"];
|
|
@@ -420,28 +554,6 @@ function scriptNonce(policy) {
|
|
|
420
554
|
return;
|
|
421
555
|
}
|
|
422
556
|
|
|
423
|
-
// ../front/src/http.ts
|
|
424
|
-
var HOP_BY_HOP = [
|
|
425
|
-
"connection",
|
|
426
|
-
"keep-alive",
|
|
427
|
-
"proxy-authenticate",
|
|
428
|
-
"proxy-authorization",
|
|
429
|
-
"proxy-connection",
|
|
430
|
-
"te",
|
|
431
|
-
"trailer",
|
|
432
|
-
"transfer-encoding",
|
|
433
|
-
"upgrade"
|
|
434
|
-
];
|
|
435
|
-
function withoutHopByHop(source) {
|
|
436
|
-
let headers = new Headers(source), connectionHeaders = headers.get("connection")?.split(",").map((name) => name.trim()).filter(Boolean) ?? [];
|
|
437
|
-
for (let name of [...HOP_BY_HOP, ...connectionHeaders])
|
|
438
|
-
headers.delete(name);
|
|
439
|
-
return headers;
|
|
440
|
-
}
|
|
441
|
-
function wantsDocument(request) {
|
|
442
|
-
return request.method === "GET" && (request.headers.get("sec-fetch-mode") === "navigate" || request.headers.get("accept")?.toLowerCase().includes("text/html") === !0);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
557
|
// ../front/src/shim.ts
|
|
446
558
|
function rewriteUrl(input, ws, ctx) {
|
|
447
559
|
let url;
|
|
@@ -578,10 +690,10 @@ function shimTag(params, nonce) {
|
|
|
578
690
|
return `<script${nonce ? ` nonce="${nonce}"` : ""} src="/__tesser/shim.js?${query.toString().replace(/&/g, "&")}"></script>`;
|
|
579
691
|
}
|
|
580
692
|
|
|
581
|
-
// src/
|
|
582
|
-
import {
|
|
693
|
+
// ../proxy/src/share.ts
|
|
694
|
+
import { Context as Context2, Effect as Effect3, Layer } from "effect";
|
|
583
695
|
|
|
584
|
-
// src/route.ts
|
|
696
|
+
// ../proxy/src/route.ts
|
|
585
697
|
var PORT_PATH = /^\/__tesser\/(?:([a-z0-9-]+(?:\.[a-z0-9-]+)*)\.)?(\d{1,5})(\/.*)?$/;
|
|
586
698
|
function parseTarget(gatewayHost, url) {
|
|
587
699
|
let bare = url.hostname.toLowerCase().replace(/\.$/, ""), suffix = `.${gatewayHost.toLowerCase()}`;
|
|
@@ -609,38 +721,13 @@ function portOrigin(protocol, target, port, first) {
|
|
|
609
721
|
return `${protocol}//${target.sub === "" ? "" : `${target.sub}.`}${gatewayLabel(target.boxId, port === first ? null : port)}.${target.gateway}`;
|
|
610
722
|
}
|
|
611
723
|
|
|
612
|
-
// src/
|
|
613
|
-
class
|
|
724
|
+
// ../proxy/src/share.ts
|
|
725
|
+
class BoxDirectory extends Context2.Service()("@tesser/proxy/BoxDirectory") {
|
|
614
726
|
}
|
|
615
727
|
|
|
616
|
-
class
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
class WakeFailed extends Schema.TaggedError()("WakeFailed", { boxId: Schema.String, reason: Schema.String }) {
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
class UpstreamRefused extends Schema.TaggedError()("UpstreamRefused", { boxId: Schema.String, port: Schema.Number }) {
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
class BoxDirectory extends Context.Service()("@tesser/gateway/BoxDirectory") {
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
class Waker extends Context.Service()("@tesser/gateway/Waker") {
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
class Upstream extends Context.Service()("@tesser/gateway/Upstream") {
|
|
632
|
-
}
|
|
633
|
-
var VIEWER_COOKIE = "__Host-tesser-viewer", GATEWAY_COOKIES = /* @__PURE__ */ new Set(["CF_Authorization", VIEWER_COOKIE]), WAKING_WINDOW = Duration.minutes(5), startingRetry = { schedule: Schedule.spaced(Duration.seconds(1)), times: 60 }, respond = (response) => ({ _tag: "response", response });
|
|
634
|
-
function page(request, state) {
|
|
635
|
-
let status = viewerStatus(state);
|
|
636
|
-
if (wantsDocument(request))
|
|
637
|
-
return respond(new Response(viewerPage(state), { status, headers: STATUS_HEADERS }));
|
|
638
|
-
return respond(new Response(`${viewerText(state)}
|
|
639
|
-
`, { status, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", "x-tesser-status": "1" } }));
|
|
640
|
-
}
|
|
641
|
-
function viewerBox(box) {
|
|
642
|
-
return { name: box.name, sharedBy: box.sharedBy };
|
|
728
|
+
class Upstream extends Context2.Service()("@tesser/proxy/Upstream") {
|
|
643
729
|
}
|
|
730
|
+
var VIEWER_COOKIE = "__Host-tesser-viewer", GATEWAY_COOKIES = /* @__PURE__ */ new Set(["CF_Authorization", VIEWER_COOKIE]);
|
|
644
731
|
function upstreamHeaders(request, target, port, root) {
|
|
645
732
|
let headers = withoutHopByHop(request.headers);
|
|
646
733
|
for (let name of [...headers.keys()])
|
|
@@ -696,65 +783,35 @@ function downstream(request, response, upstreamUrl, ctx, shownOrigin) {
|
|
|
696
783
|
});
|
|
697
784
|
return new HTMLRewriter().on("head", inject).on("body", inject).on("a[href], area[href], link[href]", link("href")).on("form[action]", link("action")).on("button[formaction], input[formaction]", link("formaction")).on("script[src], img[src], iframe[src], source[src], video[src], audio[src], embed[src]", link("src")).on("img[srcset], source[srcset]", link("srcset", (value) => rewriteSrcset(value, (url) => rewriteUrl(url, !1, ctx)))).transform(new Response(response.body, init));
|
|
698
785
|
}
|
|
699
|
-
var
|
|
700
|
-
let
|
|
701
|
-
return
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
method: request.method,
|
|
715
|
-
headers,
|
|
716
|
-
body: replayable ? void 0 : request.body,
|
|
717
|
-
redirect: "manual",
|
|
718
|
-
signal: request.signal,
|
|
719
|
-
...replayable ? {} : { duplex: "half" }
|
|
720
|
-
})));
|
|
721
|
-
return yield* (document || !replayable ? attempt : attempt.pipe(Effect2.retry(startingRetry))).pipe(Effect2.map((response) => respond(downstream(request, response, upstreamUrl, ctx, shownOrigin))), Effect2.catchTag("UpstreamRefused", () => document ? refused(request, box, port) : Effect2.succeed(page(request, { kind: "not-answering", box: viewerBox(box), port }))));
|
|
722
|
-
}), handle = Effect2.fn("Gateway.handle")(function* (request) {
|
|
723
|
-
let directory = yield* BoxDirectory, waker = yield* Waker, url = new URL(request.url), host = yield* directory.host, target = parseTarget(host, url);
|
|
724
|
-
if (target === null)
|
|
725
|
-
return page(request, { kind: "not-shared" });
|
|
726
|
-
if (target.port === null && url.pathname === "/__tesser/shim.js")
|
|
727
|
-
return respond(new Response(shimSource(), { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "public, max-age=300" } }));
|
|
728
|
-
let box = yield* directory.lookup(target.boxId), first = box.ports[0];
|
|
729
|
-
if (first === void 0)
|
|
730
|
-
return page(request, { kind: "no-port", box: viewerBox(box), port: target.port ?? target.root ?? 0, ports: [] });
|
|
731
|
-
let root = target.root ?? first, port = target.port ?? root;
|
|
732
|
-
if (!box.ports.includes(root) || !box.ports.includes(port)) {
|
|
733
|
-
let ports = box.ports.map((p) => ({ port: p, href: `${portOrigin(url.protocol, target, p, first)}/` }));
|
|
734
|
-
return page(request, { kind: "no-port", box: viewerBox(box), port: box.ports.includes(root) ? port : root, ports });
|
|
735
|
-
}
|
|
736
|
-
if (target.port !== null && request.headers.get("sec-fetch-mode") === "navigate")
|
|
737
|
-
return respond(new Response(null, { status: 307, headers: { location: `${portOrigin(url.protocol, target, port, first)}${target.path}`, "cache-control": "no-store" } }));
|
|
738
|
-
if (box.power !== "awake") {
|
|
739
|
-
let navigate = request.headers.get("sec-fetch-mode") === "navigate";
|
|
740
|
-
if (box.power === "asleep")
|
|
741
|
-
yield* waker.wake(box.boxId, navigate);
|
|
742
|
-
let status = yield* waker.status(box.boxId);
|
|
743
|
-
if (status?.failure && box.power === "asleep")
|
|
744
|
-
return page(request, { kind: "wake-failed", box: viewerBox(box), reason: status.failure });
|
|
745
|
-
if (wantsDocument(request)) {
|
|
746
|
-
if (box.power === "launching" && status === null)
|
|
747
|
-
return page(request, { kind: "booting", box: viewerBox(box) });
|
|
748
|
-
return yield* wakingPage(request, box, port, "machine", status?.since ?? (yield* Clock2.currentTimeMillis));
|
|
786
|
+
var shareResolver = Layer.effect(Resolver, Effect3.gen(function* () {
|
|
787
|
+
let directory = yield* BoxDirectory, upstream = yield* Upstream;
|
|
788
|
+
return { owner: !1, resolve: Effect3.fn("Share.resolve")(function* (request) {
|
|
789
|
+
let url = new URL(request.url), target = parseTarget(yield* directory.host, url);
|
|
790
|
+
if (target === null)
|
|
791
|
+
return viewerResponse(request, { kind: "not-shared" });
|
|
792
|
+
if (target.port === null && url.pathname === "/__tesser/shim.js")
|
|
793
|
+
return new Response(shimSource(), { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "public, max-age=300" } });
|
|
794
|
+
let box = yield* directory.lookup(target.boxId), first = box.ports[0];
|
|
795
|
+
if (first === void 0)
|
|
796
|
+
return viewerResponse(request, { kind: "no-port", box: viewerBox(box), port: target.port ?? target.root ?? 0, ports: [] });
|
|
797
|
+
let root = target.root ?? first, port = target.port ?? root;
|
|
798
|
+
if (!box.ports.includes(root) || !box.ports.includes(port)) {
|
|
799
|
+
let ports = box.ports.map((p) => ({ port: p, href: `${portOrigin(url.protocol, target, p, first)}/` }));
|
|
800
|
+
return viewerResponse(request, { kind: "no-port", box: viewerBox(box), port: box.ports.includes(root) ? port : root, ports });
|
|
749
801
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
802
|
+
if (target.port !== null && request.headers.get("sec-fetch-mode") === "navigate")
|
|
803
|
+
return new Response(null, { status: 307, headers: { location: `${portOrigin(url.protocol, target, port, first)}${target.path}`, "cache-control": "no-store" } });
|
|
804
|
+
let headers = upstreamHeaders(request, target, port, root), upstreamUrl = `http://${headers.get("host")}${target.path}`, shownOrigin = headers.get("origin");
|
|
805
|
+
return {
|
|
806
|
+
box,
|
|
807
|
+
port,
|
|
808
|
+
path: target.path,
|
|
809
|
+
headers,
|
|
810
|
+
fetch: (served, upstreamRequest) => upstream.fetch(served, port, upstreamRequest),
|
|
811
|
+
present: (served, response) => Effect3.sync(() => downstream(request, response, upstreamUrl, { href: request.url, host: target.boxHost, ports: served.ports, port: root }, shownOrigin))
|
|
812
|
+
};
|
|
813
|
+
}) };
|
|
814
|
+
}));
|
|
758
815
|
|
|
759
816
|
// src/tunnel.ts
|
|
760
817
|
var UPGRADE_TIMEOUT_MS = 30000, NULL_BODY = /* @__PURE__ */ new Set([101, 204, 205, 304]);
|
|
@@ -987,7 +1044,7 @@ class BoxRelay extends DurableObject {
|
|
|
987
1044
|
return plain(403, "this box answers for another org here");
|
|
988
1045
|
return this.acceptBox(decoded.value);
|
|
989
1046
|
}
|
|
990
|
-
let handled = await
|
|
1047
|
+
let handled = await Effect4.runPromise(handle(request).pipe(Effect4.provide(this.layer(this.viewer(request)))));
|
|
991
1048
|
if (handled._tag === "response")
|
|
992
1049
|
return handled.response;
|
|
993
1050
|
return this.bridge(request, handled);
|
|
@@ -1142,10 +1199,10 @@ class BoxRelay extends DurableObject {
|
|
|
1142
1199
|
this.webSocketClose(ws, 1006, "socket error");
|
|
1143
1200
|
}
|
|
1144
1201
|
ask(call) {
|
|
1145
|
-
return
|
|
1202
|
+
return Effect4.tryPromise({ try: call, catch: (err) => new DirectoryUnavailable({ reason: err instanceof Error ? err.message : String(err) }) });
|
|
1146
1203
|
}
|
|
1147
1204
|
layer(viewer) {
|
|
1148
|
-
let self = this, remote =
|
|
1205
|
+
let self = this, remote = Effect4.fn("Relay.remote")(function* (boxId) {
|
|
1149
1206
|
let { status, body } = yield* self.ask(() => self.lookupBox(boxId, viewer));
|
|
1150
1207
|
if (status === 403 || status === 404)
|
|
1151
1208
|
return yield* new BoxNotShared({ boxId });
|
|
@@ -1153,33 +1210,19 @@ class BoxRelay extends DurableObject {
|
|
|
1153
1210
|
let message = Schema2.decodeUnknownOption(ErrorBody)(body);
|
|
1154
1211
|
return yield* new DirectoryUnavailable({ reason: message._tag === "Some" ? message.value.error.message : `tesser answered ${status}` });
|
|
1155
1212
|
}
|
|
1156
|
-
return (yield* Schema2.decodeUnknownEffect(BoxResponse)(body).pipe(
|
|
1157
|
-
}),
|
|
1158
|
-
let ticket = self.live();
|
|
1159
|
-
if (ticket === null) {
|
|
1160
|
-
let box = yield* remote(boxId);
|
|
1161
|
-
return { ...box, power: box.power === "awake" ? "launching" : box.power };
|
|
1162
|
-
}
|
|
1163
|
-
let now = yield* Clock3.currentTimeMillis;
|
|
1164
|
-
if (viewer !== null && now - (self.verifiedAt.get(viewer) ?? 0) > RECHECK_MS) {
|
|
1165
|
-
let answer = yield* self.ask(() => self.lookupBox(boxId, viewer)).pipe(Effect3.orElseSucceed(() => null)), verdict = self.verdict(viewer, answer, now, !1);
|
|
1166
|
-
if (verdict === "revoked" || verdict === "refused")
|
|
1167
|
-
return yield* new BoxNotShared({ boxId });
|
|
1168
|
-
if (verdict === "stale")
|
|
1169
|
-
return yield* new DirectoryUnavailable({ reason: "tesser didn't answer whether you can still open this box" });
|
|
1170
|
-
}
|
|
1171
|
-
return { boxId, name: ticket.name, sharedBy: ticket.sharedBy, privateIp: "", ports: ticket.ports, power: "awake", app: "running" };
|
|
1172
|
-
}), wake = Effect3.fn("Relay.wake")(function* (boxId, retry) {
|
|
1213
|
+
return (yield* Schema2.decodeUnknownEffect(BoxResponse)(body).pipe(Effect4.mapError(() => new DirectoryUnavailable({ reason: "tesser answered with a box this gateway can't read" })))).box;
|
|
1214
|
+
}), kick = Effect4.fn("Relay.kick")(function* (boxId, asked) {
|
|
1173
1215
|
let now = yield* Clock3.currentTimeMillis, current = self.waking;
|
|
1174
1216
|
if (current?.inflight)
|
|
1175
1217
|
return;
|
|
1176
|
-
if (current?.failure && !retry)
|
|
1177
|
-
return;
|
|
1178
1218
|
if (current !== null && current.failure === null && now - current.kickedAt < WAKE_RETRY_MS)
|
|
1179
1219
|
return;
|
|
1180
|
-
let
|
|
1220
|
+
let fresh = current === null || current.failure !== null || now - current.since > Duration2.toMillis(WAKE_TIMEOUT);
|
|
1221
|
+
if (fresh && !asked)
|
|
1222
|
+
return;
|
|
1223
|
+
let waking = { since: fresh ? now : current.since, kickedAt: now, failure: null, inflight: !0 };
|
|
1181
1224
|
self.waking = waking;
|
|
1182
|
-
let kicked = yield* self.ask(() => self.wakeBox(boxId, viewer)).pipe(
|
|
1225
|
+
let kicked = yield* self.ask(() => self.wakeBox(boxId, viewer)).pipe(Effect4.result, Effect4.ensuring(Effect4.sync(() => waking.inflight = !1)));
|
|
1183
1226
|
if (kicked._tag === "Failure")
|
|
1184
1227
|
return;
|
|
1185
1228
|
let { status, body } = kicked.success;
|
|
@@ -1187,30 +1230,50 @@ class BoxRelay extends DurableObject {
|
|
|
1187
1230
|
return;
|
|
1188
1231
|
let message = Schema2.decodeUnknownOption(ErrorBody)(body);
|
|
1189
1232
|
waking.failure = message._tag === "Some" ? message.value.error.message : `tesser answered ${status}`;
|
|
1190
|
-
}),
|
|
1233
|
+
}), wake = (boxId) => kick(boxId, !0), lookup = Effect4.fn("Relay.lookup")(function* (boxId) {
|
|
1234
|
+
let ticket = self.live();
|
|
1235
|
+
if (ticket === null) {
|
|
1236
|
+
let box = yield* remote(boxId);
|
|
1237
|
+
if (box.power === "asleep")
|
|
1238
|
+
yield* kick(boxId, !1);
|
|
1239
|
+
return { ...box, power: box.power === "awake" ? "launching" : box.power };
|
|
1240
|
+
}
|
|
1241
|
+
let now = yield* Clock3.currentTimeMillis;
|
|
1242
|
+
if (viewer !== null && now - (self.verifiedAt.get(viewer) ?? 0) > RECHECK_MS) {
|
|
1243
|
+
let answer = yield* self.ask(() => self.lookupBox(boxId, viewer)).pipe(Effect4.orElseSucceed(() => null)), verdict = self.verdict(viewer, answer, now, !1);
|
|
1244
|
+
if (verdict === "revoked" || verdict === "refused")
|
|
1245
|
+
return yield* new BoxNotShared({ boxId });
|
|
1246
|
+
if (verdict === "stale")
|
|
1247
|
+
return yield* new DirectoryUnavailable({ reason: "tesser didn't answer whether you can still open this box" });
|
|
1248
|
+
}
|
|
1249
|
+
return { boxId, name: ticket.name, sharedBy: ticket.sharedBy, privateIp: "", ports: ticket.ports, power: "awake", app: "running" };
|
|
1250
|
+
}), status = Effect4.fn("Relay.status")(function* (_boxId) {
|
|
1191
1251
|
let now = yield* Clock3.currentTimeMillis, w = self.waking;
|
|
1192
1252
|
if (w === null || w.failure === null && now - w.since > Duration2.toMillis(WAKE_TIMEOUT))
|
|
1193
1253
|
return null;
|
|
1194
1254
|
return { since: w.since, failure: w.failure };
|
|
1195
|
-
}), dialed = (boxId, within, reason) =>
|
|
1196
|
-
if (self.live() === null)
|
|
1197
|
-
|
|
1255
|
+
}), dialed = (boxId, within, reason) => Effect4.promise(() => new Promise((resolve) => self.connected.push(resolve))).pipe(Effect4.timeoutOrElse({ duration: within, orElse: () => Effect4.fail(new WakeFailed({ boxId, reason })) })), awake = Effect4.fn("Relay.awake")(function* (boxId) {
|
|
1256
|
+
if (self.live() === null) {
|
|
1257
|
+
let box = yield* remote(boxId);
|
|
1258
|
+
if (box.power === "awake")
|
|
1198
1259
|
yield* dialed(boxId, DIAL_GRACE, "the box is up but hasn't connected to this gateway; its boxd may need an update");
|
|
1199
1260
|
else {
|
|
1200
|
-
yield*
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1261
|
+
let waking = yield* status(boxId);
|
|
1262
|
+
if (waking?.failure)
|
|
1263
|
+
return yield* new WakeFailed({ boxId, reason: waking.failure });
|
|
1264
|
+
if (box.power === "asleep" && waking === null)
|
|
1265
|
+
return yield* new BoxAsleep({ boxId });
|
|
1204
1266
|
yield* dialed(boxId, WAKE_TIMEOUT, "it didn't wake up within 5 minutes");
|
|
1205
1267
|
}
|
|
1268
|
+
}
|
|
1206
1269
|
return yield* lookup(boxId);
|
|
1207
|
-
}), upstream = (box, port, request) =>
|
|
1208
|
-
return
|
|
1270
|
+
}), upstream = (box, port, request) => Effect4.tryPromise({ try: () => self.tunnel.fetch(port, request), catch: () => new UpstreamRefused({ boxId: box.boxId, port }) });
|
|
1271
|
+
return Layer2.merge(Layer2.provide(shareResolver, Layer2.merge(Layer2.succeed(BoxDirectory)({ host: Effect4.succeed(self.host()), lookup }), Layer2.succeed(Upstream)({ fetch: upstream }))), Layer2.succeed(Waker)({ wake, status, awake }));
|
|
1209
1272
|
}
|
|
1210
1273
|
}
|
|
1211
1274
|
|
|
1212
1275
|
// src/cloudflare.ts
|
|
1213
|
-
var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket =
|
|
1276
|
+
var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect5.fn("Gateway.verifyTicket")(function* (env, request) {
|
|
1214
1277
|
let token = request.headers.get("authorization")?.match(/^Bearer (\S+)$/)?.[1];
|
|
1215
1278
|
if (token === void 0)
|
|
1216
1279
|
return null;
|
|
@@ -1219,8 +1282,8 @@ var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect4.fn("Gatewa
|
|
|
1219
1282
|
return null;
|
|
1220
1283
|
if (claims.sub !== `box:${env.TESSER_ORG}:${String(claims.box)}`)
|
|
1221
1284
|
return null;
|
|
1222
|
-
return yield* Schema3.decodeUnknownEffect(Ticket)(claims).pipe(
|
|
1223
|
-
}), verifyViewer =
|
|
1285
|
+
return yield* Schema3.decodeUnknownEffect(Ticket)(claims).pipe(Effect5.option);
|
|
1286
|
+
}), verifyViewer = Effect5.fn("Gateway.verifyViewer")(function* (env, request) {
|
|
1224
1287
|
let token = request.headers.get(ACCESS_HEADER);
|
|
1225
1288
|
if (token === null)
|
|
1226
1289
|
return null;
|
|
@@ -1232,7 +1295,7 @@ var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect4.fn("Gatewa
|
|
|
1232
1295
|
if (apex && url.pathname === GATEWAY_DIAL_PATH) {
|
|
1233
1296
|
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket")
|
|
1234
1297
|
return plain(426, "boxes dial this with a websocket");
|
|
1235
|
-
let ticket = await
|
|
1298
|
+
let ticket = await Effect5.runPromise(verifyTicket(env, request));
|
|
1236
1299
|
if (ticket === null || ticket._tag === "None")
|
|
1237
1300
|
return plain(401, "a box dials in with a share ticket signed by tesser");
|
|
1238
1301
|
let headers = new Headers(request.headers);
|
|
@@ -1243,7 +1306,7 @@ var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect4.fn("Gatewa
|
|
|
1243
1306
|
let boxId = gatewayHostBox(env.GATEWAY_HOST, url.hostname);
|
|
1244
1307
|
if (boxId === null)
|
|
1245
1308
|
return new Response(viewerPage({ kind: "not-shared" }), { status: 404, headers: STATUS_HEADERS });
|
|
1246
|
-
if (await
|
|
1309
|
+
if (await Effect5.runPromise(verifyViewer(env, request)) === null)
|
|
1247
1310
|
return plain(403, "this gateway only answers requests Cloudflare Access let through");
|
|
1248
1311
|
let headers = new Headers(request.headers);
|
|
1249
1312
|
return headers.delete(TICKET_HEADER), env.BOXES.get(env.BOXES.idFromName(boxId)).fetch(new Request(request, { headers }));
|