-
Notifications
You must be signed in to change notification settings - Fork 1
Unify local subdomain with builds subdomain #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,15 +3,15 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Replaces the CDP `Fetch.enable` interceptor that previously hijacked the | ||||||||||||||||||||||||||||||||||||||||||||||||
| * game subdomain inside chromium itself. Chromium now reaches us through | ||||||||||||||||||||||||||||||||||||||||||||||||
| * `--host-rules=MAP <gameSubdomain>:443 127.0.0.1:<port>`, so the network | ||||||||||||||||||||||||||||||||||||||||||||||||
| * `--host-rules=MAP *.<localHostSuffix>:443 127.0.0.1:<port>`, so the network | ||||||||||||||||||||||||||||||||||||||||||||||||
| * stack stays untouched and the bundled DevTools' Network tab works. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Routing per request (matches the previous interceptor 1:1): | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. Path `/dev-app-embed` → synthesize the embed shell (or | ||||||||||||||||||||||||||||||||||||||||||||||||
| * CUSTOM-HTML with the SDK bootstrap | ||||||||||||||||||||||||||||||||||||||||||||||||
| * injected). | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. Path in PASSTHROUGH_PREFIXES → reverse-proxy to the real | ||||||||||||||||||||||||||||||||||||||||||||||||
| * `https://<gameSubdomain>` (Node DNS, | ||||||||||||||||||||||||||||||||||||||||||||||||
| * `https://<incoming host>` (Node DNS, | ||||||||||||||||||||||||||||||||||||||||||||||||
| * no host-rules → real network). | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 3. Otherwise → serve a file from `uploadDir` with | ||||||||||||||||||||||||||||||||||||||||||||||||
| * COEP/COOP/CORP and transparent | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -30,7 +30,7 @@ import { generateCert, type CertPair } from "./cert"; | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export interface ServerConfig { | ||||||||||||||||||||||||||||||||||||||||||||||||
| uploadDir: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| gameSubdomain: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| localHostSuffix: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| verbose: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -71,7 +71,7 @@ function logServed(res: http.ServerResponse, url: string): void { | |||||||||||||||||||||||||||||||||||||||||||||||
| export async function startServer( | ||||||||||||||||||||||||||||||||||||||||||||||||
| config: ServerConfig, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<StartedServer> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const cert = generateCert(config.gameSubdomain); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const cert = generateCert(config.localHostSuffix); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const server = https.createServer( | ||||||||||||||||||||||||||||||||||||||||||||||||
| { cert: cert.certPem, key: cert.keyPem }, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -109,10 +109,20 @@ async function handle( | |||||||||||||||||||||||||||||||||||||||||||||||
| res: http.ServerResponse, | ||||||||||||||||||||||||||||||||||||||||||||||||
| config: ServerConfig, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // SNI is honored by https.createServer, so req.headers.host carries the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // browser-facing host (the game subdomain). We use that to build the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // proxy URL for passthrough paths. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const hostHeader = req.headers.host ?? config.gameSubdomain; | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Host header is the browser-facing `{gcid}-{userhash}.<localHostSuffix>`. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const hostHeader = req.headers.host; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!hostHeader) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| res.statusCode = 400; | ||||||||||||||||||||||||||||||||||||||||||||||||
| res.end("Missing Host header"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| const originHost = hostHeader.split(":")[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Defense-in-depth: pin the upstream `proxyToOrigin` sees to our suffix. | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!originHost.endsWith(`.${config.localHostSuffix}`)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| res.statusCode = 400; | ||||||||||||||||||||||||||||||||||||||||||||||||
| res.end("Unexpected Host"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| const url = new URL(req.url ?? "/", `https://${hostHeader}`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const urlPath = url.pathname; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -122,15 +132,15 @@ async function handle( | |||||||||||||||||||||||||||||||||||||||||||||||
| log( | ||||||||||||||||||||||||||||||||||||||||||||||||
| "passthrough", | ||||||||||||||||||||||||||||||||||||||||||||||||
| req.method ?? "GET", | ||||||||||||||||||||||||||||||||||||||||||||||||
| config.gameSubdomain + urlPath + url.search, | ||||||||||||||||||||||||||||||||||||||||||||||||
| originHost + urlPath + url.search, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| await proxyToOrigin(req, res, config.gameSubdomain); | ||||||||||||||||||||||||||||||||||||||||||||||||
| await proxyToOrigin(req, res, originHost); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| serveLocalFile(res, config.uploadDir, urlPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| logServed(res, config.gameSubdomain + urlPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| logServed(res, originHost + urlPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.stderr.write( | ||||||||||||||||||||||||||||||||||||||||||||||||
| `handler error for ${req.url ?? ""}: ${String(err)}\n`, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -152,15 +162,8 @@ function isPassthroughPath(p: string): boolean { | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Headers every response on the game subdomain must carry. Mirrors the | ||||||||||||||||||||||||||||||||||||||||||||||||
| * production play worker (`play/src/server/index.ts`). | ||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||
| * - `Origin-Agent-Cluster: ?1` is sticky per origin per BrowsingContextGroup, | ||||||||||||||||||||||||||||||||||||||||||||||||
| * so a single response without it locks the origin into site-keyed mode for | ||||||||||||||||||||||||||||||||||||||||||||||||
| * the rest of the chromium session and breaks `crossOriginIsolated`. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * - `Cross-Origin-Resource-Policy: cross-origin` is required because the | ||||||||||||||||||||||||||||||||||||||||||||||||
| * mainsite (which sets COEP=require-corp) is on a different site than the | ||||||||||||||||||||||||||||||||||||||||||||||||
| * playsite (wavedash.com vs wavedashcdn.com, and likewise in dev). | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Mirrors the play worker: OAC=?1 (sticky per BrowsingContextGroup, lock-in | ||||||||||||||||||||||||||||||||||||||||||||||||
| * if ever absent), CORP=cross-origin for the mainsite/playsite split. | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
164
to
167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The deletion here drops useful context that isn't actually local-vs-prod-specific — the explanation for why each header is set still applies. In particular the Suggest restoring the body of the comment and just adjusting the lead sentence to drop the "game subdomain" framing:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function setIframeOriginHeaders(res: http.ServerResponse): void { | ||||||||||||||||||||||||||||||||||||||||||||||||
| res.setHeader("Origin-Agent-Cluster", "?1"); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor:
endsWith(dotSuffix)accepts multi-level subdomains (a.b.local.wavedashcdn.com), but both the chromiumMAP *.<suffix>:443rule and the wildcard SAN cert only cover single-label (a.local.wavedashcdn.com). The asymmetry is benign today because the host-rules rule is what actually routes traffic here, so multi-level hosts never reach this proc — but pinning the check to single-label keeps the trust surface aligned with what host-rules can deliver, and reads as more intentional:Optional — happy to leave as-is if you'd rather not add the extra check.