Skip to content
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

http2: backport changes to v9.x-staging #18050

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1724a4e
src: add optional keep-alive object to SetImmediate
addaleax Nov 21, 2017
df31133
http2: don't call into JS from GC
addaleax Nov 21, 2017
8c28e30
http2: only schedule write when necessary
addaleax Nov 21, 2017
504ee24
http2: be sure to destroy the Http2Stream
jasnell Nov 27, 2017
ac89b78
http2: cleanup Http2Stream/Http2Session destroy
jasnell Dec 12, 2017
7ab5c62
http2: remove redundant write indirection
addaleax Dec 17, 2017
66be8e5
http2: refactor outgoing write mechanism
addaleax Dec 17, 2017
293c2f9
http2: convert Http2Settings to an AsyncWrap
jasnell Dec 18, 2017
08478c1
http2: fix compiling with `--debug-http2`
addaleax Dec 25, 2017
b61c3fc
http2: keep session objects alive during Http2Scope
addaleax Dec 25, 2017
afa4a7c
http2: implement ref() and unref() on client sessions
kjin Dec 11, 2017
8d8dfea
http2: remove duplicate words in comments
tniessen Jan 1, 2018
d968c02
http2: perf_hooks integration
jasnell Dec 20, 2017
bf7eed3
http2: strictly limit number on concurrent streams
jasnell Nov 21, 2017
8df5d02
http2: add altsvc support
jasnell Dec 29, 2017
ae4e308
tls: set servername on client side too
jasnell Jan 1, 2018
7d6aa20
http2: add initial support for originSet
jasnell Jan 1, 2018
760f678
http2: add aligned padding strategy
jasnell Jan 1, 2018
06ed513
http2: properly handle already closed stream error
jasnell Jan 2, 2018
e929bd0
doc: add docs for common/http2.js utility
jasnell Jan 2, 2018
c1c4a6c
src: silence http2 -Wunused-result warnings
cjihrig Jan 2, 2018
adbc38a
http2: implement maxSessionMemory
jasnell Jan 3, 2018
51dc318
http2: verify that a dependency cycle may exist
jasnell Jan 3, 2018
d1e75eb
doc: grammar fixes in http2.md
Trott Jan 4, 2018
cd6fcf3
http2: verify flood error and unsolicited frames
jasnell Jan 3, 2018
526cf84
doc: correct spelling
sreepurnajasti Dec 29, 2017
d7dd941
perf_hooks: fix scheduling regression
apapirovski Jan 9, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
http2: verify flood error and unsolicited frames
* verify protections against ping and settings flooding
* Strictly handle and verify handling of unsolicited ping and
  settings frame acks.

PR-URL: #17969
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
  • Loading branch information
jasnell authored and MylesBorins committed Jan 9, 2018
commit cd6fcf37f4d02ca7a063949c7f00923c81c1f256
40 changes: 38 additions & 2 deletions src/node_http2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1305,8 +1305,24 @@ inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
if (ack) {
Http2Ping* ping = PopPing();
if (ping != nullptr)
if (ping != nullptr) {
ping->Done(true, frame->ping.opaque_data);
} else {
// PING Ack is unsolicited. Treat as a connection error. The HTTP/2
// spec does not require this, but there is no legitimate reason to
// receive an unsolicited PING ack on a connection. Either the peer
// is buggy or malicious, and we're not going to tolerate such
// nonsense.
Isolate* isolate = env()->isolate();
HandleScope scope(isolate);
Local<Context> context = env()->context();
Context::Scope context_scope(context);

Local<Value> argv[1] = {
Integer::New(isolate, NGHTTP2_ERR_PROTO),
};
MakeCallback(env()->error_string(), arraysize(argv), argv);
}
}
}

