Skip to content

Commit

Permalink
core: restructure csp
Browse files Browse the repository at this point in the history
  • Loading branch information
terrablue committed Sep 21, 2023
1 parent 298c9ec commit 64ce252
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 98 deletions.
23 changes: 0 additions & 23 deletions packages/frontend/src/depend.js

This file was deleted.

14 changes: 7 additions & 7 deletions packages/frontend/src/frontends/common/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default config => {
const options = {
liveview: app.liveview !== undefined,
};
const {headers} = request;
if (as_layout) {
return make(name, props);
}
Expand All @@ -25,7 +24,8 @@ export default config => {
const data = components.map(component => component.props);
const names = await get_names(components);

if (options.liveview && headers.get(app.liveview.header) !== undefined) {
if (options.liveview &&
request.headers.get(app.liveview?.header) !== undefined) {
return new Response(JSON.stringify({names, data}), {
status,
headers: {...await app.headers(),
Expand All @@ -40,14 +40,14 @@ export default config => {
});

const code = client({names, data}, options);
const inlined = await app.inline(code, "module");

await app.publish({code, type: "module", inline: true});
// needs to be called before app.render
const headers$ = await app.headers();
const headers = app.headers({script: inlined.csp});
const rendered = {body, page, head: head.concat(inlined.head)};

return new Response(await app.render({body, page, head}), {
return new Response(await app.render(rendered), {
status,
headers: {...headers$, "Content-Type": MediaType.TEXT_HTML},
headers: {...headers, "Content-Type": MediaType.TEXT_HTML},
});
};
};
2 changes: 1 addition & 1 deletion packages/frontend/src/frontends/common/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import compile from "./compile.js";
import publish from "./publish.js";
import normalize from "./normalize.js";
import peers from "./peers.js";
import depend from "../../depend.js";
import depend from "../depend.js";

export default async ({
name,
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/frontends/depend.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {tryreturn} from "runtime-compat/async";
import {to} from "runtime-compat/object";
import {packager} from "runtime-compat/meta";
import errors from "./errors.js";
import errors from "../errors.js";

const {MissingDependencies} = errors;
const MODULE_NOT_FOUND = "ERR_MODULE_NOT_FOUND";
Expand Down
4 changes: 1 addition & 3 deletions packages/frontend/src/frontends/handlebars/module.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import {Response, Status} from "runtime-compat/http";
import {filter} from "runtime-compat/object";
import {compile, peers, load} from "../common/exports.js";
import depend from "../../depend.js";
import depend from "../depend.js";

const handler = ({directory, render}) =>
(name, props = {}, {status = Status.OK, page} = {}) => async app => {
const components = app.runpath(app.config.location.server, directory);
const {default : component} = await load(components.join(name));

const body = render(component, props);

const headers = await app.headers();

return new Response(await app.render({body, page}), {status, headers});
Expand Down
27 changes: 8 additions & 19 deletions packages/frontend/src/frontends/htmx/module.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Response, Status, MediaType} from "runtime-compat/http";
import {filter} from "runtime-compat/object";
import {peers} from "../common/exports.js";
import depend from "../../depend.js";
import depend from "../depend.js";

const load_component = async (file) => {
try {
Expand All @@ -11,30 +11,19 @@ const load_component = async (file) => {
}
};

const get_body = async (app, partial, file) => {
const body = await load_component(file);
return partial ? body : app.render({body});
};
const style = "'unsafe-inline'";
const code = "import {htmx} from \"app\";";

const handler = directory =>
(name, {status = Status.OK, partial = false} = {}) => async app => {
const components = app.runpath(directory);
const code = "import {htmx} from \"app\";";
await app.publish({code, type: "module", inline: true});

const headers = app.headers();
const csp = headers["Content-Security-Policy"].replace(
"style-src 'self'", "style-src 'self' 'unsafe-inline'"
);
const body = await get_body(app, partial, components.join(name).file);
const {head, csp} = await app.inline(code, "module");
const headers = {style, script: csp};
const body = await load_component(components.join(name).file);

return new Response(body, {
return new Response(partial ? body : await app.render({body, head}), {
status,
headers: {
...headers,
"Content-Type": MediaType.TEXT_HTML,
"Content-Security-Policy": csp,
},
headers: {...app.headers(headers), "Content-Type": MediaType.TEXT_HTML},
});
};

Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/frontends/markdown/module.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Response, Status, MediaType} from "runtime-compat/http";
import {stringify, filter} from "runtime-compat/object";
import {peers} from "../common/exports.js";
import depend from "../../depend.js";
import depend from "../depend.js";

const respond = (handler, directory) => (...[name, ...rest]) =>
async (app, ...noapp) => {
Expand Down
3 changes: 1 addition & 2 deletions packages/frontend/src/frontends/vue/module.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Response, Status, MediaType} from "runtime-compat/http";
import {filter} from "runtime-compat/object";
import {register, compile, peers} from "../common/exports.js";
import depend from "../../depend.js";
import depend from "../depend.js";

const name = "vue";
const dependencies = ["vue"];
Expand All @@ -10,7 +10,6 @@ const default_extension = "vue";
const handler = config => (name, props = {}, {status = Status.OK, page} = {}) =>
async app => {
const {make, createSSRApp, render} = config;

const imported = await make(name, props);
const component = createSSRApp({
render: imported.component.render,
Expand Down
62 changes: 34 additions & 28 deletions packages/primate/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {tryreturn} from "runtime-compat/async";
import {File, Path} from "runtime-compat/fs";
import {bold, blue} from "runtime-compat/colors";
import {is} from "runtime-compat/invariant";
import {transform, valmap, to} from "runtime-compat/object";
import {transform, valmap} from "runtime-compat/object";
import {globify} from "runtime-compat/string";
import * as runtime from "runtime-compat/meta";

Expand All @@ -29,6 +29,22 @@ const attribute = attributes => Object.keys(attributes).length > 0
: "";
const tag = ({name, attributes = {}, code = "", close = true}) =>
`<${name}${attribute(attributes)}${close ? `>${code}</${name}>` : "/>"}`;
const tags = {
// inline: <script type integrity>...</script>
// outline: <script type integrity src></script>
script({inline, code, type, integrity, src}) {
return inline
? tag({name: "script", attributes: {type, integrity}, code})
: tag({name: "script", attributes: {type, integrity, src}});
},
// inline: <style>...</style>
// outline: <link rel="stylesheet" href/>
style({inline, code, href, rel = "stylesheet"}) {
return inline
? tag({name: "style", code})
: tag({name: "link", attributes: {rel, href}, close: false});
},
};

const {name, version} = await new Path(import.meta.url).up(2)
.join(runtime.manifest).json();
Expand Down Expand Up @@ -96,24 +112,21 @@ export default async (log, root, config) => {
}
}));
},
headers() {
headers({script = "", style = ""} = {}) {
const csp = Object.keys(http.csp).reduce((policy, key) =>
`${policy}${key} ${http.csp[key]};`, "")
.replace("script-src 'self'", `script-src 'self' ${
.replace("script-src 'self'", `script-src 'self' ${script} ${
this.assets
.filter(({type}) => type !== "style")
.map(asset => `'${asset.integrity}'`).join(" ")
} `)
.replace("style-src 'self'", `style-src 'self' ${
}`)
.replace("style-src 'self'", `style-src 'self' ${style} ${
this.assets
.filter(({type}) => type === "style")
.map(asset => `'${asset.integrity}'`).join(" ")
} `);
}`);

return {
"Content-Security-Policy": csp,
"Referrer-Policy": "same-origin",
};
return {"Content-Security-Policy": csp, "Referrer-Policy": "same-origin"};
},
runpath(...directories) {
return this.path.build.join(...directories);
Expand All @@ -122,29 +135,22 @@ export default async (log, root, config) => {
const {location: {pages}} = this.config;

const html = await index(this.runpath(pages), page, config.pages.index);
// inline: <script type integrity>...</script>
// outline: <script type integrity src></script>
const script = ({inline, code, type, integrity, src}) => inline
? tag({name: "script", attributes: {type, integrity}, code})
: tag({name: "script", attributes: {type, integrity, src}});
// inline: <style>...</style>
// outline: <link rel="stylesheet" href/>
const style = ({inline, code, href, rel = "stylesheet"}) => inline
? tag({name: "style", code})
: tag({name: "link", attributes: {rel, href}, close: false});

const heads = head.concat("\n", to_sorted(this.assets,

const heads = to_sorted(this.assets,
({type}) => -1 * (type === "importmap"))
.map(({src, code, type, inline, integrity}) =>
type === "style"
? style({inline, code, href: src})
: script({inline, code, type, integrity, src})
).join("\n"));
// remove inline assets
this.assets = this.assets.filter(({inline, type}) => !inline
|| type === "importmap");
? tags.style({inline, code, href: src})
: tags.script({inline, code, type, integrity, src})
).join("\n").concat("\n", head);
return html.replace("%body%", _ => body).replace("%head%", _ => heads);
},
async inline(code, type) {
const integrity = await this.hash(code);
const tag_name = type === "style" ? "style" : "script";
const head = tags[tag_name]({code, type, inline: true, integrity});
return {head, csp: `'${integrity}'`};
},
async publish({src, code, type = "", inline = false, copy = true}) {
if (!inline && copy) {
const base = this.runpath(this.config.location.client).join(src);
Expand Down
25 changes: 14 additions & 11 deletions packages/primate/src/handlers/html.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import {Response, Status, MediaType} from "runtime-compat/http";

const script = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
const style = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
const inline = true;

export default (name, options = {}) => {
const {status = Status.OK, partial = false} = options;

return async app => {
const html = await app.path.components.join(name).text();
await Promise.all([...html.matchAll(script)]
.map(({groups: {code}}) => app.publish({code, inline})));
await Promise.all([...html.matchAll(style)]
.map(({groups: {code}}) => app.publish({code, type: "style", inline})));
const scripts = await Promise.all([...html.matchAll(script_re)]
.map(({groups: {code}}) => app.inline(code, "module")));
const styles = await Promise.all([...html.matchAll(style_re)]
.map(({groups: {code}}) => app.inline(code, "style")));
const assets = [...scripts, ...styles];

const body = html.replaceAll(remove, _ => "");
// needs to happen before app.render()
const headers = app.headers();
const head = assets.map(asset => asset.head).join("\n");
const script = scripts.map(asset => asset.csp).join(" ");
const style = styles.map(asset => asset.csp).join(" ");
const headers = {script, style};

return new Response(partial ? body : await app.render({body}), {
return new Response(partial ? body : await app.render({body, head}), {
status,
headers: {...headers, "Content-Type": MediaType.TEXT_HTML},
headers: {...app.headers(headers), "Content-Type": MediaType.TEXT_HTML},
});
};
};
3 changes: 1 addition & 2 deletions packages/primate/src/hooks/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@ const post = async app => {
}

const imports = {...app.importmaps, app: src.path};
const inline = true;
const type = "importmap";
await app.publish({inline, code: stringify({imports}), type});
await app.publish({inline: true, code: stringify({imports}), type});
}

if (await path.static.exists) {
Expand Down

0 comments on commit 64ce252

Please sign in to comment.