#ifndef SRC_NODE_HTTP2_CORE_INL_H_ #define SRC_NODE_HTTP2_CORE_INL_H_ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include "node_http2_core.h" #include "node_internals.h" // arraysize #include "freelist.h" namespace node { namespace http2 { #define FREELIST_MAX 1024 #define LINKED_LIST_ADD(list, item) \ do { \ if (list ## _tail_ == nullptr) { \ list ## _head_ = item; \ list ## _tail_ = item; \ } else { \ list ## _tail_->next = item; \ list ## _tail_ = item; \ } \ } while (0); extern Freelist data_chunk_free_list; extern Freelist stream_free_list; extern Freelist header_free_list; #ifdef NODE_DEBUG_HTTP2 inline int Nghttp2Session::OnNghttpError(nghttp2_session* session, const char* message, size_t len, void* user_data) { Nghttp2Session* handle = static_cast(user_data); DEBUG_HTTP2("Nghttp2Session %s: Error '%.*s'\n", handle->TypeName(), len, message); return 0; } #endif // nghttp2 calls this at the beginning a new HEADERS or PUSH_PROMISE frame. // We use it to ensure that an Nghttp2Stream instance is allocated to store // the state. inline int Nghttp2Session::OnBeginHeadersCallback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) { Nghttp2Session* handle = static_cast(user_data); // If this is a push promise frame, we want to grab the handle of // the promised stream. int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? frame->push_promise.promised_stream_id : frame->hd.stream_id; DEBUG_HTTP2("Nghttp2Session %s: beginning headers for stream %d\n", handle->TypeName(), id); Nghttp2Stream* stream = handle->FindStream(id); if (stream == nullptr) { Nghttp2Stream::Init(id, handle, frame->headers.cat); } else { stream->StartHeaders(frame->headers.cat); } return 0; } // nghttp2 calls this once for every header name-value pair in a HEADERS // or PUSH_PROMISE block. CONTINUATION frames are handled automatically // and transparently so we do not need to worry about those at all. inline int Nghttp2Session::OnHeaderCallback(nghttp2_session* session, const nghttp2_frame* frame, nghttp2_rcbuf *name, nghttp2_rcbuf *value, uint8_t flags, void* user_data) { Nghttp2Session* handle = static_cast(user_data); // If this is a push promise frame, we want to grab the handle of // the promised stream. int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? frame->push_promise.promised_stream_id : frame->hd.stream_id; Nghttp2Stream* stream = handle->FindStream(id); nghttp2_header_list* header = header_free_list.pop(); header->name = name; header->value = value; nghttp2_rcbuf_incref(name); nghttp2_rcbuf_incref(value); LINKED_LIST_ADD(stream->current_headers, header); return 0; } // When nghttp2 has completely processed a frame, it calls OnFrameReceive. // It is our responsibility to delegate out from there. We can ignore most // control frames since nghttp2 will handle those for us. inline int Nghttp2Session::OnFrameReceive(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) { Nghttp2Session* handle = static_cast(user_data); DEBUG_HTTP2("Nghttp2Session %s: complete frame received: type: %d\n", handle->TypeName(), frame->hd.type); bool ack; switch (frame->hd.type) { case NGHTTP2_DATA: handle->HandleDataFrame(frame); break; case NGHTTP2_PUSH_PROMISE: case NGHTTP2_HEADERS: handle->HandleHeadersFrame(frame); break; case NGHTTP2_SETTINGS: ack = (frame->hd.flags & NGHTTP2_FLAG_ACK) == NGHTTP2_FLAG_ACK; handle->OnSettings(ack); break; case NGHTTP2_PRIORITY: handle->HandlePriorityFrame(frame); break; case NGHTTP2_GOAWAY: handle->HandleGoawayFrame(frame); break; default: break; } return 0; } inline int Nghttp2Session::OnFrameNotSent(nghttp2_session *session, const nghttp2_frame *frame, int error_code, void *user_data) { Nghttp2Session *handle = static_cast(user_data); DEBUG_HTTP2("Nghttp2Session %s: frame type %d was not sent, code: %d\n", handle->TypeName(), 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) handle->OnFrameError(frame->hd.stream_id, frame->hd.type, error_code); return 0; } inline int Nghttp2Session::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; } // Called when nghttp2 closes a stream, either in response to an RST_STREAM // frame or the stream closing naturally on it's own inline int Nghttp2Session::OnStreamClose(nghttp2_session *session, int32_t id, uint32_t code, void *user_data) { Nghttp2Session *handle = static_cast(user_data); DEBUG_HTTP2("Nghttp2Session %s: stream %d closed, code: %d\n", handle->TypeName(), id, code); Nghttp2Stream *stream = handle->FindStream(id); // Intentionally ignore the callback if the stream does not exist if (stream != nullptr) stream->Close(code); return 0; } // Called by nghttp2 to collect the data while a file response is sent. // The buf is the DATA frame buffer that needs to be filled with at most // length bytes. flags is used to control what nghttp2 does next. inline ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session *session, int32_t id, uint8_t *buf, size_t length, uint32_t *flags, nghttp2_data_source *source, void *user_data) { Nghttp2Session *handle = static_cast(user_data); DEBUG_HTTP2("Nghttp2Session %s: reading outbound file data for stream %d\n", handle->TypeName(), id); Nghttp2Stream *stream = handle->FindStream(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(length)) length = stream->fd_length_; uv_buf_t data; data.base = reinterpret_cast(buf); data.len = length; uv_fs_t read_req; if (length > 0) { numchars = uv_fs_read(handle->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(numchars) < length || length <= 0) { DEBUG_HTTP2("Nghttp2Session %s: no more data for stream %d\n", handle->TypeName(), id); *flags |= NGHTTP2_DATA_FLAG_EOF; GetTrailers(session, handle, stream, flags); } return numchars; } // Called by nghttp2 to collect the data to pack within a DATA frame. // The buf is the DATA frame buffer that needs to be filled with at most // length bytes. flags is used to control what nghttp2 does next. inline ssize_t Nghttp2Session::OnStreamRead(nghttp2_session *session, int32_t id, uint8_t *buf, size_t length, uint32_t *flags, nghttp2_data_source *source, void *user_data) { Nghttp2Session *handle = static_cast(user_data); DEBUG_HTTP2("Nghttp2Session %s: reading outbound data for stream %d\n", handle->TypeName(), id); Nghttp2Stream *stream = handle->FindStream(id); size_t remaining = length; size_t offset = 0; // While there is data in the queue, copy data into buf until it is full. // There may be data left over, which will be sent the next time nghttp // calls this callback. while (stream->queue_head_ != nullptr) { DEBUG_HTTP2("Nghttp2Session %s: processing outbound data chunk\n", handle->TypeName()); nghttp2_stream_write_queue *head = stream->queue_head_; while (stream->queue_head_index_ < head->nbufs) { if (remaining == 0) goto end; unsigned int n = stream->queue_head_index_; // len is the number of bytes in head->bufs[n] that are yet to be written size_t len = head->bufs[n].len - stream->queue_head_offset_; size_t bytes_to_write = len < remaining ? len : remaining; memcpy(buf + offset, head->bufs[n].base + stream->queue_head_offset_, bytes_to_write); offset += bytes_to_write; remaining -= bytes_to_write; if (bytes_to_write < len) { stream->queue_head_offset_ += bytes_to_write; } else { stream->queue_head_index_++; stream->queue_head_offset_ = 0; } } stream->queue_head_offset_ = 0; stream->queue_head_index_ = 0; stream->queue_head_ = head->next; head->cb(head->req, 0); delete head; } stream->queue_tail_ = nullptr; end: // If we are no longer writable and there is no more data in the queue, // then we need to set the NGHTTP2_DATA_FLAG_EOF flag. // If we are still writable but there is not yet any data to send, set the // NGHTTP2_ERR_DEFERRED flag. This will put the stream into a pending state // that will wait for data to become available. // If neither of these flags are set, then nghttp2 will call this callback // again to get the data for the next DATA frame. int writable = stream->queue_head_ != nullptr || stream->IsWritable(); if (offset == 0 && writable && stream->queue_head_ == nullptr) { DEBUG_HTTP2("Nghttp2Session %s: deferring stream %d\n", handle->TypeName(), id); return NGHTTP2_ERR_DEFERRED; } if (!writable) { DEBUG_HTTP2("Nghttp2Session %s: no more data for stream %d\n", handle->TypeName(), id); *flags |= NGHTTP2_DATA_FLAG_EOF; GetTrailers(session, handle, stream, flags); } CHECK(offset <= length); return offset; } // Called by nghttp2 when it needs to determine how much padding to apply // to a DATA or HEADERS frame inline ssize_t Nghttp2Session::OnSelectPadding(nghttp2_session *session, const nghttp2_frame *frame, size_t maxPayloadLen, void *user_data) { Nghttp2Session *handle = static_cast(user_data); CHECK(handle->HasGetPaddingCallback()); ssize_t padding = handle->GetPadding(frame->hd.length, maxPayloadLen); DEBUG_HTTP2("Nghttp2Session %s: using padding, size: %d\n", handle->TypeName(), padding); return padding; } // Called by nghttp2 multiple times while processing a DATA frame inline int Nghttp2Session::OnDataChunkReceived(nghttp2_session *session, uint8_t flags, int32_t id, const uint8_t *data, size_t len, void *user_data) { Nghttp2Session *handle = static_cast(user_data); DEBUG_HTTP2("Nghttp2Session %s: buffering data chunk for stream %d, size: " "%d, flags: %d\n", handle->TypeName(), id, len, flags); Nghttp2Stream *stream = handle->FindStream(id); nghttp2_data_chunk_t *chunk = data_chunk_free_list.pop(); chunk->buf = uv_buf_init(new char[len], len); memcpy(chunk->buf.base, data, len); if (stream->data_chunks_tail_ == nullptr) { stream->data_chunks_head_ = stream->data_chunks_tail_ = chunk; } else { stream->data_chunks_tail_->next = chunk; stream->data_chunks_tail_ = chunk; } return 0; } inline void Nghttp2Session::GetTrailers(nghttp2_session *session, Nghttp2Session *handle, Nghttp2Stream *stream, uint32_t *flags) { if (stream->GetTrailers()) { // Only when we are done sending the last chunk of data do we check for // any trailing headers that are to be sent. This is the only opportunity // we have to make this check. If there are trailers, then the // NGHTTP2_DATA_FLAG_NO_END_STREAM flag must be set. SubmitTrailers submit_trailers{handle, stream, flags}; handle->OnTrailers(stream, submit_trailers); } } inline void Nghttp2Session::SubmitTrailers::Submit(nghttp2_nv *trailers, size_t length) const { if (length == 0) return; DEBUG_HTTP2("Nghttp2Session %s: sending trailers for stream %d, " "count: %d\n", handle_->TypeName(), stream_->id(), length); *flags_ |= NGHTTP2_DATA_FLAG_NO_END_STREAM; nghttp2_submit_trailer(handle_->session_, stream_->id(), trailers, length); } // See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html inline void Nghttp2Session::SubmitShutdownNotice() { DEBUG_HTTP2("Nghttp2Session %s: submitting shutdown notice\n", TypeName()); nghttp2_submit_shutdown_notice(session_); } // Sends a SETTINGS frame on the current session // Note that this *should* send a SETTINGS frame even if niv == 0 and there // are no settings entries to send. inline int Nghttp2Session::SubmitSettings(const nghttp2_settings_entry iv[], size_t niv) { DEBUG_HTTP2("Nghttp2Session %s: submitting settings, count: %d\n", TypeName(), niv); return nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv); } // Returns the Nghttp2Stream associated with the given id, or nullptr if none inline Nghttp2Stream* Nghttp2Session::FindStream(int32_t id) { auto s = streams_.find(id); if (s != streams_.end()) { DEBUG_HTTP2("Nghttp2Session %s: stream %d found\n", TypeName(), id); return s->second; } else { DEBUG_HTTP2("Nghttp2Session %s: stream %d not found\n", TypeName(), id); return nullptr; } } // Flushes any received queued chunks of data out to the JS layer inline void Nghttp2Stream::FlushDataChunks(bool done) { while (data_chunks_head_ != nullptr) { DEBUG_HTTP2("Nghttp2Stream %d: emitting data chunk\n", id_); nghttp2_data_chunk_t* item = data_chunks_head_; data_chunks_head_ = item->next; // item will be passed to the Buffer instance and freed on gc session_->OnDataChunk(this, item); } data_chunks_tail_ = nullptr; if (done) session_->OnDataChunk(this, nullptr); } // Passes all of the the chunks for a data frame out to the JS layer // The chunks are collected as the frame is being processed and sent out // to the JS side only when the frame is fully processed. inline void Nghttp2Session::HandleDataFrame(const nghttp2_frame* frame) { int32_t id = frame->hd.stream_id; DEBUG_HTTP2("Nghttp2Session %s: handling data frame for stream %d\n", TypeName(), id); Nghttp2Stream* stream = this->FindStream(id); // If the stream does not exist, something really bad happened CHECK_NE(stream, nullptr); bool done = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == NGHTTP2_FLAG_END_STREAM; stream->FlushDataChunks(done); } // Passes all of the collected headers for a HEADERS frame out to the JS layer. // The headers are collected as the frame is being processed and sent out // to the JS side only when the frame is fully processed. inline void Nghttp2Session::HandleHeadersFrame(const nghttp2_frame* frame) { int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? frame->push_promise.promised_stream_id : frame->hd.stream_id; DEBUG_HTTP2("Nghttp2Session %s: handling headers frame for stream %d\n", TypeName(), id); Nghttp2Stream* stream = FindStream(id); // If the stream does not exist, something really bad happened CHECK_NE(stream, nullptr); OnHeaders(stream, stream->headers(), stream->headers_category(), frame->hd.flags); stream->FreeHeaders(); } // Notifies the JS layer that a PRIORITY frame has been received inline void Nghttp2Session::HandlePriorityFrame(const nghttp2_frame* frame) { nghttp2_priority priority_frame = frame->priority; int32_t id = frame->hd.stream_id; DEBUG_HTTP2("Nghttp2Session %s: handling priority frame for stream %d\n", TypeName(), id); // Ignore the priority frame if stream ID is <= 0 // This actually should never happen because nghttp2 should treat this as // an error condition that terminates the session. if (id > 0) { nghttp2_priority_spec spec = priority_frame.pri_spec; OnPriority(id, spec.stream_id, spec.weight, spec.exclusive); } } // Notifies the JS layer that a GOAWAY frame has been received inline void Nghttp2Session::HandleGoawayFrame(const nghttp2_frame* frame) { nghttp2_goaway goaway_frame = frame->goaway; DEBUG_HTTP2("Nghttp2Session %s: handling goaway frame\n", TypeName()); OnGoAway(goaway_frame.last_stream_id, goaway_frame.error_code, goaway_frame.opaque_data, goaway_frame.opaque_data_len); } // Prompts nghttp2 to flush the queue of pending data frames inline void Nghttp2Session::SendPendingData() { DEBUG_HTTP2("Nghttp2Session %s: Sending pending data\n", TypeName()); // 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; uv_buf_t dest; AllocateSend(SEND_BUFFER_RECOMMENDED_SIZE, &dest); size_t destLength = 0; // amount of data stored in dest size_t destRemaining = dest.len; // amount space remaining 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) { DEBUG_HTTP2("Nghttp2Session %s: nghttp2 has %d bytes to send\n", TypeName(), 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_HTTP2("Nghttp2Session %s: pushing %d bytes to the socket\n", TypeName(), destRemaining); memcpy(dest.base + destOffset, src + srcOffset, destRemaining); destLength += destRemaining; Send(&dest, destLength); destOffset = 0; destLength = 0; srcRemaining -= destRemaining; srcOffset += destRemaining; destRemaining = dest.len; } if (srcRemaining > 0) { memcpy(dest.base + destOffset, src + srcOffset, srcRemaining); destLength += srcRemaining; destOffset += srcRemaining; destRemaining -= srcRemaining; srcRemaining = 0; srcOffset = 0; } } if (destLength > 0) { DEBUG_HTTP2("Nghttp2Session %s: pushing %d bytes to the socket\n", TypeName(), destLength); Send(&dest, destLength); } } // Initialize the Nghttp2Session handle by creating and // assigning the Nghttp2Session instance and associated // uv_loop_t. inline int Nghttp2Session::Init(uv_loop_t* loop, const nghttp2_session_type type, nghttp2_option* options, nghttp2_mem* mem) { loop_ = loop; session_type_ = type; DEBUG_HTTP2("Nghttp2Session %s: initializing session\n", TypeName()); destroying_ = false; int ret = 0; nghttp2_session_callbacks* callbacks = callback_struct_saved[HasGetPaddingCallback() ? 1 : 0].callbacks; nghttp2_option* opts; if (options != nullptr) { opts = options; } else { nghttp2_option_new(&opts); } switch (type) { case NGHTTP2_SESSION_SERVER: ret = nghttp2_session_server_new3(&session_, callbacks, this, opts, mem); break; case NGHTTP2_SESSION_CLIENT: ret = nghttp2_session_client_new3(&session_, callbacks, this, opts, mem); break; } if (opts != options) { nghttp2_option_del(opts); } // For every node::Http2Session instance, there is a uv_prep_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. uv_prepare_init(loop_, &prep_); uv_prepare_start(&prep_, [](uv_prepare_t* t) { Nghttp2Session* session = ContainerOf(&Nghttp2Session::prep_, t); session->SendPendingData(); }); // uv_unref(reinterpret_cast(&prep_)); return ret; } inline void Nghttp2Session::MarkDestroying() { destroying_ = true; } inline int Nghttp2Session::Free() { CHECK(session_ != nullptr); DEBUG_HTTP2("Nghttp2Session %s: freeing session\n", TypeName()); // Stop the loop uv_prepare_stop(&prep_); auto PrepClose = [](uv_handle_t* handle) { Nghttp2Session* session = ContainerOf(&Nghttp2Session::prep_, reinterpret_cast(handle)); session->OnFreeSession(); }; uv_close(reinterpret_cast(&prep_), PrepClose); nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR); nghttp2_session_del(session_); session_ = nullptr; loop_ = nullptr; DEBUG_HTTP2("Nghttp2Session %s: session freed\n", TypeName()); return 1; } // Write data received from the socket to the underlying nghttp2_session. inline ssize_t Nghttp2Session::Write(const uv_buf_t* bufs, unsigned int nbufs) { size_t total = 0; for (unsigned int n = 0; n < nbufs; n++) { ssize_t ret = nghttp2_session_mem_recv(session_, reinterpret_cast(bufs[n].base), bufs[n].len); if (ret < 0) { return ret; } else { total += ret; } } SendPendingData(); return total; } inline void Nghttp2Session::AddStream(Nghttp2Stream* stream) { streams_[stream->id()] = stream; } // Removes a stream instance from this session inline void Nghttp2Session::RemoveStream(int32_t id) { streams_.erase(id); } // Implementation for Nghttp2Stream functions inline Nghttp2Stream* Nghttp2Stream::Init( int32_t id, Nghttp2Session* session, nghttp2_headers_category category, int options) { DEBUG_HTTP2("Nghttp2Stream %d: initializing stream\n", id); Nghttp2Stream* stream = stream_free_list.pop(); stream->ResetState(id, session, category, options); session->AddStream(stream); return stream; } // Resets the state of the stream instance to defaults inline void Nghttp2Stream::ResetState( int32_t id, Nghttp2Session* session, nghttp2_headers_category category, int options) { DEBUG_HTTP2("Nghttp2Stream %d: resetting stream state\n", id); session_ = session; queue_head_ = nullptr; queue_tail_ = nullptr; data_chunks_head_ = nullptr; data_chunks_tail_ = nullptr; current_headers_head_ = nullptr; current_headers_tail_ = nullptr; current_headers_category_ = category; flags_ = NGHTTP2_STREAM_FLAG_NONE; id_ = id; code_ = NGHTTP2_NO_ERROR; prev_local_window_size_ = 65535; queue_head_index_ = 0; queue_head_offset_ = 0; getTrailers_ = options & STREAM_OPTION_GET_TRAILERS; } inline void Nghttp2Stream::Destroy() { DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_); // Do nothing if this stream instance is already destroyed if (IsDestroyed()) return; flags_ |= NGHTTP2_STREAM_FLAG_DESTROYED; Nghttp2Session* session = this->session_; if (session != nullptr) { // Remove this stream from the associated session session_->RemoveStream(this->id()); session_ = nullptr; } // Free any remaining incoming data chunks. while (data_chunks_head_ != nullptr) { nghttp2_data_chunk_t* chunk = data_chunks_head_; data_chunks_head_ = chunk->next; delete[] chunk->buf.base; data_chunk_free_list.push(chunk); } data_chunks_tail_ = nullptr; // Free any remaining outgoing data chunks. while (queue_head_ != nullptr) { nghttp2_stream_write_queue* head = queue_head_; queue_head_ = head->next; head->cb(head->req, UV_ECANCELED); delete head; } queue_tail_ = nullptr; // Free any remaining headers FreeHeaders(); // Return this stream instance to the freelist stream_free_list.push(this); } inline void Nghttp2Stream::FreeHeaders() { DEBUG_HTTP2("Nghttp2Stream %d: freeing headers\n", id_); while (current_headers_head_ != nullptr) { DEBUG_HTTP2("Nghttp2Stream %d: freeing header item\n", id_); nghttp2_header_list* item = current_headers_head_; current_headers_head_ = item->next; header_free_list.push(item); } current_headers_tail_ = nullptr; } // Submit informational headers for a stream. inline int Nghttp2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) { DEBUG_HTTP2("Nghttp2Stream %d: sending informational headers, count: %d\n", id_, len); CHECK_GT(len, 0); return nghttp2_submit_headers(session_->session(), NGHTTP2_FLAG_NONE, id_, nullptr, nva, len, nullptr); } inline int Nghttp2Stream::SubmitPriority(nghttp2_priority_spec* prispec, bool silent) { DEBUG_HTTP2("Nghttp2Stream %d: sending priority spec\n", id_); return silent ? nghttp2_session_change_stream_priority(session_->session(), id_, prispec) : nghttp2_submit_priority(session_->session(), NGHTTP2_FLAG_NONE, id_, prispec); } // Submit an RST_STREAM frame inline int Nghttp2Stream::SubmitRstStream(const uint32_t code) { DEBUG_HTTP2("Nghttp2Stream %d: sending rst-stream, code: %d\n", id_, code); session_->SendPendingData(); return nghttp2_submit_rst_stream(session_->session(), NGHTTP2_FLAG_NONE, id_, code); } // Submit a push promise. inline int32_t Nghttp2Stream::SubmitPushPromise( nghttp2_nv* nva, size_t len, Nghttp2Stream** assigned, int options) { CHECK_GT(len, 0); DEBUG_HTTP2("Nghttp2Stream %d: sending push promise\n", id_); int32_t ret = nghttp2_submit_push_promise(session_->session(), NGHTTP2_FLAG_NONE, id_, nva, len, nullptr); if (ret > 0) { auto stream = Nghttp2Stream::Init(ret, session_); if (options & STREAM_OPTION_EMPTY_PAYLOAD) stream->Shutdown(); if (assigned != nullptr) *assigned = stream; } return ret; } // Initiate a response. If the nghttp2_stream is still writable by // the time this is called, then an nghttp2_data_provider will be // initialized, causing at least one (possibly empty) data frame to // be sent. inline int Nghttp2Stream::SubmitResponse(nghttp2_nv* nva, size_t len, int options) { CHECK_GT(len, 0); DEBUG_HTTP2("Nghttp2Stream %d: submitting response\n", id_); getTrailers_ = options & STREAM_OPTION_GET_TRAILERS; nghttp2_data_provider* provider = nullptr; nghttp2_data_provider prov; prov.source.ptr = this; prov.read_callback = Nghttp2Session::OnStreamRead; if (IsWritable() && !(options & STREAM_OPTION_EMPTY_PAYLOAD)) provider = &prov; return nghttp2_submit_response(session_->session(), id_, nva, len, provider); } // Initiate a response that contains data read from a file descriptor. inline int Nghttp2Stream::SubmitFile(int fd, nghttp2_nv* nva, size_t len, int64_t offset, int64_t length, int options) { CHECK_GT(len, 0); CHECK_GT(fd, 0); DEBUG_HTTP2("Nghttp2Stream %d: submitting file\n", id_); getTrailers_ = options & STREAM_OPTION_GET_TRAILERS; nghttp2_data_provider prov; prov.source.ptr = this; prov.source.fd = fd; prov.read_callback = Nghttp2Session::OnStreamReadFD; if (offset > 0) fd_offset_ = offset; if (length > -1) fd_length_ = length; return nghttp2_submit_response(session_->session(), id_, nva, len, &prov); } // Initiate a request. If writable is true (the default), then // an nghttp2_data_provider will be initialized, causing at // least one (possibly empty) data frame to to be sent. inline int32_t Nghttp2Session::SubmitRequest( nghttp2_priority_spec* prispec, nghttp2_nv* nva, size_t len, Nghttp2Stream** assigned, int options) { CHECK_GT(len, 0); DEBUG_HTTP2("Nghttp2Session: submitting request\n"); nghttp2_data_provider* provider = nullptr; nghttp2_data_provider prov; prov.source.ptr = this; prov.read_callback = OnStreamRead; if (!(options & STREAM_OPTION_EMPTY_PAYLOAD)) provider = &prov; int32_t ret = nghttp2_submit_request(session_, prispec, nva, len, provider, nullptr); // Assign the Nghttp2Stream handle if (ret > 0) { Nghttp2Stream* stream = Nghttp2Stream::Init(ret, this, NGHTTP2_HCAT_HEADERS, options); if (options & STREAM_OPTION_EMPTY_PAYLOAD) stream->Shutdown(); if (assigned != nullptr) *assigned = stream; } return ret; } // 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 Nghttp2Stream::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_HTTP2("Nghttp2Stream %d: queuing buffers to send, count: %d\n", id_, nbufs); nghttp2_stream_write_queue* item = new nghttp2_stream_write_queue; item->cb = cb; item->req = req; item->nbufs = nbufs; item->bufs.AllocateSufficientStorage(nbufs); req->handle = this; req->item = item; memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs)); if (queue_head_ == nullptr) { queue_head_ = item; queue_tail_ = item; } else { queue_tail_->next = item; queue_tail_ = item; } nghttp2_session_resume_data(session_->session(), id_); return 0; } inline void Nghttp2Stream::ReadStart() { // Has no effect if IsReading() is true. if (IsReading()) return; DEBUG_HTTP2("Nghttp2Stream %d: start reading\n", id_); if (IsPaused()) { // If handle->reading is less than zero, read_start had never previously // been called. If handle->reading is zero, reading had started and read // stop had been previously called, meaning that the flow control window // has been explicitly set to zero. Reset the flow control window now to // restart the flow of data. nghttp2_session_set_local_window_size(session_->session(), NGHTTP2_FLAG_NONE, id_, prev_local_window_size_); } flags_ |= NGHTTP2_STREAM_FLAG_READ_START; flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED; // Flush any queued data chunks immediately out to the JS layer FlushDataChunks(); } inline void Nghttp2Stream::ReadStop() { DEBUG_HTTP2("Nghttp2Stream %d: stop reading\n", id_); // Has no effect if IsReading() is false, which will happen if we either // have not started reading yet at all (NGHTTP2_STREAM_FLAG_READ_START is not // set) or if we're already paused (NGHTTP2_STREAM_FLAG_READ_PAUSED is set. if (!IsReading()) return; flags_ |= NGHTTP2_STREAM_FLAG_READ_PAUSED; // When not reading, explicitly set the local window size to 0 so that // the peer does not keep sending data that has to be buffered int32_t ret = nghttp2_session_get_stream_local_window_size(session_->session(), id_); if (ret >= 0) prev_local_window_size_ = ret; nghttp2_session_set_local_window_size(session_->session(), NGHTTP2_FLAG_NONE, id_, 0); } Nghttp2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) { nghttp2_session_callbacks_new(&callbacks); 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); #ifdef NODE_DEBUG_HTTP2 nghttp2_session_callbacks_set_error_callback( callbacks, OnNghttpError); #endif if (kHasGetPaddingCallback) { nghttp2_session_callbacks_set_select_padding_callback( callbacks, OnSelectPadding); } } Nghttp2Session::Callbacks::~Callbacks() { nghttp2_session_callbacks_del(callbacks); } Nghttp2Session::SubmitTrailers::SubmitTrailers( Nghttp2Session* handle, Nghttp2Stream* stream, uint32_t* flags) : handle_(handle), stream_(stream), flags_(flags) { } } // namespace http2 } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #endif // SRC_NODE_HTTP2_CORE_INL_H_