From c980310d2de988d5f4e3c932eb77ebe83daa27b9 Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Sat, 7 Aug 2021 11:59:22 -0700 Subject: [PATCH] potentially serve static files with static headers Support setting additional headers when serving static files by reading from an associated metadata file. Reminiscent of a CERN-era feature. --- .markdownlint.json | 4 ++++ CHANGELOG.md | 23 +++++++++++++++++++++++ lib/common.js | 29 +++++++++++++++++++++++++++-- lib/dingus.js | 42 ++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 5 ++--- package.json | 2 +- test/lib/common.js | 23 ++++++++++++++++++++++- test/lib/dingus.js | 41 ++++++++++++++++++++++++++++++++++++++--- 8 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 .markdownlint.json create mode 100644 CHANGELOG.md diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..18e3c71 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": false, + "MD024": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4b796d1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +Releases and notable changes to this project are documented here. + +## [Unreleased] + +## [v1.1.0] - + +### Added + +- support setting additional static headers when serving static files + +## [v1.0.0] - 2021-08-01 + +### Added + +- Everything. + +--- + +[Unreleased]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=HEAD;hp=v1.1.0 +[v1.1.0]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=v1.1.0;hp=v1.0.0 +[v1.0.0]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=v1.0.0;hp=v0.0.0 \ No newline at end of file diff --git a/lib/common.js b/lib/common.js index 284427a..ff68e9d 100644 --- a/lib/common.js +++ b/lib/common.js @@ -227,6 +227,10 @@ const nullLogger = { debug: nop, }; +/** + * Populates any absent logger levels. + * @param {Object} logger + */ const ensureLoggerLevels = (logger = {}) => { for (const level in nullLogger) { if (! (level in logger)) { @@ -236,22 +240,43 @@ const ensureLoggerLevels = (logger = {}) => { return logger; }; +/** + * Merges folded header lines + * @param {String[]} lines + */ +const unfoldHeaderLines = (lines) => { + const foldedLineRE = /^(\t| +)(.*)$/; + if (lines) { + lines.reduceRight((_, line, idx) => { + const result = foldedLineRE.exec(line); + if (result && idx) { + const prevIdx = idx - 1; + const mergedLine = `${lines[prevIdx]} ${result[2]}`; + lines.splice(prevIdx, 2, mergedLine); + return mergedLine; + } + }, null); + } + return lines; +}; + module.exports = { + ensureLoggerLevels, fileScope, generateETag, get, handlerLogData, - isClientCached, httpStatusCodeClass, + isClientCached, mergeDeep, mergeEnum, nop, nullLogger, - ensureLoggerLevels, pick, requestId, requestLogData, responseLogData, setOptions, splitFirst, + unfoldHeaderLines, }; diff --git a/lib/dingus.js b/lib/dingus.js index c8e4909..1cbfd72 100644 --- a/lib/dingus.js +++ b/lib/dingus.js @@ -26,6 +26,7 @@ const defaultOptions = { proxyPrefix: '', strictAccept: true, selfBaseUrl: '', + staticMetadata: true, trustProxy: true, querystring, }; @@ -38,6 +39,7 @@ class Dingus { * @param {string} options.proxyPrefix leading part of url path to strip * @param {Boolean} options.strictAccept whether to error on unsupported Accept type * @param {string} options.selfBaseUrl for constructing links + * @param {Boolean} options.staticMetadata serve static headers with static files * @param {Boolean} options.trustProxy trust some header data to be provided by proxy * @param {Object} options.querystring alternate qs parser to use */ @@ -444,6 +446,41 @@ class Dingus { } + /** + * Potentially add additional headers from static file meta-file. + * @param {http.ServerResponse} res + * @param {string} directory + * @param {string} fileName - already normalized and filtered + */ + async _serveFileMetaHeaders(res, directory, fileName) { + const _scope = _fileScope('_serveFileMetaHeaders'); + this.logger.debug(_scope, 'called', { directory, fileName }); + + const metaPrefix = '.'; + const metaSuffix = '.meta'; + const metaFileName = `${metaPrefix}${fileName}${metaSuffix}`; + const metaFilePath = path.join(directory, metaFileName); + + const [stat, data] = await this._readFileInfo(metaFilePath); + if (!stat) { + return; + } + + const lineBreakRE = /\r\n|\n|\r/; + const lines = data.toString().split(lineBreakRE); + common.unfoldHeaderLines(lines); + + const headerParseRE = /^(?[^:]+): +(?.*)$/; + lines.forEach((line) => { + if (line) { + const result = headerParseRE.exec(line); + const { groups: header } = result; + res.setHeader(header.name, header.value); + } + }); + } + + /** * Serve a file from a directory, with rudimentary cache awareness. * This will also serve pre-encoded variations if available and requested. @@ -524,6 +561,10 @@ class Dingus { // We presume static files are relatively cacheable. res.setHeader(Enum.Header.CacheControl, 'public'); + if (this.staticMetadata) { + await this._serveFileMetaHeaders(res, directory, fileName); + } + this.logger.debug(_scope, 'serving file', { filePath, contentType }); res.end(data); } @@ -615,6 +656,7 @@ class Dingus { throw new ResponseError(Enum.ErrorResponse.BadRequest); } + /** * @param {http.ClientRequest} req * @param {http.ServerResponse} res diff --git a/package-lock.json b/package-lock.json index e32d0ce..3296492 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@squeep/api-dingus", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2827,8 +2827,7 @@ }, "y18n": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "resolved": "", "dev": true }, "yallist": { diff --git a/package.json b/package.json index 4202607..3b1199c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@squeep/api-dingus", - "version": "1.0.0", + "version": "1.1.0", "description": "A minimal API server framework", "main": "index.js", "scripts": { diff --git a/test/lib/common.js b/test/lib/common.js index b802003..a04bf61 100644 --- a/test/lib/common.js +++ b/test/lib/common.js @@ -258,7 +258,6 @@ describe('common', function () { }); }); // requestId - describe('ensureLoggerLevels', function () { it('adds missing levels', function () { const result = common.ensureLoggerLevels(); @@ -378,4 +377,26 @@ describe('common', function () { }); }); // mergeDeep + describe('unfoldHeaderLines', function () { + it('folds', function () { + const lines = [ + 'Normal-Header: some header data', + 'Folded-Header: more data', + ' second line of data', + ' third line of data', + ]; + const expected = [ + 'Normal-Header: some header data', + 'Folded-Header: more data second line of data third line of data', + ]; + const result = common.unfoldHeaderLines(lines); + assert.deepStrictEqual(result, expected); + }); + it('covers no input', function () { + const lines = undefined; + const result = common.unfoldHeaderLines(); + assert.deepStrictEqual(result, lines); + }); + }); // unfoldHeaderLines + }); diff --git a/test/lib/dingus.js b/test/lib/dingus.js index a1070a5..eeeaf3a 100644 --- a/test/lib/dingus.js +++ b/test/lib/dingus.js @@ -13,8 +13,10 @@ const Enum = require('../../lib/enum'); const noExpectedException = 'did not get expected exception'; describe('Dingus', function () { - const dingus = new Dingus(); - + let dingus; + beforeEach(function () { + dingus = new Dingus(); + }); afterEach(function () { sinon.restore(); }); @@ -224,7 +226,7 @@ describe('Dingus', function () { dingus.on('GET', '/', () => {}); assert(stubOn.called); }); - }); + }); // on describe('setEndBodyHandler', function () { let req, res, ctx, handler, origEnd, origWrite; @@ -591,6 +593,33 @@ describe('Dingus', function () { }); }); // _readFileInfo + describe('_serveFileMetaHeaders', function () { + let res, directory, fileName; + beforeEach(function () { + sinon.stub(dingus, '_readFileInfo'); + res = { + setHeader: sinon.stub(), + }; + directory = '/path'; + fileName = 'filename'; + }); + it('covers no meta file', async function() { + dingus._readFileInfo.resolves([null, null]); + await dingus._serveFileMetaHeaders(res, directory, fileName); + assert(!res.setHeader.called); + }); + it('adds extra headers', async function () { + dingus._readFileInfo.resolves([{}, Buffer.from(`Link: ; rel="relation" +X-Folded-Header: data + data under + the fold +Content-Type: image/sgi +`)]); + await dingus._serveFileMetaHeaders(res, directory, fileName); + assert(res.setHeader.called); + }); + }); // _serveFileMetaHeaders + describe('serveFile', function () { const path = require('path'); let ctx, req, res, directory, fileName, filestats; @@ -649,6 +678,12 @@ describe('Dingus', function () { assert(fs.promises.readFile.called); assert(!dingus.handlerNotFound.called); }); + it('covers no meta headers', async function () { + dingus.staticMetadata = false; + await dingus.serveFile(req, res, ctx, directory, fileName); + assert(fs.promises.readFile.called); + assert(!dingus.handlerNotFound.called); + }); it('does not serve dot-file', async function () { fileName = '.example'; await dingus.serveFile(req, res, ctx, directory, fileName); -- 2.43.2