| #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC |
| |
| #include "application.h" |
| #include <async_wrap-inl.h> |
| #include <debug_utils-inl.h> |
| #include <ngtcp2/ngtcp2.h> |
| #include <node_bob.h> |
| #include <node_sockaddr-inl.h> |
| #include <uv.h> |
| #include <v8.h> |
| #include "defs.h" |
| #include "endpoint.h" |
| #include "http3.h" |
| #include "packet.h" |
| #include "session.h" |
| |
| namespace node { |
| |
| using v8::Just; |
| using v8::Local; |
| using v8::Maybe; |
| using v8::Nothing; |
| using v8::Object; |
| using v8::Value; |
| |
| namespace quic { |
| |
| // ============================================================================ |
| // Session::Application_Options |
| const Session::Application_Options Session::Application_Options::kDefault = {}; |
| |
| Session::Application_Options::operator const nghttp3_settings() const { |
| // In theory, Application_Options might contain options for more than just |
| // HTTP/3. Here we extract only the properties that are relevant to HTTP/3. |
| return nghttp3_settings{ |
| max_field_section_size, |
| static_cast<size_t>(qpack_max_dtable_capacity), |
| static_cast<size_t>(qpack_encoder_max_dtable_capacity), |
| static_cast<size_t>(qpack_blocked_streams), |
| enable_connect_protocol, |
| enable_datagrams, |
| }; |
| } |
| |
| std::string Session::Application_Options::ToString() const { |
| DebugIndentScope indent; |
| auto prefix = indent.Prefix(); |
| std::string res("{"); |
| res += prefix + "max header pairs: " + std::to_string(max_header_pairs); |
| res += prefix + "max header length: " + std::to_string(max_header_length); |
| res += prefix + |
| "max field section size: " + std::to_string(max_field_section_size); |
| res += prefix + "qpack max dtable capacity: " + |
| std::to_string(qpack_max_dtable_capacity); |
| res += prefix + "qpack encoder max dtable capacity: " + |
| std::to_string(qpack_encoder_max_dtable_capacity); |
| res += prefix + |
| "qpack blocked streams: " + std::to_string(qpack_blocked_streams); |
| res += prefix + "enable connect protocol: " + |
| (enable_connect_protocol ? std::string("yes") : std::string("no")); |
| res += prefix + "enable datagrams: " + |
| (enable_datagrams ? std::string("yes") : std::string("no")); |
| res += indent.Close(); |
| return res; |
| } |
| |
| Maybe<Session::Application_Options> Session::Application_Options::From( |
| Environment* env, Local<Value> value) { |
| if (value.IsEmpty() || (!value->IsUndefined() && !value->IsObject())) { |
| THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); |
| return Nothing<Application_Options>(); |
| } |
| |
| Application_Options options; |
| auto& state = BindingData::Get(env); |
| if (value->IsUndefined()) { |
| return Just<Application_Options>(options); |
| } |
| |
| auto params = value.As<Object>(); |
| |
| #define SET(name) \ |
| SetOption<Session::Application_Options, \ |
| &Session::Application_Options::name>( \ |
| env, &options, params, state.name##_string()) |
| |
| if (!SET(max_header_pairs) || !SET(max_header_length) || |
| !SET(max_field_section_size) || !SET(qpack_max_dtable_capacity) || |
| !SET(qpack_encoder_max_dtable_capacity) || !SET(qpack_blocked_streams) || |
| !SET(enable_connect_protocol) || !SET(enable_datagrams)) { |
| return Nothing<Application_Options>(); |
| } |
| |
| #undef SET |
| |
| return Just<Application_Options>(options); |
| } |
| |
| // ============================================================================ |
| |
| std::string Session::Application::StreamData::ToString() const { |
| DebugIndentScope indent; |
| auto prefix = indent.Prefix(); |
| std::string res("{"); |
| res += prefix + "count: " + std::to_string(count); |
| res += prefix + "remaining: " + std::to_string(remaining); |
| res += prefix + "id: " + std::to_string(id); |
| res += prefix + "fin: " + std::to_string(fin); |
| res += indent.Close(); |
| return res; |
| } |
| |
| Session::Application::Application(Session* session, const Options& options) |
| : session_(session) {} |
| |
| bool Session::Application::Start() { |
| // By default there is nothing to do. Specific implementations may |
| // override to perform more actions. |
| Debug(session_, "Session application started"); |
| return true; |
| } |
| |
| void Session::Application::AcknowledgeStreamData(Stream* stream, |
| size_t datalen) { |
| Debug(session_, |
| "Application acknowledging stream %" PRIi64 " data: %zu", |
| stream->id(), |
| datalen); |
| DCHECK_NOT_NULL(stream); |
| stream->Acknowledge(datalen); |
| } |
| |
| void Session::Application::BlockStream(int64_t id) { |
| Debug(session_, "Application blocking stream %" PRIi64, id); |
| auto stream = session().FindStream(id); |
| if (stream) stream->EmitBlocked(); |
| } |
| |
| bool Session::Application::CanAddHeader(size_t current_count, |
| size_t current_headers_length, |
| size_t this_header_length) { |
| // By default headers are not supported. |
| Debug(session_, "Application cannot add header"); |
| return false; |
| } |
| |
| bool Session::Application::SendHeaders(const Stream& stream, |
| HeadersKind kind, |
| const v8::Local<v8::Array>& headers, |
| HeadersFlags flags) { |
| // By default do nothing. |
| Debug(session_, "Application cannot send headers"); |
| return false; |
| } |
| |
| void Session::Application::ResumeStream(int64_t id) { |
| Debug(session_, "Application resuming stream %" PRIi64, id); |
| // By default do nothing. |
| } |
| |
| void Session::Application::ExtendMaxStreams(EndpointLabel label, |
| Direction direction, |
| uint64_t max_streams) { |
| Debug(session_, "Application extending max streams"); |
| // By default do nothing. |
| } |
| |
| void Session::Application::ExtendMaxStreamData(Stream* stream, |
| uint64_t max_data) { |
| Debug(session_, "Application extending max stream data"); |
| // By default do nothing. |
| } |
| |
| void Session::Application::CollectSessionTicketAppData( |
| SessionTicket::AppData* app_data) const { |
| Debug(session_, "Application collecting session ticket app data"); |
| // By default do nothing. |
| } |
| |
| SessionTicket::AppData::Status |
| Session::Application::ExtractSessionTicketAppData( |
| const SessionTicket::AppData& app_data, |
| SessionTicket::AppData::Source::Flag flag) { |
| Debug(session_, "Application extracting session ticket app data"); |
| // By default we do not have any application data to retrieve. |
| return flag == SessionTicket::AppData::Source::Flag::STATUS_RENEW |
| ? SessionTicket::AppData::Status::TICKET_USE_RENEW |
| : SessionTicket::AppData::Status::TICKET_USE; |
| } |
| |
| void Session::Application::SetStreamPriority(const Stream& stream, |
| StreamPriority priority, |
| StreamPriorityFlags flags) { |
| Debug( |
| session_, "Application setting stream %" PRIi64 " priority", stream.id()); |
| // By default do nothing. |
| } |
| |
| StreamPriority Session::Application::GetStreamPriority(const Stream& stream) { |
| return StreamPriority::DEFAULT; |
| } |
| |
| Packet* Session::Application::CreateStreamDataPacket() { |
| return Packet::Create(env(), |
| session_->endpoint_.get(), |
| session_->remote_address_, |
| session_->max_packet_size(), |
| "stream data"); |
| } |
| |
| void Session::Application::StreamClose(Stream* stream, QuicError error) { |
| Debug(session_, |
| "Application closing stream %" PRIi64 " with error %s", |
| stream->id(), |
| error); |
| stream->Destroy(error); |
| } |
| |
| void Session::Application::StreamStopSending(Stream* stream, QuicError error) { |
| Debug(session_, |
| "Application stopping sending on stream %" PRIi64 " with error %s", |
| stream->id(), |
| error); |
| DCHECK_NOT_NULL(stream); |
| stream->ReceiveStopSending(error); |
| } |
| |
| void Session::Application::StreamReset(Stream* stream, |
| uint64_t final_size, |
| QuicError error) { |
| Debug(session_, |
| "Application resetting stream %" PRIi64 " with error %s", |
| stream->id(), |
| error); |
| stream->ReceiveStreamReset(final_size, error); |
| } |
| |
| void Session::Application::SendPendingData() { |
| static constexpr size_t kMaxPackets = 32; |
| Debug(session_, "Application sending pending data"); |
| PathStorage path; |
| StreamData stream_data; |
| |
| // The maximum size of packet to create. |
| const size_t max_packet_size = session_->max_packet_size(); |
| |
| // The maximum number of packets to send in this call to SendPendingData. |
| const size_t max_packet_count = std::min( |
| kMaxPackets, ngtcp2_conn_get_send_quantum(*session_) / max_packet_size); |
| |
| // The number of packets that have been sent in this call to SendPendingData. |
| size_t packet_send_count = 0; |
| |
| Packet* packet = nullptr; |
| uint8_t* pos = nullptr; |
| uint8_t* begin = nullptr; |
| |
| auto ensure_packet = [&] { |
| if (packet == nullptr) { |
| packet = CreateStreamDataPacket(); |
| if (packet == nullptr) return false; |
| pos = begin = ngtcp2_vec(*packet).base; |
| } |
| DCHECK_NOT_NULL(packet); |
| DCHECK_NOT_NULL(pos); |
| DCHECK_NOT_NULL(begin); |
| return true; |
| }; |
| |
| // We're going to enter a loop here to prepare and send no more than |
| // max_packet_count packets. |
| for (;;) { |
| // ndatalen is the amount of stream data that was accepted into the packet. |
| ssize_t ndatalen = 0; |
| |
| // Make sure we have a packet to write data into. |
| if (!ensure_packet()) { |
| Debug(session_, "Failed to create packet for stream data"); |
| // Doh! Could not create a packet. Time to bail. |
| session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); |
| return session_->Close(Session::CloseMethod::SILENT); |
| } |
| |
| // The stream_data is the next block of data from the application stream. |
| if (GetStreamData(&stream_data) < 0) { |
| Debug(session_, "Application failed to get stream data"); |
| session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); |
| packet->Done(UV_ECANCELED); |
| return session_->Close(Session::CloseMethod::SILENT); |
| } |
| |
| // If we got here, we were at least successful in checking for stream data. |
| // There might not be any stream data to send. |
| Debug(session_, "Application using stream data: %s", stream_data); |
| |
| // Awesome, let's write our packet! |
| ssize_t nwrite = |
| WriteVStream(&path, pos, &ndatalen, max_packet_size, stream_data); |
| Debug(session_, "Application accepted %zu bytes into packet", ndatalen); |
| |
| // A negative nwrite value indicates either an error or that there is more |
| // data to write into the packet. |
| if (nwrite < 0) { |
| switch (nwrite) { |
| case NGTCP2_ERR_STREAM_DATA_BLOCKED: { |
| // We could not write any data for this stream into the packet because |
| // the flow control for the stream itself indicates that the stream |
| // is blocked. We'll skip and move on to the next stream. |
| // ndatalen = -1 means that no stream data was accepted into the |
| // packet, which is what we want here. |
| DCHECK_EQ(ndatalen, -1); |
| DCHECK(stream_data.stream); |
| session_->StreamDataBlocked(stream_data.id); |
| continue; |
| } |
| case NGTCP2_ERR_STREAM_SHUT_WR: { |
| // Indicates that the writable side of the stream should be closed |
| // locally or the stream is being reset. In either case, we can't send |
| // any stream data! |
| Debug(session_, |
| "Stream %" PRIi64 " should be closed for writing", |
| stream_data.id); |
| // ndatalen = -1 means that no stream data was accepted into the |
| // packet, which is what we want here. |
| DCHECK_EQ(ndatalen, -1); |
| DCHECK(stream_data.stream); |
| stream_data.stream->EndWritable(); |
| continue; |
| } |
| case NGTCP2_ERR_WRITE_MORE: { |
| // This return value indicates that we should call into WriteVStream |
| // again to write more data into the same packet. |
| Debug(session_, "Application should write more to packet"); |
| DCHECK_GE(ndatalen, 0); |
| if (!StreamCommit(&stream_data, ndatalen)) { |
| packet->Done(UV_ECANCELED); |
| return session_->Close(CloseMethod::SILENT); |
| } |
| continue; |
| } |
| } |
| |
| // Some other type of error happened. |
| DCHECK_EQ(ndatalen, -1); |
| Debug(session_, |
| "Application encountered error while writing packet: %s", |
| ngtcp2_strerror(nwrite)); |
| session_->SetLastError(QuicError::ForNgtcp2Error(nwrite)); |
| packet->Done(UV_ECANCELED); |
| return session_->Close(Session::CloseMethod::SILENT); |
| } else if (ndatalen >= 0) { |
| // We wrote some data into the packet. We need to update the flow control |
| // by committing the data. |
| if (!StreamCommit(&stream_data, ndatalen)) { |
| packet->Done(UV_ECANCELED); |
| return session_->Close(CloseMethod::SILENT); |
| } |
| } |
| |
| // When nwrite is zero, it means we are congestion limited. |
| // We should stop trying to send additional packets. |
| if (nwrite == 0) { |
| Debug(session_, "Congestion limited."); |
| // There might be a partial packet already prepared. If so, send it. |
| size_t datalen = pos - begin; |
| if (datalen) { |
| Debug(session_, "Packet has %zu bytes to send", datalen); |
| // At least some data had been written into the packet. We should send |
| // it. |
| packet->Truncate(datalen); |
| session_->Send(packet, path); |
| } else { |
| packet->Done(UV_ECANCELED); |
| } |
| |
| // If there was stream data selected, we should reschedule it to try |
| // sending again. |
| if (stream_data.id >= 0) ResumeStream(stream_data.id); |
| |
| return session_->UpdatePacketTxTime(); |
| } |
| |
| // At this point we have a packet prepared to send. |
| pos += nwrite; |
| size_t datalen = pos - begin; |
| Debug(session_, "Sending packet with %zu bytes", datalen); |
| packet->Truncate(datalen); |
| session_->Send(packet, path); |
| |
| // If we have sent the maximum number of packets, we're done. |
| if (++packet_send_count == max_packet_count) { |
| return session_->UpdatePacketTxTime(); |
| } |
| |
| // Prepare to loop back around to prepare a new packet. |
| packet = nullptr; |
| pos = begin = nullptr; |
| } |
| } |
| |
| ssize_t Session::Application::WriteVStream(PathStorage* path, |
| uint8_t* dest, |
| ssize_t* ndatalen, |
| size_t max_packet_size, |
| const StreamData& stream_data) { |
| DCHECK_LE(stream_data.count, kMaxVectorCount); |
| uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; |
| if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; |
| ngtcp2_pkt_info pi; |
| return ngtcp2_conn_writev_stream(*session_, |
| &path->path, |
| &pi, |
| dest, |
| max_packet_size, |
| ndatalen, |
| flags, |
| stream_data.id, |
| stream_data.buf, |
| stream_data.count, |
| uv_hrtime()); |
| } |
| |
| // The DefaultApplication is the default implementation of Session::Application |
| // that is used for all unrecognized ALPN identifiers. |
| class DefaultApplication final : public Session::Application { |
| public: |
| // Marked NOLINT because the cpp linter gets confused about this using |
| // statement not being sorted with the using v8 statements at the top |
| // of the namespace. |
| using Application::Application; // NOLINT |
| |
| bool ReceiveStreamData(Stream* stream, |
| const uint8_t* data, |
| size_t datalen, |
| Stream::ReceiveDataFlags flags) override { |
| Debug(&session(), "Default application receiving stream data"); |
| DCHECK_NOT_NULL(stream); |
| if (!stream->is_destroyed()) stream->ReceiveData(data, datalen, flags); |
| return true; |
| } |
| |
| int GetStreamData(StreamData* stream_data) override { |
| Debug(&session(), "Default application getting stream data"); |
| DCHECK_NOT_NULL(stream_data); |
| // If the queue is empty, there aren't any streams with data yet |
| if (stream_queue_.IsEmpty()) return 0; |
| |
| const auto get_length = [](auto vec, size_t count) { |
| CHECK_NOT_NULL(vec); |
| size_t len = 0; |
| for (size_t n = 0; n < count; n++) len += vec[n].len; |
| return len; |
| }; |
| |
| Stream* stream = stream_queue_.PopFront(); |
| CHECK_NOT_NULL(stream); |
| stream_data->stream.reset(stream); |
| stream_data->id = stream->id(); |
| auto next = |
| [&](int status, const ngtcp2_vec* data, size_t count, bob::Done done) { |
| switch (status) { |
| case bob::Status::STATUS_BLOCK: |
| // Fall through |
| case bob::Status::STATUS_WAIT: |
| return; |
| case bob::Status::STATUS_EOS: |
| stream_data->fin = 1; |
| } |
| |
| stream_data->count = count; |
| |
| if (count > 0) { |
| stream->Schedule(&stream_queue_); |
| stream_data->remaining = get_length(data, count); |
| } else { |
| stream_data->remaining = 0; |
| } |
| |
| // Not calling done here because we defer committing |
| // the data until after we're sure it's written. |
| }; |
| |
| if (!stream->is_eos()) [[likely]] { |
| int ret = stream->Pull(std::move(next), |
| bob::Options::OPTIONS_SYNC, |
| stream_data->data, |
| arraysize(stream_data->data), |
| kMaxVectorCount); |
| if (ret == bob::Status::STATUS_EOS) { |
| stream_data->fin = 1; |
| } |
| } else { |
| stream_data->fin = 1; |
| } |
| |
| return 0; |
| } |
| |
| void ResumeStream(int64_t id) override { |
| Debug(&session(), "Default application resuming stream %" PRIi64, id); |
| ScheduleStream(id); |
| } |
| |
| bool ShouldSetFin(const StreamData& stream_data) override { |
| auto const is_empty = [](auto vec, size_t cnt) { |
| size_t i; |
| for (i = 0; i < cnt && vec[i].len == 0; ++i) { |
| } |
| return i == cnt; |
| }; |
| |
| return stream_data.stream && is_empty(stream_data.buf, stream_data.count); |
| } |
| |
| bool StreamCommit(StreamData* stream_data, size_t datalen) override { |
| Debug(&session(), "Default application committing stream data"); |
| DCHECK_NOT_NULL(stream_data); |
| const auto consume = [](ngtcp2_vec** pvec, size_t* pcnt, size_t len) { |
| ngtcp2_vec* v = *pvec; |
| size_t cnt = *pcnt; |
| |
| for (; cnt > 0; --cnt, ++v) { |
| if (v->len > len) { |
| v->len -= len; |
| v->base += len; |
| break; |
| } |
| len -= v->len; |
| } |
| |
| *pvec = v; |
| *pcnt = cnt; |
| }; |
| |
| CHECK(stream_data->stream); |
| stream_data->remaining -= datalen; |
| consume(&stream_data->buf, &stream_data->count, datalen); |
| stream_data->stream->Commit(datalen); |
| return true; |
| } |
| |
| SET_SELF_SIZE(DefaultApplication) |
| SET_MEMORY_INFO_NAME(DefaultApplication) |
| SET_NO_MEMORY_INFO() |
| |
| private: |
| void ScheduleStream(int64_t id) { |
| Debug(&session(), "Default application scheduling stream %" PRIi64, id); |
| auto stream = session().FindStream(id); |
| if (stream && !stream->is_destroyed()) { |
| stream->Schedule(&stream_queue_); |
| } |
| } |
| |
| void UnscheduleStream(int64_t id) { |
| Debug(&session(), "Default application unscheduling stream %" PRIi64, id); |
| auto stream = session().FindStream(id); |
| if (stream && !stream->is_destroyed()) stream->Unschedule(); |
| } |
| |
| Stream::Queue stream_queue_; |
| }; |
| |
| std::unique_ptr<Session::Application> Session::select_application() { |
| // In the future, we may end up supporting additional QUIC protocols. As they |
| // are added, extend the cases here to create and return them. |
| |
| if (config_.options.tls_options.alpn == NGHTTP3_ALPN_H3) { |
| Debug(this, "Selecting HTTP/3 application"); |
| return createHttp3Application(this, config_.options.application_options); |
| } |
| |
| Debug(this, "Selecting default application"); |
| return std::make_unique<DefaultApplication>( |
| this, config_.options.application_options); |
| } |
| |
| } // namespace quic |
| } // namespace node |
| |
| #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC |