diff --git a/node.gyp b/node.gyp index 790fd32e4db41b..b8731b21d5fb6e 100644 --- a/node.gyp +++ b/node.gyp @@ -665,6 +665,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 fc25f7a5b64411..f48f74ca700b27 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 075d1b505e5328..835e15587ff64d 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" @@ -356,66 +357,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, @@ -538,8 +479,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(); @@ -1249,34 +1190,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); } @@ -1761,15 +1698,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); @@ -1918,13 +1860,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"); @@ -2026,7 +1962,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"); @@ -2037,21 +1973,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; } @@ -2068,19 +2013,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; @@ -2129,15 +2078,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) { @@ -2221,12 +2174,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) || @@ -2234,13 +2187,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; @@ -2487,21 +2439,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)); @@ -2586,18 +2537,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"); } @@ -2605,31 +2552,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 @@ -2650,21 +2590,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); @@ -2940,12 +2877,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 b468aac175d9e1..97560c8a5bfaca 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); @@ -1189,96 +1017,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 = {