-8.16.0
+8.16.1
+8.16.0 8.15.1 8.15.0 8.14.1
diff --git a/benchmark/http2/headers.js b/benchmark/http2/headers.js
index 3c8d0465acb0d0..0ff69326073b65 100644
--- a/benchmark/http2/headers.js
+++ b/benchmark/http2/headers.js
@@ -42,8 +42,7 @@ function main(conf) {
function doRequest(remaining) {
const req = client.request(headersObject);
- req.end();
- req.on('data', () => {});
+ req.resume();
req.on('end', () => {
if (remaining > 0) {
doRequest(remaining - 1);
diff --git a/benchmark/http2/respond-with-fd.js b/benchmark/http2/respond-with-fd.js
index 791e5f3d1e7da6..f7f2a780bf3f52 100644
--- a/benchmark/http2/respond-with-fd.js
+++ b/benchmark/http2/respond-with-fd.js
@@ -8,9 +8,9 @@ const fs = require('fs');
const file = path.join(path.resolve(__dirname, '../fixtures'), 'alice.html');
const bench = common.createBenchmark(main, {
- requests: [100, 1000, 10000, 100000, 1000000],
- streams: [100, 200, 1000],
- clients: [1, 2],
+ requests: [100, 1000, 5000],
+ streams: [1, 10, 20, 40, 100, 200],
+ clients: [2],
benchmarker: ['h2load']
}, { flags: ['--no-warnings', '--expose-http2'] });
diff --git a/benchmark/http2/simple.js b/benchmark/http2/simple.js
index e8cb3ddee2dff8..f2e57de4bff743 100644
--- a/benchmark/http2/simple.js
+++ b/benchmark/http2/simple.js
@@ -9,9 +9,9 @@ const fs = require('fs');
const file = path.join(path.resolve(__dirname, '../fixtures'), 'alice.html');
const bench = common.createBenchmark(main, {
- requests: [100, 1000, 10000, 100000],
- streams: [100, 200, 1000],
- clients: [1, 2],
+ requests: [100, 1000, 5000],
+ streams: [1, 10, 20, 40, 100, 200],
+ clients: [2],
benchmarker: ['h2load']
}, { flags: ['--no-warnings', '--expose-http2'] });
diff --git a/deps/nghttp2/lib/CMakeLists.txt b/deps/nghttp2/lib/CMakeLists.txt
index 17e422b22db790..c27ee99bb7fa46 100644
--- a/deps/nghttp2/lib/CMakeLists.txt
+++ b/deps/nghttp2/lib/CMakeLists.txt
@@ -38,16 +38,23 @@ if(WIN32)
endif()
# Public shared library
-add_library(nghttp2 SHARED ${NGHTTP2_SOURCES} ${NGHTTP2_RES})
-set_target_properties(nghttp2 PROPERTIES
- COMPILE_FLAGS "${WARNCFLAGS}"
- VERSION ${LT_VERSION} SOVERSION ${LT_SOVERSION}
- C_VISIBILITY_PRESET hidden
-)
-target_include_directories(nghttp2 INTERFACE
+if(ENABLE_SHARED_LIB)
+ add_library(nghttp2 SHARED ${NGHTTP2_SOURCES} ${NGHTTP2_RES})
+ set_target_properties(nghttp2 PROPERTIES
+ COMPILE_FLAGS "${WARNCFLAGS}"
+ VERSION ${LT_VERSION} SOVERSION ${LT_SOVERSION}
+ C_VISIBILITY_PRESET hidden
+ )
+ target_include_directories(nghttp2 INTERFACE
"${CMAKE_CURRENT_BINARY_DIR}/includes"
"${CMAKE_CURRENT_SOURCE_DIR}/includes"
- )
+ )
+
+ install(TARGETS nghttp2
+ ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+ LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
+endif()
if(HAVE_CUNIT OR ENABLE_STATIC_LIB)
# Static library (for unittests because of symbol visibility)
@@ -64,8 +71,6 @@ if(HAVE_CUNIT OR ENABLE_STATIC_LIB)
endif()
endif()
-install(TARGETS nghttp2
- DESTINATION "${CMAKE_INSTALL_LIBDIR}")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libnghttp2.pc"
DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
diff --git a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h
index 8c54b9c8cc464d..313fb23daa7449 100644
--- a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h
+++ b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h
@@ -31,6 +31,11 @@
# define WIN32
#endif
+/* Compatibility for non-Clang compilers */
+#ifndef __has_declspec_attribute
+# define __has_declspec_attribute(x) 0
+#endif
+
#ifdef __cplusplus
extern "C" {
#endif
@@ -51,7 +56,8 @@ extern "C" {
#ifdef NGHTTP2_STATICLIB
# define NGHTTP2_EXTERN
-#elif defined(WIN32)
+#elif defined(WIN32) || (__has_declspec_attribute(dllexport) && \
+ __has_declspec_attribute(dllimport))
# ifdef BUILDING_NGHTTP2
# define NGHTTP2_EXTERN __declspec(dllexport)
# else /* !BUILDING_NGHTTP2 */
@@ -680,7 +686,12 @@ typedef enum {
/**
* SETTINGS_MAX_HEADER_LIST_SIZE
*/
- NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE = 0x06
+ NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE = 0x06,
+ /**
+ * SETTINGS_ENABLE_CONNECT_PROTOCOL
+ * (`RFC 8441 `_)
+ */
+ NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL = 0x08
} nghttp2_settings_id;
/* Note: If we add SETTINGS, update the capacity of
NGHTTP2_INBOUND_NUM_IV as well */
@@ -2637,6 +2648,17 @@ nghttp2_option_set_max_deflate_dynamic_table_size(nghttp2_option *option,
NGHTTP2_EXTERN void nghttp2_option_set_no_closed_streams(nghttp2_option *option,
int val);
+/**
+ * @function
+ *
+ * This function sets the maximum number of outgoing SETTINGS ACK and
+ * PING ACK frames retained in :type:`nghttp2_session` object. If
+ * more than those frames are retained, the peer is considered to be
+ * misbehaving and session will be closed. The default value is 1000.
+ */
+NGHTTP2_EXTERN void nghttp2_option_set_max_outbound_ack(nghttp2_option *option,
+ size_t val);
+
/**
* @function
*
diff --git a/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h b/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h
index 1f1d4808ca27c0..45bb0c9102cb05 100644
--- a/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h
+++ b/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h
@@ -29,7 +29,7 @@
* @macro
* Version number of the nghttp2 library release
*/
-#define NGHTTP2_VERSION "1.33.0"
+#define NGHTTP2_VERSION "1.39.2"
/**
* @macro
@@ -37,6 +37,6 @@
* release. This is a 24 bit number with 8 bits for major number, 8 bits
* for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
*/
-#define NGHTTP2_VERSION_NUM 0x012100
+#define NGHTTP2_VERSION_NUM 0x012702
#endif /* NGHTTP2VER_H */
diff --git a/deps/nghttp2/lib/nghttp2_frame.c b/deps/nghttp2/lib/nghttp2_frame.c
index 6e33f3c247f5cb..4821de40885736 100644
--- a/deps/nghttp2/lib/nghttp2_frame.c
+++ b/deps/nghttp2/lib/nghttp2_frame.c
@@ -1050,6 +1050,11 @@ int nghttp2_iv_check(const nghttp2_settings_entry *iv, size_t niv) {
break;
case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE:
break;
+ case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL:
+ if (iv[i].value != 0 && iv[i].value != 1) {
+ return 0;
+ }
+ break;
}
}
return 1;
diff --git a/deps/nghttp2/lib/nghttp2_hd.c b/deps/nghttp2/lib/nghttp2_hd.c
index 1eb3be33802c44..11ca3345f3c6b3 100644
--- a/deps/nghttp2/lib/nghttp2_hd.c
+++ b/deps/nghttp2/lib/nghttp2_hd.c
@@ -45,7 +45,7 @@
/* 3rd parameter is nghttp2_token value for header field name. We use
first enum value if same header names are repeated (e.g.,
:status). */
-static nghttp2_hd_static_entry static_table[] = {
+static const nghttp2_hd_static_entry static_table[] = {
MAKE_STATIC_ENT(":authority", "", 0, 3153725150u),
MAKE_STATIC_ENT(":method", "GET", 1, 695666056u),
MAKE_STATIC_ENT(":method", "POST", 1, 695666056u),
@@ -271,6 +271,15 @@ static int32_t lookup_token(const uint8_t *name, size_t namelen) {
break;
}
break;
+ case 9:
+ switch (name[8]) {
+ case 'l':
+ if (memeq(":protoco", name, 8)) {
+ return NGHTTP2_TOKEN__PROTOCOL;
+ }
+ break;
+ }
+ break;
case 10:
switch (name[9]) {
case 'e':
@@ -1159,7 +1168,7 @@ static search_result search_static_table(const nghttp2_nv *nv, int32_t token,
int name_only) {
search_result res = {token, 0};
int i;
- nghttp2_hd_static_entry *ent;
+ const nghttp2_hd_static_entry *ent;
if (name_only) {
return res;
@@ -1184,7 +1193,7 @@ static search_result search_hd_table(nghttp2_hd_context *context,
int indexing_mode, nghttp2_hd_map *map,
uint32_t hash) {
search_result res = {-1, 0};
- nghttp2_hd_entry *ent;
+ const nghttp2_hd_entry *ent;
int exact_match;
int name_only = indexing_mode == NGHTTP2_HD_NEVER_INDEXING;
@@ -1289,8 +1298,9 @@ nghttp2_hd_nv nghttp2_hd_table_get(nghttp2_hd_context *context, size_t idx) {
return hd_ringbuf_get(&context->hd_table, idx - NGHTTP2_STATIC_TABLE_LENGTH)
->nv;
} else {
- nghttp2_hd_static_entry *ent = &static_table[idx];
- nghttp2_hd_nv nv = {&ent->name, &ent->value, ent->token,
+ const nghttp2_hd_static_entry *ent = &static_table[idx];
+ nghttp2_hd_nv nv = {(nghttp2_rcbuf *)&ent->name,
+ (nghttp2_rcbuf *)&ent->value, ent->token,
NGHTTP2_NV_FLAG_NONE};
return nv;
}
@@ -1380,7 +1390,7 @@ static int deflate_nv(nghttp2_hd_deflater *deflater, nghttp2_bufs *bufs,
if (indexing_mode == NGHTTP2_HD_WITH_INDEXING) {
nghttp2_hd_nv hd_nv;
- if (idx != -1 && idx < (ssize_t)NGHTTP2_STATIC_TABLE_LENGTH) {
+ if (idx != -1) {
hd_nv.name = nghttp2_hd_table_get(&deflater->ctx, (size_t)idx).name;
nghttp2_rcbuf_incref(hd_nv.name);
} else {
diff --git a/deps/nghttp2/lib/nghttp2_hd.h b/deps/nghttp2/lib/nghttp2_hd.h
index c64a1f2b9b406c..14ae98078957af 100644
--- a/deps/nghttp2/lib/nghttp2_hd.h
+++ b/deps/nghttp2/lib/nghttp2_hd.h
@@ -111,6 +111,7 @@ typedef enum {
NGHTTP2_TOKEN_KEEP_ALIVE,
NGHTTP2_TOKEN_PROXY_CONNECTION,
NGHTTP2_TOKEN_UPGRADE,
+ NGHTTP2_TOKEN__PROTOCOL,
} nghttp2_token;
struct nghttp2_hd_entry;
diff --git a/deps/nghttp2/lib/nghttp2_helper.c b/deps/nghttp2/lib/nghttp2_helper.c
index 3b282c7301f95b..81a8a0cf99971a 100644
--- a/deps/nghttp2/lib/nghttp2_helper.c
+++ b/deps/nghttp2/lib/nghttp2_helper.c
@@ -340,7 +340,7 @@ const char *nghttp2_strerror(int error_code) {
}
/* Generated by gennmchartbl.py */
-static int VALID_HD_NAME_CHARS[] = {
+static const int VALID_HD_NAME_CHARS[] = {
0 /* NUL */, 0 /* SOH */, 0 /* STX */, 0 /* ETX */,
0 /* EOT */, 0 /* ENQ */, 0 /* ACK */, 0 /* BEL */,
0 /* BS */, 0 /* HT */, 0 /* LF */, 0 /* VT */,
@@ -428,7 +428,7 @@ int nghttp2_check_header_name(const uint8_t *name, size_t len) {
}
/* Generated by genvchartbl.py */
-static int VALID_HD_VALUE_CHARS[] = {
+static const int VALID_HD_VALUE_CHARS[] = {
0 /* NUL */, 0 /* SOH */, 0 /* STX */, 0 /* ETX */,
0 /* EOT */, 0 /* ENQ */, 0 /* ACK */, 0 /* BEL */,
0 /* BS */, 1 /* HT */, 0 /* LF */, 0 /* VT */,
diff --git a/deps/nghttp2/lib/nghttp2_http.c b/deps/nghttp2/lib/nghttp2_http.c
index b08f8863f7ce16..8d990299838193 100644
--- a/deps/nghttp2/lib/nghttp2_http.c
+++ b/deps/nghttp2/lib/nghttp2_http.c
@@ -113,7 +113,7 @@ static int check_path(nghttp2_stream *stream) {
}
static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv,
- int trailer) {
+ int trailer, int connect_protocol) {
if (nv->name->base[0] == ':') {
if (trailer ||
(stream->http_flags & NGHTTP2_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED)) {
@@ -146,10 +146,6 @@ static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv,
return NGHTTP2_ERR_HTTP_HEADER;
}
stream->http_flags |= NGHTTP2_HTTP_FLAG_METH_CONNECT;
- if (stream->http_flags &
- (NGHTTP2_HTTP_FLAG__PATH | NGHTTP2_HTTP_FLAG__SCHEME)) {
- return NGHTTP2_ERR_HTTP_HEADER;
- }
}
break;
case 'S':
@@ -162,9 +158,6 @@ static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv,
}
break;
case NGHTTP2_TOKEN__PATH:
- if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) {
- return NGHTTP2_ERR_HTTP_HEADER;
- }
if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG__PATH)) {
return NGHTTP2_ERR_HTTP_HEADER;
}
@@ -175,9 +168,6 @@ static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv,
}
break;
case NGHTTP2_TOKEN__SCHEME:
- if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) {
- return NGHTTP2_ERR_HTTP_HEADER;
- }
if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG__SCHEME)) {
return NGHTTP2_ERR_HTTP_HEADER;
}
@@ -186,6 +176,15 @@ static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv,
stream->http_flags |= NGHTTP2_HTTP_FLAG_SCHEME_HTTP;
}
break;
+ case NGHTTP2_TOKEN__PROTOCOL:
+ if (!connect_protocol) {
+ return NGHTTP2_ERR_HTTP_HEADER;
+ }
+
+ if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG__PROTOCOL)) {
+ return NGHTTP2_ERR_HTTP_HEADER;
+ }
+ break;
case NGHTTP2_TOKEN_HOST:
if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG_HOST)) {
return NGHTTP2_ERR_HTTP_HEADER;
@@ -264,11 +263,14 @@ static int http_response_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv,
stream->content_length = 0;
return NGHTTP2_ERR_REMOVE_HTTP_HEADER;
}
- if (stream->status_code / 100 == 1 ||
- (stream->status_code == 200 &&
- (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT))) {
+ if (stream->status_code / 100 == 1) {
return NGHTTP2_ERR_HTTP_HEADER;
}
+ /* https://tools.ietf.org/html/rfc7230#section-3.3.3 */
+ if (stream->status_code / 100 == 2 &&
+ (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT)) {
+ return NGHTTP2_ERR_REMOVE_HTTP_HEADER;
+ }
if (stream->content_length != -1) {
return NGHTTP2_ERR_HTTP_HEADER;
}
@@ -458,7 +460,9 @@ int nghttp2_http_on_header(nghttp2_session *session, nghttp2_stream *stream,
}
if (session->server || frame->hd.type == NGHTTP2_PUSH_PROMISE) {
- return http_request_on_header(stream, nv, trailer);
+ return http_request_on_header(stream, nv, trailer,
+ session->server &&
+ session->pending_enable_connect_protocol);
}
return http_response_on_header(stream, nv, trailer);
@@ -466,8 +470,11 @@ int nghttp2_http_on_header(nghttp2_session *session, nghttp2_stream *stream,
int nghttp2_http_on_request_headers(nghttp2_stream *stream,
nghttp2_frame *frame) {
- if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) {
- if ((stream->http_flags & NGHTTP2_HTTP_FLAG__AUTHORITY) == 0) {
+ if (!(stream->http_flags & NGHTTP2_HTTP_FLAG__PROTOCOL) &&
+ (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT)) {
+ if ((stream->http_flags &
+ (NGHTTP2_HTTP_FLAG__SCHEME | NGHTTP2_HTTP_FLAG__PATH)) ||
+ (stream->http_flags & NGHTTP2_HTTP_FLAG__AUTHORITY) == 0) {
return -1;
}
stream->content_length = -1;
@@ -478,6 +485,11 @@ int nghttp2_http_on_request_headers(nghttp2_stream *stream,
(NGHTTP2_HTTP_FLAG__AUTHORITY | NGHTTP2_HTTP_FLAG_HOST)) == 0) {
return -1;
}
+ if ((stream->http_flags & NGHTTP2_HTTP_FLAG__PROTOCOL) &&
+ ((stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) == 0 ||
+ (stream->http_flags & NGHTTP2_HTTP_FLAG__AUTHORITY) == 0)) {
+ return -1;
+ }
if (!check_path(stream)) {
return -1;
}
diff --git a/deps/nghttp2/lib/nghttp2_option.c b/deps/nghttp2/lib/nghttp2_option.c
index 8946d7dd38cfb8..e53f22d367f84a 100644
--- a/deps/nghttp2/lib/nghttp2_option.c
+++ b/deps/nghttp2/lib/nghttp2_option.c
@@ -116,3 +116,8 @@ void nghttp2_option_set_no_closed_streams(nghttp2_option *option, int val) {
option->opt_set_mask |= NGHTTP2_OPT_NO_CLOSED_STREAMS;
option->no_closed_streams = val;
}
+
+void nghttp2_option_set_max_outbound_ack(nghttp2_option *option, size_t val) {
+ option->opt_set_mask |= NGHTTP2_OPT_MAX_OUTBOUND_ACK;
+ option->max_outbound_ack = val;
+}
diff --git a/deps/nghttp2/lib/nghttp2_option.h b/deps/nghttp2/lib/nghttp2_option.h
index 29e72aa321007a..1f740aaa6e364e 100644
--- a/deps/nghttp2/lib/nghttp2_option.h
+++ b/deps/nghttp2/lib/nghttp2_option.h
@@ -66,6 +66,7 @@ typedef enum {
NGHTTP2_OPT_MAX_SEND_HEADER_BLOCK_LENGTH = 1 << 8,
NGHTTP2_OPT_MAX_DEFLATE_DYNAMIC_TABLE_SIZE = 1 << 9,
NGHTTP2_OPT_NO_CLOSED_STREAMS = 1 << 10,
+ NGHTTP2_OPT_MAX_OUTBOUND_ACK = 1 << 11,
} nghttp2_option_flag;
/**
@@ -80,6 +81,10 @@ struct nghttp2_option {
* NGHTTP2_OPT_MAX_DEFLATE_DYNAMIC_TABLE_SIZE
*/
size_t max_deflate_dynamic_table_size;
+ /**
+ * NGHTTP2_OPT_MAX_OUTBOUND_ACK
+ */
+ size_t max_outbound_ack;
/**
* Bitwise OR of nghttp2_option_flag to determine that which fields
* are specified.
diff --git a/deps/nghttp2/lib/nghttp2_session.c b/deps/nghttp2/lib/nghttp2_session.c
index 418ad6663585f5..3420cfa2f1c653 100644
--- a/deps/nghttp2/lib/nghttp2_session.c
+++ b/deps/nghttp2/lib/nghttp2_session.c
@@ -457,6 +457,7 @@ static int session_new(nghttp2_session **session_ptr,
(*session_ptr)->remote_settings.max_concurrent_streams = 100;
(*session_ptr)->max_send_header_block_length = NGHTTP2_MAX_HEADERSLEN;
+ (*session_ptr)->max_outbound_ack = NGHTTP2_DEFAULT_MAX_OBQ_FLOOD_ITEM;
if (option) {
if ((option->opt_set_mask & NGHTTP2_OPT_NO_AUTO_WINDOW_UPDATE) &&
@@ -516,6 +517,10 @@ static int session_new(nghttp2_session **session_ptr,
option->no_closed_streams) {
(*session_ptr)->opt_flags |= NGHTTP2_OPTMASK_NO_CLOSED_STREAMS;
}
+
+ if (option->opt_set_mask & NGHTTP2_OPT_MAX_OUTBOUND_ACK) {
+ (*session_ptr)->max_outbound_ack = option->max_outbound_ack;
+ }
}
rv = nghttp2_hd_deflate_init2(&(*session_ptr)->hd_deflater,
@@ -3619,71 +3624,73 @@ static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame,
if (call_header_cb && (inflate_flags & NGHTTP2_HD_INFLATE_EMIT)) {
rv = 0;
- if (subject_stream && session_enforce_http_messaging(session)) {
- rv = nghttp2_http_on_header(session, subject_stream, frame, &nv,
- trailer);
+ if (subject_stream) {
+ if (session_enforce_http_messaging(session)) {
+ rv = nghttp2_http_on_header(session, subject_stream, frame, &nv,
+ trailer);
- if (rv == NGHTTP2_ERR_IGN_HTTP_HEADER) {
- /* Don't overwrite rv here */
- int rv2;
+ if (rv == NGHTTP2_ERR_IGN_HTTP_HEADER) {
+ /* Don't overwrite rv here */
+ int rv2;
- rv2 = session_call_on_invalid_header(session, frame, &nv);
- if (rv2 == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
- rv = NGHTTP2_ERR_HTTP_HEADER;
- } else {
- if (rv2 != 0) {
- return rv2;
+ rv2 = session_call_on_invalid_header(session, frame, &nv);
+ if (rv2 == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
+ rv = NGHTTP2_ERR_HTTP_HEADER;
+ } else {
+ if (rv2 != 0) {
+ return rv2;
+ }
+
+ /* header is ignored */
+ DEBUGF("recv: HTTP ignored: type=%u, id=%d, header %.*s: %.*s\n",
+ frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
+ nv.name->base, (int)nv.value->len, nv.value->base);
+
+ rv2 = session_call_error_callback(
+ session, NGHTTP2_ERR_HTTP_HEADER,
+ "Ignoring received invalid HTTP header field: frame type: "
+ "%u, stream: %d, name: [%.*s], value: [%.*s]",
+ frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
+ nv.name->base, (int)nv.value->len, nv.value->base);
+
+ if (nghttp2_is_fatal(rv2)) {
+ return rv2;
+ }
}
+ }
- /* header is ignored */
- DEBUGF("recv: HTTP ignored: type=%u, id=%d, header %.*s: %.*s\n",
+ if (rv == NGHTTP2_ERR_HTTP_HEADER) {
+ DEBUGF("recv: HTTP error: type=%u, id=%d, header %.*s: %.*s\n",
frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
nv.name->base, (int)nv.value->len, nv.value->base);
- rv2 = session_call_error_callback(
+ rv = session_call_error_callback(
session, NGHTTP2_ERR_HTTP_HEADER,
- "Ignoring received invalid HTTP header field: frame type: "
+ "Invalid HTTP header field was received: frame type: "
"%u, stream: %d, name: [%.*s], value: [%.*s]",
frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
nv.name->base, (int)nv.value->len, nv.value->base);
- if (nghttp2_is_fatal(rv2)) {
- return rv2;
+ if (nghttp2_is_fatal(rv)) {
+ return rv;
}
- }
- }
-
- if (rv == NGHTTP2_ERR_HTTP_HEADER) {
- DEBUGF("recv: HTTP error: type=%u, id=%d, header %.*s: %.*s\n",
- frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
- nv.name->base, (int)nv.value->len, nv.value->base);
- rv = session_call_error_callback(
- session, NGHTTP2_ERR_HTTP_HEADER,
- "Invalid HTTP header field was received: frame type: "
- "%u, stream: %d, name: [%.*s], value: [%.*s]",
- frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
- nv.name->base, (int)nv.value->len, nv.value->base);
-
- if (nghttp2_is_fatal(rv)) {
- return rv;
+ rv = session_handle_invalid_stream2(session,
+ subject_stream->stream_id,
+ frame, NGHTTP2_ERR_HTTP_HEADER);
+ if (nghttp2_is_fatal(rv)) {
+ return rv;
+ }
+ return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
}
-
- rv =
- session_handle_invalid_stream2(session, subject_stream->stream_id,
- frame, NGHTTP2_ERR_HTTP_HEADER);
- if (nghttp2_is_fatal(rv)) {
+ }
+ if (rv == 0) {
+ rv = session_call_on_header(session, frame, &nv);
+ /* This handles NGHTTP2_ERR_PAUSE and
+ NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE as well */
+ if (rv != 0) {
return rv;
}
- return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
- }
- }
- if (rv == 0) {
- rv = session_call_on_header(session, frame, &nv);
- /* This handles NGHTTP2_ERR_PAUSE and
- NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE as well */
- if (rv != 0) {
- return rv;
}
}
}
@@ -4361,6 +4368,9 @@ int nghttp2_session_update_local_settings(nghttp2_session *session,
case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE:
session->local_settings.max_header_list_size = iv[i].value;
break;
+ case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL:
+ session->local_settings.enable_connect_protocol = iv[i].value;
+ break;
}
}
@@ -4499,6 +4509,26 @@ int nghttp2_session_on_settings_received(nghttp2_session *session,
session->remote_settings.max_header_list_size = entry->value;
+ break;
+ case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL:
+
+ if (entry->value != 0 && entry->value != 1) {
+ return session_handle_invalid_connection(
+ session, frame, NGHTTP2_ERR_PROTO,
+ "SETTINGS: invalid SETTINGS_ENABLE_CONNECT_PROTOCOL");
+ }
+
+ if (!session->server &&
+ session->remote_settings.enable_connect_protocol &&
+ entry->value == 0) {
+ return session_handle_invalid_connection(
+ session, frame, NGHTTP2_ERR_PROTO,
+ "SETTINGS: server attempted to disable "
+ "SETTINGS_ENABLE_CONNECT_PROTOCOL");
+ }
+
+ session->remote_settings.enable_connect_protocol = entry->value;
+
break;
}
}
@@ -5250,6 +5280,7 @@ static void inbound_frame_set_settings_entry(nghttp2_inbound_frame *iframe) {
case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE:
case NGHTTP2_SETTINGS_MAX_FRAME_SIZE:
case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE:
+ case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL:
break;
default:
DEBUGF("recv: unknown settings id=0x%02x\n", iv.settings_id);
@@ -6831,7 +6862,7 @@ int nghttp2_session_add_ping(nghttp2_session *session, uint8_t flags,
mem = &session->mem;
if ((flags & NGHTTP2_FLAG_ACK) &&
- session->obq_flood_counter_ >= NGHTTP2_MAX_OBQ_FLOOD_ITEM) {
+ session->obq_flood_counter_ >= session->max_outbound_ack) {
return NGHTTP2_ERR_FLOODED;
}
@@ -6976,7 +7007,7 @@ int nghttp2_session_add_settings(nghttp2_session *session, uint8_t flags,
return NGHTTP2_ERR_INVALID_ARGUMENT;
}
- if (session->obq_flood_counter_ >= NGHTTP2_MAX_OBQ_FLOOD_ITEM) {
+ if (session->obq_flood_counter_ >= session->max_outbound_ack) {
return NGHTTP2_ERR_FLOODED;
}
}
@@ -7052,6 +7083,13 @@ int nghttp2_session_add_settings(nghttp2_session *session, uint8_t flags,
}
}
+ for (i = niv; i > 0; --i) {
+ if (iv[i - 1].settings_id == NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL) {
+ session->pending_enable_connect_protocol = (uint8_t)iv[i - 1].value;
+ break;
+ }
+ }
+
return 0;
}
@@ -7360,6 +7398,8 @@ uint32_t nghttp2_session_get_remote_settings(nghttp2_session *session,
return session->remote_settings.max_frame_size;
case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE:
return session->remote_settings.max_header_list_size;
+ case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL:
+ return session->remote_settings.enable_connect_protocol;
}
assert(0);
@@ -7381,6 +7421,8 @@ uint32_t nghttp2_session_get_local_settings(nghttp2_session *session,
return session->local_settings.max_frame_size;
case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE:
return session->local_settings.max_header_list_size;
+ case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL:
+ return session->local_settings.enable_connect_protocol;
}
assert(0);
diff --git a/deps/nghttp2/lib/nghttp2_session.h b/deps/nghttp2/lib/nghttp2_session.h
index 5add50bc8bce16..90ead9c0395b4f 100644
--- a/deps/nghttp2/lib/nghttp2_session.h
+++ b/deps/nghttp2/lib/nghttp2_session.h
@@ -97,7 +97,7 @@ typedef struct {
response frames are stacked up, which leads to memory exhaustion.
The value selected here is arbitrary, but safe value and if we have
these frames in this number, it is considered suspicious. */
-#define NGHTTP2_MAX_OBQ_FLOOD_ITEM 10000
+#define NGHTTP2_DEFAULT_MAX_OBQ_FLOOD_ITEM 1000
/* The default value of maximum number of concurrent streams. */
#define NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS 0xffffffffu
@@ -164,6 +164,7 @@ typedef struct {
uint32_t initial_window_size;
uint32_t max_frame_size;
uint32_t max_header_list_size;
+ uint32_t enable_connect_protocol;
} nghttp2_settings_storage;
typedef enum {
@@ -208,9 +209,6 @@ struct nghttp2_session {
nghttp2_session_callbacks callbacks;
/* Memory allocator */
nghttp2_mem mem;
- /* Base value when we schedule next DATA frame write. This is
- updated when one frame was written. */
- uint64_t last_cycle;
void *user_data;
/* Points to the latest incoming closed stream. NULL if there is no
closed stream. Only used when session is initialized as
@@ -260,8 +258,12 @@ struct nghttp2_session {
size_t num_idle_streams;
/* The number of bytes allocated for nvbuf */
size_t nvbuflen;
- /* Counter for detecting flooding in outbound queue */
+ /* Counter for detecting flooding in outbound queue. If it exceeds
+ max_outbound_ack, session will be closed. */
size_t obq_flood_counter_;
+ /* The maximum number of outgoing SETTINGS ACK and PING ACK in
+ outbound queue. */
+ size_t max_outbound_ack;
/* The maximum length of header block to send. Calculated by the
same way as nghttp2_hd_deflate_bound() does. */
size_t max_send_header_block_length;
@@ -321,6 +323,9 @@ struct nghttp2_session {
/* Unacked local ENABLE_PUSH value. We use this to refuse
PUSH_PROMISE before SETTINGS ACK is received. */
uint8_t pending_enable_push;
+ /* Unacked local ENABLE_CONNECT_PROTOCOL value. We use this to
+ accept :protocol header field before SETTINGS_ACK is received. */
+ uint8_t pending_enable_connect_protocol;
/* Nonzero if the session is server side. */
uint8_t server;
/* Flags indicating GOAWAY is sent and/or received. The flags are
diff --git a/deps/nghttp2/lib/nghttp2_stream.c b/deps/nghttp2/lib/nghttp2_stream.c
index eccd3174ef7bda..dc3a6b11ccbf75 100644
--- a/deps/nghttp2/lib/nghttp2_stream.c
+++ b/deps/nghttp2/lib/nghttp2_stream.c
@@ -30,6 +30,7 @@
#include "nghttp2_session.h"
#include "nghttp2_helper.h"
#include "nghttp2_debug.h"
+#include "nghttp2_frame.h"
/* Maximum distance between any two stream's cycle in the same
prirority queue. Imagine stream A's cycle is A, and stream B's
@@ -40,7 +41,8 @@
words, B is really greater than or equal to A. Otherwise, A is a
result of overflow, and it is actually A > B if we consider that
fact. */
-#define NGHTTP2_MAX_CYCLE_DISTANCE (16384 * 256 + 255)
+#define NGHTTP2_MAX_CYCLE_DISTANCE \
+ ((uint64_t)NGHTTP2_MAX_FRAME_SIZE_MAX * 256 + 255)
static int stream_less(const void *lhsx, const void *rhsx) {
const nghttp2_stream *lhs, *rhs;
@@ -52,11 +54,7 @@ static int stream_less(const void *lhsx, const void *rhsx) {
return lhs->seq < rhs->seq;
}
- if (lhs->cycle < rhs->cycle) {
- return rhs->cycle - lhs->cycle <= NGHTTP2_MAX_CYCLE_DISTANCE;
- }
-
- return lhs->cycle - rhs->cycle > NGHTTP2_MAX_CYCLE_DISTANCE;
+ return rhs->cycle - lhs->cycle <= NGHTTP2_MAX_CYCLE_DISTANCE;
}
void nghttp2_stream_init(nghttp2_stream *stream, int32_t stream_id,
@@ -135,14 +133,14 @@ static int stream_subtree_active(nghttp2_stream *stream) {
/*
* Returns next cycle for |stream|.
*/
-static void stream_next_cycle(nghttp2_stream *stream, uint32_t last_cycle) {
- uint32_t penalty;
+static void stream_next_cycle(nghttp2_stream *stream, uint64_t last_cycle) {
+ uint64_t penalty;
- penalty = (uint32_t)stream->last_writelen * NGHTTP2_MAX_WEIGHT +
+ penalty = (uint64_t)stream->last_writelen * NGHTTP2_MAX_WEIGHT +
stream->pending_penalty;
stream->cycle = last_cycle + penalty / (uint32_t)stream->weight;
- stream->pending_penalty = penalty % (uint32_t)stream->weight;
+ stream->pending_penalty = (uint32_t)(penalty % (uint32_t)stream->weight);
}
static int stream_obq_push(nghttp2_stream *dep_stream, nghttp2_stream *stream) {
@@ -153,7 +151,7 @@ static int stream_obq_push(nghttp2_stream *dep_stream, nghttp2_stream *stream) {
stream_next_cycle(stream, dep_stream->descendant_last_cycle);
stream->seq = dep_stream->descendant_next_seq++;
- DEBUGF("stream: stream=%d obq push cycle=%d\n", stream->stream_id,
+ DEBUGF("stream: stream=%d obq push cycle=%lu\n", stream->stream_id,
stream->cycle);
DEBUGF("stream: push stream %d to stream %d\n", stream->stream_id,
@@ -239,7 +237,7 @@ void nghttp2_stream_reschedule(nghttp2_stream *stream) {
nghttp2_pq_push(&dep_stream->obq, &stream->pq_entry);
- DEBUGF("stream: stream=%d obq resched cycle=%d\n", stream->stream_id,
+ DEBUGF("stream: stream=%d obq resched cycle=%lu\n", stream->stream_id,
stream->cycle);
dep_stream->last_writelen = stream->last_writelen;
@@ -248,9 +246,9 @@ void nghttp2_stream_reschedule(nghttp2_stream *stream) {
void nghttp2_stream_change_weight(nghttp2_stream *stream, int32_t weight) {
nghttp2_stream *dep_stream;
- uint32_t last_cycle;
+ uint64_t last_cycle;
int32_t old_weight;
- uint32_t wlen_penalty;
+ uint64_t wlen_penalty;
if (stream->weight == weight) {
return;
@@ -273,7 +271,7 @@ void nghttp2_stream_change_weight(nghttp2_stream *stream, int32_t weight) {
nghttp2_pq_remove(&dep_stream->obq, &stream->pq_entry);
- wlen_penalty = (uint32_t)stream->last_writelen * NGHTTP2_MAX_WEIGHT;
+ wlen_penalty = (uint64_t)stream->last_writelen * NGHTTP2_MAX_WEIGHT;
/* Compute old stream->pending_penalty we used to calculate
stream->cycle */
@@ -289,9 +287,8 @@ void nghttp2_stream_change_weight(nghttp2_stream *stream, int32_t weight) {
place */
stream_next_cycle(stream, last_cycle);
- if (stream->cycle < dep_stream->descendant_last_cycle &&
- (dep_stream->descendant_last_cycle - stream->cycle) <=
- NGHTTP2_MAX_CYCLE_DISTANCE) {
+ if (dep_stream->descendant_last_cycle - stream->cycle <=
+ NGHTTP2_MAX_CYCLE_DISTANCE) {
stream->cycle = dep_stream->descendant_last_cycle;
}
@@ -299,7 +296,7 @@ void nghttp2_stream_change_weight(nghttp2_stream *stream, int32_t weight) {
nghttp2_pq_push(&dep_stream->obq, &stream->pq_entry);
- DEBUGF("stream: stream=%d obq resched cycle=%d\n", stream->stream_id,
+ DEBUGF("stream: stream=%d obq resched cycle=%lu\n", stream->stream_id,
stream->cycle);
}
diff --git a/deps/nghttp2/lib/nghttp2_stream.h b/deps/nghttp2/lib/nghttp2_stream.h
index d1d5856d800e76..a1b807d295c05c 100644
--- a/deps/nghttp2/lib/nghttp2_stream.h
+++ b/deps/nghttp2/lib/nghttp2_stream.h
@@ -130,7 +130,8 @@ typedef enum {
/* "http" or "https" scheme */
NGHTTP2_HTTP_FLAG_SCHEME_HTTP = 1 << 13,
/* set if final response is expected */
- NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE = 1 << 14
+ NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE = 1 << 14,
+ NGHTTP2_HTTP_FLAG__PROTOCOL = 1 << 15,
} nghttp2_http_flag;
struct nghttp2_stream {
@@ -147,9 +148,9 @@ struct nghttp2_stream {
/* Received body so far */
int64_t recv_content_length;
/* Base last_cycle for direct descendent streams. */
- uint32_t descendant_last_cycle;
+ uint64_t descendant_last_cycle;
/* Next scheduled time to sent item */
- uint32_t cycle;
+ uint64_t cycle;
/* Next seq used for direct descendant streams */
uint64_t descendant_next_seq;
/* Secondary key for prioritization to break a tie for cycle. This
diff --git a/doc/changelogs/CHANGELOG_V8.md b/doc/changelogs/CHANGELOG_V8.md
index 5aebf0e7c6cdf8..73a633664937d1 100644
--- a/doc/changelogs/CHANGELOG_V8.md
+++ b/doc/changelogs/CHANGELOG_V8.md
@@ -10,6 +10,7 @@
+8.16.1 8.16.0 8.15.1 8.15.0
@@ -63,6 +64,50 @@
[Node.js Long Term Support Plan](https://github.com/nodejs/LTS) and
will be supported actively until April 2019 and maintained until December 2019.
+
+## 2019-08-15, Version 8.16.1 'Carbon' (LTS), @BethGriggs
+
+### Notable changes
+
+This is a security release.
+
+Node.js, as well as many other implementations of HTTP/2, have been found
+vulnerable to Denial of Service attacks.
+See https://github.com/Netflix/security-bulletins/blob/master/advisories/third-party/2019-002.md
+for more information.
+
+Vulnerabilities fixed:
+
+* **CVE-2019-9511 “Data Dribble”**: The attacker requests a large amount of data from a specified resource over multiple streams. They manipulate window size and stream priority to force the server to queue the data in 1-byte chunks. Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both, potentially leading to a denial of service.
+* **CVE-2019-9512 “Ping Flood”**: The attacker sends continual pings to an HTTP/2 peer, causing the peer to build an internal queue of responses. Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both, potentially leading to a denial of service.
+* **CVE-2019-9513 “Resource Loop”**: The attacker creates multiple request streams and continually shuffles the priority of the streams in a way that causes substantial churn to the priority tree. This can consume excess CPU, potentially leading to a denial of service.
+* **CVE-2019-9514 “Reset Flood”**: The attacker opens a number of streams and sends an invalid request over each stream that should solicit a stream of RST_STREAM frames from the peer. Depending on how the peer queues the RST_STREAM frames, this can consume excess memory, CPU, or both, potentially leading to a denial of service.
+* **CVE-2019-9515 “Settings Flood”**: The attacker sends a stream of SETTINGS frames to the peer. Since the RFC requires that the peer reply with one acknowledgement per SETTINGS frame, an empty SETTINGS frame is almost equivalent in behavior to a ping. Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both, potentially leading to a denial of service.
+* **CVE-2019-9516 “0-Length Headers Leak”**: The attacker sends a stream of headers with a 0-length header name and 0-length header value, optionally Huffman encoded into 1-byte or greater headers. Some implementations allocate memory for these headers and keep the allocation alive until the session dies. This can consume excess memory, potentially leading to a denial of service.
+* **CVE-2019-9517 “Internal Data Buffering”**: The attacker opens the HTTP/2 window so the peer can send without constraint; however, they leave the TCP window closed so the peer cannot actually write (many of) the bytes on the wire. The attacker then sends a stream of requests for a large response object. Depending on how the servers queue the responses, this can consume excess memory, CPU, or both, potentially leading to a denial of service.
+* **CVE-2019-9518 “Empty Frames Flood”**: The attacker sends a stream of frames with an empty payload and without the end-of-stream flag. These frames can be DATA, HEADERS, CONTINUATION and/or PUSH_PROMISE. The peer spends time processing each frame disproportionate to attack bandwidth. This can consume excess CPU, potentially leading to a denial of service. (Discovered by Piotr Sikora of Google)
+
+### Commits
+
+* [[`6d427378c0`](https://github.com/nodejs/node/commit/6d427378c0)] - **deps**: update nghttp2 to 1.39.2 (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`33d4d916d5`](https://github.com/nodejs/node/commit/33d4d916d5)] - **deps**: update nghttp2 to 1.39.1 (gengjiawen) [#28448](https://github.com/nodejs/node/pull/28448)
+* [[`17fad97113`](https://github.com/nodejs/node/commit/17fad97113)] - **deps**: update nghttp2 to 1.38.0 (gengjiawen) [#27295](https://github.com/nodejs/node/pull/27295)
+* [[`0b44733695`](https://github.com/nodejs/node/commit/0b44733695)] - **deps**: update nghttp2 to 1.37.0 (gengjiawen) [#26990](https://github.com/nodejs/node/pull/26990)
+* [[`5afc77b044`](https://github.com/nodejs/node/commit/5afc77b044)] - **deps**: update nghttp2 to 1.34.0 (James M Snell) [#23284](https://github.com/nodejs/node/pull/23284)
+* [[`073108c855`](https://github.com/nodejs/node/commit/073108c855)] - **http2**: allow security revert for Ping/Settings Flood (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`6d687f7af8`](https://github.com/nodejs/node/commit/6d687f7af8)] - **http2**: pause input processing if sending output (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`854dba649e`](https://github.com/nodejs/node/commit/854dba649e)] - **http2**: stop reading from socket if writes are in progress (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`a3191689dd`](https://github.com/nodejs/node/commit/a3191689dd)] - **http2**: consider 0-length non-end DATA frames an error (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`156f2f35df`](https://github.com/nodejs/node/commit/156f2f35df)] - **http2**: shrink default `vector::reserve()` allocations (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`10f05b65c4`](https://github.com/nodejs/node/commit/10f05b65c4)] - **http2**: handle 0-length headers better (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`ac28a628a5`](https://github.com/nodejs/node/commit/ac28a628a5)] - **http2**: limit number of invalid incoming frames (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`11b4e2c0db`](https://github.com/nodejs/node/commit/11b4e2c0db)] - **http2**: limit number of rejected stream openings (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`7de642b6f9`](https://github.com/nodejs/node/commit/7de642b6f9)] - **http2**: do not create ArrayBuffers when no DATA received (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`dd60d3561a`](https://github.com/nodejs/node/commit/dd60d3561a)] - **http2**: only call into JS when necessary for session events (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`00f6846b73`](https://github.com/nodejs/node/commit/00f6846b73)] - **http2**: improve JS-side debug logging (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`b095e35f1f`](https://github.com/nodejs/node/commit/b095e35f1f)] - **http2**: improve http2 code a bit (James M Snell) [#23984](https://github.com/nodejs/node/pull/23984)
+* [[`cc282239c1`](https://github.com/nodejs/node/commit/cc282239c1)] - **test**: apply test-http2-max-session-memory-leak from v12.x (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+
## 2019-04-16, Version 8.16.0 'Carbon' (LTS), @MylesBorins
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 055009d07c815b..70974ecb74a7e8 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -64,6 +64,26 @@ const { createPromise, promiseResolve } = process.binding('util');
const { _connectionListener: httpConnectionListener } = http;
const debug = util.debuglog('http2');
+// TODO(addaleax): See if this can be made more efficient by figuring out
+// whether debugging is enabled before we perform any further steps. Currently,
+// this seems pretty fast, though.
+function debugStream(id, sessionType, message, ...args) {
+ debug('Http2Stream %s [Http2Session %s]: ' + message,
+ id, sessionName(sessionType), ...args);
+}
+
+function debugStreamObj(stream, message, ...args) {
+ debugStream(stream[kID], stream[kSession][kType], ...args);
+}
+
+function debugSession(sessionType, message, ...args) {
+ debug('Http2Session %s: ' + message, sessionName(sessionType), ...args);
+}
+
+function debugSessionObj(session, message, ...args) {
+ debugSession(session[kType], message, ...args);
+}
+
const kMaxFrameSize = (2 ** 24) - 1;
const kMaxInt = (2 ** 32) - 1;
const kMaxStreams = (2 ** 31) - 1;
@@ -88,6 +108,7 @@ const kInit = Symbol('init');
const kInfoHeaders = Symbol('sent-info-headers');
const kMaybeDestroy = Symbol('maybe-destroy');
const kLocalSettings = Symbol('local-settings');
+const kNativeFields = Symbol('kNativeFields');
const kOptions = Symbol('options');
const kOwner = Symbol('owner');
const kOrigin = Symbol('origin');
@@ -110,7 +131,15 @@ const {
paddingBuffer,
PADDING_BUF_FRAME_LENGTH,
PADDING_BUF_MAX_PAYLOAD_LENGTH,
- PADDING_BUF_RETURN_VALUE
+ PADDING_BUF_RETURN_VALUE,
+ kBitfield,
+ kSessionPriorityListenerCount,
+ kSessionFrameErrorListenerCount,
+ kSessionUint8FieldCount,
+ kSessionHasRemoteSettingsListeners,
+ kSessionRemoteSettingsIsUpToDate,
+ kSessionHasPingListeners,
+ kSessionHasAltsvcListeners,
} = binding;
const {
@@ -189,8 +218,7 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
const type = session[kType];
session[kUpdateTimer]();
- debug(`Http2Stream ${id} [Http2Session ` +
- `${sessionName(type)}]: headers received`);
+ debugStream(id, type, 'headers received');
const streams = session[kState].streams;
const endOfStream = !!(flags & NGHTTP2_FLAG_END_STREAM);
@@ -250,8 +278,7 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
const originSet = session[kState].originSet = initOriginSet(session);
originSet.delete(stream[kOrigin]);
}
- debug(`Http2Stream ${id} [Http2Session ` +
- `${sessionName(type)}]: emitting stream '${event}' event`);
+ debugStream(id, type, "emitting stream '%s' event", event);
process.nextTick(emit, stream, event, obj, flags, headers);
}
if (endOfStream) {
@@ -287,12 +314,82 @@ function submitRstStream(code) {
}
}
+// Keep track of the number/presence of JS event listeners. Knowing that there
+// are no listeners allows the C++ code to skip calling into JS for an event.
+function sessionListenerAdded(name) {
+ switch (name) {
+ case 'ping':
+ this[kNativeFields][kBitfield] |= 1 << kSessionHasPingListeners;
+ break;
+ case 'altsvc':
+ this[kNativeFields][kBitfield] |= 1 << kSessionHasAltsvcListeners;
+ break;
+ case 'remoteSettings':
+ this[kNativeFields][kBitfield] |= 1 << kSessionHasRemoteSettingsListeners;
+ break;
+ case 'priority':
+ this[kNativeFields][kSessionPriorityListenerCount]++;
+ break;
+ case 'frameError':
+ this[kNativeFields][kSessionFrameErrorListenerCount]++;
+ break;
+ }
+}
+
+function sessionListenerRemoved(name) {
+ switch (name) {
+ case 'ping':
+ if (this.listenerCount(name) > 0) return;
+ this[kNativeFields][kBitfield] &= ~(1 << kSessionHasPingListeners);
+ break;
+ case 'altsvc':
+ if (this.listenerCount(name) > 0) return;
+ this[kNativeFields][kBitfield] &= ~(1 << kSessionHasAltsvcListeners);
+ break;
+ case 'remoteSettings':
+ if (this.listenerCount(name) > 0) return;
+ this[kNativeFields][kBitfield] &=
+ ~(1 << kSessionHasRemoteSettingsListeners);
+ break;
+ case 'priority':
+ this[kNativeFields][kSessionPriorityListenerCount]--;
+ break;
+ case 'frameError':
+ this[kNativeFields][kSessionFrameErrorListenerCount]--;
+ break;
+ }
+}
+
+// Also keep track of listeners for the Http2Stream instances, as some events
+// are emitted on those objects.
+function streamListenerAdded(name) {
+ switch (name) {
+ case 'priority':
+ this[kSession][kNativeFields][kSessionPriorityListenerCount]++;
+ break;
+ case 'frameError':
+ this[kSession][kNativeFields][kSessionFrameErrorListenerCount]++;
+ break;
+ }
+}
+
+function streamListenerRemoved(name) {
+ switch (name) {
+ case 'priority':
+ this[kSession][kNativeFields][kSessionPriorityListenerCount]--;
+ break;
+ case 'frameError':
+ this[kSession][kNativeFields][kSessionFrameErrorListenerCount]--;
+ break;
+ }
+}
+
function onPing(payload) {
const session = this[kOwner];
if (session.destroyed)
return;
session[kUpdateTimer]();
- debug(`Http2Session ${sessionName(session[kType])}: new ping received`);
+ debugSessionObj(session, 'new ping received');
session.emit('ping', payload);
}
@@ -307,8 +404,7 @@ function onStreamClose(code) {
if (stream.destroyed)
return;
- debug(`Http2Stream ${stream[kID]} [Http2Session ` +
- `${sessionName(stream[kSession][kType])}]: closed with code ${code}`);
+ debugStreamObj(stream, 'closed with code %d', code);
if (!stream.closed)
closeStream(stream, code, kNoRstStream);
@@ -376,8 +472,7 @@ function onSettings() {
if (session.destroyed)
return;
session[kUpdateTimer]();
- debug(`Http2Session ${sessionName(session[kType])}: new settings received`);
- session[kRemoteSettings] = undefined;
+ debugSessionObj(session, 'new settings received');
session.emit('remoteSettings', session.remoteSettings);
}
@@ -388,9 +483,9 @@ function onPriority(id, parent, weight, exclusive) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug(`Http2Stream ${id} [Http2Session ` +
- `${sessionName(session[kType])}]: priority [parent: ${parent}, ` +
- `weight: ${weight}, exclusive: ${exclusive}]`);
+ debugStream(id, session[kType],
+ 'priority [parent: %d, weight: %d, exclusive: %s]',
+ parent, weight, exclusive);
const emitter = session[kState].streams.get(id) || session;
if (!emitter.destroyed) {
emitter[kUpdateTimer]();
@@ -404,8 +499,8 @@ function onFrameError(id, type, code) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug(`Http2Session ${sessionName(session[kType])}: error sending frame ` +
- `type ${type} on stream ${id}, code: ${code}`);
+ debugSessionObj(session, 'error sending frame type %d on stream %d, code: %d',
+ type, id, code);
const emitter = session[kState].streams.get(id) || session;
emitter[kUpdateTimer]();
emitter.emit('frameError', type, code, id);
@@ -415,8 +510,8 @@ function onAltSvc(stream, origin, alt) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug(`Http2Session ${sessionName(session[kType])}: altsvc received: ` +
- `stream: ${stream}, origin: ${origin}, alt: ${alt}`);
+ debugSessionObj(session, 'altsvc received: stream: %d, origin: %s, alt: %s',
+ stream, origin, alt);
session[kUpdateTimer]();
session.emit('altsvc', alt, origin, stream);
}
@@ -443,8 +538,7 @@ function onOrigin(origins) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug(`Http2Session ${sessionName(session[kType])}: origin received: ` +
- `${origins.join(', ')}`);
+ debugSessionObj(session, 'origin received: %j', origins);
session[kUpdateTimer]();
if (!session.encrypted || session.destroyed)
return undefined;
@@ -464,8 +558,8 @@ function onGoawayData(code, lastStreamID, buf) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug(`Http2Session ${sessionName(session[kType])}: goaway ${code} ` +
- `received [last stream id: ${lastStreamID}]`);
+ debugSessionObj(session, 'goaway %d received [last stream id: %d]',
+ code, lastStreamID);
const state = session[kState];
state.goawayCode = code;
@@ -519,8 +613,7 @@ function requestOnConnect(headers, options) {
return;
}
- debug(`Http2Session ${sessionName(session[kType])}: connected, ` +
- 'initializing request');
+ debugSessionObj(session, 'connected, initializing request');
let streamOptions = 0;
if (options.endStream)
@@ -623,13 +716,13 @@ function settingsCallback(cb, ack, duration) {
this[kState].pendingAck--;
this[kLocalSettings] = undefined;
if (ack) {
- debug(`Http2Session ${sessionName(this[kType])}: settings received`);
+ debugSessionObj(this, 'settings received');
const settings = this.localSettings;
if (typeof cb === 'function')
cb(null, settings, duration);
this.emit('localSettings', settings);
} else {
- debug(`Http2Session ${sessionName(this[kType])}: settings canceled`);
+ debugSessionObj(this, 'settings canceled');
if (typeof cb === 'function')
cb(new errors.Error('ERR_HTTP2_SETTINGS_CANCEL'));
}
@@ -639,7 +732,7 @@ function settingsCallback(cb, ack, duration) {
function submitSettings(settings, callback) {
if (this.destroyed)
return;
- debug(`Http2Session ${sessionName(this[kType])}: submitting settings`);
+ debugSessionObj(this, 'submitting settings');
this[kUpdateTimer]();
updateSettingsBuffer(settings);
if (!this[kHandle].settings(settingsCallback.bind(this, callback))) {
@@ -673,7 +766,7 @@ function submitPriority(options) {
function submitGoaway(code, lastStreamID, opaqueData) {
if (this.destroyed)
return;
- debug(`Http2Session ${sessionName(this[kType])}: submitting goaway`);
+ debugSessionObj(this, 'submitting goaway');
this[kUpdateTimer]();
this[kHandle].goaway(code, lastStreamID, opaqueData);
}
@@ -803,7 +896,9 @@ function setupHandle(socket, type, options) {
process.nextTick(emit, this, 'connect', this, socket);
return;
}
- debug(`Http2Session ${sessionName(type)}: setting up session handle`);
+
+ debugSession(type, 'setting up session handle');
+
this[kState].flags |= SESSION_FLAGS_READY;
updateOptionsBuffer(options);
@@ -828,6 +923,10 @@ function setupHandle(socket, type, options) {
handle.consume(socket._handle._externalStream);
this[kHandle] = handle;
+ if (this[kNativeFields])
+ handle.fields.set(this[kNativeFields]);
+ else
+ this[kNativeFields] = handle.fields;
if (socket.encrypted) {
this[kAlpnProtocol] = socket.alpnProtocol;
@@ -869,6 +968,7 @@ function finishSessionDestroy(session, error) {
session[kProxySocket] = undefined;
session[kSocket] = undefined;
session[kHandle] = undefined;
+ session[kNativeFields] = new Uint8Array(kSessionUint8FieldCount);
socket[kSession] = undefined;
socket[kServer] = undefined;
@@ -946,6 +1046,7 @@ class Http2Session extends EventEmitter {
this[kType] = type;
this[kProxySocket] = null;
this[kSocket] = socket;
+ this[kHandle] = undefined;
// Do not use nagle's algorithm
if (typeof socket.setNoDelay === 'function')
@@ -964,7 +1065,12 @@ class Http2Session extends EventEmitter {
setupFn();
}
- debug(`Http2Session ${sessionName(type)}: created`);
+ if (!this[kNativeFields])
+ this[kNativeFields] = new Uint8Array(kSessionUint8FieldCount);
+ this.on('newListener', sessionListenerAdded);
+ this.on('removeListener', sessionListenerRemoved);
+
+ debugSession(type, 'created');
}
// Returns undefined if the socket is not yet connected, true if the
@@ -1119,13 +1225,18 @@ class Http2Session extends EventEmitter {
// The settings currently in effect for the remote peer.
get remoteSettings() {
- const settings = this[kRemoteSettings];
- if (settings !== undefined)
- return settings;
+ if (this[kNativeFields][kBitfield] &
+ (1 << kSessionRemoteSettingsIsUpToDate)) {
+ const settings = this[kRemoteSettings];
+ if (settings !== undefined) {
+ return settings;
+ }
+ }
if (this.destroyed || this.connecting)
return {};
+ this[kNativeFields][kBitfield] |= (1 << kSessionRemoteSettingsIsUpToDate);
return this[kRemoteSettings] = getSettings(this[kHandle], true); // Remote
}
@@ -1138,7 +1249,7 @@ class Http2Session extends EventEmitter {
if (callback && typeof callback !== 'function')
throw new errors.TypeError('ERR_INVALID_CALLBACK');
- debug(`Http2Session ${sessionName(this[kType])}: sending settings`);
+ debugSessionObj(this, 'sending settings');
this[kState].pendingAck++;
@@ -1184,7 +1295,7 @@ class Http2Session extends EventEmitter {
destroy(error = NGHTTP2_NO_ERROR, code) {
if (this.destroyed)
return;
- debug(`Http2Session ${sessionName(this[kType])}: destroying`);
+ debugSessionObj(this, 'destroying');
if (typeof error === 'number') {
code = error;
@@ -1245,7 +1356,7 @@ class Http2Session extends EventEmitter {
close(callback) {
if (this.closed || this.destroyed)
return;
- debug(`Http2Session ${sessionName(this[kType])}: marking session closed`);
+ debugSessionObj(this, 'marking session closed');
this[kState].flags |= SESSION_FLAGS_CLOSED;
if (typeof callback === 'function')
this.once('close', callback);
@@ -1313,6 +1424,12 @@ class ServerHttp2Session extends Http2Session {
constructor(options, socket, server) {
super(NGHTTP2_SESSION_SERVER, options, socket);
this[kServer] = server;
+ // This is a bit inaccurate because it does not reflect changes to
+ // number of listeners made after the session was created. This should
+ // not be an issue in practice. Additionally, the 'priority' event on
+ // server instances (or any other object) is fully undocumented.
+ this[kNativeFields][kSessionPriorityListenerCount] =
+ server.listenerCount('priority');
}
get server() {
@@ -1415,7 +1532,7 @@ class ClientHttp2Session extends Http2Session {
// Submits a new HTTP2 request to the connected peer. Returns the
// associated Http2Stream instance.
request(headers, options) {
- debug(`Http2Session ${sessionName(this[kType])}: initiating request`);
+ debugSessionObj(this, 'initiating request');
if (this.destroyed)
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
@@ -1651,6 +1768,9 @@ class Http2Stream extends Duplex {
};
this.on('pause', streamOnPause);
+
+ this.on('newListener', streamListenerAdded);
+ this.on('removeListener', streamListenerRemoved);
}
[kUpdateTimer]() {
@@ -1843,8 +1963,7 @@ class Http2Stream extends Duplex {
if (this[kID] === undefined) {
this.once('ready', () => this._final(cb));
} else if (handle !== undefined) {
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(this[kSession][kType])}]: _final shutting down`);
+ debugStreamObj(this, '_final shutting down');
const req = new ShutdownWrap();
req.oncomplete = afterShutdown;
req.callback = cb;
@@ -1901,9 +2020,7 @@ class Http2Stream extends Duplex {
assertIsObject(headers, 'headers');
headers = Object.assign(Object.create(null), headers);
- const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: sending trailers`);
+ debugStreamObj(this, 'sending trailers');
this[kUpdateTimer]();
@@ -1959,8 +2076,8 @@ class Http2Stream extends Duplex {
const handle = this[kHandle];
const id = this[kID];
- debug(`Http2Stream ${this[kID] || ''} [Http2Session ` +
- `${sessionName(session[kType])}]: destroying stream`);
+ debugStream(this[kID] || 'pending', session[kType], 'destroying stream');
+
const state = this[kState];
const code = err != null ?
NGHTTP2_INTERNAL_ERROR : (state.rstCode || NGHTTP2_NO_ERROR);
@@ -2232,8 +2349,7 @@ class ServerHttp2Stream extends Http2Stream {
const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: initiating push stream`);
+ debugStreamObj(this, 'initiating push stream');
this[kUpdateTimer]();
@@ -2315,9 +2431,7 @@ class ServerHttp2Stream extends Http2Stream {
assertIsObject(options, 'options');
options = Object.assign({}, options);
- const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: initiating response`);
+ debugStreamObj(this, 'initiating response');
this[kUpdateTimer]();
options.endStream = !!options.endStream;
@@ -2404,8 +2518,7 @@ class ServerHttp2Stream extends Http2Stream {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
'fd', 'number');
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: initiating response`);
+ debugStreamObj(this, 'initiating response from fd');
this[kUpdateTimer]();
headers = processHeaders(headers);
@@ -2470,8 +2583,7 @@ class ServerHttp2Stream extends Http2Stream {
}
const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: initiating response`);
+ debugStreamObj(this, 'initiating response from file');
this[kUpdateTimer]();
@@ -2504,9 +2616,7 @@ class ServerHttp2Stream extends Http2Stream {
assertIsObject(headers, 'headers');
headers = Object.assign(Object.create(null), headers);
- const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: sending additional headers`);
+ debugStreamObj(this, 'sending additional headers');
if (headers[HTTP2_HEADER_STATUS] != null) {
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
@@ -2595,8 +2705,7 @@ function socketOnError(error) {
// we can do and the other side is fully within its rights to do so.
if (error.code === 'ECONNRESET' && session[kState].goawayCode !== null)
return session.destroy();
- debug(`Http2Session ${sessionName(session[kType])}: socket error [` +
- `${error.message}]`);
+ debugSessionObj(this, 'socket error [%s]', error.message);
session.destroy(error);
}
}
@@ -2614,7 +2723,7 @@ function sessionOnPriority(stream, parent, weight, exclusive) {
}
function sessionOnError(error) {
- if (this[kServer])
+ if (this[kServer] !== undefined)
this[kServer].emit('sessionError', error, this);
}
@@ -2641,7 +2750,8 @@ function connectionListener(socket) {
return httpConnectionListener.call(this, socket);
}
// Let event handler deal with the socket
- debug(`Unknown protocol from ${socket.remoteAddress}:${socket.remotePort}`);
+ debug('Unknown protocol from %s:%s',
+ socket.remoteAddress, socket.remotePort);
if (!this.emit('unknownProtocol', socket)) {
// We don't know what to do, so let's just tell the other side what's
// going on in a format that they *might* understand.
@@ -2662,8 +2772,10 @@ function connectionListener(socket) {
const session = new ServerHttp2Session(options, socket, this);
session.on('stream', sessionOnStream);
- session.on('priority', sessionOnPriority);
session.on('error', sessionOnError);
+ // Don't count our own internal listener.
+ session.on('priority', sessionOnPriority);
+ session[kNativeFields][kSessionPriorityListenerCount]--;
if (this.timeout)
session.setTimeout(this.timeout, sessionOnTimeout);
@@ -2766,7 +2878,7 @@ function setupCompat(ev) {
function socketOnClose() {
const session = this[kSession];
if (session !== undefined) {
- debug(`Http2Session ${sessionName(session[kType])}: socket closed`);
+ debugSessionObj(session, 'socket closed');
const err = session.connecting ?
new errors.Error('ERR_SOCKET_CLOSED') : null;
const state = session[kState];
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
index e7ef7db59077b3..ecf5b078886fa9 100644
--- a/lib/internal/http2/util.js
+++ b/lib/internal/http2/util.js
@@ -408,14 +408,20 @@ function mapToHeaders(map,
let count = 0;
const keys = Object.keys(map);
const singles = new Set();
- for (var i = 0; i < keys.length; i++) {
- let key = keys[i];
- let value = map[key];
+ let i;
+ let isArray;
+ let key;
+ let value;
+ let isSingleValueHeader;
+ let err;
+ for (i = 0; i < keys.length; i++) {
+ key = keys[i];
+ value = map[key];
if (value === undefined || key === '')
continue;
key = key.toLowerCase();
- const isSingleValueHeader = kSingleValueHeaders.has(key);
- let isArray = Array.isArray(value);
+ isSingleValueHeader = kSingleValueHeaders.has(key);
+ isArray = Array.isArray(value);
if (isArray) {
switch (value.length) {
case 0:
@@ -437,26 +443,26 @@ function mapToHeaders(map,
singles.add(key);
}
if (key[0] === ':') {
- const err = assertValuePseudoHeader(key);
+ err = assertValuePseudoHeader(key);
if (err !== undefined)
return err;
ret = `${key}\0${value}\0${ret}`;
count++;
- } else {
- if (isIllegalConnectionSpecificHeader(key, value)) {
- return new errors.Error('ERR_HTTP2_INVALID_CONNECTION_HEADERS', key);
- }
- if (isArray) {
- for (var k = 0; k < value.length; k++) {
- const val = String(value[k]);
- ret += `${key}\0${val}\0`;
- }
- count += value.length;
- } else {
- ret += `${key}\0${value}\0`;
- count++;
+ continue;
+ }
+ if (isIllegalConnectionSpecificHeader(key, value)) {
+ return new errors.Error('ERR_HTTP2_INVALID_CONNECTION_HEADERS', key);
+ }
+ if (isArray) {
+ for (var k = 0; k < value.length; k++) {
+ const val = String(value[k]);
+ ret += `${key}\0${val}\0`;
}
+ count += value.length;
+ continue;
}
+ ret += `${key}\0${value}\0`;
+ count++;
}
return [ret, count];
diff --git a/src/env.h b/src/env.h
index b47d55d87b3e69..fc2af3dfd02558 100644
--- a/src/env.h
+++ b/src/env.h
@@ -161,6 +161,7 @@ class ModuleWrap;
V(family_string, "family") \
V(fatal_exception_string, "_fatalException") \
V(fd_string, "fd") \
+ V(fields_string, "fields") \
V(file_string, "file") \
V(fingerprint_string, "fingerprint") \
V(flags_string, "flags") \
diff --git a/src/node_http2.cc b/src/node_http2.cc
index ea94713ecbaefe..eefad40a5dc3a6 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -4,6 +4,8 @@
#include "node_http2.h"
#include "node_http2_state.h"
#include "node_perf.h"
+#include "node_revert.h"
+#include "util-inl.h"
#include
@@ -20,6 +22,7 @@ using v8::ObjectTemplate;
using v8::String;
using v8::Uint32;
using v8::Uint32Array;
+using v8::Uint8Array;
using v8::Undefined;
using node::performance::PerformanceEntry;
@@ -141,6 +144,9 @@ Http2Options::Http2Options(Environment* env, nghttp2_session_type type) {
buffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS]);
}
+ if (IsReverted(SECURITY_REVERT_CVE_2019_9512))
+ nghttp2_option_set_max_outbound_ack(options_, 10000);
+
// The padding strategy sets the mechanism by which we determine how much
// additional frame padding to apply to DATA and HEADERS frames. Currently
// this is set on a per-session basis, but eventually we may switch to
@@ -665,8 +671,17 @@ Http2Session::Http2Session(Environment* env,
// fails.
CHECK_EQ(fn(&session_, callbacks, this, *opts, *allocator_info), 0);
- outgoing_storage_.reserve(4096);
+ outgoing_storage_.reserve(1024);
outgoing_buffers_.reserve(32);
+
+ {
+ // Make the js_fields_ property accessible to JS land.
+ Local ab =
+ ArrayBuffer::New(env->isolate(), js_fields_, kSessionUint8FieldCount);
+ Local uint8_arr =
+ Uint8Array::New(ab, 0, kSessionUint8FieldCount);
+ USE(wrap->Set(env->context(), env->fields_string(), uint8_arr));
+ }
}
void Http2Session::Unconsume() {
@@ -692,6 +707,7 @@ Http2Session::~Http2Session() {
DEBUG_HTTP2SESSION(this, "freeing nghttp2 session");
nghttp2_session_del(session_);
CHECK_EQ(current_nghttp2_memory_, 0);
+ free(stream_buf_allocation_.base);
}
inline bool HasHttp2Observer(Environment* env) {
@@ -905,31 +921,51 @@ inline ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
// various callback functions. Each of these will typically result in a call
// out to JavaScript so this particular function is rather hot and can be
// quite expensive. This is a potential performance optimization target later.
-inline ssize_t Http2Session::Write(const uv_buf_t* bufs, size_t nbufs) {
- size_t total = 0;
- // Note that nghttp2_session_mem_recv is a synchronous operation that
- // will trigger a number of other callbacks. Those will, in turn have
- // multiple side effects.
- for (size_t n = 0; n < nbufs; n++) {
- DEBUG_HTTP2SESSION2(this, "receiving %d bytes [wants data? %d]",
- bufs[n].len,
- nghttp2_session_want_read(session_));
- ssize_t ret =
- nghttp2_session_mem_recv(session_,
- reinterpret_cast(bufs[n].base),
- bufs[n].len);
- CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
-
- if (ret < 0)
- return ret;
+ssize_t Http2Session::ConsumeHTTP2Data() {
+ CHECK_NE(stream_buf_.base, nullptr);
+ CHECK_LT(stream_buf_offset_, stream_buf_.len);
+ size_t read_len = stream_buf_.len - stream_buf_offset_;
+
+ DEBUG_HTTP2SESSION2(this, "receiving %d bytes [wants data? %d]",
+ read_len,
+ nghttp2_session_want_read(session_));
+ flags_ &= ~SESSION_STATE_NGHTTP2_RECV_PAUSED;
+ ssize_t ret =
+ nghttp2_session_mem_recv(session_,
+ reinterpret_cast(stream_buf_.base) +
+ stream_buf_offset_,
+ read_len);
+ CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
+
+ if (flags_ & SESSION_STATE_NGHTTP2_RECV_PAUSED) {
+ CHECK_NE(flags_ & SESSION_STATE_READING_STOPPED, 0);
+
+ CHECK_GT(ret, 0);
+ CHECK_LE(static_cast(ret), read_len);
- total += ret;
+ if (static_cast(ret) < read_len) {
+ // Mark the remainder of the data as available for later consumption.
+ stream_buf_offset_ += ret;
+ return ret;
+ }
}
+
+ // We are done processing the current input chunk.
+ DecrementCurrentSessionMemory(stream_buf_.len);
+ stream_buf_offset_ = 0;
+ stream_buf_ab_.Reset();
+ free(stream_buf_allocation_.base);
+ stream_buf_allocation_ = uv_buf_init(nullptr, 0);
+ stream_buf_ = uv_buf_init(nullptr, 0);
+
+ if (ret < 0)
+ return ret;
+
// Send any data that was queued up while processing the received data.
if (!IsDestroyed()) {
SendPendingData();
}
- return total;
+ return ret;
}
@@ -953,19 +989,24 @@ inline int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle,
DEBUG_HTTP2SESSION2(session, "beginning headers for stream %d", id);
Http2Stream* stream = session->FindStream(id);
- if (stream == nullptr) {
- if (session->CanAddStream()) {
+ // The common case is that we're creating a new stream. The less likely
+ // case is that we're receiving a set of trailers
+ if (LIKELY(stream == nullptr)) {
+ if (LIKELY(session->CanAddStream())) {
new Http2Stream(session, id, frame->headers.cat);
} else {
+ if (session->rejected_stream_count_++ > 100 &&
+ !IsReverted(SECURITY_REVERT_CVE_2019_9514)) {
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ }
// Too many concurrent streams being opened
nghttp2_submit_rst_stream(**session, NGHTTP2_FLAG_NONE, id,
NGHTTP2_ENHANCE_YOUR_CALM);
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
}
- } else {
- // If the stream has already been destroyed, ignore.
- if (stream->IsDestroyed())
- return 0;
+
+ session->rejected_stream_count_ = 0;
+ } else if (!stream->IsDestroyed()) {
stream->StartHeaders(frame->headers.cat);
}
return 0;
@@ -986,7 +1027,7 @@ inline int Http2Session::OnHeaderCallback(nghttp2_session* handle,
// If stream is null at this point, either something odd has happened
// or the stream was closed locally while header processing was occurring.
// either way, do not proceed and close the stream.
- if (stream == nullptr)
+ if (UNLIKELY(stream == nullptr))
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
// If the stream has already been destroyed, ignore.
@@ -1003,18 +1044,17 @@ inline int Http2Session::OnHeaderCallback(nghttp2_session* handle,
// Called by nghttp2 when a complete HTTP2 frame has been received. There are
-// only a handful of frame types tha we care about handling here.
-inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
- const nghttp2_frame* frame,
- void* user_data) {
+// only a handful of frame types that we care about handling here.
+int Http2Session::OnFrameReceive(nghttp2_session* handle,
+ const nghttp2_frame* frame,
+ void* user_data) {
Http2Session* session = static_cast(user_data);
session->statistics_.frame_count++;
DEBUG_HTTP2SESSION2(session, "complete frame received: type: %d",
frame->hd.type);
switch (frame->hd.type) {
case NGHTTP2_DATA:
- session->HandleDataFrame(frame);
- break;
+ return session->HandleDataFrame(frame);
case NGHTTP2_PUSH_PROMISE:
// Intentional fall-through, handled just like headers frames
case NGHTTP2_HEADERS:
@@ -1052,6 +1092,10 @@ inline int Http2Session::OnInvalidFrame(nghttp2_session* handle,
DEBUG_HTTP2SESSION2(session, "invalid frame received, code: %d",
lib_error_code);
+ if (session->invalid_frame_count_++ > 1000 &&
+ !IsReverted(SECURITY_REVERT_CVE_2019_9514)) {
+ return 1;
+ }
// If the error is fatal or if error code is ERR_STREAM_CLOSED... emit error
if (nghttp2_is_fatal(lib_error_code) ||
@@ -1084,22 +1128,26 @@ inline int Http2Session::OnFrameNotSent(nghttp2_session* handle,
Environment* env = session->env();
DEBUG_HTTP2SESSION2(session, "frame type %d was not sent, code: %d",
frame->hd.type, error_code);
- // Do not report if the frame was not sent due to the session closing
- if (error_code != NGHTTP2_ERR_SESSION_CLOSING &&
- error_code != NGHTTP2_ERR_STREAM_CLOSED &&
- error_code != NGHTTP2_ERR_STREAM_CLOSING) {
- Isolate* isolate = env->isolate();
- HandleScope scope(isolate);
- Local context = env->context();
- Context::Scope context_scope(context);
- Local argv[3] = {
- Integer::New(isolate, frame->hd.stream_id),
- Integer::New(isolate, frame->hd.type),
- Integer::New(isolate, error_code)
- };
- session->MakeCallback(env->onframeerror_string(), arraysize(argv), argv);
+ // Do not report if the frame was not sent due to the session closing
+ if (error_code == NGHTTP2_ERR_SESSION_CLOSING ||
+ error_code == NGHTTP2_ERR_STREAM_CLOSED ||
+ error_code == NGHTTP2_ERR_STREAM_CLOSING ||
+ session->js_fields_[kSessionFrameErrorListenerCount] == 0) {
+ return 0;
}
+
+ Isolate* isolate = env->isolate();
+ HandleScope scope(isolate);
+ Local context = env->context();
+ Context::Scope context_scope(context);
+
+ Local argv[3] = {
+ Integer::New(isolate, frame->hd.stream_id),
+ Integer::New(isolate, frame->hd.type),
+ Integer::New(isolate, error_code)
+ };
+ session->MakeCallback(env->onframeerror_string(), arraysize(argv), argv);
return 0;
}
@@ -1126,25 +1174,26 @@ inline int Http2Session::OnStreamClose(nghttp2_session* handle,
Http2Stream* stream = session->FindStream(id);
// Intentionally ignore the callback if the stream does not exist or has
// already been destroyed
- if (stream != nullptr && !stream->IsDestroyed()) {
- stream->Close(code);
- // It is possible for the stream close to occur before the stream is
- // ever passed on to the javascript side. If that happens, skip straight
- // to destroying the stream. We can check this by looking for the
- // onstreamclose function. If it exists, then the stream has already
- // been passed on to javascript.
- Local fn =
- stream->object()->Get(context, env->onstreamclose_string())
- .ToLocalChecked();
- if (fn->IsFunction()) {
- Local argv[] = {
- Integer::NewFromUnsigned(isolate, code)
- };
- stream->MakeCallback(fn.As(), arraysize(argv), argv);
- } else {
- stream->Destroy();
- }
+ if (stream == nullptr || stream->IsDestroyed())
+ return 0;
+
+ stream->Close(code);
+ // It is possible for the stream close to occur before the stream is
+ // ever passed on to the javascript side. If that happens, skip straight
+ // to destroying the stream. We can check this by looking for the
+ // onstreamclose function. If it exists, then the stream has already
+ // been passed on to javascript.
+ Local fn =
+ stream->object()->Get(context, env->onstreamclose_string())
+ .ToLocalChecked();
+
+ if (!fn->IsFunction()) {
+ stream->Destroy();
+ return 0;
}
+
+ Local arg = Integer::NewFromUnsigned(isolate, code);
+ stream->MakeCallback(fn.As(), 1, &arg);
return 0;
}
@@ -1177,37 +1226,64 @@ inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
"%d, flags: %d", id, len, flags);
Environment* env = session->env();
HandleScope scope(env->isolate());
+
// We should never actually get a 0-length chunk so this check is
// only a precaution at this point.
- if (len > 0) {
- // Notify nghttp2 that we've consumed a chunk of data on the connection
- // so that it can send a WINDOW_UPDATE frame. This is a critical part of
- // the flow control process in http2
- CHECK_EQ(nghttp2_session_consume_connection(handle, len), 0);
- Http2Stream* stream = session->FindStream(id);
- // If the stream has been destroyed, ignore this chunk
- if (stream->IsDestroyed())
- return 0;
-
- stream->statistics_.received_bytes += len;
-
- // There is a single large array buffer for the entire data read from the
- // network; create a slice of that array buffer and emit it as the
- // received data buffer.
- CHECK(!session->stream_buf_ab_.IsEmpty());
- size_t offset = reinterpret_cast(data) - session->stream_buf_;
- // Verify that the data offset is inside the current read buffer.
- CHECK_LE(offset, session->stream_buf_size_);
-
- Local