aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/http2.md27
-rw-r--r--lib/internal/http2/util.js8
-rw-r--r--src/node_http2.cc55
-rw-r--r--src/node_http2.h45
-rw-r--r--src/node_http2_state.h1
-rw-r--r--test/parallel/test-http2-util-update-options-buffer.js7
-rw-r--r--test/sequential/test-http2-max-session-memory.js44
7 files changed, 172 insertions, 15 deletions
diff --git a/doc/api/http2.md b/doc/api/http2.md
index 4a071272fa..f87824e932 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1633,6 +1633,15 @@ changes:
* `options` {Object}
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
for deflating header fields. **Default:** `4Kib`
+ * `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
+ is permitted to use. The value is expressed in terms of number of megabytes,
+ e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
+ `10`. This is a credit based limit, existing `Http2Stream`s may cause this
+ limit to be exceeded, but new `Http2Stream` instances will be rejected
+ while this limit is exceeded. The current number of `Http2Stream` sessions,
+ the current memory use of the header compression tables, current data
+ queued to be sent, and unacknowledged PING and SETTINGS frames are all
+ counted towards the current limit.
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
**Default:** `128`. The minimum value is `4`.
* `maxOutstandingPings` {number} Sets the maximum number of outstanding,
@@ -1711,6 +1720,15 @@ changes:
`false`. See the [`'unknownProtocol'`][] event. See [ALPN negotiation][].
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
for deflating header fields. **Default:** `4Kib`
+ * `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
+ is permitted to use. The value is expressed in terms of number of megabytes,
+ e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
+ `10`. This is a credit based limit, existing `Http2Stream`s may cause this
+ limit to be exceeded, but new `Http2Stream` instances will be rejected
+ while this limit is exceeded. The current number of `Http2Stream` sessions,
+ the current memory use of the header compression tables, current data
+ queued to be sent, and unacknowledged PING and SETTINGS frames are all
+ counted towards the current limit.
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
**Default:** `128`. The minimum value is `4`.
* `maxOutstandingPings` {number} Sets the maximum number of outstanding,
@@ -1794,6 +1812,15 @@ changes:
* `options` {Object}
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
for deflating header fields. **Default:** `4Kib`
+ * `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
+ is permitted to use. The value is expressed in terms of number of megabytes,
+ e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
+ `10`. This is a credit based limit, existing `Http2Stream`s may cause this
+ limit to be exceeded, but new `Http2Stream` instances will be rejected
+ while this limit is exceeded. The current number of `Http2Stream` sessions,
+ the current memory use of the header compression tables, current data
+ queued to be sent, and unacknowledged PING and SETTINGS frames are all
+ counted towards the current limit.
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
**Default:** `128`. The minimum value is `1`.
* `maxOutstandingPings` {number} Sets the maximum number of outstanding,
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
index fffea10ab6..1411ab7cf7 100644
--- a/lib/internal/http2/util.js
+++ b/lib/internal/http2/util.js
@@ -175,7 +175,8 @@ const IDX_OPTIONS_PADDING_STRATEGY = 4;
const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
-const IDX_OPTIONS_FLAGS = 8;
+const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
+const IDX_OPTIONS_FLAGS = 9;
function updateOptionsBuffer(options) {
var flags = 0;
@@ -219,6 +220,11 @@ function updateOptionsBuffer(options) {
optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS] =
Math.max(1, options.maxOutstandingSettings);
}
+ if (typeof options.maxSessionMemory === 'number') {
+ flags |= (1 << IDX_OPTIONS_MAX_SESSION_MEMORY);
+ optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY] =
+ Math.max(1, options.maxSessionMemory);
+ }
optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
}
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 9caee6d401..85bdde9b92 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -174,6 +174,18 @@ Http2Options::Http2Options(Environment* env) {
if (flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS)) {
SetMaxOutstandingSettings(buffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS]);
}
+
+ // The HTTP2 specification places no limits on the amount of memory
+ // that a session can consume. In order to prevent abuse, we place a
+ // cap on the amount of memory a session can consume at any given time.
+ // this is a credit based system. Existing streams may cause the limit
+ // to be temporarily exceeded but once over the limit, new streams cannot
+ // created.
+ // Important: The maxSessionMemory option in javascript is expressed in
+ // terms of MB increments (i.e. the value 1 == 1 MB)
+ if (flags & (1 << IDX_OPTIONS_MAX_SESSION_MEMORY)) {
+ SetMaxSessionMemory(buffer[IDX_OPTIONS_MAX_SESSION_MEMORY] * 1e6);
+ }
}
void Http2Session::Http2Settings::Init() {
@@ -482,11 +494,13 @@ Http2Session::Http2Session(Environment* env,
// Capture the configuration options for this session
Http2Options opts(env);
- int32_t maxHeaderPairs = opts.GetMaxHeaderPairs();
+ max_session_memory_ = opts.GetMaxSessionMemory();
+
+ uint32_t maxHeaderPairs = opts.GetMaxHeaderPairs();
max_header_pairs_ =
type == NGHTTP2_SESSION_SERVER
- ? std::max(maxHeaderPairs, 4) // minimum # of request headers
- : std::max(maxHeaderPairs, 1); // minimum # of response headers
+ ? std::max(maxHeaderPairs, 4U) // minimum # of request headers
+ : std::max(maxHeaderPairs, 1U); // minimum # of response headers
max_outstanding_pings_ = opts.GetMaxOutstandingPings();
max_outstanding_settings_ = opts.GetMaxOutstandingSettings();
@@ -672,18 +686,21 @@ inline bool Http2Session::CanAddStream() {
size_t maxSize =
std::min(streams_.max_size(), static_cast<size_t>(maxConcurrentStreams));
// We can add a new stream so long as we are less than the current
- // maximum on concurrent streams
- return streams_.size() < maxSize;
+ // maximum on concurrent streams and there's enough available memory
+ return streams_.size() < maxSize &&
+ IsAvailableSessionMemory(sizeof(Http2Stream));
}
inline void Http2Session::AddStream(Http2Stream* stream) {
CHECK_GE(++statistics_.stream_count, 0);
streams_[stream->id()] = stream;
+ IncrementCurrentSessionMemory(stream->self_size());
}
-inline void Http2Session::RemoveStream(int32_t id) {
- streams_.erase(id);
+inline void Http2Session::RemoveStream(Http2Stream* stream) {
+ streams_.erase(stream->id());
+ DecrementCurrentSessionMemory(stream->self_size());
}
// Used as one of the Padding Strategy functions. Will attempt to ensure
@@ -1677,7 +1694,7 @@ Http2Stream::Http2Stream(
Http2Stream::~Http2Stream() {
if (session_ != nullptr) {
- session_->RemoveStream(id_);
+ session_->RemoveStream(this);
session_ = nullptr;
}
@@ -2007,7 +2024,7 @@ inline int Http2Stream::DoWrite(WriteWrap* req_wrap,
i == nbufs - 1 ? req_wrap : nullptr,
bufs[i]
});
- available_outbound_length_ += bufs[i].len;
+ IncrementAvailableOutboundLength(bufs[i].len);
}
CHECK_NE(nghttp2_session_resume_data(**session_, id_), NGHTTP2_ERR_NOMEM);
return 0;
@@ -2029,7 +2046,10 @@ inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name,
if (this->statistics_.first_header == 0)
this->statistics_.first_header = uv_hrtime();
size_t length = GetBufferLength(name) + GetBufferLength(value) + 32;
- if (current_headers_.size() == max_header_pairs_ ||
+ // A header can only be added if we have not exceeded the maximum number
+ // of headers and the session has memory available for it.
+ if (!session_->IsAvailableSessionMemory(length) ||
+ current_headers_.size() == max_header_pairs_ ||
current_headers_length_ + length > max_header_length_) {
return false;
}
@@ -2173,7 +2193,7 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
// Just return the length, let Http2Session::OnSendData take care of
// actually taking the buffers out of the queue.
*flags |= NGHTTP2_DATA_FLAG_NO_COPY;
- stream->available_outbound_length_ -= amount;
+ stream->DecrementAvailableOutboundLength(amount);
}
}
@@ -2196,6 +2216,15 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
return amount;
}
+inline void Http2Stream::IncrementAvailableOutboundLength(size_t amount) {
+ available_outbound_length_ += amount;
+ session_->IncrementCurrentSessionMemory(amount);
+}
+
+inline void Http2Stream::DecrementAvailableOutboundLength(size_t amount) {
+ available_outbound_length_ -= amount;
+ session_->DecrementCurrentSessionMemory(amount);
+}
// Implementation of the JavaScript API
@@ -2689,6 +2718,7 @@ Http2Session::Http2Ping* Http2Session::PopPing() {
if (!outstanding_pings_.empty()) {
ping = outstanding_pings_.front();
outstanding_pings_.pop();
+ DecrementCurrentSessionMemory(ping->self_size());
}
return ping;
}
@@ -2697,6 +2727,7 @@ bool Http2Session::AddPing(Http2Session::Http2Ping* ping) {
if (outstanding_pings_.size() == max_outstanding_pings_)
return false;
outstanding_pings_.push(ping);
+ IncrementCurrentSessionMemory(ping->self_size());
return true;
}
@@ -2705,6 +2736,7 @@ Http2Session::Http2Settings* Http2Session::PopSettings() {
if (!outstanding_settings_.empty()) {
settings = outstanding_settings_.front();
outstanding_settings_.pop();
+ DecrementCurrentSessionMemory(settings->self_size());
}
return settings;
}
@@ -2713,6 +2745,7 @@ bool Http2Session::AddSettings(Http2Session::Http2Settings* settings) {
if (outstanding_settings_.size() == max_outstanding_settings_)
return false;
outstanding_settings_.push(settings);
+ IncrementCurrentSessionMemory(settings->self_size());
return true;
}
diff --git a/src/node_http2.h b/src/node_http2.h
index f63f8133a4..4fc98f0c68 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -82,6 +82,9 @@ void inline debug_vfprintf(const char* format, ...) {
// Also strictly limit the number of outstanding SETTINGS frames a user sends
#define DEFAULT_MAX_SETTINGS 10
+// Default maximum total memory cap for Http2Session.
+#define DEFAULT_MAX_SESSION_MEMORY 1e7;
+
// These are the standard HTTP/2 defaults as specified by the RFC
#define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096
#define DEFAULT_SETTINGS_ENABLE_PUSH 1
@@ -501,8 +504,17 @@ class Http2Options {
return max_outstanding_settings_;
}
+ void SetMaxSessionMemory(uint64_t max) {
+ max_session_memory_ = max;
+ }
+
+ uint64_t GetMaxSessionMemory() {
+ return max_session_memory_;
+ }
+
private:
nghttp2_option* options_;
+ uint64_t max_session_memory_ = DEFAULT_MAX_SESSION_MEMORY;
uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE;
size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
@@ -629,6 +641,9 @@ class Http2Stream : public AsyncWrap,
// Returns the stream identifier for this stream
inline int32_t id() const { return id_; }
+ inline void IncrementAvailableOutboundLength(size_t amount);
+ inline void DecrementAvailableOutboundLength(size_t amount);
+
inline bool AddHeader(nghttp2_rcbuf* name,
nghttp2_rcbuf* value,
uint8_t flags);
@@ -848,7 +863,7 @@ class Http2Session : public AsyncWrap {
inline void AddStream(Http2Stream* stream);
// Removes a stream instance from this session
- inline void RemoveStream(int32_t id);
+ inline void RemoveStream(Http2Stream* stream);
// Write data to the session
inline ssize_t Write(const uv_buf_t* bufs, size_t nbufs);
@@ -906,6 +921,30 @@ class Http2Session : public AsyncWrap {
Http2Settings* PopSettings();
bool AddSettings(Http2Settings* settings);
+ void IncrementCurrentSessionMemory(uint64_t amount) {
+ current_session_memory_ += amount;
+ }
+
+ void DecrementCurrentSessionMemory(uint64_t amount) {
+ current_session_memory_ -= amount;
+ }
+
+ // Returns the current session memory including the current size of both
+ // the inflate and deflate hpack headers, the current outbound storage
+ // queue, and pending writes.
+ uint64_t GetCurrentSessionMemory() {
+ uint64_t total = current_session_memory_ + sizeof(Http2Session);
+ total += nghttp2_session_get_hd_deflate_dynamic_table_size(session_);
+ total += nghttp2_session_get_hd_inflate_dynamic_table_size(session_);
+ total += outgoing_storage_.size();
+ return total;
+ }
+
+ // Return true if current_session_memory + amount is less than the max
+ bool IsAvailableSessionMemory(uint64_t amount) {
+ return GetCurrentSessionMemory() + amount <= max_session_memory_;
+ }
+
struct Statistics {
uint64_t start_time;
uint64_t end_time;
@@ -1035,6 +1074,10 @@ class Http2Session : public AsyncWrap {
// The maximum number of header pairs permitted for streams on this session
uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
+ // The maximum amount of memory allocated for this session
+ uint64_t max_session_memory_ = DEFAULT_MAX_SESSION_MEMORY;
+ uint64_t current_session_memory_ = 0;
+
// The collection of active Http2Streams associated with this session
std::unordered_map<int32_t, Http2Stream*> streams_;
diff --git a/src/node_http2_state.h b/src/node_http2_state.h
index ef8696ce60..af0740c994 100644
--- a/src/node_http2_state.h
+++ b/src/node_http2_state.h
@@ -50,6 +50,7 @@ namespace http2 {
IDX_OPTIONS_MAX_HEADER_LIST_PAIRS,
IDX_OPTIONS_MAX_OUTSTANDING_PINGS,
IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS,
+ IDX_OPTIONS_MAX_SESSION_MEMORY,
IDX_OPTIONS_FLAGS
};
diff --git a/test/parallel/test-http2-util-update-options-buffer.js b/test/parallel/test-http2-util-update-options-buffer.js
index 5768ce0204..6ab8bcff02 100644
--- a/test/parallel/test-http2-util-update-options-buffer.js
+++ b/test/parallel/test-http2-util-update-options-buffer.js
@@ -20,7 +20,8 @@ const IDX_OPTIONS_PADDING_STRATEGY = 4;
const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
-const IDX_OPTIONS_FLAGS = 8;
+const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
+const IDX_OPTIONS_FLAGS = 9;
{
updateOptionsBuffer({
@@ -31,7 +32,8 @@ const IDX_OPTIONS_FLAGS = 8;
paddingStrategy: 5,
maxHeaderListPairs: 6,
maxOutstandingPings: 7,
- maxOutstandingSettings: 8
+ maxOutstandingSettings: 8,
+ maxSessionMemory: 9
});
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
@@ -42,6 +44,7 @@ const IDX_OPTIONS_FLAGS = 8;
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6);
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 7);
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS], 8);
+ strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY], 9);
const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
diff --git a/test/sequential/test-http2-max-session-memory.js b/test/sequential/test-http2-max-session-memory.js
new file mode 100644
index 0000000000..e16000d126
--- /dev/null
+++ b/test/sequential/test-http2-max-session-memory.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const http2 = require('http2');
+
+// Test that maxSessionMemory Caps work
+
+const largeBuffer = Buffer.alloc(1e6);
+
+const server = http2.createServer({ maxSessionMemory: 1 });
+
+server.on('stream', common.mustCall((stream) => {
+ stream.respond();
+ stream.end(largeBuffer);
+}));
+
+server.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+
+ {
+ const req = client.request();
+
+ req.on('response', () => {
+ // This one should be rejected because the server is over budget
+ // on the current memory allocation
+ const req = client.request();
+ req.on('error', common.expectsError({
+ code: 'ERR_HTTP2_STREAM_ERROR',
+ type: Error,
+ message: 'Stream closed with error code 11'
+ }));
+ req.on('close', common.mustCall(() => {
+ server.close();
+ client.destroy();
+ }));
+ });
+
+ req.resume();
+ req.on('close', common.mustCall());
+ }
+}));