http2: implement maxSessionMemory
The maxSessionMemory is a cap for the amount of memory an Http2Session is permitted to consume. If exceeded, new `Http2Stream` sessions will be rejected with an `ENHANCE_YOUR_CALM` error and existing `Http2Stream` instances that are still receiving headers will be terminated with an `ENHANCE_YOUR_CALM` error. PR-URL: https://github.com/nodejs/node/pull/17967 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
feaf6ac3dc
commit
882e7ef354
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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_;
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
@ -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];
|
||||
|
||||
|
44
test/sequential/test-http2-max-session-memory.js
Normal file
44
test/sequential/test-http2-max-session-memory.js
Normal file
@ -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());
|
||||
}
|
||||
}));
|
Loading…
x
Reference in New Issue
Block a user