Skip to content

Commit 999222d

Browse files
committed
http: fix handling of HTTP upgrades with bodies
Previously, we ignored all indicators of the body (content-length or transfer-encoding headers) and treated any information following the headers as part of the upgraded stream. We now fully process the requests bodies separately instead, allowing us to automatically handle correct parsing of the body like any other HTTP request. This is a breaking change if you are currently accepting HTTP Upgrade requests with request bodies. In this case, before now you will have received the request body and then the upgraded data on the same stream without any distinction or HTTP parsing applies. Now, you will need to separately read the request body from the request (the 1st argument) and the upgraded data from the upgrade stream (the 2nd argument). If you're not interested in request bodies, you can continue to just read from the upgrade stream directly. This change tries to maximize backward compatibility by ensuring the 2nd argument behaves exactly like the underlying socket even in this case. Nonetheless, it is best not to treat the 2nd argument as a socket: instead, use this argument for access to the upgrade stream data, and use request.socket if you need direct access to the underlying socket itself.
1 parent 2bda7cb commit 999222d

7 files changed

+577
-38
lines changed

doc/api/http.md

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ added: v0.1.94
745745
-->
746746

747747
* `response` {http.IncomingMessage}
748-
* `socket` {stream.Duplex}
748+
* `stream` {stream.Duplex}
749749
* `head` {Buffer}
750750

