Skip to content

Commit

Permalink
Support nonce option to be passed to inline scripts (#22593)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage authored Oct 20, 2021
1 parent 34e4c97 commit 3677c01
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 12 deletions.
38 changes: 37 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ let PropTypes;
let textCache;
let document;
let writable;
let CSPnonce = null;
let container;
let buffer = '';
let hasErrored = false;
Expand Down Expand Up @@ -91,7 +92,10 @@ describe('ReactDOMFizzServer', () => {
fakeBody.innerHTML = bufferedContent;
while (fakeBody.firstChild) {
const node = fakeBody.firstChild;
if (node.nodeName === 'SCRIPT') {
if (
node.nodeName === 'SCRIPT' &&
(CSPnonce === null || node.getAttribute('nonce') === CSPnonce)
) {
const script = document.createElement('script');
script.textContent = node.textContent;
fakeBody.removeChild(node);
Expand Down Expand Up @@ -281,6 +285,38 @@ describe('ReactDOMFizzServer', () => {
);
});

// @gate experimental
it('should support nonce scripts', async () => {
CSPnonce = 'R4nd0m';
try {
let resolve;
const Lazy = React.lazy(() => {
return new Promise(r => {
resolve = r;
});
});

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div>
<Suspense fallback={<Text text="Loading..." />}>
<Lazy text="Hello" />
</Suspense>
</div>,
{nonce: 'R4nd0m'},
);
pipe(writable);
});
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
await act(async () => {
resolve({default: Text});
});
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
} finally {
CSPnonce = null;
}
});

// @gate experimental
it('should client render a boundary if a lazy component rejects', async () => {
let rejectComponent;
Expand Down
6 changes: 5 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
nonce?: string,
progressiveChunkSize?: number,
signal?: AbortSignal,
onCompleteShell?: () => void,
Expand All @@ -39,7 +40,10 @@ function renderToReadableStream(
): ReadableStream {
const request = createRequest(
children,
createResponseState(options ? options.identifierPrefix : undefined),
createResponseState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
Expand Down
6 changes: 5 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function createDrainHandler(destination, request) {
type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
nonce?: string,
progressiveChunkSize?: number,
onCompleteShell?: () => void,
onCompleteAll?: () => void,
Expand All @@ -47,7 +48,10 @@ type Controls = {|
function createRequestImpl(children: ReactNodeList, options: void | Options) {
return createRequest(
children,
createResponseState(options ? options.identifierPrefix : undefined),
createResponseState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
Expand Down
28 changes: 20 additions & 8 deletions packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const isPrimaryRenderer = true;

// Per response, global state that is not contextual to the rendering subtree.
export type ResponseState = {
startInlineScript: PrecomputedChunk,
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
boundaryPrefix: string,
Expand All @@ -71,12 +72,22 @@ export type ResponseState = {
...
};

const startInlineScript = stringToPrecomputedChunk('<script>');

// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(
identifierPrefix: string | void,
nonce: string | void,
): ResponseState {
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
const inlineScriptWithNonce =
nonce === undefined
? startInlineScript
: stringToPrecomputedChunk(
'<script nonce="' + escapeTextForBrowser(nonce) + '">',
);
return {
startInlineScript: inlineScriptWithNonce,
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
boundaryPrefix: idPrefix + 'B:',
Expand Down Expand Up @@ -1689,9 +1700,9 @@ const clientRenderFunction =
'function $RX(a){if(a=document.getElementById(a))a=a.previousSibling,a.data="$!",a._reactRetry&&a._reactRetry()}';

const completeSegmentScript1Full = stringToPrecomputedChunk(
'<script>' + completeSegmentFunction + ';$RS("',
completeSegmentFunction + ';$RS("',
);
const completeSegmentScript1Partial = stringToPrecomputedChunk('<script>$RS("');
const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("');
const completeSegmentScript2 = stringToPrecomputedChunk('","');
const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');

Expand All @@ -1700,6 +1711,7 @@ export function writeCompletedSegmentInstruction(
responseState: ResponseState,
contentSegmentID: number,
): boolean {
writeChunk(destination, responseState.startInlineScript);
if (!responseState.sentCompleteSegmentFunction) {
// The first time we write this, we'll need to include the full implementation.
responseState.sentCompleteSegmentFunction = true;
Expand All @@ -1718,11 +1730,9 @@ export function writeCompletedSegmentInstruction(
}

const completeBoundaryScript1Full = stringToPrecomputedChunk(
'<script>' + completeBoundaryFunction + ';$RC("',
);
const completeBoundaryScript1Partial = stringToPrecomputedChunk(
'<script>$RC("',
completeBoundaryFunction + ';$RC("',
);
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');

Expand All @@ -1732,6 +1742,7 @@ export function writeCompletedBoundaryInstruction(
boundaryID: SuspenseBoundaryID,
contentSegmentID: number,
): boolean {
writeChunk(destination, responseState.startInlineScript);
if (!responseState.sentCompleteBoundaryFunction) {
// The first time we write this, we'll need to include the full implementation.
responseState.sentCompleteBoundaryFunction = true;
Expand All @@ -1756,16 +1767,17 @@ export function writeCompletedBoundaryInstruction(
}

const clientRenderScript1Full = stringToPrecomputedChunk(
'<script>' + clientRenderFunction + ';$RX("',
clientRenderFunction + ';$RX("',
);
const clientRenderScript1Partial = stringToPrecomputedChunk('<script>$RX("');
const clientRenderScript1Partial = stringToPrecomputedChunk('$RX("');
const clientRenderScript2 = stringToPrecomputedChunk('")</script>');

export function writeClientRenderBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
): boolean {
writeChunk(destination, responseState.startInlineScript);
if (!responseState.sentClientRenderFunction) {
// The first time we write this, we'll need to include the full implementation.
responseState.sentClientRenderFunction = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const isPrimaryRenderer = false;

export type ResponseState = {
// Keep this in sync with ReactDOMServerFormatConfig
startInlineScript: PrecomputedChunk,
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
boundaryPrefix: string,
Expand All @@ -46,9 +47,10 @@ export function createResponseState(
generateStaticMarkup: boolean,
identifierPrefix: string | void,
): ResponseState {
const responseState = createResponseStateImpl(identifierPrefix);
const responseState = createResponseStateImpl(identifierPrefix, undefined);
return {
// Keep this in sync with ReactDOMServerFormatConfig
startInlineScript: responseState.startInlineScript,
placeholderPrefix: responseState.placeholderPrefix,
segmentPrefix: responseState.segmentPrefix,
boundaryPrefix: responseState.boundaryPrefix,
Expand Down

0 comments on commit 3677c01

Please sign in to comment.