diff options
Diffstat (limited to 'src/node_http2.cc')
-rw-r--r-- | src/node_http2.cc | 2393 |
1 files changed, 1623 insertions, 770 deletions
diff --git a/src/node_http2.cc b/src/node_http2.cc index bdf0d31b47..b439ae588a 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -14,6 +14,8 @@ using v8::Context; using v8::Float64Array; using v8::Function; using v8::Integer; +using v8::Number; +using v8::ObjectTemplate; using v8::String; using v8::Uint32; using v8::Uint32Array; @@ -21,10 +23,11 @@ using v8::Undefined; namespace http2 { -Nghttp2Session::Callbacks Nghttp2Session::callback_struct_saved[2] = { +const Http2Session::Callbacks Http2Session::callback_struct_saved[2] = { Callbacks(false), Callbacks(true)}; + Http2Options::Http2Options(Environment* env) { nghttp2_option_new(&options_); @@ -70,8 +73,13 @@ Http2Options::Http2Options(Environment* env) { if (flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS)) { SetMaxHeaderPairs(buffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS]); } + + if (flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS)) { + SetMaxOutstandingPings(buffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS]); + } } + Http2Settings::Http2Settings(Environment* env) : env_(env) { entries_.AllocateSufficientStorage(IDX_SETTINGS_COUNT); AliasedBuffer<uint32_t, v8::Uint32Array>& buffer = @@ -82,7 +90,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) { uint32_t val = buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]; - DEBUG_HTTP2("Setting header table size: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting header table size: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; entries_[n].value = val; n++; @@ -90,7 +98,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) { uint32_t val = buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]; - DEBUG_HTTP2("Setting max concurrent streams: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting max concurrent streams: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; entries_[n].value = val; n++; @@ -98,7 +106,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) { uint32_t val = buffer[IDX_SETTINGS_MAX_FRAME_SIZE]; - DEBUG_HTTP2("Setting max frame size: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting max frame size: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_FRAME_SIZE; entries_[n].value = val; n++; @@ -106,7 +114,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) { uint32_t val = buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]; - DEBUG_HTTP2("Setting initial window size: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting initial window size: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; entries_[n].value = val; n++; @@ -114,7 +122,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) { uint32_t val = buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]; - DEBUG_HTTP2("Setting max header list size: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting max header list size: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE; entries_[n].value = val; n++; @@ -122,7 +130,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) { uint32_t val = buffer[IDX_SETTINGS_ENABLE_PUSH]; - DEBUG_HTTP2("Setting enable push: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting enable push: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; entries_[n].value = val; n++; @@ -131,6 +139,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { count_ = n; } + inline Local<Value> Http2Settings::Pack() { const size_t len = count_ * 6; Local<Value> buf = Buffer::New(env_, len).ToLocalChecked(); @@ -144,6 +153,7 @@ inline Local<Value> Http2Settings::Pack() { return Undefined(env_->isolate()); } + inline void Http2Settings::Update(Environment* env, Http2Session* session, get_setting fn) { @@ -186,6 +196,7 @@ inline void Http2Settings::RefreshDefaults(Environment* env) { (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE); } + Http2Priority::Http2Priority(Environment* env, Local<Value> parent, Local<Value> weight, @@ -199,27 +210,162 @@ Http2Priority::Http2Priority(Environment* env, nghttp2_priority_spec_init(&spec, parent_, weight_, exclusive_ ? 1 : 0); } + +inline const char* Http2Session::TypeName() { + switch (session_type_) { + case NGHTTP2_SESSION_SERVER: return "server"; + case NGHTTP2_SESSION_CLIENT: return "client"; + default: + // This should never happen + ABORT(); + } +} + + +Headers::Headers(Isolate* isolate, + Local<Context> context, + Local<Array> headers) { + Local<Value> header_string = headers->Get(context, 0).ToLocalChecked(); + Local<Value> header_count = headers->Get(context, 1).ToLocalChecked(); + count_ = header_count.As<Uint32>()->Value(); + int header_string_len = header_string.As<String>()->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<char*>( + ROUND_UP(reinterpret_cast<uintptr_t>(*buf_), alignof(nghttp2_nv))); + char* header_contents = start + (count_ * sizeof(nghttp2_nv)); + nghttp2_nv* const nva = reinterpret_cast<nghttp2_nv*>(start); + + CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length()); + CHECK_EQ(header_string.As<String>() + ->WriteOneByte(reinterpret_cast<uint8_t*>(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<uint8_t*>(p); + nva[n].namelen = strlen(p); + p += nva[n].namelen + 1; + nva[n].value = reinterpret_cast<uint8_t*>(p); + nva[n].valuelen = strlen(p); + p += nva[n].valuelen + 1; + } +} + + +Http2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) { + CHECK_EQ(nghttp2_session_callbacks_new(&callbacks), 0); + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, OnBeginHeadersCallback); + nghttp2_session_callbacks_set_on_header_callback2( + callbacks, OnHeaderCallback); + nghttp2_session_callbacks_set_on_frame_recv_callback( + callbacks, OnFrameReceive); + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, OnStreamClose); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, OnDataChunkReceived); + nghttp2_session_callbacks_set_on_frame_not_send_callback( + callbacks, OnFrameNotSent); + nghttp2_session_callbacks_set_on_invalid_header_callback2( + callbacks, OnInvalidHeader); + nghttp2_session_callbacks_set_error_callback( + callbacks, OnNghttpError); + + if (kHasGetPaddingCallback) { + nghttp2_session_callbacks_set_select_padding_callback( + callbacks, OnSelectPadding); + } +} + + +Http2Session::Callbacks::~Callbacks() { + nghttp2_session_callbacks_del(callbacks); +} + + Http2Session::Http2Session(Environment* env, Local<Object> wrap, nghttp2_session_type type) : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_HTTP2SESSION), - StreamBase(env) { + session_type_(type) { MakeWeak<Http2Session>(this); Http2Options opts(env); + int32_t maxHeaderPairs = opts.GetMaxHeaderPairs(); + max_header_pairs_ = + type == NGHTTP2_SESSION_SERVER + ? std::max(maxHeaderPairs, 4) // minimum # of request headers + : std::max(maxHeaderPairs, 1); // minimum # of response headers + + max_outstanding_pings_ = opts.GetMaxOutstandingPings(); + padding_strategy_ = opts.GetPaddingStrategy(); - int32_t maxHeaderPairs = opts.GetMaxHeaderPairs(); - maxHeaderPairs = type == NGHTTP2_SESSION_SERVER ? - std::max(maxHeaderPairs, 4) : std::max(maxHeaderPairs, 1); - Init(type, *opts, nullptr, maxHeaderPairs); + bool hasGetPaddingCallback = + padding_strategy_ == PADDING_STRATEGY_MAX || + padding_strategy_ == PADDING_STRATEGY_CALLBACK; + + nghttp2_session_callbacks* callbacks + = callback_struct_saved[hasGetPaddingCallback ? 1 : 0].callbacks; + + auto fn = type == NGHTTP2_SESSION_SERVER ? + nghttp2_session_server_new2 : + nghttp2_session_client_new2; + + // This should fail only if the system is out of memory, which + // is going to cause lots of other problems anyway, or if any + // of the options are out of acceptable range, which we should + // be catching before it gets this far. Either way, crash if this + // fails. + CHECK_EQ(fn(&session_, callbacks, this, *opts), 0); + + Start(); +} + + +Http2Session::~Http2Session() { + CHECK(persistent().IsEmpty()); + Close(); +} - // For every node::Http2Session instance, there is a uv_prepare_t handle - // whose callback is triggered on every tick of the event loop. When - // run, nghttp2 is prompted to send any queued data it may have stored. +// For every node::Http2Session instance, there is a uv_prepare_t handle +// whose callback is triggered on every tick of the event loop. When +// run, nghttp2 is prompted to send any queued data it may have stored. +// TODO(jasnell): Currently, this creates one uv_prepare_t per Http2Session, +// we should investigate to see if it's faster to create a +// single uv_prepare_t for all Http2Sessions, then iterate +// over each. +void Http2Session::Start() { prep_ = new uv_prepare_t(); - uv_prepare_init(env->event_loop(), prep_); + uv_prepare_init(env()->event_loop(), prep_); prep_->data = static_cast<void*>(this); uv_prepare_start(prep_, [](uv_prepare_t* t) { Http2Session* session = static_cast<Http2Session*>(t->data); @@ -233,39 +379,69 @@ Http2Session::Http2Session(Environment* env, }); } -Http2Session::~Http2Session() { - CHECK(persistent().IsEmpty()); - Close(); +// Stop the uv_prep_t from further activity, destroy the handle +void Http2Session::Stop() { + DEBUG_HTTP2SESSION(this, "stopping uv_prep_t handle"); + CHECK_EQ(uv_prepare_stop(prep_), 0); + auto prep_close = [](uv_handle_t* handle) { + delete reinterpret_cast<uv_prepare_t*>(handle); + }; + uv_close(reinterpret_cast<uv_handle_t*>(prep_), prep_close); + prep_ = nullptr; } + void Http2Session::Close() { + DEBUG_HTTP2SESSION(this, "closing session"); if (!object().IsEmpty()) ClearWrap(object()); persistent().Reset(); - this->Nghttp2Session::Close(); - // Stop the loop - CHECK_EQ(uv_prepare_stop(prep_), 0); - auto prep_close = [](uv_handle_t* handle) { - delete reinterpret_cast<uv_prepare_t*>(handle); - }; - uv_close(reinterpret_cast<uv_handle_t*>(prep_), prep_close); - prep_ = nullptr; + if (session_ == nullptr) + return; + + CHECK_EQ(nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR), 0); + nghttp2_session_del(session_); + session_ = nullptr; + + while (!outstanding_pings_.empty()) { + Http2Session::Http2Ping* ping = PopPing(); + ping->Done(false); + } + + Stop(); +} + + +inline Http2Stream* Http2Session::FindStream(int32_t id) { + auto s = streams_.find(id); + return s != streams_.end() ? s->second : nullptr; +} + + +inline void Http2Session::AddStream(Http2Stream* stream) { + streams_[stream->id()] = stream; +} + + +inline void Http2Session::RemoveStream(int32_t id) { + streams_.erase(id); } -ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen, - size_t maxPayloadLen) { - DEBUG_HTTP2("Http2Session: using max frame size padding\n"); + +inline ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen, + size_t maxPayloadLen) { + DEBUG_HTTP2SESSION2(this, "using max frame size padding: %d", maxPayloadLen); return maxPayloadLen; } -ssize_t Http2Session::OnCallbackPadding(size_t frameLen, - size_t maxPayloadLen) { - DEBUG_HTTP2("Http2Session: using callback padding\n"); - Isolate* isolate = env()->isolate(); - Local<Context> context = env()->context(); +inline ssize_t Http2Session::OnCallbackPadding(size_t frameLen, + size_t maxPayloadLen) { + DEBUG_HTTP2SESSION(this, "using callback to determine padding"); + Isolate* isolate = env()->isolate(); HandleScope handle_scope(isolate); + Local<Context> context = env()->context(); Context::Scope context_scope(context); #if defined(DEBUG) && DEBUG @@ -281,26 +457,1141 @@ ssize_t Http2Session::OnCallbackPadding(size_t frameLen, uint32_t retval = buffer[PADDING_BUF_RETURN_VALUE]; retval = std::min(retval, static_cast<uint32_t>(maxPayloadLen)); retval = std::max(retval, static_cast<uint32_t>(frameLen)); + DEBUG_HTTP2SESSION2(this, "using padding size %d", retval); return retval; } -void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - nghttp2_session* s = session->session(); - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: setting next stream id to %d\n", id); - nghttp2_session_set_next_stream_id(s, id); + +// Submits a graceful shutdown notice to nghttp +// See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html +inline void Http2Session::SubmitShutdownNotice() { + // Only an HTTP2 Server is permitted to send a shutdown notice + if (session_type_ == NGHTTP2_SESSION_CLIENT) + return; + DEBUG_HTTP2SESSION(this, "sending shutdown notice"); + // The only situation where this should fail is if the system is + // out of memory, which will cause other problems. Go ahead and crash + // in that case. + CHECK_EQ(nghttp2_submit_shutdown_notice(session_), 0); +} + + +// Note: This *must* send a SETTINGS frame even if niv == 0 +inline void Http2Session::Settings(const nghttp2_settings_entry iv[], + size_t niv) { + DEBUG_HTTP2SESSION2(this, "submitting %d settings", niv); + // This will fail either if the system is out of memory, or if the settings + // values are not within the appropriate range. We should be catching the + // latter before it gets this far so crash in either case. + CHECK_EQ(nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv), 0); +} + + +// Write data received from the i/o stream to the underlying nghttp2_session. +inline ssize_t Http2Session::Write(const uv_buf_t* bufs, size_t nbufs) { + size_t total = 0; + // Note that nghttp2_session_mem_recv is a synchronous operation that + // will trigger a number of other callbacks. Those will, in turn have + // multiple side effects. + for (size_t n = 0; n < nbufs; n++) { + ssize_t ret = + nghttp2_session_mem_recv(session_, + reinterpret_cast<uint8_t*>(bufs[n].base), + bufs[n].len); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + + if (ret < 0) + return ret; + + total += ret; + } + // Send any data that was queued up while processing the received data. + SendPendingData(); + return total; +} + + +inline int32_t GetFrameID(const nghttp2_frame* frame) { + // If this is a push promise, we want to grab the id of the promised stream + return (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? + frame->push_promise.promised_stream_id : + frame->hd.stream_id; +} + + +inline int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle, + const nghttp2_frame* frame, + void* user_data) { + Http2Session* session = static_cast<Http2Session*>(user_data); + int32_t id = GetFrameID(frame); + DEBUG_HTTP2SESSION2(session, "beginning headers for stream %d", id); + + Http2Stream* stream = session->FindStream(id); + if (stream == nullptr) { + new Http2Stream(session, id, frame->headers.cat); + } else { + stream->StartHeaders(frame->headers.cat); + } + return 0; +} + + +inline int Http2Session::OnHeaderCallback(nghttp2_session* handle, + const nghttp2_frame* frame, + nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags, + void* user_data) { + Http2Session* session = static_cast<Http2Session*>(user_data); + int32_t id = GetFrameID(frame); + Http2Stream* stream = session->FindStream(id); + if (!stream->AddHeader(name, value, flags)) { + // This will only happen if the connected peer sends us more + // than the allowed number of header items at any given time + stream->SubmitRstStream(NGHTTP2_ENHANCE_YOUR_CALM); + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + return 0; +} + + +inline int Http2Session::OnFrameReceive(nghttp2_session* handle, + const nghttp2_frame* frame, + void* user_data) { + Http2Session* session = static_cast<Http2Session*>(user_data); + DEBUG_HTTP2SESSION2(session, "complete frame received: type: %d", + frame->hd.type); + switch (frame->hd.type) { + case NGHTTP2_DATA: + session->HandleDataFrame(frame); + break; + case NGHTTP2_PUSH_PROMISE: + // Intentional fall-through, handled just like headers frames + case NGHTTP2_HEADERS: + session->HandleHeadersFrame(frame); + break; + case NGHTTP2_SETTINGS: + session->HandleSettingsFrame(frame); + break; + case NGHTTP2_PRIORITY: + session->HandlePriorityFrame(frame); + break; + case NGHTTP2_GOAWAY: + session->HandleGoawayFrame(frame); + break; + case NGHTTP2_PING: + session->HandlePingFrame(frame); + default: + break; + } + return 0; +} + + +inline int Http2Session::OnFrameNotSent(nghttp2_session* handle, + const nghttp2_frame* frame, + int error_code, + void* user_data) { + Http2Session* session = static_cast<Http2Session*>(user_data); + Environment* env = session->env(); + DEBUG_HTTP2SESSION2(session, "frame type %d was not sent, code: %d", + frame->hd.type, error_code); + // Do not report if the frame was not sent due to the session closing + if (error_code != NGHTTP2_ERR_SESSION_CLOSING && + error_code != NGHTTP2_ERR_STREAM_CLOSED && + error_code != NGHTTP2_ERR_STREAM_CLOSING) { + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + Local<Context> context = env->context(); + Context::Scope context_scope(context); + + Local<Value> argv[3] = { + Integer::New(isolate, frame->hd.stream_id), + Integer::New(isolate, frame->hd.type), + Integer::New(isolate, error_code) + }; + session->MakeCallback(env->onframeerror_string(), arraysize(argv), argv); + } + return 0; +} + + +inline int Http2Session::OnStreamClose(nghttp2_session* handle, + int32_t id, + uint32_t code, + void* user_data) { + Http2Session* session = static_cast<Http2Session*>(user_data); + Environment* env = session->env(); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + Local<Context> context = env->context(); + Context::Scope context_scope(context); + DEBUG_HTTP2SESSION2(session, "stream %d closed with code: %d", id, code); + Http2Stream* stream = session->FindStream(id); + // Intentionally ignore the callback if the stream does not exist + if (stream != nullptr) { + stream->Close(code); + // It is possible for the stream close to occur before the stream is + // ever passed on to the javascript side. If that happens, ignore this. + Local<Value> fn = + stream->object()->Get(context, env->onstreamclose_string()) + .ToLocalChecked(); + if (fn->IsFunction()) { + Local<Value> argv[1] = { Integer::NewFromUnsigned(isolate, code) }; + stream->MakeCallback(fn.As<Function>(), arraysize(argv), argv); + } + } + return 0; +} + + +inline int Http2Session::OnInvalidHeader(nghttp2_session* session, + const nghttp2_frame* frame, + nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags, + void* user_data) { + // Ignore invalid header fields by default. + return 0; +} + + +inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle, + uint8_t flags, + int32_t id, + const uint8_t* data, + size_t len, + void* user_data) { + Http2Session* session = static_cast<Http2Session*>(user_data); + DEBUG_HTTP2SESSION2(session, "buffering data chunk for stream %d, size: " + "%d, flags: %d", id, len, flags); + // We should never actually get a 0-length chunk so this check is + // only a precaution at this point. + if (len > 0) { + CHECK_EQ(nghttp2_session_consume_connection(handle, len), 0); + Http2Stream* stream = session->FindStream(id); + stream->AddChunk(data, len); + } + return 0; +} + + +inline ssize_t Http2Session::OnSelectPadding(nghttp2_session* session, + const nghttp2_frame* frame, + size_t maxPayloadLen, + void* user_data) { + Http2Session* handle = static_cast<Http2Session*>(user_data); + ssize_t padding = frame->hd.length; + + return handle->padding_strategy_ == PADDING_STRATEGY_MAX + ? handle->OnMaxFrameSizePadding(padding, maxPayloadLen) + : handle->OnCallbackPadding(padding, maxPayloadLen); +} + +#define BAD_PEER_MESSAGE "Remote peer returned unexpected data while we " \ + "expected SETTINGS frame. Perhaps, peer does not " \ + "support HTTP/2 properly." + +inline int Http2Session::OnNghttpError(nghttp2_session* handle, + const char* message, + size_t len, + void* user_data) { + // Unfortunately, this is currently the only way for us to know if + // the session errored because the peer is not an http2 peer. + Http2Session* session = static_cast<Http2Session*>(user_data); + DEBUG_HTTP2SESSION2(session, "Error '%.*s'", len, message); + if (strncmp(message, BAD_PEER_MESSAGE, len) == 0) { + Environment* env = session->env(); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + Local<Context> context = env->context(); + Context::Scope context_scope(context); + + Local<Value> argv[1] = { + Integer::New(isolate, NGHTTP2_ERR_PROTO), + }; + session->MakeCallback(env->error_string(), arraysize(argv), argv); + } + return 0; +} + + +inline void Http2Session::GetTrailers(Http2Stream* stream, uint32_t* flags) { + if (stream->HasTrailers()) { + Http2Stream::SubmitTrailers submit_trailers{this, stream, flags}; + stream->OnTrailers(submit_trailers); + } +} + + +Http2Stream::SubmitTrailers::SubmitTrailers( + Http2Session* session, + Http2Stream* stream, + uint32_t* flags) + : session_(session), stream_(stream), flags_(flags) { } + + +inline void Http2Stream::SubmitTrailers::Submit(nghttp2_nv* trailers, + size_t length) const { + if (length == 0) + return; + DEBUG_HTTP2SESSION2(session_, "sending trailers for stream %d, count: %d", + stream_->id(), length); + *flags_ |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + CHECK_EQ( + nghttp2_submit_trailer(**session_, stream_->id(), trailers, length), 0); +} + + +inline void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local<Context> context = env()->context(); + Context::Scope context_scope(context); + + int32_t id = GetFrameID(frame); + DEBUG_HTTP2SESSION2(this, "handle headers frame for stream %d", id); + Http2Stream* stream = FindStream(id); + + nghttp2_header* headers = stream->headers(); + size_t count = stream->headers_count(); + + Local<String> name_str; + Local<String> value_str; + + Local<Array> holder = Array::New(isolate); + Local<Function> fn = env()->push_values_to_array_function(); + Local<Value> argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2]; + + // The headers are passed in above as a queue of nghttp2_header structs. + // 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 n = 0; + while (count > 0) { + size_t j = 0; + while (count > 0 && j < arraysize(argv) / 2) { + nghttp2_header item = headers[n++]; + // The header name and value are passed as external one-byte strings + name_str = + ExternalHeader::New<true>(env(), item.name).ToLocalChecked(); + value_str = + ExternalHeader::New<false>(env(), item.value).ToLocalChecked(); + argv[j * 2] = name_str; + argv[j * 2 + 1] = value_str; + count--; + j++; + } + // For performance, we pass name and value pairs to array.protototype.push + // in batches of size NODE_PUSH_VAL_TO_ARRAY_MAX * 2 until there are no + // more items to push. + if (j > 0) { + fn->Call(env()->context(), holder, j * 2, argv).ToLocalChecked(); + } + } + + Local<Value> args[5] = { + stream->object(), + Integer::New(isolate, id), + Integer::New(isolate, stream->headers_category()), + Integer::New(isolate, frame->hd.flags), + holder + }; + MakeCallback(env()->onheaders_string(), arraysize(args), args); +} + + +inline void Http2Session::HandlePriorityFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local<Context> context = env()->context(); + Context::Scope context_scope(context); + + nghttp2_priority priority_frame = frame->priority; + int32_t id = GetFrameID(frame); + DEBUG_HTTP2SESSION2(this, "handle priority frame for stream %d", id); + // Priority frame stream ID should never be <= 0. nghttp2 handles this for us + nghttp2_priority_spec spec = priority_frame.pri_spec; + + Local<Value> argv[4] = { + Integer::New(isolate, id), + Integer::New(isolate, spec.stream_id), + Integer::New(isolate, spec.weight), + Boolean::New(isolate, spec.exclusive) + }; + MakeCallback(env()->onpriority_string(), arraysize(argv), argv); +} + + +inline void Http2Session::HandleDataFrame(const nghttp2_frame* frame) { + int32_t id = GetFrameID(frame); + DEBUG_HTTP2SESSION2(this, "handling data frame for stream %d", id); + Http2Stream* stream = FindStream(id); + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + stream->AddChunk(nullptr, 0); + } + + if (stream->IsReading()) + stream->FlushDataChunks(); +} + + +inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local<Context> context = env()->context(); + Context::Scope context_scope(context); + + nghttp2_goaway goaway_frame = frame->goaway; + DEBUG_HTTP2SESSION(this, "handling goaway frame"); + + Local<Value> argv[3] = { + Integer::NewFromUnsigned(isolate, goaway_frame.error_code), + Integer::New(isolate, goaway_frame.last_stream_id), + Undefined(isolate) + }; + + size_t length = goaway_frame.opaque_data_len; + if (length > 0) { + argv[2] = Buffer::Copy(isolate, + reinterpret_cast<char*>(goaway_frame.opaque_data), + length).ToLocalChecked(); + } + + MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv); +} + +inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) { + bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK; + if (ack) { + Http2Ping* ping = PopPing(); + if (ping != nullptr) + ping->Done(true, frame->ping.opaque_data); + } +} + + +inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local<Context> context = env()->context(); + Context::Scope context_scope(context); + + bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK; + + Local<Value> argv[1] = { Boolean::New(isolate, ack) }; + MakeCallback(env()->onsettings_string(), arraysize(argv), argv); +} + + +inline void Http2Session::SendPendingData() { + DEBUG_HTTP2SESSION(this, "sending pending data"); + // Do not attempt to send data on the socket if the destroying flag has + // been set. That means everything is shutting down and the socket + // will not be usable. + if (IsDestroying()) + return; + + WriteWrap* req = nullptr; + char* dest = nullptr; + size_t destRemaining = 0; + size_t destLength = 0; // amount of data stored in dest + size_t destOffset = 0; // current write offset of dest + + const uint8_t* src; // pointer to the serialized data + ssize_t srcLength = 0; // length of serialized data chunk + + // While srcLength is greater than zero + while ((srcLength = nghttp2_session_mem_send(session_, &src)) > 0) { + if (req == nullptr) { + req = AllocateSend(); + destRemaining = req->ExtraSize(); + dest = req->Extra(); + } + DEBUG_HTTP2SESSION2(this, "nghttp2 has %d bytes to send", srcLength); + size_t srcRemaining = srcLength; + size_t srcOffset = 0; + + // The amount of data we have to copy is greater than the space + // remaining. Copy what we can into the remaining space, send it, + // the proceed with the rest. + while (srcRemaining > destRemaining) { + DEBUG_HTTP2SESSION2(this, "pushing %d bytes to the socket", + destLength + destRemaining); + memcpy(dest + destOffset, src + srcOffset, destRemaining); + destLength += destRemaining; + Send(req, dest, destLength); + destOffset = 0; + destLength = 0; + srcRemaining -= destRemaining; + srcOffset += destRemaining; + req = AllocateSend(); + destRemaining = req->ExtraSize(); + dest = req->Extra(); + } + + if (srcRemaining > 0) { + memcpy(dest + destOffset, src + srcOffset, srcRemaining); + destLength += srcRemaining; + destOffset += srcRemaining; + destRemaining -= srcRemaining; + srcRemaining = 0; + srcOffset = 0; + } + } + CHECK_NE(srcLength, NGHTTP2_ERR_NOMEM); + + if (destLength > 0) { + DEBUG_HTTP2SESSION2(this, "pushing %d bytes to the socket", destLength); + Send(req, dest, destLength); + } +} + + +inline Http2Stream* Http2Session::SubmitRequest( + nghttp2_priority_spec* prispec, + nghttp2_nv* nva, + size_t len, + int32_t* ret, + int options) { + DEBUG_HTTP2SESSION(this, "submitting request"); + Http2Stream* stream = nullptr; + Http2Stream::Provider::Stream prov(options); + *ret = nghttp2_submit_request(session_, prispec, nva, len, *prov, nullptr); + CHECK_NE(*ret, NGHTTP2_ERR_NOMEM); + if (*ret > 0) + stream = new Http2Stream(this, *ret, NGHTTP2_HCAT_HEADERS, options); + return stream; +} + +inline void Http2Session::SetChunksSinceLastWrite(size_t n) { + chunks_sent_since_last_write_ = n; +} + + +WriteWrap* Http2Session::AllocateSend() { + HandleScope scope(env()->isolate()); + auto AfterWrite = [](WriteWrap* req, int status) { + req->Dispose(); + }; + Local<Object> obj = + env()->write_wrap_constructor_function() + ->NewInstance(env()->context()).ToLocalChecked(); + // Base the amount allocated on the remote peers max frame size + uint32_t size = + nghttp2_session_get_remote_settings( + session(), + NGHTTP2_SETTINGS_MAX_FRAME_SIZE); + // Max frame size + 9 bytes for the header + return WriteWrap::New(env(), obj, stream_, AfterWrite, size + 9); +} + +void Http2Session::Send(WriteWrap* req, char* buf, size_t length) { + DEBUG_HTTP2SESSION(this, "attempting to send data"); + if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) { + return; + } + + chunks_sent_since_last_write_++; + uv_buf_t actual = uv_buf_init(buf, length); + if (stream_->DoWrite(req, &actual, 1, nullptr)) { + req->Dispose(); + } +} + + +void Http2Session::OnStreamAllocImpl(size_t suggested_size, + uv_buf_t* buf, + void* ctx) { + Http2Session* session = static_cast<Http2Session*>(ctx); + buf->base = session->stream_alloc(); + buf->len = kAllocBufferSize; +} + + +void Http2Session::OnStreamReadImpl(ssize_t nread, + const uv_buf_t* bufs, + uv_handle_type pending, + void* ctx) { + Http2Session* session = static_cast<Http2Session*>(ctx); + if (nread < 0) { + uv_buf_t tmp_buf; + tmp_buf.base = nullptr; + tmp_buf.len = 0; + session->prev_read_cb_.fn(nread, + &tmp_buf, + pending, + session->prev_read_cb_.ctx); + return; + } + if (nread > 0) { + // Only pass data on if nread > 0 + uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) }; + ssize_t ret = session->Write(buf, 1); + if (ret < 0) { + DEBUG_HTTP2SESSION2(session, "fatal error receiving data: %d", ret); + CHECK_EQ(nghttp2_session_terminate_session(session->session(), + NGHTTP2_PROTOCOL_ERROR), 0); + } + } +} + + +void Http2Session::Consume(Local<External> external) { + StreamBase* stream = static_cast<StreamBase*>(external->Value()); + stream->Consume(); + stream_ = stream; + prev_alloc_cb_ = stream->alloc_cb(); + prev_read_cb_ = stream->read_cb(); + stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this }); + stream->set_read_cb({ Http2Session::OnStreamReadImpl, this }); + DEBUG_HTTP2SESSION(this, "i/o stream consumed"); +} + + +void Http2Session::Unconsume() { + if (prev_alloc_cb_.is_empty()) + return; + stream_->set_alloc_cb(prev_alloc_cb_); + stream_->set_read_cb(prev_read_cb_); + prev_alloc_cb_.clear(); + prev_read_cb_.clear(); + stream_ = nullptr; + DEBUG_HTTP2SESSION(this, "i/o stream unconsumed"); +} + + + + +Http2Stream::Http2Stream( + Http2Session* session, + int32_t id, + nghttp2_headers_category category, + int options) : AsyncWrap(session->env(), + session->env()->http2stream_constructor_template() + ->NewInstance(session->env()->context()) + .ToLocalChecked(), + AsyncWrap::PROVIDER_HTTP2STREAM), + StreamBase(session->env()), + session_(session), + id_(id), + current_headers_category_(category) { + MakeWeak<Http2Stream>(this); + + // Limit the number of header pairs + max_header_pairs_ = session->GetMaxHeaderPairs(); + if (max_header_pairs_ == 0) + max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS; + current_headers_.reserve(max_header_pairs_); + + // Limit the number of header octets + max_header_length_ = + std::min( + nghttp2_session_get_local_settings( + session->session(), + NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE), + MAX_MAX_HEADER_LIST_SIZE); + + if (options & STREAM_OPTION_GET_TRAILERS) + flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS; + + if (options & STREAM_OPTION_EMPTY_PAYLOAD) + Shutdown(); + session->AddStream(this); +} + + +Http2Stream::~Http2Stream() { + CHECK(persistent().IsEmpty()); + if (!object().IsEmpty()) + ClearWrap(object()); + persistent().Reset(); +} + +void Http2Stream::StartHeaders(nghttp2_headers_category category) { + DEBUG_HTTP2STREAM2(this, "starting headers, category: %d", id_, category); + current_headers_length_ = 0; + current_headers_.clear(); + current_headers_category_ = category; +} + +nghttp2_stream* Http2Stream::operator*() { + return nghttp2_session_find_stream(**session_, id_); +} + + +void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) { + DEBUG_HTTP2STREAM(this, "prompting for trailers"); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local<Context> context = env()->context(); + Context::Scope context_scope(context); + + Local<Value> ret = + MakeCallback(env()->ontrailers_string(), 0, nullptr).ToLocalChecked(); + if (!ret.IsEmpty()) { + if (ret->IsArray()) { + Local<Array> headers = ret.As<Array>(); + if (headers->Length() > 0) { + Headers trailers(isolate, context, headers); + submit_trailers.Submit(*trailers, trailers.length()); + } + } + } +} + + +inline void Http2Stream::AddChunk(const uint8_t* data, size_t len) { + char* buf = nullptr; + if (len > 0) { + buf = Malloc<char>(len); + memcpy(buf, data, len); + } + data_chunks_.emplace(uv_buf_init(buf, len)); +} + + +int Http2Stream::DoWrite(WriteWrap* req_wrap, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) { + session_->SetChunksSinceLastWrite(); + + nghttp2_stream_write_t* req = new nghttp2_stream_write_t; + req->data = req_wrap; + + auto AfterWrite = [](nghttp2_stream_write_t* req, int status) { + WriteWrap* wrap = static_cast<WriteWrap*>(req->data); + wrap->Done(status); + delete req; + }; + req_wrap->Dispatched(); + Write(req, bufs, count, AfterWrite); + return 0; +} + + +inline void Http2Stream::Close(int32_t code) { + flags_ |= NGHTTP2_STREAM_FLAG_CLOSED; + code_ = code; + DEBUG_HTTP2STREAM2(this, "closed with code %d", code); +} + + +inline void Http2Stream::Shutdown() { + flags_ |= NGHTTP2_STREAM_FLAG_SHUT; + CHECK_NE(nghttp2_session_resume_data(session_->session(), id_), + NGHTTP2_ERR_NOMEM); + DEBUG_HTTP2STREAM(this, "writable side shutdown"); +} + +int Http2Stream::DoShutdown(ShutdownWrap* req_wrap) { + req_wrap->Dispatched(); + Shutdown(); + req_wrap->Done(0); + return 0; +} + +inline void Http2Stream::Destroy() { + DEBUG_HTTP2STREAM(this, "destroying stream"); + // Do nothing if this stream instance is already destroyed + if (IsDestroyed()) + return; + + flags_ |= NGHTTP2_STREAM_FLAG_DESTROYED; + Http2Session* session = this->session_; + + if (session != nullptr) { + session_->RemoveStream(id_); + session_ = nullptr; + } + + // Free any remaining incoming data chunks. + while (!data_chunks_.empty()) { + uv_buf_t buf = data_chunks_.front(); + free(buf.base); + data_chunks_.pop(); + } + + // Free any remaining outgoing data chunks. + while (!queue_.empty()) { + nghttp2_stream_write* head = queue_.front(); + head->cb(head->req, UV_ECANCELED); + delete head; + queue_.pop(); + } + + if (!object().IsEmpty()) + ClearWrap(object()); + persistent().Reset(); + + delete this; +} + + +void Http2Stream::OnDataChunk( + uv_buf_t* chunk) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + ssize_t len = -1; + Local<Object> buf; + if (chunk != nullptr) { + len = chunk->len; + buf = Buffer::New(isolate, chunk->base, len).ToLocalChecked(); + } + EmitData(len, buf, this->object()); +} + + +inline void Http2Stream::FlushDataChunks() { + if (!data_chunks_.empty()) { + uv_buf_t buf = data_chunks_.front(); + data_chunks_.pop(); + if (buf.len > 0) { + CHECK_EQ(nghttp2_session_consume_stream(session_->session(), + id_, buf.len), 0); + OnDataChunk(&buf); + } else { + OnDataChunk(nullptr); + } + } +} + + +inline int Http2Stream::SubmitResponse(nghttp2_nv* nva, + size_t len, + int options) { + DEBUG_HTTP2STREAM(this, "submitting response"); + if (options & STREAM_OPTION_GET_TRAILERS) + flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS; + + if (!IsWritable()) + options |= STREAM_OPTION_EMPTY_PAYLOAD; + + Http2Stream::Provider::Stream prov(this, options); + int ret = nghttp2_submit_response(session_->session(), id_, nva, len, *prov); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + + +// Initiate a response that contains data read from a file descriptor. +inline int Http2Stream::SubmitFile(int fd, + nghttp2_nv* nva, size_t len, + int64_t offset, + int64_t length, + int options) { + DEBUG_HTTP2STREAM(this, "submitting file"); + if (options & STREAM_OPTION_GET_TRAILERS) + flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS; + + if (offset > 0) fd_offset_ = offset; + if (length > -1) fd_length_ = length; + + Http2Stream::Provider::FD prov(this, options, fd); + int ret = nghttp2_submit_response(session_->session(), id_, nva, len, *prov); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + + +// Submit informational headers for a stream. +inline int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) { + DEBUG_HTTP2STREAM2(this, "sending %d informational headers", len); + int ret = nghttp2_submit_headers(session_->session(), + NGHTTP2_FLAG_NONE, + id_, nullptr, + nva, len, nullptr); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + + +inline int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec, + bool silent) { + DEBUG_HTTP2STREAM(this, "sending priority spec"); + int ret = silent ? + nghttp2_session_change_stream_priority(session_->session(), + id_, prispec) : + nghttp2_submit_priority(session_->session(), + NGHTTP2_FLAG_NONE, + id_, prispec); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + + +inline int Http2Stream::SubmitRstStream(const uint32_t code) { + DEBUG_HTTP2STREAM2(this, "sending rst-stream with code %d", code); + session_->SendPendingData(); + CHECK_EQ(nghttp2_submit_rst_stream(session_->session(), + NGHTTP2_FLAG_NONE, + id_, + code), 0); + return 0; +} + + +// Submit a push promise. +inline Http2Stream* Http2Stream::SubmitPushPromise(nghttp2_nv* nva, + size_t len, + int32_t* ret, + int options) { + DEBUG_HTTP2STREAM(this, "sending push promise"); + *ret = nghttp2_submit_push_promise(session_->session(), NGHTTP2_FLAG_NONE, + id_, nva, len, nullptr); + CHECK_NE(*ret, NGHTTP2_ERR_NOMEM); + Http2Stream* stream = nullptr; + if (*ret > 0) + stream = new Http2Stream(session_, *ret, NGHTTP2_HCAT_HEADERS, options); + + return stream; +} + +inline int Http2Stream::ReadStart() { + flags_ |= NGHTTP2_STREAM_FLAG_READ_START; + flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED; + + // Flush any queued data chunks immediately out to the JS layer + FlushDataChunks(); + DEBUG_HTTP2STREAM(this, "reading starting"); + return 0; +} + + +inline int Http2Stream::ReadStop() { + if (!IsReading()) + return 0; + flags_ |= NGHTTP2_STREAM_FLAG_READ_PAUSED; + DEBUG_HTTP2STREAM(this, "reading stopped"); + return 0; +} + +// Queue the given set of uv_but_t handles for writing to an +// nghttp2_stream. The callback will be invoked once the chunks +// of data have been flushed to the underlying nghttp2_session. +// Note that this does *not* mean that the data has been flushed +// to the socket yet. +inline int Http2Stream::Write(nghttp2_stream_write_t* req, + const uv_buf_t bufs[], + unsigned int nbufs, + nghttp2_stream_write_cb cb) { + if (!IsWritable()) { + if (cb != nullptr) + cb(req, UV_EOF); + return 0; + } + DEBUG_HTTP2STREAM2(this, "queuing %d buffers to send", id_, nbufs); + nghttp2_stream_write* item = new nghttp2_stream_write; + item->cb = cb; + item->req = req; + item->nbufs = nbufs; + item->bufs.AllocateSufficientStorage(nbufs); + memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs)); + queue_.push(item); + CHECK_NE(nghttp2_session_resume_data(**session_, id_), NGHTTP2_ERR_NOMEM); + return 0; +} + +inline size_t GetBufferLength(nghttp2_rcbuf* buf) { + return nghttp2_rcbuf_get_buf(buf).len; +} + +inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags) { + size_t length = GetBufferLength(name) + GetBufferLength(value) + 32; + if (current_headers_.size() == max_header_pairs_ || + 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); + current_headers_length_ += length; + return true; +} + + +Http2Stream* GetStream(Http2Session* session, + int32_t id, + nghttp2_data_source* source) { + Http2Stream* stream = static_cast<Http2Stream*>(source->ptr); + if (stream == nullptr) + stream = session->FindStream(id); + CHECK_NE(stream, nullptr); + CHECK_EQ(id, stream->id()); + return stream; +} + +Http2Stream::Provider::Provider(Http2Stream* stream, int options) { + provider_.source.ptr = stream; + empty_ = options & STREAM_OPTION_EMPTY_PAYLOAD; +} + +Http2Stream::Provider::Provider(int options) { + provider_.source.ptr = nullptr; + empty_ = options & STREAM_OPTION_EMPTY_PAYLOAD; +} + +Http2Stream::Provider::~Provider() { + provider_.source.ptr = nullptr; +} + +Http2Stream::Provider::FD::FD(Http2Stream* stream, int options, int fd) + : Http2Stream::Provider(stream, options) { + provider_.source.fd = fd; + provider_.read_callback = Http2Stream::Provider::FD::OnRead; +} + +Http2Stream::Provider::FD::FD(int options, int fd) + : Http2Stream::Provider(options) { + provider_.source.fd = fd; + provider_.read_callback = Http2Stream::Provider::FD::OnRead; +} + +ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data) { + Http2Session* session = static_cast<Http2Session*>(user_data); + Http2Stream* stream = session->FindStream(id); + DEBUG_HTTP2SESSION2(session, "reading outbound file data for stream %d", id); + CHECK_EQ(id, stream->id()); + + int fd = source->fd; + int64_t offset = stream->fd_offset_; + ssize_t numchars = 0; + + if (stream->fd_length_ >= 0 && + stream->fd_length_ < static_cast<int64_t>(length)) + length = stream->fd_length_; + + uv_buf_t data; + data.base = reinterpret_cast<char*>(buf); + data.len = length; + + uv_fs_t read_req; + + if (length > 0) { + // TODO(addaleax): Never use synchronous I/O on the main thread. + numchars = uv_fs_read(session->event_loop(), + &read_req, + fd, &data, 1, + offset, nullptr); + uv_fs_req_cleanup(&read_req); + } + + // Close the stream with an error if reading fails + if (numchars < 0) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + + // Update the read offset for the next read + stream->fd_offset_ += numchars; + stream->fd_length_ -= numchars; + + // if numchars < length, assume that we are done. + if (static_cast<size_t>(numchars) < length || length <= 0) { + DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id); + *flags |= NGHTTP2_DATA_FLAG_EOF; + session->GetTrailers(stream, flags); + } + + return numchars; +} + +Http2Stream::Provider::Stream::Stream(int options) + : Http2Stream::Provider(options) { + provider_.read_callback = Http2Stream::Provider::Stream::OnRead; +} + +Http2Stream::Provider::Stream::Stream(Http2Stream* stream, int options) + : Http2Stream::Provider(stream, options) { + provider_.read_callback = Http2Stream::Provider::Stream::OnRead; +} + +ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data) { + Http2Session* session = static_cast<Http2Session*>(user_data); + DEBUG_HTTP2SESSION2(session, "reading outbound data for stream %d", id); + Http2Stream* stream = GetStream(session, id, source); + CHECK_EQ(id, stream->id()); + + size_t amount = 0; // amount of data being sent in this data frame. + + uv_buf_t current; + + if (!stream->queue_.empty()) { + DEBUG_HTTP2SESSION2(session, "stream %d has pending outbound data", id); + nghttp2_stream_write* head = stream->queue_.front(); + current = head->bufs[stream->queue_index_]; + size_t clen = current.len - stream->queue_offset_; + amount = std::min(clen, length); + DEBUG_HTTP2SESSION2(session, "sending %d bytes for data frame on stream %d", + amount, id); + if (amount > 0) { + memcpy(buf, current.base + stream->queue_offset_, amount); + stream->queue_offset_ += amount; + } + if (stream->queue_offset_ == current.len) { + stream->queue_index_++; + stream->queue_offset_ = 0; + } + if (stream->queue_index_ == head->nbufs) { + head->cb(head->req, 0); + delete head; + stream->queue_.pop(); + stream->queue_offset_ = 0; + stream->queue_index_ = 0; + } + } + + if (amount == 0 && stream->IsWritable() && stream->queue_.empty()) { + DEBUG_HTTP2SESSION2(session, "deferring stream %d", id); + return NGHTTP2_ERR_DEFERRED; + } + + if (stream->queue_.empty() && !stream->IsWritable()) { + DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id); + *flags |= NGHTTP2_DATA_FLAG_EOF; + + session->GetTrailers(stream, flags); + } + + return amount; } + + +// Implementation of the JavaScript API + void HttpErrorString(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); uint32_t val = args[0]->Uint32Value(env->context()).ToChecked(); args.GetReturnValue().Set( - OneByteString(env->isolate(), nghttp2_strerror(val))); + String::NewFromOneByte( + env->isolate(), + reinterpret_cast<const uint8_t*>(nghttp2_strerror(val)), + v8::NewStringType::kInternalized).ToLocalChecked()); } + // Serializes the settings object into a Buffer instance that // would be suitable, for instance, for creating the Base64 // output for an HTTP2-Settings header field. @@ -310,35 +1601,47 @@ void PackSettings(const FunctionCallbackInfo<Value>& args) { args.GetReturnValue().Set(settings.Pack()); } -// Used to fill in the spec defined initial values for each setting. + void RefreshDefaultSettings(const FunctionCallbackInfo<Value>& args) { - DEBUG_HTTP2("Http2Session: refreshing default settings\n"); Environment* env = Environment::GetCurrent(args); Http2Settings::RefreshDefaults(env); } + +void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + if (nghttp2_session_set_next_stream_id(**session, id) < 0) { + DEBUG_HTTP2SESSION2(session, "failed to set next stream id to %d", id); + return args.GetReturnValue().Set(false); + } + args.GetReturnValue().Set(true); + DEBUG_HTTP2SESSION2(session, "set next stream id to %d", id); +} + + template <get_setting fn> void Http2Session::RefreshSettings(const FunctionCallbackInfo<Value>& args) { - DEBUG_HTTP2("Http2Session: refreshing settings for session\n"); Environment* env = Environment::GetCurrent(args); Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); Http2Settings::Update(env, session, fn); + DEBUG_HTTP2SESSION(session, "settings refreshed for session"); } -// Used to fill in the spec defined initial values for each setting. -void RefreshSessionState(const FunctionCallbackInfo<Value>& args) { - DEBUG_HTTP2("Http2Session: refreshing session state\n"); + +void Http2Session::RefreshState(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsObject()); -#endif + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + DEBUG_HTTP2SESSION(session, "refreshing state"); + AliasedBuffer<double, v8::Float64Array>& buffer = env->http2_state()->session_state_buffer; - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>()); - nghttp2_session* s = session->session(); + + nghttp2_session* s = **session; buffer[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE] = nghttp2_session_get_effective_local_window_size(s); @@ -360,84 +1663,31 @@ void RefreshSessionState(const FunctionCallbackInfo<Value>& args) { nghttp2_session_get_hd_inflate_dynamic_table_size(s); } -void RefreshStreamState(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK_EQ(args.Length(), 2); - CHECK(args[0]->IsObject()); - CHECK(args[1]->IsNumber()); -#endif - int32_t id = args[1]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: refreshing stream %d state\n", id); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>()); - nghttp2_session* s = session->session(); - Nghttp2Stream* stream; - - AliasedBuffer<double, v8::Float64Array>& buffer = - env->http2_state()->stream_state_buffer; - - if ((stream = session->FindStream(id)) == nullptr) { - buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; - buffer[IDX_STREAM_STATE_WEIGHT] = - buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = - buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = - buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = - buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; - return; - } - nghttp2_stream* str = - nghttp2_session_find_stream(s, stream->id()); - - if (str == nullptr) { - buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; - buffer[IDX_STREAM_STATE_WEIGHT] = - buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = - buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = - buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = - buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; - } else { - buffer[IDX_STREAM_STATE] = - nghttp2_stream_get_state(str); - buffer[IDX_STREAM_STATE_WEIGHT] = - nghttp2_stream_get_weight(str); - buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = - nghttp2_stream_get_sum_dependency_weight(str); - buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = - nghttp2_session_get_stream_local_close(s, id); - buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = - nghttp2_session_get_stream_remote_close(s, id); - buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = - nghttp2_session_get_stream_local_window_size(s, id); - } -} void Http2Session::New(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG CHECK(args.IsConstructCall()); -#endif int val = args[0]->IntegerValue(env->context()).ToChecked(); nghttp2_session_type type = static_cast<nghttp2_session_type>(val); - DEBUG_HTTP2("Http2Session: creating a session of type: %d\n", type); - new Http2Session(env, args.This(), type); + Http2Session* session = new Http2Session(env, args.This(), type); + session->get_async_id(); // avoid compiler warning + DEBUG_HTTP2SESSION(session, "session created"); } -// Capture the stream that this session will use to send and receive data void Http2Session::Consume(const FunctionCallbackInfo<Value>& args) { Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); -#if defined(DEBUG) && DEBUG CHECK(args[0]->IsExternal()); -#endif session->Consume(args[0].As<External>()); } + void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) { Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - DEBUG_HTTP2("Http2Session: destroying session %d\n", session->type()); + DEBUG_HTTP2SESSION(session, "destroying session"); + Environment* env = Environment::GetCurrent(args); Local<Context> context = env->context(); @@ -448,79 +1698,27 @@ void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) { session->Close(); } + void Http2Session::Destroying(const FunctionCallbackInfo<Value>& args) { Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - DEBUG_HTTP2("Http2Session: preparing to destroy session %d\n", - session->type()); session->MarkDestroying(); + DEBUG_HTTP2SESSION(session, "preparing to destroy session"); } -void Http2Session::SubmitPriority(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Local<Context> context = env->context(); - - int32_t id = args[0]->Int32Value(context).ToChecked(); - Http2Priority priority(env, args[1], args[2], args[3]); - bool silent = args[4]->BooleanValue(context).ToChecked(); - DEBUG_HTTP2("Http2Session: submitting priority for stream %d", id); - - Nghttp2Stream* stream; - if (!(stream = session->FindStream(id))) { - // invalid stream - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - - args.GetReturnValue().Set(stream->SubmitPriority(*priority, silent)); -} -void Http2Session::SubmitSettings(const FunctionCallbackInfo<Value>& args) { +void Http2Session::Settings(const FunctionCallbackInfo<Value>& args) { Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); Environment* env = session->env(); Http2Settings settings(env); - args.GetReturnValue().Set( - session->Nghttp2Session::SubmitSettings(*settings, settings.length())); + session->Http2Session::Settings(*settings, settings.length()); + DEBUG_HTTP2SESSION(session, "settings submitted"); } -void Http2Session::SubmitRstStream(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); - Local<Context> context = env->context(); - -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); - CHECK(args[1]->IsNumber()); -#endif - - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - - int32_t id = args[0]->Int32Value(context).ToChecked(); - uint32_t code = args[1]->Uint32Value(context).ToChecked(); - - Nghttp2Stream* stream; - if (!(stream = session->FindStream(id))) { - // invalid stream - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - DEBUG_HTTP2("Http2Session: sending rst_stream for stream %d, code: %d\n", - id, code); - args.GetReturnValue().Set(stream->SubmitRstStream(code)); -} - -void Http2Session::SubmitRequest(const FunctionCallbackInfo<Value>& args) { - // args[0] Array of headers - // args[1] options int - // args[2] parentStream ID (for priority spec) - // args[3] weight (for priority spec) - // args[4] exclusive boolean (for priority spec) -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsArray()); -#endif +void Http2Session::Request(const FunctionCallbackInfo<Value>& args) { Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); Environment* env = session->env(); @@ -531,177 +1729,36 @@ void Http2Session::SubmitRequest(const FunctionCallbackInfo<Value>& args) { int options = args[1]->IntegerValue(context).ToChecked(); Http2Priority priority(env, args[2], args[3], args[4]); - DEBUG_HTTP2("Http2Session: submitting request: headers: %d, options: %d\n", - headers->Length(), options); - Headers list(isolate, context, headers); - int32_t ret = session->Nghttp2Session::SubmitRequest(*priority, - *list, list.length(), - nullptr, options); - DEBUG_HTTP2("Http2Session: request submitted, response: %d\n", ret); - args.GetReturnValue().Set(ret); -} + DEBUG_HTTP2SESSION(session, "request submitted"); -void Http2Session::SubmitResponse(const FunctionCallbackInfo<Value>& args) { -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); - CHECK(args[1]->IsArray()); -#endif - - Http2Session* session; - Nghttp2Stream* stream; + int32_t ret = 0; + Http2Stream* stream = + session->Http2Session::SubmitRequest(*priority, *list, list.length(), + &ret, options); - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Environment* env = session->env(); - Local<Context> context = env->context(); - Isolate* isolate = env->isolate(); - - int32_t id = args[0]->Int32Value(context).ToChecked(); - Local<Array> headers = args[1].As<Array>(); - int options = args[2]->IntegerValue(context).ToChecked(); - - DEBUG_HTTP2("Http2Session: submitting response for stream %d: headers: %d, " - "options: %d\n", id, headers->Length(), options); - - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + if (ret <= 0) { + DEBUG_HTTP2SESSION2(session, "could not submit request: %s", + nghttp2_strerror(ret)); + return args.GetReturnValue().Set(ret); } - Headers list(isolate, context, headers); - - args.GetReturnValue().Set( - stream->SubmitResponse(*list, list.length(), options)); + DEBUG_HTTP2SESSION2(session, "request submitted, new stream id %d", + stream->id()); + args.GetReturnValue().Set(stream->object()); } -void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) { -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); // Stream ID - CHECK(args[1]->IsNumber()); // File Descriptor - CHECK(args[2]->IsArray()); // Headers - CHECK(args[3]->IsNumber()); // Offset - CHECK(args[4]->IsNumber()); // Length -#endif - Http2Session* session; - Nghttp2Stream* stream; - - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Environment* env = session->env(); - Local<Context> context = env->context(); - Isolate* isolate = env->isolate(); - - int32_t id = args[0]->Int32Value(context).ToChecked(); - int fd = args[1]->Int32Value(context).ToChecked(); - Local<Array> headers = args[2].As<Array>(); - - int64_t offset = args[3]->IntegerValue(context).ToChecked(); - int64_t length = args[4]->IntegerValue(context).ToChecked(); - int options = args[5]->IntegerValue(context).ToChecked(); - -#if defined(DEBUG) && DEBUG - CHECK_GE(offset, 0); -#endif - - DEBUG_HTTP2("Http2Session: submitting file %d for stream %d: headers: %d, " - "end-stream: %d\n", fd, id, headers->Length()); - - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - - session->chunks_sent_since_last_write_ = 0; - - Headers list(isolate, context, headers); - - args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length(), - offset, length, options)); -} - -void Http2Session::SendHeaders(const FunctionCallbackInfo<Value>& args) { -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); - CHECK(args[1]->IsArray()); -#endif - - Http2Session* session; - Nghttp2Stream* stream; - - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Environment* env = session->env(); - Local<Context> context = env->context(); - Isolate* isolate = env->isolate(); - - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - Local<Array> headers = args[1].As<Array>(); - - DEBUG_HTTP2("Http2Session: sending informational headers for stream %d, " - "count: %d\n", id, headers->Length()); - - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - - Headers list(isolate, context, headers); - - args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length())); -} - -void Http2Session::ShutdownStream(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); -#endif - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Nghttp2Stream* stream; - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: shutting down stream %d\n", id); - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - stream->Shutdown(); -} - -void Http2Session::StreamReadStart(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); -#endif - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Nghttp2Stream* stream; - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - stream->ReadStart(); -} - - -void Http2Session::StreamReadStop(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); -#endif - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Nghttp2Stream* stream; - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - stream->ReadStop(); -} - -void Http2Session::SendShutdownNotice( - const FunctionCallbackInfo<Value>& args) { +void Http2Session::ShutdownNotice(const FunctionCallbackInfo<Value>& args) { Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); session->SubmitShutdownNotice(); + DEBUG_HTTP2SESSION(session, "shutdown notice sent"); } -void Http2Session::SubmitGoaway(const FunctionCallbackInfo<Value>& args) { + +void Http2Session::Goaway(const FunctionCallbackInfo<Value>& args) { Http2Session* session; Environment* env = Environment::GetCurrent(args); Local<Context> context = env->context(); @@ -721,58 +1778,25 @@ void Http2Session::SubmitGoaway(const FunctionCallbackInfo<Value>& args) { length = buf_length; } - DEBUG_HTTP2("Http2Session: initiating immediate shutdown. " - "last-stream-id: %d, code: %d, opaque-data: %d\n", - lastStreamID, errorCode, length); int status = nghttp2_submit_goaway(session->session(), NGHTTP2_FLAG_NONE, lastStreamID, errorCode, data, length); + CHECK_NE(status, NGHTTP2_ERR_NOMEM); args.GetReturnValue().Set(status); + DEBUG_HTTP2SESSION2(session, "immediate shutdown initiated with " + "last stream id %d, code %d, and opaque-data length %d", + lastStreamID, errorCode, length); } -void Http2Session::DestroyStream(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); -#if defined(DEBUG) && DEBUG - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsNumber()); -#endif - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: destroy stream %d\n", id); - Nghttp2Stream* stream; - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - stream->Destroy(); -} - -void Http2Session::FlushData(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); -#if defined(DEBUG) && DEBUG - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsNumber()); -#endif - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: flushing data to js for stream %d\n", id); - Nghttp2Stream* stream; - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - stream->ReadResume(); -} void Http2Session::UpdateChunksSent(const FunctionCallbackInfo<Value>& args) { - Http2Session* session; Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - HandleScope scope(isolate); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); uint32_t length = session->chunks_sent_since_last_write_; @@ -783,426 +1807,260 @@ void Http2Session::UpdateChunksSent(const FunctionCallbackInfo<Value>& args) { args.GetReturnValue().Set(length); } -void Http2Session::SubmitPushPromise(const FunctionCallbackInfo<Value>& args) { - Http2Session* session; + +void Http2Stream::RstStream(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); Local<Context> context = env->context(); - Isolate* isolate = env->isolate(); - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + uint32_t code = args[0]->Uint32Value(context).ToChecked(); + args.GetReturnValue().Set(stream->SubmitRstStream(code)); + DEBUG_HTTP2STREAM2(stream, "rst_stream code %d sent", code); +} -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); // parent stream ID - CHECK(args[1]->IsArray()); // headers array -#endif - Nghttp2Stream* parent; - int32_t id = args[0]->Int32Value(context).ToChecked(); - Local<Array> headers = args[1].As<Array>(); - int options = args[2]->IntegerValue(context).ToChecked(); - - DEBUG_HTTP2("Http2Session: submitting push promise for stream %d: " - "options: %d, headers: %d\n", id, options, - headers->Length()); +void Http2Stream::Respond(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Local<Context> context = env->context(); + Isolate* isolate = env->isolate(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); - if (!(parent = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } + Local<Array> headers = args[0].As<Array>(); + int options = args[1]->IntegerValue(context).ToChecked(); Headers list(isolate, context, headers); - int32_t ret = parent->SubmitPushPromise(*list, list.length(), - nullptr, options); - DEBUG_HTTP2("Http2Session: push promise submitted, ret: %d\n", ret); - args.GetReturnValue().Set(ret); + args.GetReturnValue().Set( + stream->SubmitResponse(*list, list.length(), options)); + DEBUG_HTTP2STREAM(stream, "response submitted"); } -int Http2Session::DoWrite(WriteWrap* req_wrap, - uv_buf_t* bufs, - size_t count, - uv_stream_t* send_handle) { - Environment* env = req_wrap->env(); - Local<Object> req_wrap_obj = req_wrap->object(); + +void Http2Stream::RespondFD(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); Local<Context> context = env->context(); + Isolate* isolate = env->isolate(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); - Nghttp2Stream* stream; - { - Local<Value> val = - req_wrap_obj->Get(context, env->stream_string()).ToLocalChecked(); - int32_t id = val->Int32Value(context).ToChecked(); - if (!val->IsNumber() || !(stream = FindStream(id))) { - // invalid stream - req_wrap->Dispatched(); - req_wrap->Done(0); - return NGHTTP2_ERR_INVALID_STREAM_ID; - } - } + int fd = args[0]->Int32Value(context).ToChecked(); + Local<Array> headers = args[1].As<Array>(); - chunks_sent_since_last_write_ = 0; + int64_t offset = args[2]->IntegerValue(context).ToChecked(); + int64_t length = args[3]->IntegerValue(context).ToChecked(); + int options = args[4]->IntegerValue(context).ToChecked(); - nghttp2_stream_write_t* req = new nghttp2_stream_write_t; - req->data = req_wrap; + stream->session()->SetChunksSinceLastWrite(); - auto AfterWrite = [](nghttp2_stream_write_t* req, int status) { - WriteWrap* wrap = static_cast<WriteWrap*>(req->data); - wrap->Done(status); - delete req; - }; - req_wrap->Dispatched(); - stream->Write(req, bufs, count, AfterWrite); - return 0; + Headers list(isolate, context, headers); + args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length(), + offset, length, options)); + DEBUG_HTTP2STREAM2(stream, "file response submitted for fd %d", fd); } -WriteWrap* Http2Session::AllocateSend() { - HandleScope scope(env()->isolate()); - auto AfterWrite = [](WriteWrap* req, int status) { - req->Dispose(); - }; - Local<Object> obj = - env()->write_wrap_constructor_function() - ->NewInstance(env()->context()).ToLocalChecked(); - // Base the amount allocated on the remote peers max frame size - uint32_t size = - nghttp2_session_get_remote_settings( - session(), - NGHTTP2_SETTINGS_MAX_FRAME_SIZE); - // Max frame size + 9 bytes for the header - return WriteWrap::New(env(), obj, this, AfterWrite, size + 9); -} -void Http2Session::Send(WriteWrap* req, char* buf, size_t length) { - DEBUG_HTTP2("Http2Session: Attempting to send data\n"); - if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) { - return; - } +void Http2Stream::Info(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Local<Context> context = env->context(); + Isolate* isolate = env->isolate(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); - chunks_sent_since_last_write_++; - uv_buf_t actual = uv_buf_init(buf, length); - if (stream_->DoWrite(req, &actual, 1, nullptr)) { - req->Dispose(); - } -} + Local<Array> headers = args[0].As<Array>(); -void Http2Session::OnTrailers(Nghttp2Stream* stream, - const SubmitTrailers& submit_trailers) { - DEBUG_HTTP2("Http2Session: prompting for trailers on stream %d\n", - stream->id()); - Local<Context> context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); + Headers list(isolate, context, headers); + args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length())); + DEBUG_HTTP2STREAM2(stream, "%d informational headers sent", + headers->Length()); +} - Local<Value> argv[1] = { - Integer::New(isolate, stream->id()) - }; - Local<Value> ret = MakeCallback(env()->ontrailers_string(), - arraysize(argv), argv).ToLocalChecked(); - if (!ret.IsEmpty()) { - if (ret->IsArray()) { - Local<Array> headers = ret.As<Array>(); - if (headers->Length() > 0) { - Headers trailers(isolate, context, headers); - submit_trailers.Submit(*trailers, trailers.length()); - } - } - } +void Http2Stream::GetID(const FunctionCallbackInfo<Value>& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + args.GetReturnValue().Set(stream->id()); } -void Http2Session::OnHeaders( - Nghttp2Stream* stream, - nghttp2_header* headers, - size_t count, - nghttp2_headers_category cat, - uint8_t flags) { - Local<Context> context = env()->context(); - Isolate* isolate = env()->isolate(); - Context::Scope context_scope(context); - HandleScope scope(isolate); - Local<String> name_str; - Local<String> value_str; - Local<Array> holder = Array::New(isolate); - Local<Function> fn = env()->push_values_to_array_function(); - Local<Value> argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2]; - -#if defined(DEBUG) && DEBUG - CHECK_LE(cat, NGHTTP2_HCAT_HEADERS); -#endif +void Http2Stream::Destroy(const FunctionCallbackInfo<Value>& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + DEBUG_HTTP2STREAM(stream, "destroying stream"); + stream->Destroy(); +} - // The headers are passed in above as a queue of nghttp2_header structs. - // 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 n = 0; - while (count > 0) { - size_t j = 0; - while (count > 0 && j < arraysize(argv) / 2) { - nghttp2_header item = headers[n++]; - // The header name and value are passed as external one-byte strings - name_str = - ExternalHeader::New<true>(env(), item.name).ToLocalChecked(); - value_str = - ExternalHeader::New<false>(env(), item.value).ToLocalChecked(); - argv[j * 2] = name_str; - argv[j * 2 + 1] = value_str; - count--; - j++; - } - // For performance, we pass name and value pairs to array.protototype.push - // in batches of size NODE_PUSH_VAL_TO_ARRAY_MAX * 2 until there are no - // more items to push. - if (j > 0) { - fn->Call(env()->context(), holder, j * 2, argv).ToLocalChecked(); - } - } - Local<Value> args[4] = { - Integer::New(isolate, stream->id()), - Integer::New(isolate, cat), - Integer::New(isolate, flags), - holder - }; - MakeCallback(env()->onheaders_string(), arraysize(args), args); +void Http2Stream::FlushData(const FunctionCallbackInfo<Value>& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + stream->ReadStart(); + DEBUG_HTTP2STREAM(stream, "data flushed to js"); } -void Http2Session::OnStreamClose(int32_t id, uint32_t code) { - Isolate* isolate = env()->isolate(); - Local<Context> context = env()->context(); - HandleScope scope(isolate); - Context::Scope context_scope(context); +void Http2Stream::PushPromise(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Local<Context> context = env->context(); + Isolate* isolate = env->isolate(); + Http2Stream* parent; + ASSIGN_OR_RETURN_UNWRAP(&parent, args.Holder()); - Local<Value> argv[2] = { - Integer::New(isolate, id), - Integer::NewFromUnsigned(isolate, code) - }; - MakeCallback(env()->onstreamclose_string(), arraysize(argv), argv); -} + Local<Array> headers = args[0].As<Array>(); + int options = args[1]->IntegerValue(context).ToChecked(); -void Http2Session::OnDataChunk( - Nghttp2Stream* stream, - uv_buf_t* chunk) { - Isolate* isolate = env()->isolate(); - Local<Context> context = env()->context(); - HandleScope scope(isolate); - Local<Object> obj = Object::New(isolate); - obj->Set(context, - env()->id_string(), - Integer::New(isolate, stream->id())).FromJust(); - ssize_t len = -1; - Local<Object> buf; - if (chunk != nullptr) { - len = chunk->len; - buf = Buffer::New(isolate, chunk->base, len).ToLocalChecked(); - } - EmitData(len, buf, obj); -} + Headers list(isolate, context, headers); -void Http2Session::OnSettings(bool ack) { - Local<Context> context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); + DEBUG_HTTP2STREAM(parent, "creating push promise"); - Local<Value> argv[1] = { Boolean::New(isolate, ack) }; - MakeCallback(env()->onsettings_string(), arraysize(argv), argv); + int32_t ret = 0; + Http2Stream* stream = parent->SubmitPushPromise(*list, list.length(), + &ret, options); + if (ret <= 0) { + DEBUG_HTTP2STREAM2(parent, "failed to create push stream: %d", ret); + return args.GetReturnValue().Set(ret); + } + DEBUG_HTTP2STREAM2(parent, "push stream %d created", stream->id()); + args.GetReturnValue().Set(stream->object()); } -void Http2Session::OnFrameError(int32_t id, uint8_t type, int error_code) { - Local<Context> context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); - Local<Value> argv[3] = { - Integer::New(isolate, id), - Integer::New(isolate, type), - Integer::New(isolate, error_code) - }; - MakeCallback(env()->onframeerror_string(), arraysize(argv), argv); -} +void Http2Stream::Priority(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Local<Context> context = env->context(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); -void Http2Session::OnPriority(int32_t stream, - int32_t parent, - int32_t weight, - int8_t exclusive) { - Local<Context> context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); + Http2Priority priority(env, args[0], args[1], args[2]); + bool silent = args[3]->BooleanValue(context).ToChecked(); - Local<Value> argv[4] = { - Integer::New(isolate, stream), - Integer::New(isolate, parent), - Integer::New(isolate, weight), - Boolean::New(isolate, exclusive) - }; - MakeCallback(env()->onpriority_string(), arraysize(argv), argv); + CHECK_EQ(stream->SubmitPriority(*priority, silent), 0); + DEBUG_HTTP2STREAM(stream, "priority submitted"); } -void Http2Session::OnGoAway(int32_t lastStreamID, - uint32_t errorCode, - uint8_t* data, - size_t length) { - Local<Context> context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); - Local<Value> argv[3] = { - Integer::NewFromUnsigned(isolate, errorCode), - Integer::New(isolate, lastStreamID), - Undefined(isolate) - }; +void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); - if (length > 0) { - argv[2] = Buffer::Copy(isolate, - reinterpret_cast<char*>(data), - length).ToLocalChecked(); - } + DEBUG_HTTP2STREAM(stream, "refreshing state"); - MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv); -} + AliasedBuffer<double, v8::Float64Array>& buffer = + env->http2_state()->stream_state_buffer; -void Http2Session::OnStreamAllocImpl(size_t suggested_size, - uv_buf_t* buf, - void* ctx) { - Http2Session* session = static_cast<Http2Session*>(ctx); - buf->base = session->stream_alloc(); - buf->len = kAllocBufferSize; + nghttp2_stream* str = **stream; + nghttp2_session* s = **(stream->session()); + + if (str == nullptr) { + buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; + buffer[IDX_STREAM_STATE_WEIGHT] = + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; + } else { + buffer[IDX_STREAM_STATE] = + nghttp2_stream_get_state(str); + buffer[IDX_STREAM_STATE_WEIGHT] = + nghttp2_stream_get_weight(str); + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + nghttp2_stream_get_sum_dependency_weight(str); + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + nghttp2_session_get_stream_local_close(s, stream->id()); + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + nghttp2_session_get_stream_remote_close(s, stream->id()); + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_stream_local_window_size(s, stream->id()); + } } +void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); -void Http2Session::OnStreamReadImpl(ssize_t nread, - const uv_buf_t* bufs, - uv_handle_type pending, - void* ctx) { - Http2Session* session = static_cast<Http2Session*>(ctx); - if (nread < 0) { - uv_buf_t tmp_buf; - tmp_buf.base = nullptr; - tmp_buf.len = 0; - session->prev_read_cb_.fn(nread, - &tmp_buf, - pending, - session->prev_read_cb_.ctx); - return; - } - if (nread > 0) { - // Only pass data on if nread > 0 - uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) }; - ssize_t ret = session->Write(buf, 1); - if (ret < 0) { - DEBUG_HTTP2("Http2Session: fatal error receiving data: %d\n", ret); - nghttp2_session_terminate_session(session->session(), - NGHTTP2_PROTOCOL_ERROR); - } + uint8_t* payload = nullptr; + if (Buffer::HasInstance(args[0])) { + payload = reinterpret_cast<uint8_t*>(Buffer::Data(args[0])); + CHECK_EQ(Buffer::Length(args[0]), 8); } -} + Http2Session::Http2Ping* ping = new Http2Ping(session); + Local<Object> obj = ping->object(); + obj->Set(env->context(), env->ondone_string(), args[1]).FromJust(); -void Http2Session::Consume(Local<External> external) { - DEBUG_HTTP2("Http2Session: consuming socket\n"); -#if defined(DEBUG) && DEBUG - CHECK(prev_alloc_cb_.is_empty()); -#endif - StreamBase* stream = static_cast<StreamBase*>(external->Value()); -#if defined(DEBUG) && DEBUG - CHECK_NE(stream, nullptr); -#endif - stream->Consume(); - stream_ = stream; - prev_alloc_cb_ = stream->alloc_cb(); - prev_read_cb_ = stream->read_cb(); - stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this }); - stream->set_read_cb({ Http2Session::OnStreamReadImpl, this }); -} + if (!session->AddPing(ping)) { + ping->Done(false); + return args.GetReturnValue().Set(false); + } + ping->Send(payload); + args.GetReturnValue().Set(true); +} -void Http2Session::Unconsume() { - DEBUG_HTTP2("Http2Session: unconsuming socket\n"); - if (prev_alloc_cb_.is_empty()) - return; - stream_->set_alloc_cb(prev_alloc_cb_); - stream_->set_read_cb(prev_read_cb_); - prev_alloc_cb_.clear(); - prev_read_cb_.clear(); - stream_ = nullptr; +Http2Session::Http2Ping* Http2Session::PopPing() { + Http2Ping* ping = nullptr; + if (!outstanding_pings_.empty()) { + ping = outstanding_pings_.front(); + outstanding_pings_.pop(); + } + return ping; } +bool Http2Session::AddPing(Http2Session::Http2Ping* ping) { + if (outstanding_pings_.size() == max_outstanding_pings_) + return false; + outstanding_pings_.push(ping); + return true; +} -Headers::Headers(Isolate* isolate, - Local<Context> context, - Local<Array> headers) { -#if defined(DEBUG) && DEBUG - CHECK_EQ(headers->Length(), 2); -#endif - Local<Value> header_string = headers->Get(context, 0).ToLocalChecked(); - Local<Value> header_count = headers->Get(context, 1).ToLocalChecked(); -#if defined(DEBUG) && DEBUG - CHECK(header_string->IsString()); - CHECK(header_count->IsUint32()); -#endif - count_ = header_count.As<Uint32>()->Value(); - int header_string_len = header_string.As<String>()->Length(); +Http2Session::Http2Ping::Http2Ping( + Http2Session* session) + : AsyncWrap(session->env(), + session->env()->http2ping_constructor_template() + ->NewInstance(session->env()->context()) + .ToLocalChecked(), + AsyncWrap::PROVIDER_HTTP2PING), + session_(session), + startTime_(uv_hrtime()) { } + +Http2Session::Http2Ping::~Http2Ping() { + if (!object().IsEmpty()) + ClearWrap(object()); + persistent().Reset(); + CHECK(persistent().IsEmpty()); +} - if (count_ == 0) { - CHECK_EQ(header_string_len, 0); - return; +void Http2Session::Http2Ping::Send(uint8_t* payload) { + uint8_t data[8]; + if (payload == nullptr) { + memcpy(&data, &startTime_, arraysize(data)); + payload = data; } + CHECK_EQ(nghttp2_submit_ping(**session_, NGHTTP2_FLAG_NONE, payload), 0); +} - // 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<char*>( - ROUND_UP(reinterpret_cast<uintptr_t>(*buf_), alignof(nghttp2_nv))); - char* header_contents = start + (count_ * sizeof(nghttp2_nv)); - nghttp2_nv* const nva = reinterpret_cast<nghttp2_nv*>(start); - - CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length()); - CHECK_EQ(header_string.As<String>() - ->WriteOneByte(reinterpret_cast<uint8_t*>(header_contents), - 0, header_string_len, - String::NO_NULL_TERMINATION), - header_string_len); +void Http2Session::Http2Ping::Done(bool ack, const uint8_t* payload) { + uint64_t end = uv_hrtime(); + double duration = (end - startTime_) / 1e6; - 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<uint8_t*>(p); - nva[n].namelen = strlen(p); - p += nva[n].namelen + 1; - nva[n].value = reinterpret_cast<uint8_t*>(p); - nva[n].valuelen = strlen(p); - p += nva[n].valuelen + 1; + Local<Value> buf = Undefined(env()->isolate()); + if (payload != nullptr) { + buf = Buffer::Copy(env()->isolate(), + reinterpret_cast<const char*>(payload), + 8).ToLocalChecked(); } -#if defined(DEBUG) && DEBUG - CHECK_EQ(p, header_contents + header_string_len); - CHECK_EQ(n, count_); -#endif + Local<Value> argv[3] = { + Boolean::New(env()->isolate(), ack), + Number::New(env()->isolate(), duration), + buf + }; + MakeCallback(env()->ondone_string(), arraysize(argv), argv); + delete this; } - void Initialize(Local<Object> target, Local<Value> unused, Local<Context> context, @@ -1245,60 +2103,58 @@ void Initialize(Local<Object> target, Local<String> http2SessionClassName = FIXED_ONE_BYTE_STRING(isolate, "Http2Session"); + Local<FunctionTemplate> ping = FunctionTemplate::New(env->isolate()); + ping->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Ping")); + AsyncWrap::AddWrapMethods(env, ping); + Local<ObjectTemplate> pingt = ping->InstanceTemplate(); + pingt->SetInternalFieldCount(1); + env->set_http2ping_constructor_template(pingt); + + Local<FunctionTemplate> stream = FunctionTemplate::New(env->isolate()); + stream->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Stream")); + env->SetProtoMethod(stream, "id", Http2Stream::GetID); + env->SetProtoMethod(stream, "destroy", Http2Stream::Destroy); + env->SetProtoMethod(stream, "flushData", Http2Stream::FlushData); + env->SetProtoMethod(stream, "priority", Http2Stream::Priority); + env->SetProtoMethod(stream, "pushPromise", Http2Stream::PushPromise); + env->SetProtoMethod(stream, "info", Http2Stream::Info); + env->SetProtoMethod(stream, "respondFD", Http2Stream::RespondFD); + env->SetProtoMethod(stream, "respond", Http2Stream::Respond); + env->SetProtoMethod(stream, "rstStream", Http2Stream::RstStream); + env->SetProtoMethod(stream, "refreshState", Http2Stream::RefreshState); + AsyncWrap::AddWrapMethods(env, stream); + StreamBase::AddMethods<Http2Stream>(env, stream, StreamBase::kFlagHasWritev); + Local<ObjectTemplate> streamt = stream->InstanceTemplate(); + streamt->SetInternalFieldCount(1); + env->set_http2stream_constructor_template(streamt); + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Stream"), + stream->GetFunction()).FromJust(); + Local<FunctionTemplate> session = env->NewFunctionTemplate(Http2Session::New); session->SetClassName(http2SessionClassName); session->InstanceTemplate()->SetInternalFieldCount(1); AsyncWrap::AddWrapMethods(env, session); - env->SetProtoMethod(session, "consume", - Http2Session::Consume); - env->SetProtoMethod(session, "destroy", - Http2Session::Destroy); - env->SetProtoMethod(session, "destroying", - Http2Session::Destroying); - env->SetProtoMethod(session, "sendHeaders", - Http2Session::SendHeaders); - env->SetProtoMethod(session, "submitShutdownNotice", - Http2Session::SendShutdownNotice); - env->SetProtoMethod(session, "submitGoaway", - Http2Session::SubmitGoaway); - env->SetProtoMethod(session, "submitSettings", - Http2Session::SubmitSettings); - env->SetProtoMethod(session, "submitPushPromise", - Http2Session::SubmitPushPromise); - env->SetProtoMethod(session, "submitRstStream", - Http2Session::SubmitRstStream); - env->SetProtoMethod(session, "submitResponse", - Http2Session::SubmitResponse); - env->SetProtoMethod(session, "submitFile", - Http2Session::SubmitFile); - env->SetProtoMethod(session, "submitRequest", - Http2Session::SubmitRequest); - env->SetProtoMethod(session, "submitPriority", - Http2Session::SubmitPriority); - env->SetProtoMethod(session, "shutdownStream", - Http2Session::ShutdownStream); - env->SetProtoMethod(session, "streamReadStart", - Http2Session::StreamReadStart); - env->SetProtoMethod(session, "streamReadStop", - Http2Session::StreamReadStop); + env->SetProtoMethod(session, "ping", Http2Session::Ping); + env->SetProtoMethod(session, "consume", Http2Session::Consume); + env->SetProtoMethod(session, "destroy", Http2Session::Destroy); + env->SetProtoMethod(session, "destroying", Http2Session::Destroying); + env->SetProtoMethod(session, "shutdownNotice", Http2Session::ShutdownNotice); + env->SetProtoMethod(session, "goaway", Http2Session::Goaway); + env->SetProtoMethod(session, "settings", Http2Session::Settings); + env->SetProtoMethod(session, "request", Http2Session::Request); env->SetProtoMethod(session, "setNextStreamID", Http2Session::SetNextStreamID); - env->SetProtoMethod(session, "destroyStream", - Http2Session::DestroyStream); - env->SetProtoMethod(session, "flushData", - Http2Session::FlushData); env->SetProtoMethod(session, "updateChunksSent", Http2Session::UpdateChunksSent); + env->SetProtoMethod(session, "refreshState", Http2Session::RefreshState); env->SetProtoMethod( - session, "refreshLocalSettings", + session, "localSettings", Http2Session::RefreshSettings<nghttp2_session_get_local_settings>); env->SetProtoMethod( - session, "refreshRemoteSettings", + session, "remoteSettings", Http2Session::RefreshSettings<nghttp2_session_get_remote_settings>); - StreamBase::AddMethods<Http2Session>(env, session, - StreamBase::kFlagHasWritev | - StreamBase::kFlagNoShutdown); target->Set(context, http2SessionClassName, session->GetFunction()).FromJust(); @@ -1335,7 +2191,6 @@ void Initialize(Local<Object> target, NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NONE); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NO_INDEX); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_DEFERRED); - NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_NOMEM); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_INVALID_ARGUMENT); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_CLOSED); @@ -1386,8 +2241,6 @@ HTTP_STATUS_CODES(V) #undef V env->SetMethod(target, "refreshDefaultSettings", RefreshDefaultSettings); - env->SetMethod(target, "refreshSessionState", RefreshSessionState); - env->SetMethod(target, "refreshStreamState", RefreshStreamState); env->SetMethod(target, "packSettings", PackSettings); target->Set(context, |