Skip to content

Commit

Permalink
fix: handle edge-cases when calculating body length
Browse files Browse the repository at this point in the history
  • Loading branch information
sidwebworks committed Aug 26, 2022
1 parent b4ea117 commit 5e1a5e3
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 61 deletions.
3 changes: 1 addition & 2 deletions lib/_http_incoming.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,5 @@ function onError(self, error, cb) {
module.exports = {
IncomingMessage,
readStart,
readStop,
kHeaders
readStop
};
70 changes: 36 additions & 34 deletions lib/_http_outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ const {
SafeSet,
StringPrototypeToLowerCase,
Symbol,
NumberPOSITIVE_INFINITY,
NumberParseInt
MathAbs
} = primordials;

const { getDefaultHighWaterMark } = require('internal/streams/state');
Expand Down Expand Up @@ -78,7 +77,6 @@ const {
} = require('internal/errors');
const { validateString } = require('internal/validators');
const { isUint8Array } = require('internal/util/types');
const { kHeaders } = require('_http_incoming');

let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
debug = fn;
Expand All @@ -89,7 +87,7 @@ const HIGH_WATER_MARK = getDefaultHighWaterMark();
const kCorked = Symbol('corked');
const kUniqueHeaders = Symbol('kUniqueHeaders');
const kBytesWritten = Symbol('kBytesWritten');
const kContentLength = Symbol('kContentLength');
const kEndCalled = Symbol('kEndCalled');

const nop = () => {};

Expand Down Expand Up @@ -130,7 +128,7 @@ function OutgoingMessage() {
this._removedTE = false;

this[kBytesWritten] = 0;
this[kContentLength] = null;
this[kEndCalled] = false;
this._contentLength = null;
this._hasBody = true;
this._trailer = '';
Expand Down Expand Up @@ -338,7 +336,9 @@ OutgoingMessage.prototype._send = function _send(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 (!this._headerSent && this._header != null) {
// `this._header` can be null if OutgoingMessage is used without a proper Socket
// See: /test/parallel/test-http-outgoing-message-inheritance.js
if (typeof data === 'string' &&
(encoding === 'utf8' || encoding === 'latin1' || !encoding)) {
data = this._header + data;
Expand All @@ -353,19 +353,17 @@ OutgoingMessage.prototype._send = function _send(data, encoding, callback) {
this._onPendingData(header.length);
}
this._headerSent = true;
this[kContentLength] = _getContentLength.call(this);
}
return this._writeRaw(data, encoding, callback);
};

function _getContentLength() {
const outgoing = this.getHeader('content-length');
const incoming = this.req;
if (outgoing != null) return NumberParseInt(outgoing);
if (incoming && incoming[kHeaders] && !!incoming[kHeaders]['content-length']) {
return NumberParseInt(incoming[kHeaders]['content-length']);
}
return NumberPOSITIVE_INFINITY;
function _getMessageBodySize(chunk, headers, encoding) {
const headerLength = headers ? headers.length : 0;
const chunkLength = chunk ? Buffer.byteLength(chunk, encoding) : 0;
if (Buffer.isBuffer(chunk)) return chunkLength;
if (headerLength === chunkLength) return 0;
if (headerLength < chunkLength) return MathAbs(chunkLength - headerLength);
return chunkLength;
}

OutgoingMessage.prototype._writeRaw = _writeRaw;
Expand All @@ -382,6 +380,27 @@ function _writeRaw(data, encoding, callback) {
encoding = null;
}

if (conn && conn.writable && !this._removedContLen && this._hasBody) {
const shouldContinue = conn._httpMessage.statusCode !== 304 && !this.getHeader('transfer-encoding');

if (typeof this._contentLength === 'number' && shouldContinue) {
const size = _getMessageBodySize(data, conn._httpMessage._header, encoding);

console.log({data,size, _contentLength: this._contentLength, written: this[kBytesWritten]});
console.log("\n*********************************\n");

if ((size + this[kBytesWritten]) > this._contentLength) {
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(size + this[kBytesWritten], this._contentLength);
}

if (this[kEndCalled] && (size + this[kBytesWritten]) !== this._contentLength) {
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(size + this[kBytesWritten], this._contentLength);
}

this[kBytesWritten] += size;
}
}

if (conn && conn._httpMessage === this && conn.writable) {
// There might be pending data in the this.output buffer.
if (this.outputData.length) {
Expand Down Expand Up @@ -552,9 +571,6 @@ function processHeader(self, state, key, value, validate) {
function storeHeader(self, state, key, value, validate) {
if (validate)
validateHeaderValue(key, value);
if (StringPrototypeToLowerCase(key) === 'content-length') {
self[kContentLength] = value;
}
state.header += key + ': ' + value + '\r\n';
matchHeader(self, state, key, value);
}
Expand All @@ -580,6 +596,7 @@ function matchHeader(self, state, field, value) {
break;
case 'content-length':
state.contLen = true;
self._contentLength = value;
self._removedContLen = false;
break;
case 'date':
Expand Down Expand Up @@ -835,17 +852,6 @@ function write_(msg, chunk, encoding, callback, fromEnd) {
err = new ERR_STREAM_DESTROYED('write');
}

if (msg && !msg.destroyed) {
const byteLength = Buffer.byteLength(chunk, encoding);
if (msg[kContentLength] > 0 && msg[kContentLength] !== NumberPOSITIVE_INFINITY) {
const fullSize = byteLength + msg[kBytesWritten];
if (fullSize > msg[kContentLength]) {
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(fullSize, msg[kContentLength]);
}
}
msg[kBytesWritten] += byteLength;
}

if (err) {
if (!msg.destroyed) {
onError(msg, err, callback);
Expand Down Expand Up @@ -955,11 +961,7 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
encoding = null;
}

const size = (chunk ? Buffer.byteLength(chunk, encoding) : 0) + this[kBytesWritten];

if (this[kContentLength] !== null && size !== this[kContentLength]) {
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(size, this[kContentLength]);
}
this[kEndCalled] = true;

if (chunk) {
if (this.finished) {
Expand Down
36 changes: 11 additions & 25 deletions test/parallel/test-http-content-length-mismatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const common = require('../common');
const assert = require('assert');
const http = require('http');

function shouldThrowOnMismatch() {
function shouldThrowOnMoreBytes() {
const server = http.createServer(common.mustCall((req, res) => {
res.setHeader('Content-Length', 5);
assert.throws(() => {
Expand All @@ -18,74 +18,60 @@ function shouldThrowOnMismatch() {
}));

server.listen(0, () => {
http.get({
const req = http.get({
port: server.address().port,
}, common.mustCall((res) => {
console.log(res.statusMessage);
res.resume();
assert.strictEqual(res.statusCode, 200);
server.close();
}));
req.end()
});
}

function shouldNotThrow() {
const server = http.createServer(common.mustCall((req, res) => {
res.write('hello');
res.write('helloaa');
res.statusCode = 200;
res.end('a');
res.end('ending');
}));

server.listen(0, () => {
http.get({
port: server.address().port,
headers: {
'Content-Length': '6'
}
}, common.mustCall((res) => {
console.log(res.statusMessage);
res.resume();
assert.strictEqual(res.statusCode, 200);
server.close();
}));
});
}

function shouldOverwriteContentLength() {
const server = http.createServer(common.mustCall((req, res) => {
res.writeHead(200, {
'Content-Length': '1'
});

function shouldThrowOnFewerBytes() {
const server = http.createServer(common.mustCall((req, res) => {
res.setHeader('Content-Length', 5);
assert.throws(() => {
res.write('hello');
res.write('a');
res.statusCode = 200;
}, {
code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH'
});
assert.throws(() => {
res.end();
}, {
code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH'
});
res.end('aaaa');
}));

server.listen(0, () => {
http.get({
port: server.address().port,
headers: {
'Content-Length': '6'
}
}, common.mustCall((res) => {
console.log(res.statusMessage);
res.resume();
assert.strictEqual(res.statusCode, 200);
server.close();
}));
});
}

shouldThrowOnMismatch();
shouldThrowOnMoreBytes();
shouldNotThrow();
shouldOverwriteContentLength();
shouldThrowOnFewerBytes();

0 comments on commit 5e1a5e3

Please sign in to comment.