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.

757 lines
17 KiB

  1. /**
  2. * Module dependencies.
  3. */
  4. var http = require('http')
  5. , path = require('path')
  6. , connect = require('connect')
  7. , utils = connect.utils
  8. , sign = require('cookie-signature').sign
  9. , normalizeType = require('./utils').normalizeType
  10. , normalizeTypes = require('./utils').normalizeTypes
  11. , etag = require('./utils').etag
  12. , statusCodes = http.STATUS_CODES
  13. , cookie = require('cookie')
  14. , send = require('send')
  15. , mime = connect.mime
  16. , basename = path.basename
  17. , extname = path.extname
  18. , join = path.join;
  19. /**
  20. * Response prototype.
  21. */
  22. var res = module.exports = {
  23. __proto__: http.ServerResponse.prototype
  24. };
  25. /**
  26. * Set status `code`.
  27. *
  28. * @param {Number} code
  29. * @return {ServerResponse}
  30. * @api public
  31. */
  32. res.status = function(code){
  33. this.statusCode = code;
  34. return this;
  35. };
  36. /**
  37. * Set Link header field with the given `links`.
  38. *
  39. * Examples:
  40. *
  41. * res.links({
  42. * next: 'http://api.example.com/users?page=2',
  43. * last: 'http://api.example.com/users?page=5'
  44. * });
  45. *
  46. * @param {Object} links
  47. * @return {ServerResponse}
  48. * @api public
  49. */
  50. res.links = function(links){
  51. return this.set('Link', Object.keys(links).map(function(rel){
  52. return '<' + links[rel] + '>; rel="' + rel + '"';
  53. }).join(', '));
  54. };
  55. /**
  56. * Send a response.
  57. *
  58. * Examples:
  59. *
  60. * res.send(new Buffer('wahoo'));
  61. * res.send({ some: 'json' });
  62. * res.send('<p>some html</p>');
  63. * res.send(404, 'Sorry, cant find that');
  64. * res.send(404);
  65. *
  66. * @param {Mixed} body or status
  67. * @param {Mixed} body
  68. * @return {ServerResponse}
  69. * @api public
  70. */
  71. res.send = function(body){
  72. var req = this.req;
  73. var head = 'HEAD' == req.method;
  74. var len;
  75. // allow status / body
  76. if (2 == arguments.length) {
  77. // res.send(body, status) backwards compat
  78. if ('number' != typeof body && 'number' == typeof arguments[1]) {
  79. this.statusCode = arguments[1];
  80. } else {
  81. this.statusCode = body;
  82. body = arguments[1];
  83. }
  84. }
  85. switch (typeof body) {
  86. // response status
  87. case 'number':
  88. this.get('Content-Type') || this.type('txt');
  89. this.statusCode = body;
  90. body = http.STATUS_CODES[body];
  91. break;
  92. // string defaulting to html
  93. case 'string':
  94. if (!this.get('Content-Type')) {
  95. this.charset = this.charset || 'utf-8';
  96. this.type('html');
  97. }
  98. break;
  99. case 'boolean':
  100. case 'object':
  101. if (null == body) {
  102. body = '';
  103. } else if (Buffer.isBuffer(body)) {
  104. this.get('Content-Type') || this.type('bin');
  105. } else {
  106. return this.json(body);
  107. }
  108. break;
  109. }
  110. // populate Content-Length
  111. if (undefined !== body && !this.get('Content-Length')) {
  112. this.set('Content-Length', len = Buffer.isBuffer(body)
  113. ? body.length
  114. : Buffer.byteLength(body));
  115. }
  116. // ETag support
  117. // TODO: W/ support
  118. if (len > 1024 && 'GET' == req.method) {
  119. if (!this.get('ETag')) {
  120. this.set('ETag', etag(body));
  121. }
  122. }
  123. // freshness
  124. if (req.fresh) this.statusCode = 304;
  125. // strip irrelevant headers
  126. if (204 == this.statusCode || 304 == this.statusCode) {
  127. this.removeHeader('Content-Type');
  128. this.removeHeader('Content-Length');
  129. this.removeHeader('Transfer-Encoding');
  130. body = '';
  131. }
  132. // respond
  133. this.end(head ? null : body);
  134. return this;
  135. };
  136. /**
  137. * Send JSON response.
  138. *
  139. * Examples:
  140. *
  141. * res.json(null);
  142. * res.json({ user: 'tj' });
  143. * res.json(500, 'oh noes!');
  144. * res.json(404, 'I dont have that');
  145. *
  146. * @param {Mixed} obj or status
  147. * @param {Mixed} obj
  148. * @return {ServerResponse}
  149. * @api public
  150. */
  151. res.json = function(obj){
  152. // allow status / body
  153. if (2 == arguments.length) {
  154. // res.json(body, status) backwards compat
  155. if ('number' == typeof arguments[1]) {
  156. this.statusCode = arguments[1];
  157. } else {
  158. this.statusCode = obj;
  159. obj = arguments[1];
  160. }
  161. }
  162. // settings
  163. var app = this.app;
  164. var replacer = app.get('json replacer');
  165. var spaces = app.get('json spaces');
  166. var body = JSON.stringify(obj, replacer, spaces);
  167. // content-type
  168. this.charset = this.charset || 'utf-8';
  169. this.get('Content-Type') || this.set('Content-Type', 'application/json');
  170. return this.send(body);
  171. };
  172. /**
  173. * Send JSON response with JSONP callback support.
  174. *
  175. * Examples:
  176. *
  177. * res.jsonp(null);
  178. * res.jsonp({ user: 'tj' });
  179. * res.jsonp(500, 'oh noes!');
  180. * res.jsonp(404, 'I dont have that');
  181. *
  182. * @param {Mixed} obj or status
  183. * @param {Mixed} obj
  184. * @return {ServerResponse}
  185. * @api public
  186. */
  187. res.jsonp = function(obj){
  188. // allow status / body
  189. if (2 == arguments.length) {
  190. // res.json(body, status) backwards compat
  191. if ('number' == typeof arguments[1]) {
  192. this.statusCode = arguments[1];
  193. } else {
  194. this.statusCode = obj;
  195. obj = arguments[1];
  196. }
  197. }
  198. // settings
  199. var app = this.app;
  200. var replacer = app.get('json replacer');
  201. var spaces = app.get('json spaces');
  202. var body = JSON.stringify(obj, replacer, spaces)
  203. .replace(/\u2028/g, '\\u2028')
  204. .replace(/\u2029/g, '\\u2029');
  205. var callback = this.req.query[app.get('jsonp callback name')];
  206. // content-type
  207. this.charset = this.charset || 'utf-8';
  208. this.set('Content-Type', 'application/json');
  209. // jsonp
  210. if (callback) {
  211. this.set('Content-Type', 'text/javascript');
  212. var cb = callback.replace(/[^\[\]\w$.]/g, '');
  213. body = cb + ' && ' + cb + '(' + body + ');';
  214. }
  215. return this.send(body);
  216. };
  217. /**
  218. * Transfer the file at the given `path`.
  219. *
  220. * Automatically sets the _Content-Type_ response header field.
  221. * The callback `fn(err)` is invoked when the transfer is complete
  222. * or when an error occurs. Be sure to check `res.sentHeader`
  223. * if you wish to attempt responding, as the header and some data
  224. * may have already been transferred.
  225. *
  226. * Options:
  227. *
  228. * - `maxAge` defaulting to 0
  229. * - `root` root directory for relative filenames
  230. *
  231. * Examples:
  232. *
  233. * The following example illustrates how `res.sendfile()` may
  234. * be used as an alternative for the `static()` middleware for
  235. * dynamic situations. The code backing `res.sendfile()` is actually
  236. * the same code, so HTTP cache support etc is identical.
  237. *
  238. * app.get('/user/:uid/photos/:file', function(req, res){
  239. * var uid = req.params.uid
  240. * , file = req.params.file;
  241. *
  242. * req.user.mayViewFilesFrom(uid, function(yes){
  243. * if (yes) {
  244. * res.sendfile('/uploads/' + uid + '/' + file);
  245. * } else {
  246. * res.send(403, 'Sorry! you cant see that.');
  247. * }
  248. * });
  249. * });
  250. *
  251. * @param {String} path
  252. * @param {Object|Function} options or fn
  253. * @param {Function} fn
  254. * @api public
  255. */
  256. res.sendfile = function(path, options, fn){
  257. var self = this
  258. , req = self.req
  259. , next = this.req.next
  260. , options = options || {}
  261. , done;
  262. // support function as second arg
  263. if ('function' == typeof options) {
  264. fn = options;
  265. options = {};
  266. }
  267. // socket errors
  268. req.socket.on('error', error);
  269. // errors
  270. function error(err) {
  271. if (done) return;
  272. done = true;
  273. // clean up
  274. cleanup();
  275. if (!self.headerSent) self.removeHeader('Content-Disposition');
  276. // callback available
  277. if (fn) return fn(err);
  278. // list in limbo if there's no callback
  279. if (self.headerSent) return;
  280. // delegate
  281. next(err);
  282. }
  283. // streaming
  284. function stream() {
  285. if (done) return;
  286. cleanup();
  287. if (fn) self.on('finish', fn);
  288. }
  289. // cleanup
  290. function cleanup() {
  291. req.socket.removeListener('error', error);
  292. }
  293. // transfer
  294. var file = send(req, path);
  295. if (options.root) file.root(options.root);
  296. file.maxage(options.maxAge || 0);
  297. file.on('error', error);
  298. file.on('directory', next);
  299. file.on('stream', stream);
  300. file.pipe(this);
  301. this.on('finish', cleanup);
  302. };
  303. /**
  304. * Transfer the file at the given `path` as an attachment.
  305. *
  306. * Optionally providing an alternate attachment `filename`,
  307. * and optional callback `fn(err)`. The callback is invoked
  308. * when the data transfer is complete, or when an error has
  309. * ocurred. Be sure to check `res.headerSent` if you plan to respond.
  310. *
  311. * This method uses `res.sendfile()`.
  312. *
  313. * @param {String} path
  314. * @param {String|Function} filename or fn
  315. * @param {Function} fn
  316. * @api public
  317. */
  318. res.download = function(path, filename, fn){
  319. // support function as second arg
  320. if ('function' == typeof filename) {
  321. fn = filename;
  322. filename = null;
  323. }
  324. filename = filename || path;
  325. this.set('Content-Disposition', 'attachment; filename="' + basename(filename) + '"');
  326. return this.sendfile(path, fn);
  327. };
  328. /**
  329. * Set _Content-Type_ response header with `type` through `mime.lookup()`
  330. * when it does not contain "/", or set the Content-Type to `type` otherwise.
  331. *
  332. * Examples:
  333. *
  334. * res.type('.html');
  335. * res.type('html');
  336. * res.type('json');
  337. * res.type('application/json');
  338. * res.type('png');
  339. *
  340. * @param {String} type
  341. * @return {ServerResponse} for chaining
  342. * @api public
  343. */
  344. res.contentType =
  345. res.type = function(type){
  346. return this.set('Content-Type', ~type.indexOf('/')
  347. ? type
  348. : mime.lookup(type));
  349. };
  350. /**
  351. * Respond to the Acceptable formats using an `obj`
  352. * of mime-type callbacks.
  353. *
  354. * This method uses `req.accepted`, an array of
  355. * acceptable types ordered by their quality values.
  356. * When "Accept" is not present the _first_ callback
  357. * is invoked, otherwise the first match is used. When
  358. * no match is performed the server responds with
  359. * 406 "Not Acceptable".
  360. *
  361. * Content-Type is set for you, however if you choose
  362. * you may alter this within the callback using `res.type()`
  363. * or `res.set('Content-Type', ...)`.
  364. *
  365. * res.format({
  366. * 'text/plain': function(){
  367. * res.send('hey');
  368. * },
  369. *
  370. * 'text/html': function(){
  371. * res.send('<p>hey</p>');
  372. * },
  373. *
  374. * 'appliation/json': function(){
  375. * res.send({ message: 'hey' });
  376. * }
  377. * });
  378. *
  379. * In addition to canonicalized MIME types you may
  380. * also use extnames mapped to these types:
  381. *
  382. * res.format({
  383. * text: function(){
  384. * res.send('hey');
  385. * },
  386. *
  387. * html: function(){
  388. * res.send('<p>hey</p>');
  389. * },
  390. *
  391. * json: function(){
  392. * res.send({ message: 'hey' });
  393. * }
  394. * });
  395. *
  396. * By default Express passes an `Error`
  397. * with a `.status` of 406 to `next(err)`
  398. * if a match is not made. If you provide
  399. * a `.default` callback it will be invoked
  400. * instead.
  401. *
  402. * @param {Object} obj
  403. * @return {ServerResponse} for chaining
  404. * @api public
  405. */
  406. res.format = function(obj){
  407. var req = this.req
  408. , next = req.next;
  409. var fn = obj.default;
  410. if (fn) delete obj.default;
  411. var keys = Object.keys(obj);
  412. var key = req.accepts(keys);
  413. this.set('Vary', 'Accept');
  414. if (key) {
  415. this.set('Content-Type', normalizeType(key).value);
  416. obj[key](req, this, next);
  417. } else if (fn) {
  418. fn();
  419. } else {
  420. var err = new Error('Not Acceptable');
  421. err.status = 406;
  422. err.types = normalizeTypes(keys).map(function(o){ return o.value });
  423. next(err);
  424. }
  425. return this;
  426. };
  427. /**
  428. * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
  429. *
  430. * @param {String} filename
  431. * @return {ServerResponse}
  432. * @api public
  433. */
  434. res.attachment = function(filename){
  435. if (filename) this.type(extname(filename));
  436. this.set('Content-Disposition', filename
  437. ? 'attachment; filename="' + basename(filename) + '"'
  438. : 'attachment');
  439. return this;
  440. };
  441. /**
  442. * Set header `field` to `val`, or pass
  443. * an object of header fields.
  444. *
  445. * Examples:
  446. *
  447. * res.set('Foo', ['bar', 'baz']);
  448. * res.set('Accept', 'application/json');
  449. * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
  450. *
  451. * Aliased as `res.header()`.
  452. *
  453. * @param {String|Object|Array} field
  454. * @param {String} val
  455. * @return {ServerResponse} for chaining
  456. * @api public
  457. */
  458. res.set =
  459. res.header = function(field, val){
  460. if (2 == arguments.length) {
  461. if (Array.isArray(val)) val = val.map(String);
  462. else val = String(val);
  463. this.setHeader(field, val);
  464. } else {
  465. for (var key in field) {
  466. this.set(key, field[key]);
  467. }
  468. }
  469. return this;
  470. };
  471. /**
  472. * Get value for header `field`.
  473. *
  474. * @param {String} field
  475. * @return {String}
  476. * @api public
  477. */
  478. res.get = function(field){
  479. return this.getHeader(field);
  480. };
  481. /**
  482. * Clear cookie `name`.
  483. *
  484. * @param {String} name
  485. * @param {Object} options
  486. * @param {ServerResponse} for chaining
  487. * @api public
  488. */
  489. res.clearCookie = function(name, options){
  490. var opts = { expires: new Date(1), path: '/' };
  491. return this.cookie(name, '', options
  492. ? utils.merge(opts, options)
  493. : opts);
  494. };
  495. /**
  496. * Set cookie `name` to `val`, with the given `options`.
  497. *
  498. * Options:
  499. *
  500. * - `maxAge` max-age in milliseconds, converted to `expires`
  501. * - `signed` sign the cookie
  502. * - `path` defaults to "/"
  503. *
  504. * Examples:
  505. *
  506. * // "Remember Me" for 15 minutes
  507. * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
  508. *
  509. * // save as above
  510. * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
  511. *
  512. * @param {String} name
  513. * @param {String|Object} val
  514. * @param {Options} options
  515. * @api public
  516. */
  517. res.cookie = function(name, val, options){
  518. options = utils.merge({}, options);
  519. var secret = this.req.secret;
  520. var signed = options.signed;
  521. if (signed && !secret) throw new Error('connect.cookieParser("secret") required for signed cookies');
  522. if ('number' == typeof val) val = val.toString();
  523. if ('object' == typeof val) val = 'j:' + JSON.stringify(val);
  524. if (signed) val = 's:' + sign(val, secret);
  525. if ('maxAge' in options) {
  526. options.expires = new Date(Date.now() + options.maxAge);
  527. options.maxAge /= 1000;
  528. }
  529. if (null == options.path) options.path = '/';
  530. this.set('Set-Cookie', cookie.serialize(name, String(val), options));
  531. return this;
  532. };
  533. /**
  534. * Set the location header to `url`.
  535. *
  536. * The given `url` can also be the name of a mapped url, for
  537. * example by default express supports "back" which redirects
  538. * to the _Referrer_ or _Referer_ headers or "/".
  539. *
  540. * Examples:
  541. *
  542. * res.location('/foo/bar').;
  543. * res.location('http://example.com');
  544. * res.location('../login'); // /blog/post/1 -> /blog/login
  545. *
  546. * Mounting:
  547. *
  548. * When an application is mounted and `res.location()`
  549. * is given a path that does _not_ lead with "/" it becomes
  550. * relative to the mount-point. For example if the application
  551. * is mounted at "/blog", the following would become "/blog/login".
  552. *
  553. * res.location('login');
  554. *
  555. * While the leading slash would result in a location of "/login":
  556. *
  557. * res.location('/login');
  558. *
  559. * @param {String} url
  560. * @api public
  561. */
  562. res.location = function(url){
  563. var app = this.app
  564. , req = this.req;
  565. // setup redirect map
  566. var map = { back: req.get('Referrer') || '/' };
  567. // perform redirect
  568. url = map[url] || url;
  569. // relative
  570. if (!~url.indexOf('://') && 0 != url.indexOf('//')) {
  571. var path
  572. // relative to path
  573. if ('.' == url[0]) {
  574. path = req.originalUrl.split('?')[0]
  575. url = path + ('/' == path[path.length - 1] ? '' : '/') + url;
  576. // relative to mount-point
  577. } else if ('/' != url[0]) {
  578. path = app.path();
  579. url = path + '/' + url;
  580. }
  581. }
  582. // Respond
  583. this.set('Location', url);
  584. return this;
  585. };
  586. /**
  587. * Redirect to the given `url` with optional response `status`
  588. * defaulting to 302.
  589. *
  590. * The resulting `url` is determined by `res.location()`, so
  591. * it will play nicely with mounted apps, relative paths,
  592. * `"back"` etc.
  593. *
  594. * Examples:
  595. *
  596. * res.redirect('/foo/bar');
  597. * res.redirect('http://example.com');
  598. * res.redirect(301, 'http://example.com');
  599. * res.redirect('http://example.com', 301);
  600. * res.redirect('../login'); // /blog/post/1 -> /blog/login
  601. *
  602. * @param {String} url
  603. * @param {Number} code
  604. * @api public
  605. */
  606. res.redirect = function(url){
  607. var app = this.app
  608. , head = 'HEAD' == this.req.method
  609. , status = 302
  610. , body;
  611. // allow status / url
  612. if (2 == arguments.length) {
  613. if ('number' == typeof url) {
  614. status = url;
  615. url = arguments[1];
  616. } else {
  617. status = arguments[1];
  618. }
  619. }
  620. // Set location header
  621. this.location(url);
  622. url = this.get('Location');
  623. // Support text/{plain,html} by default
  624. this.format({
  625. text: function(){
  626. body = statusCodes[status] + '. Redirecting to ' + encodeURI(url);
  627. },
  628. html: function(){
  629. var u = utils.escape(url);
  630. body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>';
  631. },
  632. default: function(){
  633. body = '';
  634. }
  635. });
  636. // Respond
  637. this.statusCode = status;
  638. this.set('Content-Length', Buffer.byteLength(body));
  639. this.end(head ? null : body);
  640. };
  641. /**
  642. * Render `view` with the given `options` and optional callback `fn`.
  643. * When a callback function is given a response will _not_ be made
  644. * automatically, otherwise a response of _200_ and _text/html_ is given.
  645. *
  646. * Options:
  647. *
  648. * - `cache` boolean hinting to the engine it should cache
  649. * - `filename` filename of the view being rendered
  650. *
  651. * @param {String} view
  652. * @param {Object|Function} options or callback function
  653. * @param {Function} fn
  654. * @api public
  655. */
  656. res.render = function(view, options, fn){
  657. var self = this
  658. , options = options || {}
  659. , req = this.req
  660. , app = req.app;
  661. // support callback function as second arg
  662. if ('function' == typeof options) {
  663. fn = options, options = {};
  664. }
  665. // merge res.locals
  666. options._locals = self.locals;
  667. // default callback to respond
  668. fn = fn || function(err, str){
  669. if (err) return req.next(err);
  670. self.send(str);
  671. };
  672. // render
  673. app.render(view, options, fn);
  674. };