You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
451 lines
8.9 KiB
451 lines
8.9 KiB
|
|
/*! |
|
* Connect - utils |
|
* Copyright(c) 2010 Sencha Inc. |
|
* Copyright(c) 2011 TJ Holowaychuk |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var crypto = require('crypto') |
|
, Path = require('path') |
|
, fs = require('fs'); |
|
|
|
/** |
|
* Flatten the given `arr`. |
|
* |
|
* @param {Array} arr |
|
* @return {Array} |
|
* @api private |
|
*/ |
|
|
|
exports.flatten = function(arr, ret){ |
|
var ret = ret || [] |
|
, len = arr.length; |
|
for (var i = 0; i < len; ++i) { |
|
if (Array.isArray(arr[i])) { |
|
exports.flatten(arr[i], ret); |
|
} else { |
|
ret.push(arr[i]); |
|
} |
|
} |
|
return ret; |
|
}; |
|
|
|
/** |
|
* Return md5 hash of the given string and optional encoding, |
|
* defaulting to hex. |
|
* |
|
* utils.md5('wahoo'); |
|
* // => "e493298061761236c96b02ea6aa8a2ad" |
|
* |
|
* @param {String} str |
|
* @param {String} encoding |
|
* @return {String} |
|
* @api public |
|
*/ |
|
|
|
exports.md5 = function(str, encoding){ |
|
return crypto |
|
.createHash('md5') |
|
.update(str) |
|
.digest(encoding || 'hex'); |
|
}; |
|
|
|
/** |
|
* Merge object b with object a. |
|
* |
|
* var a = { foo: 'bar' } |
|
* , b = { bar: 'baz' }; |
|
* |
|
* utils.merge(a, b); |
|
* // => { foo: 'bar', bar: 'baz' } |
|
* |
|
* @param {Object} a |
|
* @param {Object} b |
|
* @return {Object} |
|
* @api public |
|
*/ |
|
|
|
exports.merge = function(a, b){ |
|
if (a && b) { |
|
for (var key in b) { |
|
a[key] = b[key]; |
|
} |
|
} |
|
return a; |
|
}; |
|
|
|
/** |
|
* Escape the given string of `html`. |
|
* |
|
* @param {String} html |
|
* @return {String} |
|
* @api public |
|
*/ |
|
|
|
exports.escape = function(html){ |
|
return String(html) |
|
.replace(/&(?!\w+;)/g, '&') |
|
.replace(/</g, '<') |
|
.replace(/>/g, '>') |
|
.replace(/"/g, '"'); |
|
}; |
|
|
|
|
|
/** |
|
* Return a unique identifier with the given `len`. |
|
* |
|
* utils.uid(10); |
|
* // => "FDaS435D2z" |
|
* |
|
* @param {Number} len |
|
* @return {String} |
|
* @api public |
|
*/ |
|
|
|
exports.uid = function(len) { |
|
var buf = [] |
|
, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' |
|
, charlen = chars.length; |
|
|
|
for (var i = 0; i < len; ++i) { |
|
buf.push(chars[getRandomInt(0, charlen - 1)]); |
|
} |
|
|
|
return buf.join(''); |
|
}; |
|
|
|
/** |
|
* Parse the given cookie string into an object. |
|
* |
|
* @param {String} str |
|
* @return {Object} |
|
* @api public |
|
*/ |
|
|
|
exports.parseCookie = function(str){ |
|
var obj = {} |
|
, pairs = str.split(/[;,] */); |
|
for (var i = 0, len = pairs.length; i < len; ++i) { |
|
var pair = pairs[i] |
|
, eqlIndex = pair.indexOf('=') |
|
, key = pair.substr(0, eqlIndex).trim().toLowerCase() |
|
, val = pair.substr(++eqlIndex, pair.length).trim(); |
|
|
|
// quoted values |
|
if ('"' == val[0]) val = val.slice(1, -1); |
|
|
|
// only assign once |
|
if (undefined == obj[key]) { |
|
val = val.replace(/\+/g, ' '); |
|
try { |
|
obj[key] = decodeURIComponent(val); |
|
} catch (err) { |
|
if (err instanceof URIError) { |
|
obj[key] = val; |
|
} else { |
|
throw err; |
|
} |
|
} |
|
} |
|
} |
|
return obj; |
|
}; |
|
|
|
/** |
|
* Serialize the given object into a cookie string. |
|
* |
|
* utils.serializeCookie('name', 'tj', { httpOnly: true }) |
|
* // => "name=tj; httpOnly" |
|
* |
|
* @param {String} name |
|
* @param {String} val |
|
* @param {Object} obj |
|
* @return {String} |
|
* @api public |
|
*/ |
|
|
|
exports.serializeCookie = function(name, val, obj){ |
|
var pairs = [name + '=' + encodeURIComponent(val)] |
|
, obj = obj || {}; |
|
|
|
if (obj.domain) pairs.push('domain=' + obj.domain); |
|
if (obj.path) pairs.push('path=' + obj.path); |
|
if (obj.expires) pairs.push('expires=' + obj.expires.toUTCString()); |
|
if (obj.httpOnly) pairs.push('httpOnly'); |
|
if (obj.secure) pairs.push('secure'); |
|
|
|
return pairs.join('; '); |
|
}; |
|
|
|
/** |
|
* Pause `data` and `end` events on the given `obj`. |
|
* Middleware performing async tasks _should_ utilize |
|
* this utility (or similar), to re-emit data once |
|
* the async operation has completed, otherwise these |
|
* events may be lost. |
|
* |
|
* var pause = utils.pause(req); |
|
* fs.readFile(path, function(){ |
|
* next(); |
|
* pause.resume(); |
|
* }); |
|
* |
|
* @param {Object} obj |
|
* @return {Object} |
|
* @api public |
|
*/ |
|
|
|
exports.pause = function(obj){ |
|
var onData |
|
, onEnd |
|
, events = []; |
|
|
|
// buffer data |
|
obj.on('data', onData = function(data, encoding){ |
|
events.push(['data', data, encoding]); |
|
}); |
|
|
|
// buffer end |
|
obj.on('end', onEnd = function(data, encoding){ |
|
events.push(['end', data, encoding]); |
|
}); |
|
|
|
return { |
|
end: function(){ |
|
obj.removeListener('data', onData); |
|
obj.removeListener('end', onEnd); |
|
}, |
|
resume: function(){ |
|
this.end(); |
|
for (var i = 0, len = events.length; i < len; ++i) { |
|
obj.emit.apply(obj, events[i]); |
|
} |
|
} |
|
}; |
|
}; |
|
|
|
/** |
|
* Check `req` and `res` to see if it has been modified. |
|
* |
|
* @param {IncomingMessage} req |
|
* @param {ServerResponse} res |
|
* @return {Boolean} |
|
* @api public |
|
*/ |
|
|
|
exports.modified = function(req, res, headers) { |
|
var headers = headers || res._headers || {} |
|
, modifiedSince = req.headers['if-modified-since'] |
|
, lastModified = headers['last-modified'] |
|
, noneMatch = req.headers['if-none-match'] |
|
, etag = headers['etag']; |
|
|
|
if (noneMatch) noneMatch = noneMatch.split(/ *, */); |
|
|
|
// check If-None-Match |
|
if (noneMatch && etag && ~noneMatch.indexOf(etag)) { |
|
return false; |
|
} |
|
|
|
// check If-Modified-Since |
|
if (modifiedSince && lastModified) { |
|
modifiedSince = new Date(modifiedSince); |
|
lastModified = new Date(lastModified); |
|
// Ignore invalid dates |
|
if (!isNaN(modifiedSince.getTime())) { |
|
if (lastModified <= modifiedSince) return false; |
|
} |
|
} |
|
|
|
return true; |
|
}; |
|
|
|
/** |
|
* Strip `Content-*` headers from `res`. |
|
* |
|
* @param {ServerResponse} res |
|
* @api public |
|
*/ |
|
|
|
exports.removeContentHeaders = function(res){ |
|
Object.keys(res._headers).forEach(function(field){ |
|
if (0 == field.indexOf('content')) { |
|
res.removeHeader(field); |
|
} |
|
}); |
|
}; |
|
|
|
/** |
|
* Check if `req` is a conditional GET request. |
|
* |
|
* @param {IncomingMessage} req |
|
* @return {Boolean} |
|
* @api public |
|
*/ |
|
|
|
exports.conditionalGET = function(req) { |
|
return req.headers['if-modified-since'] |
|
|| req.headers['if-none-match']; |
|
}; |
|
|
|
/** |
|
* Respond with 403 "Forbidden". |
|
* |
|
* @param {ServerResponse} res |
|
* @api public |
|
*/ |
|
|
|
exports.forbidden = function(res) { |
|
var body = 'Forbidden'; |
|
res.setHeader('Content-Type', 'text/plain'); |
|
res.setHeader('Content-Length', body.length); |
|
res.statusCode = 403; |
|
res.end(body); |
|
}; |
|
|
|
/** |
|
* Respond with 401 "Unauthorized". |
|
* |
|
* @param {ServerResponse} res |
|
* @param {String} realm |
|
* @api public |
|
*/ |
|
|
|
exports.unauthorized = function(res, realm) { |
|
res.statusCode = 401; |
|
res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"'); |
|
res.end('Unauthorized'); |
|
}; |
|
|
|
/** |
|
* Respond with 400 "Bad Request". |
|
* |
|
* @param {ServerResponse} res |
|
* @api public |
|
*/ |
|
|
|
exports.badRequest = function(res) { |
|
res.statusCode = 400; |
|
res.end('Bad Request'); |
|
}; |
|
|
|
/** |
|
* Respond with 304 "Not Modified". |
|
* |
|
* @param {ServerResponse} res |
|
* @param {Object} headers |
|
* @api public |
|
*/ |
|
|
|
exports.notModified = function(res) { |
|
exports.removeContentHeaders(res); |
|
res.statusCode = 304; |
|
res.end(); |
|
}; |
|
|
|
/** |
|
* Return an ETag in the form of `"<size>-<mtime>"` |
|
* from the given `stat`. |
|
* |
|
* @param {Object} stat |
|
* @return {String} |
|
* @api public |
|
*/ |
|
|
|
exports.etag = function(stat) { |
|
return '"' + stat.size + '-' + Number(stat.mtime) + '"'; |
|
}; |
|
|
|
/** |
|
* Parse "Range" header `str` relative to the given file `size`. |
|
* |
|
* @param {Number} size |
|
* @param {String} str |
|
* @return {Array} |
|
* @api public |
|
*/ |
|
|
|
exports.parseRange = function(size, str){ |
|
var valid = true; |
|
var arr = str.substr(6).split(',').map(function(range){ |
|
var range = range.split('-') |
|
, start = parseInt(range[0], 10) |
|
, end = parseInt(range[1], 10); |
|
|
|
// -500 |
|
if (isNaN(start)) { |
|
start = size - end; |
|
end = size - 1; |
|
// 500- |
|
} else if (isNaN(end)) { |
|
end = size - 1; |
|
} |
|
|
|
// Invalid |
|
if (isNaN(start) || isNaN(end) || start > end) valid = false; |
|
|
|
return { start: start, end: end }; |
|
}); |
|
return valid ? arr : undefined; |
|
}; |
|
|
|
/** |
|
* Parse the given Cache-Control `str`. |
|
* |
|
* @param {String} str |
|
* @return {Object} |
|
* @api public |
|
*/ |
|
|
|
exports.parseCacheControl = function(str){ |
|
var directives = str.split(',') |
|
, obj = {}; |
|
|
|
for(var i = 0, len = directives.length; i < len; i++) { |
|
var parts = directives[i].split('=') |
|
, key = parts.shift().trim() |
|
, val = parseInt(parts.shift(), 10); |
|
|
|
obj[key] = isNaN(val) ? true : val; |
|
} |
|
|
|
return obj; |
|
}; |
|
|
|
|
|
/** |
|
* Convert array-like object to an `Array`. |
|
* |
|
* node-bench measured "16.5 times faster than Array.prototype.slice.call()" |
|
* |
|
* @param {Object} obj |
|
* @return {Array} |
|
* @api public |
|
*/ |
|
|
|
var toArray = exports.toArray = function(obj){ |
|
var len = obj.length |
|
, arr = new Array(len); |
|
for (var i = 0; i < len; ++i) { |
|
arr[i] = obj[i]; |
|
} |
|
return arr; |
|
}; |
|
|
|
/** |
|
* Retrun a random int, used by `utils.uid()` |
|
* |
|
* @param {Number} min |
|
* @param {Number} max |
|
* @return {Number} |
|
* @api private |
|
*/ |
|
|
|
function getRandomInt(min, max) { |
|
return Math.floor(Math.random() * (max - min + 1)) + min; |
|
}
|
|
|