Skip to content

Commit 3fc8d22

Browse files
authored
http2: fix h2-over-h2 connection proxying
PR-URL: #52368 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
1 parent 468cb99 commit 3fc8d22

File tree

4 files changed

+67
-14
lines changed

4 files changed

+67
-14
lines changed

lib/internal/http2/core.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ const {
177177
kUpdateTimer,
178178
kHandle,
179179
kSession,
180+
kBoundSession,
180181
setStreamTimeout,
181182
} = require('internal/stream_base_commons');
182183
const { kTimeout } = require('internal/timers');
@@ -1121,7 +1122,7 @@ function cleanupSession(session) {
11211122
if (handle)
11221123
handle.ondone = null;
11231124
if (socket) {
1124-
socket[kSession] = undefined;
1125+
socket[kBoundSession] = undefined;
11251126
socket[kServer] = undefined;
11261127
}
11271128
}
@@ -1235,10 +1236,10 @@ class Http2Session extends EventEmitter {
12351236
// If the session property already exists on the socket,
12361237
// then it has already been bound to an Http2Session instance
12371238
// and cannot be attached again.
1238-
if (socket[kSession] !== undefined)
1239+
if (socket[kBoundSession] !== undefined)
12391240
throw new ERR_HTTP2_SOCKET_BOUND();
12401241

1241-
socket[kSession] = this;
1242+
socket[kBoundSession] = this;
12421243

12431244
if (!socket._handle || !socket._handle.isStreamBase) {
12441245
socket = new JSStreamSocket(socket);
@@ -1617,7 +1618,7 @@ class Http2Session extends EventEmitter {
16171618
}
16181619

16191620
_onTimeout() {
1620-
callTimeout(this);
1621+
callTimeout(this, this);
16211622
}
16221623

16231624
ref() {
@@ -2093,7 +2094,7 @@ class Http2Stream extends Duplex {
20932094
}
20942095

20952096
_onTimeout() {
2096-
callTimeout(this, kSession);
2097+
callTimeout(this, this[kSession]);
20972098
}
20982099

20992100
// True if the HEADERS frame has been sent
@@ -2419,7 +2420,7 @@ class Http2Stream extends Duplex {
24192420
}
24202421
}
24212422

2422-
function callTimeout(self, kSession) {
2423+
function callTimeout(self, session) {
24232424
// If the session is destroyed, this should never actually be invoked,
24242425
// but just in case...
24252426
if (self.destroyed)
@@ -2430,7 +2431,7 @@ function callTimeout(self, kSession) {
24302431
// happens, meaning that if a write is ongoing it should never equal the
24312432
// newly fetched, updated value.
24322433
if (self[kState].writeQueueSize > 0) {
2433-
const handle = kSession ? self[kSession][kHandle] : self[kHandle];
2434+
const handle = session[kHandle];
24342435
const chunksSentSinceLastWrite = handle !== undefined ?
24352436
handle.chunksSentSinceLastWrite : null;
24362437
if (chunksSentSinceLastWrite !== null &&
@@ -3017,7 +3018,7 @@ ObjectDefineProperty(Http2Session.prototype, 'setTimeout', setTimeoutValue);
30173018
// When the socket emits an error, destroy the associated Http2Session and
30183019
// forward it the same error.
30193020
function socketOnError(error) {
3020-
const session = this[kSession];
3021+
const session = this[kBoundSession];
30213022
if (session !== undefined) {
30223023
// We can ignore ECONNRESET after GOAWAY was received as there's nothing
30233024
// we can do and the other side is fully within its rights to do so.
@@ -3300,7 +3301,7 @@ function setupCompat(ev) {
33003301
}
33013302

33023303
function socketOnClose() {
3303-
const session = this[kSession];
3304+
const session = this[kBoundSession];
33043305
if (session !== undefined) {
33053306
debugSessionObj(session, 'socket closed');
33063307
const err = session.connecting ? new ERR_SOCKET_CLOSED() : null;

lib/internal/js_stream_socket.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ let debug = require('internal/util/debuglog').debuglog(
1717
);
1818
const { owner_symbol } = require('internal/async_hooks').symbols;
1919
const { ERR_STREAM_WRAP } = require('internal/errors').codes;
20-
const { kSession } = require('internal/stream_base_commons');
20+
const { kBoundSession } = require('internal/stream_base_commons');
2121

2222
const kCurrentWriteRequest = Symbol('kCurrentWriteRequest');
2323
const kCurrentShutdownRequest = Symbol('kCurrentShutdownRequest');
@@ -265,12 +265,12 @@ class JSStreamSocket extends Socket {
265265
});
266266
}
267267

268-
get [kSession]() {
269-
return this.stream[kSession];
268+
get [kBoundSession]() {
269+
return this.stream[kBoundSession];
270270
}
271271

272-
set [kSession](session) {
273-
this.stream[kSession] = session;
272+
set [kBoundSession](session) {
273+
this.stream[kBoundSession] = session;
274274
}
275275
}
276276

lib/internal/stream_base_commons.js

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const kMaybeDestroy = Symbol('kMaybeDestroy');
3333
const kUpdateTimer = Symbol('kUpdateTimer');
3434
const kAfterAsyncWrite = Symbol('kAfterAsyncWrite');
3535
const kHandle = Symbol('kHandle');
36+
const kBoundSession = Symbol('kBoundSession');
3637
const kSession = Symbol('kSession');
3738

3839
let debug = require('internal/util/debuglog').debuglog('stream', (fn) => {
@@ -255,6 +256,7 @@ function setStreamTimeout(msecs, callback) {
255256
} else {
256257
this[kTimeout] = setUnrefTimeout(this._onTimeout.bind(this), msecs);
257258
if (this[kSession]) this[kSession][kUpdateTimer]();
259+
if (this[kBoundSession]) this[kBoundSession][kUpdateTimer]();
258260

259261
if (callback !== undefined) {
260262
validateFunction(callback, 'callback');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const assert = require('assert');
7+
const h2 = require('http2');
8+
9+
const server = h2.createServer();
10+
11+
server.listen(0, common.mustCall(function() {
12+
const proxyClient = h2.connect(`http://localhost:${server.address().port}`);
13+
14+
const request = proxyClient.request({
15+
':method': 'CONNECT',
16+
':authority': 'example.com:80'
17+
});
18+
19+
request.on('response', common.mustCall((connectResponse) => {
20+
assert.strictEqual(connectResponse[':status'], 200);
21+
22+
const proxiedClient = h2.connect('http://example.com', {
23+
createConnection: () => request // Tunnel via first request stream
24+
});
25+
26+
const proxiedRequest = proxiedClient.request();
27+
proxiedRequest.on('response', common.mustCall((proxiedResponse) => {
28+
assert.strictEqual(proxiedResponse[':status'], 204);
29+
30+
proxiedClient.close();
31+
proxyClient.close();
32+
server.close();
33+
}));
34+
}));
35+
}));
36+
37+
server.once('connect', common.mustCall((req, res) => {
38+
assert.strictEqual(req.headers[':method'], 'CONNECT');
39+
res.writeHead(200); // Accept the CONNECT tunnel
40+
41+
// Handle this stream as a new 'proxied' connection (pretend to forward
42+
// but actually just unwrap the tunnel ourselves):
43+
server.emit('connection', res.stream);
44+
}));
45+
46+
// Handle the 'proxied' request itself:
47+
server.once('request', common.mustCall((req, res) => {
48+
res.writeHead(204);
49+
res.end();
50+
}));

0 commit comments

Comments
 (0)