diff --git a/routes/handleStaticFile.js b/routes/handleStaticFile.js new file mode 100644 index 00000000..962cfa9c --- /dev/null +++ b/routes/handleStaticFile.js @@ -0,0 +1,112 @@ +'use strict'; + +const fs = require('fs') + , events = require('harken') + , zlib = require('zlib') + , brotli = require('iltorb') + , mime = require('mime') + + , getCompression = require('../utils').getCompression + + , not = require('../utils').not + , unmodifiedStatus = 304 + , succesStatus = 200 + + , compressionExtension = (type) => { + if (type === 'br') { + return 'brot'; + } else if (type === 'deflate') { + return 'def'; + } else { + return 'tgz'; + } + } + + , getCompressor = (type) => { + if (type === 'br') { + return brotli.compressStream(); + } else if (type === 'deflate') { + return zlib.createDeflate(); + } else { + return zlib.createGzip(); + } + } + + , streamFile = (compression, file, connection) => { + const extension = compressionExtension(compression) + , compressor = getCompressor(compression); + + fs.stat(`${file}.${extension}`, (err, exists) => { + if (!err && exists.isFile()) { + fs.createReadStream(`${file}.${extension}`).pipe(connection.res); + } else { + // no compressed file yet... + fs.createReadStream(file).pipe(compressor) + .pipe(connection.res); + + fs.createReadStream(file).pipe(compressor) + .pipe(fs.createWriteStream(`${file}.${extension}`)); + } + }); + }; + +module.exports = (file, connection, config) => { + const req = connection.req + , res = connection.res + , pathname = connection.path.pathname + , compression = getCompression(req.headers['accept-encoding'], config) + , expires = new Date().getTime() + , maxAge = config.maxAge; + + fs.stat(file, (err, exists) => { + if (!err && exists.isFile()) { + + events.required([ `etag:check:${file}`, `etag:get:${file}` ], (valid) => { + if (valid[0]) { // does the etag match? YES + res.statusCode = unmodifiedStatus; + return res.end(); + } + // No match... + res.setHeader('ETag', valid[1]); // the etag is item 2 in the array + + if (req.method.toLowerCase() === 'head') { + res.writeHead(succesStatus, { + 'Content-Type': mime.getType(pathname) + , 'Cache-Control': `maxage=${maxAge}` + , Expires: new Date(expires + maxAge).toUTCString() + , 'Content-Encoding': compression + }); + + res.end(); + } else if (not(compression === 'none')) { + // we have compression! + res.writeHead(succesStatus, { + 'Content-Type': mime.getType(pathname) + , 'Cache-Control': `maxage=${maxAge}` + , Expires: new Date(expires + maxAge).toUTCString() + , 'Content-Encoding': compression + }); + + streamFile(compression, file, connection); + events.emit('static:served', pathname); + } else { + // no compression carry on... + // return with the correct heders for the file type + res.writeHead(succesStatus, { + 'Content-Type': mime.getType(pathname) + , 'Cache-Control': `maxage=${maxAge}` + , Expires: new Date(expires + maxAge).toUTCString() + }); + fs.createReadStream(file).pipe(res); + events.emit('static:served', pathname); + } + }); + + events.emit('etag:check', { file: file, etag: req.headers['if-none-match'] }); + + } else { + events.emit('static:missing', pathname); + events.emit('error:404', connection); + } + }); +}; diff --git a/routes/router.js b/routes/router.js index 742d8c5f..def4524c 100644 --- a/routes/router.js +++ b/routes/router.js @@ -1,37 +1,40 @@ 'use strict'; const path = require('path') - , fs = require('fs') - , zlib = require('zlib') , events = require('harken') , mime = require('mime') - , brotli = require('iltorb') , routeStore = require('./routeStore') , matchSimpleRoute = require('./matchSimpleRoute') , isWildCardRoute = require('./isWildCardRoute') , parseWildCardRoute = require('./parseWildCardRoute') , setupStaticRoutes = require('./serverSetup') , setSecurityHeaders = require('../security') + , handleStaticFile = require('./handleStaticFile') - , not = require('../utils').not , send = require('../utils').send , setStatus = require('../utils').setStatus , parsePath = require('../utils').parsePath - , getCompression = require('../utils').getCompression , redirect = require('../utils').redirect , contains = require('../utils').contains , statsd = require('../utils/statsd') - , succesStatus = 200 - , unmodifiedStatus = 304; + , succesStatus = 200; module.exports = (routesJson, config) => { const publicPath = config.publicPath - , maxAge = config.maxAge , routePath = config.routePath , publicFolders = setupStaticRoutes(routePath, publicPath) - , statsdClient = config.statsd === false ? false : statsd.create(config.statsd); + , statsdClient = config.statsd === false ? false : statsd.create(config.statsd) + + , setupStatsdListeners = (res, sendStatsd, cleanup) => { + if (statsdClient) { + // Add response listeners + res.once('finish', sendStatsd); + res.once('error', cleanup); + res.once('close', cleanup); + } + }; routeStore.parse(routesJson); @@ -41,14 +44,13 @@ module.exports = (routesJson, config) => { , pathParsed = parsePath(req.url) , pathname = pathParsed.pathname , simpleRoute = matchSimpleRoute(pathname, method, routeStore.getStandard()) - , expires = new Date().getTime() , connection = { req: req , res: resIn , query: pathParsed.query , params: {} + , path: pathParsed } - , compression = getCompression(req.headers['accept-encoding'], config) , statsdStartTime = new Date().getTime() , cleanupStatsd = () => { @@ -86,17 +88,11 @@ module.exports = (routesJson, config) => { cleanupStatsd(); }; - let file - , routeInfo + let routeInfo , res = resIn; - // set up the statsd timing listeners - if (statsdClient) { - // Add response listeners - res.once('finish', sendStatsd); - res.once('error', cleanupStatsd); - res.once('close', cleanupStatsd); - } + // set up the statsd timing listeners + setupStatsdListeners(res, sendStatsd, cleanupStatsd); // add .setStatus to response res.setStatus = setStatus; @@ -104,7 +100,6 @@ module.exports = (routesJson, config) => { // add .send to the response res.send = send(req, config); res.redirect = redirect(req); - res = setSecurityHeaders(config, req, res); // match the first part of the url... for public stuff @@ -115,101 +110,8 @@ module.exports = (routesJson, config) => { // the accept-encoding header. So (gzip/deflate/no compression) res.setHeader('Vary', 'Accept-Encoding'); - file = path.join(publicPath, pathname); // read in the file and stream it to the client - fs.stat(file, (err, exists) => { - if (!err && exists.isFile()) { - - events.required([ `etag:check:${file}`, `etag:get:${file}` ], (valid) => { - if (valid[0]) { // does the etag match? YES - res.statusCode = unmodifiedStatus; - return res.end(); - } - // No match... - res.setHeader('ETag', valid[1]); // the etag is item 2 in the array - - if (req.method.toLowerCase() === 'head') { - res.writeHead(succesStatus, { - 'Content-Type': mime.getType(pathname) - , 'Cache-Control': `maxage=${maxAge}` - , Expires: new Date(expires + maxAge).toUTCString() - , 'Content-Encoding': compression - }); - - res.end(); - } else if (not(compression === 'none')) { - // we have compression! - res.writeHead(succesStatus, { - 'Content-Type': mime.getType(pathname) - , 'Cache-Control': `maxage=${maxAge}` - , Expires: new Date(expires + maxAge).toUTCString() - , 'Content-Encoding': compression - }); - - if (compression === 'deflate') { - fs.stat(`${file}.def`, (errDef, existsDef) => { - if (!errDef && existsDef.isFile()) { - fs.createReadStream(`${file}.def`).pipe(res); - } else { - // no compressed file yet... - fs.createReadStream(file).pipe(zlib.createDeflate()) - .pipe(res); - - fs.createReadStream(file).pipe(zlib.createDeflate()) - .pipe(fs.createWriteStream(`${file}.def`)); - } - }); - } else if (compression === 'br') { - // brotli compression handling - fs.stat(`${file}.brot`, (errBrotli, existsBrotli) => { - if (!errBrotli && existsBrotli.isFile()) { - fs.createReadStream(`${file}.brot`).pipe(res); - } else { - // no compressed file yet... - fs.createReadStream(file).pipe(brotli.compressStream()) - .pipe(res); - - fs.createReadStream(file).pipe(brotli.compressStream()) - .pipe(fs.createWriteStream(`${file}.brot`)); - } - }); - } else { - fs.stat(`${file}.tgz`, (errTgz, existsTgz) => { - if (!errTgz && existsTgz.isFile()) { - fs.createReadStream(`${file}.tgz`).pipe(res); - } else { - // no compressed file yet... - fs.createReadStream(file).pipe(zlib.createGzip()) - .pipe(res); - - fs.createReadStream(file).pipe(zlib.createGzip()) - .pipe(fs.createWriteStream(`${file}.tgz`)); - } - }); - } - - events.emit('static:served', pathname); - - } else { - // no compression carry on... - // return with the correct heders for the file type - res.writeHead(succesStatus, { - 'Content-Type': mime.getType(pathname) - , 'Cache-Control': `maxage=${maxAge}` - , Expires: new Date(expires + maxAge).toUTCString() - }); - fs.createReadStream(file).pipe(res); - events.emit('static:served', pathname); - } - }); - - events.emit('etag:check', { file: file, etag: req.headers['if-none-match'] }); - - } else { - events.emit('static:missing', pathname); - events.emit('error:404', connection); - } - }); + handleStaticFile(path.join(publicPath, pathname), connection, config); } else if (simpleRoute !== null) { // matches a route in the routes.json @@ -225,7 +127,6 @@ module.exports = (routesJson, config) => { } else if (isWildCardRoute(pathname, method, routeStore.getWildcard())) { // matches a route in the routes.json file that has params routeInfo = parseWildCardRoute(pathname, routeStore.getWildcard()); - connection.params = routeInfo.values; // emit the event for the url minus params and include the params diff --git a/test_stubs/deletes/static/main.js.brot b/test_stubs/deletes/static/main.js.brot index 7e5298ec..7a2159b0 100644 Binary files a/test_stubs/deletes/static/main.js.brot and b/test_stubs/deletes/static/main.js.brot differ diff --git a/test_stubs/deletes/static/main.js.def b/test_stubs/deletes/static/main.js.def index 828ee954..a00676c5 100644 --- a/test_stubs/deletes/static/main.js.def +++ b/test_stubs/deletes/static/main.js.def @@ -1,2 +1,2 @@ -xœ Â1 -€0 Ð=§ø[JçžFJ‹‰Ð¤“xw}¼´¼Ãcj‹T)gÄ¡ŽÿŽÓ1ôêD<–µÐÛÀ‚‡€ÙcMCÁ†Ré–JG. \ No newline at end of file +xœ­Ì1 +À Ð=§ø›†âìiŠ( &™JïÞ¢ðæ\Ô–T …R‚¢øìŠ.g#ŠÝg5¹&"ã&`5ó5‘±!z8r¡ðWôý* \ No newline at end of file diff --git a/test_stubs/deletes/static/main.js.tgz b/test_stubs/deletes/static/main.js.tgz index 02270527..530f6573 100644 Binary files a/test_stubs/deletes/static/main.js.tgz and b/test_stubs/deletes/static/main.js.tgz differ diff --git a/utils/getCompression.js b/utils/getCompression.js index 25e5f0ca..d44f5418 100644 --- a/utils/getCompression.js +++ b/utils/getCompression.js @@ -6,29 +6,37 @@ const isDefined = require('./tools').isDefined return isDefined(config.compress) && !config.compress; } - , supportsBrotli = (header) => { + , supportsBrotli = (header = '') => { return header.match(/\bbr\b/) || header.match(/\bbrotli\b/); } - , supportsGzip = (header) => { + , supportsGzip = (header = '') => { return header.match(/\bgzip\b/); } - , supportsDeflate = (header) => { + , supportsDeflate = (header = '') => { return header.match(/\bdeflate\b/); } - , getCompression = (header, config) => { - if (isUndefined(header) || dontCompress(config)) { - return 'none'; - } else if (supportsBrotli(header)) { + , supportsCompression = (header = '') => { + if (supportsBrotli(header)) { return 'br'; } else if (supportsGzip(header)) { return 'gzip'; } else if (supportsDeflate(header)) { return 'deflate'; - } else { + } + + return 'none'; + } + + , getCompression = (header, config) => { + const supported = header ? supportsCompression(header) : 'none'; + + if (isUndefined(header) || dontCompress(config) || supported === 'none') { return 'none'; + } else { + return supported; } }; diff --git a/utils/parser.js b/utils/parser.js index ed7d2e3e..0cda7ef7 100644 --- a/utils/parser.js +++ b/utils/parser.js @@ -42,16 +42,15 @@ const getRawBody = require('raw-body') connection.req.pipe(busboy); } + , getCharset = (contentType) => { + return typer.parse(contentType).parameters.charset || 'UTF-8'; + } + , parser = (connection, callback, scopeIn) => { // parse out the body const contentType = connection.req.headers['content-type'] ? connection.req.headers['content-type'].split(';')[0] : 'application/json' - , scope = scopeIn; - - let encoding = 'UTF-8'; - - if (isDefined(contentType)) { - encoding = typer.parse(contentType).parameters.charset || 'UTF-8'; - } + , scope = scopeIn + , encoding = isDefined(contentType) ? getCharset(contentType) : 'UTF-8'; if (contentType === 'multipart/form-data') { try {