Skip to content
Open
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
15 changes: 15 additions & 0 deletions flow-typed/npm/compression_v1.x.x.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/

declare module 'compression' {
import type {NextHandleFunction} from 'connect';

declare module.exports: () => NextHandleFunction;
}
15 changes: 15 additions & 0 deletions flow-typed/npm/nocache_v3.x.x.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/

declare module 'nocache' {
import type {NextHandleFunction} from 'connect';

declare module.exports: () => NextHandleFunction;
}
2 changes: 2 additions & 0 deletions packages/community-cli-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@
"@react-native/asset-utils": "0.87.0-main",
"@react-native/dev-middleware": "0.87.0-main",
"commander": "^12.0.0",
"compression": "^1.7.1",
"connect": "^3.6.5",
"debug": "^4.4.0",
"invariant": "^2.2.4",
"launch-editor": "^2.9.1",
"metro": "^0.87.2",
"nocache": "^3.0.1",
"open": "^8.4.2",
"semver": "^7.1.3"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import http from 'node:http';

jest.useRealTimers();

let server: http$Server;
let baseUrl: string;

beforeEach(async () => {
const connect = require('connect');
const createDevServerMiddleware =
require('../createDevServerMiddleware').default;
const app = connect();
server = http.createServer(app);
await new Promise(resolve => server.listen(0, 'localhost', resolve));
const address = server.address();
if (address == null || typeof address === 'string') {
throw new Error('Expected the server to listen on a TCP port');
}
baseUrl = `http://localhost:${address.port}`;
app
.use(
createDevServerMiddleware({
serverBaseUrl: new URL(baseUrl),
watchFolders: ['/project'],
}),
)
.use((req, res) => {
res.writeHead(404);
res.end();
});
});

afterEach(() => {
server.closeAllConnections();
return new Promise(resolve => server.close(() => resolve()));
});

