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.

403 lines
7.8 KiB

  1. /*!
  2. * Connect - utils
  3. * Copyright(c) 2010 Sencha Inc.
  4. * Copyright(c) 2011 TJ Holowaychuk
  5. * MIT Licensed
  6. */
  7. /**
  8. * Module dependencies.
  9. */
  10. var http = require('http')
  11. , crypto = require('crypto')
  12. , parse = require('url').parse
  13. , signature = require('cookie-signature')
  14. , nodeVersion = process.versions.node.split('.');
  15. // pause is broken in node < 0.10
  16. exports.brokenPause = parseInt(nodeVersion[0], 10) === 0
  17. && parseInt(nodeVersion[1], 10) < 10;
  18. /**
  19. * Return `true` if the request has a body, otherwise return `false`.
  20. *
  21. * @param {IncomingMessage} req
  22. * @return {Boolean}
  23. * @api private
  24. */
  25. exports.hasBody = function(req) {
  26. return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
  27. };
  28. /**
  29. * Extract the mime type from the given request's
  30. * _Content-Type_ header.
  31. *
  32. * @param {IncomingMessage} req
  33. * @return {String}
  34. * @api private
  35. */
  36. exports.mime = function(req) {
  37. var str = req.headers['content-type'] || '';
  38. return str.split(';')[0];
  39. };
  40. /**
  41. * Generate an `Error` from the given status `code`
  42. * and optional `msg`.
  43. *
  44. * @param {Number} code
  45. * @param {String} msg
  46. * @return {Error}
  47. * @api private
  48. */
  49. exports.error = function(code, msg){
  50. var err = new Error(msg || http.STATUS_CODES[code]);
  51. err.status = code;
  52. return err;
  53. };
  54. /**
  55. * Return md5 hash of the given string and optional encoding,
  56. * defaulting to hex.
  57. *
  58. * utils.md5('wahoo');
  59. * // => "e493298061761236c96b02ea6aa8a2ad"
  60. *
  61. * @param {String} str
  62. * @param {String} encoding
  63. * @return {String}
  64. * @api private
  65. */
  66. exports.md5 = function(str, encoding){
  67. return crypto
  68. .createHash('md5')
  69. .update(str)
  70. .digest(encoding || 'hex');
  71. };
  72. /**
  73. * Merge object b with object a.
  74. *
  75. * var a = { foo: 'bar' }
  76. * , b = { bar: 'baz' };
  77. *
  78. * utils.merge(a, b);
  79. * // => { foo: 'bar', bar: 'baz' }
  80. *
  81. * @param {Object} a
  82. * @param {Object} b
  83. * @return {Object}
  84. * @api private
  85. */
  86. exports.merge = function(a, b){
  87. if (a && b) {
  88. for (var key in b) {
  89. a[key] = b[key];
  90. }
  91. }
  92. return a;
  93. };
  94. /**
  95. * Escape the given string of `html`.
  96. *
  97. * @param {String} html
  98. * @return {String}
  99. * @api private
  100. */
  101. exports.escape = function(html){
  102. return String(html)
  103. .replace(/&(?!\w+;)/g, '&amp;')
  104. .replace(/</g, '&lt;')
  105. .replace(/>/g, '&gt;')
  106. .replace(/"/g, '&quot;');
  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. exports.uid = function(len) {
  119. return crypto.randomBytes(Math.ceil(len * 3 / 4))
  120. .toString('base64')
  121. .slice(0, len)
  122. .replace(/\//g, '-')
  123. .replace(/\+/g, '_');
  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. exports.sign = function(val, secret){
  134. console.warn('do not use utils.sign(), use https://github.com/visionmedia/node-cookie-signature')
  135. return val + '.' + crypto
  136. .createHmac('sha256', secret)
  137. .update(val)
  138. .digest('base64')
  139. .replace(/=+$/, '');
  140. };
  141. /**
  142. * Unsign and decode the given `val` with `secret`,
  143. * returning `false` if the signature is invalid.
  144. *
  145. * @param {String} val
  146. * @param {String} secret
  147. * @return {String|Boolean}
  148. * @api private
  149. */
  150. exports.unsign = function(val, secret){
  151. console.warn('do not use utils.unsign(), use https://github.com/visionmedia/node-cookie-signature')
  152. var str = val.slice(0, val.lastIndexOf('.'));
  153. return exports.sign(str, secret) == val
  154. ? str
  155. : false;
  156. };
  157. /**
  158. * Parse signed cookies, returning an object
  159. * containing the decoded key/value pairs,
  160. * while removing the signed key from `obj`.
  161. *
  162. * @param {Object} obj
  163. * @return {Object}
  164. * @api private
  165. */
  166. exports.parseSignedCookies = function(obj, secret){
  167. var ret = {};
  168. Object.keys(obj).forEach(function(key){
  169. var val = obj[key];
  170. if (0 == val.indexOf('s:')) {
  171. val = signature.unsign(val.slice(2), secret);
  172. if (val) {
  173. ret[key] = val;
  174. delete obj[key];
  175. }
  176. }
  177. });
  178. return ret;
  179. };
  180. /**
  181. * Parse a signed cookie string, return the decoded value
  182. *
  183. * @param {String} str signed cookie string
  184. * @param {String} secret
  185. * @return {String} decoded value
  186. * @api private
  187. */
  188. exports.parseSignedCookie = function(str, secret){
  189. return 0 == str.indexOf('s:')
  190. ? signature.unsign(str.slice(2), secret)
  191. : str;
  192. };
  193. /**
  194. * Parse JSON cookies.
  195. *
  196. * @param {Object} obj
  197. * @return {Object}
  198. * @api private
  199. */
  200. exports.parseJSONCookies = function(obj){
  201. Object.keys(obj).forEach(function(key){
  202. var val = obj[key];
  203. var res = exports.parseJSONCookie(val);
  204. if (res) obj[key] = res;
  205. });
  206. return obj;
  207. };
  208. /**
  209. * Parse JSON cookie string
  210. *
  211. * @param {String} str
  212. * @return {Object} Parsed object or null if not json cookie
  213. * @api private
  214. */
  215. exports.parseJSONCookie = function(str) {
  216. if (0 == str.indexOf('j:')) {
  217. try {
  218. return JSON.parse(str.slice(2));
  219. } catch (err) {
  220. // no op
  221. }
  222. }
  223. };
  224. /**
  225. * Pause `data` and `end` events on the given `obj`.
  226. * Middleware performing async tasks _should_ utilize
  227. * this utility (or similar), to re-emit data once
  228. * the async operation has completed, otherwise these
  229. * events may be lost. Pause is only required for
  230. * node versions less than 10, and is replaced with
  231. * noop's otherwise.
  232. *
  233. * var pause = utils.pause(req);
  234. * fs.readFile(path, function(){
  235. * next();
  236. * pause.resume();
  237. * });
  238. *
  239. * @param {Object} obj
  240. * @return {Object}
  241. * @api private
  242. */
  243. exports.pause = exports.brokenPause
  244. ? require('pause')
  245. : function () {
  246. return {
  247. end: noop,
  248. resume: noop
  249. }
  250. }
  251. /**
  252. * Strip `Content-*` headers from `res`.
  253. *
  254. * @param {ServerResponse} res
  255. * @api private
  256. */
  257. exports.removeContentHeaders = function(res){
  258. Object.keys(res._headers).forEach(function(field){
  259. if (0 == field.indexOf('content')) {
  260. res.removeHeader(field);
  261. }
  262. });
  263. };
  264. /**
  265. * Check if `req` is a conditional GET request.
  266. *
  267. * @param {IncomingMessage} req
  268. * @return {Boolean}
  269. * @api private
  270. */
  271. exports.conditionalGET = function(req) {
  272. return req.headers['if-modified-since']
  273. || req.headers['if-none-match'];
  274. };
  275. /**
  276. * Respond with 401 "Unauthorized".
  277. *
  278. * @param {ServerResponse} res
  279. * @param {String} realm
  280. * @api private
  281. */
  282. exports.unauthorized = function(res, realm) {
  283. res.statusCode = 401;
  284. res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"');
  285. res.end('Unauthorized');
  286. };
  287. /**
  288. * Respond with 304 "Not Modified".
  289. *
  290. * @param {ServerResponse} res
  291. * @param {Object} headers
  292. * @api private
  293. */
  294. exports.notModified = function(res) {
  295. exports.removeContentHeaders(res);
  296. res.statusCode = 304;
  297. res.end();
  298. };
  299. /**
  300. * Return an ETag in the form of `"<size>-<mtime>"`
  301. * from the given `stat`.
  302. *
  303. * @param {Object} stat
  304. * @return {String}
  305. * @api private
  306. */
  307. exports.etag = function(stat) {
  308. return '"' + stat.size + '-' + Number(stat.mtime) + '"';
  309. };
  310. /**
  311. * Parse the given Cache-Control `str`.
  312. *
  313. * @param {String} str
  314. * @return {Object}
  315. * @api private
  316. */
  317. exports.parseCacheControl = function(str){
  318. var directives = str.split(',')
  319. , obj = {};
  320. for(var i = 0, len = directives.length; i < len; i++) {
  321. var parts = directives[i].split('=')
  322. , key = parts.shift().trim()
  323. , val = parseInt(parts.shift(), 10);
  324. obj[key] = isNaN(val) ? true : val;
  325. }
  326. return obj;
  327. };
  328. /**
  329. * Parse the `req` url with memoization.
  330. *
  331. * @param {ServerRequest} req
  332. * @return {Object}
  333. * @api private
  334. */
  335. exports.parseUrl = function(req){
  336. var parsed = req._parsedUrl;
  337. if (parsed && parsed.href == req.url) {
  338. return parsed;
  339. } else {
  340. return req._parsedUrl = parse(req.url);
  341. }
  342. };
  343. /**
  344. * Parse byte `size` string.
  345. *
  346. * @param {String} size
  347. * @return {Number}
  348. * @api private
  349. */
  350. exports.parseBytes = require('bytes');
  351. function noop() {}