Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Context, Hono } from "hono";
import { logger } from "hono/logger";
import { secureHeaders } from "hono/secure-headers";

import { pageCsp } from "./utils/headers";
import { renderer } from "./utils/renderer";
import { handleTokens } from "./utils/tokens";
import {
Expand Down Expand Up @@ -29,6 +30,7 @@ export type Variables = {
refresh_token?: string;
state: string;
octokit: Octokit;
cspNonce?: string;
};

/* APP */
Expand All @@ -43,6 +45,7 @@ app.use(
})
);
app.use(renderer);
app.use("/", pageCsp);
app.use("/", handleMaxId);
app.use("/", handleTokens);

Expand Down
2 changes: 2 additions & 0 deletions src/routes/template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { Context, Hono } from "hono";

import { Bindings, Variables } from "..";
import { Repository } from "../components/repository";
import { pageCsp } from "../utils/headers";
import { handleTokens } from "../utils/tokens";
import { getRepository } from "../utils/octokit";

/* APP */
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();

/* MIDDLEWARES */
app.use(pageCsp);
app.use(handleTokens);

/* ENDPOINTS */
Expand Down
29 changes: 29 additions & 0 deletions src/utils/headers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createMiddleware } from "hono/factory";

const contentSecurityPolicy = (nonce: string): string =>
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https://avatars.githubusercontent.com https://*.githubusercontent.com",
"connect-src 'self'",
"manifest-src 'self'",
"base-uri 'none'",
"object-src 'none'",
"form-action 'self' https://github.com",
"frame-ancestors 'none'",
].join("; ");

/**
* Sets a Content-Security-Policy for the HTML page routes. Applied per-route so it
* does not touch the JSON API or the CDN-backed Swagger UI. A per-request nonce is
* stashed on the context for the streaming renderer's Suspense scripts.
* @function pageCsp
*/
export const pageCsp = createMiddleware(async (c, next) => {
const bytes = crypto.getRandomValues(new Uint8Array(16));
const nonce = btoa(String.fromCharCode(...bytes));
c.set("cspNonce", nonce);
c.header("Content-Security-Policy", contentSecurityPolicy(nonce));
await next();
});
13 changes: 9 additions & 4 deletions src/utils/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { jsxRenderer } from "hono/jsx-renderer";
import { PropsWithChildren, Suspense } from "hono/jsx";
import { JSX } from "hono/jsx/jsx-runtime";
import { useRequestContext } from "hono/jsx-renderer";
import { StreamingContext } from "hono/jsx/streaming";
import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";

import { fetchRepositoryData } from "./octokit";
Expand Down Expand Up @@ -103,11 +104,15 @@ export const renderer = jsxRenderer(
children,
repository,
}: PropsWithChildren<{ repository?: Promise<RepositoryData> }>): JSX.Element => {
const c = useRequestContext();
const scriptNonce = c.get("cspNonce");
return (
<html lang="en">
<Head repository={repository} />
<Body children={children} />
</html>
<StreamingContext.Provider value={{ scriptNonce }}>
<html lang="en">
<Head repository={repository} />
<Body children={children} />
</html>
</StreamingContext.Provider>
);
},
{ docType: "<!DOCTYPE html>", stream: true }
Expand Down
Loading