diff --git a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h index 14f8950bed8d15..8c54b9c8cc464d 100644 --- a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h +++ b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h @@ -28,7 +28,7 @@ /* Define WIN32 when build target is Win32 API (borrowed from libcurl) */ #if (defined(_WIN32) || defined(__WIN32__)) && !defined(WIN32) -#define WIN32 +# define WIN32 #endif #ifdef __cplusplus @@ -40,9 +40,9 @@ extern "C" { /* MSVC < 2013 does not have inttypes.h because it is not C99 compliant. See compiler macros and version number in https://sourceforge.net/p/predef/wiki/Compilers/ */ -#include +# include #else /* !defined(_MSC_VER) || (_MSC_VER >= 1800) */ -#include +# include #endif /* !defined(_MSC_VER) || (_MSC_VER >= 1800) */ #include #include @@ -50,20 +50,20 @@ extern "C" { #include #ifdef NGHTTP2_STATICLIB -#define NGHTTP2_EXTERN +# define NGHTTP2_EXTERN #elif defined(WIN32) -#ifdef BUILDING_NGHTTP2 -#define NGHTTP2_EXTERN __declspec(dllexport) -#else /* !BUILDING_NGHTTP2 */ -#define NGHTTP2_EXTERN __declspec(dllimport) -#endif /* !BUILDING_NGHTTP2 */ -#else /* !defined(WIN32) */ -#ifdef BUILDING_NGHTTP2 -#define NGHTTP2_EXTERN __attribute__((visibility("default"))) -#else /* !BUILDING_NGHTTP2 */ -#define NGHTTP2_EXTERN -#endif /* !BUILDING_NGHTTP2 */ -#endif /* !defined(WIN32) */ +# ifdef BUILDING_NGHTTP2 +# define NGHTTP2_EXTERN __declspec(dllexport) +# else /* !BUILDING_NGHTTP2 */ +# define NGHTTP2_EXTERN __declspec(dllimport) +# endif /* !BUILDING_NGHTTP2 */ +#else /* !defined(WIN32) */ +# ifdef BUILDING_NGHTTP2 +# define NGHTTP2_EXTERN __attribute__((visibility("default"))) +# else /* !BUILDING_NGHTTP2 */ +# define NGHTTP2_EXTERN +# endif /* !BUILDING_NGHTTP2 */ +#endif /* !defined(WIN32) */ /** * @macro @@ -611,7 +611,12 @@ typedef enum { * The ALTSVC frame, which is defined in `RFC 7383 * `_. */ - NGHTTP2_ALTSVC = 0x0a + NGHTTP2_ALTSVC = 0x0a, + /** + * The ORIGIN frame, which is defined by `RFC 8336 + * `_. + */ + NGHTTP2_ORIGIN = 0x0c } nghttp2_frame_type; /** @@ -2473,15 +2478,15 @@ nghttp2_option_set_no_auto_window_update(nghttp2_option *option, int val); * * This option sets the SETTINGS_MAX_CONCURRENT_STREAMS value of * remote endpoint as if it is received in SETTINGS frame. Without - * specifying this option, before the local endpoint receives - * SETTINGS_MAX_CONCURRENT_STREAMS in SETTINGS frame from remote - * endpoint, SETTINGS_MAX_CONCURRENT_STREAMS is unlimited. This may - * cause problem if local endpoint submits lots of requests initially - * and sending them at once to the remote peer may lead to the - * rejection of some requests. Specifying this option to the sensible - * value, say 100, may avoid this kind of issue. This value will be - * overwritten if the local endpoint receives - * SETTINGS_MAX_CONCURRENT_STREAMS from the remote endpoint. + * specifying this option, the maximum number of outgoing concurrent + * streams is initially limited to 100 to avoid issues when the local + * endpoint submits lots of requests before receiving initial SETTINGS + * frame from the remote endpoint, since sending them at once to the + * remote endpoint could lead to rejection of some of the requests. + * This value will be overwritten when the local endpoint receives + * initial SETTINGS frame from the remote endpoint, either to the + * value advertised in SETTINGS_MAX_CONCURRENT_STREAMS or to the + * default value (unlimited) if none was advertised. */ NGHTTP2_EXTERN void nghttp2_option_set_peer_max_concurrent_streams(nghttp2_option *option, @@ -3797,10 +3802,13 @@ nghttp2_priority_spec_check_default(const nghttp2_priority_spec *pri_spec); * .. warning:: * * This function returns assigned stream ID if it succeeds. But - * that stream is not opened yet. The application must not submit + * that stream is not created yet. The application must not submit * frame to that stream ID before * :type:`nghttp2_before_frame_send_callback` is called for this - * frame. + * frame. This means `nghttp2_session_get_stream_user_data()` does + * not work before the callback. But + * `nghttp2_session_set_stream_user_data()` handles this situation + * specially, and it can set data to a stream during this period. * */ NGHTTP2_EXTERN int32_t nghttp2_submit_request( @@ -4516,8 +4524,7 @@ typedef struct { * Submits ALTSVC frame. * * ALTSVC frame is a non-critical extension to HTTP/2, and defined in - * is defined in `RFC 7383 - * `_. + * `RFC 7383 `_. * * The |flags| is currently ignored and should be * :enum:`NGHTTP2_FLAG_NONE`. @@ -4551,6 +4558,81 @@ NGHTTP2_EXTERN int nghttp2_submit_altsvc(nghttp2_session *session, const uint8_t *field_value, size_t field_value_len); +/** + * @struct + * + * The single entry of an origin. + */ +typedef struct { + /** + * The pointer to origin. No validation is made against this field + * by the library. This is not necessarily NULL-terminated. + */ + uint8_t *origin; + /** + * The length of the |origin|. + */ + size_t origin_len; +} nghttp2_origin_entry; + +/** + * @struct + * + * The payload of ORIGIN frame. ORIGIN frame is a non-critical + * extension to HTTP/2 and defined by `RFC 8336 + * `_. + * + * If this frame is received, and + * `nghttp2_option_set_user_recv_extension_type()` is not set, and + * `nghttp2_option_set_builtin_recv_extension_type()` is set for + * :enum:`NGHTTP2_ORIGIN`, ``nghttp2_extension.payload`` will point to + * this struct. + * + * It has the following members: + */ +typedef struct { + /** + * The number of origins contained in |ov|. + */ + size_t nov; + /** + * The pointer to the array of origins contained in ORIGIN frame. + */ + nghttp2_origin_entry *ov; +} nghttp2_ext_origin; + +/** + * @function + * + * Submits ORIGIN frame. + * + * ORIGIN frame is a non-critical extension to HTTP/2 and defined by + * `RFC 8336 `_. + * + * The |flags| is currently ignored and should be + * :enum:`NGHTTP2_FLAG_NONE`. + * + * The |ov| points to the array of origins. The |nov| specifies the + * number of origins included in |ov|. This function creates copies + * of all elements in |ov|. + * + * The ORIGIN frame is only usable by a server. If this function is + * invoked with client side session, this function returns + * :enum:`NGHTTP2_ERR_INVALID_STATE`. + * + * :enum:`NGHTTP2_ERR_NOMEM` + * Out of memory + * :enum:`NGHTTP2_ERR_INVALID_STATE` + * The function is called from client side session. + * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` + * There are too many origins, or an origin is too large to fit + * into a default frame payload. + */ +NGHTTP2_EXTERN int nghttp2_submit_origin(nghttp2_session *session, + uint8_t flags, + const nghttp2_origin_entry *ov, + size_t nov); + /** * @function * diff --git a/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h b/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h index d32d2754441b25..1f1d4808ca27c0 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.32.0" +#define NGHTTP2_VERSION "1.33.0" /** * @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 0x012000 +#define NGHTTP2_VERSION_NUM 0x012100 #endif /* NGHTTP2VER_H */ diff --git a/deps/nghttp2/lib/nghttp2_buf.h b/deps/nghttp2/lib/nghttp2_buf.h index 9f484a221acb5f..06cce67a11bdea 100644 --- a/deps/nghttp2/lib/nghttp2_buf.h +++ b/deps/nghttp2/lib/nghttp2_buf.h @@ -26,7 +26,7 @@ #define NGHTTP2_BUF_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_callbacks.h b/deps/nghttp2/lib/nghttp2_callbacks.h index b607bbb58b8e3d..61e51fa53638de 100644 --- a/deps/nghttp2/lib/nghttp2_callbacks.h +++ b/deps/nghttp2/lib/nghttp2_callbacks.h @@ -26,7 +26,7 @@ #define NGHTTP2_CALLBACKS_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_debug.h b/deps/nghttp2/lib/nghttp2_debug.h index 2e2cd0d145e5ba..cbb4dd57547234 100644 --- a/deps/nghttp2/lib/nghttp2_debug.h +++ b/deps/nghttp2/lib/nghttp2_debug.h @@ -26,18 +26,18 @@ #define NGHTTP2_DEBUG_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include #ifdef DEBUGBUILD -#define DEBUGF(...) nghttp2_debug_vprintf(__VA_ARGS__) +# define DEBUGF(...) nghttp2_debug_vprintf(__VA_ARGS__) void nghttp2_debug_vprintf(const char *format, ...); #else -#define DEBUGF(...) \ - do { \ - } while (0) +# define DEBUGF(...) \ + do { \ + } while (0) #endif #endif /* NGHTTP2_DEBUG_H */ diff --git a/deps/nghttp2/lib/nghttp2_frame.c b/deps/nghttp2/lib/nghttp2_frame.c index fa7cb6953bc539..6e33f3c247f5cb 100644 --- a/deps/nghttp2/lib/nghttp2_frame.c +++ b/deps/nghttp2/lib/nghttp2_frame.c @@ -223,6 +223,36 @@ void nghttp2_frame_altsvc_free(nghttp2_extension *frame, nghttp2_mem *mem) { nghttp2_mem_free(mem, altsvc->origin); } +void nghttp2_frame_origin_init(nghttp2_extension *frame, + nghttp2_origin_entry *ov, size_t nov) { + nghttp2_ext_origin *origin; + size_t payloadlen = 0; + size_t i; + + for (i = 0; i < nov; ++i) { + payloadlen += 2 + ov[i].origin_len; + } + + nghttp2_frame_hd_init(&frame->hd, payloadlen, NGHTTP2_ORIGIN, + NGHTTP2_FLAG_NONE, 0); + + origin = frame->payload; + origin->ov = ov; + origin->nov = nov; +} + +void nghttp2_frame_origin_free(nghttp2_extension *frame, nghttp2_mem *mem) { + nghttp2_ext_origin *origin; + + origin = frame->payload; + if (origin == NULL) { + return; + } + /* We use the same buffer for all resources pointed by the field of + origin directly or indirectly. */ + nghttp2_mem_free(mem, origin->ov); +} + size_t nghttp2_frame_priority_len(uint8_t flags) { if (flags & NGHTTP2_FLAG_PRIORITY) { return NGHTTP2_PRIORITY_SPECLEN; @@ -746,6 +776,106 @@ int nghttp2_frame_unpack_altsvc_payload2(nghttp2_extension *frame, return 0; } +int nghttp2_frame_pack_origin(nghttp2_bufs *bufs, nghttp2_extension *frame) { + nghttp2_buf *buf; + nghttp2_ext_origin *origin; + nghttp2_origin_entry *orig; + size_t i; + + origin = frame->payload; + + buf = &bufs->head->buf; + + if (nghttp2_buf_avail(buf) < frame->hd.length) { + return NGHTTP2_ERR_FRAME_SIZE_ERROR; + } + + buf->pos -= NGHTTP2_FRAME_HDLEN; + + nghttp2_frame_pack_frame_hd(buf->pos, &frame->hd); + + for (i = 0; i < origin->nov; ++i) { + orig = &origin->ov[i]; + nghttp2_put_uint16be(buf->last, (uint16_t)orig->origin_len); + buf->last += 2; + buf->last = nghttp2_cpymem(buf->last, orig->origin, orig->origin_len); + } + + assert(nghttp2_buf_len(buf) == NGHTTP2_FRAME_HDLEN + frame->hd.length); + + return 0; +} + +int nghttp2_frame_unpack_origin_payload(nghttp2_extension *frame, + const uint8_t *payload, + size_t payloadlen, nghttp2_mem *mem) { + nghttp2_ext_origin *origin; + const uint8_t *p, *end; + uint8_t *dst; + size_t originlen; + nghttp2_origin_entry *ov; + size_t nov = 0; + size_t len = 0; + + origin = frame->payload; + p = payload; + end = p + payloadlen; + + for (; p != end;) { + if (end - p < 2) { + return NGHTTP2_ERR_FRAME_SIZE_ERROR; + } + originlen = nghttp2_get_uint16(p); + p += 2; + if (originlen == 0) { + continue; + } + if (originlen > (size_t)(end - p)) { + return NGHTTP2_ERR_FRAME_SIZE_ERROR; + } + p += originlen; + /* 1 for terminal NULL */ + len += originlen + 1; + ++nov; + } + + if (nov == 0) { + origin->ov = NULL; + origin->nov = 0; + + return 0; + } + + len += nov * sizeof(nghttp2_origin_entry); + + ov = nghttp2_mem_malloc(mem, len); + if (ov == NULL) { + return NGHTTP2_ERR_NOMEM; + } + + origin->ov = ov; + origin->nov = nov; + + dst = (uint8_t *)ov + nov * sizeof(nghttp2_origin_entry); + p = payload; + + for (; p != end;) { + originlen = nghttp2_get_uint16(p); + p += 2; + if (originlen == 0) { + continue; + } + ov->origin = dst; + ov->origin_len = originlen; + dst = nghttp2_cpymem(dst, p, originlen); + *dst++ = '\0'; + p += originlen; + ++ov; + } + + return 0; +} + nghttp2_settings_entry *nghttp2_frame_iv_copy(const nghttp2_settings_entry *iv, size_t niv, nghttp2_mem *mem) { nghttp2_settings_entry *iv_copy; diff --git a/deps/nghttp2/lib/nghttp2_frame.h b/deps/nghttp2/lib/nghttp2_frame.h index 35ca214a4a7a59..615bbf31f5d60d 100644 --- a/deps/nghttp2/lib/nghttp2_frame.h +++ b/deps/nghttp2/lib/nghttp2_frame.h @@ -26,7 +26,7 @@ #define NGHTTP2_FRAME_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include @@ -72,6 +72,7 @@ /* Union of extension frame payload */ typedef union { nghttp2_ext_altsvc altsvc; + nghttp2_ext_origin origin; } nghttp2_ext_frame_payload; void nghttp2_frame_pack_frame_hd(uint8_t *buf, const nghttp2_frame_hd *hd); @@ -392,6 +393,36 @@ int nghttp2_frame_unpack_altsvc_payload2(nghttp2_extension *frame, const uint8_t *payload, size_t payloadlen, nghttp2_mem *mem); +/* + * Packs ORIGIN frame |frame| in wire frame format and store it in + * |bufs|. + * + * The caller must make sure that nghttp2_bufs_reset(bufs) is called + * before calling this function. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP2_ERR_FRAME_SIZE_ERROR + * The length of the frame is too large. + */ +int nghttp2_frame_pack_origin(nghttp2_bufs *bufs, nghttp2_extension *ext); + +/* + * Unpacks ORIGIN wire format into |frame|. The |payload| of length + * |payloadlen| contains the frame payload. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP2_ERR_NOMEM + * Out of memory. + * NGHTTP2_ERR_FRAME_SIZE_ERROR + * The payload is too small. + */ +int nghttp2_frame_unpack_origin_payload(nghttp2_extension *frame, + const uint8_t *payload, + size_t payloadlen, nghttp2_mem *mem); /* * Initializes HEADERS frame |frame| with given values. |frame| takes * ownership of |nva|, so caller must not free it. If |stream_id| is @@ -489,6 +520,24 @@ void nghttp2_frame_altsvc_init(nghttp2_extension *frame, int32_t stream_id, */ void nghttp2_frame_altsvc_free(nghttp2_extension *frame, nghttp2_mem *mem); +/* + * Initializes ORIGIN frame |frame| with given values. This function + * assumes that frame->payload points to nghttp2_ext_origin object. + * Also |ov| and the memory pointed by the field of its elements are + * allocated in single buffer, starting with |ov|. On success, this + * function takes ownership of |ov|, so caller must not free it. + */ +void nghttp2_frame_origin_init(nghttp2_extension *frame, + nghttp2_origin_entry *ov, size_t nov); + +/* + * Frees up resources under |frame|. This function does not free + * nghttp2_ext_origin object pointed by frame->payload. This function + * only frees nghttp2_ext_origin.ov. Therefore, other fields must be + * allocated in the same buffer with ov. + */ +void nghttp2_frame_origin_free(nghttp2_extension *frame, nghttp2_mem *mem); + /* * Returns the number of padding bytes after payload. The total * padding length is given in the |padlen|. The returned value does diff --git a/deps/nghttp2/lib/nghttp2_hd.h b/deps/nghttp2/lib/nghttp2_hd.h index 760bfbc357efdc..c64a1f2b9b406c 100644 --- a/deps/nghttp2/lib/nghttp2_hd.h +++ b/deps/nghttp2/lib/nghttp2_hd.h @@ -26,7 +26,7 @@ #define NGHTTP2_HD_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_hd_huffman.h b/deps/nghttp2/lib/nghttp2_hd_huffman.h index 83323400313509..c6e3942e95f4fc 100644 --- a/deps/nghttp2/lib/nghttp2_hd_huffman.h +++ b/deps/nghttp2/lib/nghttp2_hd_huffman.h @@ -26,7 +26,7 @@ #define NGHTTP2_HD_HUFFMAN_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_helper.h b/deps/nghttp2/lib/nghttp2_helper.h index 4a32564f39dfa3..b1f18ce541ac36 100644 --- a/deps/nghttp2/lib/nghttp2_helper.h +++ b/deps/nghttp2/lib/nghttp2_helper.h @@ -26,7 +26,7 @@ #define NGHTTP2_HELPER_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_http.h b/deps/nghttp2/lib/nghttp2_http.h index ac684c4d9ecbb1..dd057cdb60757f 100644 --- a/deps/nghttp2/lib/nghttp2_http.h +++ b/deps/nghttp2/lib/nghttp2_http.h @@ -26,7 +26,7 @@ #define NGHTTP2_HTTP_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_int.h b/deps/nghttp2/lib/nghttp2_int.h index 30cf7274bc4675..b23585ccb27da2 100644 --- a/deps/nghttp2/lib/nghttp2_int.h +++ b/deps/nghttp2/lib/nghttp2_int.h @@ -26,7 +26,7 @@ #define NGHTTP2_INT_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_map.h b/deps/nghttp2/lib/nghttp2_map.h index 21262488d6d2d4..f6e29e35f2de3f 100644 --- a/deps/nghttp2/lib/nghttp2_map.h +++ b/deps/nghttp2/lib/nghttp2_map.h @@ -26,7 +26,7 @@ #define NGHTTP2_MAP_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_mem.h b/deps/nghttp2/lib/nghttp2_mem.h index 2d1bd6a0f42396..f83dbcb8f9a588 100644 --- a/deps/nghttp2/lib/nghttp2_mem.h +++ b/deps/nghttp2/lib/nghttp2_mem.h @@ -26,7 +26,7 @@ #define NGHTTP2_MEM_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_net.h b/deps/nghttp2/lib/nghttp2_net.h index 587f4189fdba4a..95ffee74a14fc9 100644 --- a/deps/nghttp2/lib/nghttp2_net.h +++ b/deps/nghttp2/lib/nghttp2_net.h @@ -26,15 +26,15 @@ #define NGHTTP2_NET_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #ifdef HAVE_ARPA_INET_H -#include +# include #endif /* HAVE_ARPA_INET_H */ #ifdef HAVE_NETINET_IN_H -#include +# include #endif /* HAVE_NETINET_IN_H */ #include @@ -44,11 +44,11 @@ define inline functions for those function so that we don't have dependeny on that lib. */ -#ifdef _MSC_VER -#define STIN static __inline -#else -#define STIN static inline -#endif +# ifdef _MSC_VER +# define STIN static __inline +# else +# define STIN static inline +# endif STIN uint32_t htonl(uint32_t hostlong) { uint32_t res; diff --git a/deps/nghttp2/lib/nghttp2_npn.h b/deps/nghttp2/lib/nghttp2_npn.h index a481bde3507ce5..c6f1c04b683594 100644 --- a/deps/nghttp2/lib/nghttp2_npn.h +++ b/deps/nghttp2/lib/nghttp2_npn.h @@ -26,7 +26,7 @@ #define NGHTTP2_NPN_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_option.c b/deps/nghttp2/lib/nghttp2_option.c index aec5dcfa8ab542..8946d7dd38cfb8 100644 --- a/deps/nghttp2/lib/nghttp2_option.c +++ b/deps/nghttp2/lib/nghttp2_option.c @@ -86,6 +86,10 @@ void nghttp2_option_set_builtin_recv_extension_type(nghttp2_option *option, option->opt_set_mask |= NGHTTP2_OPT_BUILTIN_RECV_EXT_TYPES; option->builtin_recv_ext_types |= NGHTTP2_TYPEMASK_ALTSVC; return; + case NGHTTP2_ORIGIN: + option->opt_set_mask |= NGHTTP2_OPT_BUILTIN_RECV_EXT_TYPES; + option->builtin_recv_ext_types |= NGHTTP2_TYPEMASK_ORIGIN; + return; default: return; } diff --git a/deps/nghttp2/lib/nghttp2_option.h b/deps/nghttp2/lib/nghttp2_option.h index c743e33b8ed551..29e72aa321007a 100644 --- a/deps/nghttp2/lib/nghttp2_option.h +++ b/deps/nghttp2/lib/nghttp2_option.h @@ -26,7 +26,7 @@ #define NGHTTP2_OPTION_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_outbound_item.c b/deps/nghttp2/lib/nghttp2_outbound_item.c index 1633cc36859da1..f651c8029ac024 100644 --- a/deps/nghttp2/lib/nghttp2_outbound_item.c +++ b/deps/nghttp2/lib/nghttp2_outbound_item.c @@ -86,6 +86,9 @@ void nghttp2_outbound_item_free(nghttp2_outbound_item *item, nghttp2_mem *mem) { case NGHTTP2_ALTSVC: nghttp2_frame_altsvc_free(&frame->ext, mem); break; + case NGHTTP2_ORIGIN: + nghttp2_frame_origin_free(&frame->ext, mem); + break; default: assert(0); break; diff --git a/deps/nghttp2/lib/nghttp2_outbound_item.h b/deps/nghttp2/lib/nghttp2_outbound_item.h index 89a8a92668dd5c..b5f503a312dd8c 100644 --- a/deps/nghttp2/lib/nghttp2_outbound_item.h +++ b/deps/nghttp2/lib/nghttp2_outbound_item.h @@ -26,7 +26,7 @@ #define NGHTTP2_OUTBOUND_ITEM_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_pq.h b/deps/nghttp2/lib/nghttp2_pq.h index 71cf96a14e0c77..2d7b702ac18ad0 100644 --- a/deps/nghttp2/lib/nghttp2_pq.h +++ b/deps/nghttp2/lib/nghttp2_pq.h @@ -26,7 +26,7 @@ #define NGHTTP2_PQ_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_priority_spec.h b/deps/nghttp2/lib/nghttp2_priority_spec.h index 98fac21060091e..92ece822a8f257 100644 --- a/deps/nghttp2/lib/nghttp2_priority_spec.h +++ b/deps/nghttp2/lib/nghttp2_priority_spec.h @@ -26,7 +26,7 @@ #define NGHTTP2_PRIORITY_SPEC_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_queue.h b/deps/nghttp2/lib/nghttp2_queue.h index c7eb753ca92182..a06fa6c7a46fc7 100644 --- a/deps/nghttp2/lib/nghttp2_queue.h +++ b/deps/nghttp2/lib/nghttp2_queue.h @@ -26,7 +26,7 @@ #define NGHTTP2_QUEUE_H #ifdef HAVE_CONFIG_H -#include "config.h" +# include "config.h" #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_rcbuf.h b/deps/nghttp2/lib/nghttp2_rcbuf.h index 29d1543e2c5965..6814e709fb4148 100644 --- a/deps/nghttp2/lib/nghttp2_rcbuf.h +++ b/deps/nghttp2/lib/nghttp2_rcbuf.h @@ -26,7 +26,7 @@ #define NGHTTP2_RCBUF_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_session.c b/deps/nghttp2/lib/nghttp2_session.c index a9e7a62390e56a..418ad6663585f5 100644 --- a/deps/nghttp2/lib/nghttp2_session.c +++ b/deps/nghttp2/lib/nghttp2_session.c @@ -348,6 +348,12 @@ static void session_inbound_frame_reset(nghttp2_session *session) { } nghttp2_frame_altsvc_free(&iframe->frame.ext, mem); break; + case NGHTTP2_ORIGIN: + if ((session->builtin_recv_ext_types & NGHTTP2_TYPEMASK_ORIGIN) == 0) { + break; + } + nghttp2_frame_origin_free(&iframe->frame.ext, mem); + break; } } @@ -1749,6 +1755,13 @@ static int session_predicate_altsvc_send(nghttp2_session *session, return 0; } +static int session_predicate_origin_send(nghttp2_session *session) { + if (session_is_closing(session)) { + return NGHTTP2_ERR_SESSION_CLOSING; + } + return 0; +} + /* Take into account settings max frame size and both connection-level flow control here */ static ssize_t @@ -2280,6 +2293,18 @@ static int session_prep_frame(nghttp2_session *session, nghttp2_frame_pack_altsvc(&session->aob.framebufs, &frame->ext); + return 0; + case NGHTTP2_ORIGIN: + rv = session_predicate_origin_send(session); + if (rv != 0) { + return rv; + } + + rv = nghttp2_frame_pack_origin(&session->aob.framebufs, &frame->ext); + if (rv != 0) { + return rv; + } + return 0; default: /* Unreachable here */ @@ -4385,6 +4410,12 @@ int nghttp2_session_on_settings_received(nghttp2_session *session, return session_call_on_frame_received(session, frame); } + if (!session->remote_settings_received) { + session->remote_settings.max_concurrent_streams = + NGHTTP2_DEFAULT_MAX_CONCURRENT_STREAMS; + session->remote_settings_received = 1; + } + for (i = 0; i < frame->settings.niv; ++i) { nghttp2_settings_entry *entry = &frame->settings.iv[i]; @@ -4821,6 +4852,11 @@ int nghttp2_session_on_altsvc_received(nghttp2_session *session, return session_call_on_frame_received(session, frame); } +int nghttp2_session_on_origin_received(nghttp2_session *session, + nghttp2_frame *frame) { + return session_call_on_frame_received(session, frame); +} + static int session_process_altsvc_frame(nghttp2_session *session) { nghttp2_inbound_frame *iframe = &session->iframe; nghttp2_frame *frame = &iframe->frame; @@ -4836,6 +4872,25 @@ static int session_process_altsvc_frame(nghttp2_session *session) { return nghttp2_session_on_altsvc_received(session, frame); } +static int session_process_origin_frame(nghttp2_session *session) { + nghttp2_inbound_frame *iframe = &session->iframe; + nghttp2_frame *frame = &iframe->frame; + nghttp2_mem *mem = &session->mem; + int rv; + + rv = nghttp2_frame_unpack_origin_payload(&frame->ext, iframe->lbuf.pos, + nghttp2_buf_len(&iframe->lbuf), mem); + if (rv != 0) { + if (nghttp2_is_fatal(rv)) { + return rv; + } + /* Ignore ORIGIN frame which cannot be parsed. */ + return 0; + } + + return nghttp2_session_on_origin_received(session, frame); +} + static int session_process_extension_frame(nghttp2_session *session) { int rv; nghttp2_inbound_frame *iframe = &session->iframe; @@ -5746,6 +5801,42 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, iframe->state = NGHTTP2_IB_READ_NBYTE; inbound_frame_set_mark(iframe, 2); + break; + case NGHTTP2_ORIGIN: + if (!(session->builtin_recv_ext_types & NGHTTP2_TYPEMASK_ORIGIN)) { + busy = 1; + iframe->state = NGHTTP2_IB_IGN_PAYLOAD; + break; + } + + DEBUGF("recv: ORIGIN\n"); + + iframe->frame.ext.payload = &iframe->ext_frame_payload.origin; + + if (session->server || iframe->frame.hd.stream_id || + (iframe->frame.hd.flags & 0xf0)) { + busy = 1; + iframe->state = NGHTTP2_IB_IGN_PAYLOAD; + break; + } + + iframe->frame.hd.flags = NGHTTP2_FLAG_NONE; + + if (iframe->payloadleft) { + iframe->raw_lbuf = nghttp2_mem_malloc(mem, iframe->payloadleft); + + if (iframe->raw_lbuf == NULL) { + return NGHTTP2_ERR_NOMEM; + } + + nghttp2_buf_wrap_init(&iframe->lbuf, iframe->raw_lbuf, + iframe->payloadleft); + } else { + busy = 1; + } + + iframe->state = NGHTTP2_IB_READ_ORIGIN_PAYLOAD; + break; default: busy = 1; @@ -6583,7 +6674,6 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, DEBUGF("recv: [IB_READ_ALTSVC_PAYLOAD]\n"); readlen = inbound_frame_payload_readlen(iframe, in, last); - if (readlen > 0) { iframe->lbuf.last = nghttp2_cpymem(iframe->lbuf.last, in, readlen); @@ -6601,11 +6691,44 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, } rv = session_process_altsvc_frame(session); + if (nghttp2_is_fatal(rv)) { + return rv; + } + + session_inbound_frame_reset(session); + + break; + case NGHTTP2_IB_READ_ORIGIN_PAYLOAD: + DEBUGF("recv: [IB_READ_ORIGIN_PAYLOAD]\n"); + + readlen = inbound_frame_payload_readlen(iframe, in, last); + + if (readlen > 0) { + iframe->lbuf.last = nghttp2_cpymem(iframe->lbuf.last, in, readlen); + + iframe->payloadleft -= readlen; + in += readlen; + } + + DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen, + iframe->payloadleft); + + if (iframe->payloadleft) { + assert(nghttp2_buf_avail(&iframe->lbuf) > 0); + + break; + } + + rv = session_process_origin_frame(session); if (nghttp2_is_fatal(rv)) { return rv; } + if (iframe->state == NGHTTP2_IB_IGN_ALL) { + return (ssize_t)inlen; + } + session_inbound_frame_reset(session); break; @@ -7085,12 +7208,42 @@ int nghttp2_session_set_stream_user_data(nghttp2_session *session, int32_t stream_id, void *stream_user_data) { nghttp2_stream *stream; + nghttp2_frame *frame; + nghttp2_outbound_item *item; + stream = nghttp2_session_get_stream(session, stream_id); - if (!stream) { + if (stream) { + stream->stream_user_data = stream_user_data; + return 0; + } + + if (session->server || !nghttp2_session_is_my_stream_id(session, stream_id) || + !nghttp2_outbound_queue_top(&session->ob_syn)) { return NGHTTP2_ERR_INVALID_ARGUMENT; } - stream->stream_user_data = stream_user_data; - return 0; + + frame = &nghttp2_outbound_queue_top(&session->ob_syn)->frame; + assert(frame->hd.type == NGHTTP2_HEADERS); + + if (frame->hd.stream_id > stream_id || + (uint32_t)stream_id >= session->next_stream_id) { + return NGHTTP2_ERR_INVALID_ARGUMENT; + } + + for (item = session->ob_syn.head; item; item = item->qnext) { + if (item->frame.hd.stream_id < stream_id) { + continue; + } + + if (item->frame.hd.stream_id > stream_id) { + break; + } + + item->aux_data.headers.stream_user_data = stream_user_data; + return 0; + } + + return NGHTTP2_ERR_INVALID_ARGUMENT; } int nghttp2_session_resume_data(nghttp2_session *session, int32_t stream_id) { diff --git a/deps/nghttp2/lib/nghttp2_session.h b/deps/nghttp2/lib/nghttp2_session.h index c7cb27d77c1e25..5add50bc8bce16 100644 --- a/deps/nghttp2/lib/nghttp2_session.h +++ b/deps/nghttp2/lib/nghttp2_session.h @@ -26,7 +26,7 @@ #define NGHTTP2_SESSION_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include @@ -61,7 +61,8 @@ typedef enum { */ typedef enum { NGHTTP2_TYPEMASK_NONE = 0, - NGHTTP2_TYPEMASK_ALTSVC = 1 << 0 + NGHTTP2_TYPEMASK_ALTSVC = 1 << 0, + NGHTTP2_TYPEMASK_ORIGIN = 1 << 1 } nghttp2_typemask; typedef enum { @@ -121,6 +122,7 @@ typedef enum { NGHTTP2_IB_IGN_DATA, NGHTTP2_IB_IGN_ALL, NGHTTP2_IB_READ_ALTSVC_PAYLOAD, + NGHTTP2_IB_READ_ORIGIN_PAYLOAD, NGHTTP2_IB_READ_EXTENSION_PAYLOAD } nghttp2_inbound_state; @@ -301,8 +303,10 @@ struct nghttp2_session { increased/decreased by submitting WINDOW_UPDATE. See nghttp2_submit_window_update(). */ int32_t local_window_size; - /* Settings value received from the remote endpoint. We just use ID - as index. The index = 0 is unused. */ + /* This flag is used to indicate that the local endpoint received initial + SETTINGS frame from the remote endpoint. */ + uint8_t remote_settings_received; + /* Settings value received from the remote endpoint. */ nghttp2_settings_storage remote_settings; /* Settings value of the local endpoint. */ nghttp2_settings_storage local_settings; @@ -698,7 +702,7 @@ int nghttp2_session_on_push_promise_received(nghttp2_session *session, * NGHTTP2_ERR_NOMEM * Out of memory. * NGHTTP2_ERR_CALLBACK_FAILURE - * The callback function failed. + * The callback function failed. * NGHTTP2_ERR_FLOODED * There are too many items in outbound queue, and this is most * likely caused by misbehaviour of peer. @@ -716,7 +720,7 @@ int nghttp2_session_on_ping_received(nghttp2_session *session, * NGHTTP2_ERR_NOMEM * Out of memory. * NGHTTP2_ERR_CALLBACK_FAILURE - * The callback function failed. + * The callback function failed. */ int nghttp2_session_on_goaway_received(nghttp2_session *session, nghttp2_frame *frame); @@ -731,7 +735,7 @@ int nghttp2_session_on_goaway_received(nghttp2_session *session, * NGHTTP2_ERR_NOMEM * Out of memory. * NGHTTP2_ERR_CALLBACK_FAILURE - * The callback function failed. + * The callback function failed. */ int nghttp2_session_on_window_update_received(nghttp2_session *session, nghttp2_frame *frame); @@ -744,11 +748,24 @@ int nghttp2_session_on_window_update_received(nghttp2_session *session, * negative error codes: * * NGHTTP2_ERR_CALLBACK_FAILURE - * The callback function failed. + * The callback function failed. */ int nghttp2_session_on_altsvc_received(nghttp2_session *session, nghttp2_frame *frame); +/* + * Called when ORIGIN is received, assuming |frame| is properly + * initialized. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP2_ERR_CALLBACK_FAILURE + * The callback function failed. + */ +int nghttp2_session_on_origin_received(nghttp2_session *session, + nghttp2_frame *frame); + /* * Called when DATA is received, assuming |frame| is properly * initialized. @@ -759,7 +776,7 @@ int nghttp2_session_on_altsvc_received(nghttp2_session *session, * NGHTTP2_ERR_NOMEM * Out of memory. * NGHTTP2_ERR_CALLBACK_FAILURE - * The callback function failed. + * The callback function failed. */ int nghttp2_session_on_data_received(nghttp2_session *session, nghttp2_frame *frame); diff --git a/deps/nghttp2/lib/nghttp2_stream.h b/deps/nghttp2/lib/nghttp2_stream.h index da0e5d532c2f0b..d1d5856d800e76 100644 --- a/deps/nghttp2/lib/nghttp2_stream.h +++ b/deps/nghttp2/lib/nghttp2_stream.h @@ -26,7 +26,7 @@ #define NGHTTP2_STREAM_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_submit.c b/deps/nghttp2/lib/nghttp2_submit.c index 6c15c82488e724..f604eff5c9017f 100644 --- a/deps/nghttp2/lib/nghttp2_submit.c +++ b/deps/nghttp2/lib/nghttp2_submit.c @@ -571,6 +571,89 @@ int nghttp2_submit_altsvc(nghttp2_session *session, uint8_t flags, return rv; } +int nghttp2_submit_origin(nghttp2_session *session, uint8_t flags, + const nghttp2_origin_entry *ov, size_t nov) { + nghttp2_mem *mem; + uint8_t *p; + nghttp2_outbound_item *item; + nghttp2_frame *frame; + nghttp2_ext_origin *origin; + nghttp2_origin_entry *ov_copy; + size_t len = 0; + size_t i; + int rv; + (void)flags; + + mem = &session->mem; + + if (!session->server) { + return NGHTTP2_ERR_INVALID_STATE; + } + + if (nov) { + for (i = 0; i < nov; ++i) { + len += ov[i].origin_len; + } + + if (2 * nov + len > NGHTTP2_MAX_PAYLOADLEN) { + return NGHTTP2_ERR_INVALID_ARGUMENT; + } + + /* The last nov is added for terminal NULL character. */ + ov_copy = + nghttp2_mem_malloc(mem, nov * sizeof(nghttp2_origin_entry) + len + nov); + if (ov_copy == NULL) { + return NGHTTP2_ERR_NOMEM; + } + + p = (uint8_t *)ov_copy + nov * sizeof(nghttp2_origin_entry); + + for (i = 0; i < nov; ++i) { + ov_copy[i].origin = p; + ov_copy[i].origin_len = ov[i].origin_len; + p = nghttp2_cpymem(p, ov[i].origin, ov[i].origin_len); + *p++ = '\0'; + } + + assert((size_t)(p - (uint8_t *)ov_copy) == + nov * sizeof(nghttp2_origin_entry) + len + nov); + } else { + ov_copy = NULL; + } + + item = nghttp2_mem_malloc(mem, sizeof(nghttp2_outbound_item)); + if (item == NULL) { + rv = NGHTTP2_ERR_NOMEM; + goto fail_item_malloc; + } + + nghttp2_outbound_item_init(item); + + item->aux_data.ext.builtin = 1; + + origin = &item->ext_frame_payload.origin; + + frame = &item->frame; + frame->ext.payload = origin; + + nghttp2_frame_origin_init(&frame->ext, ov_copy, nov); + + rv = nghttp2_session_add_item(session, item); + if (rv != 0) { + nghttp2_frame_origin_free(&frame->ext, mem); + nghttp2_mem_free(mem, item); + + return rv; + } + + return 0; + +fail_item_malloc: + free(ov_copy); + + return rv; +} + static uint8_t set_request_flags(const nghttp2_priority_spec *pri_spec, const nghttp2_data_provider *data_prd) { uint8_t flags = NGHTTP2_FLAG_NONE; diff --git a/deps/nghttp2/lib/nghttp2_submit.h b/deps/nghttp2/lib/nghttp2_submit.h index 545388cfa3bef4..74d702fbcf077e 100644 --- a/deps/nghttp2/lib/nghttp2_submit.h +++ b/deps/nghttp2/lib/nghttp2_submit.h @@ -26,7 +26,7 @@ #define NGHTTP2_SUBMIT_H #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/deps/nghttp2/lib/nghttp2_version.c b/deps/nghttp2/lib/nghttp2_version.c index 8c5710d419c331..4211f2cf8f624d 100644 --- a/deps/nghttp2/lib/nghttp2_version.c +++ b/deps/nghttp2/lib/nghttp2_version.c @@ -23,7 +23,7 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifdef HAVE_CONFIG_H -#include +# include #endif /* HAVE_CONFIG_H */ #include diff --git a/doc/api/errors.md b/doc/api/errors.md index 1c5836e553df1a..69cab1c524e839 100755 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -737,6 +737,11 @@ An invalid HTTP/2 header value was specified. An invalid HTTP informational status code has been specified. Informational status codes must be an integer between `100` and `199` (inclusive). + +### ERR_HTTP2_INVALID_ORIGIN + +HTTP/2 `ORIGIN` frames require a valid origin. + ### ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH @@ -775,12 +780,23 @@ required to send an acknowledgment that it has received and applied the new be sent at any given time. This error code is used when that limit has been reached. + +### ERR_HTTP2_NESTED_PUSH + +An attempt was made to initiate a new push stream from within a push stream. +Nested push streams are not permitted. + ### ERR_HTTP2_NO_SOCKET_MANIPULATION An attempt was made to directly manipulate (read, write, pause, resume, etc.) a socket attached to an `Http2Session`. + +### ERR_HTTP2_ORIGIN_LENGTH + +HTTP/2 `ORIGIN` frames are limited to a length of 16382 bytes. + ### ERR_HTTP2_OUT_OF_STREAMS @@ -826,12 +842,23 @@ send something other than a regular file. The `Http2Session` closed with a non-zero error code. + +### ERR_HTTP2_SETTINGS_CANCEL + +The `Http2Session` settings canceled. + ### ERR_HTTP2_SOCKET_BOUND An attempt was made to connect a `Http2Session` object to a `net.Socket` or `tls.TLSSocket` that had already been bound to another `Http2Session` object. + +### ERR_HTTP2_SOCKET_UNBOUND + +An attempt was made to use the `socket` property of an `Http2Session` that +has already been closed. + ### ERR_HTTP2_STATUS_101 @@ -861,6 +888,19 @@ When setting the priority for an HTTP/2 stream, the stream may be marked as a dependency for a parent stream. This error code is used when an attempt is made to mark a stream and dependent of itself. + +### ERR_HTTP2_TRAILERS_ALREADY_SENT + +Trailing headers have already been sent on the `Http2Stream`. + + +### ERR_HTTP2_TRAILERS_NOT_READY + +The `http2stream.sendTrailers()` method cannot be called until after the +`'wantTrailers'` event is emitted on an `Http2Stream` object. The +`'wantTrailers'` event will only be emitted if the `waitForTrailers` option +is set for the `Http2Stream`. + ### ERR_HTTP2_UNSUPPORTED_PROTOCOL diff --git a/doc/api/http.md b/doc/api/http.md index 0640ec417d0b88..ecffb87be4bbaf 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -1401,7 +1401,7 @@ following additional events, methods, and properties. added: v0.3.8 --> -Emitted when the request has been aborted and the network socket has closed. +Emitted when the request has been aborted. ### Event: 'close' + +* {boolean} + +The `message.aborted` property will be `true` if the request has +been aborted. + ### message.destroy([error]) -> Stability: 1 - Experimental +> Stability: 2 - Stable The `http2` module provides an implementation of the [HTTP/2][] protocol. It can be accessed using: @@ -108,6 +114,11 @@ have occasion to work with the `Http2Session` object directly, with most actions typically taken through interactions with either the `Http2Server` or `Http2Stream` objects. +User code will not create `Http2Session` instances directly. Server-side +`Http2Session` instances are created by the `Http2Server` instance when a +new HTTP/2 connection is received. Client-side `Http2Session` instances are +created using the `http2.connect()` method. + #### Http2Session and Sockets Every `Http2Session` instance is associated with exactly one [`net.Socket`][] or @@ -128,13 +139,17 @@ solely on the API of the `Http2Session`. added: v8.4.0 --> -The `'close'` event is emitted once the `Http2Session` has been destroyed. +The `'close'` event is emitted once the `Http2Session` has been destroyed. Its +listener does not expect any arguments. #### Event: 'connect' +* `session` {Http2Session} +* `socket` {net.Socket} + The `'connect'` event is emitted once the `Http2Session` has been successfully connected to the remote peer and communication may begin. @@ -145,6 +160,8 @@ connected to the remote peer and communication may begin. added: v8.4.0 --> +* `error` {Error} + The `'error'` event is emitted when an error occurs during the processing of an `Http2Session`. @@ -153,18 +170,16 @@ an `Http2Session`. added: v8.4.0 --> +* `type` {integer} The frame type. +* `code` {integer} The error code. +* `id` {integer} The stream id (or `0` if the frame isn't associated with a + stream). + The `'frameError'` event is emitted when an error occurs while attempting to send a frame on the session. If the frame that could not be sent is associated with a specific `Http2Stream`, an attempt to emit `'frameError'` event on the `Http2Stream` is made. -When invoked, the handler function will receive three arguments: - -* An integer identifying the frame type. -* An integer identifying the error code. -* An integer identifying the stream (or 0 if the frame is not associated with - a stream). - If the `'frameError'` event is associated with a stream, the stream will be closed and destroyed immediately following the `'frameError'` event. If the event is not associated with a stream, the `Http2Session` will be shut down @@ -175,26 +190,26 @@ immediately following the `'frameError'` event. added: v8.4.0 --> -The `'goaway'` event is emitted when a GOAWAY frame is received. When invoked, -the handler function will receive three arguments: - -* `errorCode` {number} The HTTP/2 error code specified in the GOAWAY frame. +* `errorCode` {number} The HTTP/2 error code specified in the `GOAWAY` frame. * `lastStreamID` {number} The ID of the last stream the remote peer successfully processed (or `0` if no ID is specified). -* `opaqueData` {Buffer} If additional opaque data was included in the GOAWAY +* `opaqueData` {Buffer} If additional opaque data was included in the `GOAWAY` frame, a `Buffer` instance will be passed containing that data. -*Note*: The `Http2Session` instance will be shut down automatically when the -`'goaway'` event is emitted. +The `'goaway'` event is emitted when a `GOAWAY` frame is received. + +The `Http2Session` instance will be shut down automatically when the `'goaway'` +event is emitted. #### Event: 'localSettings' -The `'localSettings'` event is emitted when an acknowledgment SETTINGS frame -has been received. When invoked, the handler function will receive a copy of -the local settings. +* `settings` {HTTP/2 Settings Object} A copy of the `SETTINGS` frame received. + +The `'localSettings'` event is emitted when an acknowledgment `SETTINGS` frame +has been received. *Note*: When using `http2session.settings()` to submit new settings, the modified settings do not take effect until the `'localSettings'` event is @@ -208,14 +223,25 @@ session.on('localSettings', (settings) => { }); ``` +#### Event: 'ping' + + +* `payload` {Buffer} The `PING` frame 8-byte payload + +The `'ping'` event is emitted whenever a `PING` frame is received from the +connected peer. + #### Event: 'remoteSettings' -The `'remoteSettings'` event is emitted when a new SETTINGS frame is received -from the connected peer. When invoked, the handler function will receive a copy -of the remote settings. +* `settings` {HTTP/2 Settings Object} A copy of the `SETTINGS` frame received. + +The `'remoteSettings'` event is emitted when a new `SETTINGS` frame is received +from the connected peer. ```js session.on('remoteSettings', (settings) => { @@ -228,10 +254,13 @@ session.on('remoteSettings', (settings) => { added: v8.4.0 --> -The `'stream'` event is emitted when a new `Http2Stream` is created. When -invoked, the handler function will receive a reference to the `Http2Stream` -object, a [HTTP/2 Headers Object][], and numeric flags associated with the -creation of the stream. +* `stream` {Http2Stream} A reference to the stream +* `headers` {HTTP/2 Headers Object} An object describing the headers +* `flags` {number} The associated numeric flags +* `rawHeaders` {Array} An array containing the raw header names followed by + their respective values. + +The `'stream'` event is emitted when a new `Http2Stream` is created. ```js const http2 = require('http2'); @@ -384,7 +413,7 @@ added: v8.11.2 * `code` {number} An HTTP/2 error code * `lastStreamID` {number} The numeric ID of the last processed `Http2Stream` * `opaqueData` {Buffer|TypedArray|DataView} A `TypedArray` or `DataView` - instance containing additional data to be carried within the GOAWAY frame. + instance containing additional data to be carried within the `GOAWAY` frame. Transmits a `GOAWAY` frame to the connected peer *without* shutting down the `Http2Session`. @@ -410,6 +439,8 @@ If the `Http2Session` is connected to a `TLSSocket`, the `originSet` property will return an Array of origins for which the `Http2Session` may be considered authoritative. +The `originSet` property is only available when using a secure TLS connection. + #### http2session.pendingSettingsAck + +* `origins` { string | URL | Object } One or more URL Strings passed as + separate arguments. + +Submits an `ORIGIN` frame (as defined by [RFC 8336][]) to the connected client +to advertise the set of origins for which the server is capable of providing +authoritative responses. + +```js +const http2 = require('http2'); +const options = getSecureOptionsSomehow(); +const server = http2.createSecureServer(options); +server.on('stream', (stream) => { + stream.respond(); + stream.end('ok'); +}); +server.on('session', (session) => { + session.origin('https://example.com', 'https://example.org'); +}); +``` + +When a string is passed as an `origin`, it will be parsed as a URL and the +origin will be derived. For instance, the origin for the HTTP URL +`'https://example.org/foo/bar'` is the ASCII string +`'https://example.org'`. An error will be thrown if either the given string +cannot be parsed as a URL or if a valid origin cannot be derived. + +A `URL` object, or any object with an `origin` property, may be passed as +an `origin`, in which case the value of the `origin` property will be +used. The value of the `origin` property *must* be a properly serialized +ASCII origin. + +Alternatively, the `origins` option may be used when creating a new HTTP/2 +server using the `http2.createSecureServer()` method: + +```js +const http2 = require('http2'); +const options = getSecureOptionsSomehow(); +options.origins = ['https://example.com', 'https://example.org']; +const server = http2.createSecureServer(options); +server.on('stream', (stream) => { + stream.respond(); + stream.end('ok'); +}); +``` + ### Class: ClientHttp2Session -* `alt`: {string} -* `origin`: {string} -* `streamId`: {number} +* `alt` {string} +* `origin` {string} +* `streamId` {number} The `'altsvc'` event is emitted whenever an `ALTSVC` frame is received by the client. The event is emitted with the `ALTSVC` value, origin, and stream @@ -679,6 +760,30 @@ client.on('altsvc', (alt, origin, streamId) => { }); ``` +#### Event: 'origin' + + +* `origins` {string[]} + +The `'origin'` event is emitted whenever an `ORIGIN` frame is received by +the client. The event is emitted with an array of `origin` strings. The +`http2session.originSet` will be updated to include the received +origins. + +```js +const http2 = require('http2'); +const client = http2.connect('https://example.org'); + +client.on('origin', (origins) => { + for (let n = 0; n < origins.length; n++) + console.log(origins[n]); +}); +``` + +The `'origin'` event is only emitted when using a secure TLS connection. + #### clienthttp2session.request(headers[, options]) +* `error` {Error} + The `'error'` event is emitted when an error occurs during the processing of an `Http2Stream`. @@ -874,12 +981,26 @@ The `'trailers'` event is emitted when a block of headers associated with trailing header fields is received. The listener callback is passed the [HTTP/2 Headers Object][] and flags associated with the headers. +Note that this event might not be emitted if `http2stream.end()` is called +before trailers are received and the incoming data is not being read or +listened for. + ```js stream.on('trailers', (headers, flags) => { console.log(headers); }); ``` +#### Event: 'wantTrailers' + + +The `'wantTrailers'` event is emitted when the `Http2Stream` has queued the +final `DATA` frame to be sent on a frame and the `Http2Stream` is ready to send +trailing headers. When initiating a request or response, the `waitForTrailers` +option must be set for this event to be emitted. + #### http2stream.aborted + +* {boolean} + +Set the `true` if the `END_STREAM` flag was set in the request or response +HEADERS frame received, indicating that no additional data should be received +and the readable side of the `Http2Stream` will be closed. + #### http2stream.pending + +* `headers` {HTTP/2 Headers Object} + +Sends a trailing `HEADERS` frame to the connected HTTP/2 peer. This method +will cause the `Http2Stream` to be immediately closed and must only be +called after the `'wantTrailers'` event has been emitted. When sending a +request or sending a response, the `options.waitForTrailers` option must be set +in order to keep the `Http2Stream` open after the final `DATA` frame so that +trailers can be sent. + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond(undefined, { waitForTrailers: true }); + stream.on('wantTrailers', () => { + stream.sendTrailers({ xyz: 'abc' }); + }); + stream.end('Hello World'); +}); +``` + +The HTTP/1 specification forbids trailers from containing HTTP/2 pseudo-header +fields (e.g. `':method'`, `':path'`, etc). + ### Class: ClientHttp2Stream - -If a `ServerHttp2Stream` emits an `'error'` event, it will be forwarded here. -The stream will already be destroyed when this event is triggered. - #### Event: 'stream' + +* `msecs` {number} **Default:** `120000` (2 minutes) +* `callback` {Function} +* Returns: {Http2Server} + +Used to set the timeout value for http2 server requests, +and sets a callback function that is called when there is no activity +on the `Http2Server` after `msecs` milliseconds. + +The given callback is registered as a listener on the `'timeout'` event. + +In case of no callback function were assigned, a new `ERR_INVALID_CALLBACK` +error will be thrown. + ### Class: Http2SecureServer + +* `msecs` {number} **Default:** `120000` (2 minutes) +* `callback` {Function} +* Returns: {Http2SecureServer} + +Used to set the timeout value for http2 secure server requests, +and sets a callback function that is called when there is no activity +on the `Http2SecureServer` after `msecs` milliseconds. + +The given callback is registered as a listener on the `'timeout'` event. + +In case of no callback function were assigned, a new `ERR_INVALID_CALLBACK` +error will be thrown. + ### http2.createServer(options[, onRequestHandler]) + +* {boolean} + +The `request.aborted` property will be `true` if the request has +been aborted. + #### request.destroy([error]) +* `headers` {HTTP/2 Headers Object} An object describing the headers +* `callback` {Function} Called once `http2stream.pushStream()` is finished, + or either when the attempt to create the pushed `Http2Stream` has failed or + has been rejected, or the state of `Http2ServerRequest` is closed prior to + calling the `http2stream.pushStream()` method + * `err` {Error} + * `stream` {ServerHttp2Stream} The newly-created `ServerHttp2Stream` object -Call [`http2stream.pushStream()`][] with the given headers, and wraps the -given newly created [`Http2Stream`] on `Http2ServerRespose`. - -The callback will be called with an error with code `ERR_HTTP2_STREAM_CLOSED` -if the stream is closed. +Call [`http2stream.pushStream()`][] with the given headers, and wrap the +given [`Http2Stream`] on a newly created `Http2ServerResponse` as the callback +parameter if successful. When `Http2ServerRequest` is closed, the callback is +called with an error `ERR_HTTP2_INVALID_STREAM`. ## Collecting HTTP/2 Performance Metrics @@ -3100,9 +3334,9 @@ The `name` property of the `PerformanceEntry` will be equal to either If `name` is equal to `Http2Stream`, the `PerformanceEntry` will contain the following additional properties: -* `bytesRead` {number} The number of DATA frame bytes received for this +* `bytesRead` {number} The number of `DATA` frame bytes received for this `Http2Stream`. -* `bytesWritten` {number} The number of DATA frame bytes sent for this +* `bytesWritten` {number} The number of `DATA` frame bytes sent for this `Http2Stream`. * `id` {number} The identifier of the associated `Http2Stream` * `timeToFirstByte` {number} The number of milliseconds elapsed between the @@ -3146,6 +3380,7 @@ following additional properties: [Performance Observer]: perf_hooks.html [Readable Stream]: stream.html#stream_class_stream_readable [RFC 7838]: https://tools.ietf.org/html/rfc7838 +[RFC 8336]: https://tools.ietf.org/html/rfc8336 [Using options.selectPadding]: #http2_using_options_selectpadding [Writable Stream]: stream.html#stream_writable_streams [`'checkContinue'`]: #http2_event_checkcontinue diff --git a/lib/_http_client.js b/lib/_http_client.js index 460521c8ded85c..a41a6a537f95bf 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -299,6 +299,7 @@ ClientRequest.prototype.abort = function abort() { if (!this.aborted) { process.nextTick(emitAbortNT.bind(this)); } + // Mark as aborting so we can avoid sending queued request data // This is used as a truthy flag elsewhere. The use of Date.now is for // debugging purposes only. @@ -350,7 +351,10 @@ function socketCloseListener() { req.emit('close'); if (req.res && req.res.readable) { // Socket closed before we emitted 'end' below. - if (!req.res.complete) req.res.emit('aborted'); + if (!req.res.complete) { + req.res.aborted = true; + req.res.emit('aborted'); + } var res = req.res; res.on('end', function() { res.emit('close'); diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index 696fcc3b4ce53d..d51bc3ae6bfd41 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -59,6 +59,8 @@ function IncomingMessage(socket) { this.readable = true; + this.aborted = false; + this.upgrade = null; // request (server) only diff --git a/lib/_http_server.js b/lib/_http_server.js index 2de2112dfca2ed..b281db03510681 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -436,6 +436,7 @@ function socketOnClose(socket, state) { function abortIncoming(incoming) { while (incoming.length) { var req = incoming.shift(); + req.aborted = true; req.emit('aborted'); req.emit('close'); } diff --git a/lib/http2.js b/lib/http2.js index de06de1cc414cb..1f770ff4c734cd 100644 --- a/lib/http2.js +++ b/lib/http2.js @@ -1,11 +1,5 @@ 'use strict'; -process.emitWarning( - 'The http2 module is an experimental API.', - 'ExperimentalWarning', undefined, - 'See https://github.com/nodejs/http2' -); - const { constants, getDefaultSettings, diff --git a/lib/internal/errors.js b/lib/internal/errors.js index e62ce3fb22759a..48062b6d3d40fd 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -311,6 +311,7 @@ E('ERR_HTTP2_INVALID_CONNECTION_HEADERS', E('ERR_HTTP2_INVALID_HEADER_VALUE', 'Invalid value "%s" for header "%s"'); E('ERR_HTTP2_INVALID_INFO_STATUS', (code) => `Invalid informational status code: ${code}`); +E('ERR_HTTP2_INVALID_ORIGIN', 'HTTP/2 ORIGIN frames require a valid origin'); E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH', 'Packed settings length must be a multiple of six'); E('ERR_HTTP2_INVALID_PSEUDOHEADER', @@ -321,8 +322,12 @@ E('ERR_HTTP2_INVALID_SETTING_VALUE', E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed'); E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', (max) => `Maximum number of pending settings acknowledgements (${max})`); +E('ERR_HTTP2_NESTED_PUSH', + 'A push stream cannot initiate another push stream.', Error); E('ERR_HTTP2_NO_SOCKET_MANIPULATION', 'HTTP/2 sockets should not be directly manipulated (e.g. read and written)'); +E('ERR_HTTP2_ORIGIN_LENGTH', + 'HTTP/2 ORIGIN frames are limited to 16382 bytes'); E('ERR_HTTP2_OUT_OF_STREAMS', 'No stream ID is available because maximum stream ID has been reached'); E('ERR_HTTP2_PAYLOAD_FORBIDDEN', @@ -333,16 +338,23 @@ E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', 'Cannot set HTTP/2 pseudo-headers'); E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams'); E('ERR_HTTP2_SEND_FILE', 'Only regular files can be sent'); E('ERR_HTTP2_SESSION_ERROR', 'Session closed with error code %s'); +E('ERR_HTTP2_SETTINGS_CANCEL', 'HTTP2 session settings canceled'); E('ERR_HTTP2_SOCKET_BOUND', 'The socket is already bound to an Http2Session'); +E('ERR_HTTP2_SOCKET_UNBOUND', + 'The socket has been disconnected from the Http2Session'); E('ERR_HTTP2_STATUS_101', 'HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2'); E('ERR_HTTP2_STATUS_INVALID', 'Invalid status code: %s'); E('ERR_HTTP2_STREAM_CANCEL', 'The pending stream has been canceled'); E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s'); E('ERR_HTTP2_STREAM_SELF_DEPENDENCY', 'A stream cannot depend on itself'); -E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', - (protocol) => `protocol "${protocol}" is unsupported.`); +E('ERR_HTTP2_TRAILERS_ALREADY_SENT', + 'Trailing headers have already been sent'); +E('ERR_HTTP2_TRAILERS_NOT_READY', + 'Trailing headers cannot be sent until after the wantTrailers event is ' + + 'emitted'); +E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', 'protocol "%s" is unsupported.'); E('ERR_HTTP_HEADERS_SENT', 'Cannot render headers after they are sent to the client'); E('ERR_HTTP_INVALID_CHAR', 'Invalid character in statusMessage.'); diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index 5e6c51377e94ba..06d283a4b8bbea 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -7,7 +7,6 @@ const constants = binding.constants; const errors = require('internal/errors'); const { kSocket } = require('internal/http2/util'); -const kFinish = Symbol('finish'); const kBeginSend = Symbol('begin-send'); const kState = Symbol('state'); const kStream = Symbol('stream'); @@ -19,6 +18,7 @@ const kTrailers = Symbol('trailers'); const kRawTrailers = Symbol('rawTrailers'); const kProxySocket = Symbol('proxySocket'); const kSetHeader = Symbol('setHeader'); +const kAborted = Symbol('aborted'); const { HTTP2_HEADER_AUTHORITY, @@ -103,9 +103,7 @@ function onStreamError(error) { // // errors in compatibility mode are // not forwarded to the request - // and response objects. However, - // they are forwarded to 'streamError' - // on the server by Http2Stream + // and response objects. } function onRequestPause() { @@ -125,6 +123,7 @@ function onStreamDrain() { function onStreamAbortedRequest() { const request = this[kRequest]; if (request !== undefined && request[kState].closed === false) { + request[kAborted] = true; request.emit('aborted'); } } @@ -209,6 +208,34 @@ const proxySocketHandler = { } }; +function onStreamCloseRequest() { + const req = this[kRequest]; + + if (req === undefined) + return; + + const state = req[kState]; + state.closed = true; + + req.push(null); + // if the user didn't interact with incoming data and didn't pipe it, + // dump it for compatibility with http1 + if (!state.didRead && !req._readableState.resumeScheduled) + req.resume(); + + this[kProxySocket] = null; + this[kRequest] = undefined; + + req.emit('close'); +} + +function onStreamTimeout(kind) { + return function onStreamTimeout() { + const obj = this[kind]; + obj.emit('timeout'); + }; +} + class Http2ServerRequest extends Readable { constructor(stream, headers, options, rawHeaders) { super(options); @@ -221,21 +248,25 @@ class Http2ServerRequest extends Readable { this[kTrailers] = {}; this[kRawTrailers] = []; this[kStream] = stream; + this[kAborted] = false; stream[kProxySocket] = null; stream[kRequest] = this; // Pause the stream.. - stream.pause(); - stream.on('data', onStreamData); stream.on('trailers', onStreamTrailers); stream.on('end', onStreamEnd); stream.on('error', onStreamError); stream.on('aborted', onStreamAbortedRequest); - stream.on('close', this[kFinish].bind(this)); + stream.on('close', onStreamCloseRequest); + stream.on('timeout', onStreamTimeout(kRequest)); this.on('pause', onRequestPause); this.on('resume', onRequestResume); } + get aborted() { + return this[kAborted]; + } + get complete() { return this._readableState.ended || this[kState].closed || @@ -289,8 +320,12 @@ class Http2ServerRequest extends Readable { _read(nread) { const state = this[kState]; if (!state.closed) { - state.didRead = true; - process.nextTick(resumeStream, this[kStream]); + if (!state.didRead) { + state.didRead = true; + this[kStream].on('data', onStreamData); + } else { + process.nextTick(resumeStream, this[kStream]); + } } else { this.emit('error', new errors.Error('ERR_HTTP2_INVALID_STREAM')); } @@ -328,20 +363,32 @@ class Http2ServerRequest extends Readable { return; this[kStream].setTimeout(msecs, callback); } +} - [kFinish]() { - const state = this[kState]; - if (state.closed) - return; - state.closed = true; - this.push(null); - this[kStream][kRequest] = undefined; - // if the user didn't interact with incoming data and didn't pipe it, - // dump it for compatibility with http1 - if (!state.didRead && !this._readableState.resumeScheduled) - this.resume(); - this.emit('close'); - } +function onStreamTrailersReady() { + this.sendTrailers(this[kResponse][kTrailers]); +} + +function onStreamCloseResponse() { + const res = this[kResponse]; + + if (res === undefined) + return; + + const state = res[kState]; + + if (this.headRequest !== state.headRequest) + return; + + state.closed = true; + + this[kProxySocket] = null; + + this.removeListener('wantTrailers', onStreamTrailersReady); + this[kResponse] = undefined; + + res.emit('finish'); + res.emit('close'); } class Http2ServerResponse extends Stream { @@ -362,7 +409,9 @@ class Http2ServerResponse extends Stream { this.writable = true; stream.on('drain', onStreamDrain); stream.on('aborted', onStreamAbortedResponse); - stream.on('close', this[kFinish].bind(this)); + stream.on('close', onStreamCloseResponse); + stream.on('wantTrailers', onStreamTrailersReady); + stream.on('timeout', onStreamTimeout(kResponse)); } // User land modules such as finalhandler just check truthiness of this @@ -593,7 +642,7 @@ class Http2ServerResponse extends Stream { this.writeHead(this[kState].statusCode); if (isFinished) - this[kFinish](); + onStreamCloseResponse.call(stream); else stream.end(); } @@ -632,23 +681,11 @@ class Http2ServerResponse extends Stream { headers[HTTP2_HEADER_STATUS] = state.statusCode; const options = { endStream: state.ending, - getTrailers: (trailers) => Object.assign(trailers, this[kTrailers]) + waitForTrailers: true, }; this[kStream].respond(headers, options); } - [kFinish]() { - const stream = this[kStream]; - const state = this[kState]; - if (state.closed || stream.headRequest !== state.headRequest) - return; - state.closed = true; - this[kProxySocket] = null; - stream[kResponse] = undefined; - this.emit('finish'); - this.emit('close'); - } - // TODO doesn't support callbacks writeContinue() { const stream = this[kStream]; diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 3b064749322ea1..4c184fcfe188df 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -2,39 +2,38 @@ /* eslint-disable no-use-before-define */ -require('internal/util').assertCrypto(); +const { + assertCrypto, + customInspectSymbol: kInspect, + promisify +} = require('internal/util'); + +assertCrypto(); -const { async_id_symbol } = process.binding('async_wrap'); -const http = require('http'); -const binding = process.binding('http2'); const assert = require('assert'); const { Buffer } = require('buffer'); const EventEmitter = require('events'); +const fs = require('fs'); +const http = require('http'); const net = require('net'); +const { Duplex } = require('stream'); +const { + _unrefActive, + enroll, + unenroll +} = require('timers'); const tls = require('tls'); +const { URL } = require('url'); const util = require('util'); -const fs = require('fs'); -const errors = require('internal/errors'); + const { StreamWrap } = require('_stream_wrap'); -const { Duplex } = require('stream'); -const { URL } = require('url'); + +const errors = require('internal/errors'); +const { utcDate } = require('internal/http'); const { onServerStream, Http2ServerRequest, Http2ServerResponse, } = require('internal/http2/compat'); -const { utcDate } = require('internal/http'); -const { promisify } = require('internal/util'); -const { isArrayBufferView } = require('internal/util/types'); -const { _connectionListener: httpConnectionListener } = require('http'); -const { createPromise, promiseResolve } = process.binding('util'); -const debug = util.debuglog('http2'); - -const kMaxFrameSize = (2 ** 24) - 1; -const kMaxInt = (2 ** 32) - 1; -const kMaxStreams = (2 ** 31) - 1; - -// eslint-disable-next-line no-control-regex -const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/; const { assertIsObject, @@ -55,19 +54,28 @@ const { updateSettingsBuffer } = require('internal/http2/util'); -const { - _unrefActive, - enroll, - unenroll -} = require('timers'); +const { isArrayBufferView } = require('internal/util/types'); +const { async_id_symbol } = process.binding('async_wrap'); +const binding = process.binding('http2'); const { ShutdownWrap, WriteWrap } = process.binding('stream_wrap'); +const { createPromise, promiseResolve } = process.binding('util'); + +const { _connectionListener: httpConnectionListener } = http; +const debug = util.debuglog('http2'); + +const kMaxFrameSize = (2 ** 24) - 1; +const kMaxInt = (2 ** 32) - 1; +const kMaxStreams = (2 ** 31) - 1; + +// eslint-disable-next-line no-control-regex +const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/; + const { constants, nameForErrorCode } = binding; const NETServer = net.Server; const TLSServer = tls.Server; -const kInspect = require('internal/util').customInspectSymbol; const { kIncomingMessage } = require('_http_common'); const { kServerResponse } = require('_http_server'); @@ -82,6 +90,7 @@ const kMaybeDestroy = Symbol('maybe-destroy'); const kLocalSettings = Symbol('local-settings'); const kOptions = Symbol('options'); const kOwner = Symbol('owner'); +const kOrigin = Symbol('origin'); const kProceed = Symbol('proceed'); const kProtocol = Symbol('protocol'); const kProxySocket = Symbol('proxy-socket'); @@ -93,6 +102,7 @@ const kSession = Symbol('session'); const kState = Symbol('state'); const kType = Symbol('type'); const kUpdateTimer = Symbol('update-timer'); +const kWriteGeneric = Symbol('write-generic'); const kDefaultSocketTimeout = 2 * 60 * 1000; @@ -143,6 +153,7 @@ const { HTTP_STATUS_NO_CONTENT, HTTP_STATUS_NOT_MODIFIED, HTTP_STATUS_SWITCHING_PROTOCOLS, + HTTP_STATUS_MISDIRECTED_REQUEST, STREAM_OPTION_EMPTY_PAYLOAD, STREAM_OPTION_GET_TRAILERS @@ -154,6 +165,7 @@ const STREAM_FLAGS_CLOSED = 0x2; const STREAM_FLAGS_HEADERS_SENT = 0x4; const STREAM_FLAGS_HEAD_REQUEST = 0x8; const STREAM_FLAGS_ABORTED = 0x10; +const STREAM_FLAGS_HAS_TRAILERS = 0x20; const SESSION_FLAGS_PENDING = 0x0; const SESSION_FLAGS_READY = 0x1; @@ -208,7 +220,10 @@ function onSessionHeaders(handle, id, cat, flags, headers) { } } else { stream = new ClientHttp2Stream(session, handle, id, opts); + stream.end(); } + if (endOfStream) + stream[kState].endAfterHeaders = true; process.nextTick(emit, session, 'stream', stream, obj, flags, headers); } else { let event; @@ -230,6 +245,11 @@ function onSessionHeaders(handle, id, cat, flags, headers) { } else { event = endOfStream ? 'trailers' : 'headers'; } + const session = stream.session; + if (status === HTTP_STATUS_MISDIRECTED_REQUEST) { + const originSet = session[kState].originSet = initOriginSet(session); + originSet.delete(stream[kOrigin]); + } debug(`Http2Stream ${id} [Http2Session ` + `${sessionName(type)}]: emitting stream '${event}' event`); process.nextTick(emit, stream, event, obj, flags, headers); @@ -245,25 +265,18 @@ function tryClose(fd) { fs.close(fd, (err) => assert.ifError(err)); } -// Called to determine if there are trailers to be sent at the end of a -// Stream. The 'getTrailers' callback is invoked and passed a holder object. -// The trailers to return are set on that object by the handler. Once the -// event handler returns, those are sent off for processing. Note that this -// is a necessarily synchronous operation. We need to know immediately if -// there are trailing headers to send. +// Called when the Http2Stream has finished sending data and is ready for +// trailers to be sent. This will only be called if the { hasOptions: true } +// option is set. function onStreamTrailers() { const stream = this[kOwner]; + stream[kState].trailersReady = true; if (stream.destroyed) - return []; - const trailers = Object.create(null); - stream[kState].getTrailers.call(stream, trailers); - const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer); - if (!Array.isArray(headersList)) { - stream.destroy(headersList); - return []; + return; + if (!stream.emit('wantTrailers')) { + // There are no listeners, send empty trailing HEADERS frame and close. + stream.sendTrailers({}); } - stream[kSentTrailers] = trailers; - return headersList; } // Submit an RST-STREAM frame to be sent to the remote peer. @@ -274,6 +287,15 @@ function submitRstStream(code) { } } +function onPing(payload) { + const session = this[kOwner]; + if (session.destroyed) + return; + session[kUpdateTimer](); + debug(`Http2Session ${sessionName(session[kType])}: new ping received`); + session.emit('ping', payload); +} + // Called when the stream is closed either by sending or receiving an // RST_STREAM frame, or through a natural end-of-stream. // If the writable and readable sides of the stream are still open at this @@ -285,32 +307,19 @@ function onStreamClose(code) { if (stream.destroyed) return; - const state = stream[kState]; - debug(`Http2Stream ${stream[kID]} [Http2Session ` + `${sessionName(stream[kSession][kType])}]: closed with code ${code}`); - if (!stream.closed) { - // Unenroll from timeouts - unenroll(stream); - stream.removeAllListeners('timeout'); - - // Set the state flags - state.flags |= STREAM_FLAGS_CLOSED; - state.rstCode = code; - - // Close the writable side of the stream - abort(stream); - stream.end(); - } + if (!stream.closed) + closeStream(stream, code, kNoRstStream); - if (state.fd !== undefined) - tryClose(state.fd); + if (stream[kState].fd !== undefined) + tryClose(stream[kState].fd); // Defer destroy we actually emit end. - if (stream._readableState.endEmitted || code !== NGHTTP2_NO_ERROR) { + if (!stream.readable || code !== NGHTTP2_NO_ERROR) { // If errored or ended, we can destroy immediately. - stream[kMaybeDestroy](null, code); + stream[kMaybeDestroy](code); } else { // Wait for end to destroy. stream.on('end', stream[kMaybeDestroy]); @@ -318,10 +327,15 @@ function onStreamClose(code) { // it completely. stream.push(null); - // Same as net. - if (stream._readableState.length === 0) { + // If the user hasn't tried to consume the stream (and this is a server + // session) then just dump the incoming data so that the stream can + // be destroyed. + if (stream[kSession][kType] === NGHTTP2_SESSION_SERVER && + !stream[kState].didRead && + stream._readableState.flowing === null) + stream.resume(); + else stream.read(0); - } } } @@ -346,7 +360,7 @@ function onStreamRead(nread, buf) { `${sessionName(stream[kSession][kType])}]: ending readable.`); // defer this until we actually emit end - if (stream._readableState.endEmitted) { + if (!stream.readable) { stream[kMaybeDestroy](); } else { stream.on('end', stream[kMaybeDestroy]); @@ -407,6 +421,39 @@ function onAltSvc(stream, origin, alt) { session.emit('altsvc', alt, origin, stream); } +function initOriginSet(session) { + let originSet = session[kState].originSet; + if (originSet === undefined) { + const socket = session[kSocket]; + session[kState].originSet = originSet = new Set(); + if (socket.servername != null) { + let originString = `https://${socket.servername}`; + if (socket.remotePort != null) + originString += `:${socket.remotePort}`; + // We have to ensure that it is a properly serialized + // ASCII origin string. The socket.servername might not + // be properly ASCII encoded. + originSet.add((new URL(originString)).origin); + } + } + return originSet; +} + +function onOrigin(origins) { + const session = this[kOwner]; + if (session.destroyed) + return; + debug(`Http2Session ${sessionName(session[kType])}: origin received: ` + + `${origins.join(', ')}`); + session[kUpdateTimer](); + if (!session.encrypted || session.destroyed) + return undefined; + const originSet = initOriginSet(session); + for (var n = 0; n < origins.length; n++) + originSet.add(origins[n]); + session.emit('origin', origins); +} + // Receiving a GOAWAY frame from the connected peer is a signal that no // new streams should be created. If the code === NGHTTP2_NO_ERROR, we // are going to send our close, but allow existing frames to close @@ -437,7 +484,7 @@ function onGoawayData(code, lastStreamID, buf) { // condition on this side of the session that caused the // shutdown. session.destroy(new errors.Error('ERR_HTTP2_SESSION_ERROR', code), - { errorCode: NGHTTP2_NO_ERROR }); + NGHTTP2_NO_ERROR); } } @@ -461,7 +508,7 @@ function requestOnConnect(headers, options) { // At this point, the stream should have already been destroyed during // the session.destroy() method. Do nothing else. - if (session.destroyed) + if (session === undefined || session.destroyed) return; // If the session was closed while waiting for the connect, destroy @@ -479,10 +526,8 @@ function requestOnConnect(headers, options) { if (options.endStream) streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD; - if (typeof options.getTrailers === 'function') { + if (options.waitForTrailers) streamOptions |= STREAM_OPTION_GET_TRAILERS; - this[kState].getTrailers = options.getTrailers; - } // ret will be either the reserved stream ID (if positive) // or an error code (if negative) @@ -637,7 +682,9 @@ const proxySocketHandler = { get(session, prop) { switch (prop) { case 'setTimeout': - return session.setTimeout.bind(session); + case 'ref': + case 'unref': + return session[prop].bind(session); case 'destroy': case 'emit': case 'end': @@ -645,20 +692,30 @@ const proxySocketHandler = { case 'read': case 'resume': case 'write': + case 'setEncoding': + case 'setKeepAlive': + case 'setNoDelay': throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION'); default: const socket = session[kSocket]; + if (socket === undefined) + throw new errors.Error('ERR_HTTP2_SOCKET_UNBOUND'); const value = socket[prop]; return typeof value === 'function' ? value.bind(socket) : value; } }, getPrototypeOf(session) { - return Reflect.getPrototypeOf(session[kSocket]); + const socket = session[kSocket]; + if (socket === undefined) + throw new errors.Error('ERR_HTTP2_SOCKET_UNBOUND'); + return Reflect.getPrototypeOf(socket); }, set(session, prop, value) { switch (prop) { case 'setTimeout': - session.setTimeout = value; + case 'ref': + case 'unref': + session[prop] = value; return true; case 'destroy': case 'emit': @@ -667,9 +724,15 @@ const proxySocketHandler = { case 'read': case 'resume': case 'write': + case 'setEncoding': + case 'setKeepAlive': + case 'setNoDelay': throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION'); default: - session[kSocket][prop] = value; + const socket = session[kSocket]; + if (socket === undefined) + throw new errors.Error('ERR_HTTP2_SOCKET_UNBOUND'); + socket[prop] = value; return true; } } @@ -683,8 +746,11 @@ const proxySocketHandler = { // data received on the PING acknowlegement. function pingCallback(cb) { return function pingCallback(ack, duration, payload) { - const err = ack ? null : new errors.Error('ERR_HTTP2_PING_CANCEL'); - cb(err, duration, payload); + if (ack) { + cb(null, duration, payload); + } else { + cb(new errors.Error('ERR_HTTP2_PING_CANCEL')); + } }; } @@ -746,10 +812,12 @@ function setupHandle(socket, type, options) { handle.error = onSessionInternalError; handle.onpriority = onPriority; handle.onsettings = onSettings; + handle.onping = onPing; handle.onheaders = onSessionHeaders; handle.onframeerror = onFrameError; handle.ongoawaydata = onGoawayData; handle.onaltsvc = onAltSvc; + handle.onorigin = onOrigin; if (typeof options.selectPadding === 'function') handle.ongetpadding = onSelectPadding(options.selectPadding); @@ -776,6 +844,12 @@ function setupHandle(socket, type, options) { options.settings : {}; this.settings(settings); + + if (type === NGHTTP2_SESSION_SERVER && + Array.isArray(options.origins)) { + this.origin(...options.origins); + } + process.nextTick(emit, this, 'connect', this, socket); } @@ -787,6 +861,21 @@ function emitClose(self, error) { self.emit('close'); } +function finishSessionDestroy(session, error) { + const socket = session[kSocket]; + if (!socket.destroyed) + socket.destroy(error); + + session[kProxySocket] = undefined; + session[kSocket] = undefined; + session[kHandle] = undefined; + socket[kSession] = undefined; + socket[kServer] = undefined; + + // Finally, emit the close and error events (if necessary) on next tick. + process.nextTick(emitClose, session, error); +} + // Upon creation, the Http2Session takes ownership of the socket. The session // may not be ready to use immediately if the socket is not yet fully connected. // In that case, the Http2Session will wait for the socket to connect. Once @@ -843,6 +932,8 @@ class Http2Session extends EventEmitter { this[kState] = { flags: SESSION_FLAGS_PENDING, + goawayCode: null, + goawayLastStreamID: null, streams: new Map(), pendingStreams: new Set(), pendingAck: 0, @@ -897,23 +988,7 @@ class Http2Session extends EventEmitter { get originSet() { if (!this.encrypted || this.destroyed) return undefined; - - let originSet = this[kState].originSet; - if (originSet === undefined) { - const socket = this[kSocket]; - this[kState].originSet = originSet = new Set(); - if (socket.servername != null) { - let originString = `https://${socket.servername}`; - if (socket.remotePort != null) - originString += `:${socket.remotePort}`; - // We have to ensure that it is a properly serialized - // ASCII origin string. The socket.servername might not - // be properly ASCII encoded. - originSet.add((new URL(originString)).origin); - } - } - - return Array.from(originSet); + return Array.from(initOriginSet(this)); } // True if the Http2Session is still waiting for the socket to connect @@ -1145,25 +1220,13 @@ class Http2Session extends EventEmitter { if (handle !== undefined) handle.destroy(code, socket.destroyed); - // If there is no error, use setImmediate to destroy the socket on the + // If the socket is alive, use setImmediate to destroy the session on the // next iteration of the event loop in order to give data time to transmit. // Otherwise, destroy immediately. - if (!socket.destroyed) { - if (!error) { - setImmediate(socket.destroy.bind(socket)); - } else { - socket.destroy(error); - } - } - - this[kProxySocket] = undefined; - this[kSocket] = undefined; - this[kHandle] = undefined; - socket[kSession] = undefined; - socket[kServer] = undefined; - - // Finally, emit the close and error events (if necessary) on next tick. - process.nextTick(emitClose, this, error); + if (!socket.destroyed) + setImmediate(finishSessionDestroy, this, error); + else + finishSessionDestroy(this, error); } // Closing the session will: @@ -1303,6 +1366,41 @@ class ServerHttp2Session extends Http2Session { this[kHandle].altsvc(stream, origin || '', alt); } + + // Submits an origin frame to be sent. + origin(...origins) { + if (this.destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); + + if (origins.length === 0) + return; + + let arr = ''; + let len = 0; + const count = origins.length; + for (var i = 0; i < count; i++) { + let origin = origins[i]; + if (typeof origin === 'string') { + origin = (new URL(origin)).origin; + } else if (origin != null && typeof origin === 'object') { + origin = origin.origin; + } + if (typeof origin !== 'string') + throw new errors.Error('ERR_INVALID_ARG_TYPE', 'origin', 'string', + origin); + if (origin === 'null') + throw new errors.Error('ERR_HTTP2_INVALID_ORIGIN'); + + arr += `${origin}\0`; + len += origin.length; + } + + if (len > 16382) + throw new errors.Error('ERR_HTTP2_ORIGIN_LENGTH'); + + this[kHandle].origin(arr, count); + } + } // ClientHttp2Session instances have to wait for the socket to connect after @@ -1367,27 +1465,25 @@ class ClientHttp2Session extends Http2Session { options.endStream); } - if (options.getTrailers !== undefined && - typeof options.getTrailers !== 'function') { - throw new errors.TypeError('ERR_INVALID_OPT_VALUE', - 'getTrailers', - options.getTrailers); - } - const headersList = mapToHeaders(headers); if (!Array.isArray(headersList)) throw headersList; const stream = new ClientHttp2Stream(this, undefined, undefined, {}); stream[kSentHeaders] = headers; + stream[kOrigin] = `${headers[HTTP2_HEADER_SCHEME]}://` + + `${headers[HTTP2_HEADER_AUTHORITY]}`; // Close the writable side of the stream if options.endStream is set. if (options.endStream) stream.end(); + if (options.waitForTrailers) + stream[kState].flags |= STREAM_FLAGS_HAS_TRAILERS; + const onConnect = requestOnConnect.bind(stream, headersList, options); if (this.connecting) { - this.on('connect', onConnect); + this.once('connect', onConnect); } else { onConnect(); } @@ -1441,7 +1537,7 @@ function afterDoStreamWrite(status, handle, req) { } function streamOnResume() { - if (!this.destroyed && !this.pending) + if (!this.destroyed) this[kHandle].readStart(); } @@ -1450,16 +1546,6 @@ function streamOnPause() { this[kHandle].readStop(); } -// If the writable side of the Http2Stream is still open, emit the -// 'aborted' event and set the aborted flag. -function abort(stream) { - if (!stream.aborted && - !(stream._writableState.ended || stream._writableState.ending)) { - stream[kState].flags |= STREAM_FLAGS_ABORTED; - stream.emit('aborted'); - } -} - function afterShutdown() { this.callback(); const stream = this.handle[kOwner]; @@ -1467,6 +1553,74 @@ function afterShutdown() { stream[kMaybeDestroy](); } +function finishSendTrailers(stream, headersList) { + // The stream might be destroyed and in that case + // there is nothing to do. + // This can happen because finishSendTrailers is + // scheduled via setImmediate. + if (stream.destroyed) { + return; + } + + stream[kState].flags &= ~STREAM_FLAGS_HAS_TRAILERS; + + const ret = stream[kHandle].trailers(headersList); + if (ret < 0) + stream.destroy(new NghttpError(ret)); + else + stream[kMaybeDestroy](); +} + +const kNoRstStream = 0; +const kSubmitRstStream = 1; +const kForceRstStream = 2; + +function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { + const state = stream[kState]; + state.flags |= STREAM_FLAGS_CLOSED; + state.rstCode = code; + + // Clear timeout and remove timeout listeners + stream.setTimeout(0); + stream.removeAllListeners('timeout'); + + const { ending, finished } = stream._writableState; + + if (!ending) { + // If the writable side of the Http2Stream is still open, emit the + // 'aborted' event and set the aborted flag. + if (!stream.aborted) { + state.flags |= STREAM_FLAGS_ABORTED; + stream.emit('aborted'); + } + + // Close the writable side. + stream.end(); + } + + if (rstStreamStatus !== kNoRstStream) { + const finishFn = finishCloseStream.bind(stream, code); + if (!ending || finished || code !== NGHTTP2_NO_ERROR || + rstStreamStatus === kForceRstStream) + finishFn(); + else + stream.once('finish', finishFn); + } +} + +function finishCloseStream(code) { + const rstStreamFn = submitRstStream.bind(this, code); + // If the handle has not yet been assigned, queue up the request to + // ensure that the RST_STREAM frame is sent after the stream ID has + // been determined. + if (this.pending) { + this.push(null); + this.once('ready', rstStreamFn); + return; + } + rstStreamFn(); +} + // An Http2Stream is a Duplex stream that is backed by a // node::http2::Http2Stream handle implementing StreamBase. class Http2Stream extends Duplex { @@ -1483,13 +1637,19 @@ class Http2Stream extends Duplex { this[kSession] = session; session[kState].pendingStreams.add(this); + // Allow our logic for determining whether any reads have happened to + // work in all situations. This is similar to what we do in _http_incoming. + this._readableState.readingMore = true; + this[kState] = { + didRead: false, flags: STREAM_FLAGS_PENDING, rstCode: NGHTTP2_NO_ERROR, - writeQueueSize: 0 + writeQueueSize: 0, + trailersReady: false, + endAfterHeaders: false }; - this.on('resume', streamOnResume); this.on('pause', streamOnPause); } @@ -1532,6 +1692,10 @@ class Http2Stream extends Duplex { return `Http2Stream ${util.format(obj)}`; } + get endAfterHeaders() { + return this[kState].endAfterHeaders; + } + get sentHeaders() { return this[kSentHeaders]; } @@ -1615,13 +1779,16 @@ class Http2Stream extends Duplex { 'bug in Node.js'); } - _write(data, encoding, cb) { + [kWriteGeneric](writev, data, encoding, cb) { // When the Http2Stream is first created, it is corked until the // handle and the stream ID is assigned. However, if the user calls // uncork() before that happens, the Duplex will attempt to pass // writes through. Those need to be queued up here. if (this.pending) { - this.once('ready', this._write.bind(this, data, encoding, cb)); + this.once( + 'ready', + this[kWriteGeneric].bind(this, writev, data, encoding, cb) + ); return; } @@ -1645,53 +1812,30 @@ class Http2Stream extends Duplex { req.callback = cb; req.oncomplete = afterDoStreamWrite; req.async = false; - const err = createWriteReq(req, handle, data, encoding); + + let err; + if (writev) { + const chunks = new Array(data.length << 1); + for (var i = 0; i < data.length; i++) { + const entry = data[i]; + chunks[i * 2] = entry.chunk; + chunks[i * 2 + 1] = entry.encoding; + } + err = handle.writev(req, chunks); + } else { + err = createWriteReq(req, handle, data, encoding); + } if (err) return this.destroy(errors.errnoException(err, 'write', req.error), cb); trackWriteState(this, req.bytes); } - _writev(data, cb) { - // When the Http2Stream is first created, it is corked until the - // handle and the stream ID is assigned. However, if the user calls - // uncork() before that happens, the Duplex will attempt to pass - // writes through. Those need to be queued up here. - if (this.pending) { - this.once('ready', this._writev.bind(this, data, cb)); - return; - } - - // If the stream has been destroyed, there's nothing else we can do - // because the handle has been destroyed. This should only be an - // issue if a write occurs before the 'ready' event in the case where - // the duplex is uncorked before the stream is ready to go. In that - // case, drop the data on the floor. An error should have already been - // emitted. - if (this.destroyed) - return; - - this[kUpdateTimer](); - - if (!this.headersSent) - this[kProceed](); + _write(data, encoding, cb) { + this[kWriteGeneric](false, data, encoding, cb); + } - const handle = this[kHandle]; - const req = new WriteWrap(); - req.stream = this[kID]; - req.handle = handle; - req.callback = cb; - req.oncomplete = afterDoStreamWrite; - req.async = false; - const chunks = new Array(data.length << 1); - for (var i = 0; i < data.length; i++) { - const entry = data[i]; - chunks[i * 2] = entry.chunk; - chunks[i * 2 + 1] = entry.encoding; - } - const err = handle.writev(req, chunks); - if (err) - return this.destroy(errors.errnoException(err, 'write', req.error), cb); - trackWriteState(this, req.bytes); + _writev(data, cb) { + this[kWriteGeneric](true, data, '', cb); } _final(cb) { @@ -1716,6 +1860,10 @@ class Http2Stream extends Duplex { this.push(null); return; } + if (!this[kState].didRead) { + this._readableState.readingMore = false; + this[kState].didRead = true; + } if (!this.pending) { streamOnResume.call(this); } else { @@ -1742,6 +1890,32 @@ class Http2Stream extends Duplex { priorityFn(); } + sendTrailers(headers) { + if (this.destroyed || this.closed) + throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); + if (this[kSentTrailers]) + throw new errors.Error('ERR_HTTP2_TRAILERS_ALREADY_SENT'); + if (!this[kState].trailersReady) + throw new errors.Error('ERR_HTTP2_TRAILERS_NOT_READY'); + + assertIsObject(headers, 'headers'); + headers = Object.assign(Object.create(null), headers); + + const session = this[kSession]; + debug(`Http2Stream ${this[kID]} [Http2Session ` + + `${sessionName(session[kType])}]: sending trailers`); + + this[kUpdateTimer](); + + const headersList = mapToHeaders(headers, assertValidPseudoHeaderTrailer); + if (!Array.isArray(headersList)) + throw headersList; + this[kSentTrailers] = headers; + + // Send the trailers in setImmediate so we don't do it on nghttp2 stack. + setImmediate(finishSendTrailers, this, headersList); + } + get closed() { return !!(this[kState].flags & STREAM_FLAGS_CLOSED); } @@ -1767,38 +1941,13 @@ class Http2Stream extends Duplex { if (callback !== undefined && typeof callback !== 'function') throw new errors.TypeError('ERR_INVALID_CALLBACK'); - // Unenroll the timeout. - unenroll(this); - this.removeAllListeners('timeout'); - - // Close the writable - abort(this); - this.end(); - if (this.closed) return; - const state = this[kState]; - state.flags |= STREAM_FLAGS_CLOSED; - state.rstCode = code; - - if (callback !== undefined) { + if (callback !== undefined) this.once('close', callback); - } - - if (this[kHandle] === undefined) - return; - const rstStreamFn = submitRstStream.bind(this, code); - // If the handle has not yet been assigned, queue up the request to - // ensure that the RST_STREAM frame is sent after the stream ID has - // been determined. - if (this.pending) { - this.push(null); - this.once('ready', rstStreamFn); - return; - } - rstStreamFn(); + closeStream(this, code); } // Called by this.destroy(). @@ -1813,24 +1962,19 @@ class Http2Stream extends Duplex { debug(`Http2Stream ${this[kID] || ''} [Http2Session ` + `${sessionName(session[kType])}]: destroying stream`); const state = this[kState]; - const code = state.rstCode = - err != null ? - NGHTTP2_INTERNAL_ERROR : - state.rstCode || NGHTTP2_NO_ERROR; - if (handle !== undefined) { - // If the handle exists, we need to close, then destroy the handle - this.close(code); - if (!this._readableState.ended && !this._readableState.ending) - this.push(null); + const code = err != null ? + NGHTTP2_INTERNAL_ERROR : (state.rstCode || NGHTTP2_NO_ERROR); + + const hasHandle = handle !== undefined; + + if (!this.closed) + closeStream(this, code, hasHandle ? kForceRstStream : kNoRstStream); + this.push(null); + + if (hasHandle) { handle.destroy(); session[kState].streams.delete(id); } else { - unenroll(this); - this.removeAllListeners('timeout'); - state.flags |= STREAM_FLAGS_CLOSED; - abort(this); - this.end(); - this.push(null); session[kState].pendingStreams.delete(this); } @@ -1859,20 +2003,33 @@ class Http2Stream extends Duplex { } // The Http2Stream can be destroyed if it has closed and if the readable // side has received the final chunk. - [kMaybeDestroy](error, code = NGHTTP2_NO_ERROR) { - if (error || code !== NGHTTP2_NO_ERROR) { - this.destroy(error); + [kMaybeDestroy](code = NGHTTP2_NO_ERROR) { + if (code !== NGHTTP2_NO_ERROR) { + this.destroy(); return; } // TODO(mcollina): remove usage of _*State properties - if (this._readableState.ended && - this._writableState.ended && - this._writableState.pendingcb === 0 && - this.closed) { - this.destroy(); - // This should return, but eslint complains. - // return + if (this._writableState.finished) { + if (!this.readable && this.closed) { + this.destroy(); + return; + } + + // We've submitted a response from our server session, have not attempted + // to process any incoming data, and have no trailers. This means we can + // attempt to gracefully close the session. + const state = this[kState]; + if (this.headersSent && + this[kSession][kType] === NGHTTP2_SESSION_SERVER && + !(state.flags & STREAM_FLAGS_HAS_TRAILERS) && + !state.didRead && + this._readableState.flowing === null) { + // By using setImmediate we allow pushStreams to make it through + // before the stream is officially closed. This prevents a bug + // in most browsers where those pushStreams would be rejected. + setImmediate(this.close.bind(this)); + } } } } @@ -2033,7 +2190,6 @@ function afterOpen(session, options, headers, streamOptions, err, fd) { } if (this.destroyed || this.closed) { tryClose(fd); - abort(this); return; } state.fd = fd; @@ -2072,6 +2228,8 @@ class ServerHttp2Stream extends Http2Stream { pushStream(headers, options, callback) { if (!this.pushAllowed) throw new errors.Error('ERR_HTTP2_PUSH_DISABLED'); + if (this[kID] % 2 === 0) + throw new errors.Error('ERR_HTTP2_NESTED_PUSH'); const session = this[kSession]; @@ -2107,7 +2265,7 @@ class ServerHttp2Stream extends Http2Stream { let headRequest = false; if (headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) headRequest = options.endStream = true; - options.readable = !options.endStream; + options.readable = false; const headersList = mapToHeaders(headers); if (!Array.isArray(headersList)) @@ -2169,14 +2327,9 @@ class ServerHttp2Stream extends Http2Stream { if (options.endStream) streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD; - if (options.getTrailers !== undefined) { - if (typeof options.getTrailers !== 'function') { - throw new errors.TypeError('ERR_INVALID_OPT_VALUE', - 'getTrailers', - options.getTrailers); - } + if (options.waitForTrailers) { streamOptions |= STREAM_OPTION_GET_TRAILERS; - state.getTrailers = options.getTrailers; + state.flags |= STREAM_FLAGS_HAS_TRAILERS; } headers = processHeaders(headers); @@ -2243,14 +2396,9 @@ class ServerHttp2Stream extends Http2Stream { } let streamOptions = 0; - if (options.getTrailers !== undefined) { - if (typeof options.getTrailers !== 'function') { - throw new errors.TypeError('ERR_INVALID_OPT_VALUE', - 'getTrailers', - options.getTrailers); - } + if (options.waitForTrailers) { streamOptions |= STREAM_OPTION_GET_TRAILERS; - this[kState].getTrailers = options.getTrailers; + this[kState].flags |= STREAM_FLAGS_HAS_TRAILERS; } if (typeof fd !== 'number') @@ -2317,14 +2465,9 @@ class ServerHttp2Stream extends Http2Stream { } let streamOptions = 0; - if (options.getTrailers !== undefined) { - if (typeof options.getTrailers !== 'function') { - throw new errors.TypeError('ERR_INVALID_OPT_VALUE', - 'getTrailers', - options.getTrailers); - } + if (options.waitForTrailers) { streamOptions |= STREAM_OPTION_GET_TRAILERS; - this[kState].getTrailers = options.getTrailers; + this[kState].flags |= STREAM_FLAGS_HAS_TRAILERS; } const session = this[kSession]; @@ -2449,6 +2592,10 @@ Object.defineProperty(Http2Session.prototype, 'setTimeout', setTimeout); function socketOnError(error) { const session = this[kSession]; if (session !== undefined) { + // We can ignore ECONNRESET after GOAWAY was received as there's nothing + // 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}]`); session.destroy(error); @@ -2757,13 +2904,13 @@ function getUnpackedSettings(buf, options = {}) { // Exports module.exports = { + connect, constants, + createServer, + createSecureServer, getDefaultSettings, getPackedSettings, getUnpackedSettings, - createServer, - createSecureServer, - connect, Http2Session, Http2Stream, Http2ServerRequest, diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index ef48b83d783af0..e7ef7db59077b3 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -504,7 +504,7 @@ function toHeaderObject(headers) { value |= 0; var existing = obj[name]; if (existing === undefined) { - obj[name] = value; + obj[name] = name === HTTP2_HEADER_SET_COOKIE ? [value] : value; } else if (!kSingleValueHeaders.has(name)) { switch (name) { case HTTP2_HEADER_COOKIE: @@ -523,10 +523,7 @@ function toHeaderObject(headers) { // fields with the same name. Since it cannot be combined into a // single field-value, recipients ought to handle "Set-Cookie" as a // special case while processing header fields." - if (Array.isArray(existing)) - existing.push(value); - else - obj[name] = [existing, value]; + existing.push(value); break; default: // https://tools.ietf.org/html/rfc7230#section-3.2.2 diff --git a/src/env-inl.h b/src/env-inl.h index bdf3e8ae453f9f..e30370633fd3da 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -417,12 +417,12 @@ inline void Environment::set_http_parser_buffer(char* buffer) { http_parser_buffer_ = buffer; } -inline http2::http2_state* Environment::http2_state() const { +inline http2::Http2State* Environment::http2_state() const { return http2_state_.get(); } inline void Environment::set_http2_state( - std::unique_ptr buffer) { + std::unique_ptr buffer) { CHECK(!http2_state_); // Should be set only once. http2_state_ = std::move(buffer); } diff --git a/src/env.h b/src/env.h index 7b4a6999755bcf..e378869b4ccdcb 100644 --- a/src/env.h +++ b/src/env.h @@ -219,11 +219,13 @@ class ModuleWrap; V(onnewsessiondone_string, "onnewsessiondone") \ V(onocspresponse_string, "onocspresponse") \ V(ongoawaydata_string, "ongoawaydata") \ + V(onorigin_string, "onorigin") \ V(onpriority_string, "onpriority") \ V(onread_string, "onread") \ V(onreadstart_string, "onreadstart") \ V(onreadstop_string, "onreadstop") \ V(onselect_string, "onselect") \ + V(onping_string, "onping") \ V(onsettings_string, "onsettings") \ V(onshutdown_string, "onshutdown") \ V(onsignal_string, "onsignal") \ @@ -617,8 +619,8 @@ class Environment { inline char* http_parser_buffer() const; inline void set_http_parser_buffer(char* buffer); - inline http2::http2_state* http2_state() const; - inline void set_http2_state(std::unique_ptr state); + inline http2::Http2State* http2_state() const; + inline void set_http2_state(std::unique_ptr state); inline double* fs_stats_field_array() const; inline void set_fs_stats_field_array(double* fields); @@ -759,7 +761,7 @@ class Environment { double* heap_space_statistics_buffer_ = nullptr; char* http_parser_buffer_; - std::unique_ptr http2_state_; + std::unique_ptr http2_state_; double* fs_stats_field_array_; diff --git a/src/node_http2.cc b/src/node_http2.cc index e9a06a88635882..49238dcae2d966 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -93,7 +93,7 @@ Http2Scope::~Http2Scope() { // instances to configure an appropriate nghttp2_options struct. The class // uses a single TypedArray instance that is shared with the JavaScript side // to more efficiently pass values back and forth. -Http2Options::Http2Options(Environment* env) { +Http2Options::Http2Options(Environment* env, nghttp2_session_type type) { nghttp2_option_new(&options_); // We manually handle flow control within a session in order to @@ -104,10 +104,12 @@ Http2Options::Http2Options(Environment* env) { // are required to buffer. nghttp2_option_set_no_auto_window_update(options_, 1); - // Enable built in support for ALTSVC frames. Once we add support for - // other non-built in extension frames, this will need to be handled - // a bit differently. For now, let's let nghttp2 take care of it. - nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC); + // Enable built in support for receiving ALTSVC and ORIGIN frames (but + // only on client side sessions + if (type == NGHTTP2_SESSION_CLIENT) { + nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC); + nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ORIGIN); + } AliasedBuffer& buffer = env->http2_state()->options_buffer; @@ -446,6 +448,54 @@ Headers::Headers(Isolate* isolate, } } +Origins::Origins(Local context, + Local origin_string, + size_t origin_count) : count_(origin_count) { + int origin_string_len = origin_string->Length(); + if (count_ == 0) { + CHECK_EQ(origin_string_len, 0); + return; + } + + // Allocate a single buffer with count_ nghttp2_nv structs, followed + // by the raw header data as passed from JS. This looks like: + // | possible padding | nghttp2_nv | nghttp2_nv | ... | header contents | + buf_.AllocateSufficientStorage((alignof(nghttp2_origin_entry) - 1) + + count_ * sizeof(nghttp2_origin_entry) + + origin_string_len); + + // Make sure the start address is aligned appropriately for an nghttp2_nv*. + char* start = reinterpret_cast( + ROUND_UP(reinterpret_cast(*buf_), + alignof(nghttp2_origin_entry))); + char* origin_contents = start + (count_ * sizeof(nghttp2_origin_entry)); + nghttp2_origin_entry* const nva = + reinterpret_cast(start); + + CHECK_LE(origin_contents + origin_string_len, *buf_ + buf_.length()); + CHECK_EQ(origin_string->WriteOneByte( + reinterpret_cast(origin_contents), + 0, + origin_string_len, + String::NO_NULL_TERMINATION), + origin_string_len); + + size_t n = 0; + char* p; + for (p = origin_contents; p < origin_contents + origin_string_len; n++) { + if (n >= count_) { + static uint8_t zero = '\0'; + nva[0].origin = &zero; + nva[0].origin_len = 1; + count_ = 1; + return; + } + + nva[n].origin = reinterpret_cast(p); + nva[n].origin_len = strlen(p); + p += nva[n].origin_len + 1; + } +} // Sets the various callback functions that nghttp2 will use to notify us // about significant events while processing http2 stuff. @@ -486,6 +536,92 @@ Http2Session::Callbacks::~Callbacks() { nghttp2_session_callbacks_del(callbacks); } +// Track memory allocated by nghttp2 using a custom allocator. +class Http2Session::MemoryAllocatorInfo { + public: + explicit MemoryAllocatorInfo(Http2Session* session) + : info({ session, H2Malloc, H2Free, H2Calloc, H2Realloc }) {} + + static void* H2Malloc(size_t size, void* user_data) { + return H2Realloc(nullptr, size, user_data); + } + + static void* H2Calloc(size_t nmemb, size_t size, void* user_data) { + size_t real_size = MultiplyWithOverflowCheck(nmemb, size); + void* mem = H2Malloc(real_size, user_data); + if (mem != nullptr) + memset(mem, 0, real_size); + return mem; + } + + static void H2Free(void* ptr, void* user_data) { + if (ptr == nullptr) return; // free(null); happens quite often. + void* result = H2Realloc(ptr, 0, user_data); + CHECK_EQ(result, nullptr); + } + + static void* H2Realloc(void* ptr, size_t size, void* user_data) { + Http2Session* session = static_cast(user_data); + size_t previous_size = 0; + char* original_ptr = nullptr; + + // We prepend each allocated buffer with a size_t containing the full + // size of the allocation. + if (size > 0) size += sizeof(size_t); + + if (ptr != nullptr) { + // We are free()ing or re-allocating. + original_ptr = static_cast(ptr) - sizeof(size_t); + previous_size = *reinterpret_cast(original_ptr); + // This means we called StopTracking() on this pointer before. + if (previous_size == 0) { + // Fall back to the standard Realloc() function. + char* ret = UncheckedRealloc(original_ptr, size); + if (ret != nullptr) + ret += sizeof(size_t); + return ret; + } + } + CHECK_GE(session->current_nghttp2_memory_, previous_size); + + // TODO(addaleax): Add the following, and handle NGHTTP2_ERR_NOMEM properly + // everywhere: + // + // if (size > previous_size && + // !session->IsAvailableSessionMemory(size - previous_size)) { + // return nullptr; + //} + + char* mem = UncheckedRealloc(original_ptr, size); + + if (mem != nullptr) { + // Adjust the memory info counter. + session->current_nghttp2_memory_ += size - previous_size; + *reinterpret_cast(mem) = size; + mem += sizeof(size_t); + } else if (size == 0) { + session->current_nghttp2_memory_ -= previous_size; + } + + return mem; + } + + static void StopTracking(Http2Session* session, void* ptr) { + size_t* original_ptr = reinterpret_cast( + static_cast(ptr) - sizeof(size_t)); + session->current_nghttp2_memory_ -= *original_ptr; + *original_ptr = 0; + } + + inline nghttp2_mem* operator*() { return &info; } + + nghttp2_mem info; +}; + +void Http2Session::StopTrackingRcbuf(nghttp2_rcbuf* buf) { + MemoryAllocatorInfo::StopTracking(this, buf); +} + Http2Session::Http2Session(Environment* env, Local wrap, nghttp2_session_type type) @@ -495,7 +631,7 @@ Http2Session::Http2Session(Environment* env, statistics_.start_time = uv_hrtime(); // Capture the configuration options for this session - Http2Options opts(env); + Http2Options opts(env, type); max_session_memory_ = opts.GetMaxSessionMemory(); @@ -517,15 +653,17 @@ Http2Session::Http2Session(Environment* env, = callback_struct_saved[hasGetPaddingCallback ? 1 : 0].callbacks; auto fn = type == NGHTTP2_SESSION_SERVER ? - nghttp2_session_server_new2 : - nghttp2_session_client_new2; + nghttp2_session_server_new3 : + nghttp2_session_client_new3; + + MemoryAllocatorInfo allocator_info(this); // This should fail only if the system is out of memory, which // is going to cause lots of other problems anyway, or if any // of the options are out of acceptable range, which we should // be catching before it gets this far. Either way, crash if this // fails. - CHECK_EQ(fn(&session_, callbacks, this, *opts), 0); + CHECK_EQ(fn(&session_, callbacks, this, *opts, *allocator_info), 0); outgoing_storage_.reserve(4096); outgoing_buffers_.reserve(32); @@ -548,11 +686,12 @@ Http2Session::~Http2Session() { ClearWrap(object()); persistent().Reset(); CHECK(persistent().IsEmpty()); - for (const auto& iter : streams_) - iter.second->session_ = nullptr; + for (const auto& stream : streams_) + stream.second->session_ = nullptr; Unconsume(); DEBUG_HTTP2SESSION(this, "freeing nghttp2 session"); nghttp2_session_del(session_); + CHECK_EQ(current_nghttp2_memory_, 0); } inline bool HasHttp2Observer(Environment* env) { @@ -631,9 +770,9 @@ inline void Http2Session::EmitStatistics() { void Http2Session::Close(uint32_t code, bool socket_closed) { DEBUG_HTTP2SESSION(this, "closing session"); - if (flags_ & SESSION_STATE_CLOSED) + if (flags_ & SESSION_STATE_CLOSING) return; - flags_ |= SESSION_STATE_CLOSED; + flags_ |= SESSION_STATE_CLOSING; // Stop reading on the i/o stream if (stream_ != nullptr) @@ -641,16 +780,18 @@ void Http2Session::Close(uint32_t code, bool socket_closed) { // If the socket is not closed, then attempt to send a closing GOAWAY // frame. There is no guarantee that this GOAWAY will be received by - // the peer but the HTTP/2 spec recommends sendinng it anyway. We'll + // the peer but the HTTP/2 spec recommends sending it anyway. We'll // make a best effort. if (!socket_closed) { - Http2Scope h2scope(this); DEBUG_HTTP2SESSION2(this, "terminating session with code %d", code); CHECK_EQ(nghttp2_session_terminate_session(session_, code), 0); + SendPendingData(); } else { Unconsume(); } + flags_ |= SESSION_STATE_CLOSED; + // If there are outstanding pings, those will need to be canceled, do // so on the next iteration of the event loop to avoid calling out into // javascript since this may be called during garbage collection. @@ -842,7 +983,12 @@ inline int Http2Session::OnHeaderCallback(nghttp2_session* handle, Http2Session* session = static_cast(user_data); int32_t id = GetFrameID(frame); Http2Stream* stream = session->FindStream(id); - CHECK_NE(stream, nullptr); + // 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) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + // If the stream has already been destroyed, ignore. if (stream->IsDestroyed()) return 0; @@ -889,6 +1035,9 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle, case NGHTTP2_ALTSVC: session->HandleAltSvcFrame(frame); break; + case NGHTTP2_ORIGIN: + session->HandleOriginFrame(frame); + break; default: break; } @@ -1117,36 +1266,6 @@ inline int Http2Session::OnNghttpError(nghttp2_session* handle, return 0; } -// Once all of the DATA frames for a Stream have been sent, the GetTrailers -// method calls out to JavaScript to fetch the trailing headers that need -// to be sent. -inline void Http2Session::GetTrailers(Http2Stream* stream, uint32_t* flags) { - if (!stream->IsDestroyed() && stream->HasTrailers()) { - Http2Stream::SubmitTrailers submit_trailers{this, stream, flags}; - stream->OnTrailers(submit_trailers); - } -} - - -Http2Stream::SubmitTrailers::SubmitTrailers( - Http2Session* session, - Http2Stream* stream, - uint32_t* flags) - : session_(session), stream_(stream), flags_(flags) { } - - -inline void Http2Stream::SubmitTrailers::Submit(nghttp2_nv* trailers, - size_t length) const { - Http2Scope h2scope(session_); - if (length == 0) - return; - DEBUG_HTTP2SESSION2(session_, "sending trailers for stream %d, count: %d", - stream_->id(), length); - *flags_ |= NGHTTP2_DATA_FLAG_NO_END_STREAM; - CHECK_EQ( - nghttp2_submit_trailer(**session_, stream_->id(), trailers, length), 0); -} - // Called by OnFrameReceived to notify JavaScript land that a complete // HEADERS frame has been received and processed. This method converts the @@ -1165,8 +1284,7 @@ inline void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) { if (stream->IsDestroyed()) return; - nghttp2_header* headers = stream->headers(); - size_t count = stream->headers_count(); + std::vector headers(stream->move_headers()); Local name_str; Local value_str; @@ -1183,18 +1301,17 @@ inline void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) { // this way for performance reasons (it's faster to generate and pass an // array than it is to generate and pass the object). size_t n = 0; - while (count > 0) { + while (n < headers.size()) { size_t j = 0; - while (count > 0 && j < arraysize(argv) / 2) { + while (n < headers.size() && j < arraysize(argv) / 2) { nghttp2_header item = headers[n++]; // The header name and value are passed as external one-byte strings name_str = - ExternalHeader::New(env(), item.name).ToLocalChecked(); + ExternalHeader::New(this, item.name).ToLocalChecked(); value_str = - ExternalHeader::New(env(), item.value).ToLocalChecked(); + ExternalHeader::New(this, item.value).ToLocalChecked(); argv[j * 2] = name_str; argv[j * 2 + 1] = value_str; - count--; j++; } // For performance, we pass name and value pairs to array.protototype.push @@ -1314,8 +1431,48 @@ inline void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) { MakeCallback(env()->onaltsvc_string(), arraysize(argv), argv); } +void Http2Session::HandleOriginFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local context = env()->context(); + Context::Scope context_scope(context); + + DEBUG_HTTP2SESSION2(this, "handling origin frame"); + + nghttp2_extension ext = frame->ext; + nghttp2_ext_origin* origin = static_cast(ext.payload); + + Local holder = Array::New(isolate); + Local fn = env()->push_values_to_array_function(); + Local argv[NODE_PUSH_VAL_TO_ARRAY_MAX]; + + size_t n = 0; + while (n < origin->nov) { + size_t j = 0; + while (n < origin->nov && j < arraysize(argv)) { + auto entry = origin->ov[n++]; + argv[j++] = + String::NewFromOneByte(isolate, + entry.origin, + v8::NewStringType::kNormal, + entry.origin_len).ToLocalChecked(); + } + if (j > 0) + fn->Call(context, holder, j, argv).ToLocalChecked(); + } + + Local args[1] = { holder }; + + MakeCallback(env()->onorigin_string(), arraysize(args), args); +} + // Called by OnFrameReceived when a complete PING frame has been received. inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local context = env()->context(); + Context::Scope context_scope(context); + Local arg; bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK; if (ack) { Http2Ping* ping = PopPing(); @@ -1327,16 +1484,15 @@ inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) { // receive an unsolicited PING ack on a connection. Either the peer // is buggy or malicious, and we're not going to tolerate such // nonsense. - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Local context = env()->context(); - Context::Scope context_scope(context); - - Local argv[1] = { - Integer::New(isolate, NGHTTP2_ERR_PROTO), - }; - MakeCallback(env()->error_string(), arraysize(argv), argv); + arg = Integer::New(isolate, NGHTTP2_ERR_PROTO); + MakeCallback(env()->error_string(), 1, &arg); } + } else { + // Notify the session that a ping occurred + arg = Buffer::Copy(env(), + reinterpret_cast(frame->ping.opaque_data), + 8).ToLocalChecked(); + MakeCallback(env()->onping_string(), 1, &arg); } } @@ -1417,20 +1573,46 @@ void Http2Session::MaybeScheduleWrite() { } } +void Http2Session::MaybeStopReading() { + int want_read = nghttp2_session_want_read(session_); + DEBUG_HTTP2SESSION2(this, "wants read? %d", want_read); + if (want_read == 0) + stream_->ReadStop(); +} + // Unset the sending state, finish up all current writes, and reset // storage for data and metadata that was associated with these writes. void Http2Session::ClearOutgoing(int status) { CHECK_NE(flags_ & SESSION_STATE_SENDING, 0); + flags_ &= ~SESSION_STATE_SENDING; - for (const nghttp2_stream_write& wr : outgoing_buffers_) { - WriteWrap* wrap = wr.req_wrap; - if (wrap != nullptr) - wrap->Done(status); + if (outgoing_buffers_.size() > 0) { + outgoing_storage_.clear(); + + std::vector current_outgoing_buffers_; + current_outgoing_buffers_.swap(outgoing_buffers_); + for (const nghttp2_stream_write& wr : current_outgoing_buffers_) { + WriteWrap* wrap = wr.req_wrap; + if (wrap != nullptr) + wrap->Done(status); + } } - outgoing_buffers_.clear(); - outgoing_storage_.clear(); + // Now that we've finished sending queued data, if there are any pending + // RstStreams we should try sending again and then flush them one by one. + if (pending_rst_streams_.size() > 0) { + std::vector current_pending_rst_streams; + pending_rst_streams_.swap(current_pending_rst_streams); + + SendPendingData(); + + for (int32_t stream_id : current_pending_rst_streams) { + Http2Stream* stream = FindStream(stream_id); + if (stream != nullptr) + stream->FlushRstStream(); + } + } } // Queue a given block of data for sending. This always creates a copy, @@ -1454,18 +1636,19 @@ void Http2Session::CopyDataIntoOutgoing(const uint8_t* src, size_t src_length) { // chunk out to the i/o socket to be sent. This is a particularly hot method // that will generally be called at least twice be event loop iteration. // This is a potential performance optimization target later. -void Http2Session::SendPendingData() { +// Returns non-zero value if a write is already in progress. +uint8_t Http2Session::SendPendingData() { DEBUG_HTTP2SESSION(this, "sending pending data"); // Do not attempt to send data on the socket if the destroying flag has // been set. That means everything is shutting down and the socket // will not be usable. if (IsDestroyed()) - return; + return 0; flags_ &= ~SESSION_STATE_WRITE_SCHEDULED; // SendPendingData should not be called recursively. if (flags_ & SESSION_STATE_SENDING) - return; + return 1; // This is cleared by ClearOutgoing(). flags_ |= SESSION_STATE_SENDING; @@ -1489,15 +1672,15 @@ void Http2Session::SendPendingData() { // does take care of things like closing the individual streams after // a socket has been torn down, so we still need to call it. ClearOutgoing(UV_ECANCELED); - return; + return 0; } // Part Two: Pass Data to the underlying stream size_t count = outgoing_buffers_.size(); if (count == 0) { - flags_ &= ~SESSION_STATE_SENDING; - return; + ClearOutgoing(0); + return 0; } MaybeStackBuffer bufs; bufs.AllocateSufficientStorage(count); @@ -1527,7 +1710,7 @@ void Http2Session::SendPendingData() { if (stream_->DoTryWrite(&writebufs, &count) != 0 || count == 0) { // All writes finished synchronously, nothing more to do here. ClearOutgoing(0); - return; + return 0; } WriteWrap* req = AllocateSend(); @@ -1535,8 +1718,9 @@ void Http2Session::SendPendingData() { req->Dispose(); } - DEBUG_HTTP2SESSION2(this, "wants data in return? %d", - nghttp2_session_want_read(session_)); + MaybeStopReading(); + + return 0; } @@ -1699,8 +1883,7 @@ void Http2Session::OnStreamReadImpl(ssize_t nread, }; session->MakeCallback(env->error_string(), arraysize(argv), argv); } else { - DEBUG_HTTP2SESSION2(session, "processed %d bytes. wants more? %d", ret, - nghttp2_session_want_read(**session)); + session->MaybeStopReading(); } } @@ -1783,12 +1966,17 @@ Http2Stream::Http2Stream( Http2Stream::~Http2Stream() { - if (session_ != nullptr) { - DEBUG_HTTP2STREAM(this, "tearing down stream"); - session_->RemoveStream(this); - session_ = nullptr; + for (nghttp2_header& header : current_headers_) { + nghttp2_rcbuf_decref(header.name); + nghttp2_rcbuf_decref(header.value); } + if (session_ == nullptr) + return; + DEBUG_HTTP2STREAM(this, "tearing down stream"); + session_->RemoveStream(this); + session_ = nullptr; + persistent().Reset(); CHECK(persistent().IsEmpty()); } @@ -1808,29 +1996,6 @@ nghttp2_stream* Http2Stream::operator*() { } -// Calls out to JavaScript land to fetch the actual trailer headers to send -// for this stream. -void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) { - DEBUG_HTTP2STREAM(this, "prompting for trailers"); - CHECK(!this->IsDestroyed()); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Local context = env()->context(); - Context::Scope context_scope(context); - - Local ret = - MakeCallback(env()->ontrailers_string(), 0, nullptr).ToLocalChecked(); - if (!ret.IsEmpty() && !IsDestroyed()) { - if (ret->IsArray()) { - Local headers = ret.As(); - if (headers->Length() > 0) { - Headers trailers(isolate, context, headers); - submit_trailers.Submit(*trailers, trailers.length()); - } - } - } -} - inline void Http2Stream::Close(int32_t code) { CHECK(!this->IsDestroyed()); flags_ |= NGHTTP2_STREAM_FLAG_CLOSED; @@ -1863,6 +2028,8 @@ inline void Http2Stream::Destroy() { // Do nothing if this stream instance is already destroyed if (IsDestroyed()) return; + if (session_->HasPendingRstStream(id_)) + FlushRstStream(); flags_ |= NGHTTP2_STREAM_FLAG_DESTROYED; DEBUG_HTTP2STREAM(this, "destroying stream"); @@ -1884,7 +2051,8 @@ inline void Http2Stream::Destroy() { // We can destroy the stream now if there are no writes for it // already on the socket. Otherwise, we'll wait for the garbage collector // to take care of cleaning up. - if (!stream->session()->HasWritesOnSocketForStream(stream)) + if (stream->session() == nullptr || + !stream->session()->HasWritesOnSocketForStream(stream)) delete stream; }, this, this->object()); @@ -1952,6 +2120,36 @@ inline int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) { return ret; } +void Http2Stream::OnTrailers() { + DEBUG_HTTP2STREAM(this, "let javascript know we are ready for trailers"); + CHECK(!this->IsDestroyed()); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local context = env()->context(); + Context::Scope context_scope(context); + flags_ &= ~NGHTTP2_STREAM_FLAG_TRAILERS; + MakeCallback(env()->ontrailers_string(), 0, nullptr); +} + +// Submit informational headers for a stream. +int Http2Stream::SubmitTrailers(nghttp2_nv* nva, size_t len) { + CHECK(!this->IsDestroyed()); + Http2Scope h2scope(this); + DEBUG_HTTP2STREAM2(this, "sending %d trailers", len); + int ret; + // Sending an empty trailers frame poses problems in Safari, Edge & IE. + // Instead we can just send an empty data frame with NGHTTP2_FLAG_END_STREAM + // to indicate that the stream is ready to be closed. + if (len == 0) { + Http2Stream::Provider::Stream prov(this, 0); + ret = nghttp2_submit_data(**session_, NGHTTP2_FLAG_END_STREAM, id_, *prov); + } else { + ret = nghttp2_submit_trailer(**session_, id_, nva, len); + } + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + // Submit a PRIORITY frame to the connected peer. inline int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec, bool silent) { @@ -1972,12 +2170,25 @@ inline int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec, // peer. inline void Http2Stream::SubmitRstStream(const uint32_t code) { CHECK(!this->IsDestroyed()); + code_ = code; + // If possible, force a purge of any currently pending data here to make sure + // it is sent before closing the stream. If it returns non-zero then we need + // to wait until the current write finishes and try again to avoid nghttp2 + // behaviour where it prioritizes RstStream over everything else. + if (session_->SendPendingData() != 0) { + session_->AddPendingRstStream(id_); + return; + } + + FlushRstStream(); +} + +void Http2Stream::FlushRstStream() { + if (IsDestroyed()) + return; Http2Scope h2scope(this); - // Force a purge of any currently pending data here to make sure - // it is sent before closing the stream. - session_->SendPendingData(); CHECK_EQ(nghttp2_submit_rst_stream(**session_, NGHTTP2_FLAG_NONE, - id_, code), 0); + id_, code_), 0); } @@ -2184,13 +2395,6 @@ ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle, if (static_cast(numchars) < length || length <= 0) { DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id); *flags |= NGHTTP2_DATA_FLAG_EOF; - session->GetTrailers(stream, flags); - // If the stream or session gets destroyed during the GetTrailers - // callback, check that here and close down the stream - if (stream->IsDestroyed()) - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; - if (session->IsDestroyed()) - return NGHTTP2_ERR_CALLBACK_FAILURE; } stream->statistics_.sent_bytes += numchars; @@ -2258,13 +2462,10 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle, if (stream->queue_.empty() && !stream->IsWritable()) { DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id); *flags |= NGHTTP2_DATA_FLAG_EOF; - session->GetTrailers(stream, flags); - // If the stream or session gets destroyed during the GetTrailers - // callback, check that here and close down the stream - if (stream->IsDestroyed()) - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; - if (session->IsDestroyed()) - return NGHTTP2_ERR_CALLBACK_FAILURE; + if (stream->HasTrailers()) { + *flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + stream->OnTrailers(); + } } stream->statistics_.sent_bytes += amount; @@ -2570,8 +2771,22 @@ void Http2Stream::Info(const FunctionCallbackInfo& args) { Headers list(isolate, context, headers); args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length())); - DEBUG_HTTP2STREAM2(stream, "%d informational headers sent", - headers->Length()); + DEBUG_HTTP2STREAM2(stream, "%d informational headers sent", list.length()); +} + +// Submits trailing headers on the Http2Stream +void Http2Stream::Trailers(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + Isolate* isolate = env->isolate(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + + Local headers = args[0].As(); + + Headers list(isolate, context, headers); + args.GetReturnValue().Set(stream->SubmitTrailers(*list, list.length())); + DEBUG_HTTP2STREAM2(stream, "%d trailing headers sent", list.length()); } // Grab the numeric id of the Http2Stream @@ -2686,7 +2901,12 @@ void Http2Session::AltSvc(int32_t id, origin, origin_len, value, value_len), 0); } -// Submits an AltSvc frame to the sent to the connected peer. +void Http2Session::Origin(nghttp2_origin_entry* ov, size_t count) { + Http2Scope h2scope(this); + CHECK_EQ(nghttp2_submit_origin(session_, NGHTTP2_FLAG_NONE, ov, count), 0); +} + +// Submits an AltSvc frame to be sent to the connected peer. void Http2Session::AltSvc(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Http2Session* session; @@ -2714,6 +2934,23 @@ void Http2Session::AltSvc(const FunctionCallbackInfo& args) { session->AltSvc(id, *origin, origin_len, *value, value_len); } +void Http2Session::Origin(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + Local origin_string = args[0].As(); + int count = args[1]->IntegerValue(context).ToChecked(); + + + Origins origins(env->context(), + origin_string, + count); + + session->Origin(*origins, origins.length()); +} + // Submits a PING frame to be sent to the connected peer. void Http2Session::Ping(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -2832,8 +3069,8 @@ void Http2Session::Http2Ping::Send(uint8_t* payload) { } void Http2Session::Http2Ping::Done(bool ack, const uint8_t* payload) { - session_->statistics_.ping_rtt = (uv_hrtime() - startTime_); - double duration = (session_->statistics_.ping_rtt - startTime_) / 1e6; + session_->statistics_.ping_rtt = uv_hrtime() - startTime_; + double duration = session_->statistics_.ping_rtt / 1e6; Local buf = Undefined(env()->isolate()); if (payload != nullptr) { @@ -2861,7 +3098,7 @@ void Initialize(Local target, Isolate* isolate = env->isolate(); HandleScope scope(isolate); - std::unique_ptr state(new http2_state(isolate)); + std::unique_ptr state(new Http2State(isolate)); #define SET_STATE_TYPEDARRAY(name, field) \ target->Set(context, \ @@ -2921,6 +3158,7 @@ void Initialize(Local target, env->SetProtoMethod(stream, "priority", Http2Stream::Priority); env->SetProtoMethod(stream, "pushPromise", Http2Stream::PushPromise); env->SetProtoMethod(stream, "info", Http2Stream::Info); + env->SetProtoMethod(stream, "trailers", Http2Stream::Trailers); env->SetProtoMethod(stream, "respondFD", Http2Stream::RespondFD); env->SetProtoMethod(stream, "respond", Http2Stream::Respond); env->SetProtoMethod(stream, "rstStream", Http2Stream::RstStream); @@ -2939,6 +3177,7 @@ void Initialize(Local target, session->SetClassName(http2SessionClassName); session->InstanceTemplate()->SetInternalFieldCount(1); AsyncWrap::AddWrapMethods(env, session); + env->SetProtoMethod(session, "origin", Http2Session::Origin); env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc); env->SetProtoMethod(session, "ping", Http2Session::Ping); env->SetProtoMethod(session, "consume", Http2Session::Consume); diff --git a/src/node_http2.h b/src/node_http2.h index f8bca65594a20a..ca0dd55f97f674 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -9,6 +9,7 @@ #include "stream_base-inl.h" #include "string_bytes.h" +#include #include namespace node { @@ -376,64 +377,13 @@ enum padding_strategy_type { PADDING_STRATEGY_CALLBACK }; -// These are the error codes provided by the underlying nghttp2 implementation. -#define NGHTTP2_ERROR_CODES(V) \ - V(NGHTTP2_ERR_INVALID_ARGUMENT) \ - V(NGHTTP2_ERR_BUFFER_ERROR) \ - V(NGHTTP2_ERR_UNSUPPORTED_VERSION) \ - V(NGHTTP2_ERR_WOULDBLOCK) \ - V(NGHTTP2_ERR_PROTO) \ - V(NGHTTP2_ERR_INVALID_FRAME) \ - V(NGHTTP2_ERR_EOF) \ - V(NGHTTP2_ERR_DEFERRED) \ - V(NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE) \ - V(NGHTTP2_ERR_STREAM_CLOSED) \ - V(NGHTTP2_ERR_STREAM_CLOSING) \ - V(NGHTTP2_ERR_STREAM_SHUT_WR) \ - V(NGHTTP2_ERR_INVALID_STREAM_ID) \ - V(NGHTTP2_ERR_INVALID_STREAM_STATE) \ - V(NGHTTP2_ERR_DEFERRED_DATA_EXIST) \ - V(NGHTTP2_ERR_START_STREAM_NOT_ALLOWED) \ - V(NGHTTP2_ERR_GOAWAY_ALREADY_SENT) \ - V(NGHTTP2_ERR_INVALID_HEADER_BLOCK) \ - V(NGHTTP2_ERR_INVALID_STATE) \ - V(NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) \ - V(NGHTTP2_ERR_FRAME_SIZE_ERROR) \ - V(NGHTTP2_ERR_HEADER_COMP) \ - V(NGHTTP2_ERR_FLOW_CONTROL) \ - V(NGHTTP2_ERR_INSUFF_BUFSIZE) \ - V(NGHTTP2_ERR_PAUSE) \ - V(NGHTTP2_ERR_TOO_MANY_INFLIGHT_SETTINGS) \ - V(NGHTTP2_ERR_PUSH_DISABLED) \ - V(NGHTTP2_ERR_DATA_EXIST) \ - V(NGHTTP2_ERR_SESSION_CLOSING) \ - V(NGHTTP2_ERR_HTTP_HEADER) \ - V(NGHTTP2_ERR_HTTP_MESSAGING) \ - V(NGHTTP2_ERR_REFUSED_STREAM) \ - V(NGHTTP2_ERR_INTERNAL) \ - V(NGHTTP2_ERR_CANCEL) \ - V(NGHTTP2_ERR_FATAL) \ - V(NGHTTP2_ERR_NOMEM) \ - V(NGHTTP2_ERR_CALLBACK_FAILURE) \ - V(NGHTTP2_ERR_BAD_CLIENT_MAGIC) \ - V(NGHTTP2_ERR_FLOODED) - -const char* nghttp2_errname(int rv) { - switch (rv) { -#define V(code) case code: return #code; - NGHTTP2_ERROR_CODES(V) -#undef V - default: - return "NGHTTP2_UNKNOWN_ERROR"; - } -} - enum session_state_flags { SESSION_STATE_NONE = 0x0, SESSION_STATE_HAS_SCOPE = 0x1, SESSION_STATE_WRITE_SCHEDULED = 0x2, SESSION_STATE_CLOSED = 0x4, - SESSION_STATE_SENDING = 0x8, + SESSION_STATE_CLOSING = 0x8, + SESSION_STATE_SENDING = 0x10, }; // This allows for 4 default-sized frames with their frame headers @@ -465,7 +415,7 @@ class Http2Scope { // configured. class Http2Options { public: - explicit Http2Options(Environment* env); + Http2Options(Environment* env, nghttp2_session_type type); ~Http2Options() { nghttp2_option_del(options_); @@ -581,6 +531,10 @@ class Http2Stream : public AsyncWrap, // Submit informational headers for this stream inline int SubmitInfo(nghttp2_nv* nva, size_t len); + // Submit trailing headers for this stream + int SubmitTrailers(nghttp2_nv* nva, size_t len); + void OnTrailers(); + // Submit a PRIORITY frame for this stream inline int SubmitPriority(nghttp2_priority_spec* prispec, bool silent = false); @@ -588,6 +542,8 @@ class Http2Stream : public AsyncWrap, // Submits an RST_STREAM frame using the given code inline void SubmitRstStream(const uint32_t code); + void FlushRstStream(); + // Submits a PUSH_PROMISE frame with this stream as the parent. inline Http2Stream* SubmitPushPromise( nghttp2_nv* nva, @@ -618,7 +574,7 @@ class Http2Stream : public AsyncWrap, inline bool IsClosed() const { return flags_ & NGHTTP2_STREAM_FLAG_CLOSED; - } + } inline bool HasTrailers() const { return flags_ & NGHTTP2_STREAM_FLAG_TRAILERS; @@ -645,18 +601,14 @@ class Http2Stream : public AsyncWrap, nghttp2_rcbuf* value, uint8_t flags); - inline nghttp2_header* headers() { - return current_headers_.data(); + inline std::vector move_headers() { + return std::move(current_headers_); } inline nghttp2_headers_category headers_category() const { return current_headers_category_; } - inline size_t headers_count() const { - return current_headers_.size(); - } - void StartHeaders(nghttp2_headers_category category); // Required for StreamBase @@ -677,25 +629,6 @@ class Http2Stream : public AsyncWrap, size_t self_size() const override { return sizeof(*this); } - // Handling Trailer Headers - class SubmitTrailers { - public: - inline void Submit(nghttp2_nv* trailers, size_t length) const; - - inline SubmitTrailers(Http2Session* sesion, - Http2Stream* stream, - uint32_t* flags); - - private: - Http2Session* const session_; - Http2Stream* const stream_; - uint32_t* const flags_; - - friend class Http2Stream; - }; - - void OnTrailers(const SubmitTrailers& submit_trailers); - // JavaScript API static void GetID(const FunctionCallbackInfo& args); static void Destroy(const FunctionCallbackInfo& args); @@ -705,6 +638,7 @@ class Http2Stream : public AsyncWrap, static void RefreshState(const FunctionCallbackInfo& args); static void Info(const FunctionCallbackInfo& args); static void RespondFD(const FunctionCallbackInfo& args); + static void Trailers(const FunctionCallbackInfo& args); static void Respond(const FunctionCallbackInfo& args); static void RstStream(const FunctionCallbackInfo& args); @@ -811,6 +745,7 @@ class Http2Session : public AsyncWrap { class Http2Ping; class Http2Settings; + class MemoryAllocatorInfo; inline void EmitStatistics(); @@ -826,10 +761,12 @@ class Http2Session : public AsyncWrap { size_t origin_len, uint8_t* value, size_t value_len); + void Origin(nghttp2_origin_entry* ov, size_t count); + bool Ping(v8::Local function); - inline void SendPendingData(); + inline uint8_t SendPendingData(); // Submits a new request. If the request is a success, assigned // will be a pointer to the Http2Stream instance assigned. @@ -858,6 +795,9 @@ class Http2Session : public AsyncWrap { // Schedule a write if nghttp2 indicates it wants to write to the socket. void MaybeScheduleWrite(); + // Stop reading if nghttp2 doesn't want to anymore. + void MaybeStopReading(); + // Returns pointer to the stream, or nullptr if stream does not exist inline Http2Stream* FindStream(int32_t id); @@ -883,7 +823,16 @@ class Http2Session : public AsyncWrap { return stream_buf_; } - inline void GetTrailers(Http2Stream* stream, uint32_t* flags); + // Schedule an RstStream for after the current write finishes. + inline void AddPendingRstStream(int32_t stream_id) { + pending_rst_streams_.emplace_back(stream_id); + } + + inline bool HasPendingRstStream(int32_t stream_id) { + return pending_rst_streams_.end() != std::find(pending_rst_streams_.begin(), + pending_rst_streams_.end(), + stream_id); + } static void OnStreamAllocImpl(size_t suggested_size, uv_buf_t* buf, @@ -909,6 +858,7 @@ class Http2Session : public AsyncWrap { static void RefreshState(const FunctionCallbackInfo& args); static void Ping(const FunctionCallbackInfo& args); static void AltSvc(const FunctionCallbackInfo& args); + static void Origin(const FunctionCallbackInfo& args); template static void RefreshSettings(const FunctionCallbackInfo& args); @@ -936,13 +886,15 @@ class Http2Session : public AsyncWrap { 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. + // Tell our custom memory allocator that this rcbuf is independent of + // this session now, and may outlive it. + void StopTrackingRcbuf(nghttp2_rcbuf* buf); + + // Returns the current session memory including memory allocated by nghttp2, + // 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 += current_nghttp2_memory_; total += outgoing_storage_.size(); return total; } @@ -984,6 +936,7 @@ class Http2Session : public AsyncWrap { inline void HandleSettingsFrame(const nghttp2_frame* frame); inline void HandlePingFrame(const nghttp2_frame* frame); inline void HandleAltSvcFrame(const nghttp2_frame* frame); + inline void HandleOriginFrame(const nghttp2_frame* frame); // nghttp2 callbacks static inline int OnBeginHeadersCallback( @@ -1074,6 +1027,8 @@ class Http2Session : public AsyncWrap { // 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 amount of memory allocated by nghttp2 internals + uint64_t current_nghttp2_memory_ = 0; // The collection of active Http2Streams associated with this session std::unordered_map streams_; @@ -1101,6 +1056,7 @@ class Http2Session : public AsyncWrap { std::vector outgoing_buffers_; std::vector outgoing_storage_; + std::vector pending_rst_streams_; void CopyDataIntoOutgoing(const uint8_t* src, size_t src_length); void ClearOutgoing(int status); @@ -1275,7 +1231,8 @@ class ExternalHeader : } template - static MaybeLocal New(Environment* env, nghttp2_rcbuf* buf) { + static MaybeLocal New(Http2Session* session, nghttp2_rcbuf* buf) { + Environment* env = session->env(); if (nghttp2_rcbuf_is_static(buf)) { auto& static_str_map = env->isolate_data()->http2_static_strs; v8::Eternal& eternal = static_str_map[buf]; @@ -1296,11 +1253,13 @@ class ExternalHeader : } if (may_internalize && vec.len < 64) { + nghttp2_rcbuf_decref(buf); // This is a short header name, so there is a good chance V8 already has // it internalized. return GetInternalizedString(env, vec); } + session->StopTrackingRcbuf(buf); ExternalHeader* h_str = new ExternalHeader(buf); MaybeLocal str = String::NewExternalOneByte(env->isolate(), h_str); if (str.IsEmpty()) @@ -1332,6 +1291,26 @@ class Headers { MaybeStackBuffer buf_; }; +class Origins { + public: + Origins(Local context, + Local origin_string, + size_t origin_count); + ~Origins() {} + + nghttp2_origin_entry* operator*() { + return reinterpret_cast(*buf_); + } + + size_t length() const { + return count_; + } + + private: + size_t count_; + MaybeStackBuffer buf_; +}; + } // namespace http2 } // namespace node diff --git a/src/node_http2_state.h b/src/node_http2_state.h index ed88f068a04b16..64a0942f7ffa67 100644 --- a/src/node_http2_state.h +++ b/src/node_http2_state.h @@ -84,9 +84,9 @@ namespace http2 { IDX_SESSION_STATS_COUNT }; -class http2_state { +class Http2State { public: - explicit http2_state(v8::Isolate* isolate) : + explicit Http2State(v8::Isolate* isolate) : root_buffer( isolate, sizeof(http2_state_internal)), diff --git a/src/util-inl.h b/src/util-inl.h index 56c94148d6e820..b24baece7a5561 100644 --- a/src/util-inl.h +++ b/src/util-inl.h @@ -345,8 +345,9 @@ bool StringEqualNoCaseN(const char* a, const char* b, size_t length) { return true; } -inline size_t MultiplyWithOverflowCheck(size_t a, size_t b) { - size_t ret = a * b; +template +inline T MultiplyWithOverflowCheck(T a, T b) { + auto ret = a * b; if (a != 0) CHECK_EQ(b, ret / a); diff --git a/src/util.h b/src/util.h index 1cf515bb40fe3f..bf5e515083d447 100644 --- a/src/util.h +++ b/src/util.h @@ -65,6 +65,9 @@ inline char* Calloc(size_t n); inline char* UncheckedMalloc(size_t n); inline char* UncheckedCalloc(size_t n); +template +inline T MultiplyWithOverflowCheck(T a, T b); + // Used by the allocation functions when allocation fails. // Thin wrapper around v8::Isolate::LowMemoryNotification() that checks // whether V8 is initialized. diff --git a/test/common/index.js b/test/common/index.js index 8efd72fc7d162a..2ff682e95d1347 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -691,6 +691,11 @@ exports.expectsError = function expectsError(fn, settings, exact) { fn = undefined; } function innerFn(error) { + if (arguments.length !== 1) { + // Do not use `assert.strictEqual()` to prevent `util.inspect` from + // always being called. + assert.fail(`Expected one argument, got ${util.inspect(arguments)}`); + } assert.strictEqual(error.code, settings.code); const descriptor = Object.getOwnPropertyDescriptor(error, 'message'); assert.strictEqual(descriptor.enumerable, diff --git a/test/fixtures/person-large.jpg b/test/fixtures/person-large.jpg new file mode 100644 index 00000000000000..3d0d0af42375c3 Binary files /dev/null and b/test/fixtures/person-large.jpg differ diff --git a/test/parallel/test-http-aborted.js b/test/parallel/test-http-aborted.js new file mode 100644 index 00000000000000..c3d7e4641f4501 --- /dev/null +++ b/test/parallel/test-http-aborted.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); + +const server = http.createServer(common.mustCall(function(req, res) { + req.on('aborted', common.mustCall(function() { + assert.strictEqual(this.aborted, true); + server.close(); + })); + assert.strictEqual(req.aborted, false); + res.write('hello'); +})); + +server.listen(0, common.mustCall(() => { + const req = http.get({ + port: server.address().port, + headers: { connection: 'keep-alive' } + }, common.mustCall((res) => { + res.on('aborted', common.mustCall(() => { + assert.strictEqual(res.aborted, true); + })); + req.abort(); + })); +})); diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js index eab413e2327d8f..43fc6819e21f7a 100644 --- a/test/parallel/test-http2-client-destroy.js +++ b/test/parallel/test-http2-client-destroy.js @@ -109,9 +109,6 @@ const Countdown = require('../common/countdown'); server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - // On some platforms (e.g. windows), an ECONNRESET may occur at this - // point -- or it may not. Do not make this a mustCall - client.on('error', () => {}); client.on('close', () => { server.close(); @@ -119,9 +116,24 @@ const Countdown = require('../common/countdown'); client.destroy(); }); + client.request(); + })); +} + +// test destroy before connect +{ + const server = h2.createServer(); + server.on('stream', common.mustNotCall()); + + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + + server.on('connection', common.mustCall(() => { + server.close(); + client.close(); + })); + const req = client.request(); - // On some platforms (e.g. windows), an ECONNRESET may occur at this - // point -- or it may not. Do not make this a mustCall - req.on('error', () => {}); + req.destroy(); })); } diff --git a/test/parallel/test-http2-client-request-options-errors.js b/test/parallel/test-http2-client-request-options-errors.js index 3ad808cb1fbe23..d170443f72848e 100644 --- a/test/parallel/test-http2-client-request-options-errors.js +++ b/test/parallel/test-http2-client-request-options-errors.js @@ -10,7 +10,6 @@ const http2 = require('http2'); const optionsToTest = { endStream: 'boolean', - getTrailers: 'function', weight: 'number', parent: 'number', exclusive: 'boolean', diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js index 72374611b49349..59cc7f104c17fc 100644 --- a/test/parallel/test-http2-client-rststream-before-connect.js +++ b/test/parallel/test-http2-client-rststream-before-connect.js @@ -62,8 +62,14 @@ server.listen(0, common.mustCall(() => { message: 'Stream closed with error code NGHTTP2_PROTOCOL_ERROR' })); - req.on('response', common.mustCall()); - req.resume(); + // The `response` event should not fire as the server should receive the + // RST_STREAM frame before it ever has a chance to reply. + req.on('response', common.mustNotCall()); + + // The `end` event should still fire as we close the readable stream by + // pushing a `null` chunk. req.on('end', common.mustCall()); + + req.resume(); req.end(); })); diff --git a/test/parallel/test-http2-client-upload-reject.js b/test/parallel/test-http2-client-upload-reject.js new file mode 100644 index 00000000000000..678114130e3dba --- /dev/null +++ b/test/parallel/test-http2-client-upload-reject.js @@ -0,0 +1,51 @@ +'use strict'; + +// Verifies that uploading data from a client works + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +const loc = fixtures.path('person-large.jpg'); + +assert(fs.existsSync(loc)); + +fs.readFile(loc, common.mustCall((err, data) => { + assert.ifError(err); + + const server = http2.createServer(); + + server.on('stream', common.mustCall((stream) => { + // Wait for some data to come through. + setImmediate(() => { + stream.on('close', common.mustCall(() => { + assert.strictEqual(stream.rstCode, 0); + })); + + stream.respond({ ':status': 400 }); + stream.end(); + }); + })); + + server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 400); + })); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); + + const str = fs.createReadStream(loc); + str.pipe(req); + })); +})); diff --git a/test/parallel/test-http2-client-upload.js b/test/parallel/test-http2-client-upload.js index 70a8ff3ced01c6..78c6d47cbb4f44 100644 --- a/test/parallel/test-http2-client-upload.js +++ b/test/parallel/test-http2-client-upload.js @@ -11,7 +11,7 @@ const fs = require('fs'); const fixtures = require('../common/fixtures'); const Countdown = require('../common/countdown'); -const loc = fixtures.path('person.jpg'); +const loc = fixtures.path('person-large.jpg'); let fileData; assert(fs.existsSync(loc)); diff --git a/test/parallel/test-http2-compat-aborted.js b/test/parallel/test-http2-compat-aborted.js new file mode 100644 index 00000000000000..01caf95f98688a --- /dev/null +++ b/test/parallel/test-http2-compat-aborted.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const h2 = require('http2'); +const assert = require('assert'); + + +const server = h2.createServer(common.mustCall(function(req, res) { + req.on('aborted', common.mustCall(function() { + assert.strictEqual(this.aborted, true); + })); + assert.strictEqual(req.aborted, false); + res.write('hello'); + server.close(); +})); + +server.listen(0, common.mustCall(function() { + const url = `http://localhost:${server.address().port}`; + const client = h2.connect(url, common.mustCall(() => { + const request = client.request(); + request.on('data', common.mustCall((chunk) => { + client.destroy(); + })); + })); +})); diff --git a/test/parallel/test-http2-compat-client-upload-reject.js b/test/parallel/test-http2-compat-client-upload-reject.js new file mode 100644 index 00000000000000..e6a187cb12b264 --- /dev/null +++ b/test/parallel/test-http2-compat-client-upload-reject.js @@ -0,0 +1,44 @@ +'use strict'; + +// Verifies that uploading data from a client works + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +const loc = fixtures.path('person-large.jpg'); + +assert(fs.existsSync(loc)); + +fs.readFile(loc, common.mustCall((err, data) => { + assert.ifError(err); + + const server = http2.createServer(common.mustCall((req, res) => { + setImmediate(() => { + res.writeHead(400); + res.end(); + }); + })); + + server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 400); + })); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); + + const str = fs.createReadStream(loc); + str.pipe(req); + })); +})); diff --git a/test/parallel/test-http2-compat-serverrequest-settimeout.js b/test/parallel/test-http2-compat-serverrequest-settimeout.js index f7189161802301..668ff891ef5555 100644 --- a/test/parallel/test-http2-compat-serverrequest-settimeout.js +++ b/test/parallel/test-http2-compat-serverrequest-settimeout.js @@ -6,13 +6,15 @@ if (!common.hasCrypto) const assert = require('assert'); const http2 = require('http2'); -const msecs = common.platformTimeout(1); +// Set the timeout to 10ms since ending the response stream resets the timer. +const msecs = common.platformTimeout(10); const server = http2.createServer(); server.on('request', (req, res) => { req.setTimeout(msecs, common.mustCall(() => { res.end(); })); + res.on('timeout', common.mustCall()); res.on('finish', common.mustCall(() => { req.setTimeout(msecs, common.mustNotCall()); process.nextTick(() => { diff --git a/test/parallel/test-http2-compat-serverrequest-trailers.js b/test/parallel/test-http2-compat-serverrequest-trailers.js index 285178cab66816..735ff1345e1a39 100644 --- a/test/parallel/test-http2-compat-serverrequest-trailers.js +++ b/test/parallel/test-http2-compat-serverrequest-trailers.js @@ -50,15 +50,18 @@ server.listen(0, common.mustCall(function() { ':scheme': 'http', ':authority': `localhost:${port}` }; - const request = client.request(headers, { - getTrailers(trailers) { - trailers['x-fOo'] = 'xOxOxOx'; - trailers['x-foO'] = 'OxOxOxO'; - trailers['X-fOo'] = 'xOxOxOx'; - trailers['X-foO'] = 'OxOxOxO'; - trailers['x-foo-test'] = 'test, test'; - } + const request = client.request(headers, { waitForTrailers: true }); + + request.on('wantTrailers', () => { + request.sendTrailers({ + 'x-fOo': 'xOxOxOx', + 'x-foO': 'OxOxOxO', + 'X-fOo': 'xOxOxOx', + 'X-foO': 'OxOxOxO', + 'x-foo-test': 'test, test' + }); }); + request.resume(); request.on('end', common.mustCall(function() { server.close(); diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js index 1b9aa66808eeff..1304de2b89a8ff 100755 --- a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js +++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js @@ -28,6 +28,15 @@ const server = h2.createServer((request, response) => { } ); + response.stream.on('close', () => { + response.createPushResponse({ + ':path': '/pushed', + ':method': 'GET' + }, common.mustCall((error) => { + assert.strictEqual(error.code, 'ERR_HTTP2_INVALID_STREAM'); + })); + }); + response.createPushResponse({ ':path': '/pushed', ':method': 'GET' @@ -36,16 +45,6 @@ const server = h2.createServer((request, response) => { assert.strictEqual(push.stream.id % 2, 0); push.end(pushExpect); response.end(); - - // wait for a tick, so the stream is actually closed - setImmediate(function() { - response.createPushResponse({ - ':path': '/pushed', - ':method': 'GET' - }, common.mustCall((error) => { - assert.strictEqual(error.code, 'ERR_HTTP2_INVALID_STREAM'); - })); - }); })); }); diff --git a/test/parallel/test-http2-compat-serverresponse-destroy.js b/test/parallel/test-http2-compat-serverresponse-destroy.js index b528a64f79b20d..bb28b2b352fac3 100644 --- a/test/parallel/test-http2-compat-serverresponse-destroy.js +++ b/test/parallel/test-http2-compat-serverresponse-destroy.js @@ -8,8 +8,7 @@ const http2 = require('http2'); const Countdown = require('../common/countdown'); // Check that destroying the Http2ServerResponse stream produces -// the expected result, including the ability to throw an error -// which is emitted on server.streamError +// the expected result. const errors = [ 'test-error', diff --git a/test/parallel/test-http2-compat-serverresponse-end-after-statuses-without-body.js b/test/parallel/test-http2-compat-serverresponse-end-after-statuses-without-body.js new file mode 100644 index 00000000000000..83d5521bf2473f --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-end-after-statuses-without-body.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const h2 = require('http2'); + +// This test case ensures that calling of res.end after sending +// 204, 205 and 304 HTTP statuses will not cause an error +// See issue: https://github.com/nodejs/node/issues/21740 + +const { + HTTP_STATUS_NO_CONTENT, + HTTP_STATUS_RESET_CONTENT, + HTTP_STATUS_NOT_MODIFIED +} = h2.constants; + +const statusWithouBody = [ + HTTP_STATUS_NO_CONTENT, + HTTP_STATUS_RESET_CONTENT, + HTTP_STATUS_NOT_MODIFIED, +]; +const STATUS_CODES_COUNT = statusWithouBody.length; + +const server = h2.createServer(common.mustCall(function(req, res) { + res.writeHead(statusWithouBody.pop()); + res.end(); +}, STATUS_CODES_COUNT)); + +server.listen(0, common.mustCall(function() { + const url = `http://localhost:${server.address().port}`; + const client = h2.connect(url, common.mustCall(() => { + let responseCount = 0; + const closeAfterResponse = () => { + if (STATUS_CODES_COUNT === ++responseCount) { + client.destroy(); + server.close(); + } + }; + + for (let i = 0; i < STATUS_CODES_COUNT; i++) { + const request = client.request(); + request.on('response', common.mustCall(closeAfterResponse)); + } + + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-finished.js b/test/parallel/test-http2-compat-serverresponse-finished.js index ceaa6eb5c3cf2c..4da592a5b354b0 100644 --- a/test/parallel/test-http2-compat-serverresponse-finished.js +++ b/test/parallel/test-http2-compat-serverresponse-finished.js @@ -9,14 +9,14 @@ const net = require('net'); // Http2ServerResponse.finished const server = h2.createServer(); -server.listen(0, common.mustCall(function() { +server.listen(0, common.mustCall(() => { const port = server.address().port; - server.once('request', common.mustCall(function(request, response) { + server.once('request', common.mustCall((request, response) => { assert.ok(response.socket instanceof net.Socket); assert.ok(response.connection instanceof net.Socket); assert.strictEqual(response.socket, response.connection); - response.on('finish', common.mustCall(function() { + response.on('finish', common.mustCall(() => { assert.strictEqual(response.socket, undefined); assert.strictEqual(response.connection, undefined); process.nextTick(common.mustCall(() => { @@ -30,7 +30,7 @@ server.listen(0, common.mustCall(function() { })); const url = `http://localhost:${port}`; - const client = h2.connect(url, common.mustCall(function() { + const client = h2.connect(url, common.mustCall(() => { const headers = { ':path': '/', ':method': 'GET', @@ -38,7 +38,7 @@ server.listen(0, common.mustCall(function() { ':authority': `localhost:${port}` }; const request = client.request(headers); - request.on('end', common.mustCall(function() { + request.on('end', common.mustCall(() => { client.close(); })); request.end(); diff --git a/test/parallel/test-http2-compat-serverresponse-settimeout.js b/test/parallel/test-http2-compat-serverresponse-settimeout.js index bb09633727ccf7..03cef357056069 100644 --- a/test/parallel/test-http2-compat-serverresponse-settimeout.js +++ b/test/parallel/test-http2-compat-serverresponse-settimeout.js @@ -6,13 +6,15 @@ if (!common.hasCrypto) const assert = require('assert'); const http2 = require('http2'); -const msecs = common.platformTimeout(1); +// Set the timeout to 10ms since ending the response stream resets the timer. +const msecs = common.platformTimeout(10); const server = http2.createServer(); server.on('request', (req, res) => { res.setTimeout(msecs, common.mustCall(() => { res.end(); })); + res.on('timeout', common.mustCall()); res.on('finish', common.mustCall(() => { res.setTimeout(msecs, common.mustNotCall()); process.nextTick(() => { diff --git a/test/parallel/test-http2-compat-socket-destroy-delayed.js b/test/parallel/test-http2-compat-socket-destroy-delayed.js new file mode 100644 index 00000000000000..62405047d8266e --- /dev/null +++ b/test/parallel/test-http2-compat-socket-destroy-delayed.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +const { mustCall } = common; + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); +const assert = require('assert'); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_METHOD, +} = http2.constants; + +// This tests verifies that calling `req.socket.destroy()` via +// setImmediate does not crash. +// Fixes https://github.com/nodejs/node/issues/22855. + +const app = http2.createServer(mustCall((req, res) => { + res.end('hello'); + setImmediate(() => req.socket.destroy()); +})); + +app.listen(0, mustCall(() => { + const session = http2.connect(`http://localhost:${app.address().port}`); + const request = session.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: 'get' + }); + request.once('response', mustCall((headers, flags) => { + let data = ''; + request.on('data', (chunk) => { data += chunk; }); + request.on('end', mustCall(() => { + assert.strictEqual(data, 'hello'); + session.close(); + app.close(); + })); + })); + request.end(); +})); diff --git a/test/parallel/test-http2-cookies.js b/test/parallel/test-http2-cookies.js index cf763915389287..25113eaba8eb18 100644 --- a/test/parallel/test-http2-cookies.js +++ b/test/parallel/test-http2-cookies.js @@ -48,8 +48,7 @@ server.on('listening', common.mustCall(() => { req.on('response', common.mustCall((headers) => { assert(Array.isArray(headers['set-cookie'])); - assert.deepStrictEqual(headers['set-cookie'], setCookie, - 'set-cookie header does not match'); + assert.deepStrictEqual(headers['set-cookie'], setCookie); })); req.on('end', common.mustCall(() => { diff --git a/test/parallel/test-http2-create-client-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js index 1f20ec8e42a871..8b2aa1c168cb5e 100644 --- a/test/parallel/test-http2-create-client-secure-session.js +++ b/test/parallel/test-http2-create-client-secure-session.js @@ -21,7 +21,7 @@ function onStream(stream, headers) { const socket = stream.session[kSocket]; assert(stream.session.encrypted); - assert(stream.session.alpnProtocol, 'h2'); + assert.strictEqual(stream.session.alpnProtocol, 'h2'); const originSet = stream.session.originSet; assert(Array.isArray(originSet)); assert.strictEqual(originSet[0], diff --git a/test/parallel/test-http2-create-client-session.js b/test/parallel/test-http2-create-client-session.js index 34e4e975d92d81..35de927ea7924e 100644 --- a/test/parallel/test-http2-create-client-session.js +++ b/test/parallel/test-http2-create-client-session.js @@ -55,9 +55,8 @@ server.on('listening', common.mustCall(() => { const req = client.request(); req.on('response', common.mustCall(function(headers) { - assert.strictEqual(headers[':status'], 200, 'status code is set'); - assert.strictEqual(headers['content-type'], 'text/html', - 'content type is set'); + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); assert(headers.date); })); diff --git a/test/parallel/test-http2-endafterheaders.js b/test/parallel/test-http2-endafterheaders.js new file mode 100644 index 00000000000000..429ffc3188452d --- /dev/null +++ b/test/parallel/test-http2-endafterheaders.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const Countdown = require('../common/countdown'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream, headers) => { + const check = headers[':method'] === 'GET' ? true : false; + assert.strictEqual(stream.endAfterHeaders, check); + stream.on('data', common.mustNotCall()); + stream.on('end', common.mustCall()); + stream.respond(); + stream.end('ok'); +}, 2)); + +const countdown = new Countdown(2, () => server.close()); + +server.listen(0, common.mustCall(() => { + { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.resume(); + req.on('response', common.mustCall(() => { + assert.strictEqual(req.endAfterHeaders, false); + })); + req.on('end', common.mustCall(() => { + client.close(); + countdown.dec(); + })); + } + { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':method': 'POST' }); + + req.resume(); + req.end(); + req.on('response', common.mustCall(() => { + assert.strictEqual(req.endAfterHeaders, false); + })); + req.on('end', common.mustCall(() => { + client.close(); + countdown.dec(); + })); + } +})); diff --git a/test/parallel/test-http2-large-write-close.js b/test/parallel/test-http2-large-write-close.js new file mode 100644 index 00000000000000..f9dee357d6da7b --- /dev/null +++ b/test/parallel/test-http2-large-write-close.js @@ -0,0 +1,44 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const http2 = require('http2'); + +const content = Buffer.alloc(1e5, 0x44); + +const server = http2.createSecureServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}); +server.on('stream', common.mustCall((stream) => { + stream.respond({ + 'Content-Type': 'application/octet-stream', + 'Content-Length': (content.length.toString() * 2), + 'Vary': 'Accept-Encoding' + }); + + stream.write(content); + stream.write(content); + stream.end(); + stream.close(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`https://localhost:${server.address().port}`, + { rejectUnauthorized: false }); + + const req = client.request({ ':path': '/' }); + req.end(); + + let receivedBufferLength = 0; + req.on('data', common.mustCallAtLeast((buf) => { + receivedBufferLength += buf.length; + }, 1)); + req.on('close', common.mustCall(() => { + assert.strictEqual(receivedBufferLength, content.length * 2); + client.close(); + server.close(); + })); +})); diff --git a/test/parallel/test-http2-large-write-destroy.js b/test/parallel/test-http2-large-write-destroy.js new file mode 100644 index 00000000000000..24c0a055cc943f --- /dev/null +++ b/test/parallel/test-http2-large-write-destroy.js @@ -0,0 +1,40 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); +const http2 = require('http2'); + +// This test will result in a crash due to a missed CHECK in C++ or +// a straight-up segfault if the C++ doesn't send RST_STREAM through +// properly when calling destroy. + +const content = Buffer.alloc(60000, 0x44); + +const server = http2.createSecureServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}); +server.on('stream', common.mustCall((stream) => { + stream.respond({ + 'Content-Type': 'application/octet-stream', + 'Content-Length': (content.length.toString() * 2), + 'Vary': 'Accept-Encoding' + }, { waitForTrailers: true }); + + stream.write(content); + stream.destroy(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`https://localhost:${server.address().port}`, + { rejectUnauthorized: false }); + + const req = client.request({ ':path': '/' }); + req.end(); + + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + })); +})); diff --git a/test/parallel/test-http2-misbehaving-flow-control-paused.js b/test/parallel/test-http2-misbehaving-flow-control-paused.js index 60a2cdabf847d9..26d2ed5dd244a2 100644 --- a/test/parallel/test-http2-misbehaving-flow-control-paused.js +++ b/test/parallel/test-http2-misbehaving-flow-control-paused.js @@ -70,8 +70,6 @@ server.on('stream', (stream) => { client.destroy(); })); stream.on('end', common.mustNotCall()); - stream.respond(); - stream.end('ok'); }); server.listen(0, () => { diff --git a/test/parallel/test-http2-misused-pseudoheaders.js b/test/parallel/test-http2-misused-pseudoheaders.js index 16b3fcd6a48552..2b4ea4a0f011d7 100644 --- a/test/parallel/test-http2-misused-pseudoheaders.js +++ b/test/parallel/test-http2-misused-pseudoheaders.js @@ -21,16 +21,17 @@ server.on('stream', common.mustCall((stream) => { })); }); - stream.respond({}, { - getTrailers: common.mustCall((trailers) => { - trailers[':status'] = 'bar'; - }) + stream.respond({}, { waitForTrailers: true }); + + stream.on('wantTrailers', () => { + common.expectsError(() => { + stream.sendTrailers({ ':status': 'bar' }); + }, { + code: 'ERR_HTTP2_INVALID_PSEUDOHEADER' + }); + stream.close(); }); - stream.on('error', common.expectsError({ - code: 'ERR_HTTP2_INVALID_PSEUDOHEADER' - })); - stream.end('hello world'); })); @@ -39,12 +40,6 @@ server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); const req = client.request(); - req.on('error', common.expectsError({ - code: 'ERR_HTTP2_STREAM_ERROR', - type: Error, - message: 'Stream closed with error code NGHTTP2_INTERNAL_ERROR' - })); - req.on('response', common.mustCall()); req.resume(); req.on('end', common.mustCall()); diff --git a/test/parallel/test-http2-no-wanttrailers-listener.js b/test/parallel/test-http2-no-wanttrailers-listener.js new file mode 100644 index 00000000000000..87bc21df48aa2c --- /dev/null +++ b/test/parallel/test-http2-no-wanttrailers-listener.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond(undefined, { waitForTrailers: true }); + // There is no wantTrailers handler so this should close naturally + // without hanging. If the test completes without timing out, then + // it passes. + stream.end('ok'); +} + +server.listen(0); + +server.on('listening', common.mustCall(function() { + const client = h2.connect(`http://localhost:${this.address().port}`); + const req = client.request(); + req.resume(); + req.on('trailers', common.mustNotCall()); + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/parallel/test-http2-onping.js b/test/parallel/test-http2-onping.js new file mode 100644 index 00000000000000..134a94ddb8f582 --- /dev/null +++ b/test/parallel/test-http2-onping.js @@ -0,0 +1,48 @@ +'use strict'; + +const { + hasCrypto, + mustCall, + skip +} = require('../common'); +if (!hasCrypto) + skip('missing crypto'); + +const { + deepStrictEqual +} = require('assert'); +const { + createServer, + connect +} = require('http2'); + +const check = Buffer.from([ 1, 2, 3, 4, 5, 6, 7, 8 ]); + +const server = createServer(); +server.on('stream', mustCall((stream) => { + stream.respond(); + stream.end('ok'); +})); +server.on('session', mustCall((session) => { + session.on('ping', mustCall((payload) => { + deepStrictEqual(check, payload); + })); + session.ping(check, mustCall()); +})); +server.listen(0, mustCall(() => { + const client = connect(`http://localhost:${server.address().port}`); + + client.on('ping', mustCall((payload) => { + deepStrictEqual(check, payload); + })); + client.on('connect', mustCall(() => { + client.ping(check, mustCall()); + })); + + const req = client.request(); + req.resume(); + req.on('close', mustCall(() => { + client.close(); + server.close(); + })); +})); diff --git a/test/parallel/test-http2-origin.js b/test/parallel/test-http2-origin.js new file mode 100644 index 00000000000000..385d3827fc3bbf --- /dev/null +++ b/test/parallel/test-http2-origin.js @@ -0,0 +1,184 @@ +'use strict'; + +const { + hasCrypto, + mustCall, + mustNotCall, + skip +} = require('../common'); +if (!hasCrypto) + skip('missing crypto'); + +const { + deepStrictEqual, + strictEqual, + throws +} = require('assert'); +const { + createSecureServer, + createServer, + connect +} = require('http2'); +const { URL } = require('url'); +const Countdown = require('../common/countdown'); + +const { readKey } = require('../common/fixtures'); + +const key = readKey('agent8-key.pem', 'binary'); +const cert = readKey('agent8-cert.pem', 'binary'); +const ca = readKey('fake-startcom-root-cert.pem', 'binary'); + +const exceptionHasFields = ({ code, name }) => (err) => { + return err.code === code && err.name === name; +}; + +{ + const server = createSecureServer({ key, cert }); + server.on('stream', mustCall((stream) => { + stream.session.origin('https://example.org/a/b/c', + new URL('https://example.com')); + stream.respond(); + stream.end('ok'); + })); + server.on('session', mustCall((session) => { + session.origin('https://foo.org/a/b/c', new URL('https://bar.org')); + + // Won't error, but won't send anything + session.origin(); + + [0, true, {}, []].forEach((input) => { + throws( + () => session.origin(input), + exceptionHasFields({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'Error [ERR_INVALID_ARG_TYPE]' + }) + ); + }); + + [new URL('foo://bar'), 'foo://bar'].forEach((input) => { + throws( + () => session.origin(input), + exceptionHasFields({ + code: 'ERR_HTTP2_INVALID_ORIGIN', + name: 'Error [ERR_HTTP2_INVALID_ORIGIN]' + }) + ); + }); + + ['not a valid url'].forEach((input) => { + throws( + () => session.origin(input), + exceptionHasFields({ + code: 'ERR_INVALID_URL', + name: 'TypeError [ERR_INVALID_URL]' + }) + ); + }); + })); + + server.listen(0, mustCall(() => { + const originSet = [`https://localhost:${server.address().port}`]; + const client = connect(originSet[0], { ca }); + const checks = [ + ['https://foo.org', 'https://bar.org'], + ['https://example.org', 'https://example.com'] + ]; + + const countdown = new Countdown(2, () => { + client.close(); + server.close(); + }); + + client.on('origin', mustCall((origins) => { + const check = checks.shift(); + originSet.push(...check); + deepStrictEqual(originSet, client.originSet); + deepStrictEqual(origins, check); + countdown.dec(); + }, 2)); + + client.request().on('close', mustCall()).resume(); + })); +} + +// Test automatically sending origin on connection start +{ + const origins = [ 'https://foo.org/a/b/c', 'https://bar.org' ]; + const server = createSecureServer({ key, cert, origins }); + server.on('stream', mustCall((stream) => { + stream.respond(); + stream.end('ok'); + })); + + server.listen(0, mustCall(() => { + const check = ['https://foo.org', 'https://bar.org']; + const originSet = [`https://localhost:${server.address().port}`]; + const client = connect(originSet[0], { ca }); + + client.on('origin', mustCall((origins) => { + originSet.push(...check); + deepStrictEqual(originSet, client.originSet); + deepStrictEqual(origins, check); + client.close(); + server.close(); + })); + + client.request().on('close', mustCall()).resume(); + })); +} + +// If return status is 421, the request origin must be removed from the +// originSet +{ + const server = createSecureServer({ key, cert }); + server.on('stream', mustCall((stream) => { + stream.respond({ ':status': 421 }); + stream.end(); + })); + server.on('session', mustCall((session) => { + session.origin('https://foo.org'); + })); + + server.listen(0, mustCall(() => { + const origin = `https://localhost:${server.address().port}`; + const client = connect(origin, { ca }); + + client.on('origin', mustCall((origins) => { + deepStrictEqual([origin, 'https://foo.org'], client.originSet); + const req = client.request({ ':authority': 'foo.org' }); + req.on('response', mustCall((headers) => { + strictEqual(421, headers[':status']); + deepStrictEqual([origin], client.originSet); + })); + req.resume(); + req.on('close', mustCall(() => { + client.close(); + server.close(); + })); + }, 1)); + })); +} + +// Origin is ignored on plain text HTTP/2 connections... server will still +// send them, but client will ignore them. +{ + const server = createServer(); + server.on('stream', mustCall((stream) => { + stream.session.origin('https://example.org', + new URL('https://example.com')); + stream.respond(); + stream.end('ok'); + })); + server.listen(0, mustCall(() => { + const client = connect(`http://localhost:${server.address().port}`); + client.on('origin', mustNotCall()); + strictEqual(client.originSet, undefined); + const req = client.request(); + req.resume(); + req.on('close', mustCall(() => { + client.close(); + server.close(); + })); + })); +} diff --git a/test/parallel/test-http2-perf_hooks.js b/test/parallel/test-http2-perf_hooks.js index e30d0ac83e0d1f..b06e6efa2b6727 100644 --- a/test/parallel/test-http2-perf_hooks.js +++ b/test/parallel/test-http2-perf_hooks.js @@ -26,7 +26,7 @@ const obs = new PerformanceObserver(common.mustCall((items) => { switch (entry.type) { case 'server': assert.strictEqual(entry.streamCount, 1); - assert.strictEqual(entry.framesReceived, 5); + assert(entry.framesReceived >= 3); break; case 'client': assert.strictEqual(entry.streamCount, 1); diff --git a/test/parallel/test-http2-request-remove-connect-listener.js b/test/parallel/test-http2-request-remove-connect-listener.js new file mode 100644 index 00000000000000..61de140c225144 --- /dev/null +++ b/test/parallel/test-http2-request-remove-connect-listener.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + client.once('connect', common.mustCall()); + + req.on('response', common.mustCall(() => { + assert.strictEqual(client.listenerCount('connect'), 0); + })); + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/parallel/test-http2-respond-errors.js b/test/parallel/test-http2-respond-errors.js index 5854c4fb8d02e4..821430fbb9dd27 100644 --- a/test/parallel/test-http2-respond-errors.js +++ b/test/parallel/test-http2-respond-errors.js @@ -7,48 +7,13 @@ if (!common.hasCrypto) const http2 = require('http2'); const { Http2Stream } = process.binding('http2'); -const types = { - boolean: true, - function: () => {}, - number: 1, - object: {}, - array: [], - null: null, - symbol: Symbol('test') -}; - const server = http2.createServer(); Http2Stream.prototype.respond = () => 1; server.on('stream', common.mustCall((stream) => { - // Check for all possible TypeError triggers on options.getTrailers - Object.entries(types).forEach(([type, value]) => { - if (type === 'function') { - return; - } - - common.expectsError( - () => stream.respond({ - 'content-type': 'text/plain' - }, { - ['getTrailers']: value - }), - { - type: TypeError, - code: 'ERR_INVALID_OPT_VALUE', - message: `The value "${String(value)}" is invalid ` + - 'for option "getTrailers"' - } - ); - }); - // Send headers - stream.respond({ - 'content-type': 'text/plain' - }, { - ['getTrailers']: () => common.mustCall() - }); + stream.respond({ 'content-type': 'text/plain' }); // Should throw if headers already sent common.expectsError( diff --git a/test/parallel/test-http2-respond-file-errors.js b/test/parallel/test-http2-respond-file-errors.js index 83d3900bc5c288..96dd579a154fe7 100644 --- a/test/parallel/test-http2-respond-file-errors.js +++ b/test/parallel/test-http2-respond-file-errors.js @@ -9,8 +9,7 @@ const http2 = require('http2'); const optionsWithTypeError = { offset: 'number', length: 'number', - statCheck: 'function', - getTrailers: 'function' + statCheck: 'function' }; const types = { diff --git a/test/parallel/test-http2-respond-file-fd-errors.js b/test/parallel/test-http2-respond-file-fd-errors.js index 44876b60e1c4cb..da7b003fb12543 100644 --- a/test/parallel/test-http2-respond-file-fd-errors.js +++ b/test/parallel/test-http2-respond-file-fd-errors.js @@ -10,8 +10,7 @@ const fs = require('fs'); const optionsWithTypeError = { offset: 'number', length: 'number', - statCheck: 'function', - getTrailers: 'function' + statCheck: 'function' }; const types = { diff --git a/test/parallel/test-http2-respond-file.js b/test/parallel/test-http2-respond-file.js index 9ad8e7a69648dc..1c10ceb4350723 100644 --- a/test/parallel/test-http2-respond-file.js +++ b/test/parallel/test-http2-respond-file.js @@ -19,7 +19,7 @@ const data = fs.readFileSync(fname); const stat = fs.statSync(fname); const server = http2.createServer(); -server.on('stream', (stream) => { +server.on('stream', common.mustCall((stream) => { stream.respondWithFile(fname, { [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' }, { @@ -28,9 +28,9 @@ server.on('stream', (stream) => { headers[HTTP2_HEADER_CONTENT_LENGTH] = stat.size; } }); -}); -server.listen(0, () => { +})); +server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(); @@ -49,4 +49,4 @@ server.listen(0, () => { server.close(); })); req.end(); -}); +})); diff --git a/test/parallel/test-http2-sent-headers.js b/test/parallel/test-http2-sent-headers.js index bffa4d71c6d5f3..6ec674394336f2 100644 --- a/test/parallel/test-http2-sent-headers.js +++ b/test/parallel/test-http2-sent-headers.js @@ -12,10 +12,9 @@ server.on('stream', common.mustCall((stream) => { stream.additionalHeaders({ ':status': 102 }); assert.strictEqual(stream.sentInfoHeaders[0][':status'], 102); - stream.respond({ abc: 'xyz' }, { - getTrailers(headers) { - headers.xyz = 'abc'; - } + stream.respond({ abc: 'xyz' }, { waitForTrailers: true }); + stream.on('wantTrailers', () => { + stream.sendTrailers({ xyz: 'abc' }); }); assert.strictEqual(stream.sentHeaders.abc, 'xyz'); assert.strictEqual(stream.sentHeaders[':status'], 200); diff --git a/test/parallel/test-http2-server-close-callback.js b/test/parallel/test-http2-server-close-callback.js index 66887aa62bebe5..f822d8a4a92d78 100644 --- a/test/parallel/test-http2-server-close-callback.js +++ b/test/parallel/test-http2-server-close-callback.js @@ -4,21 +4,24 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const Countdown = require('../common/countdown'); const http2 = require('http2'); const server = http2.createServer(); +let session; + +const countdown = new Countdown(2, () => { + server.close(common.mustCall()); + session.destroy(); +}); + server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); - client.on('error', (err) => { - if (err.code !== 'ECONNRESET') - throw err; - }); + client.on('connect', common.mustCall(() => countdown.dec())); })); server.on('session', common.mustCall((s) => { - setImmediate(() => { - server.close(common.mustCall()); - s.destroy(); - }); + session = s; + countdown.dec(); })); diff --git a/test/parallel/test-http2-server-push-stream.js b/test/parallel/test-http2-server-push-stream.js index 6ac10cae77f951..74d41ba4b9c672 100644 --- a/test/parallel/test-http2-server-push-stream.js +++ b/test/parallel/test-http2-server-push-stream.js @@ -22,6 +22,14 @@ server.on('stream', common.mustCall((stream, headers) => { 'x-push-data': 'pushed by server', }); push.end('pushed by server data'); + + common.expectsError(() => { + push.pushStream({}, common.mustNotCall()); + }, { + code: 'ERR_HTTP2_NESTED_PUSH', + type: Error + }); + stream.end('test'); })); } @@ -46,6 +54,7 @@ server.listen(0, common.mustCall(() => { assert.strictEqual(headers['content-type'], 'text/html'); assert.strictEqual(headers['x-push-data'], 'pushed by server'); })); + stream.on('aborted', common.mustNotCall()); })); let data = ''; diff --git a/test/parallel/test-http2-server-sessionerror.js b/test/parallel/test-http2-server-sessionerror.js index 525eb2e6efd11a..c50352fcc35c28 100644 --- a/test/parallel/test-http2-server-sessionerror.js +++ b/test/parallel/test-http2-server-sessionerror.js @@ -35,14 +35,17 @@ server.on('session', common.mustCall((session) => { server.listen(0, common.mustCall(() => { const url = `http://localhost:${server.address().port}`; http2.connect(url) - // An ECONNRESET error may occur depending on the platform (due largely - // to differences in the timing of socket closing). Do not wrap this in - // a common must call. - .on('error', () => {}) + .on('error', common.expectsError({ + code: 'ERR_HTTP2_SESSION_ERROR', + message: 'Session closed with error code 2', + })) .on('close', () => { server.removeAllListeners('error'); http2.connect(url) - .on('error', () => {}) + .on('error', common.expectsError({ + code: 'ERR_HTTP2_SESSION_ERROR', + message: 'Session closed with error code 2', + })) .on('close', () => server.close()); }); })); diff --git a/test/parallel/test-http2-server-shutdown-options-errors.js b/test/parallel/test-http2-server-shutdown-options-errors.js index 2aedec1140701a..94733b199366db 100644 --- a/test/parallel/test-http2-server-shutdown-options-errors.js +++ b/test/parallel/test-http2-server-shutdown-options-errors.js @@ -54,13 +54,7 @@ server.listen( 0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); - // On certain operating systems, an ECONNRESET may occur. We do not need - // to test for it here. Do not make this a mustCall - client.on('error', () => {}); const req = client.request(); - // On certain operating systems, an ECONNRESET may occur. We do not need - // to test for it here. Do not make this a mustCall - req.on('error', () => {}); req.resume(); req.on('close', common.mustCall(() => { client.close(); diff --git a/test/parallel/test-http2-server-socket-destroy.js b/test/parallel/test-http2-server-socket-destroy.js index 03afc1957b8af4..99595aeb63004d 100644 --- a/test/parallel/test-http2-server-socket-destroy.js +++ b/test/parallel/test-http2-server-socket-destroy.js @@ -41,14 +41,20 @@ server.on('listening', common.mustCall(() => { // The client may have an ECONNRESET error here depending on the operating // system, due mainly to differences in the timing of socket closing. Do // not wrap this in a common mustCall. - client.on('error', () => {}); + client.on('error', (err) => { + if (err.code !== 'ECONNRESET') + throw err; + }); client.on('close', common.mustCall()); const req = client.request({ ':method': 'POST' }); // The client may have an ECONNRESET error here depending on the operating // system, due mainly to differences in the timing of socket closing. Do // not wrap this in a common mustCall. - req.on('error', () => {}); + req.on('error', (err) => { + if (err.code !== 'ECONNRESET') + throw err; + }); req.on('aborted', common.mustCall()); req.resume(); diff --git a/test/parallel/test-http2-server-stream-session-destroy.js b/test/parallel/test-http2-server-stream-session-destroy.js index e70630fe0b1351..6d8de4ba5f7618 100644 --- a/test/parallel/test-http2-server-stream-session-destroy.js +++ b/test/parallel/test-http2-server-stream-session-destroy.js @@ -39,16 +39,8 @@ server.on('stream', common.mustCall((stream) => { server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - client.on('error', (err) => { - if (err.code !== 'ECONNRESET') - throw err; - }); const req = client.request(); req.resume(); req.on('end', common.mustCall()); req.on('close', common.mustCall(() => server.close(common.mustCall()))); - req.on('error', (err) => { - if (err.code !== 'ECONNRESET') - throw err; - }); })); diff --git a/test/parallel/test-http2-server-timeout.js b/test/parallel/test-http2-server-timeout.js index 581a409ce9171d..88fc4eab2e08c0 100755 --- a/test/parallel/test-http2-server-timeout.js +++ b/test/parallel/test-http2-server-timeout.js @@ -6,10 +6,10 @@ if (!common.hasCrypto) const http2 = require('http2'); const server = http2.createServer(); -server.setTimeout(common.platformTimeout(1)); +server.setTimeout(common.platformTimeout(50)); const onServerTimeout = common.mustCall((session) => { - session.close(() => session.destroy()); + session.close(); }); server.on('stream', common.mustNotCall()); @@ -18,14 +18,8 @@ server.once('timeout', onServerTimeout); server.listen(0, common.mustCall(() => { const url = `http://localhost:${server.address().port}`; const client = http2.connect(url); - // Because of the timeout, an ECONRESET error may or may not happen here. - // Keep this as a non-op and do not use common.mustCall() - client.on('error', () => {}); client.on('close', common.mustCall(() => { const client2 = http2.connect(url); - // Because of the timeout, an ECONRESET error may or may not happen here. - // Keep this as a non-op and do not use common.mustCall() - client2.on('error', () => {}); client2.on('close', common.mustCall(() => server.close())); })); })); diff --git a/test/parallel/test-http2-session-unref.js b/test/parallel/test-http2-session-unref.js index 465f01d0921f25..0381971c0eace5 100644 --- a/test/parallel/test-http2-session-unref.js +++ b/test/parallel/test-http2-session-unref.js @@ -9,16 +9,20 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); const http2 = require('http2'); +const Countdown = require('../common/countdown'); const makeDuplexPair = require('../common/duplexpair'); const server = http2.createServer(); const { clientSide, serverSide } = makeDuplexPair(); +const counter = new Countdown(3, () => server.unref()); + // 'session' event should be emitted 3 times: // - the vanilla client // - the destroyed client // - manual 'connection' event emission with generic Duplex stream server.on('session', common.mustCallAtLeast((session) => { + counter.dec(); session.unref(); }, 3)); @@ -54,6 +58,3 @@ server.listen(0, common.mustCall(() => { } })); server.emit('connection', serverSide); -server.unref(); - -setTimeout(common.mustNotCall(() => {}), 1000).unref(); diff --git a/test/parallel/test-http2-socket-proxy.js b/test/parallel/test-http2-socket-proxy.js index 17830495addc63..209c49e719c676 100644 --- a/test/parallel/test-http2-socket-proxy.js +++ b/test/parallel/test-http2-socket-proxy.js @@ -38,6 +38,9 @@ server.on('stream', common.mustCall(function(stream, headers) { common.expectsError(() => socket.read, errMsg); common.expectsError(() => socket.resume, errMsg); common.expectsError(() => socket.write, errMsg); + common.expectsError(() => socket.setEncoding, errMsg); + common.expectsError(() => socket.setKeepAlive, errMsg); + common.expectsError(() => socket.setNoDelay, errMsg); common.expectsError(() => (socket.destroy = undefined), errMsg); common.expectsError(() => (socket.emit = undefined), errMsg); @@ -46,10 +49,18 @@ server.on('stream', common.mustCall(function(stream, headers) { common.expectsError(() => (socket.read = undefined), errMsg); common.expectsError(() => (socket.resume = undefined), errMsg); common.expectsError(() => (socket.write = undefined), errMsg); + common.expectsError(() => (socket.setEncoding = undefined), errMsg); + common.expectsError(() => (socket.setKeepAlive = undefined), errMsg); + common.expectsError(() => (socket.setNoDelay = undefined), errMsg); assert.doesNotThrow(() => (socket.on = socket.on)); assert.doesNotThrow(() => (socket.once = socket.once)); + socket.unref(); + assert.strictEqual(socket._handle.hasRef(), false); + socket.ref(); + assert.strictEqual(socket._handle.hasRef(), true); + stream.respond(); socket.writable = 0; diff --git a/test/parallel/test-http2-too-many-settings.js b/test/parallel/test-http2-too-many-settings.js index 0302fe623da07c..acfd73ada68416 100644 --- a/test/parallel/test-http2-too-many-settings.js +++ b/test/parallel/test-http2-too-many-settings.js @@ -29,9 +29,10 @@ function doTest(session) { server.listen(0, common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - // On some operating systems, an ECONNRESET error may be emitted. - // On others it won't be. Do not make this a mustCall - client.on('error', () => {}); + client.on('error', common.expectsError({ + code: 'ERR_HTTP2_SESSION_ERROR', + message: 'Session closed with error code 2', + })); client.on('close', common.mustCall(() => server.close())); })); } diff --git a/test/parallel/test-http2-trailers.js b/test/parallel/test-http2-trailers.js index 1ca5bdf70d05b0..bdc0931157ad58 100644 --- a/test/parallel/test-http2-trailers.js +++ b/test/parallel/test-http2-trailers.js @@ -18,32 +18,53 @@ server.on('stream', common.mustCall(onStream)); function onStream(stream, headers, flags) { stream.on('trailers', common.mustCall((headers) => { assert.strictEqual(headers[trailerKey], trailerValue); + stream.end(body); })); stream.respond({ 'content-type': 'text/html', ':status': 200 - }, { - getTrailers: common.mustCall((trailers) => { - trailers[trailerKey] = trailerValue; - }) + }, { waitForTrailers: true }); + stream.on('wantTrailers', () => { + stream.sendTrailers({ [trailerKey]: trailerValue }); + common.expectsError( + () => stream.sendTrailers({}), + { + code: 'ERR_HTTP2_TRAILERS_ALREADY_SENT', + type: Error + } + ); }); - stream.end(body); + + common.expectsError( + () => stream.sendTrailers({}), + { + code: 'ERR_HTTP2_TRAILERS_NOT_READY', + type: Error + } + ); } server.listen(0); server.on('listening', common.mustCall(function() { const client = h2.connect(`http://localhost:${this.address().port}`); - const req = client.request({ ':path': '/', ':method': 'POST' }, { - getTrailers: common.mustCall((trailers) => { - trailers[trailerKey] = trailerValue; - }) + const req = client.request({ ':path': '/', ':method': 'POST' }, + { waitForTrailers: true }); + req.on('wantTrailers', () => { + req.sendTrailers({ [trailerKey]: trailerValue }); }); req.on('data', common.mustCall()); req.on('trailers', common.mustCall((headers) => { assert.strictEqual(headers[trailerKey], trailerValue); })); - req.on('end', common.mustCall(() => { + req.on('close', common.mustCall(() => { + common.expectsError( + () => req.sendTrailers({}), + { + code: 'ERR_HTTP2_INVALID_STREAM', + type: Error + } + ); server.close(); client.close(); })); diff --git a/test/parallel/test-http2-unbound-socket-proxy.js b/test/parallel/test-http2-unbound-socket-proxy.js new file mode 100644 index 00000000000000..18881574f2c09b --- /dev/null +++ b/test/parallel/test-http2-unbound-socket-proxy.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); +const net = require('net'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end('ok'); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const socket = client.socket; + const req = client.request(); + req.resume(); + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + + // Tests to make sure accessing the socket proxy fails with an + // informative error. + setImmediate(common.mustCall(() => { + common.expectsError(() => { + socket.example; + }, { + code: 'ERR_HTTP2_SOCKET_UNBOUND' + }); + common.expectsError(() => { + socket.example = 1; + }, { + code: 'ERR_HTTP2_SOCKET_UNBOUND' + }); + common.expectsError(() => { + socket instanceof net.Socket; + }, { + code: 'ERR_HTTP2_SOCKET_UNBOUND' + }); + })); + })); +})); diff --git a/test/parallel/test-http2-util-headers-list.js b/test/parallel/test-http2-util-headers-list.js index 0ff6b558d9a51b..3b594e727cc5ef 100644 --- a/test/parallel/test-http2-util-headers-list.js +++ b/test/parallel/test-http2-util-headers-list.js @@ -1,14 +1,14 @@ // Flags: --expose-internals 'use strict'; -// Tests the internal utility function that is used to prepare headers -// to pass to the internal binding layer. +// Tests the internal utility functions that are used to prepare headers +// to pass to the internal binding layer and to build a header object. const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { mapToHeaders } = require('internal/http2/util'); +const { mapToHeaders, toHeaderObject } = require('internal/http2/util'); const { HTTP2_HEADER_STATUS, @@ -302,3 +302,41 @@ common.expectsError({ assert(!(mapToHeaders({ te: 'trailers' }) instanceof Error)); assert(!(mapToHeaders({ te: ['trailers'] }) instanceof Error)); + + +{ + const rawHeaders = [ + ':status', '200', + 'cookie', 'foo', + 'set-cookie', 'sc1', + 'age', '10', + 'x-multi', 'first' + ]; + const headers = toHeaderObject(rawHeaders); + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers.cookie, 'foo'); + assert.deepStrictEqual(headers['set-cookie'], ['sc1']); + assert.strictEqual(headers.age, '10'); + assert.strictEqual(headers['x-multi'], 'first'); +} + +{ + const rawHeaders = [ + ':status', '200', + ':status', '400', + 'cookie', 'foo', + 'cookie', 'bar', + 'set-cookie', 'sc1', + 'set-cookie', 'sc2', + 'age', '10', + 'age', '20', + 'x-multi', 'first', + 'x-multi', 'second' + ]; + const headers = toHeaderObject(rawHeaders); + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers.cookie, 'foo; bar'); + assert.deepStrictEqual(headers['set-cookie'], ['sc1', 'sc2']); + assert.strictEqual(headers.age, '10'); + assert.strictEqual(headers['x-multi'], 'first, second'); +} diff --git a/test/sequential/test-http2-large-file.js b/test/sequential/test-http2-large-file.js new file mode 100644 index 00000000000000..d1a44e8d6be53c --- /dev/null +++ b/test/sequential/test-http2-large-file.js @@ -0,0 +1,39 @@ +'use strict'; + +// Test to ensure sending a large stream with a large initial window size works +// See: https://github.com/nodejs/node/issues/19141 + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); + +const server = http2.createServer({ settings: { initialWindowSize: 6553500 } }); +server.on('stream', (stream) => { + stream.resume(); + stream.respond(); + stream.end('ok'); +}); + +server.listen(0, common.mustCall(() => { + let remaining = 1e8; + const chunk = 1e6; + const client = http2.connect(`http://localhost:${server.address().port}`, + { settings: { initialWindowSize: 6553500 } }); + const request = client.request({ ':method': 'POST' }); + function writeChunk() { + if (remaining > 0) { + remaining -= chunk; + request.write(Buffer.alloc(chunk, 'a'), writeChunk); + } else { + request.end(); + } + } + writeChunk(); + request.on('close', common.mustCall(() => { + client.close(); + server.close(); + })); + request.resume(); +})); diff --git a/test/sequential/test-http2-session-timeout.js b/test/sequential/test-http2-session-timeout.js index fce4570563c584..01233a3bfedd17 100644 --- a/test/sequential/test-http2-session-timeout.js +++ b/test/sequential/test-http2-session-timeout.js @@ -3,14 +3,16 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const h2 = require('http2'); +const assert = require('assert'); +const http2 = require('http2'); const serverTimeout = common.platformTimeout(200); -const callTimeout = common.platformTimeout(20); -const minRuns = Math.ceil(serverTimeout / callTimeout) * 2; -const mustNotCall = common.mustNotCall(); -const server = h2.createServer(); +let requests = 0; +const mustNotCall = () => { + assert.fail(`Timeout after ${requests} request(s)`); +}; +const server = http2.createServer(); server.timeout = serverTimeout; server.on('request', (req, res) => res.end()); @@ -20,10 +22,11 @@ server.listen(0, common.mustCall(() => { const port = server.address().port; const url = `http://localhost:${port}`; - const client = h2.connect(url); - makeReq(minRuns); + const client = http2.connect(url); + const startTime = process.hrtime(); + makeReq(); - function makeReq(attempts) { + function makeReq() { const request = client.request({ ':path': '/foobar', ':method': 'GET', @@ -33,13 +36,17 @@ server.listen(0, common.mustCall(() => { request.resume(); request.end(); + requests += 1; + request.on('end', () => { - if (attempts) { - setTimeout(() => makeReq(attempts - 1), callTimeout); + const diff = process.hrtime(startTime); + const milliseconds = (diff[0] * 1e3 + diff[1] / 1e6); + if (milliseconds < serverTimeout * 2) { + makeReq(); } else { server.removeListener('timeout', mustNotCall); - client.close(); server.close(); + client.close(); } }); }