mirror of
https://github.com/nodejs/node.git
synced 2024-11-30 07:27:22 +01:00
979d0ca874
Several fields on OutgoingMessage were set after instantiation. These have been included in the constructor to prevent mutation of the object map after instantiation. "name" is now explicitly checked to be a string. Where before if a non-string was passed the following cryptic error was thrown: _http_outgoing.js:334 var key = name.toLowerCase(); ^ TypeError: undefined is not a function Signed-off-by: Trevor Norris <trev.norris@gmail.com>
623 lines
17 KiB
JavaScript
623 lines
17 KiB
JavaScript
// Copyright Joyent, Inc. and other Node contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
// following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
var assert = require('assert').ok;
|
|
var Stream = require('stream');
|
|
var timers = require('timers');
|
|
var util = require('util');
|
|
|
|
var common = require('_http_common');
|
|
|
|
var CRLF = common.CRLF;
|
|
var chunkExpression = common.chunkExpression;
|
|
var debug = common.debug;
|
|
|
|
|
|
var connectionExpression = /Connection/i;
|
|
var transferEncodingExpression = /Transfer-Encoding/i;
|
|
var closeExpression = /close/i;
|
|
var contentLengthExpression = /Content-Length/i;
|
|
var dateExpression = /Date/i;
|
|
var expectExpression = /Expect/i;
|
|
|
|
var automaticHeaders = {
|
|
connection: true,
|
|
'content-length': true,
|
|
'transfer-encoding': true,
|
|
date: true
|
|
};
|
|
|
|
|
|
var dateCache;
|
|
function utcDate() {
|
|
if (!dateCache) {
|
|
var d = new Date();
|
|
dateCache = d.toUTCString();
|
|
timers.enroll(utcDate, 1000 - d.getMilliseconds());
|
|
timers._unrefActive(utcDate);
|
|
}
|
|
return dateCache;
|
|
}
|
|
utcDate._onTimeout = function() {
|
|
dateCache = undefined;
|
|
};
|
|
|
|
|
|
function OutgoingMessage() {
|
|
Stream.call(this);
|
|
|
|
this.output = [];
|
|
this.outputEncodings = [];
|
|
this.outputCallbacks = [];
|
|
|
|
this.writable = true;
|
|
|
|
this._last = false;
|
|
this.chunkedEncoding = false;
|
|
this.shouldKeepAlive = true;
|
|
this.useChunkedEncodingByDefault = true;
|
|
this.sendDate = false;
|
|
this._removedHeader = {};
|
|
|
|
this._hasBody = true;
|
|
this._trailer = '';
|
|
|
|
this.finished = false;
|
|
this._hangupClose = false;
|
|
this._headerSent = false;
|
|
|
|
this.socket = null;
|
|
this.connection = null;
|
|
this._header = null;
|
|
this._headers = null;
|
|
this._headerNames = {};
|
|
}
|
|
util.inherits(OutgoingMessage, Stream);
|
|
|
|
|
|
exports.OutgoingMessage = OutgoingMessage;
|
|
|
|
|
|
OutgoingMessage.prototype.setTimeout = function(msecs, callback) {
|
|
if (callback)
|
|
this.on('timeout', callback);
|
|
if (!this.socket) {
|
|
this.once('socket', function(socket) {
|
|
socket.setTimeout(msecs);
|
|
});
|
|
} else
|
|
this.socket.setTimeout(msecs);
|
|
};
|
|
|
|
|
|
// It's possible that the socket will be destroyed, and removed from
|
|
// any messages, before ever calling this. In that case, just skip
|
|
// it, since something else is destroying this connection anyway.
|
|
OutgoingMessage.prototype.destroy = function(error) {
|
|
if (this.socket)
|
|
this.socket.destroy(error);
|
|
else
|
|
this.once('socket', function(socket) {
|
|
socket.destroy(error);
|
|
});
|
|
};
|
|
|
|
|
|
// This abstract either writing directly to the socket or buffering it.
|
|
OutgoingMessage.prototype._send = function(data, encoding, callback) {
|
|
// This is a shameful hack to get the headers and first body chunk onto
|
|
// the same packet. Future versions of Node are going to take care of
|
|
// this at a lower level and in a more general way.
|
|
if (!this._headerSent) {
|
|
if (util.isString(data) &&
|
|
encoding !== 'hex' &&
|
|
encoding !== 'base64') {
|
|
data = this._header + data;
|
|
} else {
|
|
this.output.unshift(this._header);
|
|
this.outputEncodings.unshift('binary');
|
|
this.outputCallbacks.unshift(null);
|
|
}
|
|
this._headerSent = true;
|
|
}
|
|
return this._writeRaw(data, encoding, callback);
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype._writeRaw = function(data, encoding, callback) {
|
|
if (util.isFunction(encoding)) {
|
|
callback = encoding;
|
|
encoding = null;
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
if (util.isFunction(callback))
|
|
process.nextTick(callback);
|
|
return true;
|
|
}
|
|
|
|
if (this.connection &&
|
|
this.connection._httpMessage === this &&
|
|
this.connection.writable &&
|
|
!this.connection.destroyed) {
|
|
// There might be pending data in the this.output buffer.
|
|
while (this.output.length) {
|
|
if (!this.connection.writable) {
|
|
this._buffer(data, encoding, callback);
|
|
return false;
|
|
}
|
|
var c = this.output.shift();
|
|
var e = this.outputEncodings.shift();
|
|
var cb = this.outputCallbacks.shift();
|
|
this.connection.write(c, e, cb);
|
|
}
|
|
|
|
// Directly write to socket.
|
|
return this.connection.write(data, encoding, callback);
|
|
} else if (this.connection && this.connection.destroyed) {
|
|
// The socket was destroyed. If we're still trying to write to it,
|
|
// then we haven't gotten the 'close' event yet.
|
|
return false;
|
|
} else {
|
|
// buffer, as long as we're not destroyed.
|
|
this._buffer(data, encoding, callback);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype._buffer = function(data, encoding, callback) {
|
|
this.output.push(data);
|
|
this.outputEncodings.push(encoding);
|
|
this.outputCallbacks.push(callback);
|
|
return false;
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
|
|
// firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n'
|
|
// in the case of response it is: 'HTTP/1.1 200 OK\r\n'
|
|
var state = {
|
|
sentConnectionHeader: false,
|
|
sentContentLengthHeader: false,
|
|
sentTransferEncodingHeader: false,
|
|
sentDateHeader: false,
|
|
sentExpect: false,
|
|
messageHeader: firstLine
|
|
};
|
|
|
|
var field, value;
|
|
|
|
if (headers) {
|
|
var keys = Object.keys(headers);
|
|
var isArray = util.isArray(headers);
|
|
var field, value;
|
|
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
var key = keys[i];
|
|
if (isArray) {
|
|
field = headers[key][0];
|
|
value = headers[key][1];
|
|
} else {
|
|
field = key;
|
|
value = headers[key];
|
|
}
|
|
|
|
if (util.isArray(value)) {
|
|
for (var j = 0; j < value.length; j++) {
|
|
storeHeader(this, state, field, value[j]);
|
|
}
|
|
} else {
|
|
storeHeader(this, state, field, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Date header
|
|
if (this.sendDate === true && state.sentDateHeader === false) {
|
|
state.messageHeader += 'Date: ' + utcDate() + CRLF;
|
|
}
|
|
|
|
// Force the connection to close when the response is a 204 No Content or
|
|
// a 304 Not Modified and the user has set a "Transfer-Encoding: chunked"
|
|
// header.
|
|
//
|
|
// RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but
|
|
// node.js used to send out a zero chunk anyway to accommodate clients
|
|
// that don't have special handling for those responses.
|
|
//
|
|
// It was pointed out that this might confuse reverse proxies to the point
|
|
// of creating security liabilities, so suppress the zero chunk and force
|
|
// the connection to close.
|
|
var statusCode = this.statusCode;
|
|
if ((statusCode === 204 || statusCode === 304) &&
|
|
this.chunkedEncoding === true) {
|
|
debug(statusCode + ' response should not use chunked encoding,' +
|
|
' closing connection.');
|
|
this.chunkedEncoding = false;
|
|
this.shouldKeepAlive = false;
|
|
}
|
|
|
|
// keep-alive logic
|
|
if (this._removedHeader.connection) {
|
|
this._last = true;
|
|
this.shouldKeepAlive = false;
|
|
} else if (state.sentConnectionHeader === false) {
|
|
var shouldSendKeepAlive = this.shouldKeepAlive &&
|
|
(state.sentContentLengthHeader ||
|
|
this.useChunkedEncodingByDefault ||
|
|
this.agent);
|
|
if (shouldSendKeepAlive) {
|
|
state.messageHeader += 'Connection: keep-alive\r\n';
|
|
} else {
|
|
this._last = true;
|
|
state.messageHeader += 'Connection: close\r\n';
|
|
}
|
|
}
|
|
|
|
if (state.sentContentLengthHeader === false &&
|
|
state.sentTransferEncodingHeader === false) {
|
|
if (this._hasBody && !this._removedHeader['transfer-encoding']) {
|
|
if (this.useChunkedEncodingByDefault) {
|
|
state.messageHeader += 'Transfer-Encoding: chunked\r\n';
|
|
this.chunkedEncoding = true;
|
|
} else {
|
|
this._last = true;
|
|
}
|
|
} else {
|
|
// Make sure we don't end the 0\r\n\r\n at the end of the message.
|
|
this.chunkedEncoding = false;
|
|
}
|
|
}
|
|
|
|
this._header = state.messageHeader + CRLF;
|
|
this._headerSent = false;
|
|
|
|
// wait until the first body chunk, or close(), is sent to flush,
|
|
// UNLESS we're sending Expect: 100-continue.
|
|
if (state.sentExpect) this._send('');
|
|
};
|
|
|
|
function storeHeader(self, state, field, value) {
|
|
// Protect against response splitting. The if statement is there to
|
|
// minimize the performance impact in the common case.
|
|
if (/[\r\n]/.test(value))
|
|
value = value.replace(/[\r\n]+[ \t]*/g, '');
|
|
|
|
state.messageHeader += field + ': ' + value + CRLF;
|
|
|
|
if (connectionExpression.test(field)) {
|
|
state.sentConnectionHeader = true;
|
|
if (closeExpression.test(value)) {
|
|
self._last = true;
|
|
} else {
|
|
self.shouldKeepAlive = true;
|
|
}
|
|
|
|
} else if (transferEncodingExpression.test(field)) {
|
|
state.sentTransferEncodingHeader = true;
|
|
if (chunkExpression.test(value)) self.chunkedEncoding = true;
|
|
|
|
} else if (contentLengthExpression.test(field)) {
|
|
state.sentContentLengthHeader = true;
|
|
} else if (dateExpression.test(field)) {
|
|
state.sentDateHeader = true;
|
|
} else if (expectExpression.test(field)) {
|
|
state.sentExpect = true;
|
|
}
|
|
}
|
|
|
|
|
|
OutgoingMessage.prototype.setHeader = function(name, value) {
|
|
if (typeof name !== 'string')
|
|
throw new TypeError('"name" should be a string');
|
|
if (value === undefined)
|
|
throw new Error('"name" and "value" are required for setHeader().');
|
|
if (this._header)
|
|
throw new Error('Can\'t set headers after they are sent.');
|
|
|
|
if (this._headers === null)
|
|
this._headers = {};
|
|
|
|
var key = name.toLowerCase();
|
|
this._headers[key] = value;
|
|
this._headerNames[key] = name;
|
|
|
|
if (automaticHeaders[key])
|
|
this._removedHeader[key] = false;
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype.getHeader = function(name) {
|
|
if (arguments.length < 1) {
|
|
throw new Error('`name` is required for getHeader().');
|
|
}
|
|
|
|
if (!this._headers) return;
|
|
|
|
var key = name.toLowerCase();
|
|
return this._headers[key];
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype.removeHeader = function(name) {
|
|
if (arguments.length < 1) {
|
|
throw new Error('`name` is required for removeHeader().');
|
|
}
|
|
|
|
if (this._header) {
|
|
throw new Error('Can\'t remove headers after they are sent.');
|
|
}
|
|
|
|
var key = name.toLowerCase();
|
|
|
|
if (key === 'date')
|
|
this.sendDate = false;
|
|
else if (automaticHeaders[key])
|
|
this._removedHeader[key] = true;
|
|
|
|
if (this._headers) {
|
|
delete this._headers[key];
|
|
delete this._headerNames[key];
|
|
}
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype._renderHeaders = function() {
|
|
if (this._header) {
|
|
throw new Error('Can\'t render headers after they are sent to the client.');
|
|
}
|
|
|
|
if (!this._headers) return {};
|
|
|
|
var headers = {};
|
|
var keys = Object.keys(this._headers);
|
|
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
var key = keys[i];
|
|
headers[this._headerNames[key]] = this._headers[key];
|
|
}
|
|
return headers;
|
|
};
|
|
|
|
|
|
Object.defineProperty(OutgoingMessage.prototype, 'headersSent', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: function() { return !!this._header; }
|
|
});
|
|
|
|
|
|
OutgoingMessage.prototype.write = function(chunk, encoding, callback) {
|
|
var self = this;
|
|
|
|
if (this.finished) {
|
|
var err = new Error('write after end');
|
|
process.nextTick(function() {
|
|
self.emit('error', err);
|
|
if (callback) callback(err);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
if (!this._header) {
|
|
this._implicitHeader();
|
|
}
|
|
|
|
if (!this._hasBody) {
|
|
debug('This type of response MUST NOT have a body. ' +
|
|
'Ignoring write() calls.');
|
|
return true;
|
|
}
|
|
|
|
if (!util.isString(chunk) && !util.isBuffer(chunk)) {
|
|
throw new TypeError('first argument must be a string or Buffer');
|
|
}
|
|
|
|
|
|
// If we get an empty string or buffer, then just do nothing, and
|
|
// signal the user to keep writing.
|
|
if (chunk.length === 0) return true;
|
|
|
|
var len, ret;
|
|
if (this.chunkedEncoding) {
|
|
if (util.isString(chunk) &&
|
|
encoding !== 'hex' &&
|
|
encoding !== 'base64' &&
|
|
encoding !== 'binary') {
|
|
len = Buffer.byteLength(chunk, encoding);
|
|
chunk = len.toString(16) + CRLF + chunk + CRLF;
|
|
ret = this._send(chunk, encoding, callback);
|
|
} else {
|
|
// buffer, or a non-toString-friendly encoding
|
|
if (util.isString(chunk))
|
|
len = Buffer.byteLength(chunk, encoding);
|
|
else
|
|
len = chunk.length;
|
|
|
|
if (this.connection && !this.connection.corked) {
|
|
this.connection.cork();
|
|
var conn = this.connection;
|
|
process.nextTick(function connectionCork() {
|
|
if (conn)
|
|
conn.uncork();
|
|
});
|
|
}
|
|
this._send(len.toString(16), 'binary', null);
|
|
this._send(crlf_buf, null, null);
|
|
this._send(chunk, encoding, null);
|
|
ret = this._send(crlf_buf, null, callback);
|
|
}
|
|
} else {
|
|
ret = this._send(chunk, encoding, callback);
|
|
}
|
|
|
|
debug('write ret = ' + ret);
|
|
return ret;
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype.addTrailers = function(headers) {
|
|
this._trailer = '';
|
|
var keys = Object.keys(headers);
|
|
var isArray = util.isArray(headers);
|
|
var field, value;
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
var key = keys[i];
|
|
if (isArray) {
|
|
field = headers[key][0];
|
|
value = headers[key][1];
|
|
} else {
|
|
field = key;
|
|
value = headers[key];
|
|
}
|
|
|
|
this._trailer += field + ': ' + value + CRLF;
|
|
}
|
|
};
|
|
|
|
|
|
var crlf_buf = new Buffer('\r\n');
|
|
|
|
|
|
OutgoingMessage.prototype.end = function(data, encoding, callback) {
|
|
if (util.isFunction(data)) {
|
|
callback = data;
|
|
data = null;
|
|
} else if (util.isFunction(encoding)) {
|
|
callback = encoding;
|
|
encoding = null;
|
|
}
|
|
|
|
if (data && !util.isString(data) && !util.isBuffer(data)) {
|
|
throw new TypeError('first argument must be a string or Buffer');
|
|
}
|
|
|
|
if (this.finished) {
|
|
return false;
|
|
}
|
|
|
|
var self = this;
|
|
function finish() {
|
|
self.emit('finish');
|
|
}
|
|
|
|
if (util.isFunction(callback))
|
|
this.once('finish', callback);
|
|
|
|
|
|
if (!this._header) {
|
|
this._implicitHeader();
|
|
}
|
|
|
|
if (data && !this._hasBody) {
|
|
debug('This type of response MUST NOT have a body. ' +
|
|
'Ignoring data passed to end().');
|
|
data = null;
|
|
}
|
|
|
|
if (this.connection && data)
|
|
this.connection.cork();
|
|
|
|
var ret;
|
|
if (data) {
|
|
// Normal body write.
|
|
ret = this.write(data, encoding);
|
|
}
|
|
|
|
if (this._hasBody && this.chunkedEncoding) {
|
|
ret = this._send('0\r\n' + this._trailer + '\r\n', 'binary', finish);
|
|
} else {
|
|
// Force a flush, HACK.
|
|
ret = this._send('', 'binary', finish);
|
|
}
|
|
|
|
if (this.connection && data)
|
|
this.connection.uncork();
|
|
|
|
this.finished = true;
|
|
|
|
// There is the first message on the outgoing queue, and we've sent
|
|
// everything to the socket.
|
|
debug('outgoing message end.');
|
|
if (this.output.length === 0 && this.connection._httpMessage === this) {
|
|
this._finish();
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype._finish = function() {
|
|
assert(this.connection);
|
|
this.emit('prefinish');
|
|
};
|
|
|
|
|
|
// This logic is probably a bit confusing. Let me explain a bit:
|
|
//
|
|
// In both HTTP servers and clients it is possible to queue up several
|
|
// outgoing messages. This is easiest to imagine in the case of a client.
|
|
// Take the following situation:
|
|
//
|
|
// req1 = client.request('GET', '/');
|
|
// req2 = client.request('POST', '/');
|
|
//
|
|
// When the user does
|
|
//
|
|
// req2.write('hello world\n');
|
|
//
|
|
// it's possible that the first request has not been completely flushed to
|
|
// the socket yet. Thus the outgoing messages need to be prepared to queue
|
|
// up data internally before sending it on further to the socket's queue.
|
|
//
|
|
// This function, outgoingFlush(), is called by both the Server and Client
|
|
// to attempt to flush any pending messages out to the socket.
|
|
OutgoingMessage.prototype._flush = function() {
|
|
if (this.socket && this.socket.writable) {
|
|
var ret;
|
|
while (this.output.length) {
|
|
var data = this.output.shift();
|
|
var encoding = this.outputEncodings.shift();
|
|
var cb = this.outputCallbacks.shift();
|
|
ret = this.socket.write(data, encoding, cb);
|
|
}
|
|
|
|
if (this.finished) {
|
|
// This is a queue to the server or client to bring in the next this.
|
|
this._finish();
|
|
} else if (ret) {
|
|
// This is necessary to prevent https from breaking
|
|
this.emit('drain');
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
OutgoingMessage.prototype.flush = function() {
|
|
if (!this._header) {
|
|
// Force-flush the headers.
|
|
this._implicitHeader();
|
|
this._send('');
|
|
}
|
|
};
|