diff --git a/lib/App/etag.js b/lib/App/etag.js new file mode 100644 index 0000000..613ef2f --- /dev/null +++ b/lib/App/etag.js @@ -0,0 +1,5 @@ +const crypto = require('crypto') + +module.exports = function(data) { + return crypto.createHash('sha1').update(data).digest('hex') +} \ No newline at end of file diff --git a/lib/App/onFileRequest.js b/lib/App/onFileRequest.js index 5785234..2f0b440 100644 --- a/lib/App/onFileRequest.js +++ b/lib/App/onFileRequest.js @@ -1,5 +1,4 @@ const fs = require('fs') -const xxhash = require('xxhash') const path = require('path') const getMIMEType = require('mime-types').lookup @@ -52,7 +51,7 @@ module.exports = function(request, response) { const headers = { 'Content-Length': stats.size, - 'ETag': xxhash.hash(Buffer.from(stats.mtime.toString()), 0), + 'ETag': this.etag(stats.mtime.toString()), 'Cache-Control': 'max-age=864000' } diff --git a/lib/App/routePage.js b/lib/App/routePage.js index ed4d5b6..af07d68 100644 --- a/lib/App/routePage.js +++ b/lib/App/routePage.js @@ -1,5 +1,6 @@ -const xxhash = require('xxhash') const zlib = require('zlib') +const crypto = require('crypto') +const NodeCache = require('node-cache') // This should be close to the MTU size of a TCP packet. // Regarding performance it makes no sense to compress smaller files. @@ -9,6 +10,15 @@ const zlib = require('zlib') // we're trying to optimize for performance, not bandwidth. const gzipThreshold = 1450 +// We cache the gzipped response using the ETag as a key +// for the in-memory cache. By default we save the cached +// responses only for 10 minutes to prevent using a lot of memory. +const gzipCache = new NodeCache({ + stdTTL: 600, + checkperiod: 610 +}) + +// GZip compression options const bestCompressionOptions = { level: zlib.Z_BEST_COMPRESSION } @@ -17,14 +27,19 @@ const fastCompressionOptions = { level: zlib.Z_BEST_SPEED } +// Boilerplate HTML content const mobileMetaTag = '' const manifestTag = '' const scriptTag = '' +// Hash a UTF-8 string using SHA-1 +function hash(code) { + return crypto.createHash('sha1').update(code, 'utf8').digest('hex') +} + // Generic respond functionality -const respond = function(code, headers, request, response) { - let codeBuffer = Buffer.from(code) - let etag = xxhash.hash(codeBuffer, 0).toString() +function respond(code, headers, request, response) { + let etag = hash(code) if(request.headers['if-none-match'] === etag) { response.writeHead(304) @@ -42,24 +57,36 @@ const respond = function(code, headers, request, response) { localHeaders.ETag = etag // Send response - if(!request.fromProxy && code.length >= gzipThreshold) { + if(code.length >= gzipThreshold) { localHeaders['Content-Encoding'] = 'gzip' + let gzippedCode = gzipCache.get(etag) + + if(gzippedCode !== undefined) { + localHeaders['Content-Length'] = gzippedCode.length + + response.writeHead(response.statusCode || 200, localHeaders) + response.end(gzippedCode) + return + } + zlib.gzip(code, fastCompressionOptions, function(error, gzippedCode) { localHeaders['Content-Length'] = gzippedCode.length response.writeHead(response.statusCode || 200, localHeaders) response.end(gzippedCode) + + gzipCache.set(etag, gzippedCode) }) } else { - localHeaders['Content-Length'] = Buffer.byteLength(code, 'utf8') + localHeaders['Content-Length'] = Buffer.byteLength(code, 'utf8') //codeBuffer.length response.writeHead(response.statusCode || 200, localHeaders) response.end(code) } } -const respondStatic = function(code, baseHeaders) { +function respondStatic(code, baseHeaders) { const headers = Object.assign({}, baseHeaders) headers['Cache-Control'] = 'no-cache' @@ -72,7 +99,7 @@ const respondStatic = function(code, baseHeaders) { const gzippedCode = zlib.gzipSync(code, bestCompressionOptions) headers['Content-Length'] = gzippedCode.length - headers.ETag = xxhash.hash(Buffer.from(gzippedCode), 0).toString() + headers.ETag = hash(gzippedCode) return function(request, response) { if(request.headers['if-none-match'] === headers.ETag) { @@ -89,7 +116,7 @@ const respondStatic = function(code, baseHeaders) { // Keep in mind that the client needs to uncompress and that takes time as well. // Therefore we send an uncompressed version. headers['Content-Length'] = Buffer.byteLength(code, 'utf8') - headers.ETag = xxhash.hash(Buffer.from(code), 0).toString() + headers.ETag = hash(code) return function(request, response) { if(request.headers['if-none-match'] === headers.ETag) { @@ -188,6 +215,9 @@ module.exports = function(page) { return } + // So we can avoid the bind calls + const app = this + // Routing if(page.controller) { if(page.template) { @@ -203,24 +233,24 @@ module.exports = function(page) { page.httpVerbs.forEach(method => { const next = page.controller[method].bind(page.controller) - this[method](page.url, (request, response) => { - renderLayout(request, layoutControllerParams => { - response.render = params => { - const code = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params))) + this[method](page.url, function requestHandler(request, response) { + renderLayout(request, function handleLayout(layoutControllerParams) { + response.render = function handlePage(params) { + const content = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params))) if(layoutControllerParams) { if(layoutData || request.globals) Object.assign(layoutControllerParams, request.globals, layoutData) - layoutControllerParams.content = code - layoutControllerParams.app = this + layoutControllerParams.content = content + layoutControllerParams.app = app layoutControllerParams.page = page respond(renderLayoutTemplate(layoutControllerParams), headers, request, response) } else { respond(renderLayoutTemplate(Object.assign({ - content: code, - app: this, + content, + app, page }, request.globals, @@ -242,13 +272,13 @@ module.exports = function(page) { page.httpVerbs.forEach(method => { const runPageController = page.controller[method].bind(page.controller) - this[method](page.url, (request, response) => { - response.render = params => { - const code = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params))) + this[method](page.url, function requestHandler(request, response) { + response.render = function handlePage(params) { + const content = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params))) const layoutParams = { - content: code, - app: this, + content, + app, page } @@ -280,8 +310,8 @@ module.exports = function(page) { const renderLayout = this.layout.controller.render.bind(this.layout.controller) const layoutData = this.layout.json - this.get(page.url, (request, response) => { - renderLayout(request, layoutControllerParams => { + this.get(page.url, function(request, response) { + renderLayout(request, function(layoutControllerParams) { if(!layoutControllerParams) layoutControllerParams = {} @@ -289,7 +319,7 @@ module.exports = function(page) { Object.assign(layoutControllerParams, request.globals, layoutData) layoutControllerParams.content = page.code - layoutControllerParams.app = this + layoutControllerParams.app = app layoutControllerParams.page = page respond(renderLayoutTemplate(layoutControllerParams), headers, request, response) diff --git a/lib/App/routeScripts.js b/lib/App/routeScripts.js index d2d7831..714b310 100644 --- a/lib/App/routeScripts.js +++ b/lib/App/routeScripts.js @@ -1,5 +1,4 @@ const compress = require('brotli').compress -const xxhash = require('xxhash') module.exports = function() { const combinedJS = '"use strict";' + (this.liveReload ? this.liveReload.script : '') + this.pluginScripts.join(';') + this.js.map(script => script.code).join(';') @@ -27,7 +26,7 @@ module.exports = function() { compressedScriptsHeaders['Content-Length'] = Buffer.byteLength(compressedScripts, 'utf8') } - compressedScriptsHeaders.ETag = xxhash.hash(Buffer.from(compressedScripts), 0) + compressedScriptsHeaders.ETag = this.etag(compressedScripts) // Route this.server.routes.GET['scripts.js'] = (request, response) => { diff --git a/lib/App/routeStyles.js b/lib/App/routeStyles.js index 5f293f6..da63447 100644 --- a/lib/App/routeStyles.js +++ b/lib/App/routeStyles.js @@ -1,5 +1,4 @@ const compress = require('brotli').compress -const xxhash = require('xxhash') module.exports = function() { this.combinedCSS = this.pluginStyles.join(' ') + this.css.map(style => style.code).join(' ') @@ -27,7 +26,7 @@ module.exports = function() { compressedStylesHeaders['Content-Length'] = Buffer.byteLength(compressedStyles, 'utf8') } - compressedStylesHeaders.ETag = xxhash.hash(Buffer.from(compressedStyles), 0) + compressedStylesHeaders.ETag = this.etag(compressedStyles) // Route this.server.routes.GET['styles.css'] = (request, response) => { diff --git a/lib/Server/run.js b/lib/Server/run.js index 516b92c..27d1776 100644 --- a/lib/Server/run.js +++ b/lib/Server/run.js @@ -25,19 +25,15 @@ let run = function(app) { this.redirectServer = require('http').createServer((request, response) => { // If there is a proxy sending our HTTP contents to an HTTPS client we allow // accessing the contents directly via HTTP without redirecting to HTTPS. - if(request.headers['x-forwarded-proto'] === 'https') { - request.fromProxy = true + if(request.headers['x-forwarded-proto'] === 'https') return this.onRequest(request, response) - } // In production mode we allow local proxy servers to access HTTP contents. if(app.production) { let remoteAddress = request.connection.remoteAddress - if(remoteAddress === '::ffff:127.0.0.1' || remoteAddress === '127.0.0.1') { - request.fromProxy = true + if(remoteAddress === '::ffff:127.0.0.1' || remoteAddress === '127.0.0.1') return this.onRequest(request, response) - } } // If there's no proxy (e.g. direct browser access to HTTP) we redirect diff --git a/package.json b/package.json index 528cf91..4355009 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aero", - "version": "2.0.3", + "version": "2.0.4", "description": "The fastest node.js framework.", "repository": "aerojs/aero", "homepage": "https://github.com/aerojs/aero", @@ -17,6 +17,7 @@ "markdown-it": "8.x", "mime-types": "2.x", "mkdirp": "0.x", + "node-cache": "4.x", "node-watch": "0.x", "pem": "1.x", "pug": "2.0.0-beta6", @@ -27,8 +28,7 @@ "stylus": ">=0.54.0", "uglify-js-harmony": ">=2.6", "uws": "0.x", - "ws": ">=1.1.1", - "xxhash": "0.x" + "ws": ">=1.1.1" }, "devDependencies": { "aero-ajax": "*", diff --git a/test/index.js b/test/index.js index 51ff323..318252b 100644 --- a/test/index.js +++ b/test/index.js @@ -79,7 +79,6 @@ require('strict-mode')(function () { assert(app.config.ports) assert(app.config.ports.http) assert(app.config.ports.https) - assert(app.config.ports.liveReload) assert(app.package) assert(app.package.name) diff --git a/yarn.lock b/yarn.lock index d5eafd9..f39e958 100644 --- a/yarn.lock +++ b/yarn.lock @@ -210,8 +210,8 @@ character-parser@^2.1.1: is-regex "^1.0.3" clean-css@^3.2.10, clean-css@^3.3.0: - version "3.4.20" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.20.tgz#c0d8963b5448e030f0bcd3ddd0dac4dfe3dea501" + version "3.4.21" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.21.tgz#2101d5dbd19d63dbc16a75ebd570e7c33948f65b" dependencies: commander "2.8.x" source-map "0.4.x" @@ -232,6 +232,10 @@ cliui@^3.0.3: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" +clone@1.0.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -816,6 +820,10 @@ load-class@1.x: dependencies: bluebird "*" +lodash@4.x: + version "4.17.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42" + log-driver@1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" @@ -886,9 +894,12 @@ multi-stage-sourcemap@0.2.1: dependencies: source-map "^0.1.34" -nan@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" +node-cache@4.x: + version "4.1.0" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-4.1.0.tgz#2a6a66460bf063781138206988237ec02c135157" + dependencies: + clone "1.0.x" + lodash "4.x" node-uuid@~1.4.7: version "1.4.7" @@ -1611,12 +1622,6 @@ xtend@^4.0.0, xtend@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" -xxhash@0.x: - version "0.2.4" - resolved "https://registry.yarnpkg.com/xxhash/-/xxhash-0.2.4.tgz#8b8a48162cfccc21b920fa500261187d40216c39" - dependencies: - nan "^2.4.0" - y18n@^3.2.0: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"