123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677 |
- "use strict";
- const path = require("path");
- const mime = require("mime-types");
- const onFinishedStream = require("on-finished");
- const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
- const {
- setStatusCode,
- getStatusCode,
- getRequestHeader,
- getRequestMethod,
- getRequestURL,
- getResponseHeader,
- setResponseHeader,
- removeResponseHeader,
- getResponseHeaders,
- getHeadersSent,
- send,
- finish,
- pipe,
- createReadStreamOrReadFileSync,
- getOutgoing,
- initState,
- setState,
- getReadyReadableStreamState
- } = require("./utils/compatibleAPI");
- const ready = require("./utils/ready");
- const parseTokenList = require("./utils/parseTokenList");
- const memorize = require("./utils/memorize");
- const BYTES_RANGE_REGEXP = /^ *bytes/i;
- function getValueContentRangeHeader(type, size, range) {
- return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
- }
- function parseHttpDate(date) {
- const timestamp = date && Date.parse(date);
-
- return typeof timestamp === "number" ? timestamp : NaN;
- }
- const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/;
- function destroyStream(stream, suppress) {
- if (typeof stream.destroy === "function") {
- stream.destroy();
- }
- if (typeof stream.close === "function") {
-
- stream.on("open",
-
- function onOpenClose() {
-
- if (typeof this.fd === "number") {
-
- this.close();
- }
- });
- }
- if (typeof stream.addListener === "function" && suppress) {
- stream.removeAllListeners("error");
- stream.addListener("error", () => {});
- }
- }
- const statuses = {
- 400: "Bad Request",
- 403: "Forbidden",
- 404: "Not Found",
- 416: "Range Not Satisfiable",
- 500: "Internal Server Error"
- };
- const parseRangeHeaders = memorize(
- value => {
- const [len, rangeHeader] = value.split("|");
-
- return require("range-parser")(Number(len), rangeHeader, {
- combine: true
- });
- });
- const MAX_MAX_AGE = 31536000000;
- function wrapper(context) {
- return async function middleware(req, res, next) {
- const acceptedMethods = context.options.methods || ["GET", "HEAD"];
- initState(res);
- async function goNext() {
- if (!context.options.serverSideRender) {
- return next();
- }
- return new Promise(resolve => {
- ready(context, () => {
- setState(res, "webpack", {
- devMiddleware: context
- });
- resolve(next());
- }, req);
- });
- }
- const method = getRequestMethod(req);
- if (method && !acceptedMethods.includes(method)) {
- await goNext();
- return;
- }
-
- function sendError(status, options) {
-
- const escapeHtml = require("./utils/escapeHtml");
- const content = statuses[status] || String(status);
- let document = Buffer.from(`<!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>Error</title>
- </head>
- <body>
- <pre>${escapeHtml(content)}</pre>
- </body>
- </html>`, "utf-8");
-
- const headers = getResponseHeaders(res);
- for (let i = 0; i < headers.length; i++) {
- removeResponseHeader(res, headers[i]);
- }
- if (options && options.headers) {
- const keys = Object.keys(options.headers);
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i];
- const value = options.headers[key];
- if (typeof value !== "undefined") {
- setResponseHeader(res, key, value);
- }
- }
- }
-
- setStatusCode(res, status);
- setResponseHeader(res, "Content-Type", "text/html; charset=utf-8");
- setResponseHeader(res, "Content-Security-Policy", "default-src 'none'");
- setResponseHeader(res, "X-Content-Type-Options", "nosniff");
- let byteLength = Buffer.byteLength(document);
- if (options && options.modifyResponseData) {
- ({
- data: document,
- byteLength
- } =
- options.modifyResponseData(req, res, document, byteLength));
- }
- setResponseHeader(res, "Content-Length", byteLength);
- finish(res, document);
- }
-
- function errorHandler(error) {
- switch (error.code) {
- case "ENAMETOOLONG":
- case "ENOENT":
- case "ENOTDIR":
- sendError(404, {
- modifyResponseData: context.options.modifyResponseData
- });
- break;
- default:
- sendError(500, {
- modifyResponseData: context.options.modifyResponseData
- });
- break;
- }
- }
- function isConditionalGET() {
- return getRequestHeader(req, "if-match") || getRequestHeader(req, "if-unmodified-since") || getRequestHeader(req, "if-none-match") || getRequestHeader(req, "if-modified-since");
- }
- function isPreconditionFailure() {
-
- const ifMatch = getRequestHeader(req, "if-match");
-
-
-
-
-
- if (ifMatch) {
- const etag = getResponseHeader(res, "ETag");
- return !etag || ifMatch !== "*" && parseTokenList(ifMatch).every(match => match !== etag && match !== `W/${etag}` && `W/${match}` !== etag);
- }
-
- const ifUnmodifiedSince =
- getRequestHeader(req, "if-unmodified-since");
- if (ifUnmodifiedSince) {
- const unmodifiedSince = parseHttpDate(ifUnmodifiedSince);
-
-
- if (!isNaN(unmodifiedSince)) {
- const lastModified = parseHttpDate( getResponseHeader(res, "Last-Modified"));
- return isNaN(lastModified) || lastModified > unmodifiedSince;
- }
- }
- return false;
- }
-
- function isCachable() {
- const statusCode = getStatusCode(res);
- return statusCode >= 200 && statusCode < 300 || statusCode === 304 ||
-
- statusCode === 404;
- }
-
- function isFresh(resHeaders) {
-
-
- const cacheControl =
- getRequestHeader(req, "cache-control");
- if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
- return false;
- }
-
- const noneMatch =
- getRequestHeader(req, "if-none-match");
- const modifiedSince =
- getRequestHeader(req, "if-modified-since");
-
- if (!noneMatch && !modifiedSince) {
- return false;
- }
-
- if (noneMatch && noneMatch !== "*") {
- if (!resHeaders.etag) {
- return false;
- }
- const matches = parseTokenList(noneMatch);
- let etagStale = true;
- for (let i = 0; i < matches.length; i++) {
- const match = matches[i];
- if (match === resHeaders.etag || match === `W/${resHeaders.etag}` || `W/${match}` === resHeaders.etag) {
- etagStale = false;
- break;
- }
- }
- if (etagStale) {
- return false;
- }
- }
-
-
-
- if (noneMatch) {
- return true;
- }
-
- if (modifiedSince) {
- const lastModified = resHeaders["last-modified"];
-
-
-
- const modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince));
- if (modifiedStale) {
- return false;
- }
- }
- return true;
- }
- function isRangeFresh() {
- const ifRange =
- getRequestHeader(req, "if-range");
- if (!ifRange) {
- return true;
- }
-
- if (ifRange.indexOf('"') !== -1) {
- const etag =
- getResponseHeader(res, "ETag");
- if (!etag) {
- return true;
- }
- return Boolean(etag && ifRange.indexOf(etag) !== -1);
- }
-
- const lastModified =
- getResponseHeader(res, "Last-Modified");
- if (!lastModified) {
- return true;
- }
- return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
- }
-
- function getRangeHeader() {
- const range = getRequestHeader(req, "range");
- if (range && BYTES_RANGE_REGEXP.test(range)) {
- return range;
- }
-
- return undefined;
- }
-
- function getOffsetAndLenFromRange(range) {
- const offset = range.start;
- const len = range.end - range.start + 1;
- return [offset, len];
- }
-
- function calcStartAndEnd(offset, len) {
- const start = offset;
- const end = Math.max(offset, offset + len - 1);
- return [start, end];
- }
- async function processRequest() {
-
-
- const extra = {};
- const filename = getFilenameFromUrl(context, getRequestURL(req), extra);
- if (extra.errorCode) {
- if (extra.errorCode === 403) {
- context.logger.error(`Malicious path "${filename}".`);
- }
- sendError(extra.errorCode, {
- modifyResponseData: context.options.modifyResponseData
- });
- await goNext();
- return;
- }
- if (!filename) {
- await goNext();
- return;
- }
- if (getHeadersSent(res)) {
- await goNext();
- return;
- }
- const {
- size
- } = extra.stats;
- let len = size;
- let offset = 0;
-
- if (context.options.headers) {
- let {
- headers
- } = context.options;
- if (typeof headers === "function") {
- headers =
- headers(req, res, context);
- }
-
- const allHeaders = [];
- if (typeof headers !== "undefined") {
- if (!Array.isArray(headers)) {
-
- for (const name in headers) {
- allHeaders.push({
- key: name,
- value: headers[name]
- });
- }
- headers = allHeaders;
- }
- for (const {
- key,
- value
- } of headers) {
- setResponseHeader(res, key, value);
- }
- }
- }
- if (!getResponseHeader(res, "Accept-Ranges")) {
- setResponseHeader(res, "Accept-Ranges", "bytes");
- }
- if (!getResponseHeader(res, "Cache-Control")) {
-
- const cacheControl = context.options.cacheImmutable && extra.immutable ? {
- immutable: true
- } : context.options.cacheControl;
- if (cacheControl) {
- let cacheControlValue;
- if (typeof cacheControl === "boolean") {
- cacheControlValue = "public, max-age=31536000";
- } else if (typeof cacheControl === "number") {
- const maxAge = Math.floor(Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000);
- cacheControlValue = `public, max-age=${maxAge}`;
- } else if (typeof cacheControl === "string") {
- cacheControlValue = cacheControl;
- } else {
- const maxAge = cacheControl.maxAge ? Math.floor(Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) / 1000) : MAX_MAX_AGE / 1000;
- cacheControlValue = `public, max-age=${maxAge}`;
- if (cacheControl.immutable) {
- cacheControlValue += ", immutable";
- }
- }
- setResponseHeader(res, "Cache-Control", cacheControlValue);
- }
- }
- if (context.options.lastModified && !getResponseHeader(res, "Last-Modified")) {
- const modified =
- extra.stats.mtime.toUTCString();
- setResponseHeader(res, "Last-Modified", modified);
- }
-
- let start;
-
- let end;
-
- let bufferOrStream;
-
- let byteLength;
- const rangeHeader = getRangeHeader();
- if (context.options.etag && !getResponseHeader(res, "ETag")) {
-
- let value;
-
- if (context.options.etag === "weak") {
- value = extra.stats;
- } else {
- if (rangeHeader) {
- const parsedRanges =
- parseRangeHeaders(`${size}|${rangeHeader}`);
- if (parsedRanges !== -2 && parsedRanges !== -1 && parsedRanges.length === 1) {
- [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
- }
- }
- [start, end] = calcStartAndEnd(offset, len);
- try {
- const result = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end);
- value = result.bufferOrStream;
- ({
- bufferOrStream,
- byteLength
- } = result);
- } catch (error) {
- errorHandler( error);
- await goNext();
- return;
- }
- }
- if (value) {
-
- const result = await require("./utils/etag")(value);
-
- if (result.buffer) {
- bufferOrStream = result.buffer;
- }
- setResponseHeader(res, "ETag", result.hash);
- }
- }
- if (!getResponseHeader(res, "Content-Type") || getStatusCode(res) === 404) {
- removeResponseHeader(res, "Content-Type");
-
- const contentType = mime.contentType(path.extname(filename));
-
-
- if (contentType) {
- setResponseHeader(res, "Content-Type", contentType);
- } else if (context.options.mimeTypeDefault) {
- setResponseHeader(res, "Content-Type", context.options.mimeTypeDefault);
- }
- }
-
- if (isConditionalGET()) {
- if (isPreconditionFailure()) {
- sendError(412, {
- modifyResponseData: context.options.modifyResponseData
- });
- await goNext();
- return;
- }
- if (isCachable() && isFresh({
- etag: (
- getResponseHeader(res, "ETag")),
- "last-modified": (
- getResponseHeader(res, "Last-Modified"))
- })) {
- setStatusCode(res, 304);
-
- removeResponseHeader(res, "Content-Encoding");
- removeResponseHeader(res, "Content-Language");
- removeResponseHeader(res, "Content-Length");
- removeResponseHeader(res, "Content-Range");
- removeResponseHeader(res, "Content-Type");
- finish(res);
- await goNext();
- return;
- }
- }
- let isPartialContent = false;
- if (rangeHeader) {
- let parsedRanges =
- parseRangeHeaders(`${size}|${rangeHeader}`);
-
- if (!isRangeFresh()) {
- parsedRanges = [];
- }
- if (parsedRanges === -1) {
- context.logger.error("Unsatisfiable range for 'Range' header.");
- setResponseHeader(res, "Content-Range", getValueContentRangeHeader("bytes", size));
- sendError(416, {
- headers: {
- "Content-Range": getResponseHeader(res, "Content-Range")
- },
- modifyResponseData: context.options.modifyResponseData
- });
- await goNext();
- return;
- } else if (parsedRanges === -2) {
- context.logger.error("A malformed 'Range' header was provided. A regular response will be sent for this request.");
- } else if (parsedRanges.length > 1) {
- context.logger.error("A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request.");
- }
- if (parsedRanges !== -2 && parsedRanges.length === 1) {
-
- setStatusCode(res, 206);
- setResponseHeader(res, "Content-Range", getValueContentRangeHeader("bytes", size, parsedRanges[0]));
- isPartialContent = true;
- [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
- }
- }
-
- if (!bufferOrStream) {
- [start, end] = calcStartAndEnd(offset, len);
- try {
- ({
- bufferOrStream,
- byteLength
- } = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end));
- } catch (error) {
- errorHandler( error);
- await goNext();
- return;
- }
- }
- if (context.options.modifyResponseData) {
- ({
- data: bufferOrStream,
- byteLength
- } = context.options.modifyResponseData(req, res, bufferOrStream,
- byteLength));
- }
- setResponseHeader(res, "Content-Length",
- byteLength);
- if (method === "HEAD") {
- if (!isPartialContent) {
- setStatusCode(res, 200);
- }
- finish(res);
- await goNext();
- return;
- }
- if (!isPartialContent) {
- setStatusCode(res, 200);
- }
- const isPipeSupports = typeof ( bufferOrStream.pipe) === "function";
- if (!isPipeSupports) {
- send(res, bufferOrStream);
- await goNext();
- return;
- }
-
- const cleanup = () => {
- destroyStream( bufferOrStream, true);
- };
-
-
- bufferOrStream.on("error", error => {
-
- cleanup();
- errorHandler(error);
- goNext();
- }).on(getReadyReadableStreamState(res), () => {
- goNext();
- });
- pipe(res, bufferOrStream);
- const outgoing = getOutgoing(res);
- if (outgoing) {
-
- onFinishedStream(outgoing, cleanup);
- }
- }
- ready(context, processRequest, req);
- };
- }
- module.exports = wrapper;
|