Skip to content

Commit

Permalink
refactor: rearrange some dependencies, preparing for adapter(s)
Browse files Browse the repository at this point in the history
  • Loading branch information
atilafassina committed Oct 6, 2024
1 parent edf676c commit 17140ce
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 90 deletions.
71 changes: 36 additions & 35 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,52 @@ import { type SecHeaders } from "./types.js";
import crypto from "node:crypto";
import { appendHeader } from "vinxi/http";
import { DEFAULT_HEADERS, HEADER_NAMES } from "./defaults.js";
import { keyIsHeader } from "./utils.js";
import { generateCSP } from "./lib/csp.js";
import { deepFallbackMerge, keyIsHeader } from "./utils.js";
import { chooseCSP, generateCSP } from "./lib/csp.js";

export const secureRequest = (options?: SecHeaders) => (event: FetchEvent) => {
const settings: SecHeaders = { ...DEFAULT_HEADERS, ...options };

const chooseCSP = () => {
if (!settings.csp) {
return;
}
if (process.env.NODE_ENV === "development") {
return settings.csp.dev || settings.csp.prod;
} else {
return settings.csp.prod;
}
const h3Attacher =
(event: FetchEvent["nativeEvent"]) => (key: string, headerValue: string) => {
appendHeader(event, key, headerValue);
};

const nonce = crypto.randomBytes(16).toString("base64");
event.locals.nonce = nonce;

function attachSecHeaders(
settings: SecHeaders,
attacher: ReturnType<typeof h3Attacher>,
) {
const entries = Object.entries(settings) as Array<
[keyof SecHeaders, string | null]
>;

entries.forEach(([configKey, headerValue]) => {
for (const [configKey, headerValue] of entries) {
if (headerValue === null) {
return;
}

if (keyIsHeader(configKey)) {
const key = HEADER_NAMES[configKey];

appendHeader(event.nativeEvent, key, headerValue);
attacher(HEADER_NAMES[configKey], headerValue);
}
});

const csp = chooseCSP();

if (csp) {
appendHeader(
event.nativeEvent,
csp.cspBlock || !csp.cspReportOnly
? "Content-Security-Policy"
: "Content-Security-Policy-Report-Only",
generateCSP(csp.value, nonce),
);
}
};
}

// SolidStart FetchEvent is H3Event["context"]
export const secureRequest =
(options?: Partial<SecHeaders>) => (event: FetchEvent) => {
const settings = options
? deepFallbackMerge<SecHeaders>(options, DEFAULT_HEADERS)
: DEFAULT_HEADERS;
const addHeader = h3Attacher(event.nativeEvent);
const csp = chooseCSP(settings);

const nonce = crypto.randomBytes(16).toString("base64");
event.locals.nonce = nonce;

attachSecHeaders(settings, addHeader);

if (csp) {
addHeader(
csp.cspBlock || !csp.cspReportOnly
? "Content-Security-Policy"
: "Content-Security-Policy-Report-Only",
generateCSP(csp.value, nonce),
);
}
};
106 changes: 58 additions & 48 deletions src/lib/csp.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,76 @@
import {
getCSP,
nonce,
CSPDirectives,
} from "csp-header";
import { type CSP, type CSPHeaderConfig } from "../types.js";
import { getCSP, nonce, CSPDirectives } from "csp-header";
import { SecHeaders, type CSP, type CSPHeaderConfig } from "../types.js";
import { DEV_DEFAULT_CSP, PROD_DEFAULT_CSP } from "../defaults.js";

const cspNonceDirectives = [
"script-src",
"style-src",
"img-src",
"font-src",
"media-src",
"object-src",
"default-src",
"script-src",
"style-src",
"img-src",
"font-src",
"media-src",
"object-src",
"default-src",
] as const;

const DEFAULT_CSP: CSPHeaderConfig = {
prod: {
withNonce: true,
value: PROD_DEFAULT_CSP,
cspBlock: false,
cspReportOnly: true,
},
dev: {
withNonce: true,
value: DEV_DEFAULT_CSP,
cspBlock: true,
cspReportOnly: false,
},
prod: {
withNonce: true,
value: PROD_DEFAULT_CSP,
cspBlock: false,
cspReportOnly: true,
},
dev: {
withNonce: true,
value: DEV_DEFAULT_CSP,
cspBlock: true,
cspReportOnly: false,
},
};

export const addNonceToDirectives = (
userDefinedCSP: CSP["value"],
nonceString: string
userDefinedCSP: CSP["value"],
nonceString: string,
): CSP["value"] => {
const csp: Partial<CSPDirectives> = {
...DEFAULT_CSP.prod.value,
...userDefinedCSP,
};
const csp: Partial<CSPDirectives> = {
...DEFAULT_CSP.prod.value,
...userDefinedCSP,
};

cspNonceDirectives.forEach((directive) => {
if (csp[directive] && Array.isArray(csp[directive])) {
csp[directive].push(nonce(nonceString));
}
});
cspNonceDirectives.forEach((directive) => {
if (csp[directive] && Array.isArray(csp[directive])) {
csp[directive].push(nonce(nonceString));
}
});

return csp;
return csp;
};

export function generateCSP(cspOptions: CSP["value"], nonceString?: string) {
const isDev = process.env.NODE_ENV === "development";
const directives = nonceString && !isDev ? addNonceToDirectives(cspOptions, nonceString) : cspOptions;
const isDev = process.env.NODE_ENV === "development";
const directives =
nonceString && !isDev
? addNonceToDirectives(cspOptions, nonceString)
: cspOptions;

if (Object.prototype.hasOwnProperty.call(directives,"report-uri")) {
const reportUri = directives["report-uri"];
delete directives["report-uri"];
if (Object.prototype.hasOwnProperty.call(directives, "report-uri")) {
const reportUri = directives["report-uri"];
delete directives["report-uri"];

return getCSP({ directives, reportUri }) as string;
} else {
return getCSP({
directives,
}) as string;
}
return getCSP({ directives, reportUri }) as string;
} else {
return getCSP({
directives,
}) as string;
}
}

export const chooseCSP = (settings: SecHeaders) => {
if (!settings.csp) {
return;
}
if (process.env.NODE_ENV === "development") {
return settings.csp.dev || settings.csp.prod;
} else {
return settings.csp.prod;
}
};
14 changes: 7 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { HEADER_NAMES } from "./defaults.js";
import { type HeaderNames } from "./types.js";
import { SecHeaders, type HeaderNames } from "./types.js";

Check failure on line 2 in src/utils.ts

View workflow job for this annotation

GitHub Actions / Lint

'SecHeaders' is defined but never used

export function deepMerge(
target: Record<string, unknown>,
source: Record<string, unknown>,
): Record<string, unknown> {
export function deepFallbackMerge<TargetShape = Record<string, unknown>>(
target: Partial<TargetShape>,
source: TargetShape,
): TargetShape {
for (const key in source) {
if (source[key] instanceof Object) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
}

deepMerge(
deepFallbackMerge(
target[key] as Record<string, unknown>,
source[key] as Record<string, unknown>,
);
} else {
Object.assign(target, { [key]: source[key] });
}
}
return target;
return target as TargetShape;
}

export const keyIsHeader = (key: string): key is keyof HeaderNames => {
Expand Down

0 comments on commit 17140ce

Please sign in to comment.