netlify-cli
Version:
Netlify command line tool
776 lines • 37.3 kB
JavaScript
import { Buffer } from 'buffer';
import { once } from 'events';
import { readFile } from 'fs/promises';
import http from 'http';
import https from 'https';
import { isIPv6 } from 'net';
import { Socket } from 'node:net';
import { Readable } from 'node:stream';
import path from 'path';
import process from 'process';
import url from 'url';
import util from 'util';
import zlib from 'zlib';
import contentType from 'content-type';
import cookie from 'cookie';
import { getProperty } from 'dot-prop';
import generateETag from 'etag';
import getAvailablePort from 'get-port';
import httpProxy from 'http-proxy';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { jwtDecode } from 'jwt-decode';
import { locatePath } from 'locate-path';
import throttle from 'lodash/throttle.js';
import pFilter from 'p-filter';
import { handleProxyRequest, initializeProxy as initializeEdgeFunctionsProxy, isEdgeFunctionsRequest, } from '../lib/edge-functions/proxy.js';
import { fileExistsAsync, isFileAsync } from '../lib/fs.js';
import { getFormHandler } from '../lib/functions/form-submissions-handler.js';
import { DEFAULT_FUNCTION_URL_EXPRESSION } from '../lib/functions/registry.js';
import { initializeProxy as initializeImageProxy, isImageRequest } from '../lib/images/proxy.js';
import renderErrorTemplate from '../lib/render-error-template.js';
import { NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, log } from './command-helpers.js';
import createStreamPromise from './create-stream-promise.js';
import { NFFunctionName, NFFunctionRoute, NFRequestID, headersForPath, parseHeaders } from './headers.js';
import { generateRequestID } from './request-id.js';
import { createRewriter, onChanges } from './rules-proxy.js';
import { signRedirect } from './sign-redirect.js';
const gunzip = util.promisify(zlib.gunzip);
const gzip = util.promisify(zlib.gzip);
const brotliDecompress = util.promisify(zlib.brotliDecompress);
const brotliCompress = util.promisify(zlib.brotliCompress);
const deflate = util.promisify(zlib.deflate);
const inflate = util.promisify(zlib.inflate);
const shouldGenerateETag = Symbol('Internal: response should generate ETag');
const decompressResponseBody = async function (body, contentEncoding = '') {
switch (contentEncoding) {
case 'gzip':
return await gunzip(body);
case 'br':
return await brotliDecompress(body);
case 'deflate':
return await inflate(body);
default:
return body;
}
};
const compressResponseBody = async function (body, contentEncoding = '') {
switch (contentEncoding) {
case 'gzip':
return await gzip(body);
case 'br':
return await brotliCompress(body);
case 'deflate':
return await deflate(body);
default:
return Buffer.from(body, 'utf8');
}
};
const injectHtml = async function (responseBody, proxyRes, htmlInjections) {
const decompressedBody = await decompressResponseBody(responseBody, proxyRes.headers['content-encoding']);
const bodyWithInjections = (htmlInjections ?? []).reduce((accum, htmlInjection) => {
if (!htmlInjection.html || typeof htmlInjection.html !== 'string') {
return accum;
}
const location = htmlInjection.location ?? 'before_closing_head_tag';
if (location === 'before_closing_head_tag') {
accum = accum.replace('</head>', `${htmlInjection.html}</head>`);
}
else if (location === 'before_closing_body_tag') {
accum = accum.replace('</body>', `${htmlInjection.html}</body>`);
}
return accum;
}, decompressedBody.toString());
return await compressResponseBody(bodyWithInjections, proxyRes.headers['content-encoding']);
};
const formatEdgeFunctionError = (errorBuffer, acceptsHtml) => {
const { error: { message, name, stack }, } = JSON.parse(errorBuffer.toString());
if (!acceptsHtml) {
return `${name}: ${message}\n ${stack}`;
}
return JSON.stringify({
errorType: name,
errorMessage: message,
trace: stack.split('\\n'),
});
};
function isInternal(url) {
return url?.startsWith('/.netlify/') ?? false;
}
function isFunction(functionsPort, url) {
return functionsPort && url.match(DEFAULT_FUNCTION_URL_EXPRESSION);
}
function getAddonUrl(addonsUrls, req) {
const matches = req.url?.match(/^\/.netlify\/([^/]+)(\/.*)/);
const addonUrl = matches && addonsUrls[matches[1]];
return addonUrl ? `${addonUrl}${matches[2]}` : null;
}
const getStatic = async function (pathname, publicFolder) {
const alternatives = [pathname, ...alternativePathsFor(pathname)].map((filePath) => path.resolve(publicFolder, filePath.slice(1)));
const file = await locatePath(alternatives);
if (file === undefined) {
return false;
}
return `/${path.relative(publicFolder, file)}`;
};
const isEndpointExists = async function (endpoint, origin) {
const url = new URL(endpoint, origin);
try {
const res = await fetch(url, { method: 'HEAD' });
return res.status !== 404;
}
catch (e) {
return false;
}
};
const isExternal = function (match) {
return 'to' in match && /^https?:\/\//.exec(match.to) != null;
};
const stripOrigin = function ({ hash, pathname, search }) {
return `${pathname}${search}${hash}`;
};
const proxyToExternalUrl = function ({ dest, destURL, req, res, }) {
const handler = createProxyMiddleware({
target: dest.origin,
changeOrigin: true,
pathRewrite: () => destURL,
// hide logging
logLevel: 'warn',
...(Buffer.isBuffer(req.originalBody) && { buffer: Readable.from(req.originalBody) }),
});
// @ts-expect-error TS(2345) FIXME: Argument of type 'Request' is not assignable to parameter of type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.
handler(req, res, () => { });
};
// @ts-expect-error TS(7031) FIXME: Binding element 'addonUrl' implicitly has an 'any'... Remove this comment to see the full error message
const handleAddonUrl = function ({ addonUrl, req, res }) {
const dest = new URL(addonUrl);
const destURL = stripOrigin(dest);
proxyToExternalUrl({ req, res, dest, destURL });
};
const isRedirect = function (match) {
return 'status' in match && match.status != null && match.status >= 300 && match.status <= 400;
};
const render404 = async function (publicFolder) {
const maybe404Page = path.resolve(publicFolder, '404.html');
try {
const isFile = await isFileAsync(maybe404Page);
if (isFile)
return await readFile(maybe404Page, 'utf-8');
}
catch (error) {
console.warn(NETLIFYDEVWARN, 'Error while serving 404.html file', error instanceof Error ? error.message : error?.toString());
}
return 'Not Found';
};
// Used as an optimization to avoid dual lookups for missing assets
const assetExtensionRegExp = /\.(html?|png|jpg|js|css|svg|gif|ico|woff|woff2)$/;
// @ts-expect-error TS(7006) FIXME: Parameter 'url' implicitly has an 'any' type.
const alternativePathsFor = function (url) {
if (isFunction(true, url)) {
return [];
}
const paths = [];
if (url[url.length - 1] === '/') {
const end = url.length - 1;
if (url !== '/') {
paths.push(`${url.slice(0, end)}.html`, `${url.slice(0, end)}.htm`);
}
paths.push(`${url}index.html`, `${url}index.htm`);
}
else if (!assetExtensionRegExp.test(url)) {
paths.push(`${url}.html`, `${url}.htm`, `${url}/index.html`, `${url}/index.htm`);
}
return paths;
};
const notifyActivity = throttle((api, siteId, devServerId) => {
// @ts-expect-error(serhalp) -- It looks like the generated API types don't include "internal" methods
// (https://github.com/netlify/open-api/blob/66813d46e47f207443b7aebce2c22c4a4c8ca867/swagger.yml#L2642). Fix?
api.markDevServerActivity({ siteId, devServerId }).catch((error) => {
console.error(`${NETLIFYDEVWARN} Failed to notify activity`, error);
});
}, 30 * 1000);
const serveRedirect = async function ({ env, functionsRegistry, imageProxy, match, options, proxy, req, res, siteInfo, }) {
if (!match)
return proxy.web(req, res, options);
options = options || req.proxyOptions || {};
options.match = null;
if (match.force404) {
res.writeHead(404);
res.end(await render404(options.publicFolder));
return;
}
if (match.proxyHeaders && Object.keys(match.proxyHeaders).length >= 0) {
Object.entries(match.proxyHeaders).forEach(([key, value]) => {
req.headers[key] = value;
});
}
if (match.signingSecret) {
const signingSecretVar = env[match.signingSecret];
if (signingSecretVar) {
req.headers['x-nf-sign'] = signRedirect({
deployContext: 'dev',
secret: signingSecretVar.value,
siteID: siteInfo.id,
siteURL: siteInfo.url,
});
}
else {
log(NETLIFYDEVWARN, `Could not sign redirect because environment variable ${chalk.yellow(match.signingSecret)} is not set`);
}
}
if (isFunction(options.functionsPort, req.url)) {
return proxy.web(req, res, { target: options.functionsServer });
}
const urlForAddons = getAddonUrl(options.addonsUrls, req);
if (urlForAddons) {
handleAddonUrl({ req, res, addonUrl: urlForAddons });
return;
}
const originalURL = req.url;
if (match.exceptions && match.exceptions.JWT) {
// Some values of JWT can start with :, so, make sure to normalize them
const expectedRoles = new Set(match.exceptions.JWT.split(',').map((value) => (value.startsWith(':') ? value.slice(1) : value)));
const cookieValues = cookie.parse(req.headers.cookie || '');
const token = cookieValues.nf_jwt;
// Serve not found by default
req.url = '/.netlify/non-existent-path';
if (token) {
let jwtValue = {};
try {
jwtValue = jwtDecode(token) || {};
}
catch (error) {
// @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'.
console.warn(NETLIFYDEVWARN, 'Error while decoding JWT provided in request', error.message);
res.writeHead(400);
res.end('Invalid JWT provided. Please see logs for more info.');
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'exp' does not exist on type '{}'.
if ((jwtValue.exp || 0) < Math.round(Date.now() / MILLISEC_TO_SEC)) {
console.warn(NETLIFYDEVWARN, 'Expired JWT provided in request', req.url);
}
else {
const presentedRoles = getProperty(jwtValue, options.jwtRolePath) || [];
if (!Array.isArray(presentedRoles)) {
console.warn(NETLIFYDEVWARN, `Invalid roles value provided in JWT ${options.jwtRolePath}`, presentedRoles);
res.writeHead(400);
res.end('Invalid JWT provided. Please see logs for more info.');
return;
}
// Restore the URL if everything is correct
if (presentedRoles.some((pr) => expectedRoles.has(pr))) {
req.url = originalURL;
}
}
}
}
const reqUrl = reqToURL(req, req.url);
const isHiddenProxy = match.proxyHeaders &&
Object.entries(match.proxyHeaders).some(([key, val]) => key.toLowerCase() === 'x-nf-hidden-proxy' && val === 'true');
const staticFile = await getStatic(decodeURIComponent(reqUrl.pathname), options.publicFolder);
const endpointExists = !staticFile &&
!isHiddenProxy &&
process.env.NETLIFY_DEV_SERVER_CHECK_SSG_ENDPOINTS &&
(await isEndpointExists(decodeURIComponent(reqUrl.pathname), options.target));
if (staticFile || endpointExists) {
const pathname = staticFile || reqUrl.pathname;
req.url = encodeURI(pathname) + reqUrl.search;
// if there is an existing static file and it is not a forced redirect, return the file
if (!match.force) {
return proxy.web(req, res, { ...options, staticFile });
}
}
if (match.force || !staticFile || !options.framework || req.method === 'POST') {
// construct destination URL from redirect rule match
const dest = new URL(match.to, `${reqUrl.protocol}//${reqUrl.host}`);
// We pass through request params if the redirect rule
// doesn't have any query params
if ([...dest.searchParams].length === 0) {
dest.searchParams.forEach((_, key) => {
dest.searchParams.delete(key);
});
const requestParams = new URLSearchParams(reqUrl.searchParams);
requestParams.forEach((val, key) => {
dest.searchParams.append(key, val);
});
}
let destURL = stripOrigin(dest);
if (isExternal(match)) {
if (isRedirect(match)) {
// This is a redirect, so we set the complete external URL as destination
destURL = `${dest}`;
}
else {
if (!isHiddenProxy) {
console.log(`${NETLIFYDEVLOG} Proxying to ${dest}`);
}
proxyToExternalUrl({ req, res, dest, destURL });
return;
}
}
if (isRedirect(match)) {
console.log(`${NETLIFYDEVLOG} Redirecting ${req.url} to ${destURL}`);
res.writeHead(match.status, {
Location: destURL,
'Cache-Control': 'no-cache',
});
res.end(`Redirecting to ${destURL}`);
return;
}
const ct = req.headers['content-type'] ? contentType.parse(req).type : '';
if (req.method === 'POST' &&
!isInternal(req.url) &&
!isInternal(destURL) &&
(ct.endsWith('/x-www-form-urlencoded') || ct === 'multipart/form-data')) {
return proxy.web(req, res, { target: options.functionsServer });
}
const destStaticFile = await getStatic(dest.pathname, options.publicFolder);
const matchingFunction = functionsRegistry &&
(await functionsRegistry.getFunctionForURLPath(destURL, req.method, () => Boolean(destStaticFile)));
let statusValue;
if (match.force ||
(!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL) || matchingFunction))) {
req.url = destStaticFile ? destStaticFile + dest.search : destURL;
const { status } = match;
statusValue = status;
console.log(`${NETLIFYDEVLOG} Rewrote URL to`, req.url);
}
if (matchingFunction) {
const functionHeaders = matchingFunction.func
? {
[NFFunctionName]: matchingFunction.func?.name,
[NFFunctionRoute]: matchingFunction.route,
}
: {};
const url = reqToURL(req, originalURL);
req.headers['x-netlify-original-pathname'] = url.pathname;
req.headers['x-netlify-original-search'] = url.search;
return proxy.web(req, res, { headers: functionHeaders, target: options.functionsServer });
}
if (isImageRequest(req)) {
return imageProxy(req, res);
}
const addonUrl = getAddonUrl(options.addonsUrls, req);
if (addonUrl) {
handleAddonUrl({ req, res, addonUrl });
return;
}
return proxy.web(req, res, { ...options, status: statusValue });
}
return proxy.web(req, res, options);
};
// @ts-expect-error TS(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
const reqToURL = function (req, pathname) {
return new URL(pathname, `${req.protocol || (req.headers.scheme && `${req.headers.scheme}:`) || 'http:'}//${req.headers.host || req.hostname}`);
};
const MILLISEC_TO_SEC = 1e3;
const initializeProxy = async function ({ config, configPath, distDir, env, host, imageProxy, port, projectDir, siteInfo, }) {
const proxy = httpProxy.createProxyServer({
selfHandleResponse: true,
target: {
host,
port,
},
});
const headersFiles = [...new Set([path.resolve(projectDir, '_headers'), path.resolve(distDir, '_headers')])];
let headers = await parseHeaders({ headersFiles, configPath, config });
const watchedHeadersFiles = configPath === undefined ? headersFiles : [...headersFiles, configPath];
onChanges(watchedHeadersFiles, async () => {
const existingHeadersFiles = await pFilter(watchedHeadersFiles, fileExistsAsync);
console.log(`${NETLIFYDEVLOG} Reloading headers files from`, existingHeadersFiles.map((headerFile) => path.relative(projectDir, headerFile)));
headers = await parseHeaders({ headersFiles, configPath, config });
});
// @ts-expect-error TS(2339) FIXME: Property 'before' does not exist on type 'Server'.
proxy.before('web', 'stream', (req) => {
// See https://github.com/http-party/node-http-proxy/issues/1219#issuecomment-511110375
if (req.headers.expect) {
req.__expectHeader = req.headers.expect;
delete req.headers.expect;
}
});
proxy.on('error', (err, req, res, proxyUrl) => {
// @ts-expect-error TS(2339) FIXME: Property 'proxyOptions' does not exist on type 'In... Remove this comment to see the full error message
const options = req.proxyOptions;
const isConRefused = 'code' in err && err.code === 'ECONNREFUSED';
if (options?.detectTarget && !(res instanceof Socket) && isConRefused && proxyUrl) {
// got econnrefused while detectTarget set to true -> try to switch between current ipVer and other (4 to 6 and vice versa)
// proxyUrl is parsed in http-proxy using url, parsing the same here. Difference between it and
// URL that hostname not includes [] symbols when using url.parse
// eslint-disable-next-line n/no-deprecated-api
const targetUrl = typeof proxyUrl === 'string' ? url.parse(proxyUrl) : proxyUrl;
const isCurrentHost = targetUrl.hostname === options.targetHostname;
if (targetUrl.hostname && isCurrentHost) {
const newHost = isIPv6(targetUrl.hostname) ? '127.0.0.1' : '::1';
options.target = `http://${isIPv6(newHost) ? `[${newHost}]` : newHost}:${targetUrl.port}`;
options.targetHostname = newHost;
options.isChangingTarget = true;
proxy.web(req, res, options);
return;
}
}
if (res instanceof http.ServerResponse) {
res.writeHead(500, {
'Content-Type': 'text/plain',
});
}
const message = isEdgeFunctionsRequest(req)
? 'There was an error with an Edge Function. Please check the terminal for more details.'
: 'Could not proxy request.';
res.end(message);
});
proxy.on('proxyReq', (proxyReq, req) => {
const requestID = generateRequestID();
proxyReq.setHeader(NFRequestID, requestID);
req.headers[NFRequestID] = requestID;
if (isEdgeFunctionsRequest(req)) {
handleProxyRequest(req, proxyReq);
}
// @ts-expect-error TS(2339) FIXME: Property '__expectHeader' does not exist on type '... Remove this comment to see the full error message
if (req.__expectHeader) {
// @ts-expect-error TS(2339) FIXME: Property '__expectHeader' does not exist on type '... Remove this comment to see the full error message
proxyReq.setHeader('Expect', req.__expectHeader);
}
// @ts-expect-error TS(2339) FIXME: Property 'originalBody' does not exist on type 'In... Remove this comment to see the full error message
if (req.originalBody) {
// @ts-expect-error TS(2339) FIXME: Property 'originalBody' does not exist on type 'In... Remove this comment to see the full error message
proxyReq.write(req.originalBody);
}
});
proxy.on('proxyRes', (proxyRes, req, res) => {
res.setHeader('server', 'Netlify');
const requestID = req.headers[NFRequestID];
if (requestID) {
res.setHeader(NFRequestID, requestID);
}
// @ts-expect-error TS(2339) FIXME: Property 'proxyOptions' does not exist on type 'In... Remove this comment to see the full error message
const options = req.proxyOptions;
if (options.isChangingTarget) {
// got a response after switching the ipVer for host (and its not an error since we will be in on('error') handler) - let's remember this host now
// options are not exported in ts for the proxy:
// @ts-expect-error TS(2339) FIXME: Property 'options' does not exist on type 'In...
proxy.options.target.host = options.targetHostname;
options.changeSettings?.({
frameworkHost: options.targetHostname,
detectFrameworkHost: false,
});
console.log(`${NETLIFYDEVLOG} Switched host to ${options.targetHostname}`);
}
if (proxyRes.statusCode === 404 || proxyRes.statusCode === 403) {
// If a request for `/path` has failed, we'll a few variations like
// `/path/index.html` to mimic the CDN behavior.
// @ts-expect-error TS(2339) FIXME: Property 'alternativePaths' does not exist on type... Remove this comment to see the full error message
if (req.alternativePaths && req.alternativePaths.length !== 0) {
// @ts-expect-error TS(2339) FIXME: Property 'alternativePaths' does not exist on type... Remove this comment to see the full error message
req.url = req.alternativePaths.shift();
// @ts-expect-error TS(2339) FIXME: Property 'proxyOptions' does not exist on type 'In... Remove this comment to see the full error message
proxy.web(req, res, req.proxyOptions);
return;
}
// The request has failed but we might still have a matching redirect
// rule (without `force`) that should kick in. This is how we mimic the
// file shadowing behavior from the CDN.
if (options && options.match) {
return serveRedirect({
// We don't want to match functions at this point because any redirects
// to functions will have already been processed, so we don't supply a
// functions registry to `serveRedirect`.
functionsRegistry: null,
req,
res,
proxy: handlers,
imageProxy,
match: options.match,
options,
siteInfo,
env,
});
}
}
if (options.staticFile && isRedirect({ status: proxyRes.statusCode }) && proxyRes.headers.location) {
req.url = proxyRes.headers.location;
return serveRedirect({
// We don't want to match functions at this point because any redirects
// to functions will have already been processed, so we don't supply a
// functions registry to `serveRedirect`.
functionsRegistry: null,
req,
res,
proxy: handlers,
imageProxy,
match: null,
options,
siteInfo,
env,
});
}
// @ts-expect-error TS(7034) FIXME: Variable 'responseData' implicitly has type 'any[]... Remove this comment to see the full error message
const responseData = [];
// @ts-expect-error TS(2345) FIXME: Argument of type 'string | undefined' is not assig... Remove this comment to see the full error message
const requestURL = new URL(req.url, `http://${req.headers.host || '127.0.0.1'}`);
const headersRules = headersForPath(headers, requestURL.pathname);
const configInjections = config.dev?.processing?.html?.injections ?? [];
const htmlInjections = configInjections.length > 0 && proxyRes.headers?.['content-type']?.startsWith('text/html')
? configInjections
: undefined;
// for streamed responses, we can't do etag generation nor error templates.
// we'll just stream them through!
// when html_injections are present in dev config, we can't use streamed response
const isStreamedResponse = proxyRes.headers['content-length'] === undefined;
if (isStreamedResponse && !htmlInjections) {
Object.entries(headersRules).forEach(([key, val]) => {
// @ts-expect-error TS(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message
res.setHeader(key, val);
});
res.writeHead(options.status || proxyRes.statusCode, proxyRes.headers);
proxyRes.on('data', function onData(data) {
res.write(data);
});
proxyRes.on('end', function onEnd() {
res.end();
});
return;
}
proxyRes.on('data', function onData(data) {
responseData.push(data);
});
proxyRes.on('end', async function onEnd() {
// @ts-expect-error TS(7005) FIXME: Variable 'responseData' implicitly has an 'any[]' ... Remove this comment to see the full error message
let responseBody = Buffer.concat(responseData);
let responseStatus = options.status || proxyRes.statusCode;
// `req[shouldGenerateETag]` may contain a function that determines
// whether the response should have an ETag header.
if (
// @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
typeof req[shouldGenerateETag] === 'function' &&
// @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
req[shouldGenerateETag]({ statusCode: responseStatus }) === true) {
const etag = generateETag(responseBody, { weak: true });
if (req.headers['if-none-match'] === etag) {
responseStatus = 304;
}
res.setHeader('etag', etag);
}
Object.entries(headersRules).forEach(([key, val]) => {
// @ts-expect-error TS(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message
res.setHeader(key, val);
});
const isUncaughtError = proxyRes.headers['x-nf-uncaught-error'] === '1';
if (isEdgeFunctionsRequest(req) && isUncaughtError) {
const acceptsHtml = req.headers.accept?.includes('text/html') ?? false;
const decompressedBody = await decompressResponseBody(responseBody, proxyRes.headers['content-encoding']);
const formattedBody = formatEdgeFunctionError(decompressedBody, acceptsHtml);
const errorResponse = acceptsHtml
? await renderErrorTemplate(formattedBody, '../../src/lib/templates/function-error.html', 'edge function')
: formattedBody;
const contentLength = Buffer.from(errorResponse, 'utf8').byteLength;
res.setHeader('content-length', contentLength);
res.statusCode = 500;
res.write(errorResponse);
return res.end();
}
let proxyResHeaders = proxyRes.headers;
if (htmlInjections) {
responseBody = await injectHtml(responseBody, proxyRes, htmlInjections);
proxyResHeaders = {
...proxyResHeaders,
'content-length': String(responseBody.byteLength),
};
delete proxyResHeaders['transfer-encoding'];
}
res.writeHead(responseStatus, proxyResHeaders);
if (responseStatus !== 304) {
res.write(responseBody);
}
res.end();
});
});
const handlers = {
// @ts-expect-error TS(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
web: (req, res, options) => {
const requestURL = new URL(req.url, 'http://127.0.0.1');
req.proxyOptions = options;
req.alternativePaths = alternativePathsFor(requestURL.pathname).map((filePath) => filePath + requestURL.search);
// Ref: https://nodejs.org/api/net.html#net_socket_remoteaddress
req.headers['x-forwarded-for'] = req.connection.remoteAddress || '';
proxy.web(req, res, options);
},
// @ts-expect-error TS(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
ws: (req, socket, head, options) => {
proxy.ws(req, socket, head, options);
},
};
return handlers;
};
const onRequest = async ({ addonsUrls, api, edgeFunctionsProxy, env, functionsRegistry, functionsServer, imageProxy, proxy, rewriter, settings, siteInfo, }, req, res) => {
req.originalBody =
req.method && ['GET', 'OPTIONS', 'HEAD'].includes(req.method) ? null : await createStreamPromise(req, BYTES_LIMIT);
if (isImageRequest(req)) {
return imageProxy(req, res);
}
const edgeFunctionsProxyURL = await edgeFunctionsProxy?.(req);
if (edgeFunctionsProxyURL !== undefined) {
return proxy.web(req, res, { target: edgeFunctionsProxyURL });
}
const functionMatch = functionsRegistry &&
(await functionsRegistry.getFunctionForURLPath(req.url, req.method, () => getStatic(decodeURIComponent(reqToURL(req, req.url).pathname), settings.dist ?? '')));
if (functionMatch) {
// Setting an internal header with the function name so that we don't
// have to match the URL again in the functions server.
const headers = {};
if (functionMatch.func) {
headers[NFFunctionName] = functionMatch.func.name;
}
if (functionMatch.route) {
headers[NFFunctionRoute] = functionMatch.route.pattern;
}
return proxy.web(req, res, { headers, target: functionsServer });
}
const addonUrl = getAddonUrl(addonsUrls, req);
if (addonUrl) {
handleAddonUrl({ req, res, addonUrl });
return;
}
const match = await rewriter(req);
const options = {
match,
addonsUrls,
target: `http://${settings.frameworkHost && isIPv6(settings.frameworkHost) ? `[${settings.frameworkHost}]` : settings.frameworkHost}:${settings.frameworkPort}`,
detectTarget: settings.detectFrameworkHost,
targetHostname: settings.frameworkHost,
publicFolder: settings.dist,
functionsServer,
functionsPort: settings.functionsPort,
jwtRolePath: settings.jwtRolePath,
framework: settings.framework,
changeSettings(newSettings) {
Object.assign(settings, newSettings);
},
};
if (match) {
// We don't want to generate an ETag for 3xx redirects.
// @ts-expect-error TS(7031) FIXME: Binding element 'statusCode' implicitly has an 'an... Remove this comment to see the full error message
req[shouldGenerateETag] = ({ statusCode }) => statusCode < 300 || statusCode >= 400;
return serveRedirect({ req, res, proxy, imageProxy, match, options, siteInfo, env, functionsRegistry });
}
// The request will be served by the framework server, which means we want to
// generate an ETag unless we're rendering an error page. The only way for
// us to know that is by looking at the status code
// @ts-expect-error TS(7031) FIXME: Binding element 'statusCode' implicitly has an 'an... Remove this comment to see the full error message
req[shouldGenerateETag] = ({ statusCode }) => statusCode >= 200 && statusCode < 300;
const hasFormSubmissionHandler = functionsRegistry && getFormHandler({ functionsRegistry, logWarning: false });
const ct = req.headers['content-type'] ? contentType.parse(req).type : '';
if (hasFormSubmissionHandler &&
functionsServer &&
req.method === 'POST' &&
!isInternal(req.url) &&
(ct.endsWith('/x-www-form-urlencoded') || ct === 'multipart/form-data')) {
return proxy.web(req, res, { target: functionsServer });
}
if (req.method === 'GET' && api && process.env.NETLIFY_DEV_SERVER_ID) {
notifyActivity(api, siteInfo.id, process.env.NETLIFY_DEV_SERVER_ID);
}
proxy.web(req, res, options);
};
export const getProxyUrl = function (settings) {
const scheme = settings.https ? 'https' : 'http';
return `${scheme}://localhost:${settings.port}`;
};
export const startProxy = async function ({ accountId, addonsUrls, api, blobsContext, command, config, configPath, debug, disableEdgeFunctions, env, functionsRegistry, geoCountry, geolocationMode, getUpdatedConfig, inspectSettings, offline, projectDir, repositoryRoot, settings, siteInfo, state, }) {
const secondaryServerPort = settings.https ? await getAvailablePort() : null;
const functionsServer = settings.functionsPort ? `http://127.0.0.1:${settings.functionsPort}` : null;
let edgeFunctionsProxy;
if (disableEdgeFunctions) {
log(NETLIFYDEVWARN, 'Edge functions are disabled. Run without the --internal-disable-edge-functions flag to enable them.');
}
else {
edgeFunctionsProxy = await initializeEdgeFunctionsProxy({
command,
blobsContext,
config,
configPath,
debug,
env,
geolocationMode,
geoCountry,
getUpdatedConfig,
inspectSettings,
mainPort: settings.port,
offline,
passthroughPort: secondaryServerPort || settings.port,
settings,
projectDir,
repositoryRoot,
siteInfo,
accountId,
state,
});
}
const imageProxy = initializeImageProxy({
config,
settings,
});
const proxy = await initializeProxy({
env,
host: settings.frameworkHost,
port: settings.frameworkPort,
distDir: settings.dist,
projectDir,
configPath,
siteInfo,
imageProxy,
config,
});
const rewriter = await createRewriter({
config,
configPath,
distDir: settings.dist,
geoCountry,
jwtSecret: settings.jwtSecret,
jwtRoleClaim: settings.jwtRolePath,
projectDir,
});
const onRequestWithOptions = onRequest.bind(undefined, {
proxy,
rewriter,
settings,
addonsUrls,
functionsRegistry,
functionsServer,
edgeFunctionsProxy,
imageProxy,
siteInfo,
env,
api,
});
const primaryServer = settings.https
? https.createServer({ cert: settings.https.cert, key: settings.https.key }, onRequestWithOptions)
: http.createServer(onRequestWithOptions);
const onUpgrade = async function onUpgrade(req, socket, head) {
const match = await rewriter(req);
if (match && !match.force404 && isExternal(match)) {
const reqUrl = reqToURL(req, req.url);
const dest = new URL(match.to, `${reqUrl.protocol}//${reqUrl.host}`);
const destURL = stripOrigin(dest);
proxy.ws(req, socket, head, { target: dest.origin, changeOrigin: true, pathRewrite: () => destURL });
return;
}
proxy.ws(req, socket, head, {});
};
primaryServer.on('upgrade', onUpgrade);
primaryServer.listen({ port: settings.port });
const eventQueue = [once(primaryServer, 'listening')];
// If we're running the main server on HTTPS, we need to start a secondary
// server on HTTP for receiving passthrough requests from edge functions.
// This lets us run the Deno server on HTTP and avoid the complications of
// Deno talking to Node on HTTPS with potentially untrusted certificates.
if (secondaryServerPort) {
const secondaryServer = http.createServer(onRequestWithOptions);
secondaryServer.on('upgrade', onUpgrade);
secondaryServer.listen({ port: secondaryServerPort });
eventQueue.push(once(secondaryServer, 'listening'));
}
await Promise.all(eventQueue);
return getProxyUrl(settings);
};
const BYTES_LIMIT = 30;
//# sourceMappingURL=proxy.js.map