UNPKG

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