describe('createDevServerMiddleware', () => {
test('refuses a cross-origin request before any route', async () => {
const resp = await fetch(baseUrl + '/open-url', {
headers: {Origin: 'https://example.com'},
});

expect(resp.status).toBe(403);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ beforeEach(() => {
});

async function startServer(
mount: (app: Server, serverBaseUrl: URL) => void,
mount: (app: Server) => void,
): Promise<{server: http$Server, baseUrl: string}> {
const app = connect();
const server = http.createServer(app);
Expand All @@ -62,19 +62,18 @@ async function startServer(
throw new Error('Expected the server to listen on a TCP port');
}
const baseUrl = `http://localhost:${address.port}`;
mount(app, new URL(baseUrl));
mount(app);
app.use((req, res) => {
res.writeHead(404);
res.end();
});
return {server, baseUrl};
}

function mountOpenStackFrame(app: Server, serverBaseUrl: URL) {
function mountOpenStackFrame(app: Server) {
app.use(
'/open-stack-frame',
require('../openStackFrameMiddleware').default({
serverBaseUrl,
watchFolders: [projectRoot],
}),
);
Expand All @@ -85,14 +84,10 @@ function stopServer(server: http$Server): Promise<void> {
return new Promise(resolve => server.close(() => resolve()));
}

function postFrame(
baseUrl: string,
body: string,
headers?: {Origin?: string},
): Promise<Response> {
function postFrame(baseUrl: string, body: string): Promise<Response> {
return fetch(baseUrl + '/open-stack-frame', {
method: 'POST',
headers: {...headers, 'Content-Type': 'application/json'},
headers: {'Content-Type': 'application/json'},
body,
});
}
Expand Down Expand Up @@ -129,17 +124,6 @@ describe('/open-stack-frame', () => {
expect(launchEditor).toHaveBeenCalledTimes(1);
});

test('refuses a cross-origin request', async () => {
const resp = await postFrame(
baseUrl,
JSON.stringify({file: appFile, lineNumber: 12}),
{Origin: 'https://example.com'},
);

expect(resp.status).toBe(403);
expect(launchEditor).not.toHaveBeenCalled();
});

test('rejects a body that is not JSON', async () => {
const resp = await postFrame(baseUrl, 'not json');

Expand Down Expand Up @@ -188,30 +172,3 @@ describe('/open-stack-frame', () => {
expect(launchEditor).not.toHaveBeenCalled();
});
});

describe('/open-stack-frame behind a middleware that serves it', () => {
let server: http$Server;
let baseUrl: string;

beforeEach(async () => {
({server, baseUrl} = await startServer((app, serverBaseUrl) => {
app.use('/open-stack-frame', (req, res, next) => {
res.end('handled upstream');
next();
});
mountOpenStackFrame(app, serverBaseUrl);
}));
});

afterEach(() => stopServer(server));

test('leaves the response to that middleware', async () => {
const resp = await postFrame(
baseUrl,
JSON.stringify({file: appFile, lineNumber: 12}),
);

expect(await resp.text()).toBe('handled upstream');
expect(launchEditor).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,7 @@ beforeEach(async () => {
}
baseUrl = `http://localhost:${address.port}`;
app
.use(
'/open-url',
require('../openURLMiddleware').default({
serverBaseUrl: new URL(baseUrl),
}),
)
.use('/open-url', require('../openURLMiddleware').default())
.use((req, res) => {
res.writeHead(404);
res.end();
Expand All @@ -49,10 +44,10 @@ afterEach(() => {
return new Promise(resolve => server.close(() => resolve()));
});

function postURL(body: string, headers?: {Origin?: string}): Promise<Response> {
function postURL(body: string): Promise<Response> {
return fetch(baseUrl + '/open-url', {
method: 'POST',
headers: {...headers, 'Content-Type': 'application/json'},
headers: {'Content-Type': 'application/json'},
body,
});
}
Expand Down Expand Up @@ -80,16 +75,6 @@ describe('/open-url', () => {
expect(open).toHaveBeenCalledTimes(1);
});

test('refuses a cross-origin request', async () => {
const resp = await postURL(
JSON.stringify({url: 'https://reactnative.dev/'}),
{Origin: 'https://example.com'},
);

expect(resp.status).toBe(403);
expect(open).not.toHaveBeenCalled();
});

test('rejects a body that is not JSON', async () => {
const resp = await postURL('not json');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import securityHeadersMiddleware from '../securityHeadersMiddleware';
import connect from 'connect';
import http from 'node:http';

jest.useRealTimers();

let server: http$Server;
let baseUrl: string;
let reached: JestMockFn<[], void>;

beforeEach(async () => {
reached = jest.fn();
const app = connect();
server = http.createServer(app);
await new Promise(resolve => server.listen(0, 'localhost', resolve));
const address = server.address();
if (address == null || typeof address === 'string') {
throw new Error('Expected the server to listen on a TCP port');
}
baseUrl = `http://localhost:${address.port}`;
app
.use(securityHeadersMiddleware({serverBaseUrl: new URL(baseUrl)}))
.use((req, res) => {
reached();
res.end('OK');
});
});

afterEach(() => {
server.closeAllConnections();
return new Promise(resolve => server.close(() => resolve()));
});

describe('securityHeadersMiddleware', () => {
test.each([
['no origin', undefined],
['the dev server origin', 'SERVER'],
['a local origin on another port', 'http://localhost:3000'],
['Chrome DevTools', 'devtools://devtools/bundled/inspector.html'],
])('accepts a request from %s', async (_, origin) => {
const resp = await fetch(baseUrl, {
headers:
origin == null ? {} : {Origin: origin === 'SERVER' ? baseUrl : origin},
});

expect(resp.status).toBe(200);
expect(resp.headers.get('X-Content-Type-Options')).toBe('nosniff');
expect(reached).toHaveBeenCalledTimes(1);
});

test.each([
['another site', 'https://example.com'],
['a malformed origin', 'not an origin'],
])('refuses a request from %s', async (_, origin) => {
const resp = await fetch(baseUrl, {headers: {Origin: origin}});

expect(resp.status).toBe(403);
expect(reached).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import type {Server} from 'connect';

import openStackFrameMiddleware from './openStackFrameMiddleware';
import openURLMiddleware from './openURLMiddleware';
import securityHeadersMiddleware from './securityHeadersMiddleware';
import compression from 'compression';
import connect from 'connect';
import nocache from 'nocache';

type Options = Readonly<{
serverBaseUrl: URL,
watchFolders: ReadonlyArray<string>,
}>;

/**
* The dev server routes that React Native tooling and apps call outside
* Metro and `@react-native/dev-middleware`.
*/
export default function createDevServerMiddleware({
serverBaseUrl,
watchFolders,
}: Options): Server {
return connect()
.use(securityHeadersMiddleware({serverBaseUrl}))
.use(compression())
.use(nocache())
.use('/open-stack-frame', openStackFrameMiddleware({watchFolders}))
.use('/open-url', openURLMiddleware());
}
Loading
Loading