Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.

[dev server] refactor middleware into other files #3666

Merged
merged 3 commits into from
Jul 14, 2021
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
81 changes: 6 additions & 75 deletions packages/dev-server/src/MetroDevServer.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import Log from '@expo/bunyan';
import type Log from '@expo/bunyan';
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import * as ExpoMetroConfig from '@expo/metro-config';
import {
createDevServerMiddleware,
securityHeadersMiddleware,
} from '@react-native-community/cli-server-api';
import bodyParser from 'body-parser';
import type { Server as ConnectServer, HandleFunction } from 'connect';
import http from 'http';
import type { IncomingMessage, ServerResponse } from 'http';
import type { Server as ConnectServer } from 'connect';
import type http from 'http';
import type Metro from 'metro';
import path from 'path';
import resolveFrom from 'resolve-from';
import semver from 'semver';
import { parse as parseUrl } from 'url';

import {
buildHermesBundleAsync,
Expand All @@ -23,6 +21,9 @@ import {
import LogReporter from './LogReporter';
import clientLogsMiddleware from './middleware/clientLogsMiddleware';
import createJsInspectorMiddleware from './middleware/createJsInspectorMiddleware';
import { remoteDevtoolsCorsMiddleware } from './middleware/remoteDevtoolsCorsMiddleware';
import { remoteDevtoolsSecurityHeadersMiddleware } from './middleware/remoteDevtoolsSecurityHeadersMiddleware';
import { replaceMiddlewareWith } from './middleware/replaceMiddlewareWith';

export type MetroDevServerOptions = ExpoMetroConfig.LoadOptions & {
logger: Log;
Expand Down Expand Up @@ -257,76 +258,6 @@ function importMetroServerFromProject(projectRoot: string): typeof Metro.Server
return require(resolvedPath);
}

function replaceMiddlewareWith(
app: ConnectServer,
sourceMiddleware: HandleFunction,
targetMiddleware: HandleFunction
) {
const item = app.stack.find(middleware => middleware.handle === sourceMiddleware);
if (item) {
item.handle = targetMiddleware;
}
}

// Like securityHeadersMiddleware but further allow cross-origin requests
// from https://chrome-devtools-frontend.appspot.com/
function remoteDevtoolsSecurityHeadersMiddleware(
req: IncomingMessage,
res: ServerResponse,
next: (err?: Error) => void
) {
// Block any cross origin request.
if (
typeof req.headers.origin === 'string' &&
!req.headers.origin.match(/^https?:\/\/localhost:/) &&
!req.headers.origin.match(/^https:\/\/chrome-devtools-frontend\.appspot\.com/)
) {
next(
new Error(
`Unauthorized request from ${req.headers.origin}. ` +
'This may happen because of a conflicting browser extension to intercept HTTP requests. ' +
'Please try again without browser extensions or using incognito mode.'
)
);
return;
}

// Block MIME-type sniffing.
res.setHeader('X-Content-Type-Options', 'nosniff');

next();
}

// Middleware that accepts multiple Access-Control-Allow-Origin for processing *.map.
// This is a hook middleware before metro processing *.map,
// which originally allow only devtools://devtools
function remoteDevtoolsCorsMiddleware(
req: IncomingMessage,
res: ServerResponse,
next: (err?: Error) => void
) {
if (req.url) {
const url = parseUrl(req.url);
const origin = req.headers.origin;
const isValidOrigin =
origin &&
['devtools://devtools', 'https://chrome-devtools-frontend.appspot.com'].includes(origin);
if (url.pathname?.endsWith('.map') && origin && isValidOrigin) {
res.setHeader('Access-Control-Allow-Origin', origin);

// Prevent metro overwrite Access-Control-Allow-Origin header
const setHeader = res.setHeader.bind(res);
res.setHeader = (key, ...args) => {
if (key === 'Access-Control-Allow-Origin') {
return;
}
setHeader(key, ...args);
};
}
}
next();
}

// Cloned from xdl/src/Versions.ts, we cannot use that because of circular dependency
function gteSdkVersion(expJson: Pick<ExpoConfig, 'sdkVersion'>, sdkVersion: string): boolean {
if (!expJson.sdkVersion) {
Expand Down
32 changes: 32 additions & 0 deletions packages/dev-server/src/middleware/remoteDevtoolsCorsMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { IncomingMessage, ServerResponse } from 'http';
import { parse as parseUrl } from 'url';

// Middleware that accepts multiple Access-Control-Allow-Origin for processing *.map.
// This is a hook middleware before metro processing *.map,
// which originally allow only devtools://devtools
export function remoteDevtoolsCorsMiddleware(
req: IncomingMessage,
res: ServerResponse,
next: (err?: Error) => void
) {
if (req.url) {
const url = parseUrl(req.url);
const origin = req.headers.origin;
const isValidOrigin =
origin &&
['devtools://devtools', 'https://chrome-devtools-frontend.appspot.com'].includes(origin);
if (url.pathname?.endsWith('.map') && origin && isValidOrigin) {
res.setHeader('Access-Control-Allow-Origin', origin);

// Prevent metro overwrite Access-Control-Allow-Origin header
const setHeader = res.setHeader.bind(res);
res.setHeader = (key, ...args) => {
if (key === 'Access-Control-Allow-Origin') {
return;
}
setHeader(key, ...args);
};
}
}
next();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { IncomingMessage, ServerResponse } from 'http';

// Like securityHeadersMiddleware but further allow cross-origin requests
// from https://chrome-devtools-frontend.appspot.com/
export function remoteDevtoolsSecurityHeadersMiddleware(
req: IncomingMessage,
res: ServerResponse,
next: (err?: Error) => void
) {
// Block any cross origin request.
if (
typeof req.headers.origin === 'string' &&
!req.headers.origin.match(/^https?:\/\/localhost:/) &&
!req.headers.origin.match(/^https:\/\/chrome-devtools-frontend\.appspot\.com/)
) {
next(
new Error(
`Unauthorized request from ${req.headers.origin}. ` +
'This may happen because of a conflicting browser extension to intercept HTTP requests. ' +
'Please try again without browser extensions or using incognito mode.'
)
);
return;
}

// Block MIME-type sniffing.
res.setHeader('X-Content-Type-Options', 'nosniff');

next();
}
12 changes: 12 additions & 0 deletions packages/dev-server/src/middleware/replaceMiddlewareWith.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Server as ConnectServer, HandleFunction } from 'connect';

export function replaceMiddlewareWith(
app: ConnectServer,
sourceMiddleware: HandleFunction,
targetMiddleware: HandleFunction
) {
const item = app.stack.find(middleware => middleware.handle === sourceMiddleware);
if (item) {
item.handle = targetMiddleware;
}
}