Expand All @@ -1317,8 +1333,28 @@ inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
// If this is an acknowledgement, we should have an Http2Settings
// object for it.
Http2Settings* settings = PopSettings();
if (settings != nullptr)
if (settings != nullptr) {
settings->Done(true);
} else {
// SETTINGS Ack is unsolicited. Treat as a connection error. The HTTP/2
// spec does not require this, but there is no legitimate reason to
// receive an unsolicited SETTINGS ack on a connection. Either the peer
// is buggy or malicious, and we're not going to tolerate such
// nonsense.
// Note that nghttp2 currently prevents this from happening for SETTINGS
// frames, so this block is purely defensive just in case that behavior
// changes. Specifically, unlike unsolicited PING acks, unsolicited
// SETTINGS acks should *never* make it this far.
Isolate* isolate = env()->isolate();
HandleScope scope(isolate);
Local<Context> context = env()->context();
Context::Scope context_scope(context);

Local<Value> argv[1] = {
Integer::New(isolate, NGHTTP2_ERR_PROTO),
};
MakeCallback(env()->error_string(), arraysize(argv), argv);
}
} else {
// Otherwise, notify the session about a new settings
MakeCallback(env()->onsettings_string(), 0, nullptr);
Expand Down
10 changes: 10 additions & 0 deletions test/common/http2.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,21 @@ class HeadersFrame extends Frame {
}
}

class PingFrame extends Frame {
constructor(ack = false) {
const buffers = [Buffer.alloc(8)];
super(8, 6, ack ? 1 : 0, 0);
buffers.unshift(this[kFrameData]);
this[kFrameData] = Buffer.concat(buffers);
}
}