751751
Emitted each time a server responds to a request with an upgrade. If this
@@ -768,13 +768,13 @@ const server = http.createServer((req, res) => {
768768
res.writeHead(200, { 'Content-Type': 'text/plain' });
769769
res.end('okay');
770770
});
771-
server.on('upgrade', (req, socket, head) => {
772-
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
771+
server.on('upgrade', (req, stream, head) => {
772+
stream.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
773773
'Upgrade: WebSocket\r\n' +
774774
'Connection: Upgrade\r\n' +
775775
'\r\n');
776776

777-
socket.pipe(socket); // echo back
777+
stream.pipe(stream); // echo back
778778
});
779779

780780
// Now that server is running
@@ -793,9 +793,9 @@ server.listen(1337, '127.0.0.1', () => {
793793
const req = http.request(options);
794794
req.end();
795795

796-
req.on('upgrade', (res, socket, upgradeHead) => {
796+
req.on('upgrade', (res, stream, upgradeHead) => {
797797
console.log('got upgraded!');
798-
socket.end();
798+
stream.end();
799799
process.exit(0);
800800
});
801801
});
@@ -809,13 +809,13 @@ const server = http.createServer((req, res) => {
809809
res.writeHead(200, { 'Content-Type': 'text/plain' });
810810
res.end('okay');
811811
});
812-
server.on('upgrade', (req, socket, head) => {
813-
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
812+
server.on('upgrade', (req, stream, head) => {
813+
stream.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
814814
'Upgrade: WebSocket\r\n' +
815815
'Connection: Upgrade\r\n' +
816816
'\r\n');
817817

818-
socket.pipe(socket); // echo back
818+
stream.pipe(stream); // echo back
819819
});
820820

821821
// Now that server is running
@@ -834,9 +834,9 @@ server.listen(1337, '127.0.0.1', () => {
834834
const req = http.request(options);
835835
req.end();
836836

837-
req.on('upgrade', (res, socket, upgradeHead) => {
837+
req.on('upgrade', (res, stream, upgradeHead) => {
838838
console.log('got upgraded!');
839-
socket.end();
839+
stream.end();
840840
process.exit(0);
841841
});
842842
});
@@ -1674,6 +1674,14 @@ per connection (in the case of HTTP Keep-Alive connections).
16741674
<!-- YAML
16751675
added: v0.1.94
16761676
changes:
1677+
- version: REPLACEME
1678+
pr-url: https://github.com/nodejs/node/pull/60016
1679+
description: Request bodies are no longer exposed raw (unparsed) on the
1680+
socket argument. Instead, if a body is received, the stream
1681+
argument will be a duplex that emits socket content only
1682+
after the request body, while the parsed request body data
1683+
will be emitted from the request, just as in normal server
1684+
`'request'` events.
16771685
- version:
16781686
- v24.9.0
16791687
- v22.21.0
@@ -1689,31 +1697,38 @@ changes:
16891697

16901698
* `request` {http.IncomingMessage} Arguments for the HTTP request, as it is in
16911699
the [`'request'`][] event
1692-
* `socket` {stream.Duplex} Network socket between the server and client
1700+
* `stream` {stream.Duplex} The upgraded stream between the server and client
16931701
* `head` {Buffer} The first packet of the upgraded stream (may be empty)
16941702

16951703
Emitted each time a client's HTTP upgrade request is accepted. By default
16961704
all HTTP upgrade requests are ignored (i.e. only regular `'request'` events
16971705
are emitted, sticking with the normal HTTP request/response flow) unless you
16981706
listen to this event, in which case they are all accepted (i.e. the `'upgrade'`
16991707
event is emitted instead, and future communication must handled directly
1700-
through the raw socket). You can control this more precisely by using the
1708+
through the raw stream). You can control this more precisely by using the
17011709
server `shouldUpgradeCallback` option.
17021710

17031711
Listening to this event is optional and clients cannot insist on a protocol
17041712
change.
17051713

1706-
After this event is emitted, the request's socket will not have a `'data'`
1707-
event listener, meaning it will need to be bound in order to handle data
1708-
sent to the server on that socket.
1709-
17101714
If an upgrade is accepted by `shouldUpgradeCallback` but no event handler
1711-
is registered then the socket is destroyed, resulting in an immediate
1715+
is registered then the socket will be destroyed, resulting in an immediate
17121716
connection closure for the client.
17131717

1714-
This event is guaranteed to be passed an instance of the {net.Socket} class,
1715-
a subclass of {stream.Duplex}, unless the user specifies a socket
1716-
type other than {net.Socket}.
1718+
In the uncommon case that the incoming request has a body, this body will be
1719+
parsed as normal, separate to the upgrade stream, and the raw stream data will
1720+
only begin after it has completed. To ensure that reading from the stream isn't
1721+
blocked by waiting for the request body to be read, any reads on the stream
1722+
will start the request body flowing automatically. If you want to read the
1723+
request body, ensure that you do so (i.e. you attach `'data'` listeners)
1724+
before starting to read from the upgraded stream.
1725+
1726+
The stream argument will typically be the {net.Socket} instance used by the
1727+
request, but in some cases (such as with a request body) it may be a duplex
1728+
stream (though socket properties are exposed for backward compatibility). If
1729+
required, you can access the raw connection underlying the request via
1730+
[`request.socket`][], which is guaranteed to be an instance of {net.Socket}
1731+
unless the user specified another socket type.
17171732

17181733
### `server.close([callback])`
17191734

lib/_http_server.js

Lines changed: 153 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,19 @@ const {
2626
Error,
2727
MathMin,
2828
NumberIsFinite,
29+
ObjectGetPrototypeOf,
2930
ObjectKeys,
3031
ObjectSetPrototypeOf,
32+
Proxy,
3133
ReflectApply,
34+
ReflectGet,
35+
ReflectSet,
3236
Symbol,
3337
SymbolAsyncDispose,
3438
SymbolFor,
3539
} = primordials;
3640

41+
const { Duplex } = require('stream');
3742
const net = require('net');
3843
const EE = require('events');
3944
const assert = require('internal/assert');
@@ -43,6 +48,7 @@ const {
4348
continueExpression,
4449
chunkExpression,
4550
kIncomingMessage,
51+
kSocket,
4652
HTTPParser,
4753
isLenient,
4854
_checkInvalidHeaderChar: checkInvalidHeaderChar,
@@ -106,6 +112,7 @@ const onResponseFinishChannel = dc.channel('http.server.response.finish');
106112

107113
const kServerResponse = Symbol('ServerResponse');
108114
const kServerResponseStatistics = Symbol('ServerResponseStatistics');
115+
const kUpgradeStream = Symbol('UpgradeStream');
109116

110117
const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');
111118

@@ -953,6 +960,106 @@ function socketOnError(e) {
953960
}
954961
}
955962

963+
class UpgradeStream extends Duplex {
964+
constructor(socket, req) {
965+
super({
966+
allowHalfOpen: socket.allowHalfOpen,
967+
});
968+
969+
this[kSocket] = socket;
970+
this[kIncomingMessage] = req;
971+
972+
// Proxy error, end & closure events immediately.
973+
socket.on('error', (err) => this.destroy(err));
974+
975+
socket.on('close', () => this.destroy());
976+
this.on('close', () => socket.destroy());
977+
978+
socket.on('end', () => {
979+
this.push(null);
980+
981+
// Match the socket behaviour, where 'end' will fire despite no 'data'
982+
// listeners if a socket with no pending data ends:
983+
if (this.readableLength === 0) {
984+
this.resume();
985+
}
986+
});
987+
988+
// Other events (most notably, reading) all only
989+
// activate after requestBodyCompleted is called.
990+
991+
// This stream wraps itself in a proxy which forwards all non-stream
992+
// property lookups back to the underlying socket, for backward
993+
// compatibility.
994+
// eslint-disable-next-line no-constructor-return
995+
return new Proxy(this, {
996+
__proto__: null,
997+
has(target, prop) {
998+
return prop in target || prop in socket;
999+
},
1000+
get(target, prop, receiver) {
1001+
if (prop in target) {
1002+
return ReflectGet(target, prop, receiver);
1003+
}
1004+
1005+
return socket[prop];
1006+
},
1007+
set(target, prop, value, receiver) {
1008+
if (prop in target) {
1009+
return ReflectSet(target, prop, value, receiver);
1010+
}
1011+
1012+
socket[prop] = value;
1013+
return true;
1014+
},
1015+
getPrototypeOf(target) {
1016+
return ObjectGetPrototypeOf(socket);
1017+
},
1018+
});
1019+
}
1020+
1021+
requestBodyCompleted(upgradeHead) {
1022+
this[kIncomingMessage] = null;
1023+
1024+
// When the request body is completed, we begin streaming all the
1025+
// post-body data for the upgraded protocol:
1026+
if (upgradeHead?.length > 0) {
1027+
if (!this.push(upgradeHead)) {
1028+
this[kSocket].pause();
1029+
}
1030+
}
1031+
1032+
this[kSocket].on('data', (data) => {
1033+
if (!this.push(data)) {
1034+
this[kSocket].pause();
1035+
}
1036+
});
1037+
}
1038+
1039+
_read(size) {
1040+
// Reading the upgrade stream starts the request stream flowing. It's
1041+
// important that this happens, even if there are no listeners, or it
1042+
// would be impossible to read this without explicitly reading all the
1043+
// request body first, which is backward incompatible & awkward.
1044+
this[kIncomingMessage]?.resume();
1045+
1046+
this[kSocket].resume();
1047+
}
1048+
1049+
_final(callback) {
1050+
this[kSocket].end(callback);
1051+
}
1052+
1053+
_write(chunk, encoding, callback) {
1054+
this[kSocket].write(chunk, encoding, callback);
1055+
}
1056+
1057+
_destroy(err, callback) {
1058+
this[kSocket].destroy(err);
1059+
callback(err);
1060+
}
1061+
}
1062+
9561063
function onParserExecuteCommon(server, socket, parser, state, ret, d) {
9571064
if (ret instanceof Error) {
9581065
prepareError(ret, parser, d);
@@ -962,28 +1069,56 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) {
9621069
// Upgrade or CONNECT
9631070
const req = parser.incoming;
9641071
debug('SERVER upgrade or connect', req.method);
1072+
const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
1073+
1074+
let upgradeStream;
1075+
if (req.complete) {
1076+
d ||= parser.getCurrentBuffer();
1077+
1078+
socket.removeListener('data', state.onData);
1079+
socket.removeListener('end', state.onEnd);
1080+
socket.removeListener('close', state.onClose);
1081+
socket.removeListener('drain', state.onDrain);
1082+
socket.removeListener('error', socketOnError);
1083+
socket.removeListener('timeout', socketOnTimeout);
1084+
1085+
unconsume(parser, socket);
1086+
parser.finish();
1087+
freeParser(parser, req, socket);
1088+
parser = null;
1089+
1090+
// If the request is complete (no body, or all body read upfront) then
1091+
// we just emit the socket directly as the upgrade stream.
1092+
upgradeStream = socket;
1093+
} else {
1094+
// If the body hasn't been fully parsed yet, we emit immediately but
1095+
// we add a wrapper around the socket to not expose incoming data
1096+
// until the request body has finished.
1097+
1098+
if (socket[kUpgradeStream]) {
1099+
// We've already emitted the incomplete upgrade - nothing do to
1100+
// until actual body parsing completion.
1101+
return;
1102+
}
9651103

966-
d ||= parser.getCurrentBuffer();
1104+
d ||= Buffer.alloc(0);
9671105

968-
socket.removeListener('data', state.onData);
969-
socket.removeListener('end', state.onEnd);
970-
socket.removeListener('close', state.onClose);
971-
socket.removeListener('drain', state.onDrain);
972-
socket.removeListener('error', socketOnError);
973-
socket.removeListener('timeout', socketOnTimeout);
974-
unconsume(parser, socket);
975-
parser.finish();
976-
freeParser(parser, req, socket);
977-
parser = null;
1106+
upgradeStream = new UpgradeStream(socket, req);
1107+
socket[kUpgradeStream] = upgradeStream;
1108+
}
9781109

979-
const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
9801110
if (server.listenerCount(eventName) > 0) {
9811111
debug('SERVER have listener for %s', eventName);
982-
const bodyHead = d.slice(ret, d.length);
9831112

984-
socket.readableFlowing = null;
1113+
const bodyHead = d.slice(ret, d.length);
9851114

986-
server.emit(eventName, req, socket, bodyHead);
1115+
if (req.complete && socket[kUpgradeStream]) {
1116+
// Previously emitted, now completed - just activate the stream
1117+
socket[kUpgradeStream].requestBodyCompleted(bodyHead);
1118+
} else {
1119+
socket.readableFlowing = null;
1120+
server.emit(eventName, req, upgradeStream, bodyHead);
1121+
}
9871122
} else {
9881123
// Got upgrade or CONNECT method, but have no handler.
9891124
socket.destroy();
@@ -1089,8 +1224,9 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
10891224
if (req.upgrade) {
10901225
req.upgrade = req.method === 'CONNECT' ||
10911226
!!server.shouldUpgradeCallback(req);
1092-
if (req.upgrade)
1093-
return 2;
1227+
if (req.upgrade) {
1228+
return 0;
1229+
}
10941230
}
10951231

10961232
state.incoming.push(req);

0 commit comments

Comments
 (0)