diff options
| author | Joe Mou <dev@mou.fo> | 2026-08-11 11:39:01 -0400 |
|---|---|---|
| committer | Joe Mou <dev@mou.fo> | 2026-08-11 11:57:29 -0400 |
| commit | 9731ebd1bece1187f943a29da474f4aed6e7ee3b (patch) | |
| tree | d4dd79ffc9e71bcf66992de6395938e9c3ab7434 /cgithub | |
| parent | ab52f48e50d0dd242ad5b0cfbee9c458946cd427 (diff) | |
Redirect to GitHub with a meta refresh
A browser extension redirects github.com to cgithub with declarative net
requests. Its rules match our HTTP redirects back to github.com too, so
those bounce straight back here and loop forever. A meta refresh makes
cgithub the initiator origin instead, which the extension's rules can
exclude, and its delay leaves a page the user can stop on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'cgithub')
| -rw-r--r-- | cgithub/README.md | 30 | ||||
| -rw-r--r-- | cgithub/package.json | 6 | ||||
| -rw-r--r-- | cgithub/src/app.test.ts | 50 | ||||
| -rw-r--r-- | cgithub/src/app.ts | 35 | ||||
| -rw-r--r-- | cgithub/views/redirect.eta | 14 |
5 files changed, 119 insertions, 16 deletions
diff --git a/cgithub/README.md b/cgithub/README.md index 3bcf017..a625116 100644 --- a/cgithub/README.md +++ b/cgithub/README.md @@ -24,6 +24,36 @@ $ pnpm run deploy The worker name and other settings live in `wrangler.jsonc`. +## Browser extension redirects + +Pages cgithub can't render (marketing pages, code search, anything unrouted) are +redirected back to github.com. A browser extension that rewrites github.com URLs +to a cgithub instance with `declarativeNetRequest` will match those redirects +too and send the request straight back, looping forever. + +To avoid this, cgithub never answers with an HTTP redirect: it serves a small +page that navigates to github.com with a two-second `<meta http-equiv="refresh">`. +Because that navigation is initiated by the cgithub origin, the extension can +let it through with `excludedInitiatorDomains`; without that, the user at least +lands on a page they can stop on, with a link, instead of an endless loop. + +A rule that redirects to a cgithub instance should therefore look like this: + +```jsonc +{ + "action": { + "type": "redirect", + "redirect": { "transform": { "host": "cgithub.example" } }, + }, + "condition": { + "urlFilter": "||github.com", + "resourceTypes": ["main_frame"], + // So cgithub's own meta refresh to github.com isn't redirected back here. + "excludedInitiatorDomains": ["cgithub.example"], + }, +} +``` + ## Disclosures AI coding assistants, in particular Claude, are used in the development process. diff --git a/cgithub/package.json b/cgithub/package.json index 7673612..ce25701 100644 --- a/cgithub/package.json +++ b/cgithub/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "node scripts/build.ts && wrangler dev", "deploy": "node scripts/build.ts && wrangler deploy", - "test": "node --test --test-concurrency=4 src/scraper.test.ts" + "test": "node --test --test-concurrency=4 src/app.test.ts src/scraper.test.ts" }, "dependencies": { "css-select": "^6.0.0", @@ -18,7 +18,9 @@ "htmlparser2": "^10.1.0" }, "pnpm": { - "onlyBuiltDependencies": ["workerd"] + "onlyBuiltDependencies": [ + "workerd" + ] }, "devDependencies": { "@types/node": "^22.10.2", diff --git a/cgithub/src/app.test.ts b/cgithub/src/app.test.ts new file mode 100644 index 0000000..498d8a6 --- /dev/null +++ b/cgithub/src/app.test.ts @@ -0,0 +1,50 @@ +import assert from "node:assert"; +import path from "node:path"; +import { describe, it } from "node:test"; +import { Eta } from "eta"; +import { createApp } from "./app.ts"; + +// The routes exercised here redirect without scraping, so no network is needed. +const eta = new Eta({ views: path.join(import.meta.dirname, "..", "views") }); +const app = createApp(eta); + +describe("redirects to GitHub", () => { + // An HTTP redirect would be caught by the extension's declarativeNetRequest + // rules and bounced straight back here. + it("should redirect unknown paths with a meta refresh", async () => { + const res = await app.request("http://cgithub.example/a/b/c?x=1"); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.headers.get("location"), null); + assert.strictEqual(res.headers.get("Referrer-Policy"), "no-referrer"); + + const body = await res.text(); + assert.match( + body, + /<meta http-equiv="refresh" content="2; url=https:\/\/github.com\/a\/b\/c\?x=1">/, + ); + assert.doesNotMatch(body, /<script/); + }); + + it("should redirect unhandled search types", async () => { + const res = await app.request( + "http://cgithub.example/actions/deploy-pages/search?q=x&type=code", + ); + + assert.strictEqual(res.status, 200); + assert.match( + await res.text(), + /https:\/\/github.com\/actions\/deploy-pages\/search\?q=x&type=code/, + ); + }); + + it("should redirect raw file requests to raw.githubusercontent.com", async () => { + const res = await app.request("http://cgithub.example/actions/deploy-pages/raw/main/README.md"); + + assert.strictEqual(res.status, 200); + assert.match( + await res.text(), + /url=https:\/\/raw.githubusercontent.com\/actions\/deploy-pages\/main\/README.md"/, + ); + }); +}); diff --git a/cgithub/src/app.ts b/cgithub/src/app.ts index 0218918..9a7ee8e 100644 --- a/cgithub/src/app.ts +++ b/cgithub/src/app.ts @@ -19,9 +19,22 @@ import { RedirectError, } from "./scraper.ts"; +// Our URLs mirror GitHub's, so the same path there is the page we scraped. +function githubUrlFor(c: Context) { + const { pathname, search } = new URL(c.req.url); + return `https://github.com${pathname}${search}`; +} + export function createApp(eta: Eta) { const app = new Hono(); + // Use a meta refresh to avoid redirect loops in certain situations; we become + // the initiator origin even if we are the target of a redirection. + function redirectToGitHub(c: Context, location: string) { + c.header("Referrer-Policy", "no-referrer"); + return c.html(eta.render("redirect.eta", { location })); + } + app.get("/", async (c) => { return c.html(eta.render("home.eta", {})); }); @@ -33,7 +46,7 @@ export function createApp(eta: Eta) { } catch (e) { if (e instanceof RedirectError) { if (e.location.startsWith("https://")) { - c.header("Referrer-Policy", "no-referrer"); + return redirectToGitHub(c, e.location); } return c.redirect(e.location); } else if (e instanceof GitHubHTTPError) { @@ -45,10 +58,7 @@ export function createApp(eta: Eta) { return c.html(eta.render("error.eta", { message: "" + e })); } } - // Our URLs mirror GitHub's, so the page we scraped is the same path there. - const { pathname, search } = new URL(c.req.url); - const githubUrl = `https://github.com${pathname}${search}`; - return c.html(eta.render(template, { ...data, githubUrl, commit })); + return c.html(eta.render(template, { ...data, githubUrl: githubUrlFor(c), commit })); } // For fragments fetched asynchronously by client-side JS (see public/static/refs.js): @@ -96,8 +106,10 @@ export function createApp(eta: Eta) { app.get("/:owner/:repo/raw/:branch/:path{.*}", async (c) => { const { owner, repo, branch, path } = c.req.param(); - c.header("Referrer-Policy", "no-referrer"); - return c.redirect(`https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`); + return redirectToGitHub( + c, + `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`, + ); }); app.get("/:owner/:repo/issues", async (c) => { @@ -131,10 +143,7 @@ export function createApp(eta: Eta) { } // Redirect unhandled search types (like code, which requires sign in anyway). - c.header("Referrer-Policy", "no-referrer"); - return c.redirect( - `https://github.com/${owner}/${repo}/search?${new URLSearchParams(c.req.query()).toString()}`, - ); + return redirectToGitHub(c, githubUrlFor(c)); }); app.get("/:owner/:repo/commits/:branch/:path{.*}?", async (c) => { @@ -179,9 +188,7 @@ export function createApp(eta: Eta) { }); app.all("*", async (c) => { - const path = c.req.path; - c.header("Referrer-Policy", "no-referrer"); - return c.redirect(`https://github.com${path}`); + return redirectToGitHub(c, githubUrlFor(c)); }); return app; diff --git a/cgithub/views/redirect.eta b/cgithub/views/redirect.eta new file mode 100644 index 0000000..393a527 --- /dev/null +++ b/cgithub/views/redirect.eta @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="referrer" content="no-referrer"> + <meta http-equiv="refresh" content="1; url=<%= it.location %>"> + <title>Redirecting to GitHub</title> + <link rel="stylesheet" href="/static/style.css"> +</head> +<body> + <p>cgithub can't render this page. Redirecting to + <a href="<%= it.location %>" rel="noreferrer"><%= it.location %></a>…</p> +</body> +</html> |
