-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
Fix handling of HTTP upgrades with bodies #60016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ const { | |
| SymbolFor, | ||
| } = primordials; | ||
|
|
||
| const { Duplex } = require('stream'); | ||
| const net = require('net'); | ||
| const EE = require('events'); | ||
| const assert = require('internal/assert'); | ||
|
|
@@ -43,6 +44,7 @@ const { | |
| continueExpression, | ||
| chunkExpression, | ||
| kIncomingMessage, | ||
| kSocket, | ||
| HTTPParser, | ||
| isLenient, | ||
| _checkInvalidHeaderChar: checkInvalidHeaderChar, | ||
|
|
@@ -106,6 +108,7 @@ const onResponseFinishChannel = dc.channel('http.server.response.finish'); | |
|
|
||
| const kServerResponse = Symbol('ServerResponse'); | ||
| const kServerResponseStatistics = Symbol('ServerResponseStatistics'); | ||
| const kUpgradeStream = Symbol('UpgradeStream'); | ||
|
|
||
| const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption'); | ||
|
|
||
|
|
@@ -953,6 +956,77 @@ function socketOnError(e) { | |
| } | ||
| } | ||
|
|
||
| class UpgradeStream extends Duplex { | ||
| constructor(socket, req) { | ||
| super({ | ||
| allowHalfOpen: socket.allowHalfOpen, | ||
| }); | ||
|
|
||
| this[kSocket] = socket; | ||
| this[kIncomingMessage] = req; | ||
|
|
||
| // Proxy error, end & closure events immediately. | ||
| socket.on('error', (err) => this.destroy(err)); | ||
|
|
||
| socket.on('close', () => this.destroy()); | ||
| this.on('close', () => socket.destroy()); | ||
|
|
||
| socket.on('end', () => { | ||
| this.push(null); | ||
|
|
||
| // Match the socket behaviour, where 'end' will fire despite no 'data' | ||
| // listeners if a socket with no pending data ends: | ||
| if (this.readableLength === 0) { | ||
| this.resume(); | ||
| } | ||
| }); | ||
|
|
||
| // Other events (most notably, reading) all only | ||
| // activate after requestBodyCompleted is called. | ||
| } | ||
|
|
||
| requestBodyCompleted(upgradeHead) { | ||
| this[kIncomingMessage] = null; | ||
|
|
||
| // When the request body is completed, we begin streaming all the | ||
| // post-body data for the upgraded protocol: | ||
| if (upgradeHead?.length > 0) { | ||
| if (!this.push(upgradeHead)) { | ||
| this[kSocket].pause(); | ||
| } | ||
| } | ||
|
|
||
| this[kSocket].on('data', (data) => { | ||
| if (!this.push(data)) { | ||
| this[kSocket].pause(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| _read(size) { | ||
| // Reading the upgrade stream starts the request stream flowing. It's | ||
| // important that this happens, even if there are no listeners, or it | ||
| // would be impossible to read this without explicitly reading all the | ||
| // request body first, which is backward incompatible & awkward. | ||
| this[kIncomingMessage]?.resume(); | ||
|
|
||
| this[kSocket].resume(); | ||
| } | ||
|
|
||
| _final(callback) { | ||
| this[kSocket].end(callback); | ||
| } | ||
|
|
||
| _write(chunk, encoding, callback) { | ||
| this[kSocket].write(chunk, encoding, callback); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I remember correctly the callback is always called asynchronously even if the write is synchronous so there will be a small overhead over a raw
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, there is definitely a small performance hit here. I doubt it will be significant for most cases, but it's not zero. I'm open to ideas to improve that perf, but I think it's worth it for the correctness in this case, and the intention is that it only applies in this upgrade-with-body case, so it doesn't affect any non-body upgrade requests at all. |
||
| } | ||
|
|
||
| _destroy(err, callback) { | ||
| this[kSocket].destroy(err); | ||
| callback(err); | ||
| } | ||
| } | ||
|
|
||
| function onParserExecuteCommon(server, socket, parser, state, ret, d) { | ||
| if (ret instanceof Error) { | ||
| prepareError(ret, parser, d); | ||
|
|
@@ -962,28 +1036,56 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) { | |
| // Upgrade or CONNECT | ||
| const req = parser.incoming; | ||
| debug('SERVER upgrade or connect', req.method); | ||
| const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; | ||
|
|
||
| let upgradeStream; | ||
| if (req.complete) { | ||
| d ||= parser.getCurrentBuffer(); | ||
|
|
||
| socket.removeListener('data', state.onData); | ||
| socket.removeListener('end', state.onEnd); | ||
| socket.removeListener('close', state.onClose); | ||
| socket.removeListener('drain', state.onDrain); | ||
| socket.removeListener('error', socketOnError); | ||
| socket.removeListener('timeout', socketOnTimeout); | ||
|
|
||
| unconsume(parser, socket); | ||
| parser.finish(); | ||
| freeParser(parser, req, socket); | ||
| parser = null; | ||
|
|
||
| // If the request is complete (no body, or all body read upfront) then | ||
| // we just emit the socket directly as the upgrade stream. | ||
| upgradeStream = socket; | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } else { | ||
| // If the body hasn't been fully parsed yet, we emit immediately but | ||
| // we add a wrapper around the socket to not expose incoming data | ||
| // until the request body has finished. | ||
|
|
||
| if (socket[kUpgradeStream]) { | ||
| // We've already emitted the incomplete upgrade - nothing do to | ||
| // until actual body parsing completion. | ||
| return; | ||
| } | ||
|
|
||
| d ||= parser.getCurrentBuffer(); | ||
| d ||= Buffer.alloc(0); | ||
|
|
||
| socket.removeListener('data', state.onData); | ||
| socket.removeListener('end', state.onEnd); | ||
| socket.removeListener('close', state.onClose); | ||
| socket.removeListener('drain', state.onDrain); | ||
| socket.removeListener('error', socketOnError); | ||
| socket.removeListener('timeout', socketOnTimeout); | ||
| unconsume(parser, socket); | ||
| parser.finish(); | ||
| freeParser(parser, req, socket); | ||
| parser = null; | ||
| upgradeStream = new UpgradeStream(socket, req); | ||
| socket[kUpgradeStream] = upgradeStream; | ||
| } | ||
|
|
||
| const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; | ||
| if (server.listenerCount(eventName) > 0) { | ||
| debug('SERVER have listener for %s', eventName); | ||
| const bodyHead = d.slice(ret, d.length); | ||
|
|
||
| socket.readableFlowing = null; | ||
| const bodyHead = d.slice(ret, d.length); | ||
|
|
||
| server.emit(eventName, req, socket, bodyHead); | ||
| if (req.complete && socket[kUpgradeStream]) { | ||
| // Previously emitted, now completed - just activate the stream | ||
| socket[kUpgradeStream].requestBodyCompleted(bodyHead); | ||
| } else { | ||
| socket.readableFlowing = null; | ||
| server.emit(eventName, req, upgradeStream, bodyHead); | ||
| } | ||
| } else { | ||
| // Got upgrade or CONNECT method, but have no handler. | ||
| socket.destroy(); | ||
|
|
@@ -1089,8 +1191,9 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { | |
| if (req.upgrade) { | ||
| req.upgrade = req.method === 'CONNECT' || | ||
| !!server.shouldUpgradeCallback(req); | ||
| if (req.upgrade) | ||
| return 2; | ||
| if (req.upgrade) { | ||
| return 0; | ||
| } | ||
| } | ||
|
|
||
| state.incoming.push(req); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.