%PDF- %PDF-
| Direktori : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/src/quic/ |
| Current File : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/src/quic/session.cc |
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include "session.h"
#include <aliased_struct-inl.h>
#include <async_wrap-inl.h>
#include <crypto/crypto_util.h>
#include <debug_utils-inl.h>
#include <env-inl.h>
#include <memory_tracker-inl.h>
#include <ngtcp2/ngtcp2.h>
#include <node_bob-inl.h>
#include <node_errors.h>
#include <node_http_common-inl.h>
#include <node_sockaddr-inl.h>
#include <req_wrap-inl.h>
#include <timer_wrap-inl.h>
#include <util-inl.h>
#include <uv.h>
#include <v8.h>
#include "application.h"
#include "bindingdata.h"
#include "cid.h"
#include "data.h"
#include "defs.h"
#include "endpoint.h"
#include "logstream.h"
#include "packet.h"
#include "preferredaddress.h"
#include "sessionticket.h"
#include "streams.h"
#include "tlscontext.h"
#include "transportparams.h"
namespace node {
using v8::Array;
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::BigInt;
using v8::Boolean;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::Integer;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::Nothing;
using v8::Object;
using v8::PropertyAttribute;
using v8::String;
using v8::Uint32;
using v8::Undefined;
using v8::Value;
namespace quic {
#define SESSION_STATE(V) \
/* Set if the JavaScript wrapper has a path-validation event listener */ \
V(PATH_VALIDATION, path_validation, uint8_t) \
/* Set if the JavaScript wrapper has a version-negotiation event listener */ \
V(VERSION_NEGOTIATION, version_negotiation, uint8_t) \
/* Set if the JavaScript wrapper has a datagram event listener */ \
V(DATAGRAM, datagram, uint8_t) \
/* Set if the JavaScript wrapper has a session-ticket event listener */ \
V(SESSION_TICKET, session_ticket, uint8_t) \
V(CLOSING, closing, uint8_t) \
V(GRACEFUL_CLOSE, graceful_close, uint8_t) \
V(SILENT_CLOSE, silent_close, uint8_t) \
V(STATELESS_RESET, stateless_reset, uint8_t) \
V(DESTROYED, destroyed, uint8_t) \
V(HANDSHAKE_COMPLETED, handshake_completed, uint8_t) \
V(HANDSHAKE_CONFIRMED, handshake_confirmed, uint8_t) \
V(STREAM_OPEN_ALLOWED, stream_open_allowed, uint8_t) \
V(PRIORITY_SUPPORTED, priority_supported, uint8_t) \
/* A Session is wrapped if it has been passed out to JS */ \
V(WRAPPED, wrapped, uint8_t) \
V(LAST_DATAGRAM_ID, last_datagram_id, uint64_t)
#define SESSION_STATS(V) \
V(CREATED_AT, created_at) \
V(CLOSING_AT, closing_at) \
V(DESTROYED_AT, destroyed_at) \
V(HANDSHAKE_COMPLETED_AT, handshake_completed_at) \
V(HANDSHAKE_CONFIRMED_AT, handshake_confirmed_at) \
V(GRACEFUL_CLOSING_AT, graceful_closing_at) \
V(BYTES_RECEIVED, bytes_received) \
V(BYTES_SENT, bytes_sent) \
V(BIDI_IN_STREAM_COUNT, bidi_in_stream_count) \
V(BIDI_OUT_STREAM_COUNT, bidi_out_stream_count) \
V(UNI_IN_STREAM_COUNT, uni_in_stream_count) \
V(UNI_OUT_STREAM_COUNT, uni_out_stream_count) \
V(KEYUPDATE_COUNT, keyupdate_count) \
V(LOSS_RETRANSMIT_COUNT, loss_retransmit_count) \
V(MAX_BYTES_IN_FLIGHT, max_bytes_in_flight) \
V(BYTES_IN_FLIGHT, bytes_in_flight) \
V(BLOCK_COUNT, block_count) \
V(CWND, cwnd) \
V(LATEST_RTT, latest_rtt) \
V(MIN_RTT, min_rtt) \
V(RTTVAR, rttvar) \
V(SMOOTHED_RTT, smoothed_rtt) \
V(SSTHRESH, ssthresh) \
V(DATAGRAMS_RECEIVED, datagrams_received) \
V(DATAGRAMS_SENT, datagrams_sent) \
V(DATAGRAMS_ACKNOWLEDGED, datagrams_acknowledged) \
V(DATAGRAMS_LOST, datagrams_lost)
#define SESSION_JS_METHODS(V) \
V(DoDestroy, destroy, false) \
V(GetRemoteAddress, getRemoteAddress, true) \
V(GetCertificate, getCertificate, true) \
V(GetEphemeralKeyInfo, getEphemeralKey, true) \
V(GetPeerCertificate, getPeerCertificate, true) \
V(GracefulClose, gracefulClose, false) \
V(SilentClose, silentClose, false) \
V(UpdateKey, updateKey, false) \
V(DoOpenStream, openStream, false) \
V(DoSendDatagram, sendDatagram, false)
struct Session::State {
#define V(_, name, type) type name;
SESSION_STATE(V)
#undef V
};
STAT_STRUCT(Session, SESSION)
// ============================================================================
// Used to conditionally trigger sending an explicit connection
// close. If there are multiple MaybeCloseConnectionScope in the
// stack, the determination of whether to send the close will be
// done once the final scope is closed.
struct Session::MaybeCloseConnectionScope final {
Session* session;
bool silent = false;
MaybeCloseConnectionScope(Session* session_, bool silent_)
: session(session_),
silent(silent_ || session->connection_close_depth_ > 0) {
Debug(session_,
"Entering maybe close connection scope. Silent? %s",
silent ? "yes" : "no");
session->connection_close_depth_++;
}
MaybeCloseConnectionScope(const MaybeCloseConnectionScope&) = delete;
MaybeCloseConnectionScope(MaybeCloseConnectionScope&&) = delete;
MaybeCloseConnectionScope& operator=(const MaybeCloseConnectionScope&) =
delete;
MaybeCloseConnectionScope& operator=(MaybeCloseConnectionScope&&) = delete;
~MaybeCloseConnectionScope() {
// We only want to trigger the sending the connection close if ...
// a) Silent is not explicitly true at this scope.
// b) We're not within the scope of an ngtcp2 callback, and
// c) We are not already in a closing or draining period.
if (--session->connection_close_depth_ == 0 && !silent &&
session->can_send_packets()) {
session->SendConnectionClose();
}
}
};
// ============================================================================
// Used to conditionally trigger sending of any pending data the session may
// be holding onto. If there are multiple SendPendingDataScope in the stack,
// the determination of whether to send the data will be done once the final
// scope is closed.
Session::SendPendingDataScope::SendPendingDataScope(Session* session)
: session(session) {
Debug(session, "Entering send pending data scope");
session->send_scope_depth_++;
}
Session::SendPendingDataScope::SendPendingDataScope(
const BaseObjectPtr<Session>& session)
: SendPendingDataScope(session.get()) {}
Session::SendPendingDataScope::~SendPendingDataScope() {
if (--session->send_scope_depth_ == 0 && session->can_send_packets()) {
session->application().SendPendingData();
}
}
// ============================================================================
namespace {
inline const char* getEncryptionLevelName(ngtcp2_encryption_level level) {
switch (level) {
case NGTCP2_ENCRYPTION_LEVEL_1RTT:
return "1rtt";
case NGTCP2_ENCRYPTION_LEVEL_0RTT:
return "0rtt";
case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE:
return "handshake";
case NGTCP2_ENCRYPTION_LEVEL_INITIAL:
return "initial";
}
return "<unknown>";
}
// Qlog is a JSON-based logging format that is being standardized for low-level
// debug logging of QUIC connections and dataflows. The qlog output is generated
// optionally by ngtcp2 for us. The on_qlog_write callback is registered with
// ngtcp2 to emit the qlog information. Every Session will have it's own qlog
// stream.
void on_qlog_write(void* user_data,
uint32_t flags,
const void* data,
size_t len) {
static_cast<Session*>(user_data)->HandleQlog(flags, data, len);
}
// Forwards detailed(verbose) debugging information from ngtcp2. Enabled using
// the NODE_DEBUG_NATIVE=NGTCP2_DEBUG category.
void ngtcp2_debug_log(void* user_data, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
std::string format(fmt, strlen(fmt) + 1);
format[strlen(fmt)] = '\n';
// Debug() does not work with the va_list here. So we use vfprintf
// directly instead. Ngtcp2DebugLog is only enabled when the debug
// category is enabled.
vfprintf(stderr, format.c_str(), ap);
va_end(ap);
}
template <typename Opt, PreferredAddress::Policy Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<Object>& object,
const v8::Local<String>& name) {
Local<Value> value;
PreferredAddress::Policy policy =
PreferredAddress::Policy::USE_PREFERRED_ADDRESS;
if (!object->Get(env->context(), name).ToLocal(&value) ||
!PreferredAddress::tryGetPolicy(env, value).To(&policy)) {
return false;
}
options->*member = policy;
return true;
}
template <typename Opt, TLSContext::Options Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<Object>& object,
const v8::Local<String>& name) {
Local<Value> value;
TLSContext::Options opts;
if (!object->Get(env->context(), name).ToLocal(&value) ||
!TLSContext::Options::From(env, value).To(&opts)) {
return false;
}
options->*member = opts;
return true;
}
template <typename Opt, Session::Application_Options Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<Object>& object,
const v8::Local<String>& name) {
Local<Value> value;
Session::Application_Options opts;
if (!object->Get(env->context(), name).ToLocal(&value) ||
!Session::Application_Options::From(env, value).To(&opts)) {
return false;
}
options->*member = opts;
return true;
}
template <typename Opt, TransportParams::Options Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<Object>& object,
const v8::Local<String>& name) {
Local<Value> value;
TransportParams::Options opts;
if (!object->Get(env->context(), name).ToLocal(&value) ||
!TransportParams::Options::From(env, value).To(&opts)) {
return false;
}
options->*member = opts;
return true;
}
} // namespace
// ============================================================================
Session::Config::Config(Side side,
const Endpoint& endpoint,
const Options& options,
uint32_t version,
const SocketAddress& local_address,
const SocketAddress& remote_address,
const CID& dcid,
const CID& scid,
std::optional<SessionTicket> session_ticket,
const CID& ocid)
: side(side),
options(options),
version(version),
local_address(local_address),
remote_address(remote_address),
dcid(dcid),
scid(scid),
ocid(ocid),
session_ticket(session_ticket) {
ngtcp2_settings_default(&settings);
settings.initial_ts = uv_hrtime();
// We currently do not support Path MTU Discovery. Once we do, unset this.
settings.no_pmtud = 1;
settings.tokenlen = 0;
settings.token = nullptr;
if (options.qlog) {
settings.qlog_write = on_qlog_write;
}
if (endpoint.env()->enabled_debug_list()->enabled(
DebugCategory::NGTCP2_DEBUG)) {
settings.log_printf = ngtcp2_debug_log;
}
// We pull parts of the settings for the session from the endpoint options.
auto& config = endpoint.options();
settings.no_tx_udp_payload_size_shaping = config.no_udp_payload_size_shaping;
settings.handshake_timeout = config.handshake_timeout;
settings.max_stream_window = config.max_stream_window;
settings.max_window = config.max_window;
settings.cc_algo = config.cc_algorithm;
settings.max_tx_udp_payload_size = config.max_payload_size;
if (config.unacknowledged_packet_threshold > 0) {
settings.ack_thresh = config.unacknowledged_packet_threshold;
}
}
Session::Config::Config(const Endpoint& endpoint,
const Options& options,
const SocketAddress& local_address,
const SocketAddress& remote_address,
std::optional<SessionTicket> session_ticket,
const CID& ocid)
: Config(Side::CLIENT,
endpoint,
options,
options.version,
local_address,
remote_address,
CID::Factory::random().Generate(NGTCP2_MIN_INITIAL_DCIDLEN),
options.cid_factory->Generate(),
session_ticket,
ocid) {}
void Session::Config::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("options", options);
tracker->TrackField("local_address", local_address);
tracker->TrackField("remote_address", remote_address);
tracker->TrackField("dcid", dcid);
tracker->TrackField("scid", scid);
tracker->TrackField("ocid", ocid);
tracker->TrackField("retry_scid", retry_scid);
if (session_ticket.has_value())
tracker->TrackField("session_ticket", session_ticket.value());
}
void Session::Config::set_token(const uint8_t* token,
size_t len,
ngtcp2_token_type type) {
settings.token = token;
settings.tokenlen = len;
settings.token_type = type;
}
void Session::Config::set_token(const RetryToken& token) {
ngtcp2_vec vec = token;
set_token(vec.base, vec.len, NGTCP2_TOKEN_TYPE_RETRY);
}
void Session::Config::set_token(const RegularToken& token) {
ngtcp2_vec vec = token;
set_token(vec.base, vec.len, NGTCP2_TOKEN_TYPE_NEW_TOKEN);
}
std::string Session::Config::ToString() const {
DebugIndentScope indent;
auto prefix = indent.Prefix();
std::string res("{");
auto sidestr = ([&] {
switch (side) {
case Side::CLIENT:
return "client";
case Side::SERVER:
return "server";
}
return "<unknown>";
})();
res += prefix + "side: " + std::string(sidestr);
res += prefix + "options: " + options.ToString();
res += prefix + "version: " + std::to_string(version);
res += prefix + "local address: " + local_address.ToString();
res += prefix + "remote address: " + remote_address.ToString();
res += prefix + "dcid: " + dcid.ToString();
res += prefix + "scid: " + scid.ToString();
res += prefix + "ocid: " + ocid.ToString();
res += prefix + "retry scid: " + retry_scid.ToString();
res += prefix + "preferred address cid: " + preferred_address_cid.ToString();
if (session_ticket.has_value()) {
res += prefix + "session ticket: yes";
} else {
res += prefix + "session ticket: <none>";
}
res += indent.Close();
return res;
}
// ============================================================================
Maybe<Session::Options> Session::Options::From(Environment* env,
Local<Value> value) {
if (value.IsEmpty() || !value->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Options>();
}
auto& state = BindingData::Get(env);
auto params = value.As<Object>();
Options options;
#define SET(name) \
SetOption<Session::Options, &Session::Options::name>( \
env, &options, params, state.name##_string())
if (!SET(version) || !SET(min_version) || !SET(preferred_address_strategy) ||
!SET(transport_params) || !SET(tls_options) ||
!SET(application_options) || !SET(qlog)) {
return Nothing<Options>();
}
#undef SET
// TODO(@jasnell): Later we will also support setting the CID::Factory.
// For now, we're just using the default random factory.
return Just<Options>(options);
}
void Session::Options::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("transport_params", transport_params);
tracker->TrackField("crypto_options", tls_options);
tracker->TrackField("application_options", application_options);
tracker->TrackField("cid_factory_ref", cid_factory_ref);
}
std::string Session::Options::ToString() const {
DebugIndentScope indent;
auto prefix = indent.Prefix();
std::string res("{");
res += prefix + "version: " + std::to_string(version);
res += prefix + "min version: " + std::to_string(min_version);
auto policy = ([&] {
switch (preferred_address_strategy) {
case PreferredAddress::Policy::USE_PREFERRED_ADDRESS:
return "use";
case PreferredAddress::Policy::IGNORE_PREFERRED_ADDRESS:
return "ignore";
}
return "<unknown>";
})();
res += prefix + "preferred address policy: " + std::string(policy);
res += prefix + "transport params: " + transport_params.ToString();
res += prefix + "crypto options: " + tls_options.ToString();
res += prefix + "application options: " + application_options.ToString();
res += prefix + "qlog: " + (qlog ? std::string("yes") : std::string("no"));
res += indent.Close();
return res;
}
// ============================================================================
bool Session::HasInstance(Environment* env, Local<Value> value) {
return GetConstructorTemplate(env)->HasInstance(value);
}
BaseObjectPtr<Session> Session::Create(Endpoint* endpoint,
const Config& config) {
Local<Object> obj;
if (!GetConstructorTemplate(endpoint->env())
->InstanceTemplate()
->NewInstance(endpoint->env()->context())
.ToLocal(&obj)) {
return BaseObjectPtr<Session>();
}
return MakeDetachedBaseObject<Session>(endpoint, obj, config);
}
Session::Session(Endpoint* endpoint,
v8::Local<v8::Object> object,
const Config& config)
: AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUIC_SESSION),
stats_(env()->isolate()),
state_(env()->isolate()),
allocator_(BindingData::Get(env())),
endpoint_(BaseObjectWeakPtr<Endpoint>(endpoint)),
config_(config),
local_address_(config.local_address),
remote_address_(config.remote_address),
connection_(InitConnection()),
tls_context_(env(), config_.side, this, config_.options.tls_options),
application_(select_application()),
timer_(env(),
[this, self = BaseObjectPtr<Session>(this)] { OnTimeout(); }) {
MakeWeak();
Debug(this, "Session created.");
timer_.Unref();
application().ExtendMaxStreams(EndpointLabel::LOCAL,
Direction::BIDIRECTIONAL,
TransportParams::DEFAULT_MAX_STREAMS_BIDI);
application().ExtendMaxStreams(EndpointLabel::LOCAL,
Direction::UNIDIRECTIONAL,
TransportParams::DEFAULT_MAX_STREAMS_UNI);
const auto defineProperty = [&](auto name, auto value) {
object
->DefineOwnProperty(
env()->context(), name, value, PropertyAttribute::ReadOnly)
.Check();
};
defineProperty(env()->state_string(), state_.GetArrayBuffer());
defineProperty(env()->stats_string(), stats_.GetArrayBuffer());
auto& state = BindingData::Get(env());
if (UNLIKELY(config_.options.qlog)) {
qlog_stream_ = LogStream::Create(env());
if (qlog_stream_)
defineProperty(state.qlog_string(), qlog_stream_->object());
}
if (UNLIKELY(config_.options.tls_options.keylog)) {
keylog_stream_ = LogStream::Create(env());
if (keylog_stream_)
defineProperty(state.keylog_string(), keylog_stream_->object());
}
// We index the Session by our local CID (the scid) and dcid (the peer's cid)
endpoint_->AddSession(config_.scid, BaseObjectPtr<Session>(this));
endpoint_->AssociateCID(config_.dcid, config_.scid);
tls_context_.Start();
UpdateDataStats();
}
Session::~Session() {
Debug(this, "Session destroyed.");
if (conn_closebuf_) {
conn_closebuf_->Done(0);
}
if (qlog_stream_) {
Debug(this, "Closing the qlog stream for this session");
env()->SetImmediate(
[ptr = std::move(qlog_stream_)](Environment*) { ptr->End(); });
}
if (keylog_stream_) {
Debug(this, "Closing the keylog stream for this session");
env()->SetImmediate(
[ptr = std::move(keylog_stream_)](Environment*) { ptr->End(); });
}
DCHECK(streams_.empty());
}
Session::operator ngtcp2_conn*() const {
return connection_.get();
}
uint32_t Session::version() const {
return config_.version;
}
Endpoint& Session::endpoint() const {
return *endpoint_;
}
TLSContext& Session::tls_context() {
return tls_context_;
}
Session::Application& Session::application() {
return *application_;
}
const SocketAddress& Session::remote_address() const {
return remote_address_;
}
const SocketAddress& Session::local_address() const {
return local_address_;
}
bool Session::is_closing() const {
return state_->closing;
}
bool Session::is_graceful_closing() const {
return state_->graceful_close;
}
bool Session::is_silent_closing() const {
return state_->silent_close;
}
bool Session::is_destroyed() const {
return state_->destroyed;
}
bool Session::is_server() const {
return config_.side == Side::SERVER;
}
std::string Session::diagnostic_name() const {
const auto get_type = [&] { return is_server() ? "server" : "client"; };
return std::string("Session (") + get_type() + "," +
std::to_string(env()->thread_id()) + ":" +
std::to_string(static_cast<int64_t>(get_async_id())) + ")";
}
const Session::Config& Session::config() const {
return config_;
}
const Session::Options& Session::options() const {
return config_.options;
}
void Session::HandleQlog(uint32_t flags, const void* data, size_t len) {
if (qlog_stream_) {
// Fun fact... ngtcp2 does not emit the final qlog statement until the
// ngtcp2_conn object is destroyed. Ideally, destroying is explicit, but
// sometimes the Session object can be garbage collected without being
// explicitly destroyed. During those times, we cannot call out to
// JavaScript. Because we don't know for sure if we're in in a GC when this
// is called, it is safer to just defer writes to immediate, and to keep it
// consistent, let's just always defer (this is not performance sensitive so
// the deferring is fine).
std::vector<uint8_t> buffer(len);
memcpy(buffer.data(), data, len);
Debug(this, "Emitting qlog data to the qlog stream");
env()->SetImmediate(
[ptr = qlog_stream_, buffer = std::move(buffer), flags](Environment*) {
ptr->Emit(buffer.data(),
buffer.size(),
flags & NGTCP2_QLOG_WRITE_FLAG_FIN
? LogStream::EmitOption::FIN
: LogStream::EmitOption::NONE);
});
}
}
TransportParams Session::GetLocalTransportParams() const {
DCHECK(!is_destroyed());
return TransportParams(ngtcp2_conn_get_local_transport_params(*this));
}
TransportParams Session::GetRemoteTransportParams() const {
DCHECK(!is_destroyed());
return TransportParams(ngtcp2_conn_get_remote_transport_params(*this));
}
void Session::SetLastError(QuicError&& error) {
Debug(this, "Setting last error to %s", error);
last_error_ = std::move(error);
}
void Session::Close(Session::CloseMethod method) {
if (is_destroyed()) return;
switch (method) {
case CloseMethod::DEFAULT: {
Debug(this, "Closing session");
DoClose(false);
break;
}
case CloseMethod::SILENT: {
Debug(this, "Closing session silently");
DoClose(true);
break;
}
case CloseMethod::GRACEFUL: {
if (is_graceful_closing()) return;
Debug(this, "Closing session gracefully");
// If there are no open streams, then we can close just immediately and
// not worry about waiting around for the right moment.
if (streams_.empty()) {
DoClose(false);
} else {
state_->graceful_close = 1;
STAT_RECORD_TIMESTAMP(Stats, graceful_closing_at);
}
break;
}
}
}
void Session::Destroy() {
if (is_destroyed()) return;
Debug(this, "Session destroyed");
// The DoClose() method should have already been called.
DCHECK(state_->closing);
// We create a copy of the streams because they will remove themselves
// from streams_ as they are cleaning up, causing the iterator to be
// invalidated.
auto streams = streams_;
for (auto& stream : streams) stream.second->Destroy(last_error_);
DCHECK(streams_.empty());
STAT_RECORD_TIMESTAMP(Stats, destroyed_at);
state_->closing = 0;
state_->graceful_close = 0;
timer_.Stop();
// The Session instances are kept alive using a in the Endpoint. Removing the
// Session from the Endpoint will free that pointer, allowing the Session to
// be deconstructed once the stack unwinds and any remaining
// BaseObjectPtr<Session> instances fall out of scope.
MaybeStackBuffer<ngtcp2_cid, 10> cids(ngtcp2_conn_get_scid(*this, nullptr));
ngtcp2_conn_get_scid(*this, cids.out());
MaybeStackBuffer<ngtcp2_cid_token, 10> tokens(
ngtcp2_conn_get_active_dcid(*this, nullptr));
ngtcp2_conn_get_active_dcid(*this, tokens.out());
endpoint_->DisassociateCID(config_.dcid);
endpoint_->DisassociateCID(config_.preferred_address_cid);
for (size_t n = 0; n < cids.length(); n++) {
endpoint_->DisassociateCID(CID(cids[n]));
}
for (size_t n = 0; n < tokens.length(); n++) {
if (tokens[n].token_present) {
endpoint_->DisassociateStatelessResetToken(
StatelessResetToken(tokens[n].token));
}
}
state_->destroyed = 1;
// Removing the session from the endpoint may cause the endpoint to be
// destroyed if it is waiting on the last session to be destroyed. Let's grab
// a reference just to be safe for the rest of the function.
BaseObjectPtr<Endpoint> endpoint = std::move(endpoint_);
endpoint->RemoveSession(config_.scid);
}
bool Session::Receive(Store&& store,
const SocketAddress& local_address,
const SocketAddress& remote_address) {
if (is_destroyed()) return false;
const auto receivePacket = [&](ngtcp2_path* path, ngtcp2_vec vec) {
DCHECK(!is_destroyed());
uint64_t now = uv_hrtime();
ngtcp2_pkt_info pi{}; // Not used but required.
int err = ngtcp2_conn_read_pkt(*this, path, &pi, vec.base, vec.len, now);
switch (err) {
case 0: {
// Return true so we send after receiving.
Debug(this, "Session successfully received packet");
return true;
}
case NGTCP2_ERR_DRAINING: {
// Connection has entered the draining state, no further data should be
// sent. This happens when the remote peer has sent a CONNECTION_CLOSE.
Debug(this, "Session is draining");
return false;
}
case NGTCP2_ERR_CLOSING: {
// Connection has entered the closing state, no further data should be
// sent. This happens when the local peer has called
// ngtcp2_conn_write_connection_close.
Debug(this, "Session is closing");
return false;
}
case NGTCP2_ERR_CRYPTO: {
// Crypto error happened! Set the last error to the tls alert
last_error_ = QuicError::ForTlsAlert(ngtcp2_conn_get_tls_alert(*this));
Debug(this, "Crypto error while receiving packet: %s", last_error_);
Close();
return false;
}
case NGTCP2_ERR_RETRY: {
// This should only ever happen on the server. We have to send a path
// validation challenge in the form of a RETRY packet to the peer and
// drop the connection.
DCHECK(is_server());
Debug(this, "Server must send a retry packet");
endpoint_->SendRetry(PathDescriptor{
version(),
config_.dcid,
config_.scid,
local_address_,
remote_address_,
});
Close(CloseMethod::SILENT);
return false;
}
case NGTCP2_ERR_DROP_CONN: {
// There's nothing else to do but drop the connection state.
Debug(this, "Session must drop the connection");
Close(CloseMethod::SILENT);
return false;
}
}
// Shouldn't happen but just in case.
last_error_ = QuicError::ForNgtcp2Error(err);
Debug(this, "Error while receiving packet: %s (%d)", last_error_, err);
Close();
return false;
};
auto update_stats = OnScopeLeave([&] { UpdateDataStats(); });
remote_address_ = remote_address;
Path path(local_address, remote_address_);
Debug(this, "Session is receiving packet received along path %s", path);
STAT_INCREMENT_N(Stats, bytes_received, store.length());
if (receivePacket(&path, store)) application().SendPendingData();
if (!is_destroyed()) UpdateTimer();
return true;
}
void Session::Send(Packet* packet) {
// Sending a Packet is generally best effort. If we're not in a state
// where we can send a packet, it's ok to drop it on the floor. The
// packet loss mechanisms will cause the packet data to be resent later
// if appropriate (and possible).
DCHECK(!is_destroyed());
DCHECK(!is_in_draining_period());
if (can_send_packets() && packet->length() > 0) {
Debug(this, "Session is sending %s", packet->ToString());
STAT_INCREMENT_N(Stats, bytes_sent, packet->length());
endpoint_->Send(packet);
return;
}
Debug(this, "Session could not send %s", packet->ToString());
packet->Done(packet->length() > 0 ? UV_ECANCELED : 0);
}
void Session::Send(Packet* packet, const PathStorage& path) {
UpdatePath(path);
Send(packet);
}
uint64_t Session::SendDatagram(Store&& data) {
auto tp = ngtcp2_conn_get_remote_transport_params(*this);
uint64_t max_datagram_size = tp->max_datagram_frame_size;
if (max_datagram_size == 0 || data.length() > max_datagram_size) {
// Datagram is too large.
Debug(this, "Data is too large to send as a datagram");
return 0;
}
Debug(this, "Session is sending datagram");
Packet* packet = nullptr;
uint8_t* pos = nullptr;
int accepted = 0;
ngtcp2_vec vec = data;
PathStorage path;
int flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE;
uint64_t did = state_->last_datagram_id + 1;
// Let's give it a max number of attempts to send the datagram
static const int kMaxAttempts = 16;
int attempts = 0;
for (;;) {
// We may have to make several attempts at encoding and sending the
// datagram packet. On each iteration here we'll try to encode the
// datagram. It's entirely up to ngtcp2 whether to include the datagram
// in the packet on each call to ngtcp2_conn_writev_datagram.
if (packet == nullptr) {
packet = Packet::Create(env(),
endpoint_.get(),
remote_address_,
ngtcp2_conn_get_max_tx_udp_payload_size(*this),
"datagram");
// Typically sending datagrams is best effort, but if we cannot create
// the packet, then we handle it as a fatal error.
if (packet == nullptr) {
last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL);
Close(CloseMethod::SILENT);
return 0;
}
pos = ngtcp2_vec(*packet).base;
}
ssize_t nwrite = ngtcp2_conn_writev_datagram(*this,
&path.path,
nullptr,
pos,
packet->length(),
&accepted,
flags,
did,
&vec,
1,
uv_hrtime());
ngtcp2_conn_update_pkt_tx_time(*this, uv_hrtime());
if (nwrite <= 0) {
// Nothing was written to the packet.
switch (nwrite) {
case 0: {
// We cannot send data because of congestion control or the data will
// not fit. Since datagrams are best effort, we are going to abandon
// the attempt and just return.
CHECK_EQ(accepted, 0);
packet->Done(UV_ECANCELED);
return 0;
}
case NGTCP2_ERR_WRITE_MORE: {
// We keep on looping! Keep on sending!
continue;
}
case NGTCP2_ERR_INVALID_STATE: {
// The remote endpoint does not want to accept datagrams. That's ok,
// just return 0.
packet->Done(UV_ECANCELED);
return 0;
}
case NGTCP2_ERR_INVALID_ARGUMENT: {
// The datagram is too large. That should have been caught above but
// that's ok. We'll just abandon the attempt and return.
packet->Done(UV_ECANCELED);
return 0;
}
case NGTCP2_ERR_PKT_NUM_EXHAUSTED: {
// We've exhausted the packet number space. Sadly we have to treat it
// as a fatal condition.
break;
}
case NGTCP2_ERR_CALLBACK_FAILURE: {
// There was an internal failure. Sadly we have to treat it as a fatal
// condition.
break;
}
}
packet->Done(UV_ECANCELED);
last_error_ = QuicError::ForNgtcp2Error(nwrite);
Close(CloseMethod::SILENT);
return 0;
}
// In this case, a complete packet was written and we need to send it along.
// Note that this doesn't mean that the packet actually contains the
// datagram! We'll check that next by checking the accepted value.
packet->Truncate(nwrite);
Send(std::move(packet));
if (accepted != 0) {
// Yay! The datagram was accepted into the packet we just sent and we can
// return the datagram ID.
Debug(this, "Session successfully encoded datagram");
STAT_INCREMENT(Stats, datagrams_sent);
STAT_INCREMENT_N(Stats, bytes_sent, vec.len);
state_->last_datagram_id = did;
return did;
}
// We sent a packet, but it wasn't the datagram packet. That can happen.
// Let's loop around and try again.
if (++attempts == kMaxAttempts) {
Debug(this, "Too many attempts to send the datagram");
// Too many attempts to send the datagram.
break;
}
}
return 0;
}
void Session::UpdatePath(const PathStorage& storage) {
remote_address_.Update(storage.path.remote.addr, storage.path.remote.addrlen);
local_address_.Update(storage.path.local.addr, storage.path.local.addrlen);
Debug(this,
"path updated. local %s, remote %s",
local_address_,
remote_address_);
}
BaseObjectPtr<Stream> Session::FindStream(int64_t id) const {
auto it = streams_.find(id);
return it == std::end(streams_) ? BaseObjectPtr<Stream>() : it->second;
}
BaseObjectPtr<Stream> Session::CreateStream(int64_t id) {
if (!can_create_streams()) return BaseObjectPtr<Stream>();
auto stream = Stream::Create(this, id);
if (stream) AddStream(stream);
return stream;
}
BaseObjectPtr<Stream> Session::OpenStream(Direction direction) {
if (!can_create_streams()) return BaseObjectPtr<Stream>();
int64_t id;
switch (direction) {
case Direction::BIDIRECTIONAL: {
Debug(this, "Opening bidirectional stream");
if (ngtcp2_conn_open_bidi_stream(*this, &id, nullptr) == 0)
return CreateStream(id);
break;
}
case Direction::UNIDIRECTIONAL: {
Debug(this, "Opening uni-directional stream");
if (ngtcp2_conn_open_uni_stream(*this, &id, nullptr) == 0)
return CreateStream(id);
break;
}
}
return BaseObjectPtr<Stream>();
}
void Session::AddStream(const BaseObjectPtr<Stream>& stream) {
Debug(this, "Adding stream %" PRIi64 " to session", stream->id());
ngtcp2_conn_set_stream_user_data(*this, stream->id(), stream.get());
streams_[stream->id()] = stream;
// Update tracking statistics for the number of streams associated with this
// session.
switch (stream->origin()) {
case Side::CLIENT: {
if (is_server()) {
switch (stream->direction()) {
case Direction::BIDIRECTIONAL:
STAT_INCREMENT(Stats, bidi_in_stream_count);
break;
case Direction::UNIDIRECTIONAL:
STAT_INCREMENT(Stats, uni_in_stream_count);
break;
}
} else {
switch (stream->direction()) {
case Direction::BIDIRECTIONAL:
STAT_INCREMENT(Stats, bidi_out_stream_count);
break;
case Direction::UNIDIRECTIONAL:
STAT_INCREMENT(Stats, uni_out_stream_count);
break;
}
}
break;
}
case Side::SERVER: {
if (is_server()) {
switch (stream->direction()) {
case Direction::BIDIRECTIONAL:
STAT_INCREMENT(Stats, bidi_out_stream_count);
break;
case Direction::UNIDIRECTIONAL:
STAT_INCREMENT(Stats, uni_out_stream_count);
break;
}
} else {
switch (stream->direction()) {
case Direction::BIDIRECTIONAL:
STAT_INCREMENT(Stats, bidi_in_stream_count);
break;
case Direction::UNIDIRECTIONAL:
STAT_INCREMENT(Stats, uni_in_stream_count);
break;
}
}
break;
}
}
}
void Session::RemoveStream(int64_t id) {
// ngtcp2 does not extend the max streams count automatically except in very
// specific conditions, none of which apply once we've gotten this far. We
// need to manually extend when a remote peer initiated stream is removed.
Debug(this, "Removing stream %" PRIi64 " from session", id);
if (!is_in_draining_period() && !is_in_closing_period() &&
!state_->silent_close &&
!ngtcp2_conn_is_local_stream(connection_.get(), id)) {
if (ngtcp2_is_bidi_stream(id))
ngtcp2_conn_extend_max_streams_bidi(connection_.get(), 1);
else
ngtcp2_conn_extend_max_streams_uni(connection_.get(), 1);
}
// Frees the persistent reference to the Stream object, allowing it to be gc'd
// any time after the JS side releases it's own reference.
streams_.erase(id);
ngtcp2_conn_set_stream_user_data(*this, id, nullptr);
}
void Session::ResumeStream(int64_t id) {
Debug(this, "Resuming stream %" PRIi64, id);
SendPendingDataScope send_scope(this);
application_->ResumeStream(id);
}
void Session::ShutdownStream(int64_t id, QuicError error) {
Debug(this, "Shutting down stream %" PRIi64 " with error %s", id, error);
SendPendingDataScope send_scope(this);
ngtcp2_conn_shutdown_stream(*this,
0,
id,
error.type() == QuicError::Type::APPLICATION
? error.code()
: NGTCP2_APP_NOERROR);
}
void Session::StreamDataBlocked(int64_t id) {
Debug(this, "Stream %" PRIi64 " is blocked", id);
STAT_INCREMENT(Stats, block_count);
application_->BlockStream(id);
}
void Session::ShutdownStreamWrite(int64_t id, QuicError code) {
Debug(this, "Shutting down stream %" PRIi64 " write with error %s", id, code);
SendPendingDataScope send_scope(this);
ngtcp2_conn_shutdown_stream_write(*this,
0,
id,
code.type() == QuicError::Type::APPLICATION
? code.code()
: NGTCP2_APP_NOERROR);
}
void Session::CollectSessionTicketAppData(
SessionTicket::AppData* app_data) const {
application_->CollectSessionTicketAppData(app_data);
}
SessionTicket::AppData::Status Session::ExtractSessionTicketAppData(
const SessionTicket::AppData& app_data,
SessionTicket::AppData::Source::Flag flag) {
return application_->ExtractSessionTicketAppData(app_data, flag);
}
void Session::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("config", config_);
tracker->TrackField("endpoint", endpoint_);
tracker->TrackField("streams", streams_);
tracker->TrackField("local_address", local_address_);
tracker->TrackField("remote_address", remote_address_);
tracker->TrackField("application", application_);
tracker->TrackField("tls_context", tls_context_);
tracker->TrackField("timer", timer_);
tracker->TrackField("conn_closebuf", conn_closebuf_);
tracker->TrackField("qlog_stream", qlog_stream_);
tracker->TrackField("keylog_stream", keylog_stream_);
}
bool Session::is_in_closing_period() const {
return ngtcp2_conn_in_closing_period(*this) != 0;
}
bool Session::is_in_draining_period() const {
return ngtcp2_conn_in_draining_period(*this) != 0;
}
bool Session::wants_session_ticket() const {
return state_->session_ticket == 1;
}
void Session::SetStreamOpenAllowed() {
// TODO(@jasnell): Might remove this. May not be needed
state_->stream_open_allowed = 1;
}
bool Session::can_send_packets() const {
// We can send packets if we're not in the middle of a ngtcp2 callback,
// we're not destroyed, we're not in a draining or closing period, and
// endpoint is set.
return !NgTcp2CallbackScope::in_ngtcp2_callback(env()) && !is_destroyed() &&
!is_in_draining_period() && !is_in_closing_period() && endpoint_;
}
bool Session::can_create_streams() const {
return !state_->destroyed && !state_->graceful_close && !state_->closing &&
!is_in_closing_period() && !is_in_draining_period();
}
uint64_t Session::max_data_left() const {
return ngtcp2_conn_get_max_data_left(*this);
}
uint64_t Session::max_local_streams_uni() const {
return ngtcp2_conn_get_streams_uni_left(*this);
}
uint64_t Session::max_local_streams_bidi() const {
return ngtcp2_conn_get_local_transport_params(*this)
->initial_max_streams_bidi;
}
void Session::set_wrapped() {
state_->wrapped = 1;
}
void Session::set_priority_supported(bool on) {
state_->priority_supported = on ? 1 : 0;
}
void Session::DoClose(bool silent) {
DCHECK(!is_destroyed());
Debug(this, "Session is closing. Silently %s", silent ? "yes" : "no");
// Once Close has been called, we cannot re-enter
if (state_->closing == 1) return;
state_->closing = 1;
state_->silent_close = silent ? 1 : 0;
STAT_RECORD_TIMESTAMP(Stats, closing_at);
// Iterate through all of the known streams and close them. The streams
// will remove themselves from the Session as soon as they are closed.
// Note: we create a copy because the streams will remove themselves
// while they are cleaning up which will invalidate the iterator.
auto streams = streams_;
for (auto& stream : streams) stream.second->Destroy(last_error_);
DCHECK(streams.empty());
// If the state has not been passed out to JavaScript yet, we can skip closing
// entirely and drop directly out to Destroy.
if (!state_->wrapped) return Destroy();
// If we're not running within a ngtcp2 callback scope, schedule a
// CONNECTION_CLOSE to be sent. If we are within a ngtcp2 callback scope,
// sending the CONNECTION_CLOSE will be deferred.
{ MaybeCloseConnectionScope close_scope(this, silent); }
// We emit a close callback so that the JavaScript side can clean up anything
// it needs to clean up before destroying. It's the JavaScript side's
// responsibility to call destroy() when ready.
EmitClose();
}
void Session::ExtendStreamOffset(int64_t id, size_t amount) {
Debug(this, "Extending stream %" PRIi64 " offset by %zu", id, amount);
ngtcp2_conn_extend_max_stream_offset(*this, id, amount);
}
void Session::ExtendOffset(size_t amount) {
Debug(this, "Extending offset by %zu", amount);
ngtcp2_conn_extend_max_offset(*this, amount);
}
void Session::UpdateDataStats() {
if (state_->destroyed) return;
Debug(this, "Updating data stats");
ngtcp2_conn_info info;
ngtcp2_conn_get_conn_info(*this, &info);
STAT_SET(Stats, bytes_in_flight, info.bytes_in_flight);
STAT_SET(Stats, cwnd, info.cwnd);
STAT_SET(Stats, latest_rtt, info.latest_rtt);
STAT_SET(Stats, min_rtt, info.min_rtt);
STAT_SET(Stats, rttvar, info.rttvar);
STAT_SET(Stats, smoothed_rtt, info.smoothed_rtt);
STAT_SET(Stats, ssthresh, info.ssthresh);
STAT_SET(
Stats,
max_bytes_in_flight,
std::max(STAT_GET(Stats, max_bytes_in_flight), info.bytes_in_flight));
}
void Session::SendConnectionClose() {
DCHECK(!NgTcp2CallbackScope::in_ngtcp2_callback(env()));
if (is_destroyed() || is_in_draining_period() || state_->silent_close) return;
Debug(this, "Sending connection close");
auto on_exit = OnScopeLeave([this] { UpdateTimer(); });
switch (config_.side) {
case Side::SERVER: {
if (!is_in_closing_period() && !StartClosingPeriod()) {
Close(CloseMethod::SILENT);
} else {
DCHECK(conn_closebuf_);
Send(conn_closebuf_->Clone());
}
return;
}
case Side::CLIENT: {
Path path(local_address_, remote_address_);
auto packet = Packet::Create(env(),
endpoint_.get(),
remote_address_,
kDefaultMaxPacketLength,
"immediate connection close (client)");
ngtcp2_vec vec = *packet;
ssize_t nwrite = ngtcp2_conn_write_connection_close(
*this, &path, nullptr, vec.base, vec.len, last_error_, uv_hrtime());
if (UNLIKELY(nwrite < 0)) {
packet->Done(UV_ECANCELED);
last_error_ = QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR);
Close(CloseMethod::SILENT);
} else {
packet->Truncate(nwrite);
Send(std::move(packet));
}
return;
}
}
UNREACHABLE();
}
void Session::OnTimeout() {
HandleScope scope(env()->isolate());
if (is_destroyed()) return;
int ret = ngtcp2_conn_handle_expiry(*this, uv_hrtime());
if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period() &&
env()->can_call_into_js()) {
SendPendingDataScope send_scope(this);
return;
}
Debug(this, "Session timed out");
last_error_ = QuicError::ForNgtcp2Error(ret);
Close(CloseMethod::SILENT);
}
void Session::UpdateTimer() {
// Both uv_hrtime and ngtcp2_conn_get_expiry return nanosecond units.
uint64_t expiry = ngtcp2_conn_get_expiry(*this);
uint64_t now = uv_hrtime();
Debug(
this, "Updating timer. Expiry: %" PRIu64 ", now: %" PRIu64, expiry, now);
if (expiry <= now) {
// The timer has already expired.
return OnTimeout();
}
auto timeout = (expiry - now) / NGTCP2_MILLISECONDS;
// If timeout is zero here, it means our timer is less than a millisecond
// off from expiry. Let's bump the timer to 1.
timer_.Update(timeout == 0 ? 1 : timeout);
}
bool Session::StartClosingPeriod() {
if (is_in_closing_period()) return true;
if (is_destroyed()) return false;
Debug(this, "Session is entering closing period");
conn_closebuf_ = Packet::CreateConnectionClosePacket(
env(), endpoint_.get(), remote_address_, *this, last_error_);
// If we were unable to create a connection close packet, we're in trouble.
// Set the internal error and return false so that the session will be
// silently closed.
if (!conn_closebuf_) {
last_error_ = QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR);
return false;
}
return true;
}
void Session::DatagramStatus(uint64_t datagramId, quic::DatagramStatus status) {
switch (status) {
case quic::DatagramStatus::ACKNOWLEDGED: {
Debug(this, "Datagram %" PRIu64 " was acknowledged", datagramId);
STAT_INCREMENT(Stats, datagrams_acknowledged);
break;
}
case quic::DatagramStatus::LOST: {
Debug(this, "Datagram %" PRIu64 " was lost", datagramId);
STAT_INCREMENT(Stats, datagrams_lost);
break;
}
}
EmitDatagramStatus(datagramId, status);
}
void Session::DatagramReceived(const uint8_t* data,
size_t datalen,
DatagramReceivedFlags flag) {
// If there is nothing watching for the datagram on the JavaScript side,
// we just drop it on the floor.
if (state_->datagram == 0 || datalen == 0) return;
auto backing = ArrayBuffer::NewBackingStore(env()->isolate(), datalen);
Debug(this, "Session is receiving datagram of size %zu", datalen);
memcpy(backing->Data(), data, datalen);
STAT_INCREMENT(Stats, datagrams_received);
STAT_INCREMENT_N(Stats, bytes_received, datalen);
EmitDatagram(Store(std::move(backing), datalen), flag);
}
bool Session::GenerateNewConnectionId(ngtcp2_cid* cid,
size_t len,
uint8_t* token) {
CID cid_ = config_.options.cid_factory->Generate(len);
Debug(this, "Generated new connection id %s", cid_);
StatelessResetToken new_token(
token, endpoint_->options().reset_token_secret, cid_);
endpoint_->AssociateCID(cid_, config_.scid);
endpoint_->AssociateStatelessResetToken(new_token, this);
return true;
}
bool Session::HandshakeCompleted() {
if (state_->handshake_completed) return false;
state_->handshake_completed = true;
Debug(this, "Session handshake completed");
STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at);
if (!tls_context_.early_data_was_accepted())
ngtcp2_conn_tls_early_data_rejected(*this);
// When in a server session, handshake completed == handshake confirmed.
if (is_server()) {
HandshakeConfirmed();
if (!endpoint().is_closed() && !endpoint().is_closing()) {
auto token = endpoint().GenerateNewToken(version(), remote_address_);
ngtcp2_vec vec = token;
if (NGTCP2_ERR(ngtcp2_conn_submit_new_token(*this, vec.base, vec.len))) {
// Submitting the new token failed... In this case we're going to
// fail because submitting the new token should only fail if we
// ran out of memory or some other unrecoverable state.
return false;
}
}
}
EmitHandshakeComplete();
return true;
}
void Session::HandshakeConfirmed() {
if (state_->handshake_confirmed) return;
Debug(this, "Session handshake confirmed");
state_->handshake_confirmed = true;
STAT_RECORD_TIMESTAMP(Stats, handshake_confirmed_at);
}
void Session::SelectPreferredAddress(PreferredAddress* preferredAddress) {
if (config_.options.preferred_address_strategy ==
PreferredAddress::Policy::IGNORE_PREFERRED_ADDRESS) {
Debug(this, "Ignoring preferred address");
return;
}
auto local_address = endpoint_->local_address();
int family = local_address.family();
switch (family) {
case AF_INET: {
Debug(this, "Selecting preferred address for AF_INET");
auto ipv4 = preferredAddress->ipv4();
if (ipv4.has_value()) {
if (ipv4->address.empty() || ipv4->port == 0) return;
CHECK(SocketAddress::New(AF_INET,
std::string(ipv4->address).c_str(),
ipv4->port,
&remote_address_));
preferredAddress->Use(ipv4.value());
}
break;
}
case AF_INET6: {
Debug(this, "Selecting preferred address for AF_INET6");
auto ipv6 = preferredAddress->ipv6();
if (ipv6.has_value()) {
if (ipv6->address.empty() || ipv6->port == 0) return;
CHECK(SocketAddress::New(AF_INET,
std::string(ipv6->address).c_str(),
ipv6->port,
&remote_address_));
preferredAddress->Use(ipv6.value());
}
break;
}
}
}
CID Session::new_cid(size_t len) const {
return config_.options.cid_factory->Generate(len);
}
// JavaScript callouts
void Session::EmitClose(const QuicError& error) {
DCHECK(!is_destroyed());
if (!env()->can_call_into_js()) return Destroy();
CallbackScope<Session> cb_scope(this);
Local<Value> argv[] = {
Integer::New(env()->isolate(), static_cast<int>(error.type())),
BigInt::NewFromUnsigned(env()->isolate(), error.code()),
Undefined(env()->isolate()),
};
if (error.reason().length() > 0 &&
!ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) {
return;
}
Debug(this, "Notifying JavaScript of session close");
MakeCallback(
BindingData::Get(env()).session_close_callback(), arraysize(argv), argv);
}
void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) {
DCHECK(!is_destroyed());
if (!env()->can_call_into_js()) return;
CallbackScope cbv_scope(this);
Local<Value> argv[] = {datagram.ToUint8Array(env()),
v8::Boolean::New(env()->isolate(), flag.early)};
Debug(this, "Notifying JavaScript of datagram");
MakeCallback(BindingData::Get(env()).session_datagram_callback(),
arraysize(argv),
argv);
}
void Session::EmitDatagramStatus(uint64_t id, quic::DatagramStatus status) {
DCHECK(!is_destroyed());
if (!env()->can_call_into_js()) return;
CallbackScope<Session> cb_scope(this);
auto& state = BindingData::Get(env());
const auto status_to_string = ([&] {
switch (status) {
case quic::DatagramStatus::ACKNOWLEDGED:
return state.acknowledged_string();
case quic::DatagramStatus::LOST:
return state.lost_string();
}
UNREACHABLE();
})();
Local<Value> argv[] = {BigInt::NewFromUnsigned(env()->isolate(), id),
status_to_string};
Debug(this, "Notifying JavaScript of datagram status");
MakeCallback(state.session_datagram_status_callback(), arraysize(argv), argv);
}
void Session::EmitHandshakeComplete() {
DCHECK(!is_destroyed());
if (!env()->can_call_into_js()) return;
CallbackScope<Session> cb_scope(this);
auto isolate = env()->isolate();
static constexpr auto kServerName = 0;
static constexpr auto kSelectedAlpn = 1;
static constexpr auto kCipherName = 2;
static constexpr auto kCipherVersion = 3;
static constexpr auto kValidationErrorReason = 4;
static constexpr auto kValidationErrorCode = 5;
Local<Value> argv[] = {
Undefined(isolate), // The negotiated server name
Undefined(isolate), // The selected alpn
Undefined(isolate), // Cipher name
Undefined(isolate), // Cipher version
Undefined(isolate), // Validation error reason
Undefined(isolate), // Validation error code
v8::Boolean::New(isolate, tls_context_.early_data_was_accepted())};
int err = tls_context_.VerifyPeerIdentity();
if (err != X509_V_OK && (!crypto::GetValidationErrorReason(env(), err)
.ToLocal(&argv[kValidationErrorReason]) ||
!crypto::GetValidationErrorCode(env(), err)
.ToLocal(&argv[kValidationErrorCode]))) {
return;
}
if (!ToV8Value(env()->context(), tls_context_.servername())
.ToLocal(&argv[kServerName]) ||
!ToV8Value(env()->context(), tls_context_.alpn())
.ToLocal(&argv[kSelectedAlpn]) ||
tls_context_.cipher_name(env()).ToLocal(&argv[kCipherName]) ||
!tls_context_.cipher_version(env()).ToLocal(&argv[kCipherVersion])) {
return;
}
Debug(this, "Notifying JavaScript of handshake complete");
MakeCallback(BindingData::Get(env()).session_handshake_callback(),
arraysize(argv),
argv);
}
void Session::EmitPathValidation(PathValidationResult result,
PathValidationFlags flags,
const ValidatedPath& newPath,
const std::optional<ValidatedPath>& oldPath) {
DCHECK(!is_destroyed());
if (!env()->can_call_into_js()) return;
if (LIKELY(state_->path_validation == 0)) return;
auto isolate = env()->isolate();
CallbackScope<Session> cb_scope(this);
auto& state = BindingData::Get(env());
const auto resultToString = ([&] {
switch (result) {
case PathValidationResult::ABORTED:
return state.aborted_string();
case PathValidationResult::FAILURE:
return state.failure_string();
case PathValidationResult::SUCCESS:
return state.success_string();
}
UNREACHABLE();
})();
Local<Value> argv[] = {
resultToString,
SocketAddressBase::Create(env(), newPath.local)->object(),
SocketAddressBase::Create(env(), newPath.remote)->object(),
Undefined(isolate),
Undefined(isolate),
Boolean::New(isolate, flags.preferredAddress)};
if (oldPath.has_value()) {
argv[3] = SocketAddressBase::Create(env(), oldPath->local)->object();
argv[4] = SocketAddressBase::Create(env(), oldPath->remote)->object();
}
Debug(this, "Notifying JavaScript of path validation");
MakeCallback(state.session_path_validation_callback(), arraysize(argv), argv);
}
void Session::EmitSessionTicket(Store&& ticket) {
DCHECK(!is_destroyed());
if (!env()->can_call_into_js()) return;
// If there is nothing listening for the session ticket, don't bother
// emitting.
if (LIKELY(state_->session_ticket == 0)) return;
CallbackScope<Session> cb_scope(this);
auto remote_transport_params = GetRemoteTransportParams();
Store transport_params;
if (remote_transport_params)
transport_params = remote_transport_params.Encode(env());
SessionTicket session_ticket(std::move(ticket), std::move(transport_params));
Local<Value> argv;
if (session_ticket.encode(env()).ToLocal(&argv)) {
Debug(this, "Notifying JavaScript of session ticket");
MakeCallback(BindingData::Get(env()).session_ticket_callback(), 1, &argv);
}
}
void Session::EmitStream(BaseObjectPtr<Stream> stream) {
if (is_destroyed()) return;
if (!env()->can_call_into_js()) return;
CallbackScope<Session> cb_scope(this);
Local<Value> arg = stream->object();
Debug(this, "Notifying JavaScript of stream created");
MakeCallback(BindingData::Get(env()).stream_created_callback(), 1, &arg);
}
void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd,
const uint32_t* sv,
size_t nsv) {
DCHECK(!is_destroyed());
DCHECK(!is_server());
if (!env()->can_call_into_js()) return;
auto isolate = env()->isolate();
const auto to_integer = [&](uint32_t version) {
return Integer::NewFromUnsigned(isolate, version);
};
CallbackScope<Session> cb_scope(this);
// version() is the version that was actually configured for this session.
// versions are the versions requested by the peer.
MaybeStackBuffer<Local<Value>, 5> versions;
versions.AllocateSufficientStorage(nsv);
for (size_t n = 0; n < nsv; n++) versions[n] = to_integer(sv[n]);
// supported are the versons we acutually support expressed as a range.
// The first value is the minimum version, the second is the maximum.
Local<Value> supported[] = {to_integer(config_.options.min_version),
to_integer(config_.options.version)};
Local<Value> argv[] = {// The version configured for this session.
to_integer(version()),
// The versions requested.
Array::New(isolate, versions.out(), nsv),
// The versions we actually support.
Array::New(isolate, supported, arraysize(supported))};
Debug(this, "Notifying JavaScript of version negotiation");
MakeCallback(BindingData::Get(env()).session_version_negotiation_callback(),
arraysize(argv),
argv);
}
void Session::EmitKeylog(const char* line) {
if (!env()->can_call_into_js()) return;
if (keylog_stream_) {
Debug(this, "Emitting keylog line");
env()->SetImmediate([ptr = keylog_stream_, data = std::string(line) + "\n"](
Environment* env) { ptr->Emit(data); });
}
}
// ============================================================================
// ngtcp2 static callback functions
#define NGTCP2_CALLBACK_SCOPE(name) \
auto name = Impl::From(conn, user_data); \
if (UNLIKELY(name->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE; \
NgTcp2CallbackScope scope(session->env());
struct Session::Impl {
static Session* From(ngtcp2_conn* conn, void* user_data) {
DCHECK_NOT_NULL(user_data);
auto session = static_cast<Session*>(user_data);
DCHECK_EQ(conn, session->connection_.get());
return session;
}
static void DoDestroy(const FunctionCallbackInfo<Value>& args) {
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
session->Destroy();
}
static void GetRemoteAddress(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
auto address = session->remote_address();
args.GetReturnValue().Set(
SocketAddressBase::Create(env, std::make_shared<SocketAddress>(address))
->object());
}
static void GetCertificate(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
Local<Value> ret;
if (session->tls_context().cert(env).ToLocal(&ret))
args.GetReturnValue().Set(ret);
}
static void GetEphemeralKeyInfo(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
Local<Object> ret;
if (!session->is_server() &&
session->tls_context().ephemeral_key(env).ToLocal(&ret))
args.GetReturnValue().Set(ret);
}
static void GetPeerCertificate(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
Local<Value> ret;
if (session->tls_context().peer_cert(env).ToLocal(&ret))
args.GetReturnValue().Set(ret);
}
static void GracefulClose(const FunctionCallbackInfo<Value>& args) {
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
session->Close(Session::CloseMethod::GRACEFUL);
}
static void SilentClose(const FunctionCallbackInfo<Value>& args) {
// This is exposed for testing purposes only!
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
session->Close(Session::CloseMethod::SILENT);
}
static void UpdateKey(const FunctionCallbackInfo<Value>& args) {
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
// Initiating a key update may fail if it is done too early (either
// before the TLS handshake has been confirmed or while a previous
// key update is being processed). When it fails, InitiateKeyUpdate()
// will return false.
args.GetReturnValue().Set(session->tls_context().InitiateKeyUpdate());
}
static void DoOpenStream(const FunctionCallbackInfo<Value>& args) {
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
DCHECK(args[0]->IsUint32());
auto direction = static_cast<Direction>(args[0].As<Uint32>()->Value());
BaseObjectPtr<Stream> stream = session->OpenStream(direction);
if (stream) args.GetReturnValue().Set(stream->object());
}
static void DoSendDatagram(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
DCHECK(args[0]->IsArrayBufferView());
args.GetReturnValue().Set(BigInt::New(
env->isolate(),
session->SendDatagram(Store(args[0].As<ArrayBufferView>()))));
}
static int on_acknowledge_stream_data_offset(ngtcp2_conn* conn,
int64_t stream_id,
uint64_t offset,
uint64_t datalen,
void* user_data,
void* stream_user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->application().AcknowledgeStreamData(Stream::From(stream_user_data),
datalen);
return NGTCP2_SUCCESS;
}
static int on_acknowledge_datagram(ngtcp2_conn* conn,
uint64_t dgram_id,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->DatagramStatus(dgram_id, quic::DatagramStatus::ACKNOWLEDGED);
return NGTCP2_SUCCESS;
}
static int on_cid_status(ngtcp2_conn* conn,
ngtcp2_connection_id_status_type type,
uint64_t seq,
const ngtcp2_cid* cid,
const uint8_t* token,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
std::optional<StatelessResetToken> maybe_reset_token;
if (token != nullptr) maybe_reset_token.emplace(token);
auto& endpoint = session->endpoint();
switch (type) {
case NGTCP2_CONNECTION_ID_STATUS_TYPE_ACTIVATE: {
endpoint.AssociateCID(session->config_.scid, CID(cid));
if (token != nullptr) {
endpoint.AssociateStatelessResetToken(StatelessResetToken(token),
session);
}
break;
}
case NGTCP2_CONNECTION_ID_STATUS_TYPE_DEACTIVATE: {
endpoint.DisassociateCID(CID(cid));
if (token != nullptr) {
endpoint.DisassociateStatelessResetToken(StatelessResetToken(token));
}
break;
}
}
return NGTCP2_SUCCESS;
}
static int on_extend_max_remote_streams_bidi(ngtcp2_conn* conn,
uint64_t max_streams,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->application().ExtendMaxStreams(
EndpointLabel::REMOTE, Direction::BIDIRECTIONAL, max_streams);
return NGTCP2_SUCCESS;
}
static int on_extend_max_remote_streams_uni(ngtcp2_conn* conn,
uint64_t max_streams,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->application().ExtendMaxStreams(
EndpointLabel::REMOTE, Direction::UNIDIRECTIONAL, max_streams);
return NGTCP2_SUCCESS;
}
static int on_extend_max_streams_bidi(ngtcp2_conn* conn,
uint64_t max_streams,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->application().ExtendMaxStreams(
EndpointLabel::LOCAL, Direction::BIDIRECTIONAL, max_streams);
return NGTCP2_SUCCESS;
}
static int on_extend_max_streams_uni(ngtcp2_conn* conn,
uint64_t max_streams,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->application().ExtendMaxStreams(
EndpointLabel::LOCAL, Direction::UNIDIRECTIONAL, max_streams);
return NGTCP2_SUCCESS;
}
static int on_extend_max_stream_data(ngtcp2_conn* conn,
int64_t stream_id,
uint64_t max_data,
void* user_data,
void* stream_user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->application().ExtendMaxStreamData(Stream::From(stream_user_data),
max_data);
return NGTCP2_SUCCESS;
}
static int on_get_new_cid(ngtcp2_conn* conn,
ngtcp2_cid* cid,
uint8_t* token,
size_t cidlen,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
return session->GenerateNewConnectionId(cid, cidlen, token)
? NGTCP2_SUCCESS
: NGTCP2_ERR_CALLBACK_FAILURE;
}
static int on_handshake_completed(ngtcp2_conn* conn, void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
return session->HandshakeCompleted() ? NGTCP2_SUCCESS
: NGTCP2_ERR_CALLBACK_FAILURE;
}
static int on_handshake_confirmed(ngtcp2_conn* conn, void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->HandshakeConfirmed();
return NGTCP2_SUCCESS;
}
static int on_lost_datagram(ngtcp2_conn* conn,
uint64_t dgram_id,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->DatagramStatus(dgram_id, quic::DatagramStatus::LOST);
return NGTCP2_SUCCESS;
}
static int on_path_validation(ngtcp2_conn* conn,
uint32_t flags,
const ngtcp2_path* path,
const ngtcp2_path* old_path,
ngtcp2_path_validation_result res,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
bool flag_preferred_address =
flags & NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR;
ValidatedPath newValidatedPath{
std::make_shared<SocketAddress>(path->local.addr),
std::make_shared<SocketAddress>(path->remote.addr)};
std::optional<ValidatedPath> oldValidatedPath = std::nullopt;
if (old_path != nullptr) {
oldValidatedPath =
ValidatedPath{std::make_shared<SocketAddress>(old_path->local.addr),
std::make_shared<SocketAddress>(old_path->remote.addr)};
}
session->EmitPathValidation(static_cast<PathValidationResult>(res),
PathValidationFlags{flag_preferred_address},
newValidatedPath,
oldValidatedPath);
return NGTCP2_SUCCESS;
}
static int on_receive_datagram(ngtcp2_conn* conn,
uint32_t flags,
const uint8_t* data,
size_t datalen,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
DatagramReceivedFlags f;
f.early = flags & NGTCP2_DATAGRAM_FLAG_0RTT;
session->DatagramReceived(data, datalen, f);
return NGTCP2_SUCCESS;
}
static int on_receive_new_token(ngtcp2_conn* conn,
const uint8_t* token,
size_t tokenlen,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
// We currently do nothing with this callback.
return NGTCP2_SUCCESS;
}
static int on_receive_rx_key(ngtcp2_conn* conn,
ngtcp2_encryption_level level,
void* user_data) {
auto session = Impl::From(conn, user_data);
if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE;
Debug(session,
"Receiving RX key for level %d for dcid %s",
getEncryptionLevelName(level),
session->config().dcid);
if (!session->is_server() && (level == NGTCP2_ENCRYPTION_LEVEL_0RTT ||
level == NGTCP2_ENCRYPTION_LEVEL_1RTT)) {
if (!session->application().Start()) return NGTCP2_ERR_CALLBACK_FAILURE;
}
return NGTCP2_SUCCESS;
}
static int on_receive_stateless_reset(ngtcp2_conn* conn,
const ngtcp2_pkt_stateless_reset* sr,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->state_->stateless_reset = 1;
return NGTCP2_SUCCESS;
}
static int on_receive_stream_data(ngtcp2_conn* conn,
uint32_t flags,
int64_t stream_id,
uint64_t offset,
const uint8_t* data,
size_t datalen,
void* user_data,
void* stream_user_data) {
NGTCP2_CALLBACK_SCOPE(session)
Stream::ReceiveDataFlags f;
f.early = flags & NGTCP2_STREAM_DATA_FLAG_0RTT;
f.fin = flags & NGTCP2_STREAM_DATA_FLAG_FIN;
if (stream_user_data == nullptr) {
// We have an implicitly created stream.
auto stream = session->CreateStream(stream_id);
if (stream) {
session->EmitStream(stream);
session->application().ReceiveStreamData(
stream.get(), data, datalen, f);
} else {
return ngtcp2_conn_shutdown_stream(
*session, 0, stream_id, NGTCP2_APP_NOERROR) == 0
? NGTCP2_SUCCESS
: NGTCP2_ERR_CALLBACK_FAILURE;
}
} else {
session->application().ReceiveStreamData(
Stream::From(stream_user_data), data, datalen, f);
}
return NGTCP2_SUCCESS;
}
static int on_receive_tx_key(ngtcp2_conn* conn,
ngtcp2_encryption_level level,
void* user_data) {
auto session = Impl::From(conn, user_data);
if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE;
Debug(session,
"Receiving TX key for level %d for dcid %s",
getEncryptionLevelName(level),
session->config().dcid);
if (session->is_server() && (level == NGTCP2_ENCRYPTION_LEVEL_0RTT ||
level == NGTCP2_ENCRYPTION_LEVEL_1RTT)) {
if (!session->application().Start()) return NGTCP2_ERR_CALLBACK_FAILURE;
}
return NGTCP2_SUCCESS;
}
static int on_receive_version_negotiation(ngtcp2_conn* conn,
const ngtcp2_pkt_hd* hd,
const uint32_t* sv,
size_t nsv,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->EmitVersionNegotiation(*hd, sv, nsv);
return NGTCP2_SUCCESS;
}
static int on_remove_connection_id(ngtcp2_conn* conn,
const ngtcp2_cid* cid,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->endpoint().DisassociateCID(CID(cid));
return NGTCP2_SUCCESS;
}
static int on_select_preferred_address(ngtcp2_conn* conn,
ngtcp2_path* dest,
const ngtcp2_preferred_addr* paddr,
void* user_data) {
NGTCP2_CALLBACK_SCOPE(session)
PreferredAddress preferred_address(dest, paddr);
session->SelectPreferredAddress(&preferred_address);
return NGTCP2_SUCCESS;
}
static int on_stream_close(ngtcp2_conn* conn,
uint32_t flags,
int64_t stream_id,
uint64_t app_error_code,
void* user_data,
void* stream_user_data) {
NGTCP2_CALLBACK_SCOPE(session)
if (flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET) {
session->application().StreamClose(
Stream::From(stream_user_data),
QuicError::ForApplication(app_error_code));
} else {
session->application().StreamClose(Stream::From(stream_user_data));
}
return NGTCP2_SUCCESS;
}
static int on_stream_reset(ngtcp2_conn* conn,
int64_t stream_id,
uint64_t final_size,
uint64_t app_error_code,
void* user_data,
void* stream_user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->application().StreamReset(
Stream::From(stream_user_data),
final_size,
QuicError::ForApplication(app_error_code));
return NGTCP2_SUCCESS;
}
static int on_stream_stop_sending(ngtcp2_conn* conn,
int64_t stream_id,
uint64_t app_error_code,
void* user_data,
void* stream_user_data) {
NGTCP2_CALLBACK_SCOPE(session)
session->application().StreamStopSending(
Stream::From(stream_user_data),
QuicError::ForApplication(app_error_code));
return NGTCP2_SUCCESS;
}
static void on_rand(uint8_t* dest,
size_t destlen,
const ngtcp2_rand_ctx* rand_ctx) {
CHECK(crypto::CSPRNG(dest, destlen).is_ok());
}
static int on_early_data_rejected(ngtcp2_conn* conn, void* user_data) {
// TODO(@jasnell): Called when early data was rejected by server during the
// TLS handshake or client decided not to attempt early data.
return NGTCP2_SUCCESS;
}
static constexpr ngtcp2_callbacks CLIENT = {
ngtcp2_crypto_client_initial_cb,
nullptr,
ngtcp2_crypto_recv_crypto_data_cb,
on_handshake_completed,
on_receive_version_negotiation,
ngtcp2_crypto_encrypt_cb,
ngtcp2_crypto_decrypt_cb,
ngtcp2_crypto_hp_mask_cb,
on_receive_stream_data,
on_acknowledge_stream_data_offset,
nullptr,
on_stream_close,
on_receive_stateless_reset,
ngtcp2_crypto_recv_retry_cb,
on_extend_max_streams_bidi,
on_extend_max_streams_uni,
on_rand,
on_get_new_cid,
on_remove_connection_id,
ngtcp2_crypto_update_key_cb,
on_path_validation,
on_select_preferred_address,
on_stream_reset,
on_extend_max_remote_streams_bidi,
on_extend_max_remote_streams_uni,
on_extend_max_stream_data,
on_cid_status,
on_handshake_confirmed,
on_receive_new_token,
ngtcp2_crypto_delete_crypto_aead_ctx_cb,
ngtcp2_crypto_delete_crypto_cipher_ctx_cb,
on_receive_datagram,
on_acknowledge_datagram,
on_lost_datagram,
ngtcp2_crypto_get_path_challenge_data_cb,
on_stream_stop_sending,
ngtcp2_crypto_version_negotiation_cb,
on_receive_rx_key,
on_receive_tx_key,
on_early_data_rejected};
static constexpr ngtcp2_callbacks SERVER = {
nullptr,
ngtcp2_crypto_recv_client_initial_cb,
ngtcp2_crypto_recv_crypto_data_cb,
on_handshake_completed,
nullptr,
ngtcp2_crypto_encrypt_cb,
ngtcp2_crypto_decrypt_cb,
ngtcp2_crypto_hp_mask_cb,
on_receive_stream_data,
on_acknowledge_stream_data_offset,
nullptr,
on_stream_close,
on_receive_stateless_reset,
nullptr,
on_extend_max_streams_bidi,
on_extend_max_streams_uni,
on_rand,
on_get_new_cid,
on_remove_connection_id,
ngtcp2_crypto_update_key_cb,
on_path_validation,
nullptr,
on_stream_reset,
on_extend_max_remote_streams_bidi,
on_extend_max_remote_streams_uni,
on_extend_max_stream_data,
on_cid_status,
nullptr,
nullptr,
ngtcp2_crypto_delete_crypto_aead_ctx_cb,
ngtcp2_crypto_delete_crypto_cipher_ctx_cb,
on_receive_datagram,
on_acknowledge_datagram,
on_lost_datagram,
ngtcp2_crypto_get_path_challenge_data_cb,
on_stream_stop_sending,
ngtcp2_crypto_version_negotiation_cb,
on_receive_rx_key,
on_receive_tx_key,
on_early_data_rejected};
};
#undef NGTCP2_CALLBACK_SCOPE
Local<FunctionTemplate> Session::GetConstructorTemplate(Environment* env) {
auto& state = BindingData::Get(env);
auto tmpl = state.session_constructor_template();
if (tmpl.IsEmpty()) {
auto isolate = env->isolate();
tmpl = NewFunctionTemplate(isolate, IllegalConstructor);
tmpl->SetClassName(state.session_string());
tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env));
tmpl->InstanceTemplate()->SetInternalFieldCount(
Session::kInternalFieldCount);
#define V(name, key, no_side_effect) \
if (no_side_effect) { \
SetProtoMethodNoSideEffect(isolate, tmpl, #key, Impl::name); \
} else { \
SetProtoMethod(isolate, tmpl, #key, Impl::name); \
}
SESSION_JS_METHODS(V)
#undef V
state.set_session_constructor_template(tmpl);
}
return tmpl;
}
void Session::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
#define V(name, _, __) registry->Register(Impl::name);
SESSION_JS_METHODS(V)
#undef V
}
Session::QuicConnectionPointer Session::InitConnection() {
ngtcp2_conn* conn;
Path path(local_address_, remote_address_);
Debug(this, "Initializing session for path %s", path);
TransportParams::Config tp_config(
config_.side, config_.ocid, config_.retry_scid);
TransportParams transport_params(tp_config, config_.options.transport_params);
transport_params.GenerateSessionTokens(this);
switch (config_.side) {
case Side::SERVER: {
CHECK_EQ(ngtcp2_conn_server_new(&conn,
config_.dcid,
config_.scid,
path,
config_.version,
&Impl::SERVER,
&config_.settings,
transport_params,
&allocator_,
this),
0);
return QuicConnectionPointer(conn);
}
case Side::CLIENT: {
CHECK_EQ(ngtcp2_conn_client_new(&conn,
config_.dcid,
config_.scid,
path,
config_.version,
&Impl::CLIENT,
&config_.settings,
transport_params,
&allocator_,
this),
0);
if (config_.session_ticket.has_value())
tls_context_.MaybeSetEarlySession(config_.session_ticket.value());
return QuicConnectionPointer(conn);
}
}
UNREACHABLE();
}
void Session::InitPerIsolate(IsolateData* data,
v8::Local<v8::ObjectTemplate> target) {
// TODO(@jasnell): Implement the per-isolate state
}
void Session::InitPerContext(Realm* realm, Local<Object> target) {
// Make sure the Session constructor template is initialized.
USE(GetConstructorTemplate(realm->env()));
TransportParams::Initialize(realm->env(), target);
PreferredAddress::Initialize(realm->env(), target);
static constexpr auto STREAM_DIRECTION_BIDIRECTIONAL =
static_cast<uint32_t>(Direction::BIDIRECTIONAL);
static constexpr auto STREAM_DIRECTION_UNIDIRECTIONAL =
static_cast<uint32_t>(Direction::UNIDIRECTIONAL);
static constexpr auto QUIC_PROTO_MAX = NGTCP2_PROTO_VER_MAX;
static constexpr auto QUIC_PROTO_MIN = NGTCP2_PROTO_VER_MIN;
NODE_DEFINE_CONSTANT(target, STREAM_DIRECTION_BIDIRECTIONAL);
NODE_DEFINE_CONSTANT(target, STREAM_DIRECTION_UNIDIRECTIONAL);
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_HEADER_LIST_PAIRS);
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_HEADER_LENGTH);
NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MAX);
NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MIN);
#define V(name, _) IDX_STATS_SESSION_##name,
enum SessionStatsIdx { SESSION_STATS(V) IDX_STATS_SESSION_COUNT };
#undef V
#define V(name, key, __) \
auto IDX_STATE_SESSION_##name = offsetof(Session::State, key);
SESSION_STATE(V)
#undef V
#define V(name, _) NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_##name);
SESSION_STATS(V)
NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_COUNT);
#undef V
#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_SESSION_##name);
SESSION_STATE(V)
#undef V
}
} // namespace quic
} // namespace node
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC