UNPKG

9.78 kBJavaScriptView Raw
1
2/*!
3 * Connect - utils
4 * Copyright(c) 2010 Sencha Inc.
5 * Copyright(c) 2011 TJ Holowaychuk
6 * MIT Licensed
7 */
8
9/**
10 * Module dependencies.
11 */
12
13var http = require('http')
14 , crypto = require('crypto')
15 , crc16 = require('crc').crc16
16 , Path = require('path')
17 , fs = require('fs');
18
19/**
20 * Extract the mime type from the given request's
21 * _Content-Type_ header.
22 *
23 * @param {IncomingMessage} req
24 * @return {String}
25 * @api private
26 */
27
28exports.mime = function(req) {
29 var str = req.headers['content-type'] || '';
30 return str.split(';')[0];
31};
32
33/**
34 * Generate an `Error` from the given status `code`.
35 *
36 * @param {Number} code
37 * @return {Error}
38 * @api private
39 */
40
41exports.error = function(code){
42 var err = new Error(http.STATUS_CODES[code]);
43 err.status = code;
44 return err;
45};
46
47/**
48 * Return md5 hash of the given string and optional encoding,
49 * defaulting to hex.
50 *
51 * utils.md5('wahoo');
52 * // => "e493298061761236c96b02ea6aa8a2ad"
53 *
54 * @param {String} str
55 * @param {String} encoding
56 * @return {String}
57 * @api public
58 */
59
60exports.md5 = function(str, encoding){
61 return crypto
62 .createHash('md5')
63 .update(str)
64 .digest(encoding || 'hex');
65};
66
67/**
68 * Merge object b with object a.
69 *
70 * var a = { foo: 'bar' }
71 * , b = { bar: 'baz' };
72 *
73 * utils.merge(a, b);
74 * // => { foo: 'bar', bar: 'baz' }
75 *
76 * @param {Object} a
77 * @param {Object} b
78 * @return {Object}
79 * @api private
80 */
81
82exports.merge = function(a, b){
83 if (a && b) {
84 for (var key in b) {
85 a[key] = b[key];
86 }
87 }
88 return a;
89};
90
91/**
92 * Escape the given string of `html`.
93 *
94 * @param {String} html
95 * @return {String}
96 * @api private
97 */
98
99exports.escape = function(html){
100 return String(html)
101 .replace(/&(?!\w+;)/g, '&')
102 .replace(/</g, '&lt;')
103 .replace(/>/g, '&gt;')
104 .replace(/"/g, '&quot;');
105};
106
107
108/**
109 * Return a unique identifier with the given `len`.
110 *
111 * utils.uid(10);
112 * // => "FDaS435D2z"
113 *
114 * @param {Number} len
115 * @return {String}
116 * @api private
117 */
118
119exports.uid = function(len) {
120 return crypto.randomBytes(Math.ceil(len * 3 / 4))
121 .toString('base64')
122 .slice(0, len);
123};
124
125/**
126 * Sign the given `val` with `secret`.
127 *
128 * @param {String} val
129 * @param {String} secret
130 * @return {String}
131 * @api private
132 */
133
134exports.sign = function(val, secret){
135 return val + '.' + crypto
136 .createHmac('sha256', secret)
137 .update(val)
138 .digest('base64')
139 .replace(/=+$/, '');
140};
141
142/**
143 * Unsign and decode the given `val` with `secret`,
144 * returning `false` if the signature is invalid.
145 *
146 * @param {String} val
147 * @param {String} secret
148 * @return {String|Boolean}
149 * @api private
150 */
151
152exports.unsign = function(val, secret){
153 var str = val.slice(0,val.lastIndexOf('.'));
154 return exports.sign(str, secret) == val
155 ? str
156 : false;
157};
158
159/**
160 * Parse signed cookies, returning an object
161 * containing the decoded key/value pairs,
162 * while removing the signed key from `obj`.
163 *
164 * @param {Object} obj
165 * @return {Object}
166 * @api private
167 */
168
169exports.parseSignedCookies = function(obj, secret){
170 var ret = {};
171 Object.keys(obj).forEach(function(key){
172 var val = obj[key]
173 , signed = exports.unsign(val, secret);
174
175 if (signed) {
176 ret[key] = signed;
177 delete obj[key];
178 }
179 });
180 return ret;
181};
182
183/**
184 * Parse JSON cookies.
185 *
186 * @param {Object} obj
187 * @return {Object}
188 * @api private
189 */
190
191exports.parseJSONCookies = function(obj){
192 var hashes = {};
193
194 Object.keys(obj).forEach(function(key){
195 var val = obj[key];
196 if (0 == val.indexOf('j:')) {
197 try {
198 hashes[key] = crc16(val); // only crc json cookies for now
199 obj[key] = JSON.parse(val.slice(2));
200 } catch (err) {
201 // nothing
202 }
203 }
204 });
205
206 return {
207 cookies: obj,
208 hashes: hashes
209 };
210};
211
212/**
213 * Parse the given cookie string into an object.
214 *
215 * @param {String} str
216 * @return {Object}
217 * @api private
218 */
219
220exports.parseCookie = function(str){
221 var obj = {}
222 , pairs = str.split(/[;,] */);
223 for (var i = 0, len = pairs.length; i < len; ++i) {
224 var pair = pairs[i]
225 , eqlIndex = pair.indexOf('=')
226 , key = pair.substr(0, eqlIndex).trim()
227 , val = pair.substr(++eqlIndex, pair.length).trim();
228
229 // quoted values
230 if ('"' == val[0]) val = val.slice(1, -1);
231
232 // only assign once
233 if (undefined == obj[key]) {
234 val = val.replace(/\+/g, ' ');
235 try {
236 obj[key] = decodeURIComponent(val);
237 } catch (err) {
238 if (err instanceof URIError) {
239 obj[key] = val;
240 } else {
241 throw err;
242 }
243 }
244 }
245 }
246 return obj;
247};
248
249/**
250 * Serialize the given object into a cookie string.
251 *
252 * utils.serializeCookie('name', 'tj', { httpOnly: true })
253 * // => "name=tj; httpOnly"
254 *
255 * @param {String} name
256 * @param {String} val
257 * @param {Object} obj
258 * @return {String}
259 * @api private
260 */
261
262exports.serializeCookie = function(name, val, obj){
263 var pairs = [name + '=' + encodeURIComponent(val)]
264 , obj = obj || {};
265
266 if (obj.domain) pairs.push('domain=' + obj.domain);
267 if (obj.path) pairs.push('path=' + obj.path);
268 if (obj.expires) pairs.push('expires=' + obj.expires.toUTCString());
269 if (obj.httpOnly) pairs.push('httpOnly');
270 if (obj.secure) pairs.push('secure');
271
272 return pairs.join('; ');
273};
274
275/**
276 * Pause `data` and `end` events on the given `obj`.
277 * Middleware performing async tasks _should_ utilize
278 * this utility (or similar), to re-emit data once
279 * the async operation has completed, otherwise these
280 * events may be lost.
281 *
282 * var pause = utils.pause(req);
283 * fs.readFile(path, function(){
284 * next();
285 * pause.resume();
286 * });
287 *
288 * @param {Object} obj
289 * @return {Object}
290 * @api private
291 */
292
293exports.pause = function(obj){
294 var onData
295 , onEnd
296 , events = [];
297
298 // buffer data
299 obj.on('data', onData = function(data, encoding){
300 events.push(['data', data, encoding]);
301 });
302
303 // buffer end
304 obj.on('end', onEnd = function(data, encoding){
305 events.push(['end', data, encoding]);
306 });
307
308 return {
309 end: function(){
310 obj.removeListener('data', onData);
311 obj.removeListener('end', onEnd);
312 },
313 resume: function(){
314 this.end();
315 for (var i = 0, len = events.length; i < len; ++i) {
316 obj.emit.apply(obj, events[i]);
317 }
318 }
319 };
320};
321
322/**
323 * Check `req` and `res` to see if it has been modified.
324 *
325 * @param {IncomingMessage} req
326 * @param {ServerResponse} res
327 * @return {Boolean}
328 * @api private
329 */
330
331exports.modified = function(req, res, headers) {
332 var headers = headers || res._headers || {}
333 , modifiedSince = req.headers['if-modified-since']
334 , lastModified = headers['last-modified']
335 , noneMatch = req.headers['if-none-match']
336 , etag = headers['etag'];
337
338 if (noneMatch) noneMatch = noneMatch.split(/ *, */);
339
340 // check If-None-Match
341 if (noneMatch && etag && ~noneMatch.indexOf(etag)) {
342 return false;
343 }
344
345 // check If-Modified-Since
346 if (modifiedSince && lastModified) {
347 modifiedSince = new Date(modifiedSince);
348 lastModified = new Date(lastModified);
349 // Ignore invalid dates
350 if (!isNaN(modifiedSince.getTime())) {
351 if (lastModified <= modifiedSince) return false;
352 }
353 }
354
355 return true;
356};
357
358/**
359 * Strip `Content-*` headers from `res`.
360 *
361 * @param {ServerResponse} res
362 * @api private
363 */
364
365exports.removeContentHeaders = function(res){
366 Object.keys(res._headers).forEach(function(field){
367 if (0 == field.indexOf('content')) {
368 res.removeHeader(field);
369 }
370 });
371};
372
373/**
374 * Check if `req` is a conditional GET request.
375 *
376 * @param {IncomingMessage} req
377 * @return {Boolean}
378 * @api private
379 */
380
381exports.conditionalGET = function(req) {
382 return req.headers['if-modified-since']
383 || req.headers['if-none-match'];
384};
385
386/**
387 * Respond with 401 "Unauthorized".
388 *
389 * @param {ServerResponse} res
390 * @param {String} realm
391 * @api private
392 */
393
394exports.unauthorized = function(res, realm) {
395 res.statusCode = 401;
396 res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"');
397 res.end('Unauthorized');
398};
399
400/**
401 * Respond with 304 "Not Modified".
402 *
403 * @param {ServerResponse} res
404 * @param {Object} headers
405 * @api private
406 */
407
408exports.notModified = function(res) {
409 exports.removeContentHeaders(res);
410 res.statusCode = 304;
411 res.end();
412};
413
414/**
415 * Return an ETag in the form of `"<size>-<mtime>"`
416 * from the given `stat`.
417 *
418 * @param {Object} stat
419 * @return {String}
420 * @api private
421 */
422
423exports.etag = function(stat) {
424 return '"' + stat.size + '-' + Number(stat.mtime) + '"';
425};
426
427/**
428 * Parse "Range" header `str` relative to the given file `size`.
429 *
430 * @param {Number} size
431 * @param {String} str
432 * @return {Array}
433 * @api private
434 */
435
436exports.parseRange = function(size, str){
437 var valid = true;
438 var arr = str.substr(6).split(',').map(function(range){
439 var range = range.split('-')
440 , start = parseInt(range[0], 10)
441 , end = parseInt(range[1], 10);
442
443 // -500
444 if (isNaN(start)) {
445 start = size - end;
446 end = size - 1;
447 // 500-
448 } else if (isNaN(end)) {
449 end = size - 1;
450 }
451
452 // Invalid
453 if (isNaN(start)
454 || isNaN(end)
455 || start > end
456 || start < 0) valid = false;
457
458 return {
459 start: start,
460 end: end
461 };
462 });
463
464 return valid ? arr : null;
465};
466
467/**
468 * Parse the given Cache-Control `str`.
469 *
470 * @param {String} str
471 * @return {Object}
472 * @api private
473 */
474
475exports.parseCacheControl = function(str){
476 var directives = str.split(',')
477 , obj = {};
478
479 for(var i = 0, len = directives.length; i < len; i++) {
480 var parts = directives[i].split('=')
481 , key = parts.shift().trim()
482 , val = parseInt(parts.shift(), 10);
483
484 obj[key] = isNaN(val) ? true : val;
485 }
486
487 return obj;
488};