diff --git a/lib/_http_client.js b/lib/_http_client.js index 18a58936ee..8e0da8a37c 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -131,8 +131,12 @@ function ClientRequest(options, cb) { self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n', options.headers); } else if (self.getHeader('expect')) { + if (self._header) { + throw new Error('Can\'t render headers after they are sent to the ' + + 'client'); + } self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n', - self._renderHeaders()); + self._headers); } this._ended = false; @@ -224,8 +228,11 @@ ClientRequest.prototype._finish = function _finish() { }; ClientRequest.prototype._implicitHeader = function _implicitHeader() { + if (this._header) { + throw new Error('Can\'t render headers after they are sent to the client'); + } this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', - this._renderHeaders()); + this._headers); }; ClientRequest.prototype.abort = function abort() { diff --git a/lib/_http_common.js b/lib/_http_common.js index 2ce523fe62..f585d97d7b 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -14,8 +14,8 @@ const debug = require('util').debuglog('http'); exports.debug = debug; exports.CRLF = '\r\n'; -exports.chunkExpression = /chunk/i; -exports.continueExpression = /100-continue/i; +exports.chunkExpression = /(?:^|\W)chunked(?:$|\W)/i; +exports.continueExpression = /(?:^|\W)100-continue(?:$|\W)/i; exports.methods = methods; const kOnHeaders = HTTPParser.kOnHeaders | 0; diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index bf018d988d..52b3a9006c 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -11,18 +11,8 @@ const checkIsHttpToken = common._checkIsHttpToken; const checkInvalidHeaderChar = common._checkInvalidHeaderChar; const CRLF = common.CRLF; -const trfrEncChunkExpression = common.chunkExpression; const debug = common.debug; -const upgradeExpression = /^Upgrade$/i; -const transferEncodingExpression = /^Transfer-Encoding$/i; -const contentLengthExpression = /^Content-Length$/i; -const dateExpression = /^Date$/i; -const expectExpression = /^Expect$/i; -const trailerExpression = /^Trailer$/i; -const connectionExpression = /^Connection$/i; -const connCloseExpression = /(^|\W)close(\W|$)/i; -const connUpgradeExpression = /(^|\W)upgrade(\W|$)/i; const automaticHeaders = { connection: true, @@ -31,6 +21,10 @@ const automaticHeaders = { date: true }; +var RE_FIELDS = new RegExp('^(?:Connection|Transfer-Encoding|Content-Length|' + + 'Date|Expect|Trailer|Upgrade)$', 'i'); +var RE_CONN_VALUES = /(?:^|\W)close|upgrade(?:$|\W)/ig; +var RE_TE_CHUNKED = common.chunkExpression; var dateCache; function utcDate() { @@ -83,7 +77,6 @@ function OutgoingMessage() { this.connection = null; this._header = null; this._headers = null; - this._headerNames = {}; this._onPendingData = null; } @@ -198,57 +191,72 @@ function _storeHeader(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, - sentConnectionUpgrade: false, - sentContentLengthHeader: false, - sentTransferEncodingHeader: false, - sentDateHeader: false, - sentExpect: false, - sentTrailer: false, - sentUpgrade: false, - messageHeader: firstLine + connection: false, + connUpgrade: false, + contLen: false, + te: false, + date: false, + expect: false, + trailer: false, + upgrade: false, + header: firstLine }; - var i; - var j; var field; + var key; var value; - if (headers instanceof Array) { - for (i = 0; i < headers.length; ++i) { + var i; + var j; + if (headers === this._headers) { + for (key in headers) { + var entry = headers[key]; + field = entry[0]; + value = entry[1]; + + if (value instanceof Array) { + for (j = 0; j < value.length; j++) { + storeHeader(this, state, field, value[j], false); + } + } else { + storeHeader(this, state, field, value, false); + } + } + } else if (headers instanceof Array) { + for (i = 0; i < headers.length; i++) { field = headers[i][0]; value = headers[i][1]; if (value instanceof Array) { for (j = 0; j < value.length; j++) { - storeHeader(this, state, field, value[j]); + storeHeader(this, state, field, value[j], true); } } else { - storeHeader(this, state, field, value); + storeHeader(this, state, field, value, true); } } } else if (headers) { var keys = Object.keys(headers); - for (i = 0; i < keys.length; ++i) { + for (i = 0; i < keys.length; i++) { field = keys[i]; value = headers[field]; if (value instanceof Array) { for (j = 0; j < value.length; j++) { - storeHeader(this, state, field, value[j]); + storeHeader(this, state, field, value[j], true); } } else { - storeHeader(this, state, field, value); + storeHeader(this, state, field, value, true); } } } // Are we upgrading the connection? - if (state.sentConnectionUpgrade && state.sentUpgrade) + if (state.connUpgrade && state.upgrade) this.upgrading = true; // Date header - if (this.sendDate && !state.sentDateHeader) { - state.messageHeader += 'Date: ' + utcDate() + CRLF; + if (this.sendDate && !state.date) { + state.header += 'Date: ' + utcDate() + CRLF; } // Force the connection to close when the response is a 204 No Content or @@ -274,33 +282,30 @@ function _storeHeader(firstLine, headers) { if (this._removedHeader.connection) { this._last = true; this.shouldKeepAlive = false; - } else if (!state.sentConnectionHeader) { + } else if (!state.connection) { var shouldSendKeepAlive = this.shouldKeepAlive && - (state.sentContentLengthHeader || - this.useChunkedEncodingByDefault || - this.agent); + (state.contLen || this.useChunkedEncodingByDefault || this.agent); if (shouldSendKeepAlive) { - state.messageHeader += 'Connection: keep-alive\r\n'; + state.header += 'Connection: keep-alive\r\n'; } else { this._last = true; - state.messageHeader += 'Connection: close\r\n'; + state.header += 'Connection: close\r\n'; } } - if (!state.sentContentLengthHeader && !state.sentTransferEncodingHeader) { + if (!state.contLen && !state.te) { if (!this._hasBody) { // Make sure we don't end the 0\r\n\r\n at the end of the message. this.chunkedEncoding = false; } else if (!this.useChunkedEncodingByDefault) { this._last = true; } else { - if (!state.sentTrailer && !this._removedHeader['content-length'] && + if (!state.trailer && typeof this._contentLength === 'number') { - state.messageHeader += 'Content-Length: ' + this._contentLength + - '\r\n'; } else if (!this._removedHeader['transfer-encoding']) { - state.messageHeader += 'Transfer-Encoding: chunked\r\n'; + state.header += 'Content-Length: ' + this._contentLength + CRLF; + state.header += 'Transfer-Encoding: chunked\r\n'; this.chunkedEncoding = true; } else { // We should only be able to get here if both Content-Length and @@ -311,70 +316,94 @@ function _storeHeader(firstLine, headers) { } } - this._header = state.messageHeader + CRLF; + this._header = state.header + 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(''); + if (state.expect) this._send(''); } -function storeHeader(self, state, field, value) { - if (!checkIsHttpToken(field)) { - throw new TypeError( - 'Header name must be a valid HTTP Token ["' + field + '"]'); - } - if (checkInvalidHeaderChar(value)) { - debug('Header "%s" contains invalid characters', field); - throw new TypeError('The header content contains invalid characters'); - } - state.messageHeader += field + ': ' + escapeHeaderValue(value) + CRLF; - - if (connectionExpression.test(field)) { - state.sentConnectionHeader = true; - if (connCloseExpression.test(value)) { - self._last = true; - } else { - self.shouldKeepAlive = true; +function storeHeader(self, state, field, value, validate) { + if (validate) { + if (!checkIsHttpToken(field)) { + throw new TypeError( + 'Header name must be a valid HTTP Token ["' + field + '"]'); + } + if (value === undefined) { + throw new Error('Header "%s" value must not be undefined', field); + } else if (checkInvalidHeaderChar(value)) { + debug('Header "%s" contains invalid characters', field); + throw new TypeError('The header content contains invalid characters'); } - if (connUpgradeExpression.test(value)) - state.sentConnectionUpgrade = true; - } else if (transferEncodingExpression.test(field)) { - state.sentTransferEncodingHeader = true; - if (trfrEncChunkExpression.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; - } else if (trailerExpression.test(field)) { - state.sentTrailer = true; - } else if (upgradeExpression.test(field)) { - state.sentUpgrade = true; } + state.header += field + ': ' + escapeHeaderValue(value) + CRLF; + matchHeader(self, state, field, value); } +function matchConnValue(self, state, value) { + var sawClose = false; + var m = RE_CONN_VALUES.exec(value); + while (m) { + if (m[0].length === 5) + sawClose = true; + else + state.connUpgrade = true; + m = RE_CONN_VALUES.exec(value); + } + if (sawClose) + self._last = true; + else + self.shouldKeepAlive = true; +} -OutgoingMessage.prototype.setHeader = function setHeader(name, value) { +function matchHeader(self, state, field, value) { + var m = RE_FIELDS.exec(field); + if (!m) + return; + var len = m[0].length; + if (len === 10) { + state.connection = true; + matchConnValue(self, state, value); + } else if (len === 17) { + state.te = true; + if (RE_TE_CHUNKED.test(value)) self.chunkedEncoding = true; + } else if (len === 14) { + state.contLen = true; + } else if (len === 4) { + state.date = true; + } else if (len === 6) { + state.expect = true; + } else if (len === 7) { + var ch = m[0].charCodeAt(0); + if (ch === 85 || ch === 117) + state.upgrade = true; + else + state.trailer = true; + } +} + +function validateHeader(msg, name, value) { if (!checkIsHttpToken(name)) throw new TypeError( 'Header name must be a valid HTTP Token ["' + name + '"]'); if (value === undefined) throw new Error('"value" required in setHeader("' + name + '", value)'); - if (this._header) + if (msg._header) throw new Error('Can\'t set headers after they are sent.'); if (checkInvalidHeaderChar(value)) { debug('Header "%s" contains invalid characters', name); throw new TypeError('The header content contains invalid characters'); } - if (this._headers === null) +} +OutgoingMessage.prototype.setHeader = function setHeader(name, value) { + validateHeader(this, name, value); + + if (!this._headers) this._headers = {}; - var key = name.toLowerCase(); - this._headers[key] = value; - this._headerNames[key] = name; + const key = name.toLowerCase(); + this._headers[key] = [name, value]; if (automaticHeaders[key]) this._removedHeader[key] = false; @@ -388,7 +417,10 @@ OutgoingMessage.prototype.getHeader = function getHeader(name) { if (!this._headers) return; - return this._headers[name.toLowerCase()]; + var entry = this._headers[name.toLowerCase()]; + if (!entry) + return; + return entry[1]; }; @@ -410,30 +442,10 @@ OutgoingMessage.prototype.removeHeader = function removeHeader(name) { if (this._headers) { delete this._headers[key]; - delete this._headerNames[key]; } }; -OutgoingMessage.prototype._renderHeaders = function _renderHeaders() { - if (this._header) { - throw new Error('Can\'t render headers after they are sent to the client'); - } - - var headersMap = this._headers; - if (!headersMap) return {}; - - var headers = {}; - var keys = Object.keys(headersMap); - var headerNames = this._headerNames; - - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - headers[headerNames[key]] = headersMap[key]; - } - return headers; -}; - OutgoingMessage.prototype._implicitHeader = function _implicitHeader() { throw new Error('_implicitHeader() method is not implemented'); }; @@ -492,6 +504,7 @@ OutgoingMessage.prototype.write = function write(chunk, encoding, callback) { this.connection.cork(); process.nextTick(connectionCorkNT, this.connection); } + this._send(len.toString(16), 'latin1', null); this._send(crlf_buf, null, null); this._send(chunk, encoding, null); diff --git a/lib/_http_server.js b/lib/_http_server.js index fe563977a6..d473fd3281 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -161,7 +161,6 @@ ServerResponse.prototype._implicitHeader = function _implicitHeader() { ServerResponse.prototype.writeHead = writeHead; function writeHead(statusCode, reason, obj) { - var headers; statusCode |= 0; if (statusCode < 100 || statusCode > 999) throw new RangeError(`Invalid status code: ${statusCode}`); @@ -177,17 +176,25 @@ function writeHead(statusCode, reason, obj) { } this.statusCode = statusCode; + var headers; if (this._headers) { // Slow-case: when progressive API and header fields are passed. + var k; if (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { - var k = keys[i]; + k = keys[i]; if (k) this.setHeader(k, obj[k]); } } + if (k === undefined) { + if (this._header) { + throw new Error('Can\'t render headers after they are sent to the ' + + 'client'); + } + } // only progressive api is used - headers = this._renderHeaders(); + headers = this._headers; } else { // only writeHead() called headers = obj;