diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.test.ts | 50 | ||||
| -rw-r--r-- | src/app.ts | 35 |
2 files changed, 71 insertions, 14 deletions
diff --git a/src/app.test.ts b/src/app.test.ts new file mode 100644 index 0000000..498d8a6 --- /dev/null +++ b/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"/, + ); + }); +}); @@ -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; |
