Skip to content

Issue 1002 #1028

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 7, 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
36 changes: 18 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

192 changes: 145 additions & 47 deletions src/middleware.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import path from "path";

import mime from "mime-types";
import parseRange from "range-parser";

import getFilenameFromUrl from "./utils/getFilenameFromUrl";
import handleRangeHeaders from "./utils/handleRangeHeaders";
import {
getHeaderNames,
getHeaderFromRequest,
getHeaderFromResponse,
setHeaderForResponse,
setStatusCode,
send,
} from "./utils/compatibleAPI";
import ready from "./utils/ready";

function getValueContentRangeHeader(type, size, range) {
return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
}

function createHtmlDocument(title, body) {
return (
`${
"<!DOCTYPE html>\n" +
'<html lang="en">\n' +
"<head>\n" +
'<meta charset="utf-8">\n' +
"<title>"
}${title}</title>\n` +
`</head>\n` +
`<body>\n` +
`<pre>${body}</pre>\n` +
`</body>\n` +
`</html>\n`
);
}

const BYTES_RANGE_REGEXP = /^ *bytes/i;

export default function wrapper(context) {
return async function middleware(req, res, next) {
const acceptedMethods = context.options.methods || ["GET", "HEAD"];
Expand All @@ -16,6 +47,7 @@ export default function wrapper(context) {

if (!acceptedMethods.includes(req.method)) {
await goNext();

return;
}

Expand All @@ -42,80 +74,146 @@ export default function wrapper(context) {

async function processRequest() {
const filename = getFilenameFromUrl(context, req.url);
let { headers } = context.options;

if (typeof headers === "function") {
headers = headers(req, res, context);
}

let content;

if (!filename) {
await goNext();

return;
}

try {
content = context.outputFileSystem.readFileSync(filename);
} catch (_ignoreError) {
await goNext();
return;
let { headers } = context.options;

if (typeof headers === "function") {
headers = headers(req, res, context);
}

const contentTypeHeader = res.get
? res.get("Content-Type")
: res.getHeader("Content-Type");
if (headers) {
const names = Object.keys(headers);

for (const name of names) {
setHeaderForResponse(res, name, headers[name]);
}
}

if (!contentTypeHeader) {
if (!getHeaderFromResponse(res, "Content-Type")) {
// content-type name(like application/javascript; charset=utf-8) or false
const contentType = mime.contentType(path.extname(filename));

// Only set content-type header if media type is known
// https://tools.ietf.org/html/rfc7231#section-3.1.1.5
if (contentType) {
// Express API
if (res.set) {
res.set("Content-Type", contentType);
}
// Node.js API
else {
res.setHeader("Content-Type", contentType);
}
setHeaderForResponse(res, "Content-Type", contentType);
}
}

if (headers) {
const names = Object.keys(headers);
if (!getHeaderFromResponse(res, "Accept-Ranges")) {
setHeaderForResponse(res, "Accept-Ranges", "bytes");
}

for (const name of names) {
// Express API
if (res.set) {
res.set(name, headers[name]);
}
// Node.js API
else {
res.setHeader(name, headers[name]);
const rangeHeader = getHeaderFromRequest(req, "range");

let start;
let end;

if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) {
const size = await new Promise((resolve) => {
context.outputFileSystem.lstat(filename, (error, stats) => {
if (error) {
context.logger.error(error);

return;
}

resolve(stats.size);
});
});

const parsedRanges = parseRange(size, rangeHeader, {
combine: true,
});

if (parsedRanges === -1) {
const message = "Unsatisfiable range for 'Range' header.";

context.logger.error(message);

const existingHeaders = getHeaderNames(res);

for (let i = 0; i < existingHeaders.length; i++) {
res.removeHeader(existingHeaders[i]);
}

setStatusCode(res, 416);
setHeaderForResponse(
res,
"Content-Range",
getValueContentRangeHeader("bytes", size)
);
setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8");

const document = createHtmlDocument(416, `Error: ${message}`);
const byteLength = Buffer.byteLength(document);

setHeaderForResponse(
res,
"Content-Length",
Buffer.byteLength(document)
);

send(req, res, document, byteLength);

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."
);
}
}

// Buffer
content = handleRangeHeaders(context, content, req, res);
if (parsedRanges !== -2 && parsedRanges.length === 1) {
// Content-Range
setStatusCode(res, 206);
setHeaderForResponse(
res,
"Content-Range",
getValueContentRangeHeader("bytes", size, parsedRanges[0])
);

// Express API
if (res.send) {
res.send(content);
[{ start, end }] = parsedRanges;
}
}
// Node.js API
else {
res.setHeader("Content-Length", content.length);

if (req.method === "HEAD") {
res.end();
const isFsSupportsStream =
typeof context.outputFileSystem.createReadStream === "function";

let bufferOtStream;
let byteLength;

try {
if (
typeof start !== "undefined" &&
typeof end !== "undefined" &&
isFsSupportsStream
) {
bufferOtStream = context.outputFileSystem.createReadStream(filename, {
start,
end,
});
byteLength = end - start + 1;
} else {
res.end(content);
bufferOtStream = context.outputFileSystem.readFileSync(filename);
byteLength = Buffer.byteLength(bufferOtStream);
}
} catch (_ignoreError) {
await goNext();

return;
}

send(req, res, bufferOtStream, byteLength);
}
};
}
Loading