Skip to content

Commit eaa71a1

Browse files
committed
http2: do not allow socket manipulation
Because of the specific serialization and processing requirements of HTTP/2, sockets should not be directly manipulated. This forbids any interactions with destroy, emit, end, once, on, pause, read, resume and write methods of the socket. It also redirects setTimeout to session instead of socket. Fixes: nodejs#16252 Refs: nodejs#16211
1 parent 4108072 commit eaa71a1

9 files changed

+182
-51
lines changed

lib/internal/http2/compat.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const Readable = Stream.Readable;
55
const binding = process.binding('http2');
66
const constants = binding.constants;
77
const errors = require('internal/errors');
8+
const { kSocket } = require('internal/http2/util');
89

910
const kFinish = Symbol('finish');
1011
const kBeginSend = Symbol('begin-send');
@@ -176,15 +177,15 @@ const proxySocketHandler = {
176177
throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION');
177178
default:
178179
const ref = stream.session !== undefined ?
179-
stream.session.socket : stream;
180+
stream.session[kSocket] : stream;
180181
const value = ref[prop];
181182
return typeof value === 'function' ? value.bind(ref) : value;
182183
}
183184
},
184185
getPrototypeOf(stream) {
185186
if (stream.session !== undefined)
186-
return stream.session.socket.constructor.prototype;
187-
return stream.prototype;
187+
return Reflect.getPrototypeOf(stream.session[kSocket]);
188+
return Reflect.getPrototypeOf(stream);
188189
},
189190
set(stream, prop, value) {
190191
switch (prop) {
@@ -201,9 +202,9 @@ const proxySocketHandler = {
201202
case 'setTimeout':
202203
const session = stream.session;
203204
if (session !== undefined)
204-
session[prop] = value;
205+
session.setTimeout = value;
205206
else
206-
stream[prop] = value;
207+
stream.setTimeout = value;
207208
return true;
208209
case 'write':
209210
case 'read':
@@ -212,7 +213,7 @@ const proxySocketHandler = {
212213
throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION');
213214
default:
214215
const ref = stream.session !== undefined ?
215-
stream.session.socket : stream;
216+
stream.session[kSocket] : stream;
216217
ref[prop] = value;
217218
return true;
218219
}

lib/internal/http2/core.js

+54-32
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const {
3838
getSettings,
3939
getStreamState,
4040
isPayloadMeaningless,
41+
kSocket,
4142
mapToHeaders,
4243
NghttpError,
4344
sessionName,
@@ -70,10 +71,10 @@ const kOptions = Symbol('options');
7071
const kOwner = Symbol('owner');
7172
const kProceed = Symbol('proceed');
7273
const kProtocol = Symbol('protocol');
74+
const kProxySocket = Symbol('proxy-socket');
7375
const kRemoteSettings = Symbol('remote-settings');
7476
const kServer = Symbol('server');
7577
const kSession = Symbol('session');
76-
const kSocket = Symbol('socket');
7778
const kState = Symbol('state');
7879
const kType = Symbol('type');
7980

@@ -672,6 +673,52 @@ function finishSessionDestroy(self, socket) {
672673
debug(`[${sessionName(self[kType])}] nghttp2session destroyed`);
673674
}
674675

676+
const proxySocketHandler = {
677+
get(session, prop) {
678+
switch (prop) {
679+
case 'setTimeout':
680+
return session.setTimeout.bind(session);
681+
case 'destroy':
682+
case 'emit':
683+
case 'end':
684+
case 'once':
685+
case 'on':
686+
case 'pause':
687+
case 'read':
688+
case 'resume':
689+
case 'write':
690+
throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION');
691+
default:
692+
const socket = session[kSocket];
693+
const value = socket[prop];
694+
return typeof value === 'function' ? value.bind(socket) : value;
695+
}
696+
},
697+
getPrototypeOf(session) {
698+
return Reflect.getPrototypeOf(session[kSocket]);
699+
},
700+
set(session, prop, value) {
701+
switch (prop) {
702+
case 'setTimeout':
703+
session.setTimeout = value;
704+
return true;
705+
case 'destroy':
706+
case 'emit':
707+
case 'end':
708+
case 'once':
709+
case 'on':
710+
case 'pause':
711+
case 'read':
712+
case 'resume':
713+
case 'write':
714+
throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION');
715+
default:
716+
session[kSocket][prop] = value;
717+
return true;
718+
}
719+
}
720+
};
721+
675722
// Upon creation, the Http2Session takes ownership of the socket. The session
676723
// may not be ready to use immediately if the socket is not yet fully connected.
677724
class Http2Session extends EventEmitter {
@@ -707,6 +754,7 @@ class Http2Session extends EventEmitter {
707754
};
708755

709756
this[kType] = type;
757+
this[kProxySocket] = null;
710758
this[kSocket] = socket;
711759

712760
// Do not use nagle's algorithm
@@ -756,7 +804,10 @@ class Http2Session extends EventEmitter {
756804

757805
// The socket owned by this session
758806
get socket() {
759-
return this[kSocket];
807+
const proxySocket = this[kProxySocket];
808+
if (proxySocket === null)
809+
return this[kProxySocket] = new Proxy(this, proxySocketHandler);
810+
return proxySocket;
760811
}
761812

762813
// The session type
@@ -957,6 +1008,7 @@ class Http2Session extends EventEmitter {
9571008
// Disassociate from the socket and server
9581009
const socket = this[kSocket];
9591010
// socket.pause();
1011+
delete this[kProxySocket];
9601012
delete this[kSocket];
9611013
delete this[kServer];
9621014

@@ -2155,30 +2207,6 @@ function socketDestroy(error) {
21552207
this.destroy(error);
21562208
}
21572209

2158-
function socketOnResume() {
2159-
if (this._paused)
2160-
return this.pause();
2161-
if (this._handle && !this._handle.reading) {
2162-
this._handle.reading = true;
2163-
this._handle.readStart();
2164-
}
2165-
}
2166-
2167-
function socketOnPause() {
2168-
if (this._handle && this._handle.reading) {
2169-
this._handle.reading = false;
2170-
this._handle.readStop();
2171-
}
2172-
}
2173-
2174-
function socketOnDrain() {
2175-
const needPause = 0 > this._writableState.highWaterMark;
2176-
if (this._paused && !needPause) {
2177-
this._paused = false;
2178-
this.resume();
2179-
}
2180-
}
2181-
21822210
// When an Http2Session emits an error, first try to forward it to the
21832211
// server as a sessionError; failing that, forward it to the socket as
21842212
// a sessionError; failing that, destroy, remove the error listener, and
@@ -2267,9 +2295,6 @@ function connectionListener(socket) {
22672295
}
22682296

22692297
socket.on('error', socketOnError);
2270-
socket.on('resume', socketOnResume);
2271-
socket.on('pause', socketOnPause);
2272-
socket.on('drain', socketOnDrain);
22732298
socket.on('close', socketOnClose);
22742299

22752300
// Set up the Session
@@ -2426,9 +2451,6 @@ function connect(authority, options, listener) {
24262451
}
24272452

24282453
socket.on('error', socketOnError);
2429-
socket.on('resume', socketOnResume);
2430-
socket.on('pause', socketOnPause);
2431-
socket.on('drain', socketOnDrain);
24322454
socket.on('close', socketOnClose);
24332455

24342456
const session = new ClientHttp2Session(options, socket);

lib/internal/http2/util.js

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
const binding = process.binding('http2');
44
const errors = require('internal/errors');
55

6+
const kSocket = Symbol('socket');
7+
68
const {
79
NGHTTP2_SESSION_CLIENT,
810
NGHTTP2_SESSION_SERVER,
@@ -551,6 +553,7 @@ module.exports = {
551553
getSettings,
552554
getStreamState,
553555
isPayloadMeaningless,
556+
kSocket,
554557
mapToHeaders,
555558
NghttpError,
556559
sessionName,

test/parallel/test-http2-client-destroy.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
// Flags: --expose-internals
2+
13
'use strict';
24

35
const common = require('../common');
46
if (!common.hasCrypto)
57
common.skip('missing crypto');
68
const assert = require('assert');
79
const h2 = require('http2');
10+
const { kSocket } = require('internal/http2/util');
811

912
{
1013
const server = h2.createServer();
@@ -13,7 +16,7 @@ const h2 = require('http2');
1316
common.mustCall(() => {
1417
const destroyCallbacks = [
1518
(client) => client.destroy(),
16-
(client) => client.socket.destroy()
19+
(client) => client[kSocket].destroy()
1720
];
1821

1922
let remaining = destroyCallbacks.length;
@@ -23,9 +26,9 @@ const h2 = require('http2');
2326
client.on(
2427
'connect',
2528
common.mustCall(() => {
26-
const socket = client.socket;
29+
const socket = client[kSocket];
2730

28-
assert(client.socket, 'client session has associated socket');
31+
assert(socket, 'client session has associated socket');
2932
assert(
3033
!client.destroyed,
3134
'client has not been destroyed before destroy is called'
@@ -41,7 +44,7 @@ const h2 = require('http2');
4144
destroyCallback(client);
4245

4346
assert(
44-
!client.socket,
47+
!client[kSocket],
4548
'client.socket undefined after destroy is called'
4649
);
4750

test/parallel/test-http2-client-socket-destroy.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
// Flags: --expose-internals
2+
13
'use strict';
24

35
const common = require('../common');
46
if (!common.hasCrypto)
57
common.skip('missing crypto');
68
const h2 = require('http2');
9+
const { kSocket } = require('internal/http2/util');
10+
711
const body =
812
'<html><head></head><body><h1>this is some data</h2></body></html>';
913

@@ -32,7 +36,7 @@ server.on('listening', common.mustCall(function() {
3236

3337
req.on('response', common.mustCall(() => {
3438
// send a premature socket close
35-
client.socket.destroy();
39+
client[kSocket].destroy();
3640
}));
3741
req.on('data', common.mustNotCall());
3842

test/parallel/test-http2-create-client-secure-session.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Flags: --expose-internals
2+
13
'use strict';
24

35
const common = require('../common');
@@ -8,14 +10,15 @@ if (!common.hasCrypto)
810
const assert = require('assert');
911
const fixtures = require('../common/fixtures');
1012
const h2 = require('http2');
13+
const { kSocket } = require('internal/http2/util');
1114
const tls = require('tls');
1215

1316
function loadKey(keyname) {
1417
return fixtures.readKey(keyname, 'binary');
1518
}
1619

1720
function onStream(stream, headers) {
18-
const socket = stream.session.socket;
21+
const socket = stream.session[kSocket];
1922
assert(headers[':authority'].startsWith(socket.servername));
2023
stream.respond({
2124
'content-type': 'text/html',
@@ -55,7 +58,7 @@ function verifySecureSession(key, cert, ca, opts) {
5558
assert.strictEqual(jsonData.servername, opts.servername || 'localhost');
5659
assert.strictEqual(jsonData.alpnProtocol, 'h2');
5760
server.close();
58-
client.socket.destroy();
61+
client[kSocket].destroy();
5962
}));
6063
req.end();
6164
});

test/parallel/test-http2-server-socket-destroy.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
// Flags: --expose-internals
2+
13
'use strict';
24

35
const common = require('../common');
46
if (!common.hasCrypto)
57
common.skip('missing crypto');
6-
const h2 = require('http2');
78
const assert = require('assert');
9+
const h2 = require('http2');
10+
const { kSocket } = require('internal/http2/util');
811

912
const {
1013
HTTP2_HEADER_METHOD,
@@ -24,7 +27,7 @@ function onStream(stream) {
2427
});
2528
stream.write('test');
2629

27-
const socket = stream.session.socket;
30+
const socket = stream.session[kSocket];
2831

2932
// When the socket is destroyed, the close events must be triggered
3033
// on the socket, server and session.

test/parallel/test-http2-server-socketerror.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
// Flags: --expose-internals
2+
13
'use strict';
24

35
const common = require('../common');
46
if (!common.hasCrypto)
57
common.skip('missing crypto');
68
const assert = require('assert');
79
const http2 = require('http2');
10+
const { kSocket } = require('internal/http2/util');
811

912
const server = http2.createServer();
1013
server.on('stream', common.mustCall((stream) => {
@@ -19,12 +22,12 @@ server.on('session', common.mustCall((session) => {
1922
type: Error,
2023
message: 'test'
2124
})(error);
22-
assert.strictEqual(socket, session.socket);
25+
assert.strictEqual(socket, session[kSocket]);
2326
});
2427
const isNotCalled = common.mustNotCall();
2528
session.on('socketError', handler);
2629
server.on('socketError', isNotCalled);
27-
session.socket.emit('error', new Error('test'));
30+
session[kSocket].emit('error', new Error('test'));
2831
session.removeListener('socketError', handler);
2932
server.removeListener('socketError', isNotCalled);
3033

@@ -35,10 +38,10 @@ server.on('session', common.mustCall((session) => {
3538
type: Error,
3639
message: 'test'
3740
})(error);
38-
assert.strictEqual(socket, session.socket);
41+
assert.strictEqual(socket, session[kSocket]);
3942
assert.strictEqual(session, session);
4043
}));
41-
session.socket.emit('error', new Error('test'));
44+
session[kSocket].emit('error', new Error('test'));
4245
}));
4346

4447
server.listen(0, common.mustCall(() => {

0 commit comments

Comments
 (0)