module.exports = {
Frame,
DataFrame,
HeadersFrame,
SettingsFrame,
PingFrame,
kFakeRequestHeaders,
kFakeResponseHeaders,
kClientMagic
Expand Down
43 changes: 43 additions & 0 deletions test/parallel/test-http2-ping-unsolicited-ack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const http2 = require('http2');
const net = require('net');
const http2util = require('../common/http2');

// Test that ping flooding causes the session to be torn down

const kSettings = new http2util.SettingsFrame();
const kPingAck = new http2util.PingFrame(true);

const server = http2.createServer();

server.on('stream', common.mustNotCall());
server.on('session', common.mustCall((session) => {
session.on('error', common.expectsError({
code: 'ERR_HTTP2_ERROR',
message: 'Protocol error'
}));
session.on('close', common.mustCall(() => server.close()));
}));

server.listen(0, common.mustCall(() => {
const client = net.connect(server.address().port);

client.on('connect', common.mustCall(() => {
client.write(http2util.kClientMagic, () => {
client.write(kSettings.data);
// Send an unsolicited ping ack
client.write(kPingAck.data);
});
}));

// An error event may or may not be emitted, depending on operating system
// and timing. We do not really care if one is emitted here or not, as the
// error on the server side is what we are testing for. Do not make this
// a common.mustCall() and there's no need to check the error details.
client.on('error', () => {});
}));
50 changes: 50 additions & 0 deletions test/parallel/test-http2-settings-unsolicited-ack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const http2 = require('http2');
const net = require('net');
const http2util = require('../common/http2');
const Countdown = require('../common/countdown');

// Test that an unsolicited settings ack is ignored.

const kSettings = new http2util.SettingsFrame();
const kSettingsAck = new http2util.SettingsFrame(true);

const server = http2.createServer();
let client;

const countdown = new Countdown(3, () => {
client.destroy();
server.close();
});

server.on('stream', common.mustNotCall());
server.on('session', common.mustCall((session) => {
session.on('remoteSettings', common.mustCall(() => countdown.dec()));
}));

server.listen(0, common.mustCall(() => {
client = net.connect(server.address().port);

// Ensures that the clients settings frames are not sent until the
// servers are received, so that the first ack is actually expected.
client.once('data', (chunk) => {
// The very first chunk of data we get from the server should
// be a settings frame.
assert.deepStrictEqual(chunk.slice(0, 9), kSettings.data);
// The first ack is expected.
client.write(kSettingsAck.data, () => countdown.dec());
// The second one is not and will be ignored.
client.write(kSettingsAck.data, () => countdown.dec());
});

client.on('connect', common.mustCall(() => {
client.write(http2util.kClientMagic);
client.write(kSettings.data);
}));
}));
2 changes: 2 additions & 0 deletions test/sequential/sequential.status
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ test-inspector-async-call-stack : PASS, FLAKY
test-inspector-bindings : PASS, FLAKY
test-inspector-debug-end : PASS, FLAKY
test-inspector-async-hook-setup-at-signal: PASS, FLAKY
test-http2-ping-flood : PASS, FLAKY
test-http2-settings-flood : PASS, FLAKY

[$system==linux]

Expand Down
56 changes: 56 additions & 0 deletions test/sequential/test-http2-ping-flood.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const http2 = require('http2');
const net = require('net');
const http2util = require('../common/http2');

// Test that ping flooding causes the session to be torn down

const kSettings = new http2util.SettingsFrame();
const kPing = new http2util.PingFrame();

const server = http2.createServer();

server.on('stream', common.mustNotCall());
server.on('session', common.mustCall((session) => {
session.on('error', common.expectsError({
code: 'ERR_HTTP2_ERROR',
message:
'Flooding was detected in this HTTP/2 session, and it must be closed'
}));
session.on('close', common.mustCall(() => {
server.close();
}));
}));

server.listen(0, common.mustCall(() => {
const client = net.connect(server.address().port);

// nghttp2 uses a limit of 10000 items in it's outbound queue.
// If this number is exceeded, a flooding error is raised. Set
// this lim higher to account for the ones that nghttp2 is
// successfully able to respond to.
// TODO(jasnell): Unfortunately, this test is inherently flaky because
// it is entirely dependent on how quickly the server is able to handle
// the inbound frames and whether those just happen to overflow nghttp2's
// outbound queue. The threshold at which the flood error occurs can vary
// from one system to another, and from one test run to another.
client.on('connect', common.mustCall(() => {
client.write(http2util.kClientMagic, () => {
client.write(kSettings.data, () => {
for (let n = 0; n < 35000; n++)
client.write(kPing.data);
});
});
}));

// An error event may or may not be emitted, depending on operating system
// and timing. We do not really care if one is emitted here or not, as the
// error on the server side is what we are testing for. Do not make this
// a common.mustCall() and there's no need to check the error details.
client.on('error', () => {});
}));
53 changes: 53 additions & 0 deletions test/sequential/test-http2-settings-flood.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const http2 = require('http2');
const net = require('net');
const http2util = require('../common/http2');

// Test that settings flooding causes the session to be torn down

const kSettings = new http2util.SettingsFrame();

const server = http2.createServer();

server.on('stream', common.mustNotCall());
server.on('session', common.mustCall((session) => {
session.on('error', common.expectsError({
code: 'ERR_HTTP2_ERROR',
message:
'Flooding was detected in this HTTP/2 session, and it must be closed'
}));
session.on('close', common.mustCall(() => {
server.close();
}));
}));

server.listen(0, common.mustCall(() => {
const client = net.connect(server.address().port);

// nghttp2 uses a limit of 10000 items in it's outbound queue.
// If this number is exceeded, a flooding error is raised. Set
// this lim higher to account for the ones that nghttp2 is
// successfully able to respond to.
// TODO(jasnell): Unfortunately, this test is inherently flaky because
// it is entirely dependent on how quickly the server is able to handle
// the inbound frames and whether those just happen to overflow nghttp2's
// outbound queue. The threshold at which the flood error occurs can vary
// from one system to another, and from one test run to another.
client.on('connect', common.mustCall(() => {
client.write(http2util.kClientMagic, () => {
for (let n = 0; n < 35000; n++)
client.write(kSettings.data);
});
}));

// An error event may or may not be emitted, depending on operating system
// and timing. We do not really care if one is emitted here or not, as the
// error on the server side is what we are testing for. Do not make this
// a common.mustCall() and there's no need to check the error details.
client.on('error', () => {});
}));