├── .gitignore ├── README.md ├── index.js ├── lib ├── gzip.js └── staticGzip.js ├── package.json └── test ├── fixtures ├── blank.gif ├── index.html ├── style.css └── sub │ └── index.html ├── gzip.test.js ├── helpers └── index.js └── staticGzip.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.gz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connect-gzip 2 | 3 | Gzip middleware for [Connect](http://senchalabs.github.com/connect/) on [Node.js](http://nodejs.org). Uses Unix [`gzip`](http://www.freebsd.org/cgi/man.cgi?query=gzip) command to perform compression of dynamic requests or static files. Originally based on implementation included with Connect before version 1.0. 4 | 5 | 6 | ## Installation 7 | 8 | Install via npm: 9 | 10 | $ npm install connect-gzip 11 | 12 | 13 | ## Usage 14 | 15 | ### gzip.gzip([options]) 16 | 17 | Include this middleware to dynamically gzip data sent via `res.write` or `res.end` based on the Content-Type header. 18 | 19 | var connect = require('connect'), 20 | gzip = require('connect-gzip'); 21 | 22 | connect.createServer( 23 | gzip.gzip(), 24 | function(req, res) { 25 | res.setHeader('Content-Type', 'text/html'); 26 | res.end('

Some gzipped HTML!

'); 27 | } 28 | ).listen(3000); 29 | 30 | 31 | // Only gzip css files: 32 | gzip.gzip({ matchType: /css/ }) 33 | 34 | // Use maximum compression: 35 | gzip.gzip({ flags: '--best' }) 36 | 37 | Options: 38 | 39 | - `matchType` - A regular expression tested against the Content-Type header to determine whether the response should be gzipped or not. The default value is `/text|javascript|json/`. 40 | - `bin` - Command executed to perform gzipping. Defaults to `'gzip'`. 41 | - `flags` - Command flags passed to the gzip binary. Nothing by default for dynamic gzipping, so gzip will typically default to a compression level of 6. 42 | 43 | 44 | ### gzip.staticGzip(root, [options]) 45 | 46 | Gzips files in a root directory, and then serves them using the default [`connect.static`](http://senchalabs.github.com/connect/middleware-static.html) middleware. Note that options get passed through as well, so the `maxAge` and other options supported by `connect.static` also work. 47 | 48 | If a file under the root path (such as an image) does not have an appropriate MIME type for compression, it will still be passed through to `connect.static` and served uncompressed. Thus, you can simply use `gzip.staticGzip` in place of `connect.static`. 49 | 50 | var connect = require('connect'), 51 | gzip = require('connect-gzip'); 52 | 53 | connect.createServer( 54 | gzip.staticGzip(__dirname + '/public') 55 | ).listen(3000); 56 | 57 | 58 | // Only gzip javascript files: 59 | gzip.staticGzip(__dirname + '/public', { matchType: /javascript/ }) 60 | 61 | // Set a maxAge in milliseconds for browsers to cache files 62 | var oneDay = 86400000; 63 | gzip.staticGzip(__dirname + '/public', { maxAge: oneDay }) 64 | 65 | Options: 66 | 67 | - `matchType` - A regular expression tested against the file MIME type to determine whether the response should be gzipped or not. As in `connect.static`, MIME types are determined based on file extensions using [node-mime](https://github.com/bentomas/node-mime). The default value is `/text|javascript|json/`. 68 | - `bin` - Command executed to perform gzipping. Defaults to `'gzip'`. 69 | - `flags` - Command flags passed to the gzip binary. Defaults to `'--best'` for staticGzip. 70 | 71 | 72 | ## Tests 73 | 74 | Run the tests with 75 | 76 | expresso test 77 | 78 | 79 | ## License 80 | 81 | (The MIT License) 82 | 83 | Copyright (c) 2011 Nate Smith <nate@nateps.com> 84 | 85 | Permission is hereby granted, free of charge, to any person obtaining 86 | a copy of this software and associated documentation files (the 87 | 'Software'), to deal in the Software without restriction, including 88 | without limitation the rights to use, copy, modify, merge, publish, 89 | distribute, sublicense, and/or sell copies of the Software, and to 90 | permit persons to whom the Software is furnished to do so, subject to 91 | the following conditions: 92 | 93 | The above copyright notice and this permission notice shall be 94 | included in all copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 97 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 98 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 99 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 100 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 101 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 102 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.gzip = require('./lib/gzip'); 2 | exports.staticGzip = require('./lib/staticGzip'); -------------------------------------------------------------------------------- /lib/gzip.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ext JS Connect 3 | * Copyright(c) 2010 Sencha Inc. 4 | * MIT Licensed 5 | */ 6 | 7 | var spawn = require('child_process').spawn; 8 | 9 | /** 10 | * Connect middleware providing gzip compression on the fly. By default, it 11 | * compresses requests with mime types that match the expression 12 | * /text|javascript|json/. 13 | * 14 | * Options: 15 | * 16 | * - `matchType` Regular expression matching mime types to be compressed 17 | * - `flags` String of flags passed to the binary. Nothing by default 18 | * - `bin` Binary executable defaulting to "gzip" 19 | * 20 | * @param {Object} options 21 | * @api public 22 | */ 23 | 24 | exports = module.exports = function gzip(options) { 25 | var options = options || {}, 26 | matchType = options.matchType || /text|javascript|json/, 27 | bin = options.bin || 'gzip', 28 | flags = options.flags || ''; 29 | 30 | if (!matchType.test) throw new Error('option matchType must be a regular expression'); 31 | 32 | flags = (flags) ? '-c ' + flags : '-c'; 33 | flags = flags.split(' '); 34 | 35 | return function gzip(req, res, next) { 36 | var writeHead = res.writeHead, 37 | defaults = {}; 38 | 39 | ['write', 'end'].forEach(function(name) { 40 | defaults[name] = res[name]; 41 | res[name] = function() { 42 | // Make sure headers are setup if they haven't been called yet 43 | if (res.writeHead !== writeHead) { 44 | res.writeHead(res.statusCode); 45 | } 46 | res[name].apply(this, arguments); 47 | }; 48 | }); 49 | 50 | res.writeHead = function(code) { 51 | var args = Array.prototype.slice.call(arguments, 0), 52 | write = defaults.write, 53 | end = defaults.end, 54 | headers, key, accept, type, encoding, gzip, ua; 55 | if (args.length > 1) { 56 | headers = args.pop(); 57 | for (key in headers) { 58 | res.setHeader(key, headers[key]); 59 | } 60 | } 61 | 62 | ua = req.headers['user-agent'] || ''; 63 | accept = req.headers['accept-encoding'] || ''; 64 | type = res.getHeader('content-type') || ''; 65 | encoding = res.getHeader('content-encoding'); 66 | 67 | if (req.method === 'HEAD' || code !== 200 || !~accept.indexOf('gzip') || 68 | !matchType.test(type) || encoding || 69 | (~ua.indexOf('MSIE 6') && !~ua.indexOf('SV1'))) { 70 | res.write = write; 71 | res.end = end; 72 | return finish(); 73 | } 74 | 75 | res.setHeader('Content-Encoding', 'gzip'); 76 | res.setHeader('Vary', 'Accept-Encoding'); 77 | res.removeHeader('Content-Length'); 78 | 79 | gzip = spawn(bin, flags); 80 | 81 | res.write = function(chunk, encoding) { 82 | gzip.stdin.write(chunk, encoding); 83 | }; 84 | 85 | res.end = function(chunk, encoding) { 86 | if (chunk) { 87 | res.write(chunk, encoding); 88 | } 89 | gzip.stdin.end(); 90 | }; 91 | 92 | gzip.stdout.addListener('data', function(chunk) { 93 | write.call(res, chunk); 94 | }); 95 | 96 | gzip.addListener('exit', function(code) { 97 | res.write = write; 98 | res.end = end; 99 | res.end(); 100 | }); 101 | 102 | finish(); 103 | 104 | function finish() { 105 | res.writeHead = writeHead; 106 | res.writeHead.apply(res, args); 107 | } 108 | }; 109 | 110 | next(); 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /lib/staticGzip.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ext JS Connect 3 | * Copyright(c) 2010 Sencha Inc. 4 | * MIT Licensed 5 | */ 6 | 7 | var fs = require('fs'), 8 | parse = require('url').parse, 9 | path = require('path'), 10 | mime = require('mime'), 11 | exec = require('child_process').exec, 12 | staticSend = require('connect').static.send; 13 | 14 | /** 15 | * staticGzip gzips statics and then serves them with the regular Connect 16 | * static provider. By default, it compresses files with mime types that 17 | * match the expression /text|javascript|json/. 18 | * 19 | * Options: 20 | * 21 | * - `matchType` Regular expression matching mime types to be compressed 22 | * - `flags` String of flags passed to the binary. Defaults to "--best" 23 | * - `bin` Binary executable defaulting to "gzip" 24 | * 25 | * @param {String} root 26 | * @param {Object} options 27 | * @api public 28 | */ 29 | 30 | exports = module.exports = function staticGzip(root, options) { 31 | var options = options || {}, 32 | matchType = options.matchType || /text|javascript|json/, 33 | bin = options.bin || 'gzip', 34 | flags = options.flags || '--best', 35 | rootLength; 36 | 37 | if (!root) throw new Error('staticGzip root must be set'); 38 | if (!matchType.test) throw new Error('option matchType must be a regular expression'); 39 | 40 | options.root = root; 41 | rootLength = root.length; 42 | 43 | return function(req, res, next) { 44 | var url, filename, type, acceptEncoding, ua; 45 | 46 | if (req.method !== 'GET') return next(); 47 | 48 | url = parse(req.url); 49 | filename = path.join(root, url.pathname); 50 | if ('/' == filename[filename.length - 1]) filename += 'index.html'; 51 | 52 | type = mime.lookup(filename); 53 | if (!matchType.test(type)) { 54 | return passToStatic(filename); 55 | } 56 | 57 | acceptEncoding = req.headers['accept-encoding'] || ''; 58 | if (!~acceptEncoding.indexOf('gzip')) { 59 | return passToStatic(filename); 60 | } 61 | 62 | ua = req.headers['user-agent'] || ''; 63 | if (~ua.indexOf('MSIE 6') && !~ua.indexOf('SV1')) { 64 | return passToStatic(filename); 65 | } 66 | 67 | // Potentially malicious path 68 | if (~filename.indexOf('..')) { 69 | return passToStatic(filename); 70 | } 71 | 72 | // Check for requested file 73 | fs.stat(filename, function(err, stat) { 74 | if (err || stat.isDirectory()) { 75 | return passToStatic(filename); 76 | } 77 | 78 | // Check for compressed file 79 | var base = path.basename(filename), 80 | dir = path.dirname(filename), 81 | gzipname = path.join(dir, base + '.' + Number(stat.mtime) + '.gz'); 82 | fs.stat(gzipname, function(err) { 83 | if (err && err.code === 'ENOENT') { 84 | // Remove any old gz files 85 | exec('rm ' + path.join(dir, base + '.*.gz'), function(err) { 86 | // Gzipped file doesn't exist, so make it then send 87 | gzip(bin, flags, filename, gzipname, function(err) { 88 | return sendGzip(); 89 | }); 90 | }); 91 | } else if (err) { 92 | return passToStatic(filename); 93 | } else { 94 | return sendGzip(); 95 | } 96 | }); 97 | 98 | function sendGzip() { 99 | var charset = mime.charsets.lookup(type), 100 | contentType = type + (charset ? '; charset=' + charset : ''); 101 | res.setHeader('Content-Type', contentType); 102 | res.setHeader('Content-Encoding', 'gzip'); 103 | res.setHeader('Vary', 'Accept-Encoding'); 104 | passToStatic(gzipname); 105 | } 106 | }); 107 | 108 | function passToStatic(name) { 109 | var o = Object.create(options); 110 | o.path = name.substr(rootLength); 111 | staticSend(req, res, next, o); 112 | } 113 | }; 114 | }; 115 | 116 | function gzip(bin, flags, src, dest, callback) { 117 | var cmd = bin + ' ' + flags + ' -c ' + src + ' > ' + dest; 118 | exec(cmd, function(err, stdout, stderr) { 119 | if (err) { 120 | console.error('\n' + err.stack); 121 | fs.unlink(dest); 122 | } 123 | callback(err); 124 | }); 125 | } 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-gzip", 3 | "description": "Gzip middleware for Connect. Based on implementation in Connect 0.5.9. Original source: https://github.com/senchalabs/connect/tree/c9a0c1e0e98451bb5fffb70c622b827a11bf4fc7", 4 | "version": "0.1.5", 5 | "author": "Nate Smith", 6 | "main": "./index.js", 7 | "dependencies": { 8 | "connect": ">=1 <2", 9 | "mime": ">=0.0.1" 10 | }, 11 | "engines": { 12 | "node": "*" 13 | }, 14 | "devDependencies": { 15 | "expresso": ">=0.9", 16 | "should": ">=0.3.1" 17 | } 18 | } -------------------------------------------------------------------------------- /test/fixtures/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nateps/connect-gzip/1a5a7b9461c9f68b8a60339a66a50c570b63a648/test/fixtures/blank.gif -------------------------------------------------------------------------------- /test/fixtures/index.html: -------------------------------------------------------------------------------- 1 |

Wahoo!

-------------------------------------------------------------------------------- /test/fixtures/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 12px; 3 | color: red; 4 | } -------------------------------------------------------------------------------- /test/fixtures/sub/index.html: -------------------------------------------------------------------------------- 1 |

Wahoo!

-------------------------------------------------------------------------------- /test/gzip.test.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect'), 2 | fs = require('fs'), 3 | helpers = require('./helpers'), 4 | testUncompressed = helpers.testUncompressed, 5 | testCompressed = helpers.testCompressed, 6 | gzip = require('../index'), 7 | 8 | fixturesPath = __dirname + '/fixtures', 9 | cssBody = fs.readFileSync(fixturesPath + '/style.css', 'utf8'), 10 | htmlBody = fs.readFileSync(fixturesPath + '/index.html', 'utf8'), 11 | cssPath = '/style.css', 12 | htmlPath = '/', 13 | matchCss = /text\/css/, 14 | matchHtml = /text\/html/; 15 | 16 | function server() { 17 | var args = Array.prototype.slice.call(arguments, 0), 18 | callback = args.pop(); 19 | args.push(function(req, res) { 20 | var headers = {}, 21 | body; 22 | if (req.url === cssPath) { 23 | headers['Content-Type'] = 'text/css; charset=utf-8'; 24 | body = cssBody; 25 | } else if (req.url === htmlPath) { 26 | headers['Content-Type'] = 'text/html; charset=utf-8'; 27 | body = htmlBody; 28 | } 29 | headers['Content-Length'] = body.length; 30 | callback(res, headers, body); 31 | }); 32 | return connect.createServer.apply(null, args); 33 | } 34 | 35 | function setHeaders(res, headers) { 36 | for (var key in headers) { 37 | res.setHeader(key, headers[key]); 38 | } 39 | } 40 | var setHeadersWriteHeadWrite = server(gzip.gzip(), function(res, headers, body) { 41 | setHeaders(res, headers); 42 | res.writeHead(200); 43 | res.write(body); 44 | res.end(); 45 | }); 46 | var setHeadersWriteHeadEnd = server(gzip.gzip(), function(res, headers, body) { 47 | setHeaders(res, headers); 48 | res.writeHead(200); 49 | res.end(body); 50 | }); 51 | var setHeadersWrite = server(gzip.gzip(), function(res, headers, body) { 52 | setHeaders(res, headers); 53 | res.write(body); 54 | res.end(); 55 | }); 56 | var setHeadersEnd = server(gzip.gzip(), function(res, headers, body) { 57 | setHeaders(res, headers); 58 | res.end(body); 59 | }); 60 | var writeHeadWrite = server(gzip.gzip(), function(res, headers, body) { 61 | res.writeHead(200, headers); 62 | res.write(body); 63 | res.end(); 64 | }); 65 | var writeHeadEnd = server(gzip.gzip(), function(res, headers, body) { 66 | res.writeHead(200, headers); 67 | res.end(body); 68 | }); 69 | var css = server(gzip.gzip({ matchType: /css/ }), function(res, headers, body) { 70 | res.writeHead(200, headers); 71 | res.end(body); 72 | }); 73 | var best = server(gzip.gzip({ flags: '--best' }), function(res, headers, body) { 74 | res.writeHead(200, headers); 75 | res.end(body); 76 | }); 77 | 78 | module.exports = { 79 | 'gzip test uncompressable: no Accept-Encoding': testUncompressed( 80 | css, cssPath, {}, cssBody, matchCss 81 | ), 82 | 'gzip test uncompressable: does not accept gzip': testUncompressed( 83 | css, cssPath, { 'Accept-Encoding': 'deflate' }, cssBody, matchCss 84 | ), 85 | 'gzip test uncompressable: unmatched mime type': testUncompressed( 86 | css, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 87 | ), 88 | 'gzip test compressable': testCompressed( 89 | css, cssPath, { 'Accept-Encoding': 'gzip' }, cssBody, matchCss 90 | ), 91 | 'gzip test compressable: multiple Accept-Encoding types': testCompressed( 92 | css, cssPath, { 'Accept-Encoding': 'deflate, gzip, sdch' }, cssBody, matchCss 93 | ), 94 | 'gzip test uncompressable: HEAD request': testUncompressed( 95 | css, cssPath, { 'Accept-Encoding': 'gzip' }, '', matchCss, 'HEAD' 96 | ), 97 | 98 | 'gzip test compressable: specify --best flag': testCompressed( 99 | best, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 100 | ), 101 | 102 | 'gzip test uncompressable: setHeaders, writeHead, write, end': testUncompressed( 103 | setHeadersWriteHeadWrite, htmlPath, {}, htmlBody, matchHtml 104 | ), 105 | 'gzip test compressable: setHeaders, writeHead, write, end': testCompressed( 106 | setHeadersWriteHeadWrite, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 107 | ), 108 | 'gzip test uncompressable: setHeaders, writeHead, end': testUncompressed( 109 | setHeadersWriteHeadEnd, htmlPath, {}, htmlBody, matchHtml 110 | ), 111 | 'gzip test compressable: setHeaders, writeHead, end': testCompressed( 112 | setHeadersWriteHeadEnd, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 113 | ), 114 | 115 | 'gzip test uncompressable: setHeaders, write, end': testUncompressed( 116 | setHeadersWrite, htmlPath, {}, htmlBody, matchHtml 117 | ), 118 | 'gzip test compressable: setHeaders, write, end': testCompressed( 119 | setHeadersWrite, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 120 | ), 121 | 'gzip test uncompressable: setHeaders, end': testUncompressed( 122 | setHeadersEnd, htmlPath, {}, htmlBody, matchHtml 123 | ), 124 | 'gzip test compressable: setHeaders, end': testCompressed( 125 | setHeadersEnd, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 126 | ), 127 | 128 | // See: http://sebduggan.com/posts/ie6-gzip-bug-solved-using-isapi-rewrite 129 | 'gzip test uncompressable: IE6 before XP SP2': testUncompressed( 130 | setHeadersEnd, htmlPath, { 'Accept-Encoding': 'gzip', 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)' }, htmlBody, matchHtml 131 | ), 132 | 'gzip test compressable: IE6 after XP SP2': testCompressed( 133 | setHeadersEnd, htmlPath, { 'Accept-Encoding': 'gzip', 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1' }, htmlBody, matchHtml 134 | ), 135 | 'gzip test compressable: IE7': testCompressed( 136 | setHeadersEnd, htmlPath, { 'Accept-Encoding': 'gzip', 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)' }, htmlBody, matchHtml 137 | ), 138 | 'gzip test compressable: Chrome': testCompressed( 139 | setHeadersEnd, htmlPath, { 'Accept-Encoding': 'gzip', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.186 Safari/535.1' }, htmlBody, matchHtml 140 | ), 141 | 142 | 'gzip test uncompressable: writeHead, write, end': testUncompressed( 143 | writeHeadWrite, htmlPath, {}, htmlBody, matchHtml 144 | ), 145 | 'gzip test compressable: writeHead, write, end': testCompressed( 146 | writeHeadWrite, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 147 | ), 148 | 'gzip test uncompressable: writeHead, end': testUncompressed( 149 | writeHeadEnd, htmlPath, {}, htmlBody, matchHtml 150 | ), 151 | 'gzip test compressable: writeHead, end': testCompressed( 152 | writeHeadEnd, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 153 | ), 154 | } 155 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | should = require('should'), 3 | spawn = require('child_process').spawn; 4 | 5 | function wrapTest(func, numCallbacks) { 6 | numCallbacks = numCallbacks || 1; 7 | return function(beforeExit) { 8 | var n = 0; 9 | function done() { n++; } 10 | func(done); 11 | beforeExit(function() { 12 | n.should.equal(numCallbacks); 13 | }); 14 | } 15 | } 16 | 17 | exports.testUncompressed = function(app, url, headers, resBody, resType, method) { 18 | return wrapTest(function(done) { 19 | assert.response(app, { 20 | url: url, 21 | method: method ? method : 'GET', 22 | headers: headers 23 | }, { 24 | status: 200, 25 | body: resBody, 26 | headers: { 'Content-Type': resType } 27 | }, function(res) { 28 | res.headers.should.not.have.property('content-encoding'); 29 | done(); 30 | } 31 | ); 32 | }); 33 | } 34 | 35 | exports.testCompressed = function(app, url, headers, resBody, resType, method) { 36 | return wrapTest(function(done) { 37 | assert.response(app, { 38 | url: url, 39 | method: method ? method : 'GET', 40 | headers: headers, 41 | encoding: 'binary' 42 | }, { 43 | status: 200, 44 | headers: { 45 | 'Content-Type': resType, 46 | 'Content-Encoding': 'gzip', 47 | 'Vary': 'Accept-Encoding' 48 | } 49 | }, function(res) { 50 | res.body.should.not.equal(resBody); 51 | gunzip(res.body, function(err, body) { 52 | body.should.equal(resBody); 53 | done(); 54 | }); 55 | } 56 | ); 57 | }); 58 | } 59 | 60 | exports.testRedirect = function(app, url, headers, location) { 61 | return wrapTest(function(done) { 62 | assert.response(app, { 63 | url: url, 64 | headers: headers 65 | }, { 66 | status: 301, 67 | headers: { 68 | 'Location': location 69 | } 70 | }, done 71 | ); 72 | }); 73 | } 74 | 75 | exports.testMaxAge = function(app, url, headers, maxAge) { 76 | return wrapTest(function(done) { 77 | assert.response(app, { 78 | url: url, 79 | headers: headers 80 | }, { 81 | status: 200, 82 | headers: { 83 | 'Cache-Control': 'public, max-age=' + Math.floor(maxAge / 1000) 84 | } 85 | }, done 86 | ); 87 | }); 88 | } 89 | 90 | function gunzip(data, callback) { 91 | var process = spawn('gunzip', ['-c']), 92 | out = '', 93 | err = ''; 94 | process.stdout.on('data', function(data) { 95 | out += data; 96 | }); 97 | process.stderr.on('data', function(data) { 98 | err += data; 99 | }); 100 | process.on('exit', function(code) { 101 | if (callback) callback(err, out); 102 | }); 103 | process.stdin.end(data, 'binary'); 104 | } 105 | -------------------------------------------------------------------------------- /test/staticGzip.test.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect'), 2 | fs = require('fs'), 3 | helpers = require('./helpers'), 4 | testUncompressed = helpers.testUncompressed, 5 | testCompressed = helpers.testCompressed, 6 | testRedirect = helpers.testRedirect, 7 | testMaxAge = helpers.testMaxAge, 8 | gzip = require('../index'), 9 | 10 | fixturesPath = __dirname + '/fixtures', 11 | cssBody = fs.readFileSync(fixturesPath + '/style.css', 'utf8'), 12 | htmlBody = fs.readFileSync(fixturesPath + '/index.html', 'utf8'), 13 | appBody = 'Non-static html', 14 | cssPath = '/style.css', 15 | gifPath = '/blank.gif', 16 | htmlPath = '/', 17 | matchCss = /text\/css/, 18 | matchHtml = /text\/html/, 19 | 20 | staticDefault = connect.createServer( 21 | gzip.staticGzip(fixturesPath) 22 | ), 23 | staticCss = connect.createServer( 24 | gzip.staticGzip(fixturesPath, { matchType: /css/ }), 25 | function(req, res) { 26 | if (req.url === '/app') { 27 | res.setHeader('Content-Type', 'text/html; charset=utf-8'); 28 | res.setHeader('Content-Length', appBody.length); 29 | res.end(appBody); 30 | } 31 | } 32 | ), 33 | staticMaxAge = connect.createServer( 34 | gzip.staticGzip(fixturesPath, { maxAge: 1234000 }) 35 | ); 36 | 37 | module.exports = { 38 | 'staticGzip test uncompressable: no Accept-Encoding': testUncompressed( 39 | staticCss, cssPath, {}, cssBody, matchCss 40 | ), 41 | 'staticGzip test uncompressable: does not accept gzip': testUncompressed( 42 | staticCss, cssPath, { 'Accept-Encoding': 'deflate' }, cssBody, matchCss 43 | ), 44 | 'staticGzip test uncompressable: unmatched mime type': testUncompressed( 45 | staticCss, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 46 | ), 47 | 'staticGzip test uncompressable: non-static request': testUncompressed( 48 | staticCss, '/app', { 'Accept-Encoding': 'gzip' }, appBody, matchHtml 49 | ), 50 | 'staticGzip test compressable': testCompressed( 51 | staticCss, cssPath, { 'Accept-Encoding': 'gzip' }, cssBody, matchCss 52 | ), 53 | 'staticGzip test compressable: multiple Accept-Encoding types': testCompressed( 54 | staticCss, cssPath, { 'Accept-Encoding': 'deflate, gzip, sdch' }, cssBody, matchCss 55 | ), 56 | 57 | 'staticGzip test uncompressable: default content types': testUncompressed( 58 | staticDefault, htmlPath, {}, htmlBody, matchHtml 59 | ), 60 | 'staticGzip test compressable: default content types': testCompressed( 61 | staticDefault, htmlPath, { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 62 | ), 63 | 64 | // See: http://sebduggan.com/posts/ie6-gzip-bug-solved-using-isapi-rewrite 65 | 'staticGzip test uncompressable: IE6 before XP SP2': testUncompressed( 66 | staticDefault, htmlPath, { 'Accept-Encoding': 'gzip', 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)' }, htmlBody, matchHtml 67 | ), 68 | 'staticGzip test compressable: IE6 after XP SP2': testCompressed( 69 | staticDefault, htmlPath, { 'Accept-Encoding': 'gzip', 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1' }, htmlBody, matchHtml 70 | ), 71 | 'staticGzip test compressable: IE7': testCompressed( 72 | staticDefault, htmlPath, { 'Accept-Encoding': 'gzip', 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)' }, htmlBody, matchHtml 73 | ), 74 | 'staticGzip test compressable: Chrome': testCompressed( 75 | staticDefault, htmlPath, { 'Accept-Encoding': 'gzip', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.186 Safari/535.1' }, htmlBody, matchHtml 76 | ), 77 | 78 | 'staticGzip test compressable: subdirectory': testCompressed( 79 | staticDefault, '/sub/', { 'Accept-Encoding': 'gzip' }, htmlBody, matchHtml 80 | ), 81 | 'staticGzip test compressable: subdirectory redirect': testRedirect( 82 | staticDefault, '/sub', { 'Accept-Encoding': 'gzip' }, '/sub/' 83 | ), 84 | 'staticGzip test compressable with Accept-Encoding: maxAge': testMaxAge( 85 | staticMaxAge, cssPath, {'Accept-Encoding': 'gzip'}, 1234000 86 | ), 87 | 'staticGzip test uncompressable with Accept-Encoding: maxAge': testMaxAge( 88 | staticMaxAge, gifPath, {'Accept-Encoding': 'gzip'}, 1234000 89 | ), 90 | 'staticGzip test compressable without Accept-Encoding: maxAge': testMaxAge( 91 | staticMaxAge, cssPath, {}, 1234000 92 | ), 93 | 'staticGzip test uncompressable without Accept-Encoding: maxAge': testMaxAge( 94 | staticMaxAge, gifPath, {}, 1234000 95 | ) 96 | } 97 | --------------------------------------------------------------------------------