connect-injector.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. 'use strict';
  2. var Proto = require('uberproto');
  3. var zlib = require('zlib');
  4. var Q = require('q');
  5. var debug = require('debug')('connect-injector');
  6. var WritableStream = require('stream-buffers').WritableStreamBuffer;
  7. var each = function(obj, callback) {
  8. if(obj) {
  9. Object.keys(obj).forEach(function(key) {
  10. callback(obj[key], key);
  11. });
  12. }
  13. };
  14. module.exports = function (when, converter) {
  15. return function (req, res, next) {
  16. // Allows more than one injector
  17. if (res.injectors) {
  18. debug('injector already initialized, adding to existing');
  19. res.injectors.push({
  20. when: when,
  21. converter: converter
  22. });
  23. return next();
  24. }
  25. debug('initializing new injector');
  26. var oldWrite = res.write;
  27. // An object that we can mix into the response
  28. var mixin = {
  29. injectors: [
  30. {
  31. when: when,
  32. converter: converter
  33. }
  34. ],
  35. // Checks if this response should be intercepted
  36. _interceptCheck: function () {
  37. var self = this;
  38. if (typeof this._isIntercepted === 'undefined') {
  39. // Got through all injector objects
  40. each(res.injectors, function (obj) {
  41. if (obj.when(req, res)) {
  42. self._isIntercepted = true;
  43. obj.active = true;
  44. }
  45. });
  46. if (!this._isIntercepted) {
  47. this._isIntercepted = false;
  48. }
  49. debug('first time _interceptCheck ran', this._isIntercepted);
  50. }
  51. return this._isIntercepted;
  52. },
  53. // Overwrite setting the response headers. We can't do content-length
  54. // so lets just use transfer-encoding chunked
  55. setHeader: function (name, value) {
  56. if (name === 'content-length' || name === 'Content-Length') {
  57. debug('not setting content-length header');
  58. return;
  59. }
  60. return this._super(name, value);
  61. },
  62. // Overwrite writeHead since it can also set the headers and we need to override
  63. // the transfer-encoding
  64. writeHead: function(status, reasonPhrase, headers) {
  65. var self = this;
  66. each(headers || reasonPhrase, function(value, name) {
  67. self.setHeader(name, value);
  68. });
  69. return this._super(status, typeof reasonPhrase === 'string' ? reasonPhrase : undefined);
  70. },
  71. // Write into the buffer if this request is intercepted
  72. write: function (chunk, encoding) {
  73. if (this._interceptCheck()) {
  74. if(!this._interceptBuffer) {
  75. debug('initializing _interceptBuffer');
  76. this._interceptBuffer = new WritableStream();
  77. }
  78. return this._interceptBuffer.write(chunk, encoding);
  79. }
  80. return this._super.apply(this, arguments);
  81. },
  82. // End the request.
  83. end: function (data, encoding) {
  84. var self = this;
  85. var _super = this._super.bind(this);
  86. if (!this._interceptCheck()) {
  87. debug('not intercepting, ending with original .end');
  88. return _super(data, encoding);
  89. }
  90. if (data) {
  91. this.write(data, encoding);
  92. }
  93. debug('resetting to original response.write');
  94. this.write = oldWrite;
  95. // Responses without a body can just be ended
  96. if(!this._hasBody || !this._interceptBuffer) {
  97. debug('ending empty resopnse without injecting anything');
  98. return _super();
  99. }
  100. var gzipped = this.getHeader('content-encoding') === 'gzip';
  101. var chain = Q(this._interceptBuffer.getContents());
  102. if(gzipped) {
  103. debug('unzipping content');
  104. // Unzip the buffer
  105. chain = chain.then(function(buffer) {
  106. return Q.nfcall(zlib.gunzip, buffer);
  107. });
  108. }
  109. this.injectors.forEach(function(injector) {
  110. // Run all converters, if they are active
  111. // In series, using the previous output
  112. if(injector.active) {
  113. debug('adding injector to chain');
  114. var converter = injector.converter.bind(self);
  115. chain = chain.then(function(prev) {
  116. return Q.nfcall(converter, prev, req, res);
  117. });
  118. }
  119. });
  120. if(gzipped) {
  121. debug('re-zipping content');
  122. // Zip it again
  123. chain = chain.then(function(result) {
  124. return Q.nfcall(zlib.gzip, result);
  125. });
  126. }
  127. chain.then(_super).fail(function(e) {
  128. debug('injector chain failed, emitting error event');
  129. self.emit('error', e);
  130. });
  131. return true;
  132. }
  133. };
  134. Proto.mixin(mixin, res);
  135. return next();
  136. };
  137. };