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
8 changes: 8 additions & 0 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ function createManifest(
checkOrigin: false,
middleware: manifest?.middleware ?? middlewareInstance,
key: createKey(),
clientScriptHashes: manifest?.clientScriptHashes ?? [],
clientStyleHashes: manifest?.clientStyleHashes ?? [],
shouldInjectCspMetaTags: manifest?.shouldInjectCspMetaTags ?? false,
astroIslandHashes: manifest?.astroIslandHashes ?? [],
};
}

Expand Down Expand Up @@ -250,6 +254,10 @@ type AstroContainerManifest = Pick<
| 'publicDir'
| 'outDir'
| 'cacheDir'
| 'clientScriptHashes'
| 'clientStyleHashes'
| 'shouldInjectCspMetaTags'
| 'astroIslandHashes'
>;

type AstroContainerConstructor = {
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export type SSRManifest = {
publicDir: string | URL;
buildClientDir: string | URL;
buildServerDir: string | URL;
clientScriptHashes: string[];
clientStyleHashes: string[];
/**
* When enabled, Astro tracks the hashes of script and styles, and eventually it will render the `<meta>` tag
*/
shouldInjectCspMetaTags: boolean;
astroIslandHashes: string[];
};

export type SSRActions = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// This file is code-generated, please don't change it manually
export default [
export const ASTRO_ISLAND_HASHES = [
"GI/D8grziRZwfj/Mqmn+dcgU/i8sylHSR/IfobqcUT4=",
"HDWxd14AUw8OvjrhhRRyyZFHCGnzxXGDrg59Qi8ayhc=",
"XN6a2Vn8uvpBr/WhdYPdK0jVeCzlcOD2XYaP10veV4Y=",
"ZR0ZAU8UNTzLmo/ApeWH0y1mVLT+XtFkvZ5nw32W8jI=",
"cSNmhdbFlyTDRozeu9HPjo+B2S4QAeMp0RO41PqgAcA=",
"mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=",
"mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI="
"mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=",
"s81ZcLcyAa7P/Jh5M5hUxYthTGwW+iZY3e6aHrQ8H9E="
];
19 changes: 16 additions & 3 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
getStaticImageList,
prepareAssetsGenerationEnv,
} from '../../assets/build/generate.js';
import { type BuildInternals, hasPrerenderedPages } from '../../core/build/internal.js';
import { type BuildInternals, hasPrerenderedPages } from './internal.js';
import {
isRelativePath,
joinPaths,
Expand Down Expand Up @@ -49,6 +49,8 @@ import type {
StylesheetAsset,
} from './types.js';
import { getTimeStat, shouldAppendForwardSlash } from './util.js';
import { shouldTrackCspHashes, trackScriptHashes, trackStyleHashes } from '../csp/common.js';
import { ASTRO_ISLAND_HASHES } from '../astro-islands-hashes.js';

export async function generatePages(options: StaticBuildOptions, internals: BuildInternals) {
const generatePagesTimer = performance.now();
Expand Down Expand Up @@ -600,8 +602,6 @@ function getPrettyRouteName(route: RouteData): string {
* It creates a `SSRManifest` from the `AstroSettings`.
*
* Renderers needs to be pulled out from the page module emitted during the build.
* @param settings
* @param renderers
*/
function createBuildManifest(
settings: AstroSettings,
Expand All @@ -612,6 +612,15 @@ function createBuildManifest(
key: Promise<CryptoKey>,
): SSRManifest {
let i18nManifest: SSRManifestI18n | undefined = undefined;

let clientStyleHashes: string[] = [];
let clientScriptHashes: string[] = [];

if (shouldTrackCspHashes(settings.config)) {
clientScriptHashes = trackScriptHashes(internals, settings);
clientStyleHashes = trackStyleHashes(internals);
}

if (settings.config.i18n) {
i18nManifest = {
fallback: settings.config.i18n.fallback,
Expand Down Expand Up @@ -655,5 +664,9 @@ function createBuildManifest(
checkOrigin:
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
key,
clientStyleHashes,
clientScriptHashes,
shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config),
astroIslandHashes: ASTRO_ISLAND_HASHES,
};
}
14 changes: 14 additions & 0 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { type BuildInternals, cssOrder, mergeInlineCss } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
import type { StaticBuildOptions } from '../types.js';
import { makePageDataKey } from './util.js';
import { shouldTrackCspHashes, trackScriptHashes, trackStyleHashes } from '../../csp/common.js';
import { ASTRO_ISLAND_HASHES } from '../../astro-islands-hashes.js';

const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"]${manifestReplace}['"]`, 'g');
Expand Down Expand Up @@ -275,6 +277,14 @@ function buildManifest(
};
}

let clientScriptHashes: string[] = [];
let clientStyleHashes: string[] = [];

if (shouldTrackCspHashes(settings.config)) {
clientScriptHashes = trackScriptHashes(internals, opts.settings);
clientStyleHashes = trackStyleHashes(internals);
}

return {
hrefRoot: opts.settings.config.root.toString(),
cacheDir: opts.settings.config.cacheDir.toString(),
Expand Down Expand Up @@ -304,5 +314,9 @@ function buildManifest(
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
key: encodedKey,
sessionConfig: settings.config.session,
shouldInjectCspMetaTags: shouldTrackCspHashes(opts.settings.config),
clientStyleHashes,
clientScriptHashes,
astroIslandHashes: ASTRO_ISLAND_HASHES,
};
}
2 changes: 2 additions & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
session: false,
headingIdCompat: false,
preserveScriptOrder: false,
csp: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };

Expand Down Expand Up @@ -626,6 +627,7 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.preserveScriptOrder),
csp: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.csp),
})
.strict(
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/experimental-flags/ for a list of all current experiments.`,
Expand Down
40 changes: 40 additions & 0 deletions packages/astro/src/core/csp/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { AstroConfig } from '../../types/public/index.js';
import type { BuildInternals } from '../build/internal.js';
import crypto from 'node:crypto';
import type { AstroSettings } from '../../types/astro.js';

export function shouldTrackCspHashes(config: AstroConfig): boolean {
return config.experimental?.csp === true;
}

export function trackStyleHashes(internals: BuildInternals): string[] {
const clientStyleHashes: string[] = [];
for (const [_, page] of internals.pagesByViteID.entries()) {
for (const style of page.styles) {
if (style.sheet.type === 'inline') {
clientStyleHashes.push(
crypto.createHash('sha256').update(style.sheet.content).digest('base64'),
);
}
}
}

return clientStyleHashes;
}

export function trackScriptHashes(internals: BuildInternals, settings: AstroSettings): string[] {
const clientScriptHashes: string[] = [];

for (const script of internals.inlinedScripts.values()) {
clientScriptHashes.push(crypto.createHash('sha256').update(script).digest('base64'));
}

for (const script of settings.scripts) {
const { content, stage } = script;
if (stage === 'head-inline' || stage === 'before-hydration') {
clientScriptHashes.push(crypto.createHash('sha256').update(content).digest('base64'));
}
}

return clientScriptHashes;
}
3 changes: 3 additions & 0 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,9 @@ export class RenderContext {
extraHead: [],
propagators: new Set(),
},
shouldInjectCspMetaTags: manifest.shouldInjectCspMetaTags,
clientScriptHashes: manifest.clientScriptHashes,
clientStyleHashes: manifest.clientStyleHashes,
};

return result;
Expand Down
16 changes: 9 additions & 7 deletions packages/astro/src/integrations/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,21 @@ export function normalizeInjectedTypeFilename(filename: string, integrationName:
return `${normalizeCodegenDir(integrationName)}${filename.replace(SAFE_CHARS_RE, '_')}`;
}

interface RunHookConfigSetup {
settings: AstroSettings;
command: 'dev' | 'build' | 'preview' | 'sync';
logger: Logger;
isRestart?: boolean;
fs?: typeof fsMod;
}

export async function runHookConfigSetup({
settings,
command,
logger,
isRestart = false,
fs = fsMod,
}: {
settings: AstroSettings;
command: 'dev' | 'build' | 'preview' | 'sync';
logger: Logger;
isRestart?: boolean;
fs?: typeof fsMod;
}): Promise<AstroSettings> {
}: RunHookConfigSetup): Promise<AstroSettings> {
// An adapter is an integration, so if one is provided add it to the list of integrations.
if (settings.config.adapter) {
settings.config.integrations.unshift(settings.config.adapter);
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/runtime/server/astro-island-styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ISLAND_STYLES =
'<style>astro-island,astro-slot,astro-static-slot{display:contents}</style>';
2 changes: 1 addition & 1 deletion packages/astro/src/runtime/server/render/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { RenderInstruction } from './instruction.js';

import type { SSRResult } from '../../../types/public/internal.js';
import type { HTMLBytes, HTMLString } from '../escape.js';
import { markHTMLString } from '../escape.js';
Expand Down Expand Up @@ -99,6 +98,7 @@ function stringifyChunk(
}
return '';
}

default: {
throw new Error(`Unknown chunk type: ${(chunk as any).type}`);
}
Expand Down
21 changes: 21 additions & 0 deletions packages/astro/src/runtime/server/render/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ export function renderAllHeadContent(result: SSRResult) {
}
}

const hashes = [];

if (result.shouldInjectCspMetaTags) {
for (const scriptHash of [...result.clientScriptHashes, ...result.clientStyleHashes]) {
hashes.push(
renderElement(
'meta',
{
props: {
'http-equiv': 'content-security-policy',
content: scriptHash,
},
children: '',
},
false,
),
);
}
}
content += hashes.join('\n');

return markHTMLString(content);
}

Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/runtime/server/scripts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { SSRResult } from '../../types/public/internal.js';
import islandScriptDev from './astro-island.prebuilt-dev.js';
import islandScript from './astro-island.prebuilt.js';

const ISLAND_STYLES = `<style>astro-island,astro-slot,astro-static-slot{display:contents}</style>`;
import { ISLAND_STYLES } from './astro-island-styles.js';

export function determineIfNeedsHydrationScript(result: SSRResult): boolean {
if (result._metadata.hasHydrationScript) {
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2170,6 +2170,13 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
*/
headingIdCompat?: boolean;


/**
*
*/
// TODO: add docs once we are reaching the end
csp?: boolean,

/**
* @name experimental.preserveScriptOrder
* @type {boolean}
Expand Down
6 changes: 6 additions & 0 deletions packages/astro/src/types/public/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ export interface SSRResult {
trailingSlash: AstroConfig['trailingSlash'];
key: Promise<CryptoKey>;
_metadata: SSRMetadata;
/**
* Whether Astro should inject the CSP <meta> tag into the head of the component.
*/
shouldInjectCspMetaTags: boolean;
clientScriptHashes: string[];
clientStyleHashes: string[];
}

/**
Expand Down
9 changes: 7 additions & 2 deletions packages/astro/src/vite-plugin-astro-server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { DevPipeline } from './pipeline.js';
import { handleRequest } from './request.js';
import { setRouteError } from './server-state.js';
import { trailingSlashMiddleware } from './trailing-slash.js';
import { ASTRO_ISLAND_HASHES } from '../core/astro-islands-hashes.js';
import { shouldTrackCspHashes } from '../core/csp/common.js';

export interface AstroPluginOptions {
settings: AstroSettings;
Expand Down Expand Up @@ -100,8 +102,7 @@ export default function createVitePluginAstroServer({
});
const store = localStorage.getStore();
if (store instanceof IncomingMessage) {
const request = store;
setRouteError(controller.state, request.url!, error);
setRouteError(controller.state, store.url!, error);
}
const { errorWithMetadata } = recordServerError(loader, settings.config, pipeline, error);
setTimeout(
Expand Down Expand Up @@ -207,5 +208,9 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
};
},
sessionConfig: settings.config.experimental.session ? settings.config.session : undefined,
clientScriptHashes: [],
clientStyleHashes: [],
shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config),
astroIslandHashes: ASTRO_ISLAND_HASHES,
};
}
42 changes: 42 additions & 0 deletions packages/astro/test/csp.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { before, describe, it } from 'node:test';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';
import assert from 'node:assert/strict';
import * as cheerio from 'cheerio';

describe('CSP', () => {
let app;
/**
* @type {import('../dist/core/build/types.js').SSGManifest}
*/
let manifest;
/** @type {import('./test-utils.js').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/csp/',
adapter: testAdapter({
setManifest(_manifest) {
manifest = _manifest;
},
}),
});
await fixture.build();
app = await fixture.loadTestAdapterApp();
});

it('should contain the meta style hashes when CSS is imported from Astro component', async () => {
if (manifest) {
const request = new Request('http://example.com/index.html');
const response = await app.render(request);
const $ = cheerio.load(await response.text());

for (const hash of manifest.clientStyleHashes) {
let meta = $('meta[http-equiv="Content-Security-Policy"][content="' + hash + '"]');
assert.equal(meta.length, 1, `Should have a CSP meta tag for ${hash}`);
}
} else {
assert.fail('Should have the manifest');
}
});
});
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/csp/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';

export default defineConfig({
experimental: {
csp: true,
}
});

Loading