blob: d63e66988ac2accc146ec8d320628e4b27daf6c7 [file] [edit]
#include "node_quic_stream-inl.h" // NOLINT(build/include)
#include "aliased_struct-inl.h"
#include "async_wrap-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "node.h"
#include "node_buffer.h"
#include "node_internals.h"
#include "stream_base-inl.h"
#include "node_sockaddr-inl.h"
#include "node_http_common-inl.h"
#include "node_quic_session-inl.h"
#include "node_quic_socket-inl.h"
#include "node_quic_util-inl.h"
#include "v8.h"
#include "uv.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
namespace node {
using v8::Array;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::ObjectTemplate;
using v8::PropertyAttribute;
using v8::String;
using v8::Value;
namespace quic {
QuicStream::QuicStream(
QuicSession* sess,
Local<Object> wrap,
int64_t stream_id,
int64_t push_id)
: AsyncWrap(sess->env(), wrap, AsyncWrap::PROVIDER_QUICSTREAM),
StreamBase(sess->env()),
StatsBase(sess->env(), wrap,
HistogramOptions::ACK |
HistogramOptions::RATE |
HistogramOptions::SIZE),
session_(sess),
stream_id_(stream_id),
push_id_(push_id),
state_(sess->env()->isolate()),
quic_state_(sess->quic_state()) {
CHECK_NOT_NULL(sess);
Debug(this, "Created");
StreamBase::AttachToObject(GetObject());
wrap->DefineOwnProperty(
env()->context(),
env()->state_string(),
state_.GetArrayBuffer(),
PropertyAttribute::ReadOnly).Check();
ngtcp2_transport_params params;
ngtcp2_conn_get_local_transport_params(session()->connection(), &params);
IncrementStat(&QuicStreamStats::max_offset, params.initial_max_data);
}
QuicStream::~QuicStream() {
DebugStats();
}
template <typename Fn>
void QuicStreamStatsTraits::ToString(const QuicStream& ptr, Fn&& add_field) {
#define V(_n, name, label) \
add_field(label, ptr.GetStat(&QuicStreamStats::name));
STREAM_STATS(V)
#undef V
}
// Acknowledge is called when ngtcp2 has received an acknowledgement
// for one or more stream frames for this QuicStream. This will cause
// data stored in the streambuf_ outbound queue to be consumed and may
// result in the JavaScript callback for the write to be invoked.
void QuicStream::Acknowledge(uint64_t offset, size_t datalen) {
if (is_destroyed())
return;
// ngtcp2 guarantees that offset must always be greater
// than the previously received offset, but let's just
// make sure that holds.
CHECK_GE(offset, GetStat(&QuicStreamStats::max_offset_ack));
SetStat(&QuicStreamStats::max_offset_ack, offset);
Debug(this, "Acknowledging %d bytes", datalen);
// Consumes the given number of bytes in the buffer. This may
// have the side-effect of causing the onwrite callback to be
// invoked if a complete chunk of buffered data has been acknowledged.
streambuf_.Consume(datalen);
RecordAck(&QuicStreamStats::acked_at);
}
// While not all QUIC applications will support headers, QuicStream
// includes basic, generic support for storing them.
bool QuicStream::AddHeader(std::unique_ptr<QuicHeader> header) {
size_t len = header->length();
QuicApplication* app = session()->application();
// We cannot add the header if we've either reached
// * the max number of header pairs or
// * the max number of header bytes
if (headers_.size() == app->max_header_pairs() ||
current_headers_length_ + len > app->max_header_length()) {
return false;
}
current_headers_length_ += header->length();
Debug(this, "Header - %s", header.get());
headers_.emplace_back(std::move(header));
return true;
}
std::string QuicStream::diagnostic_name() const {
return std::string("QuicStream ") + std::to_string(stream_id_) +
" (" + std::to_string(static_cast<int64_t>(get_async_id())) +
", " + session_->diagnostic_name() + ")";
}
void QuicStream::Destroy(QuicError* error) {
if (destroyed_)
return;
destroyed_ = true;
if (is_writable() || is_readable())
session()->ShutdownStream(id(), 0);
CancelPendingWrites();
session_->RemoveStream(stream_id_);
}
// Do shutdown is called when the JS stream writable side is closed.
// If we're not within an ngtcp2 callback, this will trigger the
// QuicSession to send any pending data. If a final stream frame
// has not already been sent, it will be after this.
int QuicStream::DoShutdown(ShutdownWrap* req_wrap) {
if (is_destroyed())
return UV_EPIPE;
// If the fin bit has already been sent, we can return
// immediately because there's nothing else to do. The
// _final callback will be invoked immediately.
if (state_->fin_sent || !is_writable()) {
Debug(this, "Shutdown write immediately");
return 1;
}
Debug(this, "Deferred shutdown. Waiting for fin sent");
CHECK_NULL(shutdown_done_);
CHECK_NOT_NULL(req_wrap);
shutdown_done_ = [=](int status) {
CHECK_NOT_NULL(req_wrap);
shutdown_done_ = nullptr;
req_wrap->Done(status);
};
QuicSession::SendSessionScope send_scope(session());
Debug(this, "Shutdown writable side");
RecordTimestamp(&QuicStreamStats::closing_at);
state_->write_ended = 1;
streambuf_.End();
session()->ResumeStream(stream_id_);
return 0;
}
int QuicStream::DoWrite(
WriteWrap* req_wrap,
uv_buf_t* bufs,
size_t nbufs,
uv_stream_t* send_handle) {
CHECK_NULL(send_handle);
CHECK(!streambuf_.is_ended());
// A write should not have happened if we've been destroyed or
// the QuicStream is no longer (or was never) writable.
if (is_destroyed() || !is_writable()) {
req_wrap->Done(UV_EPIPE);
return 0;
}
// Nothing to write.
size_t length = get_length(bufs, nbufs);
if (length == 0) {
req_wrap->Done(0);
return 0;
}
QuicSession::SendSessionScope send_scope(session());
Debug(this, "Queuing %" PRIu64 " bytes of data from %d buffers",
length, nbufs);
IncrementStat(&QuicStreamStats::bytes_sent, static_cast<uint64_t>(length));
BaseObjectPtr<AsyncWrap> strong_ref{req_wrap->GetAsyncWrap()};
// The list of buffers will be appended onto streambuf_ without
// copying. Those will remain in the buffer until the serialized
// stream frames are acknowledged.
// This callback function will be invoked once this
// complete batch of buffers has been acknowledged
// by the peer. This will have the side effect of
// blocking additional pending writes from the
// javascript side, so writing data to the stream
// will be throttled by how quickly the peer is
// able to acknowledge stream packets. This is good
// in the sense of providing back-pressure, but
// also means that writes will be significantly
// less performant unless written in batches.
streambuf_.Push(
bufs,
nbufs,
[req_wrap, strong_ref](int status) {
req_wrap->Done(status);
});
// If end() was called on the JS side, the write_ended flag
// will have been set. This allows us to know early if this
// is the final chunk. But this is only only to be triggered
// if end() was called with a final chunk of data to write.
// Otherwise, we have to wait for DoShutdown to be called.
if (state_->write_ended == 1) {
RecordTimestamp(&QuicStreamStats::closing_at);
streambuf_.End();
}
session()->ResumeStream(stream_id_);
return 0;
}
bool QuicStream::IsAlive() {
return !is_destroyed() && !IsClosing();
}
bool QuicStream::IsClosing() {
return !is_writable() && !is_readable();
}
int QuicStream::ReadStart() {
CHECK(!is_destroyed());
CHECK(is_readable());
state_->read_started = 1;
state_->read_paused = 0;
IncrementStat(
&QuicStreamStats::max_offset,
inbound_consumed_data_while_paused_);
session_->ExtendStreamOffset(id(), inbound_consumed_data_while_paused_);
return 0;
}
int QuicStream::ReadStop() {
CHECK(!is_destroyed());
CHECK(is_readable());
state_->read_paused = 1;
return 0;
}
void QuicStream::IncrementStats(size_t datalen) {
uint64_t len = static_cast<uint64_t>(datalen);
IncrementStat(&QuicStreamStats::bytes_received, len);
RecordRate(&QuicStreamStats::received_at);
RecordSize(len);
}
void QuicStream::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("buffer", &streambuf_);
StatsBase::StatsMemoryInfo(tracker);
tracker->TrackField("headers", headers_);
}
BaseObjectPtr<QuicStream> QuicStream::New(
QuicSession* session,
int64_t stream_id,
int64_t push_id) {
Local<Object> obj;
if (!session->env()
->quicserverstream_instance_template()
->NewInstance(session->env()->context()).ToLocal(&obj)) {
return {};
}
BaseObjectPtr<QuicStream> stream =
MakeDetachedBaseObject<QuicStream>(
session,
obj,
stream_id,
push_id);
CHECK(stream);
session->AddStream(stream);
return stream;
}
// Passes chunks of data on to the JavaScript side as soon as they are
// received but only if we're still readable. The caller of this must have a
// HandleScope.
//
// Note that this is pushing data to the JS side regardless of whether
// anything is listening. For flow-control, we only send window updates
// to the sending peer if the stream is in flowing mode, so the sender
// should not be sending too much data.
void QuicStream::ReceiveData(
uint32_t flags,
const uint8_t* data,
size_t datalen,
uint64_t offset) {
CHECK(!is_destroyed());
Debug(this, "Receiving %d bytes. Final? %s. Readable? %s",
datalen,
flags & NGTCP2_STREAM_DATA_FLAG_FIN ? "yes" : "no",
is_readable() ? "yes" : "no");
// If the QuicStream is not (or was never) readable, just ignore the chunk.
if (!is_readable())
return;
// ngtcp2 guarantees that datalen will only be 0 if fin is set.
// Let's just make sure.
CHECK(datalen > 0 || flags & NGTCP2_STREAM_DATA_FLAG_FIN);
// ngtcp2 guarantees that offset is always greater than the previously
// received offset. Let's just make sure.
CHECK_GE(offset, GetStat(&QuicStreamStats::max_offset_received));
SetStat(&QuicStreamStats::max_offset_received, offset);
if (datalen > 0) {
// IncrementStats will update the data_rx_rate_ and data_rx_size_
// histograms. These will provide data necessary to detect and
// prevent Slow Send DOS attacks specifically by allowing us to
// see if a connection is sending very small chunks of data at very
// slow speeds. It is important to emphasize, however, that slow send
// rates may be perfectly legitimate so we cannot simply take blanket
// action when slow rates are detected. Nor can we reliably define what
// a slow rate even is! Will will need to determine some reasonable
// default and allow user code to change the default as well as determine
// what action to take. The current strategy will be to trigger an event
// on the stream when data transfer rates are likely to be considered too
// slow.
IncrementStats(datalen);
while (datalen > 0) {
uv_buf_t buf = EmitAlloc(datalen);
size_t avail = std::min(static_cast<size_t>(buf.len), datalen);
// For now, we're allocating and copying. Once we determine if we can
// safely switch to a non-allocated mode like we do with http2 streams,
// we can make this branch more efficient by using the LIKELY
// optimization. The way ngtcp2 currently works, however, we have
// to memcpy here.
if (UNLIKELY(buf.base == nullptr))
buf.base = reinterpret_cast<char*>(const_cast<uint8_t*>(data));
else
memcpy(buf.base, data, avail);
data += avail;
datalen -= avail;
// Capture read_paused before EmitRead in case user code callbacks
// alter the state when EmitRead is called.
bool read_paused = state_->read_paused == 1;
EmitRead(avail, buf);
// Reading can be paused while we are processing. If that's
// the case, we still want to acknowledge the current bytes
// so that pausing does not throw off our flow control.
if (read_paused) {
inbound_consumed_data_while_paused_ += avail;
} else {
IncrementStat(&QuicStreamStats::max_offset, avail);
session_->ExtendStreamOffset(id(), avail);
}
}
}
// When fin != 0, we've received that last chunk of data for this
// stream, indicating that the stream will no longer be readable.
if (flags & NGTCP2_STREAM_DATA_FLAG_FIN) {
set_final_size(offset + datalen);
EmitRead(UV_EOF);
}
}
int QuicStream::DoPull(
bob::Next<ngtcp2_vec> next,
int options,
ngtcp2_vec* data,
size_t count,
size_t max_count_hint) {
return streambuf_.Pull(
std::move(next),
options,
data,
count,
max_count_hint);
}
// JavaScript API
namespace {
void QuicStreamGetID(const FunctionCallbackInfo<Value>& args) {
QuicStream* stream;
ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
args.GetReturnValue().Set(static_cast<double>(stream->id()));
}
void OpenUnidirectionalStream(const FunctionCallbackInfo<Value>& args) {
CHECK(!args.IsConstructCall());
CHECK(args[0]->IsObject());
QuicSession* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>());
int64_t stream_id;
if (!session->OpenUnidirectionalStream(&stream_id))
return;
BaseObjectPtr<QuicStream> stream = QuicStream::New(session, stream_id);
args.GetReturnValue().Set(stream->object());
}
void OpenBidirectionalStream(const FunctionCallbackInfo<Value>& args) {
CHECK(!args.IsConstructCall());
CHECK(args[0]->IsObject());
QuicSession* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>());
int64_t stream_id;
if (!session->OpenBidirectionalStream(&stream_id))
return;
BaseObjectPtr<QuicStream> stream = QuicStream::New(session, stream_id);
args.GetReturnValue().Set(stream->object());
}
void QuicStreamDestroy(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
QuicStream* stream;
ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
QuicError error(env, args[0], args[1], QUIC_ERROR_APPLICATION);
stream->Destroy(&error);
}
void QuicStreamReset(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
QuicStream* stream;
ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
QuicError error(env, args[0], args[1], QUIC_ERROR_APPLICATION);
stream->ResetStream(
error.family == QUIC_ERROR_APPLICATION ?
error.code : static_cast<uint64_t>(NGTCP2_NO_ERROR));
}
void QuicStreamStopSending(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
QuicStream* stream;
ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
QuicError error(env, args[0], args[1], QUIC_ERROR_APPLICATION);
stream->StopSending(
error.family == QUIC_ERROR_APPLICATION ?
error.code : static_cast<uint64_t>(NGTCP2_NO_ERROR));
}
// Requests transmission of a block of informational headers. Not all
// QUIC Applications will support headers. If headers are not supported,
// This will set the return value to false, otherwise the return value
// is set to true
void QuicStreamSubmitInformation(const FunctionCallbackInfo<Value>& args) {
QuicStream* stream;
ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
CHECK(args[0]->IsArray());
args.GetReturnValue().Set(stream->SubmitInformation(args[0].As<Array>()));
}
// Requests transmission of a block of initial headers. Not all
// QUIC Applications will support headers. If headers are not supported,
// this will set the return value to false, otherwise the return value
// is set to true. For http/3, these may be request or response headers.
void QuicStreamSubmitHeaders(const FunctionCallbackInfo<Value>& args) {
QuicStream* stream;
ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
CHECK(args[0]->IsArray());
uint32_t flags = QUICSTREAM_HEADER_FLAGS_NONE;
CHECK(args[1]->Uint32Value(stream->env()->context()).To(&flags));
args.GetReturnValue().Set(stream->SubmitHeaders(args[0].As<Array>(), flags));
}
// Requests transmission of a block of trailing headers. Not all
// QUIC Applications will support headers. If headers are not supported,
// this will set the return value to false, otherwise the return value
// is set to true.
void QuicStreamSubmitTrailers(const FunctionCallbackInfo<Value>& args) {
QuicStream* stream;
ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
CHECK(args[0]->IsArray());
args.GetReturnValue().Set(stream->SubmitTrailers(args[0].As<Array>()));
}
// Requests creation of a push stream. Not all QUIC Applications will
// support push streams. If pushes are not supported, the return value
// will be undefined, otherwise the return value will be the created
// QuicStream representing the push.
void QuicStreamSubmitPush(const FunctionCallbackInfo<Value>& args) {
QuicStream* stream;
ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
CHECK(args[0]->IsArray());
BaseObjectPtr<QuicStream> push_stream =
stream->SubmitPush(args[0].As<Array>());
if (push_stream)
args.GetReturnValue().Set(push_stream->object());
}
} // namespace
void QuicStream::Initialize(
Environment* env,
Local<Object> target,
Local<Context> context) {
Isolate* isolate = env->isolate();
Local<String> class_name = FIXED_ONE_BYTE_STRING(isolate, "QuicStream");
Local<FunctionTemplate> stream = FunctionTemplate::New(env->isolate());
stream->SetClassName(class_name);
stream->Inherit(AsyncWrap::GetConstructorTemplate(env));
StreamBase::AddMethods(env, stream);
Local<ObjectTemplate> streamt = stream->InstanceTemplate();
streamt->SetInternalFieldCount(StreamBase::kInternalFieldCount);
streamt->Set(env->owner_symbol(), Null(env->isolate()));
env->SetProtoMethod(stream, "destroy", QuicStreamDestroy);
env->SetProtoMethod(stream, "resetStream", QuicStreamReset);
env->SetProtoMethod(stream, "stopSending", QuicStreamStopSending);
env->SetProtoMethod(stream, "id", QuicStreamGetID);
env->SetProtoMethod(stream, "submitInformation", QuicStreamSubmitInformation);
env->SetProtoMethod(stream, "submitHeaders", QuicStreamSubmitHeaders);
env->SetProtoMethod(stream, "submitTrailers", QuicStreamSubmitTrailers);
env->SetProtoMethod(stream, "submitPush", QuicStreamSubmitPush);
env->set_quicserverstream_instance_template(streamt);
target->Set(env->context(),
class_name,
stream->GetFunction(env->context()).ToLocalChecked()).Check();
env->SetMethod(target, "openBidirectionalStream", OpenBidirectionalStream);
env->SetMethod(target, "openUnidirectionalStream", OpenUnidirectionalStream);
}
} // namespace quic
} // namespace node