From a03777096ea495821f1c234c947fd52b4cdba50c Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 2 Mar 2020 17:27:06 -0800 Subject: [PATCH] src,http2: introduce node_http_common The nghttp2 and nghttp3 (used in the QUIC implementation) share nearly identical structs for header handling. However, they differ enough that they need to be handled slightly different in each case. This PR includes some elements introduced in the QUIC PR separated out to make them independently reviewable, and updates the http2 implementation to use the shared utilities. Signed-off-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/32069 Reviewed-By: Anna Henningsen Reviewed-By: Matteo Collina --- node.gyp | 2 + src/env.h | 2 +- src/node_http2.cc | 235 +++++-------- src/node_http2.h | 378 +++----------------- src/node_http_common-inl.h | 181 ++++++++++ src/node_http_common.h | 527 ++++++++++++++++++++++++++++ test/parallel/test-http2-binding.js | 11 +- 7 files changed, 862 insertions(+), 474 deletions(-) create mode 100644 src/node_http_common-inl.h create mode 100644 src/node_http_common.h diff --git a/node.gyp b/node.gyp index d540903c777c69..86c953ca9a1736 100644 --- a/node.gyp +++ b/node.gyp @@ -664,6 +664,8 @@ 'src/node_errors.h', 'src/node_file.h', 'src/node_file-inl.h', + 'src/node_http_common.h', + 'src/node_http_common-inl.h', 'src/node_http2.h', 'src/node_http2_state.h', 'src/node_i18n.h', diff --git a/src/env.h b/src/env.h index 3c6aeb0f4ce6eb..0693c93161eb92 100644 --- a/src/env.h +++ b/src/env.h @@ -510,7 +510,7 @@ class IsolateData : public MemoryRetainer { #undef VS #undef VP - std::unordered_map> http2_static_strs; + std::unordered_map> http_static_strs; inline v8::Isolate* isolate() const; IsolateData(const IsolateData&) = delete; IsolateData& operator=(const IsolateData&) = delete; diff --git a/src/node_http2.cc b/src/node_http2.cc index 3de43f5f3af2cb..b8eaf9af8ab0cd 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -5,6 +5,7 @@ #include "node_buffer.h" #include "node_http2.h" #include "node_http2_state.h" +#include "node_http_common-inl.h" #include "node_mem-inl.h" #include "node_perf.h" #include "node_revert.h" @@ -355,66 +356,6 @@ const char* Http2Session::TypeName() const { } } -// The Headers class initializes a proper array of nghttp2_nv structs -// containing the header name value pairs. -Headers::Headers(Isolate* isolate, - Local context, - Local headers) { - Local header_string = headers->Get(context, 0).ToLocalChecked(); - Local header_count = headers->Get(context, 1).ToLocalChecked(); - count_ = header_count.As()->Value(); - int header_string_len = header_string.As()->Length(); - - if (count_ == 0) { - CHECK_EQ(header_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_nv) - 1) + - count_ * sizeof(nghttp2_nv) + - header_string_len); - // Make sure the start address is aligned appropriately for an nghttp2_nv*. - char* start = reinterpret_cast( - RoundUp(reinterpret_cast(*buf_), alignof(nghttp2_nv))); - char* header_contents = start + (count_ * sizeof(nghttp2_nv)); - nghttp2_nv* const nva = reinterpret_cast(start); - - CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length()); - CHECK_EQ(header_string.As()->WriteOneByte( - isolate, - reinterpret_cast(header_contents), - 0, - header_string_len, - String::NO_NULL_TERMINATION), - header_string_len); - - size_t n = 0; - char* p; - for (p = header_contents; p < header_contents + header_string_len; n++) { - if (n >= count_) { - // This can happen if a passed header contained a null byte. In that - // case, just provide nghttp2 with an invalid header to make it reject - // the headers list. - static uint8_t zero = '\0'; - nva[0].name = nva[0].value = &zero; - nva[0].namelen = nva[0].valuelen = 1; - count_ = 1; - return; - } - - nva[n].flags = NGHTTP2_NV_FLAG_NONE; - nva[n].name = reinterpret_cast(p); - nva[n].namelen = strlen(p); - p += nva[n].namelen + 1; - nva[n].value = reinterpret_cast(p); - nva[n].valuelen = strlen(p); - p += nva[n].valuelen + 1; - } -} - Origins::Origins(Isolate* isolate, Local context, Local origin_string, @@ -537,8 +478,8 @@ Http2Session::Http2Session(Environment* env, uint32_t maxHeaderPairs = opts.GetMaxHeaderPairs(); max_header_pairs_ = type == NGHTTP2_SESSION_SERVER - ? std::max(maxHeaderPairs, 4U) // minimum # of request headers - : std::max(maxHeaderPairs, 1U); // minimum # of response headers + ? GetServerMaxHeaderPairs(maxHeaderPairs) + : GetClientMaxHeaderPairs(maxHeaderPairs); max_outstanding_pings_ = opts.GetMaxOutstandingPings(); max_outstanding_settings_ = opts.GetMaxOutstandingSettings(); @@ -1248,34 +1189,30 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) { if (stream->IsDestroyed()) return; - std::vector headers(stream->move_headers()); - DecrementCurrentSessionMemory(stream->current_headers_length_); - stream->current_headers_length_ = 0; - - // The headers are passed in above as a queue of nghttp2_header structs. + // The headers are stored as a vector of Http2Header instances. // The following converts that into a JS array with the structure: // [name1, value1, name2, value2, name3, value3, name3, value4] and so on. // That array is passed up to the JS layer and converted into an Object form // like {name1: value1, name2: value2, name3: [value3, value4]}. We do it // 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 headers_size = headers.size(); - std::vector> headers_v(headers_size * 2); - for (size_t i = 0; i < headers_size; ++i) { - const nghttp2_header& item = headers[i]; - // The header name and value are passed as external one-byte strings - headers_v[i * 2] = - ExternalHeader::New(this, item.name).ToLocalChecked(); - headers_v[i * 2 + 1] = - ExternalHeader::New(this, item.value).ToLocalChecked(); - } + + std::vector> headers_v(stream->headers_count() * 2); + stream->TransferHeaders([&](const Http2Header& header, size_t i) { + headers_v[i * 2] = header.GetName(this).ToLocalChecked(); + headers_v[i * 2 + 1] = header.GetValue(this).ToLocalChecked(); + }); + CHECK_EQ(stream->headers_count(), 0); + + DecrementCurrentSessionMemory(stream->current_headers_length_); + stream->current_headers_length_ = 0; Local args[5] = { stream->object(), Integer::New(isolate, id), Integer::New(isolate, stream->headers_category()), Integer::New(isolate, frame->hd.flags), - Array::New(isolate, headers_v.data(), headers_size * 2)}; + Array::New(isolate, headers_v.data(), headers_v.size())}; MakeCallback(env()->http2session_on_headers_function(), arraysize(args), args); } @@ -1760,15 +1697,20 @@ int Http2Session::OnSendData( // Creates a new Http2Stream and submits a new http2 request. Http2Stream* Http2Session::SubmitRequest( nghttp2_priority_spec* prispec, - nghttp2_nv* nva, - size_t len, + const Http2Headers& headers, int32_t* ret, int options) { Debug(this, "submitting request"); Http2Scope h2scope(this); Http2Stream* stream = nullptr; Http2Stream::Provider::Stream prov(options); - *ret = nghttp2_submit_request(session_, prispec, nva, len, *prov, nullptr); + *ret = nghttp2_submit_request( + session_, + prispec, + headers.data(), + headers.length(), + *prov, + nullptr); CHECK_NE(*ret, NGHTTP2_ERR_NOMEM); if (LIKELY(*ret > 0)) stream = Http2Stream::New(this, *ret, NGHTTP2_HCAT_HEADERS, options); @@ -1917,13 +1859,7 @@ Http2Stream::Http2Stream(Http2Session* session, session->AddStream(this); } - Http2Stream::~Http2Stream() { - for (nghttp2_header& header : current_headers_) { - nghttp2_rcbuf_decref(header.name); - nghttp2_rcbuf_decref(header.value); - } - if (!session_) return; Debug(this, "tearing down stream"); @@ -2025,7 +1961,7 @@ void Http2Stream::Destroy() { // Initiates a response on the Http2Stream using data provided via the // StreamBase Streams API. -int Http2Stream::SubmitResponse(nghttp2_nv* nva, size_t len, int options) { +int Http2Stream::SubmitResponse(const Http2Headers& headers, int options) { CHECK(!this->IsDestroyed()); Http2Scope h2scope(this); Debug(this, "submitting response"); @@ -2036,21 +1972,30 @@ int Http2Stream::SubmitResponse(nghttp2_nv* nva, size_t len, int options) { options |= STREAM_OPTION_EMPTY_PAYLOAD; Http2Stream::Provider::Stream prov(this, options); - int ret = nghttp2_submit_response(**session_, id_, nva, len, *prov); + int ret = nghttp2_submit_response( + **session_, + id_, + headers.data(), + headers.length(), + *prov); CHECK_NE(ret, NGHTTP2_ERR_NOMEM); return ret; } // Submit informational headers for a stream. -int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) { +int Http2Stream::SubmitInfo(const Http2Headers& headers) { CHECK(!this->IsDestroyed()); Http2Scope h2scope(this); - Debug(this, "sending %d informational headers", len); - int ret = nghttp2_submit_headers(**session_, - NGHTTP2_FLAG_NONE, - id_, nullptr, - nva, len, nullptr); + Debug(this, "sending %d informational headers", headers.length()); + int ret = nghttp2_submit_headers( + **session_, + NGHTTP2_FLAG_NONE, + id_, + nullptr, + headers.data(), + headers.length(), + nullptr); CHECK_NE(ret, NGHTTP2_ERR_NOMEM); return ret; } @@ -2067,19 +2012,23 @@ void Http2Stream::OnTrailers() { } // Submit informational headers for a stream. -int Http2Stream::SubmitTrailers(nghttp2_nv* nva, size_t len) { +int Http2Stream::SubmitTrailers(const Http2Headers& headers) { CHECK(!this->IsDestroyed()); Http2Scope h2scope(this); - Debug(this, "sending %d trailers", len); + Debug(this, "sending %d trailers", headers.length()); 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) { + if (headers.length() == 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); + ret = nghttp2_submit_trailer( + **session_, + id_, + headers.data(), + headers.length()); } CHECK_NE(ret, NGHTTP2_ERR_NOMEM); return ret; @@ -2128,15 +2077,19 @@ void Http2Stream::FlushRstStream() { // Submit a push promise and create the associated Http2Stream if successful. -Http2Stream* Http2Stream::SubmitPushPromise(nghttp2_nv* nva, - size_t len, +Http2Stream* Http2Stream::SubmitPushPromise(const Http2Headers& headers, int32_t* ret, int options) { CHECK(!this->IsDestroyed()); Http2Scope h2scope(this); Debug(this, "sending push promise"); - *ret = nghttp2_submit_push_promise(**session_, NGHTTP2_FLAG_NONE, - id_, nva, len, nullptr); + *ret = nghttp2_submit_push_promise( + **session_, + NGHTTP2_FLAG_NONE, + id_, + headers.data(), + headers.length(), + nullptr); CHECK_NE(*ret, NGHTTP2_ERR_NOMEM); Http2Stream* stream = nullptr; if (*ret > 0) { @@ -2220,12 +2173,12 @@ bool Http2Stream::AddHeader(nghttp2_rcbuf* name, nghttp2_rcbuf* value, uint8_t flags) { CHECK(!this->IsDestroyed()); - if (this->statistics_.first_header == 0) - this->statistics_.first_header = uv_hrtime(); - size_t name_len = nghttp2_rcbuf_get_buf(name).len; - if (name_len == 0) return true; // Ignore headers with empty names. - size_t value_len = nghttp2_rcbuf_get_buf(value).len; - size_t length = name_len + value_len + 32; + + if (Http2RcBufferPointer::IsZeroLength(name)) + return true; // Ignore empty headers. + + Http2Header header(env(), name, value, flags); + size_t length = header.length() + 32; // A header can only be added if we have not exceeded the maximum number // of headers and the session has memory available for it. if (!session_->IsAvailableSessionMemory(length) || @@ -2233,13 +2186,12 @@ bool Http2Stream::AddHeader(nghttp2_rcbuf* name, current_headers_length_ + length > max_header_length_) { return false; } - nghttp2_header header; - header.name = name; - header.value = value; - header.flags = flags; - current_headers_.push_back(header); - nghttp2_rcbuf_incref(name); - nghttp2_rcbuf_incref(value); + + if (statistics_.first_header == 0) + statistics_.first_header = uv_hrtime(); + + current_headers_.push_back(std::move(header)); + current_headers_length_ += length; session_->IncrementCurrentSessionMemory(length); return true; @@ -2486,21 +2438,20 @@ void Http2Session::Request(const FunctionCallbackInfo& args) { Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); Environment* env = session->env(); - Local context = env->context(); - Isolate* isolate = env->isolate(); Local headers = args[0].As(); - int options = args[1]->IntegerValue(context).ToChecked(); + int options = args[1]->IntegerValue(env->context()).ToChecked(); Http2Priority priority(env, args[2], args[3], args[4]); - Headers list(isolate, context, headers); - Debug(session, "request submitted"); int32_t ret = 0; Http2Stream* stream = - session->Http2Session::SubmitRequest(*priority, *list, list.length(), - &ret, options); + session->Http2Session::SubmitRequest( + *priority, + Http2Headers(env, headers), + &ret, + options); if (ret <= 0 || stream == nullptr) { Debug(session, "could not submit request: %s", nghttp2_strerror(ret)); @@ -2585,18 +2536,14 @@ void Http2Stream::RstStream(const FunctionCallbackInfo& args) { // outbound DATA frames. void Http2Stream::Respond(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(); - int options = args[1]->IntegerValue(context).ToChecked(); - - Headers list(isolate, context, headers); + int options = args[1]->IntegerValue(env->context()).ToChecked(); args.GetReturnValue().Set( - stream->SubmitResponse(*list, list.length(), options)); + stream->SubmitResponse(Http2Headers(env, headers), options)); Debug(stream, "response submitted"); } @@ -2604,31 +2551,24 @@ void Http2Stream::Respond(const FunctionCallbackInfo& args) { // Submits informational headers on the Http2Stream void Http2Stream::Info(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->SubmitInfo(*list, list.length())); - Debug(stream, "%d informational headers sent", list.length()); + args.GetReturnValue().Set(stream->SubmitInfo(Http2Headers(env, headers))); } // 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(stream, "%d trailing headers sent", list.length()); + args.GetReturnValue().Set( + stream->SubmitTrailers(Http2Headers(env, headers))); } // Grab the numeric id of the Http2Stream @@ -2649,21 +2589,18 @@ void Http2Stream::Destroy(const FunctionCallbackInfo& args) { // Initiate a Push Promise and create the associated Http2Stream void Http2Stream::PushPromise(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - Local context = env->context(); - Isolate* isolate = env->isolate(); Http2Stream* parent; ASSIGN_OR_RETURN_UNWRAP(&parent, args.Holder()); Local headers = args[0].As(); - int options = args[1]->IntegerValue(context).ToChecked(); - - Headers list(isolate, context, headers); + int options = args[1]->IntegerValue(env->context()).ToChecked(); Debug(parent, "creating push promise"); int32_t ret = 0; - Http2Stream* stream = parent->SubmitPushPromise(*list, list.length(), - &ret, options); + Http2Stream* stream = + parent->SubmitPushPromise(Http2Headers(env, headers), &ret, options); + if (ret <= 0 || stream == nullptr) { Debug(parent, "failed to create push stream: %d", ret); return args.GetReturnValue().Set(ret); @@ -2939,12 +2876,6 @@ void nghttp2_stream_write::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("buf", buf); } - -void nghttp2_header::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackFieldWithSize("name", nghttp2_rcbuf_get_buf(name).len); - tracker->TrackFieldWithSize("value", nghttp2_rcbuf_get_buf(value).len); -} - void SetCallbackFunctions(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK_EQ(args.Length(), 11); diff --git a/src/node_http2.h b/src/node_http2.h index d3e66dfba6894f..690a97027848c8 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -8,6 +8,7 @@ #include "nghttp2/nghttp2.h" #include "node_http2_state.h" +#include "node_http_common.h" #include "node_mem.h" #include "node_perf.h" #include "stream_base-inl.h" @@ -50,8 +51,36 @@ using performance::PerformanceEntry; #define MIN_MAX_FRAME_SIZE DEFAULT_SETTINGS_MAX_FRAME_SIZE #define MAX_INITIAL_WINDOW_SIZE 2147483647 -#define MAX_MAX_HEADER_LIST_SIZE 16777215u -#define DEFAULT_MAX_HEADER_LIST_PAIRS 128u +struct Http2HeadersTraits { + typedef nghttp2_nv nv_t; + static const uint8_t kNoneFlag = NGHTTP2_NV_FLAG_NONE; +}; + +struct Http2RcBufferPointerTraits { + typedef nghttp2_rcbuf rcbuf_t; + typedef nghttp2_vec vector_t; + + static void inc(rcbuf_t* buf) { + CHECK_NOT_NULL(buf); + nghttp2_rcbuf_incref(buf); + } + static void dec(rcbuf_t* buf) { + CHECK_NOT_NULL(buf); + nghttp2_rcbuf_decref(buf); + } + static vector_t get_vec(rcbuf_t* buf) { + CHECK_NOT_NULL(buf); + return nghttp2_rcbuf_get_buf(buf); + } + static bool is_static(const rcbuf_t* buf) { + CHECK_NOT_NULL(buf); + return nghttp2_rcbuf_is_static(buf); + } +}; + +using Http2Headers = NgHeaders; +using Http2RcBufferPointer = NgRcBufPointer; + enum nghttp2_session_type { NGHTTP2_SESSION_SERVER, @@ -96,224 +125,6 @@ struct nghttp2_stream_write : public MemoryRetainer { SET_SELF_SIZE(nghttp2_stream_write) }; -struct nghttp2_header : public MemoryRetainer { - nghttp2_rcbuf* name = nullptr; - nghttp2_rcbuf* value = nullptr; - uint8_t flags = 0; - - void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(nghttp2_header) - SET_SELF_SIZE(nghttp2_header) -}; - - -// Unlike the HTTP/1 implementation, the HTTP/2 implementation is not limited -// to a fixed number of known supported HTTP methods. These constants, therefore -// are provided strictly as a convenience to users and are exposed via the -// require('http2').constants object. -#define HTTP_KNOWN_METHODS(V) \ - V(ACL, "ACL") \ - V(BASELINE_CONTROL, "BASELINE-CONTROL") \ - V(BIND, "BIND") \ - V(CHECKIN, "CHECKIN") \ - V(CHECKOUT, "CHECKOUT") \ - V(CONNECT, "CONNECT") \ - V(COPY, "COPY") \ - V(DELETE, "DELETE") \ - V(GET, "GET") \ - V(HEAD, "HEAD") \ - V(LABEL, "LABEL") \ - V(LINK, "LINK") \ - V(LOCK, "LOCK") \ - V(MERGE, "MERGE") \ - V(MKACTIVITY, "MKACTIVITY") \ - V(MKCALENDAR, "MKCALENDAR") \ - V(MKCOL, "MKCOL") \ - V(MKREDIRECTREF, "MKREDIRECTREF") \ - V(MKWORKSPACE, "MKWORKSPACE") \ - V(MOVE, "MOVE") \ - V(OPTIONS, "OPTIONS") \ - V(ORDERPATCH, "ORDERPATCH") \ - V(PATCH, "PATCH") \ - V(POST, "POST") \ - V(PRI, "PRI") \ - V(PROPFIND, "PROPFIND") \ - V(PROPPATCH, "PROPPATCH") \ - V(PUT, "PUT") \ - V(REBIND, "REBIND") \ - V(REPORT, "REPORT") \ - V(SEARCH, "SEARCH") \ - V(TRACE, "TRACE") \ - V(UNBIND, "UNBIND") \ - V(UNCHECKOUT, "UNCHECKOUT") \ - V(UNLINK, "UNLINK") \ - V(UNLOCK, "UNLOCK") \ - V(UPDATE, "UPDATE") \ - V(UPDATEREDIRECTREF, "UPDATEREDIRECTREF") \ - V(VERSION_CONTROL, "VERSION-CONTROL") - -// These are provided strictly as a convenience to users and are exposed via the -// require('http2').constants objects -#define HTTP_KNOWN_HEADERS(V) \ - V(STATUS, ":status") \ - V(METHOD, ":method") \ - V(AUTHORITY, ":authority") \ - V(SCHEME, ":scheme") \ - V(PATH, ":path") \ - V(PROTOCOL, ":protocol") \ - V(ACCEPT_CHARSET, "accept-charset") \ - V(ACCEPT_ENCODING, "accept-encoding") \ - V(ACCEPT_LANGUAGE, "accept-language") \ - V(ACCEPT_RANGES, "accept-ranges") \ - V(ACCEPT, "accept") \ - V(ACCESS_CONTROL_ALLOW_CREDENTIALS, "access-control-allow-credentials") \ - V(ACCESS_CONTROL_ALLOW_HEADERS, "access-control-allow-headers") \ - V(ACCESS_CONTROL_ALLOW_METHODS, "access-control-allow-methods") \ - V(ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin") \ - V(ACCESS_CONTROL_EXPOSE_HEADERS, "access-control-expose-headers") \ - V(ACCESS_CONTROL_MAX_AGE, "access-control-max-age") \ - V(ACCESS_CONTROL_REQUEST_HEADERS, "access-control-request-headers") \ - V(ACCESS_CONTROL_REQUEST_METHOD, "access-control-request-method") \ - V(AGE, "age") \ - V(ALLOW, "allow") \ - V(AUTHORIZATION, "authorization") \ - V(CACHE_CONTROL, "cache-control") \ - V(CONNECTION, "connection") \ - V(CONTENT_DISPOSITION, "content-disposition") \ - V(CONTENT_ENCODING, "content-encoding") \ - V(CONTENT_LANGUAGE, "content-language") \ - V(CONTENT_LENGTH, "content-length") \ - V(CONTENT_LOCATION, "content-location") \ - V(CONTENT_MD5, "content-md5") \ - V(CONTENT_RANGE, "content-range") \ - V(CONTENT_TYPE, "content-type") \ - V(COOKIE, "cookie") \ - V(DATE, "date") \ - V(DNT, "dnt") \ - V(ETAG, "etag") \ - V(EXPECT, "expect") \ - V(EXPIRES, "expires") \ - V(FORWARDED, "forwarded") \ - V(FROM, "from") \ - V(HOST, "host") \ - V(IF_MATCH, "if-match") \ - V(IF_MODIFIED_SINCE, "if-modified-since") \ - V(IF_NONE_MATCH, "if-none-match") \ - V(IF_RANGE, "if-range") \ - V(IF_UNMODIFIED_SINCE, "if-unmodified-since") \ - V(LAST_MODIFIED, "last-modified") \ - V(LINK, "link") \ - V(LOCATION, "location") \ - V(MAX_FORWARDS, "max-forwards") \ - V(PREFER, "prefer") \ - V(PROXY_AUTHENTICATE, "proxy-authenticate") \ - V(PROXY_AUTHORIZATION, "proxy-authorization") \ - V(RANGE, "range") \ - V(REFERER, "referer") \ - V(REFRESH, "refresh") \ - V(RETRY_AFTER, "retry-after") \ - V(SERVER, "server") \ - V(SET_COOKIE, "set-cookie") \ - V(STRICT_TRANSPORT_SECURITY, "strict-transport-security") \ - V(TRAILER, "trailer") \ - V(TRANSFER_ENCODING, "transfer-encoding") \ - V(TE, "te") \ - V(TK, "tk") \ - V(UPGRADE_INSECURE_REQUESTS, "upgrade-insecure-requests") \ - V(UPGRADE, "upgrade") \ - V(USER_AGENT, "user-agent") \ - V(VARY, "vary") \ - V(VIA, "via") \ - V(WARNING, "warning") \ - V(WWW_AUTHENTICATE, "www-authenticate") \ - V(X_CONTENT_TYPE_OPTIONS, "x-content-type-options") \ - V(X_FRAME_OPTIONS, "x-frame-options") \ - V(HTTP2_SETTINGS, "http2-settings") \ - V(KEEP_ALIVE, "keep-alive") \ - V(PROXY_CONNECTION, "proxy-connection") - -enum http_known_headers { - HTTP_KNOWN_HEADER_MIN, -#define V(name, value) HTTP_HEADER_##name, - HTTP_KNOWN_HEADERS(V) -#undef V - HTTP_KNOWN_HEADER_MAX -}; - -// While some of these codes are used within the HTTP/2 implementation in -// core, they are provided strictly as a convenience to users and are exposed -// via the require('http2').constants object. -#define HTTP_STATUS_CODES(V) \ - V(CONTINUE, 100) \ - V(SWITCHING_PROTOCOLS, 101) \ - V(PROCESSING, 102) \ - V(EARLY_HINTS, 103) \ - V(OK, 200) \ - V(CREATED, 201) \ - V(ACCEPTED, 202) \ - V(NON_AUTHORITATIVE_INFORMATION, 203) \ - V(NO_CONTENT, 204) \ - V(RESET_CONTENT, 205) \ - V(PARTIAL_CONTENT, 206) \ - V(MULTI_STATUS, 207) \ - V(ALREADY_REPORTED, 208) \ - V(IM_USED, 226) \ - V(MULTIPLE_CHOICES, 300) \ - V(MOVED_PERMANENTLY, 301) \ - V(FOUND, 302) \ - V(SEE_OTHER, 303) \ - V(NOT_MODIFIED, 304) \ - V(USE_PROXY, 305) \ - V(TEMPORARY_REDIRECT, 307) \ - V(PERMANENT_REDIRECT, 308) \ - V(BAD_REQUEST, 400) \ - V(UNAUTHORIZED, 401) \ - V(PAYMENT_REQUIRED, 402) \ - V(FORBIDDEN, 403) \ - V(NOT_FOUND, 404) \ - V(METHOD_NOT_ALLOWED, 405) \ - V(NOT_ACCEPTABLE, 406) \ - V(PROXY_AUTHENTICATION_REQUIRED, 407) \ - V(REQUEST_TIMEOUT, 408) \ - V(CONFLICT, 409) \ - V(GONE, 410) \ - V(LENGTH_REQUIRED, 411) \ - V(PRECONDITION_FAILED, 412) \ - V(PAYLOAD_TOO_LARGE, 413) \ - V(URI_TOO_LONG, 414) \ - V(UNSUPPORTED_MEDIA_TYPE, 415) \ - V(RANGE_NOT_SATISFIABLE, 416) \ - V(EXPECTATION_FAILED, 417) \ - V(TEAPOT, 418) \ - V(MISDIRECTED_REQUEST, 421) \ - V(UNPROCESSABLE_ENTITY, 422) \ - V(LOCKED, 423) \ - V(FAILED_DEPENDENCY, 424) \ - V(TOO_EARLY, 425) \ - V(UPGRADE_REQUIRED, 426) \ - V(PRECONDITION_REQUIRED, 428) \ - V(TOO_MANY_REQUESTS, 429) \ - V(REQUEST_HEADER_FIELDS_TOO_LARGE, 431) \ - V(UNAVAILABLE_FOR_LEGAL_REASONS, 451) \ - V(INTERNAL_SERVER_ERROR, 500) \ - V(NOT_IMPLEMENTED, 501) \ - V(BAD_GATEWAY, 502) \ - V(SERVICE_UNAVAILABLE, 503) \ - V(GATEWAY_TIMEOUT, 504) \ - V(HTTP_VERSION_NOT_SUPPORTED, 505) \ - V(VARIANT_ALSO_NEGOTIATES, 506) \ - V(INSUFFICIENT_STORAGE, 507) \ - V(LOOP_DETECTED, 508) \ - V(BANDWIDTH_LIMIT_EXCEEDED, 509) \ - V(NOT_EXTENDED, 510) \ - V(NETWORK_AUTHENTICATION_REQUIRED, 511) - -enum http_status_codes { -#define V(name, code) HTTP_STATUS_##name = code, - HTTP_STATUS_CODES(V) -#undef V -}; - // The Padding Strategy determines the method by which extra padding is // selected for HEADERS and DATA frames. These are configurable via the // options passed in to a Http2Session object. @@ -446,6 +257,17 @@ class Http2StreamListener : public StreamListener { void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override; }; +struct Http2HeaderTraits { + typedef Http2RcBufferPointer rcbufferpointer_t; + typedef Http2Session allocator_t; + + // HTTP/2 does not support identifying header names by token id. + // HTTP/3 will, however, so we prepare for that now. + static const char* ToHttpHeaderName(int32_t token) { return nullptr; } +}; + +using Http2Header = NgHeader; + class Http2Stream : public AsyncWrap, public StreamBase { public: @@ -476,13 +298,13 @@ class Http2Stream : public AsyncWrap, bool HasWantsWrite() const override { return true; } // Initiate a response on this stream. - int SubmitResponse(nghttp2_nv* nva, size_t len, int options); + int SubmitResponse(const Http2Headers& headers, int options); // Submit informational headers for this stream - int SubmitInfo(nghttp2_nv* nva, size_t len); + int SubmitInfo(const Http2Headers& headers); // Submit trailing headers for this stream - int SubmitTrailers(nghttp2_nv* nva, size_t len); + int SubmitTrailers(const Http2Headers& headers); void OnTrailers(); // Submit a PRIORITY frame for this stream @@ -495,8 +317,7 @@ class Http2Stream : public AsyncWrap, // Submits a PUSH_PROMISE frame with this stream as the parent. Http2Stream* SubmitPushPromise( - nghttp2_nv* nva, - size_t len, + const Http2Headers& headers, int32_t* ret, int options = 0); @@ -545,8 +366,16 @@ class Http2Stream : public AsyncWrap, bool AddHeader(nghttp2_rcbuf* name, nghttp2_rcbuf* value, uint8_t flags); - inline std::vector move_headers() { - return std::move(current_headers_); + template + void TransferHeaders(Fn&& fn) { + size_t i = 0; + for (const auto& header : current_headers_ ) + fn(header, i++); + current_headers_.clear(); + } + + size_t headers_count() const { + return current_headers_.size(); } inline nghttp2_headers_category headers_category() const { @@ -625,7 +454,7 @@ class Http2Stream : public AsyncWrap, // signalling the end of the HEADERS frame nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS; uint32_t current_headers_length_ = 0; // total number of octets - std::vector current_headers_; + std::vector current_headers_; // This keeps track of the amount of data read from the socket while the // socket was in paused mode. When `ReadStart()` is called (and not before @@ -743,8 +572,7 @@ class Http2Session : public AsyncWrap, // This only works if the session is a client session. Http2Stream* SubmitRequest( nghttp2_priority_spec* prispec, - nghttp2_nv* nva, - size_t len, + const Http2Headers& headers, int32_t* ret, int options = 0); @@ -1188,96 +1016,6 @@ class Http2Session::Http2Settings : public AsyncWrap { nghttp2_settings_entry entries_[IDX_SETTINGS_COUNT]; }; -class ExternalHeader : - public String::ExternalOneByteStringResource { - public: - explicit ExternalHeader(nghttp2_rcbuf* buf) - : buf_(buf), vec_(nghttp2_rcbuf_get_buf(buf)) { - } - - ~ExternalHeader() override { - nghttp2_rcbuf_decref(buf_); - buf_ = nullptr; - } - - const char* data() const override { - return const_cast(reinterpret_cast(vec_.base)); - } - - size_t length() const override { - return vec_.len; - } - - static inline - MaybeLocal GetInternalizedString(Environment* env, - const nghttp2_vec& vec) { - return String::NewFromOneByte(env->isolate(), - vec.base, - v8::NewStringType::kInternalized, - vec.len); - } - - template - 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]; - if (eternal.IsEmpty()) { - Local str = - GetInternalizedString(env, nghttp2_rcbuf_get_buf(buf)) - .ToLocalChecked(); - eternal.Set(env->isolate(), str); - return str; - } - return eternal.Get(env->isolate()); - } - - nghttp2_vec vec = nghttp2_rcbuf_get_buf(buf); - if (vec.len == 0) { - nghttp2_rcbuf_decref(buf); - return String::Empty(env->isolate()); - } - - 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()) - delete h_str; - - return str; - } - - private: - nghttp2_rcbuf* buf_; - nghttp2_vec vec_; -}; - -class Headers { - public: - Headers(Isolate* isolate, Local context, Local headers); - ~Headers() = default; - - nghttp2_nv* operator*() { - return reinterpret_cast(*buf_); - } - - size_t length() const { - return count_; - } - - private: - size_t count_; - MaybeStackBuffer buf_; -}; - class Origins { public: Origins(Isolate* isolate, diff --git a/src/node_http_common-inl.h b/src/node_http_common-inl.h new file mode 100644 index 00000000000000..1bc7b46d63a827 --- /dev/null +++ b/src/node_http_common-inl.h @@ -0,0 +1,181 @@ +#ifndef SRC_NODE_HTTP_COMMON_INL_H_ +#define SRC_NODE_HTTP_COMMON_INL_H_ + +#include "node_http_common.h" +#include "node.h" +#include "node_mem-inl.h" +#include "env-inl.h" +#include "v8.h" + +#include + +namespace node { + +template +NgHeaders::NgHeaders(Environment* env, v8::Local headers) { + v8::Local header_string = + headers->Get(env->context(), 0).ToLocalChecked(); + v8::Local header_count = + headers->Get(env->context(), 1).ToLocalChecked(); + CHECK(header_count->IsUint32()); + CHECK(header_string->IsString()); + count_ = header_count.As()->Value(); + int header_string_len = header_string.As()->Length(); + + if (count_ == 0) { + CHECK_EQ(header_string_len, 0); + return; + } + + buf_.AllocateSufficientStorage((alignof(nv_t) - 1) + + count_ * sizeof(nv_t) + + header_string_len); + + char* start = reinterpret_cast( + RoundUp(reinterpret_cast(*buf_), alignof(nv_t))); + char* header_contents = start + (count_ * sizeof(nv_t)); + nv_t* const nva = reinterpret_cast(start); + + CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length()); + CHECK_EQ(header_string.As()->WriteOneByte( + env->isolate(), + reinterpret_cast(header_contents), + 0, + header_string_len, + v8::String::NO_NULL_TERMINATION), + header_string_len); + + size_t n = 0; + char* p; + for (p = header_contents; p < header_contents + header_string_len; n++) { + if (n >= count_) { + static uint8_t zero = '\0'; + nva[0].name = nva[0].value = &zero; + nva[0].namelen = nva[0].valuelen = 1; + count_ = 1; + return; + } + + nva[n].flags = T::kNoneFlag; + nva[n].name = reinterpret_cast(p); + nva[n].namelen = strlen(p); + p += nva[n].namelen + 1; + nva[n].value = reinterpret_cast(p); + nva[n].valuelen = strlen(p); + p += nva[n].valuelen + 1; + } +} + +size_t GetClientMaxHeaderPairs(size_t max_header_pairs) { + static constexpr size_t min_header_pairs = 1; + return std::max(max_header_pairs, min_header_pairs); +} + +size_t GetServerMaxHeaderPairs(size_t max_header_pairs) { + static constexpr size_t min_header_pairs = 4; + return std::max(max_header_pairs, min_header_pairs); +} + +template +bool NgHeader::IsZeroLength( + NgHeader::rcbuf_t* name, + NgHeader::rcbuf_t* value) { + return IsZeroLength(-1, name, value); +} + +template +bool NgHeader::IsZeroLength( + int32_t token, + NgHeader::rcbuf_t* name, + NgHeader::rcbuf_t* value) { + + if (NgHeader::rcbufferpointer_t::IsZeroLength(value)) + return true; + + const char* header_name = T::ToHttpHeaderName(token); + return header_name != nullptr || + NgHeader::rcbufferpointer_t::IsZeroLength(name); +} + +template +NgHeader::NgHeader( + Environment* env, + NgHeader::rcbuf_t* name, + NgHeader::rcbuf_t* value, + uint8_t flags) + : NgHeader(env, -1, name, value, flags) {} + +template +NgHeader::NgHeader( + Environment* env, + int32_t token, + NgHeader::rcbuf_t* name, + NgHeader::rcbuf_t* value, + uint8_t flags) : env_(env), token_(token), flags_(flags) { + if (token == -1) { + CHECK_NOT_NULL(name); + name_.reset(name, true); // Internalizable + } + CHECK_NOT_NULL(value); + name_.reset(name, true); // Internalizable + value_.reset(value); +} + +template +NgHeader::NgHeader(NgHeader&& other) noexcept + : token_(other.token_), + name_(std::move(other.name_)), + value_(std::move(other.value_)), + flags_(other.flags_) { + other.token_ = -1; + other.flags_ = 0; + other.env_ = nullptr; +} + +template +v8::MaybeLocal NgHeader::GetName( + NgHeader::allocator_t* allocator) const { + + // Not all instances will support using token id's for header names. + // HTTP/2 specifically does not support it. + const char* header_name = T::ToHttpHeaderName(token_); + + // If header_name is not nullptr, then it is a known header with + // a statically defined name. We can safely internalize it here. + if (header_name != nullptr) { + auto& static_str_map = env_->isolate_data()->http_static_strs; + v8::Eternal eternal = static_str_map[header_name]; + if (eternal.IsEmpty()) { + v8::Local str = OneByteString(env_->isolate(), header_name); + eternal.Set(env_->isolate(), str); + return str; + } + return eternal.Get(env_->isolate()); + } + return rcbufferpointer_t::External::New(allocator, name_); +} + +template +v8::MaybeLocal NgHeader::GetValue( + NgHeader::allocator_t* allocator) const { + return rcbufferpointer_t::External::New(allocator, value_); +} + +template +std::string NgHeader::name() const { + return name_.str(); +} + +template +std::string NgHeader::value() const { + return value_.str(); +} + +template +size_t NgHeader::length() const { + return name_.len() + value_.len(); +} + +} // namespace node + +#endif // SRC_NODE_HTTP_COMMON_INL_H_ diff --git a/src/node_http_common.h b/src/node_http_common.h new file mode 100644 index 00000000000000..d43418ba6f217c --- /dev/null +++ b/src/node_http_common.h @@ -0,0 +1,527 @@ +#ifndef SRC_NODE_HTTP_COMMON_H_ +#define SRC_NODE_HTTP_COMMON_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "v8.h" +#include "node_mem.h" + +#include + +namespace node { + +class Environment; + +#define MAX_MAX_HEADER_LIST_SIZE 16777215u +#define DEFAULT_MAX_HEADER_LIST_PAIRS 128u +#define DEFAULT_MAX_HEADER_LENGTH 8192 + +#define HTTP_SPECIAL_HEADERS(V) \ + V(STATUS, ":status") \ + V(METHOD, ":method") \ + V(AUTHORITY, ":authority") \ + V(SCHEME, ":scheme") \ + V(PATH, ":path") \ + V(PROTOCOL, ":protocol") + +#define HTTP_REGULAR_HEADERS(V) \ + V(ACCEPT_ENCODING, "accept-encoding") \ + V(ACCEPT_LANGUAGE, "accept-language") \ + V(ACCEPT_RANGES, "accept-ranges") \ + V(ACCEPT, "accept") \ + V(ACCESS_CONTROL_ALLOW_CREDENTIALS, "access-control-allow-credentials") \ + V(ACCESS_CONTROL_ALLOW_HEADERS, "access-control-allow-headers") \ + V(ACCESS_CONTROL_ALLOW_METHODS, "access-control-allow-methods") \ + V(ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin") \ + V(ACCESS_CONTROL_EXPOSE_HEADERS, "access-control-expose-headers") \ + V(ACCESS_CONTROL_REQUEST_HEADERS, "access-control-request-headers") \ + V(ACCESS_CONTROL_REQUEST_METHOD, "access-control-request-method") \ + V(AGE, "age") \ + V(AUTHORIZATION, "authorization") \ + V(CACHE_CONTROL, "cache-control") \ + V(CONNECTION, "connection") \ + V(CONTENT_DISPOSITION, "content-disposition") \ + V(CONTENT_ENCODING, "content-encoding") \ + V(CONTENT_LENGTH, "content-length") \ + V(CONTENT_TYPE, "content-type") \ + V(COOKIE, "cookie") \ + V(DATE, "date") \ + V(ETAG, "etag") \ + V(FORWARDED, "forwarded") \ + V(HOST, "host") \ + V(IF_MODIFIED_SINCE, "if-modified-since") \ + V(IF_NONE_MATCH, "if-none-match") \ + V(IF_RANGE, "if-range") \ + V(LAST_MODIFIED, "last-modified") \ + V(LINK, "link") \ + V(LOCATION, "location") \ + V(RANGE, "range") \ + V(REFERER, "referer") \ + V(SERVER, "server") \ + V(SET_COOKIE, "set-cookie") \ + V(STRICT_TRANSPORT_SECURITY, "strict-transport-security") \ + V(TRANSFER_ENCODING, "transfer-encoding") \ + V(TE, "te") \ + V(UPGRADE_INSECURE_REQUESTS, "upgrade-insecure-requests") \ + V(UPGRADE, "upgrade") \ + V(USER_AGENT, "user-agent") \ + V(VARY, "vary") \ + V(X_CONTENT_TYPE_OPTIONS, "x-content-type-options") \ + V(X_FRAME_OPTIONS, "x-frame-options") \ + V(KEEP_ALIVE, "keep-alive") \ + V(PROXY_CONNECTION, "proxy-connection") \ + V(X_XSS_PROTECTION, "x-xss-protection") \ + V(ALT_SVC, "alt-svc") \ + V(CONTENT_SECURITY_POLICY, "content-security-policy") \ + V(EARLY_DATA, "early-data") \ + V(EXPECT_CT, "expect-ct") \ + V(ORIGIN, "origin") \ + V(PURPOSE, "purpose") \ + V(TIMING_ALLOW_ORIGIN, "timing-allow-origin") \ + V(X_FORWARDED_FOR, "x-forwarded-for") + +#define HTTP_ADDITIONAL_HEADERS(V) \ + V(ACCEPT_CHARSET, "accept-charset") \ + V(ACCESS_CONTROL_MAX_AGE, "access-control-max-age") \ + V(ALLOW, "allow") \ + V(CONTENT_LANGUAGE, "content-language") \ + V(CONTENT_LOCATION, "content-location") \ + V(CONTENT_MD5, "content-md5") \ + V(CONTENT_RANGE, "content-range") \ + V(DNT, "dnt") \ + V(EXPECT, "expect") \ + V(EXPIRES, "expires") \ + V(FROM, "from") \ + V(IF_MATCH, "if-match") \ + V(IF_UNMODIFIED_SINCE, "if-unmodified-since") \ + V(MAX_FORWARDS, "max-forwards") \ + V(PREFER, "prefer") \ + V(PROXY_AUTHENTICATE, "proxy-authenticate") \ + V(PROXY_AUTHORIZATION, "proxy-authorization") \ + V(REFRESH, "refresh") \ + V(RETRY_AFTER, "retry-after") \ + V(TRAILER, "trailer") \ + V(TK, "tk") \ + V(VIA, "via") \ + V(WARNING, "warning") \ + V(WWW_AUTHENTICATE, "www-authenticate") \ + V(HTTP2_SETTINGS, "http2-settings") + +// Special and regular headers are handled specifically by the HTTP/2 (and +// later HTTP/3) implementation. +#define HTTP_KNOWN_HEADERS(V) \ + HTTP_SPECIAL_HEADERS(V) \ + HTTP_REGULAR_HEADERS(V) \ + HTTP_ADDITIONAL_HEADERS(V) + +enum http_known_headers { + HTTP_KNOWN_HEADER_MIN, +#define V(name, value) HTTP_HEADER_##name, + HTTP_KNOWN_HEADERS(V) +#undef V + HTTP_KNOWN_HEADER_MAX +}; + +#define HTTP_STATUS_CODES(V) \ + V(CONTINUE, 100) \ + V(SWITCHING_PROTOCOLS, 101) \ + V(PROCESSING, 102) \ + V(EARLY_HINTS, 103) \ + V(OK, 200) \ + V(CREATED, 201) \ + V(ACCEPTED, 202) \ + V(NON_AUTHORITATIVE_INFORMATION, 203) \ + V(NO_CONTENT, 204) \ + V(RESET_CONTENT, 205) \ + V(PARTIAL_CONTENT, 206) \ + V(MULTI_STATUS, 207) \ + V(ALREADY_REPORTED, 208) \ + V(IM_USED, 226) \ + V(MULTIPLE_CHOICES, 300) \ + V(MOVED_PERMANENTLY, 301) \ + V(FOUND, 302) \ + V(SEE_OTHER, 303) \ + V(NOT_MODIFIED, 304) \ + V(USE_PROXY, 305) \ + V(TEMPORARY_REDIRECT, 307) \ + V(PERMANENT_REDIRECT, 308) \ + V(BAD_REQUEST, 400) \ + V(UNAUTHORIZED, 401) \ + V(PAYMENT_REQUIRED, 402) \ + V(FORBIDDEN, 403) \ + V(NOT_FOUND, 404) \ + V(METHOD_NOT_ALLOWED, 405) \ + V(NOT_ACCEPTABLE, 406) \ + V(PROXY_AUTHENTICATION_REQUIRED, 407) \ + V(REQUEST_TIMEOUT, 408) \ + V(CONFLICT, 409) \ + V(GONE, 410) \ + V(LENGTH_REQUIRED, 411) \ + V(PRECONDITION_FAILED, 412) \ + V(PAYLOAD_TOO_LARGE, 413) \ + V(URI_TOO_LONG, 414) \ + V(UNSUPPORTED_MEDIA_TYPE, 415) \ + V(RANGE_NOT_SATISFIABLE, 416) \ + V(EXPECTATION_FAILED, 417) \ + V(TEAPOT, 418) \ + V(MISDIRECTED_REQUEST, 421) \ + V(UNPROCESSABLE_ENTITY, 422) \ + V(LOCKED, 423) \ + V(FAILED_DEPENDENCY, 424) \ + V(TOO_EARLY, 425) \ + V(UPGRADE_REQUIRED, 426) \ + V(PRECONDITION_REQUIRED, 428) \ + V(TOO_MANY_REQUESTS, 429) \ + V(REQUEST_HEADER_FIELDS_TOO_LARGE, 431) \ + V(UNAVAILABLE_FOR_LEGAL_REASONS, 451) \ + V(INTERNAL_SERVER_ERROR, 500) \ + V(NOT_IMPLEMENTED, 501) \ + V(BAD_GATEWAY, 502) \ + V(SERVICE_UNAVAILABLE, 503) \ + V(GATEWAY_TIMEOUT, 504) \ + V(HTTP_VERSION_NOT_SUPPORTED, 505) \ + V(VARIANT_ALSO_NEGOTIATES, 506) \ + V(INSUFFICIENT_STORAGE, 507) \ + V(LOOP_DETECTED, 508) \ + V(BANDWIDTH_LIMIT_EXCEEDED, 509) \ + V(NOT_EXTENDED, 510) \ + V(NETWORK_AUTHENTICATION_REQUIRED, 511) + +enum http_status_codes { +#define V(name, code) HTTP_STATUS_##name = code, + HTTP_STATUS_CODES(V) +#undef V +}; + +// Unlike the HTTP/1 implementation, the HTTP/2 implementation is not limited +// to a fixed number of known supported HTTP methods. These constants, therefore +// are provided strictly as a convenience to users and are exposed via the +// require('http2').constants object. +#define HTTP_KNOWN_METHODS(V) \ + V(ACL, "ACL") \ + V(BASELINE_CONTROL, "BASELINE-CONTROL") \ + V(BIND, "BIND") \ + V(CHECKIN, "CHECKIN") \ + V(CHECKOUT, "CHECKOUT") \ + V(CONNECT, "CONNECT") \ + V(COPY, "COPY") \ + V(DELETE, "DELETE") \ + V(GET, "GET") \ + V(HEAD, "HEAD") \ + V(LABEL, "LABEL") \ + V(LINK, "LINK") \ + V(LOCK, "LOCK") \ + V(MERGE, "MERGE") \ + V(MKACTIVITY, "MKACTIVITY") \ + V(MKCALENDAR, "MKCALENDAR") \ + V(MKCOL, "MKCOL") \ + V(MKREDIRECTREF, "MKREDIRECTREF") \ + V(MKWORKSPACE, "MKWORKSPACE") \ + V(MOVE, "MOVE") \ + V(OPTIONS, "OPTIONS") \ + V(ORDERPATCH, "ORDERPATCH") \ + V(PATCH, "PATCH") \ + V(POST, "POST") \ + V(PRI, "PRI") \ + V(PROPFIND, "PROPFIND") \ + V(PROPPATCH, "PROPPATCH") \ + V(PUT, "PUT") \ + V(REBIND, "REBIND") \ + V(REPORT, "REPORT") \ + V(SEARCH, "SEARCH") \ + V(TRACE, "TRACE") \ + V(UNBIND, "UNBIND") \ + V(UNCHECKOUT, "UNCHECKOUT") \ + V(UNLINK, "UNLINK") \ + V(UNLOCK, "UNLOCK") \ + V(UPDATE, "UPDATE") \ + V(UPDATEREDIRECTREF, "UPDATEREDIRECTREF") \ + V(VERSION_CONTROL, "VERSION-CONTROL") + +// NgHeaders takes as input a block of headers provided by the +// JavaScript side (see http2's mapToHeaders function) and +// converts it into a array of ng header structs. This is done +// generically to handle both http/2 and (in the future) http/3, +// which use nearly identical structs. The template parameter +// takes a Traits type that defines the ng header struct and +// the kNoneFlag value. See Http2HeaderTraits in node_http2.h +// for an example. +template +class NgHeaders { + public: + typedef typename T::nv_t nv_t; + inline NgHeaders(Environment* env, v8::Local headers); + ~NgHeaders() = default; + + const nv_t* operator*() const { + return reinterpret_cast(*buf_); + } + + const nv_t* data() const { + return reinterpret_cast(*buf_); + } + + size_t length() const { + return count_; + } + + private: + size_t count_; + MaybeStackBuffer buf_; +}; + +// The ng libraries (nghttp2 and nghttp3) each use nearly identical +// reference counted structures for retaining header name and value +// information in memory until the application is done with it. +// The NgRcBufPointer is an intelligent pointer capable of working +// with either type, handling the ref counting increment and +// decrement as appropriate. The Template takes a single Traits +// type that provides the rc buffer and vec type, as well as +// implementations for multiple static functions. +// See Http2RcBufferPointerTraits in node_http2.h for an example. +template +class NgRcBufPointer : public MemoryRetainer { + public: + typedef typename T::rcbuf_t rcbuf_t; + typedef typename T::vector_t vector_t; + + NgRcBufPointer() = default; + + explicit NgRcBufPointer(rcbuf_t* buf) { + reset(buf); + } + + template + NgRcBufPointer(const NgRcBufPointer& other) { + reset(other.get()); + } + + NgRcBufPointer(const NgRcBufPointer& other) { + reset(other.get()); + } + + template + NgRcBufPointer& operator=(const NgRcBufPointer& other) { + if (other.get() == get()) return *this; + this->~NgRcBufPointer(); + return *new (this) NgRcBufPointer(other); + } + + NgRcBufPointer& operator=(const NgRcBufPointer& other) { + if (other.get() == get()) return *this; + this->~NgRcBufPointer(); + return *new (this) NgRcBufPointer(other); + } + + NgRcBufPointer(NgRcBufPointer&& other) { + this->~NgRcBufPointer(); + buf_ = other.buf_; + other.buf_ = nullptr; + } + + NgRcBufPointer& operator=(NgRcBufPointer&& other) { + this->~NgRcBufPointer(); + return *new (this) NgRcBufPointer(std::move(other)); + } + + ~NgRcBufPointer() { + reset(); + } + + // Returns the underlying ngvec for this rcbuf + uint8_t* data() const { + vector_t v = T::get_vec(buf_); + return v.base; + } + + size_t len() const { + vector_t v = T::get_vec(buf_); + return v.len; + } + + std::string str() const { + return std::string(reinterpret_cast(data()), len()); + } + + void reset(rcbuf_t* ptr = nullptr, bool internalizable = false) { + if (buf_ == ptr) + return; + + if (buf_ != nullptr) + T::dec(buf_); + + buf_ = ptr; + + if (ptr != nullptr) { + T::inc(ptr); + internalizable_ = internalizable; + } + } + + rcbuf_t* get() const { return buf_; } + rcbuf_t& operator*() const { return *get(); } + rcbuf_t* operator->() const { return buf_; } + operator bool() const { return buf_ != nullptr; } + bool IsStatic() const { return T::is_static(buf_) != 0; } + void SetInternalizable() { internalizable_ = true; } + bool IsInternalizable() const { return internalizable_; } + + static inline bool IsZeroLength(rcbuf_t* buf) { + if (buf == nullptr) + return true; + vector_t b = T::get_vec(buf); + return b.len == 0; + } + + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackFieldWithSize("buf", len(), "buf"); + } + + SET_MEMORY_INFO_NAME(NgRcBufPointer) + SET_SELF_SIZE(NgRcBufPointer) + + class External : public v8::String::ExternalOneByteStringResource { + public: + explicit External(const NgRcBufPointer& ptr) : ptr_(ptr) {} + + const char* data() const override { + return const_cast(reinterpret_cast(ptr_.data())); + } + + size_t length() const override { + return ptr_.len(); + } + + static inline + v8::MaybeLocal GetInternalizedString( + Environment* env, + const NgRcBufPointer& ptr) { + return v8::String::NewFromOneByte( + env->isolate(), + ptr.data(), + v8::NewStringType::kInternalized, + ptr.len()); + } + + template + static v8::MaybeLocal New( + Allocator* allocator, + NgRcBufPointer ptr) { + Environment* env = allocator->env(); + if (ptr.IsStatic()) { + auto& static_str_map = env->isolate_data()->http_static_strs; + const char* header_name = reinterpret_cast(ptr.data()); + v8::Eternal& eternal = static_str_map[header_name]; + if (eternal.IsEmpty()) { + v8::Local str = + GetInternalizedString(env, ptr).ToLocalChecked(); + eternal.Set(env->isolate(), str); + return str; + } + return eternal.Get(env->isolate()); + } + + size_t len = ptr.len(); + + if (len == 0) { + ptr.reset(); + return v8::String::Empty(env->isolate()); + } + + if (ptr.IsInternalizable() && len < 64) { + v8::MaybeLocal ret = GetInternalizedString(env, ptr); + ptr.reset(); + return ret; + } + + allocator->StopTrackingMemory(ptr.get()); + External* h_str = new External(std::move(ptr)); + v8::MaybeLocal str = + v8::String::NewExternalOneByte(env->isolate(), h_str); + if (str.IsEmpty()) + delete h_str; + + return str; + } + + private: + NgRcBufPointer ptr_; + }; + + private: + rcbuf_t* buf_ = nullptr; + bool internalizable_ = false; +}; + +// The ng libraries use nearly identical structs to represent +// received http headers. The NgHeader class wraps those in a +// consistent way and allows converting the name and value to +// v8 strings. The template is given a Traits type that provides +// the NgRcBufPointer type, the NgLibMemoryManager to use for +// memory tracking, and implementation of static utility functions. +// See Http2HeaderTraits in node_http2.h for an example. +template +class NgHeader : public MemoryRetainer { + public: + typedef typename T::rcbufferpointer_t rcbufferpointer_t; + typedef typename T::rcbufferpointer_t::rcbuf_t rcbuf_t; + typedef typename T::allocator_t allocator_t; + + inline static bool IsZeroLength(rcbuf_t* name, rcbuf_t* value); + inline static bool IsZeroLength(int32_t token, rcbuf_t* name, rcbuf_t* value); + inline NgHeader( + Environment* env, + rcbuf_t* name, + rcbuf_t* value, + uint8_t flags); + inline NgHeader( + Environment* env, + int32_t token, + rcbuf_t* name, + rcbuf_t* value, + uint8_t flags); + inline NgHeader(NgHeader&& other) noexcept; + + // Calling GetName and GetValue will have the effect of releasing + // control over the reference counted buffer from this NgHeader + // object to the v8 string. Once the v8 string is garbage collected, + // the reference counter will be decremented. + + inline v8::MaybeLocal GetName(allocator_t* allocator) const; + inline v8::MaybeLocal GetValue(allocator_t* allocator) const; + + inline std::string name() const; + inline std::string value() const; + inline size_t length() const; + + void MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("name", name_); + tracker->TrackField("value", value_); + } + + SET_MEMORY_INFO_NAME(NgHeader) + SET_SELF_SIZE(NgHeader) + + std::string ToString() const { + std::string ret = name(); + ret += " = "; + ret += value(); + return ret; + } + + private: + Environment* env_; + rcbufferpointer_t name_; + rcbufferpointer_t value_; + int32_t token_ = -1; + uint8_t flags_ = 0; +}; + +inline size_t GetServerMaxHeaderPairs(size_t max_header_pairs); +inline size_t GetClientMaxHeaderPairs(size_t max_header_pairs); + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_HTTP_COMMON_H_ diff --git a/test/parallel/test-http2-binding.js b/test/parallel/test-http2-binding.js index 81f49691e3ecff..e81a58dfe8a5c1 100644 --- a/test/parallel/test-http2-binding.js +++ b/test/parallel/test-http2-binding.js @@ -170,7 +170,16 @@ const expectedHeaderNames = { HTTP2_HEADER_CONTENT_MD5: 'content-md5', HTTP2_HEADER_TE: 'te', HTTP2_HEADER_UPGRADE: 'upgrade', - HTTP2_HEADER_HTTP2_SETTINGS: 'http2-settings' + HTTP2_HEADER_HTTP2_SETTINGS: 'http2-settings', + HTTP2_HEADER_X_XSS_PROTECTION: 'x-xss-protection', + HTTP2_HEADER_ALT_SVC: 'alt-svc', + HTTP2_HEADER_CONTENT_SECURITY_POLICY: 'content-security-policy', + HTTP2_HEADER_EARLY_DATA: 'early-data', + HTTP2_HEADER_EXPECT_CT: 'expect-ct', + HTTP2_HEADER_ORIGIN: 'origin', + HTTP2_HEADER_PURPOSE: 'purpose', + HTTP2_HEADER_TIMING_ALLOW_ORIGIN: 'timing-allow-origin', + HTTP2_HEADER_X_FORWARDED_FOR: 'x-forwarded-for', }; const expectedNGConstants = {