blob: 876290bbbbb2c1e1fb64ef7cb32363130fe426f5 [file] [edit]
#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