diff --git a/package-lock.json b/package-lock.json index e6e79c3..2fcd0da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "mime": "^4.1.0", "minimist": "^1.2.8", "morphdom": "^2.7.8", - "send": "^1.2.1", + "range-parser": "^1.2.1", "urlpattern-polyfill": "^10.1.0", "ws": "^8.20.0" }, @@ -856,15 +856,6 @@ } } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -977,15 +968,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fast-diff": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", @@ -1107,15 +1089,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1201,26 +1174,6 @@ "dev": true, "license": "ISC" }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -1278,12 +1231,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/irregular-plurals": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", @@ -1541,31 +1488,6 @@ "node": ">=16" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -1993,32 +1915,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -2035,12 +1931,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2327,15 +2217,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", diff --git a/package.json b/package.json index 5420a7e..0a6c97b 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "mime": "^4.1.0", "minimist": "^1.2.8", "morphdom": "^2.7.8", - "send": "^1.2.1", + "range-parser": "^1.2.1", "urlpattern-polyfill": "^10.1.0", "ws": "^8.20.0" }, diff --git a/server.js b/server.js index ac46c63..6c521c0 100644 --- a/server.js +++ b/server.js @@ -10,7 +10,7 @@ import "urlpattern-polyfill"; import finalhandler from "finalhandler"; import WebSocket, { WebSocketServer } from "ws"; import mime from "mime"; -import send from "send"; +import parseRange from "range-parser"; import chokidar from "chokidar"; import { TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; import debugUtil from "debug"; @@ -648,7 +648,20 @@ export default class EleventyDevServer { return `sha512-${crypto.createHash("sha512").update(data).digest("base64")}` } - // This runs at the end of the middleware chain + /** + * @param {String} type + * @param {number} size + * @param {number=} range + */ + #contentRange(type, size, range) { + return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size + } + + /** + * @param {import('node:http').IncomingMessage} req + * @param {import('node:http').OutgoingMessage} res + * This runs at the end of the middleware chain + */ eleventyProjectMiddleware(req, res) { // Known issue with `finalhandler` and HTTP/2: // UnsupportedWarning: Status message is not supported by HTTP/2 (RFC7540 8.1.2.4) @@ -678,10 +691,62 @@ export default class EleventyDevServer { if (match) { if (match.statusCode === 200 && match.filepath) { // Content-Range request, probably Safari trying to stream video - if (req.headers.range) { - return send(req, match.filepath).pipe(res); + // If the client includes an If-Range header, + // serve them the whole thing. We don't include + // last-modified or etags headers, so these + // requests are invalid. + if (req.headers.range && !req.headers['if-range']) { + return fs.stat(match.filepath, (err, stat) => { + if (err) { + res.statusCode = 404; + res.send('File not found'); + return; + } + + let len = stat.size; + let offset = 0; + + const ranges = parseRange(len, req.headers.range, { + combine: true + }) + + // Tell clients that they can send ranges. + res.setHeader('Accept-Ranges', 'bytes'); + res.setHeader('Cache-Control', 'public, max-age=0'); + + // unsatisfiable + if (ranges === -1) { + // 416 Requested Range Not Satisfiable + res.statusCode = 416; + res.setHeader('Content-Range', this.#contentRange('bytes', len)) + return res.end(); + } else if (ranges !== -2 && ranges.length === 1) { + // valid (syntactically invalid/multiple ranges are treated as a regular response) + // Content-Range + res.statusCode = 206; + res.setHeader('Content-Range', this.#contentRange('bytes', len, ranges[0])) + + // adjust for requested range + let start = ranges[0].start + len = ranges[0].end - ranges[0].start + 1 + let end = Math.max(offset, offset + len - 1) + res.setHeader('Content-Length', len) + if (req.method === 'HEAD') { + res.end() + return + } + const stream = fs.createReadStream(match.filepath, { + start, end + }); + stream.pipe(res); + const cleanup = () => { + stream.destroy(); + } + stream.on('error', cleanup); + stream.on('end', cleanup); + } + }); } - return this.renderFile(match.filepath, res); }