Skip to content

Modernization and bugfixes #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 79 additions & 34 deletions lib/gzip.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
* MIT Licensed
*/

var spawn = require('child_process').spawn;

/**
* Connect middleware providing gzip compression on the fly. By default, it
* compresses requests with mime types that match the expression
Expand All @@ -14,28 +12,40 @@ var spawn = require('child_process').spawn;
* Options:
*
* - `matchType` Regular expression matching mime types to be compressed
* - `flags` String of flags passed to the binary. Nothing by default
* - `bin` Binary executable defaulting to "gzip"
* - gzip options chunkSize, windowBits, level, memLevel, strategy, flush
*
* @param {Object} options
* @api public
*/

var zlib = require('zlib');

function hasOwnProperty(o, key) {
return Object.prototype.hasOwnProperty.call(o, key);
}
function selectProperties(o, key1, key2) {
var object = {};
for (var i = 1, ii = arguments.length; i < ii; i++) {
var key = arguments[i];
if (hasOwnProperty(o, key)) {
object[key] = o[key];
}
}
return object;
}

exports = module.exports = function gzip(options) {
var options = options || {},
matchType = options.matchType || /text|javascript|json/,
bin = options.bin || 'gzip',
flags = options.flags || '';
gzipOptions = selectProperties(options,
'chunkSize', 'windowBits', 'level', 'memLevel', 'strategy', 'flush');

if (!matchType.test) throw new Error('option matchType must be a regular expression');

flags = (flags) ? '-c ' + flags : '-c';
flags = flags.split(' ');


return function gzip(req, res, next) {
var writeHead = res.writeHead,
defaults = {};

['write', 'end'].forEach(function(name) {
defaults[name] = res[name];
res[name] = function() {
Expand All @@ -46,7 +56,7 @@ exports = module.exports = function gzip(options) {
res[name].apply(this, arguments);
};
});

res.writeHead = function(code) {
var args = Array.prototype.slice.call(arguments, 0),
write = defaults.write,
Expand All @@ -58,53 +68,88 @@ exports = module.exports = function gzip(options) {
res.setHeader(key, headers[key]);
}
}

ua = req.headers['user-agent'] || '';
accept = req.headers['accept-encoding'] || '';
type = res.getHeader('content-type') || '';
encoding = res.getHeader('content-encoding');

if (req.method === 'HEAD' || code !== 200 || !~accept.indexOf('gzip') ||
!matchType.test(type) || encoding ||
(~ua.indexOf('MSIE 6') && !~ua.indexOf('SV1'))) {
res.write = write;
res.end = end;
return finish();
}

res.setHeader('Content-Encoding', 'gzip');
res.setHeader('Vary', 'Accept-Encoding');
var hasLength = res.get('Content-Length');
res.removeHeader('Content-Length');
gzip = spawn(bin, flags);

var gzip = zlib.createGzip(gzipOptions);

res.write = function(chunk, encoding) {
gzip.stdin.write(chunk, encoding);
gzip.write(chunk, encoding);
};

res.end = function(chunk, encoding) {
if (chunk) {
res.write(chunk, encoding);
}
gzip.stdin.end();
gzip.end();
};

gzip.stdout.addListener('data', function(chunk) {
write.call(res, chunk);
});
if(hasLength) {
// if length is defined, send the compressed content as whole
var chunks = [];
gzip.addListener('data', function(chunk) {
chunks.push(chunk);
});

gzip.addListener('exit', function(code) {
res.write = write;
res.end = end;
res.end();
});

finish();

function finish() {
res.writeHead = writeHead;
res.writeHead.apply(res, args);
gzip.addListener('end', function() {
res.write = write;
res.end = end;

var l = 0;
for(var i=0; i < chunks.length; i++) {
l += chunks[i].length;
}
res.setHeader('Content-Length', l);
finish();

for(var i=0; i < chunks.length; i++) {
res.write( chunks[i] );
}
res.end();
});

gzip.addListener('error', function(error) {
finish();
res.close();
});

} else {
gzip.addListener('data', function(chunk) {
write.call(res, chunk);
});

gzip.addListener('end', function() {
res.write = write;
res.end = end;
res.end();
});

gzip.addListener('error', function(error) {
res.close();
});

finish();
}
function finish() {
res.writeHead = writeHead;
res.writeHead.apply(res, args);
}
};

next();
Expand Down
92 changes: 50 additions & 42 deletions lib/staticGzip.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ var fs = require('fs'),
parse = require('url').parse,
path = require('path'),
mime = require('mime'),
exec = require('child_process').exec,
staticSend = require('connect').static.send;
zlib = require('zlib'),
send = require('send');

/**
* staticGzip gzips statics and then serves them with the regular Connect
Expand All @@ -19,8 +19,8 @@ var fs = require('fs'),
* Options:
*
* - `matchType` Regular expression matching mime types to be compressed
* - `flags` String of flags passed to the binary. Defaults to "--best"
* - `bin` Binary executable defaulting to "gzip"
* - `flags` DEPRECATED: String of flags passed to the binary. Nothing
* by default
*
* @param {String} root
* @param {Object} options
Expand All @@ -30,30 +30,28 @@ var fs = require('fs'),
exports = module.exports = function staticGzip(root, options) {
var options = options || {},
matchType = options.matchType || /text|javascript|json/,
bin = options.bin || 'gzip',
flags = options.flags || '--best',
rootLength;

if (!root) throw new Error('staticGzip root must be set');
if (!matchType.test) throw new Error('option matchType must be a regular expression');

options.root = root;
rootLength = root.length;

return function(req, res, next) {
var url, filename, type, acceptEncoding, ua;
if (req.method !== 'GET') return next();

if (req.method !== 'GET' && req.method !== 'HEAD' ) return next();

url = parse(req.url);
filename = path.join(root, url.pathname);
if ('/' == filename[filename.length - 1]) filename += 'index.html';

type = mime.lookup(filename);
if (!matchType.test(type)) {
return passToStatic(filename);
}

acceptEncoding = req.headers['accept-encoding'] || '';
if (!~acceptEncoding.indexOf('gzip')) {
return passToStatic(filename);
Expand All @@ -63,38 +61,55 @@ exports = module.exports = function staticGzip(root, options) {
if (~ua.indexOf('MSIE 6') && !~ua.indexOf('SV1')) {
return passToStatic(filename);
}

// Potentially malicious path
if (~filename.indexOf('..')) {
return passToStatic(filename);
}

// Check for requested file
fs.stat(filename, function(err, stat) {
if (err || stat.isDirectory()) {
return passToStatic(filename);
return next();
}

// Check for compressed file
var base = path.basename(filename),
dir = path.dirname(filename),
gzipname = path.join(dir, base + '.' + Number(stat.mtime) + '.gz');
fs.stat(gzipname, function(err) {
if (err && err.code === 'ENOENT') {
// Remove any old gz files
exec('rm ' + path.join(dir, base + '.*.gz'), function(err) {
// Gzipped file doesn't exist, so make it then send
gzip(bin, flags, filename, gzipname, function(err) {
return sendGzip();
});
});

var gzip = zlib.createGzip()
// Gzipped file doesn't exist, so make it and then send

// First write compressed data to a temporary file. This
// avoids race condition when several node instances are
// competing for the same file and other node instance
// would try to send half-done file.
var tmpname = gzipname + '.' + process.pid + '.tmp';
var outfile = fs.createWriteStream( tmpname );
var infile = fs.createReadStream( filename );

outfile.on('close', function() {
// compressed data has been written to the temporary
// file and file descriptor is closed.
fs.rename( tmpname, gzipname, function() {
// temporary file renamed to final file
return sendGzip();
});
});

// pipe compressed data to file
infile.pipe(gzip).pipe(outfile);

} else if (err) {
return passToStatic(filename);
return passToStatic(filename);
} else {
return sendGzip();
return sendGzip();
}
});

function sendGzip() {
var charset = mime.charsets.lookup(type),
contentType = type + (charset ? '; charset=' + charset : '');
Expand All @@ -104,22 +119,15 @@ exports = module.exports = function staticGzip(root, options) {
passToStatic(gzipname);
}
});


// send file
function passToStatic(name) {
var o = Object.create(options);
o.path = name.substr(rootLength);
staticSend(req, res, next, o);
send(req, name.substr(rootLength))
.root(options.root)
.maxage(options.maxAge || 0)
// in case of error just pass through
.on('error', function() { next(); })
.pipe(res);
}
};
};

function gzip(bin, flags, src, dest, callback) {
var cmd = bin + ' ' + flags + ' -c ' + src + ' > ' + dest;
exec(cmd, function(err, stdout, stderr) {
if (err) {
console.error('\n' + err.stack);
fs.unlink(dest);
}
callback(err);
});
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"name": "connect-gzip",
"description": "Gzip middleware for Connect. Based on implementation in Connect 0.5.9. Original source: https://github.com/senchalabs/connect/tree/c9a0c1e0e98451bb5fffb70c622b827a11bf4fc7",
"version": "0.1.5",
"version": "0.1.5-f6",
"author": "Nate Smith",
"main": "./index.js",
"dependencies": {
"connect": ">=1 <2",
"send": ">=0.1",
"mime": ">=0.0.1"
},
"engines": {
"node": "*"
"node": ">=0.6.0"
},
"devDependencies": {
"expresso": ">=0.9",
"should": ">=0.3.1"
}
}
}