mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
quic: rework TLSContext, additional cleanups
PR-URL: https://github.com/nodejs/node/pull/51340 Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io> Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
parent
3f74b68e39
commit
06a3a2a1fb
28 changed files with 1269 additions and 924 deletions
1
node.gyp
1
node.gyp
|
@ -415,6 +415,7 @@
|
|||
'test/cctest/test_node_crypto.cc',
|
||||
'test/cctest/test_node_crypto_env.cc',
|
||||
'test/cctest/test_quic_cid.cc',
|
||||
'test/cctest/test_quic_error.cc',
|
||||
'test/cctest/test_quic_tokens.cc',
|
||||
],
|
||||
'node_cctest_inspector_sources': [
|
||||
|
|
|
@ -53,7 +53,7 @@ static const char system_cert_path[] = NODE_OPENSSL_SYSTEM_CERT_PATH;
|
|||
|
||||
static bool extra_root_certs_loaded = false;
|
||||
|
||||
inline X509_STORE* GetOrCreateRootCertStore() {
|
||||
X509_STORE* GetOrCreateRootCertStore() {
|
||||
// Guaranteed thread-safe by standard, just don't use -fno-threadsafe-statics.
|
||||
static X509_STORE* store = NewRootCertStore();
|
||||
return store;
|
||||
|
@ -140,6 +140,8 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
|
|||
return ret;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Read a file that contains our certificate in "PEM" format,
|
||||
// possibly followed by a sequence of CA certificates that should be
|
||||
// sent to the peer in the Certificate message.
|
||||
|
@ -194,8 +196,6 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
|
|||
issuer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
X509_STORE* NewRootCertStore() {
|
||||
static std::vector<X509*> root_certs_vector;
|
||||
static Mutex root_certs_vector_mutex;
|
||||
|
|
|
@ -24,6 +24,8 @@ void IsExtraRootCertsFileLoaded(
|
|||
|
||||
X509_STORE* NewRootCertStore();
|
||||
|
||||
X509_STORE* GetOrCreateRootCertStore();
|
||||
|
||||
BIOPointer LoadBIO(Environment* env, v8::Local<v8::Value> v);
|
||||
|
||||
class SecureContext final : public BaseObject {
|
||||
|
@ -153,6 +155,11 @@ class SecureContext final : public BaseObject {
|
|||
unsigned char ticket_key_hmac_[16];
|
||||
};
|
||||
|
||||
int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
|
||||
BIOPointer&& in,
|
||||
X509Pointer* cert,
|
||||
X509Pointer* issuer);
|
||||
|
||||
} // namespace crypto
|
||||
} // namespace node
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#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>
|
||||
|
@ -95,6 +96,20 @@ Maybe<Session::Application_Options> Session::Application_Options::From(
|
|||
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) {}
|
||||
|
||||
|
@ -189,7 +204,7 @@ Packet* Session::Application::CreateStreamDataPacket() {
|
|||
return Packet::Create(env(),
|
||||
session_->endpoint_.get(),
|
||||
session_->remote_address_,
|
||||
ngtcp2_conn_get_max_tx_udp_payload_size(*session_),
|
||||
session_->max_packet_size(),
|
||||
"stream data");
|
||||
}
|
||||
|
||||
|
@ -221,141 +236,188 @@ void Session::Application::StreamReset(Stream* stream,
|
|||
}
|
||||
|
||||
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;
|
||||
int err = 0;
|
||||
|
||||
size_t maxPacketCount = std::min(static_cast<size_t>(64000),
|
||||
ngtcp2_conn_get_send_quantum(*session_));
|
||||
size_t packetSendCount = 0;
|
||||
|
||||
const auto updateTimer = [&] {
|
||||
Debug(session_, "Application updating the session timer");
|
||||
ngtcp2_conn_update_pkt_tx_time(*session_, uv_hrtime());
|
||||
session_->UpdateTimer();
|
||||
};
|
||||
|
||||
const auto congestionLimited = [&](auto packet) {
|
||||
auto len = pos - ngtcp2_vec(*packet).base;
|
||||
// We are either congestion limited or done.
|
||||
if (len) {
|
||||
// Some data was serialized into the packet. We need to send it.
|
||||
packet->Truncate(len);
|
||||
session_->Send(std::move(packet), path);
|
||||
}
|
||||
|
||||
updateTimer();
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
ssize_t ndatalen;
|
||||
StreamData stream_data;
|
||||
|
||||
err = GetStreamData(&stream_data);
|
||||
|
||||
if (err < 0) {
|
||||
session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL);
|
||||
return session_->Close(Session::CloseMethod::SILENT);
|
||||
}
|
||||
uint8_t* begin = nullptr;
|
||||
|
||||
auto ensure_packet = [&] {
|
||||
if (packet == nullptr) {
|
||||
packet = CreateStreamDataPacket();
|
||||
if (packet == nullptr) {
|
||||
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);
|
||||
}
|
||||
pos = ngtcp2_vec(*packet).base;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
ssize_t nwrite = WriteVStream(&path, pos, &ndatalen, stream_data);
|
||||
// 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);
|
||||
|
||||
if (nwrite <= 0) {
|
||||
// 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 0:
|
||||
if (stream_data.id >= 0) ResumeStream(stream_data.id);
|
||||
return congestionLimited(std::move(packet));
|
||||
case NGTCP2_ERR_STREAM_DATA_BLOCKED: {
|
||||
session().StreamDataBlocked(stream_data.id);
|
||||
if (session().max_data_left() == 0) {
|
||||
if (stream_data.id >= 0) ResumeStream(stream_data.id);
|
||||
return congestionLimited(std::move(packet));
|
||||
}
|
||||
CHECK_LE(ndatalen, 0);
|
||||
// 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 has been closed
|
||||
// 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!
|
||||
CHECK_GE(stream_data.id, 0);
|
||||
// We need to notify the stream that the writable side has been closed
|
||||
// and no more outbound data can be sent.
|
||||
CHECK_LE(ndatalen, 0);
|
||||
auto stream = session_->FindStream(stream_data.id);
|
||||
if (stream) stream->EndWritable();
|
||||
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: {
|
||||
CHECK_GT(ndatalen, 0);
|
||||
if (!StreamCommit(&stream_data, ndatalen)) return session_->Close();
|
||||
pos += ndatalen;
|
||||
// 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);
|
||||
session_->last_error_ = QuicError::ForNgtcp2Error(nwrite);
|
||||
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;
|
||||
if (ndatalen > 0 && !StreamCommit(&stream_data, ndatalen)) {
|
||||
// Since we are closing the session here, we don't worry about updating
|
||||
// the pkt tx time. The failed StreamCommit should have updated the
|
||||
// last_error_ appropriately.
|
||||
packet->Done(UV_ECANCELED);
|
||||
return session_->Close(Session::CloseMethod::SILENT);
|
||||
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();
|
||||
}
|
||||
|
||||
if (stream_data.id >= 0 && ndatalen < 0) ResumeStream(stream_data.id);
|
||||
|
||||
packet->Truncate(nwrite);
|
||||
session_->Send(std::move(packet), path);
|
||||
|
||||
pos = nullptr;
|
||||
|
||||
if (++packetSendCount == maxPacketCount) {
|
||||
break;
|
||||
// Prepare to loop back around to prepare a new packet.
|
||||
packet = nullptr;
|
||||
pos = begin = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer();
|
||||
}
|
||||
|
||||
ssize_t Session::Application::WriteVStream(PathStorage* path,
|
||||
uint8_t* buf,
|
||||
uint8_t* dest,
|
||||
ssize_t* ndatalen,
|
||||
size_t max_packet_size,
|
||||
const StreamData& stream_data) {
|
||||
CHECK_LE(stream_data.count, kMaxVectorCount);
|
||||
uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_NONE;
|
||||
if (stream_data.remaining > 0) flags |= NGTCP2_WRITE_STREAM_FLAG_MORE;
|
||||
DCHECK_LE(stream_data.count, kMaxVectorCount);
|
||||
uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE;
|
||||
if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN;
|
||||
ssize_t ret = ngtcp2_conn_writev_stream(
|
||||
*session_,
|
||||
ngtcp2_pkt_info pi;
|
||||
return ngtcp2_conn_writev_stream(*session_,
|
||||
&path->path,
|
||||
nullptr,
|
||||
buf,
|
||||
ngtcp2_conn_get_max_tx_udp_payload_size(*session_),
|
||||
&pi,
|
||||
dest,
|
||||
max_packet_size,
|
||||
ndatalen,
|
||||
flags,
|
||||
stream_data.id,
|
||||
stream_data.buf,
|
||||
stream_data.count,
|
||||
uv_hrtime());
|
||||
return ret;
|
||||
}
|
||||
|
||||
// The DefaultApplication is the default implementation of Session::Application
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include "quic/defs.h"
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
|
||||
#include "bindingdata.h"
|
||||
#include "defs.h"
|
||||
#include "session.h"
|
||||
#include "sessionticket.h"
|
||||
#include "streams.h"
|
||||
|
@ -18,10 +20,7 @@ class Session::Application : public MemoryRetainer {
|
|||
using Options = Session::Application_Options;
|
||||
|
||||
Application(Session* session, const Options& options);
|
||||
Application(const Application&) = delete;
|
||||
Application(Application&&) = delete;
|
||||
Application& operator=(const Application&) = delete;
|
||||
Application& operator=(Application&&) = delete;
|
||||
DISALLOW_COPY_AND_MOVE(Application)
|
||||
|
||||
virtual bool Start();
|
||||
|
||||
|
@ -115,26 +114,26 @@ class Session::Application : public MemoryRetainer {
|
|||
// the default stream priority.
|
||||
virtual StreamPriority GetStreamPriority(const Stream& stream);
|
||||
|
||||
protected:
|
||||
inline Environment* env() const { return session_->env(); }
|
||||
inline Session& session() { return *session_; }
|
||||
inline const Session& session() const { return *session_; }
|
||||
|
||||
Packet* CreateStreamDataPacket();
|
||||
|
||||
struct StreamData;
|
||||
|
||||
virtual int GetStreamData(StreamData* data) = 0;
|
||||
virtual bool StreamCommit(StreamData* data, size_t datalen) = 0;
|
||||
virtual bool ShouldSetFin(const StreamData& data) = 0;
|
||||
|
||||
inline Environment* env() const { return session_->env(); }
|
||||
inline Session& session() { return *session_; }
|
||||
inline const Session& session() const { return *session_; }
|
||||
|
||||
private:
|
||||
Packet* CreateStreamDataPacket();
|
||||
|
||||
// Write the given stream_data into the buffer.
|
||||
ssize_t WriteVStream(PathStorage* path,
|
||||
uint8_t* buf,
|
||||
ssize_t* ndatalen,
|
||||
size_t max_packet_size,
|
||||
const StreamData& stream_data);
|
||||
|
||||
private:
|
||||
Session* session_;
|
||||
};
|
||||
|
||||
|
@ -151,6 +150,8 @@ struct Session::Application::StreamData final {
|
|||
BaseObjectPtr<Stream> stream;
|
||||
|
||||
inline operator nghttp3_vec() const { return {data[0].base, data[0].len}; }
|
||||
|
||||
std::string ToString() const;
|
||||
};
|
||||
|
||||
} // namespace quic
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
#include <node.h>
|
||||
#include <node_mem.h>
|
||||
#include <v8.h>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include "defs.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
@ -22,61 +22,6 @@ namespace quic {
|
|||
class Endpoint;
|
||||
class Packet;
|
||||
|
||||
enum class Side {
|
||||
CLIENT,
|
||||
SERVER,
|
||||
};
|
||||
|
||||
enum class EndpointLabel {
|
||||
LOCAL,
|
||||
REMOTE,
|
||||
};
|
||||
|
||||
enum class Direction {
|
||||
BIDIRECTIONAL,
|
||||
UNIDIRECTIONAL,
|
||||
};
|
||||
|
||||
enum class HeadersKind {
|
||||
HINTS,
|
||||
INITIAL,
|
||||
TRAILING,
|
||||
};
|
||||
|
||||
enum class HeadersFlags {
|
||||
NONE,
|
||||
TERMINAL,
|
||||
};
|
||||
|
||||
enum class StreamPriority {
|
||||
DEFAULT = NGHTTP3_DEFAULT_URGENCY,
|
||||
LOW = NGHTTP3_URGENCY_LOW,
|
||||
HIGH = NGHTTP3_URGENCY_HIGH,
|
||||
};
|
||||
|
||||
enum class StreamPriorityFlags {
|
||||
NONE,
|
||||
NON_INCREMENTAL,
|
||||
};
|
||||
|
||||
enum class PathValidationResult : uint8_t {
|
||||
SUCCESS = NGTCP2_PATH_VALIDATION_RESULT_SUCCESS,
|
||||
FAILURE = NGTCP2_PATH_VALIDATION_RESULT_FAILURE,
|
||||
ABORTED = NGTCP2_PATH_VALIDATION_RESULT_ABORTED,
|
||||
};
|
||||
|
||||
enum class DatagramStatus {
|
||||
ACKNOWLEDGED,
|
||||
LOST,
|
||||
};
|
||||
|
||||
constexpr uint64_t NGTCP2_APP_NOERROR = 65280;
|
||||
constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
|
||||
constexpr size_t kMaxSizeT = std::numeric_limits<size_t>::max();
|
||||
constexpr uint64_t kMaxSafeJsInteger = 9007199254740991;
|
||||
constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
|
||||
constexpr size_t kMaxVectorCount = 16;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// The FunctionTemplates the BindingData will store for us.
|
||||
|
@ -135,7 +80,6 @@ constexpr size_t kMaxVectorCount = 16;
|
|||
V(failure, "failure") \
|
||||
V(groups, "groups") \
|
||||
V(handshake_timeout, "handshakeTimeout") \
|
||||
V(hostname, "hostname") \
|
||||
V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \
|
||||
V(initial_max_data, "initialMaxData") \
|
||||
V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \
|
||||
|
@ -172,11 +116,10 @@ constexpr size_t kMaxVectorCount = 16;
|
|||
V(reject_unauthorized, "rejectUnauthorized") \
|
||||
V(reno, "reno") \
|
||||
V(retry_token_expiration, "retryTokenExpiration") \
|
||||
V(request_peer_certificate, "requestPeerCertificate") \
|
||||
V(reset_token_secret, "resetTokenSecret") \
|
||||
V(rx_loss, "rxDiagnosticLoss") \
|
||||
V(session, "Session") \
|
||||
V(session_id_ctx, "sessionIDContext") \
|
||||
V(sni, "sni") \
|
||||
V(stream, "Stream") \
|
||||
V(success, "success") \
|
||||
V(tls_options, "tls") \
|
||||
|
@ -189,7 +132,8 @@ constexpr size_t kMaxVectorCount = 16;
|
|||
V(udp_ttl, "udpTTL") \
|
||||
V(unacknowledged_packet_threshold, "unacknowledgedPacketThreshold") \
|
||||
V(validate_address, "validateAddress") \
|
||||
V(verify_hostname_identity, "verifyHostnameIdentity") \
|
||||
V(verify_client, "verifyClient") \
|
||||
V(verify_private_key, "verifyPrivateKey") \
|
||||
V(version, "version")
|
||||
|
||||
// =============================================================================
|
||||
|
@ -209,6 +153,7 @@ class BindingData final
|
|||
static BindingData& Get(Environment* env);
|
||||
|
||||
BindingData(Realm* realm, v8::Local<v8::Object> object);
|
||||
DISALLOW_COPY_AND_MOVE(BindingData)
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
SET_MEMORY_INFO_NAME(BindingData)
|
||||
|
@ -288,6 +233,7 @@ void IllegalConstructor(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|||
struct NgTcp2CallbackScope {
|
||||
Environment* env;
|
||||
explicit NgTcp2CallbackScope(Environment* env);
|
||||
DISALLOW_COPY_AND_MOVE(NgTcp2CallbackScope)
|
||||
~NgTcp2CallbackScope();
|
||||
static bool in_ngtcp2_callback(Environment* env);
|
||||
};
|
||||
|
@ -295,6 +241,7 @@ struct NgTcp2CallbackScope {
|
|||
struct NgHttp3CallbackScope {
|
||||
Environment* env;
|
||||
explicit NgHttp3CallbackScope(Environment* env);
|
||||
DISALLOW_COPY_AND_MOVE(NgHttp3CallbackScope)
|
||||
~NgHttp3CallbackScope();
|
||||
static bool in_nghttp3_callback(Environment* env);
|
||||
};
|
||||
|
@ -305,10 +252,7 @@ struct CallbackScopeBase {
|
|||
v8::TryCatch try_catch;
|
||||
|
||||
explicit CallbackScopeBase(Environment* env);
|
||||
CallbackScopeBase(const CallbackScopeBase&) = delete;
|
||||
CallbackScopeBase(CallbackScopeBase&&) = delete;
|
||||
CallbackScopeBase& operator=(const CallbackScopeBase&) = delete;
|
||||
CallbackScopeBase& operator=(CallbackScopeBase&&) = delete;
|
||||
DISALLOW_COPY_AND_MOVE(CallbackScopeBase)
|
||||
~CallbackScopeBase();
|
||||
};
|
||||
|
||||
|
@ -319,6 +263,7 @@ struct CallbackScope final : public CallbackScopeBase {
|
|||
BaseObjectPtr<T> ref;
|
||||
explicit CallbackScope(const T* ptr)
|
||||
: CallbackScopeBase(ptr->env()), ref(ptr) {}
|
||||
DISALLOW_COPY_AND_MOVE(CallbackScope)
|
||||
explicit CallbackScope(T* ptr) : CallbackScopeBase(ptr->env()), ref(ptr) {}
|
||||
};
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include <memory_tracker-inl.h>
|
||||
#include <node_mutex.h>
|
||||
#include <string_bytes.h>
|
||||
#include "quic/defs.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
@ -99,10 +100,7 @@ namespace {
|
|||
class RandomCIDFactory : public CID::Factory {
|
||||
public:
|
||||
RandomCIDFactory() = default;
|
||||
RandomCIDFactory(const RandomCIDFactory&) = delete;
|
||||
RandomCIDFactory(RandomCIDFactory&&) = delete;
|
||||
RandomCIDFactory& operator=(const RandomCIDFactory&) = delete;
|
||||
RandomCIDFactory& operator=(RandomCIDFactory&&) = delete;
|
||||
DISALLOW_COPY_AND_MOVE(RandomCIDFactory)
|
||||
|
||||
CID Generate(size_t length_hint) const override {
|
||||
DCHECK_GE(length_hint, CID::kMinLength);
|
||||
|
@ -114,7 +112,7 @@ class RandomCIDFactory : public CID::Factory {
|
|||
return CID(start, length_hint);
|
||||
}
|
||||
|
||||
void GenerateInto(ngtcp2_cid* cid,
|
||||
CID GenerateInto(ngtcp2_cid* cid,
|
||||
size_t length_hint = CID::kMaxLength) const override {
|
||||
DCHECK_GE(length_hint, CID::kMinLength);
|
||||
DCHECK_LE(length_hint, CID::kMaxLength);
|
||||
|
@ -123,6 +121,7 @@ class RandomCIDFactory : public CID::Factory {
|
|||
auto start = pool_ + pos_;
|
||||
pos_ += length_hint;
|
||||
ngtcp2_cid_init(cid, start, length_hint);
|
||||
return CID(cid);
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <memory_tracker.h>
|
||||
#include <ngtcp2/ngtcp2.h>
|
||||
#include <string>
|
||||
#include "defs.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
@ -50,9 +51,8 @@ class CID final : public MemoryRetainer {
|
|||
explicit CID(const ngtcp2_cid* cid);
|
||||
|
||||
CID(const CID& other);
|
||||
CID(CID&& other) = delete;
|
||||
|
||||
CID& operator=(const CID& other);
|
||||
CID(CID&&) = delete;
|
||||
|
||||
struct Hash final {
|
||||
size_t operator()(const CID& cid) const;
|
||||
|
@ -111,9 +111,8 @@ class CID::Factory {
|
|||
virtual CID Generate(size_t length_hint = CID::kMaxLength) const = 0;
|
||||
|
||||
// Generate a new CID into the given ngtcp2_cid. This variation of
|
||||
// Generate should be used far less commonly. It is provided largely
|
||||
// for a couple of internal cases.
|
||||
virtual void GenerateInto(ngtcp2_cid* cid,
|
||||
// Generate should be used far less commonly.
|
||||
virtual CID GenerateInto(ngtcp2_cid* cid,
|
||||
size_t length_hint = CID::kMaxLength) const = 0;
|
||||
|
||||
// The default random CID generator instance.
|
||||
|
|
|
@ -48,12 +48,28 @@ std::string Path::ToString() const {
|
|||
}
|
||||
|
||||
PathStorage::PathStorage() {
|
||||
ngtcp2_path_storage_zero(this);
|
||||
Reset();
|
||||
}
|
||||
PathStorage::operator ngtcp2_path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
void PathStorage::Reset() {
|
||||
ngtcp2_path_storage_zero(this);
|
||||
}
|
||||
|
||||
void PathStorage::CopyTo(PathStorage* path) const {
|
||||
ngtcp2_path_copy(&path->path, &this->path);
|
||||
}
|
||||
|
||||
bool PathStorage::operator==(const PathStorage& other) const {
|
||||
return ngtcp2_path_eq(&path, &other.path) != 0;
|
||||
}
|
||||
|
||||
bool PathStorage::operator!=(const PathStorage& other) const {
|
||||
return ngtcp2_path_eq(&path, &other.path) == 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Store::Store(std::shared_ptr<v8::BackingStore> store,
|
||||
|
@ -146,7 +162,7 @@ std::string TypeName(QuicError::Type type) {
|
|||
}
|
||||
} // namespace
|
||||
|
||||
QuicError::QuicError(const std::string_view reason)
|
||||
QuicError::QuicError(const std::string& reason)
|
||||
: reason_(reason), error_(), ptr_(&error_) {
|
||||
ngtcp2_ccerr_default(&error_);
|
||||
}
|
||||
|
@ -186,7 +202,7 @@ QuicError::Type QuicError::type() const {
|
|||
return static_cast<Type>(ptr_->type);
|
||||
}
|
||||
|
||||
QuicError::error_code QuicError::code() const {
|
||||
error_code QuicError::code() const {
|
||||
return ptr_->error_code;
|
||||
}
|
||||
|
||||
|
@ -206,6 +222,39 @@ QuicError::operator const ngtcp2_ccerr*() const {
|
|||
return ptr_;
|
||||
}
|
||||
|
||||
std::string QuicError::reason_for_liberr(int liberr) {
|
||||
return ngtcp2_strerror(liberr);
|
||||
}
|
||||
|
||||
std::string QuicError::reason_for_h3_liberr(int liberr) {
|
||||
return nghttp3_strerror(liberr);
|
||||
}
|
||||
|
||||
bool QuicError::is_fatal_liberror(int liberr) {
|
||||
return ngtcp2_err_is_fatal(liberr) != 0;
|
||||
}
|
||||
|
||||
bool QuicError::is_fatal_h3_liberror(int liberr) {
|
||||
return nghttp3_err_is_fatal(liberr) != 0;
|
||||
}
|
||||
|
||||
error_code QuicError::liberr_to_code(int liberr) {
|
||||
return ngtcp2_err_infer_quic_transport_error_code(liberr);
|
||||
}
|
||||
|
||||
error_code QuicError::h3_liberr_to_code(int liberr) {
|
||||
return nghttp3_err_infer_quic_app_error_code(liberr);
|
||||
}
|
||||
|
||||
bool QuicError::is_crypto() const {
|
||||
return code() & NGTCP2_CRYPTO_ERROR;
|
||||
}
|
||||
|
||||
std::optional<int> QuicError::crypto_error() const {
|
||||
if (!is_crypto()) return std::nullopt;
|
||||
return code() & ~NGTCP2_CRYPTO_ERROR;
|
||||
}
|
||||
|
||||
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
|
||||
Local<Value> argv[] = {
|
||||
Integer::New(env->isolate(), static_cast<int>(type())),
|
||||
|
@ -217,6 +266,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
|
|||
!node::ToV8Value(env->context(), reason()).ToLocal(&argv[2])) {
|
||||
return MaybeLocal<Value>();
|
||||
}
|
||||
|
||||
return Array::New(env->isolate(), argv, arraysize(argv)).As<Value>();
|
||||
}
|
||||
|
||||
|
@ -232,39 +282,37 @@ void QuicError::MemoryInfo(MemoryTracker* tracker) const {
|
|||
tracker->TrackField("reason", reason_.length());
|
||||
}
|
||||
|
||||
QuicError QuicError::ForTransport(error_code code,
|
||||
const std::string_view reason) {
|
||||
QuicError error(reason);
|
||||
QuicError QuicError::ForTransport(error_code code, std::string reason) {
|
||||
QuicError error(std::move(reason));
|
||||
ngtcp2_ccerr_set_transport_error(
|
||||
&error.error_, code, error.reason_c_str(), reason.length());
|
||||
return error;
|
||||
}
|
||||
|
||||
QuicError QuicError::ForApplication(error_code code,
|
||||
const std::string_view reason) {
|
||||
QuicError error(reason);
|
||||
QuicError QuicError::ForApplication(error_code code, std::string reason) {
|
||||
QuicError error(std::move(reason));
|
||||
ngtcp2_ccerr_set_application_error(
|
||||
&error.error_, code, error.reason_c_str(), reason.length());
|
||||
return error;
|
||||
}
|
||||
|
||||
QuicError QuicError::ForVersionNegotiation(const std::string_view reason) {
|
||||
return ForNgtcp2Error(NGTCP2_ERR_RECV_VERSION_NEGOTIATION, reason);
|
||||
QuicError QuicError::ForVersionNegotiation(std::string reason) {
|
||||
return ForNgtcp2Error(NGTCP2_ERR_RECV_VERSION_NEGOTIATION, std::move(reason));
|
||||
}
|
||||
|
||||
QuicError QuicError::ForIdleClose(const std::string_view reason) {
|
||||
return ForNgtcp2Error(NGTCP2_ERR_IDLE_CLOSE, reason);
|
||||
QuicError QuicError::ForIdleClose(std::string reason) {
|
||||
return ForNgtcp2Error(NGTCP2_ERR_IDLE_CLOSE, std::move(reason));
|
||||
}
|
||||
|
||||
QuicError QuicError::ForNgtcp2Error(int code, const std::string_view reason) {
|
||||
QuicError error(reason);
|
||||
QuicError QuicError::ForNgtcp2Error(int code, std::string reason) {
|
||||
QuicError error(std::move(reason));
|
||||
ngtcp2_ccerr_set_liberr(
|
||||
&error.error_, code, error.reason_c_str(), reason.length());
|
||||
return error;
|
||||
}
|
||||
|
||||
QuicError QuicError::ForTlsAlert(int code, const std::string_view reason) {
|
||||
QuicError error(reason);
|
||||
QuicError QuicError::ForTlsAlert(int code, std::string reason) {
|
||||
QuicError error(std::move(reason));
|
||||
ngtcp2_ccerr_set_tls_alert(
|
||||
&error.error_, code, error.reason_c_str(), reason.length());
|
||||
return error;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
|
||||
|
@ -11,6 +10,8 @@
|
|||
#include <node_internals.h>
|
||||
#include <node_sockaddr.h>
|
||||
#include <v8.h>
|
||||
#include <string>
|
||||
#include "defs.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
@ -24,6 +25,12 @@ struct Path final : public ngtcp2_path {
|
|||
struct PathStorage final : public ngtcp2_path_storage {
|
||||
PathStorage();
|
||||
operator ngtcp2_path();
|
||||
|
||||
void Reset();
|
||||
void CopyTo(PathStorage* path) const;
|
||||
|
||||
bool operator==(const PathStorage& other) const;
|
||||
bool operator!=(const PathStorage& other) const;
|
||||
};
|
||||
|
||||
class Store final : public MemoryRetainer {
|
||||
|
@ -67,8 +74,6 @@ class Store final : public MemoryRetainer {
|
|||
|
||||
class QuicError final : public MemoryRetainer {
|
||||
public:
|
||||
using error_code = uint64_t;
|
||||
|
||||
static constexpr error_code QUIC_NO_ERROR = NGTCP2_NO_ERROR;
|
||||
static constexpr error_code QUIC_APP_NO_ERROR = 65280;
|
||||
|
||||
|
@ -79,12 +84,7 @@ class QuicError final : public MemoryRetainer {
|
|||
IDLE_CLOSE = NGTCP2_CCERR_TYPE_IDLE_CLOSE,
|
||||
};
|
||||
|
||||
static constexpr error_code QUIC_ERROR_TYPE_TRANSPORT =
|
||||
NGTCP2_CCERR_TYPE_TRANSPORT;
|
||||
static constexpr error_code QUIC_ERROR_TYPE_APPLICATION =
|
||||
NGTCP2_CCERR_TYPE_APPLICATION;
|
||||
|
||||
explicit QuicError(const std::string_view reason = "");
|
||||
explicit QuicError(const std::string& reason = "");
|
||||
explicit QuicError(const ngtcp2_ccerr* ptr);
|
||||
explicit QuicError(const ngtcp2_ccerr& error);
|
||||
|
||||
|
@ -100,6 +100,9 @@ class QuicError final : public MemoryRetainer {
|
|||
// transport or application.
|
||||
operator bool() const;
|
||||
|
||||
bool is_crypto() const;
|
||||
std::optional<int> crypto_error() const;
|
||||
|
||||
bool operator==(const QuicError& other) const;
|
||||
bool operator!=(const QuicError& other) const;
|
||||
|
||||
|
@ -110,14 +113,19 @@ class QuicError final : public MemoryRetainer {
|
|||
std::string ToString() const;
|
||||
v8::MaybeLocal<v8::Value> ToV8Value(Environment* env) const;
|
||||
|
||||
static QuicError ForTransport(error_code code,
|
||||
const std::string_view reason = "");
|
||||
static QuicError ForApplication(error_code code,
|
||||
const std::string_view reason = "");
|
||||
static QuicError ForVersionNegotiation(const std::string_view reason = "");
|
||||
static QuicError ForIdleClose(const std::string_view reason = "");
|
||||
static QuicError ForNgtcp2Error(int code, const std::string_view reason = "");
|
||||
static QuicError ForTlsAlert(int code, const std::string_view reason = "");
|
||||
static std::string reason_for_liberr(int liberr);
|
||||
static std::string reason_for_h3_liberr(int liberr);
|
||||
static bool is_fatal_liberror(int liberr);
|
||||
static bool is_fatal_h3_liberror(int liberr);
|
||||
static error_code liberr_to_code(int liberr);
|
||||
static error_code h3_liberr_to_code(int liberr);
|
||||
|
||||
static QuicError ForTransport(error_code code, std::string reason = "");
|
||||
static QuicError ForApplication(error_code code, std::string reason = "");
|
||||
static QuicError ForVersionNegotiation(std::string reason = "");
|
||||
static QuicError ForIdleClose(std::string reason = "");
|
||||
static QuicError ForNgtcp2Error(int code, std::string reason = "");
|
||||
static QuicError ForTlsAlert(int code, std::string reason = "");
|
||||
|
||||
static QuicError FromConnectionClose(ngtcp2_conn* session);
|
||||
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
#include <aliased_struct.h>
|
||||
#include <env.h>
|
||||
#include <nghttp3/nghttp3.h>
|
||||
#include <ngtcp2/ngtcp2.h>
|
||||
#include <node_errors.h>
|
||||
#include <uv.h>
|
||||
#include <v8.h>
|
||||
#include <limits>
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
@ -16,6 +19,18 @@ namespace quic {
|
|||
#define IF_QUIC_DEBUG(env) \
|
||||
if (UNLIKELY(env->enabled_debug_list()->enabled(DebugCategory::QUIC)))
|
||||
|
||||
#define DISALLOW_COPY(Name) \
|
||||
Name(const Name&) = delete; \
|
||||
Name& operator=(const Name&) = delete;
|
||||
|
||||
#define DISALLOW_MOVE(Name) \
|
||||
Name(Name&&) = delete; \
|
||||
Name& operator=(Name&&) = delete;
|
||||
|
||||
#define DISALLOW_COPY_AND_MOVE(Name) \
|
||||
DISALLOW_COPY(Name) \
|
||||
DISALLOW_MOVE(Name)
|
||||
|
||||
template <typename Opt, std::string Opt::*member>
|
||||
bool SetOption(Environment* env,
|
||||
Opt* options,
|
||||
|
@ -150,20 +165,74 @@ uint64_t GetStat(Stats* stats) {
|
|||
#define JS_METHOD(name) \
|
||||
static void name(const v8::FunctionCallbackInfo<v8::Value>& args)
|
||||
|
||||
enum class Side : uint8_t {
|
||||
CLIENT,
|
||||
SERVER,
|
||||
};
|
||||
|
||||
enum class EndpointLabel : uint8_t {
|
||||
LOCAL,
|
||||
REMOTE,
|
||||
};
|
||||
|
||||
enum class Direction : uint8_t {
|
||||
BIDIRECTIONAL,
|
||||
UNIDIRECTIONAL,
|
||||
};
|
||||
|
||||
enum class HeadersKind : uint8_t {
|
||||
HINTS,
|
||||
INITIAL,
|
||||
TRAILING,
|
||||
};
|
||||
|
||||
enum class HeadersFlags : uint8_t {
|
||||
NONE,
|
||||
TERMINAL,
|
||||
};
|
||||
|
||||
enum class StreamPriority : uint8_t {
|
||||
DEFAULT = NGHTTP3_DEFAULT_URGENCY,
|
||||
LOW = NGHTTP3_URGENCY_LOW,
|
||||
HIGH = NGHTTP3_URGENCY_HIGH,
|
||||
};
|
||||
|
||||
enum class StreamPriorityFlags : uint8_t {
|
||||
NONE,
|
||||
NON_INCREMENTAL,
|
||||
};
|
||||
|
||||
enum class PathValidationResult : uint8_t {
|
||||
SUCCESS = NGTCP2_PATH_VALIDATION_RESULT_SUCCESS,
|
||||
FAILURE = NGTCP2_PATH_VALIDATION_RESULT_FAILURE,
|
||||
ABORTED = NGTCP2_PATH_VALIDATION_RESULT_ABORTED,
|
||||
};
|
||||
|
||||
enum class DatagramStatus : uint8_t {
|
||||
ACKNOWLEDGED,
|
||||
LOST,
|
||||
};
|
||||
|
||||
constexpr uint64_t NGTCP2_APP_NOERROR = 65280;
|
||||
constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
|
||||
constexpr size_t kMaxSizeT = std::numeric_limits<size_t>::max();
|
||||
constexpr uint64_t kMaxSafeJsInteger = 9007199254740991;
|
||||
constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
|
||||
constexpr size_t kMaxVectorCount = 16;
|
||||
|
||||
using error_code = uint64_t;
|
||||
|
||||
class DebugIndentScope {
|
||||
public:
|
||||
inline DebugIndentScope() { ++indent_; }
|
||||
DebugIndentScope(const DebugIndentScope&) = delete;
|
||||
DebugIndentScope(DebugIndentScope&&) = delete;
|
||||
DebugIndentScope& operator=(const DebugIndentScope&) = delete;
|
||||
DebugIndentScope& operator=(DebugIndentScope&&) = delete;
|
||||
DISALLOW_COPY_AND_MOVE(DebugIndentScope)
|
||||
inline ~DebugIndentScope() { --indent_; }
|
||||
std::string Prefix() const {
|
||||
inline std::string Prefix() const {
|
||||
std::string res("\n");
|
||||
res.append(indent_, '\t');
|
||||
return res;
|
||||
}
|
||||
std::string Close() const {
|
||||
inline std::string Close() const {
|
||||
std::string res("\n");
|
||||
res.append(indent_ - 1, '\t');
|
||||
res += "}";
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <ngtcp2/ngtcp2.h>
|
||||
#include <node_errors.h>
|
||||
#include <node_external_reference.h>
|
||||
#include <node_process-inl.h>
|
||||
#include <node_sockaddr-inl.h>
|
||||
#include <req_wrap-inl.h>
|
||||
#include <util-inl.h>
|
||||
|
@ -71,11 +72,6 @@ namespace quic {
|
|||
V(STATELESS_RESET_COUNT, stateless_reset_count) \
|
||||
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count)
|
||||
|
||||
#define ENDPOINT_CC(V) \
|
||||
V(RENO, reno) \
|
||||
V(CUBIC, cubic) \
|
||||
V(BBR, bbr)
|
||||
|
||||
struct Endpoint::State {
|
||||
#define V(_, name, type) type name;
|
||||
ENDPOINT_STATE(V)
|
||||
|
@ -340,12 +336,11 @@ std::string Endpoint::Options::ToString() const {
|
|||
|
||||
auto ccalg = ([&] {
|
||||
switch (cc_algorithm) {
|
||||
case NGTCP2_CC_ALGO_RENO:
|
||||
return "reno";
|
||||
case NGTCP2_CC_ALGO_CUBIC:
|
||||
return "cubic";
|
||||
case NGTCP2_CC_ALGO_BBR:
|
||||
return "bbr";
|
||||
#define V(name, label) \
|
||||
case NGTCP2_CC_ALGO_##name: \
|
||||
return #label;
|
||||
ENDPOINT_CC(V)
|
||||
#undef V
|
||||
}
|
||||
return "<unknown>";
|
||||
})();
|
||||
|
@ -622,8 +617,8 @@ void Endpoint::InitPerIsolate(IsolateData* data, Local<ObjectTemplate> target) {
|
|||
|
||||
void Endpoint::InitPerContext(Realm* realm, Local<Object> target) {
|
||||
#define V(name, str) \
|
||||
NODE_DEFINE_CONSTANT(target, QUIC_CC_ALGO_##name); \
|
||||
NODE_DEFINE_STRING_CONSTANT(target, "QUIC_CC_ALGO_" #name "_STR", #str);
|
||||
NODE_DEFINE_CONSTANT(target, CC_ALGO_##name); \
|
||||
NODE_DEFINE_STRING_CONSTANT(target, "CC_ALGO_" #name "_STR", #str);
|
||||
ENDPOINT_CC(V)
|
||||
#undef V
|
||||
|
||||
|
@ -958,9 +953,32 @@ bool Endpoint::Start() {
|
|||
|
||||
void Endpoint::Listen(const Session::Options& options) {
|
||||
if (is_closed() || is_closing() || state_->listening == 1) return;
|
||||
server_options_ = options;
|
||||
DCHECK(!server_state_.has_value());
|
||||
|
||||
// We need at least one key and one cert to complete the tls handshake on the
|
||||
// server. Why not make this an error? We could but it's not strictly
|
||||
// necessary.
|
||||
if (options.tls_options.keys.empty() || options.tls_options.certs.empty()) {
|
||||
env()->EmitProcessEnvWarning();
|
||||
ProcessEmitWarning(env(),
|
||||
"The QUIC TLS options did not include a key or cert. "
|
||||
"This means the TLS handshake will fail. This is likely "
|
||||
"not what you want.");
|
||||
}
|
||||
|
||||
auto context = TLSContext::CreateServer(options.tls_options);
|
||||
if (!*context) {
|
||||
THROW_ERR_INVALID_STATE(
|
||||
env(), "Failed to create TLS context: %s", context->validation_error());
|
||||
return;
|
||||
}
|
||||
|
||||
server_state_ = {
|
||||
options,
|
||||
std::move(context),
|
||||
};
|
||||
if (Start()) {
|
||||
Debug(this, "Listening with options %s", server_options_.value());
|
||||
Debug(this, "Listening with options %s", server_state_->options);
|
||||
state_->listening = 1;
|
||||
}
|
||||
}
|
||||
|
@ -972,8 +990,7 @@ BaseObjectPtr<Session> Endpoint::Connect(
|
|||
// If starting fails, the endpoint will be destroyed.
|
||||
if (!Start()) return BaseObjectPtr<Session>();
|
||||
|
||||
Session::Config config(
|
||||
*this, options, local_address(), remote_address, session_ticket);
|
||||
Session::Config config(*this, options, local_address(), remote_address);
|
||||
|
||||
IF_QUIC_DEBUG(env()) {
|
||||
Debug(
|
||||
|
@ -985,7 +1002,21 @@ BaseObjectPtr<Session> Endpoint::Connect(
|
|||
session_ticket.has_value() ? "yes" : "no");
|
||||
}
|
||||
|
||||
auto session = Session::Create(this, config);
|
||||
auto tls_context = TLSContext::CreateClient(options.tls_options);
|
||||
if (!*tls_context) {
|
||||
THROW_ERR_INVALID_STATE(env(),
|
||||
"Failed to create TLS context: %s",
|
||||
tls_context->validation_error());
|
||||
return BaseObjectPtr<Session>();
|
||||
}
|
||||
auto session =
|
||||
Session::Create(this, config, tls_context.get(), session_ticket);
|
||||
if (!session->tls_session()) {
|
||||
THROW_ERR_INVALID_STATE(env(),
|
||||
"Failed to create TLS session: %s",
|
||||
session->tls_session().validation_error());
|
||||
return BaseObjectPtr<Session>();
|
||||
}
|
||||
if (!session) return BaseObjectPtr<Session>();
|
||||
session->set_wrapped();
|
||||
|
||||
|
@ -1093,9 +1124,19 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
|||
// as a server, then we cannot accept the initial packet.
|
||||
if (is_closed() || is_closing() || !is_listening()) return;
|
||||
|
||||
Debug(this, "Trying to create new session for %s", config.dcid);
|
||||
auto session = Session::Create(this, config);
|
||||
Debug(this, "Creating new session for %s", config.dcid);
|
||||
|
||||
std::optional<SessionTicket> no_ticket = std::nullopt;
|
||||
auto session = Session::Create(
|
||||
this, config, server_state_->tls_context.get(), no_ticket);
|
||||
if (session) {
|
||||
if (!session->tls_session()) {
|
||||
Debug(this,
|
||||
"Failed to create TLS session for %s: %s",
|
||||
config.dcid,
|
||||
session->tls_session().validation_error());
|
||||
return;
|
||||
}
|
||||
receive(session.get(),
|
||||
std::move(store),
|
||||
config.local_address,
|
||||
|
@ -1183,7 +1224,7 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
|||
// because that is the value *this* session will use as the outbound dcid.
|
||||
Session::Config config(Side::SERVER,
|
||||
*this,
|
||||
server_options_.value(),
|
||||
server_state_->options,
|
||||
version,
|
||||
local_address,
|
||||
remote_address,
|
||||
|
@ -1197,7 +1238,7 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
|||
// identifies us. config.dcid should equal scid. config.scid should *not*
|
||||
// equal dcid.
|
||||
DCHECK(config.dcid == scid);
|
||||
DCHECK(config.scid != dcid);
|
||||
DCHECK(config.scid == dcid);
|
||||
|
||||
const auto is_remote_address_validated = ([&] {
|
||||
auto info = addrLRU_.Peek(remote_address);
|
||||
|
@ -1582,8 +1623,9 @@ bool Endpoint::is_listening() const {
|
|||
void Endpoint::MemoryInfo(MemoryTracker* tracker) const {
|
||||
tracker->TrackField("options", options_);
|
||||
tracker->TrackField("udp", udp_);
|
||||
if (server_options_.has_value()) {
|
||||
tracker->TrackField("server_options", server_options_.value());
|
||||
if (server_state_.has_value()) {
|
||||
tracker->TrackField("server_options", server_state_->options);
|
||||
tracker->TrackField("server_tls_context", server_state_->tls_context);
|
||||
}
|
||||
tracker->TrackField("token_map", token_map_);
|
||||
tracker->TrackField("sessions", sessions_);
|
||||
|
|
|
@ -20,6 +20,11 @@
|
|||
namespace node {
|
||||
namespace quic {
|
||||
|
||||
#define ENDPOINT_CC(V) \
|
||||
V(RENO, reno) \
|
||||
V(CUBIC, cubic) \
|
||||
V(BBR, bbr)
|
||||
|
||||
// An Endpoint encapsulates the UDP local port binding and is responsible for
|
||||
// sending and receiving QUIC packets. A single endpoint can act as both a QUIC
|
||||
// client and server simultaneously.
|
||||
|
@ -33,9 +38,9 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
|
|||
static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10;
|
||||
static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10;
|
||||
|
||||
static constexpr auto QUIC_CC_ALGO_RENO = NGTCP2_CC_ALGO_RENO;
|
||||
static constexpr auto QUIC_CC_ALGO_CUBIC = NGTCP2_CC_ALGO_CUBIC;
|
||||
static constexpr auto QUIC_CC_ALGO_BBR = NGTCP2_CC_ALGO_BBR;
|
||||
#define V(name, _) static constexpr auto CC_ALGO_##name = NGTCP2_CC_ALGO_##name;
|
||||
ENDPOINT_CC(V)
|
||||
#undef V
|
||||
|
||||
// Endpoint configuration options
|
||||
struct Options final : public MemoryRetainer {
|
||||
|
@ -144,7 +149,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
|
|||
// which to use by default is arbitrary and we can choose whichever we'd
|
||||
// like. Additional performance profiling will be needed to determine which
|
||||
// is the better of the two for our needs.
|
||||
ngtcp2_cc_algo cc_algorithm = NGTCP2_CC_ALGO_CUBIC;
|
||||
ngtcp2_cc_algo cc_algorithm = CC_ALGO_CUBIC;
|
||||
|
||||
// By default, when the endpoint is created, it will generate a
|
||||
// reset_token_secret at random. This is a secret used in generating
|
||||
|
@ -409,8 +414,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
|
|||
const Options options_;
|
||||
UDP udp_;
|
||||
|
||||
struct ServerState {
|
||||
Session::Options options;
|
||||
std::shared_ptr<TLSContext> tls_context;
|
||||
};
|
||||
// Set if/when the endpoint is configured to listen.
|
||||
std::optional<Session::Options> server_options_{};
|
||||
std::optional<ServerState> server_state_ = std::nullopt;
|
||||
|
||||
// A Session is generally identified by one or more CIDs. We use two
|
||||
// maps for this rather than one to avoid creating a whole bunch of
|
||||
|
|
|
@ -362,8 +362,12 @@ class Http3Application final : public Session::Application {
|
|||
return static_cast<int>(ret);
|
||||
} else {
|
||||
data->remaining = data->count = static_cast<size_t>(ret);
|
||||
if (data->id > 0) {
|
||||
data->stream = session().FindStream(data->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
DCHECK_NOT_NULL(data->buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -161,7 +161,7 @@ Packet* Packet::FromFreeList(Environment* env,
|
|||
CHECK_NOT_NULL(packet);
|
||||
CHECK_EQ(env, packet->env());
|
||||
Debug(packet, "Reusing packet from freelist");
|
||||
packet->data_ = data;
|
||||
packet->data_ = std::move(data);
|
||||
packet->destination_ = destination;
|
||||
packet->listener_ = listener;
|
||||
return packet;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include "bindingdata.h"
|
||||
#include "cid.h"
|
||||
#include "data.h"
|
||||
#include "defs.h"
|
||||
#include "tokens.h"
|
||||
|
||||
namespace node {
|
||||
|
@ -76,10 +77,7 @@ class Packet final : public ReqWrap<uv_udp_send_t> {
|
|||
const SocketAddress& destination,
|
||||
std::shared_ptr<Data> data);
|
||||
|
||||
Packet(const Packet&) = delete;
|
||||
Packet(Packet&&) = delete;
|
||||
Packet& operator=(const Packet&) = delete;
|
||||
Packet& operator=(Packet&&) = delete;
|
||||
DISALLOW_COPY_AND_MOVE(Packet)
|
||||
|
||||
const SocketAddress& destination() const;
|
||||
size_t length() const;
|
||||
|
|
|
@ -97,23 +97,6 @@ bool resolve(const PreferredAddress::AddressInfo& address,
|
|||
}
|
||||
} // namespace
|
||||
|
||||
Maybe<PreferredAddress::Policy> PreferredAddress::GetPolicy(
|
||||
Environment* env, Local<Value> value) {
|
||||
CHECK(value->IsUint32());
|
||||
uint32_t val = 0;
|
||||
if (value->Uint32Value(env->context()).To(&val)) {
|
||||
switch (val) {
|
||||
case QUIC_PREFERRED_ADDRESS_USE:
|
||||
return Just(Policy::USE_PREFERRED_ADDRESS);
|
||||
case QUIC_PREFERRED_ADDRESS_IGNORE:
|
||||
return Just(Policy::IGNORE_PREFERRED_ADDRESS);
|
||||
}
|
||||
}
|
||||
THROW_ERR_INVALID_ARG_VALUE(
|
||||
env, "%d is not a valid preferred address policy", val);
|
||||
return Nothing<Policy>();
|
||||
}
|
||||
|
||||
PreferredAddress::PreferredAddress(ngtcp2_path* dest,
|
||||
const ngtcp2_preferred_addr* paddr)
|
||||
: dest_(dest), paddr_(paddr) {
|
||||
|
@ -160,14 +143,15 @@ void PreferredAddress::Set(ngtcp2_transport_params* params,
|
|||
Maybe<PreferredAddress::Policy> PreferredAddress::tryGetPolicy(
|
||||
Environment* env, Local<Value> value) {
|
||||
if (value->IsUndefined()) {
|
||||
return Just(PreferredAddress::Policy::USE_PREFERRED_ADDRESS);
|
||||
return Just(PreferredAddress::Policy::USE_PREFERRED);
|
||||
}
|
||||
if (value->IsUint32()) {
|
||||
auto val = value.As<Uint32>()->Value();
|
||||
if (val == static_cast<uint32_t>(Policy::IGNORE_PREFERRED_ADDRESS))
|
||||
return Just(Policy::IGNORE_PREFERRED_ADDRESS);
|
||||
if (val == static_cast<uint32_t>(Policy::USE_PREFERRED_ADDRESS))
|
||||
return Just(Policy::USE_PREFERRED_ADDRESS);
|
||||
switch (value.As<Uint32>()->Value()) {
|
||||
case PREFERRED_ADDRESS_IGNORE:
|
||||
return Just(Policy::IGNORE_PREFERRED);
|
||||
case PREFERRED_ADDRESS_USE:
|
||||
return Just(Policy::USE_PREFERRED);
|
||||
}
|
||||
}
|
||||
THROW_ERR_INVALID_ARG_VALUE(env, "invalid preferred address policy");
|
||||
return Nothing<PreferredAddress::Policy>();
|
||||
|
@ -175,8 +159,8 @@ Maybe<PreferredAddress::Policy> PreferredAddress::tryGetPolicy(
|
|||
|
||||
void PreferredAddress::Initialize(Environment* env,
|
||||
v8::Local<v8::Object> target) {
|
||||
NODE_DEFINE_CONSTANT(target, QUIC_PREFERRED_ADDRESS_IGNORE);
|
||||
NODE_DEFINE_CONSTANT(target, QUIC_PREFERRED_ADDRESS_USE);
|
||||
NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_IGNORE);
|
||||
NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_USE);
|
||||
NODE_DEFINE_CONSTANT(target, DEFAULT_PREFERRED_ADDRESS_POLICY);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include <node_internals.h>
|
||||
#include <v8.h>
|
||||
#include <string>
|
||||
#include "defs.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
@ -18,11 +19,11 @@ namespace quic {
|
|||
// the preferred address to be selected.
|
||||
class PreferredAddress final {
|
||||
public:
|
||||
enum class Policy {
|
||||
enum class Policy : uint32_t {
|
||||
// Ignore the server-advertised preferred address.
|
||||
IGNORE_PREFERRED_ADDRESS,
|
||||
IGNORE_PREFERRED,
|
||||
// Use the server-advertised preferred address.
|
||||
USE_PREFERRED_ADDRESS,
|
||||
USE_PREFERRED,
|
||||
};
|
||||
|
||||
static v8::Maybe<Policy> tryGetPolicy(Environment* env,
|
||||
|
@ -30,18 +31,15 @@ class PreferredAddress final {
|
|||
|
||||
// The QUIC_* constants are expected to be exported out to be used on
|
||||
// the JavaScript side of the API.
|
||||
static constexpr uint32_t QUIC_PREFERRED_ADDRESS_USE =
|
||||
static_cast<uint32_t>(Policy::USE_PREFERRED_ADDRESS);
|
||||
static constexpr uint32_t QUIC_PREFERRED_ADDRESS_IGNORE =
|
||||
static_cast<uint32_t>(Policy::IGNORE_PREFERRED_ADDRESS);
|
||||
static constexpr uint32_t DEFAULT_PREFERRED_ADDRESS_POLICY =
|
||||
static_cast<uint32_t>(Policy::USE_PREFERRED_ADDRESS);
|
||||
static constexpr auto PREFERRED_ADDRESS_USE =
|
||||
static_cast<uint32_t>(Policy::USE_PREFERRED);
|
||||
static constexpr auto PREFERRED_ADDRESS_IGNORE =
|
||||
static_cast<uint32_t>(Policy::IGNORE_PREFERRED);
|
||||
static constexpr auto DEFAULT_PREFERRED_ADDRESS_POLICY =
|
||||
static_cast<uint32_t>(Policy::USE_PREFERRED);
|
||||
|
||||
static void Initialize(Environment* env, v8::Local<v8::Object> target);
|
||||
|
||||
static v8::Maybe<Policy> GetPolicy(Environment* env,
|
||||
v8::Local<v8::Value> value);
|
||||
|
||||
struct AddressInfo final {
|
||||
char host[NI_MAXHOST];
|
||||
int family;
|
||||
|
@ -51,10 +49,7 @@ class PreferredAddress final {
|
|||
|
||||
explicit PreferredAddress(ngtcp2_path* dest,
|
||||
const ngtcp2_preferred_addr* paddr);
|
||||
PreferredAddress(const PreferredAddress&) = delete;
|
||||
PreferredAddress(PreferredAddress&&) = delete;
|
||||
PreferredAddress& operator=(const PreferredAddress&) = delete;
|
||||
PreferredAddress& operator=(PreferredAddress&&) = delete;
|
||||
DISALLOW_COPY_AND_MOVE(PreferredAddress)
|
||||
|
||||
void Use(const AddressInfo& address);
|
||||
|
||||
|
|
|
@ -90,7 +90,6 @@ namespace quic {
|
|||
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) \
|
||||
|
@ -142,11 +141,7 @@ struct Session::MaybeCloseConnectionScope final {
|
|||
silent ? "yes" : "no");
|
||||
session->connection_close_depth_++;
|
||||
}
|
||||
MaybeCloseConnectionScope(const MaybeCloseConnectionScope&) = delete;
|
||||
MaybeCloseConnectionScope(MaybeCloseConnectionScope&&) = delete;
|
||||
MaybeCloseConnectionScope& operator=(const MaybeCloseConnectionScope&) =
|
||||
delete;
|
||||
MaybeCloseConnectionScope& operator=(MaybeCloseConnectionScope&&) = delete;
|
||||
DISALLOW_COPY_AND_MOVE(MaybeCloseConnectionScope)
|
||||
~MaybeCloseConnectionScope() {
|
||||
// We only want to trigger the sending the connection close if ...
|
||||
// a) Silent is not explicitly true at this scope.
|
||||
|
@ -185,7 +180,7 @@ Session::SendPendingDataScope::~SendPendingDataScope() {
|
|||
|
||||
namespace {
|
||||
|
||||
inline const char* getEncryptionLevelName(ngtcp2_encryption_level level) {
|
||||
inline std::string to_string(ngtcp2_encryption_level level) {
|
||||
switch (level) {
|
||||
case NGTCP2_ENCRYPTION_LEVEL_1RTT:
|
||||
return "1rtt";
|
||||
|
@ -231,8 +226,7 @@ bool SetOption(Environment* env,
|
|||
const v8::Local<Object>& object,
|
||||
const v8::Local<String>& name) {
|
||||
Local<Value> value;
|
||||
PreferredAddress::Policy policy =
|
||||
PreferredAddress::Policy::USE_PREFERRED_ADDRESS;
|
||||
PreferredAddress::Policy policy = PreferredAddress::Policy::USE_PREFERRED;
|
||||
if (!object->Get(env->context(), name).ToLocal(&value) ||
|
||||
!PreferredAddress::tryGetPolicy(env, value).To(&policy)) {
|
||||
return false;
|
||||
|
@ -297,7 +291,6 @@ Session::Config::Config(Side side,
|
|||
const SocketAddress& remote_address,
|
||||
const CID& dcid,
|
||||
const CID& scid,
|
||||
std::optional<SessionTicket> session_ticket,
|
||||
const CID& ocid)
|
||||
: side(side),
|
||||
options(options),
|
||||
|
@ -306,8 +299,7 @@ Session::Config::Config(Side side,
|
|||
remote_address(remote_address),
|
||||
dcid(dcid),
|
||||
scid(scid),
|
||||
ocid(ocid),
|
||||
session_ticket(session_ticket) {
|
||||
ocid(ocid) {
|
||||
ngtcp2_settings_default(&settings);
|
||||
settings.initial_ts = uv_hrtime();
|
||||
|
||||
|
@ -343,7 +335,6 @@ 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,
|
||||
|
@ -353,7 +344,6 @@ Session::Config::Config(const Endpoint& endpoint,
|
|||
remote_address,
|
||||
CID::Factory::random().Generate(NGTCP2_MIN_INITIAL_DCIDLEN),
|
||||
options.cid_factory->Generate(),
|
||||
session_ticket,
|
||||
ocid) {}
|
||||
|
||||
void Session::Config::MemoryInfo(MemoryTracker* tracker) const {
|
||||
|
@ -364,8 +354,6 @@ void Session::Config::MemoryInfo(MemoryTracker* tracker) const {
|
|||
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,
|
||||
|
@ -410,13 +398,6 @@ std::string Session::Config::ToString() const {
|
|||
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;
|
||||
}
|
||||
|
@ -468,9 +449,9 @@ std::string Session::Options::ToString() const {
|
|||
|
||||
auto policy = ([&] {
|
||||
switch (preferred_address_strategy) {
|
||||
case PreferredAddress::Policy::USE_PREFERRED_ADDRESS:
|
||||
case PreferredAddress::Policy::USE_PREFERRED:
|
||||
return "use";
|
||||
case PreferredAddress::Policy::IGNORE_PREFERRED_ADDRESS:
|
||||
case PreferredAddress::Policy::IGNORE_PREFERRED:
|
||||
return "ignore";
|
||||
}
|
||||
return "<unknown>";
|
||||
|
@ -490,8 +471,11 @@ bool Session::HasInstance(Environment* env, Local<Value> value) {
|
|||
return GetConstructorTemplate(env)->HasInstance(value);
|
||||
}
|
||||
|
||||
BaseObjectPtr<Session> Session::Create(Endpoint* endpoint,
|
||||
const Config& config) {
|
||||
BaseObjectPtr<Session> Session::Create(
|
||||
Endpoint* endpoint,
|
||||
const Config& config,
|
||||
TLSContext* tls_context,
|
||||
const std::optional<SessionTicket>& ticket) {
|
||||
Local<Object> obj;
|
||||
if (!GetConstructorTemplate(endpoint->env())
|
||||
->InstanceTemplate()
|
||||
|
@ -500,12 +484,15 @@ BaseObjectPtr<Session> Session::Create(Endpoint* endpoint,
|
|||
return BaseObjectPtr<Session>();
|
||||
}
|
||||
|
||||
return MakeDetachedBaseObject<Session>(endpoint, obj, config);
|
||||
return MakeDetachedBaseObject<Session>(
|
||||
endpoint, obj, config, tls_context, ticket);
|
||||
}
|
||||
|
||||
Session::Session(Endpoint* endpoint,
|
||||
v8::Local<v8::Object> object,
|
||||
const Config& config)
|
||||
const Config& config,
|
||||
TLSContext* tls_context,
|
||||
const std::optional<SessionTicket>& session_ticket)
|
||||
: AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUIC_SESSION),
|
||||
stats_(env()->isolate()),
|
||||
state_(env()->isolate()),
|
||||
|
@ -515,7 +502,7 @@ Session::Session(Endpoint* endpoint,
|
|||
local_address_(config.local_address),
|
||||
remote_address_(config.remote_address),
|
||||
connection_(InitConnection()),
|
||||
tls_context_(env(), config_.side, this, config_.options.tls_options),
|
||||
tls_session_(tls_context->NewSession(this, session_ticket)),
|
||||
application_(select_application()),
|
||||
timer_(env(),
|
||||
[this, self = BaseObjectPtr<Session>(this)] { OnTimeout(); }) {
|
||||
|
@ -560,8 +547,6 @@ Session::Session(Endpoint* endpoint,
|
|||
endpoint_->AddSession(config_.scid, BaseObjectPtr<Session>(this));
|
||||
endpoint_->AssociateCID(config_.dcid, config_.scid);
|
||||
|
||||
tls_context_.Start();
|
||||
|
||||
UpdateDataStats();
|
||||
}
|
||||
|
||||
|
@ -583,6 +568,10 @@ Session::~Session() {
|
|||
DCHECK(streams_.empty());
|
||||
}
|
||||
|
||||
size_t Session::max_packet_size() const {
|
||||
return ngtcp2_conn_get_max_tx_udp_payload_size(*this);
|
||||
}
|
||||
|
||||
Session::operator ngtcp2_conn*() const {
|
||||
return connection_.get();
|
||||
}
|
||||
|
@ -595,8 +584,8 @@ Endpoint& Session::endpoint() const {
|
|||
return *endpoint_;
|
||||
}
|
||||
|
||||
TLSContext& Session::tls_context() {
|
||||
return tls_context_;
|
||||
TLSSession& Session::tls_session() {
|
||||
return *tls_session_;
|
||||
}
|
||||
|
||||
Session::Application& Session::application() {
|
||||
|
@ -873,6 +862,10 @@ void Session::Send(Packet* packet, const PathStorage& path) {
|
|||
Send(packet);
|
||||
}
|
||||
|
||||
void Session::UpdatePacketTxTime() {
|
||||
ngtcp2_conn_update_pkt_tx_time(*this, uv_hrtime());
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -1169,7 +1162,7 @@ void Session::MemoryInfo(MemoryTracker* tracker) const {
|
|||
tracker->TrackField("local_address", local_address_);
|
||||
tracker->TrackField("remote_address", remote_address_);
|
||||
tracker->TrackField("application", application_);
|
||||
tracker->TrackField("tls_context", tls_context_);
|
||||
tracker->TrackField("tls_session", tls_session_);
|
||||
tracker->TrackField("timer", timer_);
|
||||
tracker->TrackField("conn_closebuf", conn_closebuf_);
|
||||
tracker->TrackField("qlog_stream", qlog_stream_);
|
||||
|
@ -1189,7 +1182,6 @@ bool Session::wants_session_ticket() const {
|
|||
}
|
||||
|
||||
void Session::SetStreamOpenAllowed() {
|
||||
// TODO(@jasnell): Might remove this. May not be needed
|
||||
state_->stream_open_allowed = 1;
|
||||
}
|
||||
|
||||
|
@ -1334,8 +1326,8 @@ void Session::OnTimeout() {
|
|||
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()) {
|
||||
if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) {
|
||||
Debug(this, "Sending pending data after timr expiry");
|
||||
SendPendingDataScope send_scope(this);
|
||||
return;
|
||||
}
|
||||
|
@ -1358,6 +1350,7 @@ void Session::UpdateTimer() {
|
|||
}
|
||||
|
||||
auto timeout = (expiry - now) / NGTCP2_MILLISECONDS;
|
||||
Debug(this, "Updating timeout to %zu milliseconds", timeout);
|
||||
|
||||
// If timeout is zero here, it means our timer is less than a millisecond
|
||||
// off from expiry. Let's bump the timer to 1.
|
||||
|
@ -1418,7 +1411,7 @@ void Session::DatagramReceived(const uint8_t* data,
|
|||
bool Session::GenerateNewConnectionId(ngtcp2_cid* cid,
|
||||
size_t len,
|
||||
uint8_t* token) {
|
||||
CID cid_ = config_.options.cid_factory->Generate(len);
|
||||
CID cid_ = config_.options.cid_factory->GenerateInto(cid, len);
|
||||
Debug(this, "Generated new connection id %s", cid_);
|
||||
StatelessResetToken new_token(
|
||||
token, endpoint_->options().reset_token_secret, cid_);
|
||||
|
@ -1428,13 +1421,14 @@ bool Session::GenerateNewConnectionId(ngtcp2_cid* cid,
|
|||
}
|
||||
|
||||
bool Session::HandshakeCompleted() {
|
||||
if (state_->handshake_completed) return false;
|
||||
state_->handshake_completed = true;
|
||||
|
||||
Debug(this, "Session handshake completed");
|
||||
|
||||
if (state_->handshake_completed) return false;
|
||||
state_->handshake_completed = 1;
|
||||
|
||||
STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at);
|
||||
|
||||
if (!tls_context_.early_data_was_accepted())
|
||||
if (!tls_session().early_data_was_accepted())
|
||||
ngtcp2_conn_tls_early_data_rejected(*this);
|
||||
|
||||
// When in a server session, handshake completed == handshake confirmed.
|
||||
|
@ -1469,7 +1463,7 @@ void Session::HandshakeConfirmed() {
|
|||
|
||||
void Session::SelectPreferredAddress(PreferredAddress* preferredAddress) {
|
||||
if (config_.options.preferred_address_strategy ==
|
||||
PreferredAddress::Policy::IGNORE_PREFERRED_ADDRESS) {
|
||||
PreferredAddress::Policy::IGNORE_PREFERRED) {
|
||||
Debug(this, "Ignoring preferred address");
|
||||
return;
|
||||
}
|
||||
|
@ -1592,23 +1586,21 @@ void Session::EmitHandshakeComplete() {
|
|||
Undefined(isolate), // Cipher version
|
||||
Undefined(isolate), // Validation error reason
|
||||
Undefined(isolate), // Validation error code
|
||||
v8::Boolean::New(isolate, tls_context_.early_data_was_accepted())};
|
||||
v8::Boolean::New(isolate, tls_session().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]))) {
|
||||
auto& tls = tls_session();
|
||||
auto peerVerifyError = tls.VerifyPeerIdentity(env());
|
||||
if (peerVerifyError.has_value() &&
|
||||
(!peerVerifyError->reason.ToLocal(&argv[kValidationErrorReason]) ||
|
||||
!peerVerifyError->code.ToLocal(&argv[kValidationErrorCode]))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ToV8Value(env()->context(), tls_context_.servername())
|
||||
if (!ToV8Value(env()->context(), tls.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])) {
|
||||
!ToV8Value(env()->context(), tls.alpn()).ToLocal(&argv[kSelectedAlpn]) ||
|
||||
!tls.cipher_name(env()).ToLocal(&argv[kCipherName]) ||
|
||||
!tls.cipher_version(env()).ToLocal(&argv[kCipherVersion])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1665,7 +1657,10 @@ void Session::EmitSessionTicket(Store&& ticket) {
|
|||
|
||||
// If there is nothing listening for the session ticket, don't bother
|
||||
// emitting.
|
||||
if (LIKELY(state_->session_ticket == 0)) return;
|
||||
if (LIKELY(!wants_session_ticket())) {
|
||||
Debug(this, "Session ticket was discarded");
|
||||
return;
|
||||
}
|
||||
|
||||
CallbackScope<Session> cb_scope(this);
|
||||
|
||||
|
@ -1777,7 +1772,7 @@ struct Session::Impl {
|
|||
Session* session;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
|
||||
Local<Value> ret;
|
||||
if (session->tls_context().cert(env).ToLocal(&ret))
|
||||
if (session->tls_session().cert(env).ToLocal(&ret))
|
||||
args.GetReturnValue().Set(ret);
|
||||
}
|
||||
|
||||
|
@ -1787,7 +1782,7 @@ struct Session::Impl {
|
|||
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
|
||||
Local<Object> ret;
|
||||
if (!session->is_server() &&
|
||||
session->tls_context().ephemeral_key(env).ToLocal(&ret))
|
||||
session->tls_session().ephemeral_key(env).ToLocal(&ret))
|
||||
args.GetReturnValue().Set(ret);
|
||||
}
|
||||
|
||||
|
@ -1796,7 +1791,7 @@ struct Session::Impl {
|
|||
Session* session;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
|
||||
Local<Value> ret;
|
||||
if (session->tls_context().peer_cert(env).ToLocal(&ret))
|
||||
if (session->tls_session().peer_cert(env).ToLocal(&ret))
|
||||
args.GetReturnValue().Set(ret);
|
||||
}
|
||||
|
||||
|
@ -1820,7 +1815,8 @@ struct Session::Impl {
|
|||
// 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());
|
||||
Debug(session, "Initiating key update");
|
||||
args.GetReturnValue().Set(session->tls_session().InitiateKeyUpdate());
|
||||
}
|
||||
|
||||
static void DoOpenStream(const FunctionCallbackInfo<Value>& args) {
|
||||
|
@ -2022,17 +2018,17 @@ struct Session::Impl {
|
|||
void* user_data) {
|
||||
auto session = Impl::From(conn, user_data);
|
||||
if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE;
|
||||
CHECK(!session->is_server());
|
||||
|
||||
if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS;
|
||||
|
||||
Debug(session,
|
||||
"Receiving RX key for level %d for dcid %s",
|
||||
getEncryptionLevelName(level),
|
||||
to_string(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;
|
||||
return session->application().Start() ? NGTCP2_SUCCESS
|
||||
: NGTCP2_ERR_CALLBACK_FAILURE;
|
||||
}
|
||||
|
||||
static int on_receive_stateless_reset(ngtcp2_conn* conn,
|
||||
|
@ -2081,17 +2077,16 @@ struct Session::Impl {
|
|||
void* user_data) {
|
||||
auto session = Impl::From(conn, user_data);
|
||||
if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE;
|
||||
CHECK(session->is_server());
|
||||
|
||||
if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS;
|
||||
|
||||
Debug(session,
|
||||
"Receiving TX key for level %d for dcid %s",
|
||||
getEncryptionLevelName(level),
|
||||
to_string(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;
|
||||
return session->application().Start() ? NGTCP2_SUCCESS
|
||||
: NGTCP2_ERR_CALLBACK_FAILURE;
|
||||
}
|
||||
|
||||
static int on_receive_version_negotiation(ngtcp2_conn* conn,
|
||||
|
@ -2216,7 +2211,7 @@ struct Session::Impl {
|
|||
on_stream_stop_sending,
|
||||
ngtcp2_crypto_version_negotiation_cb,
|
||||
on_receive_rx_key,
|
||||
on_receive_tx_key,
|
||||
nullptr,
|
||||
on_early_data_rejected};
|
||||
|
||||
static constexpr ngtcp2_callbacks SERVER = {
|
||||
|
@ -2257,7 +2252,7 @@ struct Session::Impl {
|
|||
ngtcp2_crypto_get_path_challenge_data_cb,
|
||||
on_stream_stop_sending,
|
||||
ngtcp2_crypto_version_negotiation_cb,
|
||||
on_receive_rx_key,
|
||||
nullptr,
|
||||
on_receive_tx_key,
|
||||
on_early_data_rejected};
|
||||
};
|
||||
|
@ -2280,7 +2275,6 @@ Local<FunctionTemplate> Session::GetConstructorTemplate(Environment* env) {
|
|||
} else { \
|
||||
SetProtoMethod(isolate, tmpl, #key, Impl::name); \
|
||||
}
|
||||
|
||||
SESSION_JS_METHODS(V)
|
||||
|
||||
#undef V
|
||||
|
@ -2317,7 +2311,7 @@ Session::QuicConnectionPointer Session::InitConnection() {
|
|||
&allocator_,
|
||||
this),
|
||||
0);
|
||||
return QuicConnectionPointer(conn);
|
||||
break;
|
||||
}
|
||||
case Side::CLIENT: {
|
||||
CHECK_EQ(ngtcp2_conn_client_new(&conn,
|
||||
|
@ -2331,12 +2325,10 @@ Session::QuicConnectionPointer Session::InitConnection() {
|
|||
&allocator_,
|
||||
this),
|
||||
0);
|
||||
if (config_.session_ticket.has_value())
|
||||
tls_context_.MaybeSetEarlySession(config_.session_ticket.value());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return QuicConnectionPointer(conn);
|
||||
}
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
void Session::InitPerIsolate(IsolateData* data,
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
#include "quic/tokens.h"
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
|
||||
|
@ -108,7 +106,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
|||
// By default a client session will use the preferred address advertised by
|
||||
// the the server. This option is only relevant for client sessions.
|
||||
PreferredAddress::Policy preferred_address_strategy =
|
||||
PreferredAddress::Policy::USE_PREFERRED_ADDRESS;
|
||||
PreferredAddress::Policy::USE_PREFERRED;
|
||||
|
||||
TransportParams::Options transport_params =
|
||||
TransportParams::Options::kDefault;
|
||||
|
@ -166,10 +164,6 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
|||
CID retry_scid = CID::kInvalid;
|
||||
CID preferred_address_cid = CID::kInvalid;
|
||||
|
||||
// If this is a client session, the session_ticket is used to resume
|
||||
// a TLS session using a previously established session ticket.
|
||||
std::optional<SessionTicket> session_ticket = std::nullopt;
|
||||
|
||||
ngtcp2_settings settings = {};
|
||||
operator ngtcp2_settings*() { return &settings; }
|
||||
operator const ngtcp2_settings*() const { return &settings; }
|
||||
|
@ -182,14 +176,12 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
|||
const SocketAddress& remote_address,
|
||||
const CID& dcid,
|
||||
const CID& scid,
|
||||
std::optional<SessionTicket> session_ticket = std::nullopt,
|
||||
const CID& ocid = CID::kInvalid);
|
||||
|
||||
Config(const Endpoint& endpoint,
|
||||
const Options& options,
|
||||
const SocketAddress& local_address,
|
||||
const SocketAddress& remote_address,
|
||||
std::optional<SessionTicket> session_ticket = std::nullopt,
|
||||
const CID& ocid = CID::kInvalid);
|
||||
|
||||
void set_token(const uint8_t* token,
|
||||
|
@ -213,18 +205,23 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
|||
static void InitPerContext(Realm* env, v8::Local<v8::Object> target);
|
||||
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
|
||||
|
||||
static BaseObjectPtr<Session> Create(Endpoint* endpoint,
|
||||
const Config& config);
|
||||
static BaseObjectPtr<Session> Create(
|
||||
Endpoint* endpoint,
|
||||
const Config& config,
|
||||
TLSContext* tls_context,
|
||||
const std::optional<SessionTicket>& ticket);
|
||||
|
||||
// Really should be private but MakeDetachedBaseObject needs visibility.
|
||||
Session(Endpoint* endpoint,
|
||||
v8::Local<v8::Object> object,
|
||||
const Config& config);
|
||||
const Config& config,
|
||||
TLSContext* tls_context,
|
||||
const std::optional<SessionTicket>& ticket);
|
||||
~Session() override;
|
||||
|
||||
uint32_t version() const;
|
||||
Endpoint& endpoint() const;
|
||||
TLSContext& tls_context();
|
||||
TLSSession& tls_session();
|
||||
Application& application();
|
||||
const Config& config() const;
|
||||
const Options& options() const;
|
||||
|
@ -237,6 +234,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
|||
bool is_destroyed() const;
|
||||
bool is_server() const;
|
||||
|
||||
size_t max_packet_size() const;
|
||||
|
||||
void set_priority_supported(bool on = true);
|
||||
|
||||
std::string diagnostic_name() const override;
|
||||
|
@ -248,6 +247,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
|||
|
||||
TransportParams GetLocalTransportParams() const;
|
||||
TransportParams GetRemoteTransportParams() const;
|
||||
void UpdatePacketTxTime();
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
SET_MEMORY_INFO_NAME(Session)
|
||||
|
@ -290,10 +290,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
|||
Session* session;
|
||||
explicit SendPendingDataScope(Session* session);
|
||||
explicit SendPendingDataScope(const BaseObjectPtr<Session>& session);
|
||||
SendPendingDataScope(const SendPendingDataScope&) = delete;
|
||||
SendPendingDataScope(SendPendingDataScope&&) = delete;
|
||||
SendPendingDataScope& operator=(const SendPendingDataScope&) = delete;
|
||||
SendPendingDataScope& operator=(SendPendingDataScope&&) = delete;
|
||||
DISALLOW_COPY_AND_MOVE(SendPendingDataScope)
|
||||
~SendPendingDataScope();
|
||||
};
|
||||
|
||||
|
@ -418,7 +415,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
|||
SocketAddress local_address_;
|
||||
SocketAddress remote_address_;
|
||||
QuicConnectionPointer connection_;
|
||||
TLSContext tls_context_;
|
||||
std::unique_ptr<TLSSession> tls_session_;
|
||||
std::unique_ptr<Application> application_;
|
||||
StreamsMap streams_;
|
||||
TimerWrapHandle timer_;
|
||||
|
@ -437,6 +434,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
|||
friend struct SendPendingDataScope;
|
||||
friend class Stream;
|
||||
friend class TLSContext;
|
||||
friend class TLSSession;
|
||||
friend class TransportParams;
|
||||
};
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <uv.h>
|
||||
#include <v8.h>
|
||||
#include "data.h"
|
||||
#include "defs.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
@ -74,10 +75,7 @@ class SessionTicket::AppData final {
|
|||
};
|
||||
|
||||
explicit AppData(SSL* session);
|
||||
AppData(const AppData&) = delete;
|
||||
AppData(AppData&&) = delete;
|
||||
AppData& operator=(const AppData&) = delete;
|
||||
AppData& operator=(AppData&&) = delete;
|
||||
DISALLOW_COPY_AND_MOVE(AppData)
|
||||
|
||||
bool Set(const uv_buf_t& data);
|
||||
std::optional<const uv_buf_t> Get() const;
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
#include "tlscontext.h"
|
||||
#include <async_wrap-inl.h>
|
||||
#include <base_object-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 <ngtcp2/ngtcp2_crypto.h>
|
||||
#include <ngtcp2/ngtcp2_crypto_quictls.h>
|
||||
#include <node_process-inl.h>
|
||||
#include <node_sockaddr-inl.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <v8.h>
|
||||
|
@ -21,7 +21,6 @@
|
|||
namespace node {
|
||||
|
||||
using v8::ArrayBuffer;
|
||||
using v8::BackingStore;
|
||||
using v8::Just;
|
||||
using v8::Local;
|
||||
using v8::Maybe;
|
||||
|
@ -32,10 +31,9 @@ using v8::Value;
|
|||
|
||||
namespace quic {
|
||||
|
||||
const TLSContext::Options TLSContext::Options::kDefault = {};
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
// TODO(@jasnell): One time initialization. ngtcp2 says this is optional but
|
||||
// highly recommended to deal with some perf regression. Unfortunately doing
|
||||
// this breaks some existing tests and we need to understand the potential
|
||||
|
@ -45,202 +43,6 @@ namespace {
|
|||
// return 0;
|
||||
// }();
|
||||
|
||||
constexpr size_t kMaxAlpnLen = 255;
|
||||
|
||||
int AllowEarlyDataCallback(SSL* ssl, void* arg) {
|
||||
// Currently, we always allow early data. Later we might make
|
||||
// it configurable.
|
||||
return 1;
|
||||
}
|
||||
|
||||
int NewSessionCallback(SSL* ssl, SSL_SESSION* session) {
|
||||
// We use this event to trigger generation of the SessionTicket
|
||||
// if the user has requested to receive it.
|
||||
return TLSContext::From(ssl).OnNewSession(session);
|
||||
}
|
||||
|
||||
void KeylogCallback(const SSL* ssl, const char* line) {
|
||||
TLSContext::From(ssl).Keylog(line);
|
||||
}
|
||||
|
||||
int AlpnSelectionCallback(SSL* ssl,
|
||||
const unsigned char** out,
|
||||
unsigned char* outlen,
|
||||
const unsigned char* in,
|
||||
unsigned int inlen,
|
||||
void* arg) {
|
||||
auto& context = TLSContext::From(ssl);
|
||||
|
||||
auto requested = context.options().alpn;
|
||||
if (requested.length() > kMaxAlpnLen) return SSL_TLSEXT_ERR_NOACK;
|
||||
|
||||
// The Session supports exactly one ALPN identifier. If that does not match
|
||||
// any of the ALPN identifiers provided in the client request, then we fail
|
||||
// here. Note that this will not fail the TLS handshake, so we have to check
|
||||
// later if the ALPN matches the expected identifier or not.
|
||||
//
|
||||
// We might eventually want to support the ability to negotiate multiple
|
||||
// possible ALPN's on a single endpoint/session but for now, we only support
|
||||
// one.
|
||||
if (SSL_select_next_proto(
|
||||
const_cast<unsigned char**>(out),
|
||||
outlen,
|
||||
reinterpret_cast<const unsigned char*>(requested.c_str()),
|
||||
requested.length(),
|
||||
in,
|
||||
inlen) == OPENSSL_NPN_NO_OVERLAP) {
|
||||
return SSL_TLSEXT_ERR_NOACK;
|
||||
}
|
||||
|
||||
return SSL_TLSEXT_ERR_OK;
|
||||
}
|
||||
|
||||
BaseObjectPtr<crypto::SecureContext> InitializeSecureContext(
|
||||
Session* session,
|
||||
Side side,
|
||||
Environment* env,
|
||||
const TLSContext::Options& options) {
|
||||
auto context = crypto::SecureContext::Create(env);
|
||||
|
||||
auto& ctx = context->ctx();
|
||||
|
||||
switch (side) {
|
||||
case Side::SERVER: {
|
||||
Debug(session, "Initializing secure context for server");
|
||||
ctx.reset(SSL_CTX_new(TLS_server_method()));
|
||||
SSL_CTX_set_app_data(ctx.get(), context);
|
||||
|
||||
if (ngtcp2_crypto_quictls_configure_server_context(ctx.get()) != 0) {
|
||||
return BaseObjectPtr<crypto::SecureContext>();
|
||||
}
|
||||
|
||||
SSL_CTX_set_max_early_data(ctx.get(), UINT32_MAX);
|
||||
SSL_CTX_set_allow_early_data_cb(
|
||||
ctx.get(), AllowEarlyDataCallback, nullptr);
|
||||
SSL_CTX_set_options(ctx.get(),
|
||||
(SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) |
|
||||
SSL_OP_SINGLE_ECDH_USE |
|
||||
SSL_OP_CIPHER_SERVER_PREFERENCE |
|
||||
SSL_OP_NO_ANTI_REPLAY);
|
||||
SSL_CTX_set_mode(ctx.get(), SSL_MODE_RELEASE_BUFFERS);
|
||||
SSL_CTX_set_alpn_select_cb(ctx.get(), AlpnSelectionCallback, nullptr);
|
||||
SSL_CTX_set_session_ticket_cb(ctx.get(),
|
||||
SessionTicket::GenerateCallback,
|
||||
SessionTicket::DecryptedCallback,
|
||||
nullptr);
|
||||
|
||||
const unsigned char* sid_ctx = reinterpret_cast<const unsigned char*>(
|
||||
options.session_id_ctx.c_str());
|
||||
SSL_CTX_set_session_id_context(
|
||||
ctx.get(), sid_ctx, options.session_id_ctx.length());
|
||||
|
||||
break;
|
||||
}
|
||||
case Side::CLIENT: {
|
||||
Debug(session, "Initializing secure context for client");
|
||||
ctx.reset(SSL_CTX_new(TLS_client_method()));
|
||||
SSL_CTX_set_app_data(ctx.get(), context);
|
||||
|
||||
if (ngtcp2_crypto_quictls_configure_client_context(ctx.get()) != 0) {
|
||||
return BaseObjectPtr<crypto::SecureContext>();
|
||||
}
|
||||
|
||||
SSL_CTX_set_session_cache_mode(
|
||||
ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE);
|
||||
SSL_CTX_sess_set_new_cb(ctx.get(), NewSessionCallback);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
SSL_CTX_set_default_verify_paths(ctx.get());
|
||||
|
||||
if (options.keylog) SSL_CTX_set_keylog_callback(ctx.get(), KeylogCallback);
|
||||
|
||||
if (SSL_CTX_set_ciphersuites(ctx.get(), options.ciphers.c_str()) != 1) {
|
||||
return BaseObjectPtr<crypto::SecureContext>();
|
||||
}
|
||||
|
||||
if (SSL_CTX_set1_groups_list(ctx.get(), options.groups.c_str()) != 1) {
|
||||
return BaseObjectPtr<crypto::SecureContext>();
|
||||
}
|
||||
|
||||
// Handle CA certificates...
|
||||
|
||||
const auto addCACert = [&](uv_buf_t ca) {
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(ca.base, ca.len);
|
||||
if (!bio) return false;
|
||||
context->SetCACert(bio);
|
||||
return true;
|
||||
};
|
||||
|
||||
const auto addRootCerts = [&] {
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
context->SetRootCerts();
|
||||
};
|
||||
|
||||
if (!options.ca.empty()) {
|
||||
for (auto& ca : options.ca) {
|
||||
if (!addCACert(ca)) {
|
||||
return BaseObjectPtr<crypto::SecureContext>();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addRootCerts();
|
||||
}
|
||||
|
||||
// Handle Certs
|
||||
|
||||
const auto addCert = [&](uv_buf_t cert) {
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(cert.base, cert.len);
|
||||
if (!bio) return Just(false);
|
||||
auto ret = context->AddCert(env, std::move(bio));
|
||||
return ret;
|
||||
};
|
||||
|
||||
for (auto& cert : options.certs) {
|
||||
if (!addCert(cert).IsJust()) {
|
||||
return BaseObjectPtr<crypto::SecureContext>();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle keys
|
||||
|
||||
const auto addKey = [&](auto& key) {
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
return context->UseKey(env, key);
|
||||
// TODO(@jasnell): Maybe SSL_CTX_check_private_key also?
|
||||
};
|
||||
|
||||
for (auto& key : options.keys) {
|
||||
if (!addKey(key).IsJust()) {
|
||||
return BaseObjectPtr<crypto::SecureContext>();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle CRL
|
||||
|
||||
const auto addCRL = [&](uv_buf_t crl) {
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(crl.base, crl.len);
|
||||
if (!bio) return Just(false);
|
||||
return context->SetCRL(env, bio);
|
||||
};
|
||||
|
||||
for (auto& crl : options.crl) {
|
||||
if (!addCRL(crl).IsJust()) {
|
||||
return BaseObjectPtr<crypto::SecureContext>();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(@jasnell): Possibly handle other bits. Such a pfx, client cert engine,
|
||||
// and session timeout.
|
||||
return BaseObjectPtr<crypto::SecureContext>(context);
|
||||
}
|
||||
|
||||
void EnableTrace(Environment* env, crypto::BIOPointer* bio, SSL* ssl) {
|
||||
#if HAVE_SSL_TRACE
|
||||
static bool warn_trace_tls = true;
|
||||
|
@ -345,217 +147,270 @@ bool SetOption(Environment* env,
|
|||
}
|
||||
} // namespace
|
||||
|
||||
Side TLSContext::side() const {
|
||||
return side_;
|
||||
std::shared_ptr<TLSContext> TLSContext::CreateClient(const Options& options) {
|
||||
return std::make_shared<TLSContext>(Side::CLIENT, options);
|
||||
}
|
||||
|
||||
const TLSContext::Options& TLSContext::options() const {
|
||||
return options_;
|
||||
std::shared_ptr<TLSContext> TLSContext::CreateServer(const Options& options) {
|
||||
return std::make_shared<TLSContext>(Side::SERVER, options);
|
||||
}
|
||||
|
||||
inline const TLSContext& TLSContext::From(const SSL* ssl) {
|
||||
auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
|
||||
TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
|
||||
return *context;
|
||||
TLSContext::TLSContext(Side side, const Options& options)
|
||||
: side_(side), options_(options), ctx_(Initialize()) {}
|
||||
|
||||
TLSContext::operator SSL_CTX*() const {
|
||||
DCHECK(ctx_);
|
||||
return ctx_.get();
|
||||
}
|
||||
|
||||
inline TLSContext& TLSContext::From(SSL* ssl) {
|
||||
auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
|
||||
TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
|
||||
return *context;
|
||||
}
|
||||
int TLSContext::OnSelectAlpn(SSL* ssl,
|
||||
const unsigned char** out,
|
||||
unsigned char* outlen,
|
||||
const unsigned char* in,
|
||||
unsigned int inlen,
|
||||
void* arg) {
|
||||
static constexpr size_t kMaxAlpnLen = 255;
|
||||
auto& session = TLSSession::From(ssl);
|
||||
|
||||
TLSContext::TLSContext(Environment* env,
|
||||
Side side,
|
||||
Session* session,
|
||||
const Options& options)
|
||||
: conn_ref_({getConnection, this}),
|
||||
side_(side),
|
||||
env_(env),
|
||||
session_(session),
|
||||
options_(options),
|
||||
secure_context_(InitializeSecureContext(session, side, env, options)) {
|
||||
CHECK(secure_context_);
|
||||
ssl_.reset(SSL_new(secure_context_->ctx().get()));
|
||||
CHECK(ssl_ && SSL_is_quic(ssl_.get()));
|
||||
const auto& requested = session.context().options().alpn;
|
||||
if (requested.length() > kMaxAlpnLen) return SSL_TLSEXT_ERR_NOACK;
|
||||
|
||||
SSL_set_app_data(ssl_.get(), &conn_ref_);
|
||||
SSL_set_verify(ssl_.get(), SSL_VERIFY_NONE, crypto::VerifyCallback);
|
||||
|
||||
// Enable tracing if the `--trace-tls` command line flag is used.
|
||||
if (UNLIKELY(env->options()->trace_tls || options.enable_tls_trace))
|
||||
EnableTrace(env, &bio_trace_, ssl_.get());
|
||||
|
||||
switch (side) {
|
||||
case Side::CLIENT: {
|
||||
SSL_set_connect_state(ssl_.get());
|
||||
CHECK_EQ(0,
|
||||
SSL_set_alpn_protos(ssl_.get(),
|
||||
reinterpret_cast<const unsigned char*>(
|
||||
options_.alpn.c_str()),
|
||||
options_.alpn.length()));
|
||||
CHECK_EQ(0,
|
||||
SSL_set_tlsext_host_name(ssl_.get(), options_.hostname.c_str()));
|
||||
break;
|
||||
}
|
||||
case Side::SERVER: {
|
||||
SSL_set_accept_state(ssl_.get());
|
||||
if (options.request_peer_certificate) {
|
||||
int verify_mode = SSL_VERIFY_PEER;
|
||||
if (options.reject_unauthorized)
|
||||
verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
|
||||
SSL_set_verify(ssl_.get(), verify_mode, crypto::VerifyCallback);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
UNREACHABLE();
|
||||
// The Session supports exactly one ALPN identifier. If that does not match
|
||||
// any of the ALPN identifiers provided in the client request, then we fail
|
||||
// here. Note that this will not fail the TLS handshake, so we have to check
|
||||
// later if the ALPN matches the expected identifier or not.
|
||||
//
|
||||
// We might eventually want to support the ability to negotiate multiple
|
||||
// possible ALPN's on a single endpoint/session but for now, we only support
|
||||
// one.
|
||||
if (SSL_select_next_proto(
|
||||
const_cast<unsigned char**>(out),
|
||||
outlen,
|
||||
reinterpret_cast<const unsigned char*>(requested.data()),
|
||||
requested.length(),
|
||||
in,
|
||||
inlen) == OPENSSL_NPN_NO_OVERLAP) {
|
||||
Debug(&session.session(), "ALPN negotiation failed");
|
||||
return SSL_TLSEXT_ERR_NOACK;
|
||||
}
|
||||
|
||||
Debug(&session.session(), "ALPN negotiation succeeded");
|
||||
return SSL_TLSEXT_ERR_OK;
|
||||
}
|
||||
|
||||
void TLSContext::Start() {
|
||||
Debug(session_, "Crypto context is starting");
|
||||
ngtcp2_conn_set_tls_native_handle(*session_, ssl_.get());
|
||||
int TLSContext::OnNewSession(SSL* ssl, SSL_SESSION* sess) {
|
||||
auto& session = TLSSession::From(ssl).session();
|
||||
|
||||
TransportParams tp(ngtcp2_conn_get_local_transport_params(*session_));
|
||||
Store store = tp.Encode(env_);
|
||||
if (store && store.length() > 0) {
|
||||
ngtcp2_vec vec = store;
|
||||
SSL_set_quic_transport_params(ssl_.get(), vec.base, vec.len);
|
||||
}
|
||||
}
|
||||
// If there is nothing listening for the session ticket, do not bother.
|
||||
if (session.wants_session_ticket()) {
|
||||
Debug(&session, "Preparing TLS session resumption ticket");
|
||||
|
||||
void TLSContext::Keylog(const char* line) const {
|
||||
session_->EmitKeylog(line);
|
||||
}
|
||||
// Pre-fight to see how much space we need to allocate for the session
|
||||
// ticket.
|
||||
size_t size = i2d_SSL_SESSION(sess, nullptr);
|
||||
|
||||
int TLSContext::OnNewSession(SSL_SESSION* session) {
|
||||
Debug(session_, "Crypto context received new crypto session");
|
||||
// Used to generate and emit a SessionTicket for TLS session resumption.
|
||||
|
||||
// If there is nothing listening for the session ticket, don't both emitting.
|
||||
if (!session_->wants_session_ticket()) return 0;
|
||||
|
||||
// Pre-fight to see how much space we need to allocate for the session ticket.
|
||||
size_t size = i2d_SSL_SESSION(session, nullptr);
|
||||
|
||||
if (size > 0 && size < crypto::SecureContext::kMaxSessionSize) {
|
||||
// Generate the actual ticket. If this fails, we'll simply carry on without
|
||||
// emitting the ticket.
|
||||
std::shared_ptr<BackingStore> ticket =
|
||||
ArrayBuffer::NewBackingStore(env_->isolate(), size);
|
||||
unsigned char* data = reinterpret_cast<unsigned char*>(ticket->Data());
|
||||
if (i2d_SSL_SESSION(session, &data) <= 0) return 0;
|
||||
session_->EmitSessionTicket(Store(std::move(ticket), size));
|
||||
}
|
||||
// If size == 0, there's no session ticket data to emit. Let's ignore it
|
||||
// If size is 0 or the size is greater than our max, let's ignore it
|
||||
// and continue without emitting the sessionticket event.
|
||||
if (size > 0 && size <= crypto::SecureContext::kMaxSessionSize) {
|
||||
auto ticket =
|
||||
ArrayBuffer::NewBackingStore(session.env()->isolate(), size);
|
||||
auto data = reinterpret_cast<unsigned char*>(ticket->Data());
|
||||
if (i2d_SSL_SESSION(sess, &data) > 0) {
|
||||
session.EmitSessionTicket(Store(std::move(ticket), size));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool TLSContext::InitiateKeyUpdate() {
|
||||
Debug(session_, "Crypto context initiating key update");
|
||||
if (session_->is_destroyed() || in_key_update_) return false;
|
||||
auto leave = OnScopeLeave([this] { in_key_update_ = false; });
|
||||
in_key_update_ = true;
|
||||
|
||||
return ngtcp2_conn_initiate_key_update(*session_, uv_hrtime()) == 0;
|
||||
void TLSContext::OnKeylog(const SSL* ssl, const char* line) {
|
||||
TLSSession::From(ssl).session().EmitKeylog(line);
|
||||
}
|
||||
|
||||
int TLSContext::VerifyPeerIdentity() {
|
||||
Debug(session_, "Crypto context verifying peer identity");
|
||||
return crypto::VerifyPeerCertificate(ssl_);
|
||||
int TLSContext::OnVerifyClientCertificate(int preverify_ok,
|
||||
X509_STORE_CTX* ctx) {
|
||||
// TODO(@jasnell): Implement the logic to verify the client certificate
|
||||
return 1;
|
||||
}
|
||||
|
||||
void TLSContext::MaybeSetEarlySession(const SessionTicket& sessionTicket) {
|
||||
Debug(session_, "Crypto context setting early session");
|
||||
uv_buf_t buf = sessionTicket.ticket();
|
||||
crypto::SSLSessionPointer ticket = crypto::GetTLSSession(
|
||||
reinterpret_cast<unsigned char*>(buf.base), buf.len);
|
||||
std::unique_ptr<TLSSession> TLSContext::NewSession(
|
||||
Session* session, const std::optional<SessionTicket>& maybeSessionTicket) {
|
||||
// Passing a session ticket only makes sense with a client session.
|
||||
CHECK_IMPLIES(session->is_server(), !maybeSessionTicket.has_value());
|
||||
return std::make_unique<TLSSession>(
|
||||
session, shared_from_this(), maybeSessionTicket);
|
||||
}
|
||||
|
||||
// Silently ignore invalid TLS session
|
||||
if (!ticket || !SSL_SESSION_get_max_early_data(ticket.get())) return;
|
||||
crypto::SSLCtxPointer TLSContext::Initialize() {
|
||||
crypto::SSLCtxPointer ctx;
|
||||
switch (side_) {
|
||||
case Side::SERVER: {
|
||||
static constexpr unsigned char kSidCtx[] = "Node.js QUIC Server";
|
||||
ctx.reset(SSL_CTX_new(TLS_server_method()));
|
||||
CHECK_EQ(ngtcp2_crypto_quictls_configure_server_context(ctx.get()), 0);
|
||||
CHECK_EQ(SSL_CTX_set_max_early_data(ctx.get(), UINT32_MAX), 1);
|
||||
SSL_CTX_set_options(ctx.get(),
|
||||
(SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) |
|
||||
SSL_OP_SINGLE_ECDH_USE |
|
||||
SSL_OP_CIPHER_SERVER_PREFERENCE |
|
||||
SSL_OP_NO_ANTI_REPLAY);
|
||||
SSL_CTX_set_mode(ctx.get(), SSL_MODE_RELEASE_BUFFERS);
|
||||
SSL_CTX_set_alpn_select_cb(ctx.get(), OnSelectAlpn, this);
|
||||
CHECK_EQ(SSL_CTX_set_session_id_context(
|
||||
ctx.get(), kSidCtx, sizeof(kSidCtx) - 1),
|
||||
1);
|
||||
|
||||
// The early data will just be ignored if it's invalid.
|
||||
if (!crypto::SetTLSSession(ssl_, ticket)) return;
|
||||
if (options_.verify_client) {
|
||||
SSL_CTX_set_verify(ctx.get(),
|
||||
SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE |
|
||||
SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
|
||||
OnVerifyClientCertificate);
|
||||
}
|
||||
|
||||
ngtcp2_vec rtp = sessionTicket.transport_params();
|
||||
// Decode and attempt to set the early transport parameters configured
|
||||
// for the early session. If non-zero is returned, decoding or setting
|
||||
// failed, in which case we just ignore it.
|
||||
if (ngtcp2_conn_decode_and_set_0rtt_transport_params(
|
||||
*session_, rtp.base, rtp.len) != 0)
|
||||
return;
|
||||
CHECK_EQ(SSL_CTX_set_session_ticket_cb(ctx.get(),
|
||||
SessionTicket::GenerateCallback,
|
||||
SessionTicket::DecryptedCallback,
|
||||
nullptr),
|
||||
1);
|
||||
break;
|
||||
}
|
||||
case Side::CLIENT: {
|
||||
ctx_.reset(SSL_CTX_new(TLS_client_method()));
|
||||
CHECK_EQ(ngtcp2_crypto_quictls_configure_client_context(ctx.get()), 0);
|
||||
|
||||
session_->SetStreamOpenAllowed();
|
||||
SSL_CTX_set_session_cache_mode(
|
||||
ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL);
|
||||
SSL_CTX_sess_set_new_cb(ctx.get(), OnNewSession);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SSL_CTX_set_default_verify_paths(ctx.get());
|
||||
SSL_CTX_set_keylog_callback(ctx.get(), OnKeylog);
|
||||
|
||||
if (SSL_CTX_set_ciphersuites(ctx.get(), options_.ciphers.c_str()) != 1) {
|
||||
validation_error_ = "Invalid cipher suite";
|
||||
return crypto::SSLCtxPointer();
|
||||
}
|
||||
|
||||
if (SSL_CTX_set1_groups_list(ctx.get(), options_.groups.c_str()) != 1) {
|
||||
validation_error_ = "Invalid cipher groups";
|
||||
return crypto::SSLCtxPointer();
|
||||
}
|
||||
|
||||
{
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
if (options_.ca.empty()) {
|
||||
auto store = crypto::GetOrCreateRootCertStore();
|
||||
X509_STORE_up_ref(store);
|
||||
SSL_CTX_set_cert_store(ctx.get(), store);
|
||||
} else {
|
||||
for (const auto& ca : options_.ca) {
|
||||
uv_buf_t buf = ca;
|
||||
if (buf.len == 0) {
|
||||
auto store = crypto::GetOrCreateRootCertStore();
|
||||
X509_STORE_up_ref(store);
|
||||
SSL_CTX_set_cert_store(ctx.get(), store);
|
||||
} else {
|
||||
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
|
||||
CHECK(bio);
|
||||
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get());
|
||||
while (crypto::X509Pointer x509 = crypto::X509Pointer(
|
||||
PEM_read_bio_X509_AUX(bio.get(),
|
||||
nullptr,
|
||||
crypto::NoPasswordCallback,
|
||||
nullptr))) {
|
||||
if (cert_store == crypto::GetOrCreateRootCertStore()) {
|
||||
cert_store = crypto::NewRootCertStore();
|
||||
SSL_CTX_set_cert_store(ctx.get(), cert_store);
|
||||
}
|
||||
CHECK_EQ(1, X509_STORE_add_cert(cert_store, x509.get()));
|
||||
CHECK_EQ(1, SSL_CTX_add_client_CA(ctx.get(), x509.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
for (const auto& cert : options_.certs) {
|
||||
uv_buf_t buf = cert;
|
||||
if (buf.len > 0) {
|
||||
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
|
||||
CHECK(bio);
|
||||
cert_.reset();
|
||||
issuer_.reset();
|
||||
if (crypto::SSL_CTX_use_certificate_chain(
|
||||
ctx.get(), std::move(bio), &cert_, &issuer_) == 0) {
|
||||
validation_error_ = "Invalid certificate";
|
||||
return crypto::SSLCtxPointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
for (const auto& key : options_.keys) {
|
||||
if (key->GetKeyType() != crypto::KeyType::kKeyTypePrivate) {
|
||||
validation_error_ = "Invalid key";
|
||||
return crypto::SSLCtxPointer();
|
||||
}
|
||||
if (!SSL_CTX_use_PrivateKey(ctx.get(), key->GetAsymmetricKey().get())) {
|
||||
validation_error_ = "Invalid key";
|
||||
return crypto::SSLCtxPointer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
for (const auto& crl : options_.crl) {
|
||||
uv_buf_t buf = crl;
|
||||
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
|
||||
DeleteFnPtr<X509_CRL, X509_CRL_free> crlptr(PEM_read_bio_X509_CRL(
|
||||
bio.get(), nullptr, crypto::NoPasswordCallback, nullptr));
|
||||
|
||||
if (!crlptr) {
|
||||
validation_error_ = "Invalid CRL";
|
||||
return crypto::SSLCtxPointer();
|
||||
}
|
||||
|
||||
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get());
|
||||
if (cert_store == crypto::GetOrCreateRootCertStore()) {
|
||||
cert_store = crypto::NewRootCertStore();
|
||||
SSL_CTX_set_cert_store(ctx.get(), cert_store);
|
||||
}
|
||||
|
||||
CHECK_EQ(1, X509_STORE_add_crl(cert_store, crlptr.get()));
|
||||
CHECK_EQ(
|
||||
1,
|
||||
X509_STORE_set_flags(
|
||||
cert_store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
crypto::ClearErrorOnReturn clear_error_on_return;
|
||||
if (options_.verify_private_key &&
|
||||
SSL_CTX_check_private_key(ctx.get()) != 1) {
|
||||
validation_error_ = "Invalid private key";
|
||||
return crypto::SSLCtxPointer();
|
||||
}
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void TLSContext::MemoryInfo(MemoryTracker* tracker) const {
|
||||
tracker->TrackField("options", options_);
|
||||
tracker->TrackField("secure_context", secure_context_);
|
||||
}
|
||||
|
||||
MaybeLocal<Object> TLSContext::cert(Environment* env) const {
|
||||
return crypto::X509Certificate::GetCert(env, ssl_);
|
||||
}
|
||||
|
||||
MaybeLocal<Object> TLSContext::peer_cert(Environment* env) const {
|
||||
crypto::X509Certificate::GetPeerCertificateFlag flag =
|
||||
side_ == Side::SERVER
|
||||
? crypto::X509Certificate::GetPeerCertificateFlag::SERVER
|
||||
: crypto::X509Certificate::GetPeerCertificateFlag::NONE;
|
||||
return crypto::X509Certificate::GetPeerCert(env, ssl_, flag);
|
||||
}
|
||||
|
||||
MaybeLocal<Value> TLSContext::cipher_name(Environment* env) const {
|
||||
return crypto::GetCurrentCipherName(env, ssl_);
|
||||
}
|
||||
|
||||
MaybeLocal<Value> TLSContext::cipher_version(Environment* env) const {
|
||||
return crypto::GetCurrentCipherVersion(env, ssl_);
|
||||
}
|
||||
|
||||
MaybeLocal<Object> TLSContext::ephemeral_key(Environment* env) const {
|
||||
return crypto::GetEphemeralKey(env, ssl_);
|
||||
}
|
||||
|
||||
const std::string_view TLSContext::servername() const {
|
||||
const char* servername = crypto::GetServerName(ssl_.get());
|
||||
return servername != nullptr ? std::string_view(servername)
|
||||
: std::string_view();
|
||||
}
|
||||
|
||||
const std::string_view TLSContext::alpn() const {
|
||||
const unsigned char* alpn_buf = nullptr;
|
||||
unsigned int alpnlen;
|
||||
SSL_get0_alpn_selected(ssl_.get(), &alpn_buf, &alpnlen);
|
||||
return alpnlen ? std::string_view(reinterpret_cast<const char*>(alpn_buf),
|
||||
alpnlen)
|
||||
: std::string_view();
|
||||
}
|
||||
|
||||
bool TLSContext::early_data_was_accepted() const {
|
||||
return (early_data_ &&
|
||||
SSL_get_early_data_status(ssl_.get()) == SSL_EARLY_DATA_ACCEPTED);
|
||||
}
|
||||
|
||||
void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const {
|
||||
tracker->TrackField("keys", keys);
|
||||
tracker->TrackField("certs", certs);
|
||||
tracker->TrackField("ca", ca);
|
||||
tracker->TrackField("crl", crl);
|
||||
}
|
||||
|
||||
ngtcp2_conn* TLSContext::getConnection(ngtcp2_crypto_conn_ref* ref) {
|
||||
TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
|
||||
return *context->session_;
|
||||
}
|
||||
|
||||
Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
|
||||
Local<Value> value) {
|
||||
if (value.IsEmpty()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
|
||||
return Nothing<Options>();
|
||||
}
|
||||
|
||||
|
@ -563,19 +418,11 @@ Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
|
|||
auto& state = BindingData::Get(env);
|
||||
|
||||
if (value->IsUndefined()) {
|
||||
// We need at least one key and one cert to complete the tls handshake.
|
||||
// Why not make this an error? We could but it's not strictly necessary.
|
||||
env->EmitProcessEnvWarning();
|
||||
ProcessEmitWarning(
|
||||
env,
|
||||
"The default QUIC TLS options are being used. "
|
||||
"This means there is no key or certificate configured and the "
|
||||
"TLS handshake will fail. This is likely not what you want.");
|
||||
return Just<Options>(options);
|
||||
return Just(TLSContext::Options::kDefault);
|
||||
}
|
||||
|
||||
if (!value->IsObject()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
|
||||
THROW_ERR_INVALID_ARG_TYPE(env, "tls options must be an object");
|
||||
return Nothing<Options>();
|
||||
}
|
||||
|
||||
|
@ -589,26 +436,15 @@ Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
|
|||
SetOption<TLSContext::Options, &TLSContext::Options::name>( \
|
||||
env, &options, params, state.name##_string())
|
||||
|
||||
if (!SET(keylog) || !SET(reject_unauthorized) || !SET(enable_tls_trace) ||
|
||||
!SET(request_peer_certificate) || !SET(verify_hostname_identity) ||
|
||||
!SET(alpn) || !SET(hostname) || !SET(session_id_ctx) || !SET(ciphers) ||
|
||||
!SET(groups) ||
|
||||
if (!SET(verify_client) || !SET(enable_tls_trace) || !SET(alpn) ||
|
||||
!SET(sni) || !SET(ciphers) || !SET(groups) || !SET(verify_private_key) ||
|
||||
!SET(keylog) ||
|
||||
!SET_VECTOR(std::shared_ptr<crypto::KeyObjectData>, keys) ||
|
||||
!SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) ||
|
||||
!SET_VECTOR(Store, crl)) {
|
||||
return Nothing<Options>();
|
||||
}
|
||||
|
||||
// We need at least one key and one cert to complete the tls handshake.
|
||||
// Why not make this an error? We could but it's not strictly necessary.
|
||||
if (options.keys.empty() || options.certs.empty()) {
|
||||
env->EmitProcessEnvWarning();
|
||||
ProcessEmitWarning(env,
|
||||
"The QUIC TLS options did not include a key or cert. "
|
||||
"This means the TLS handshake will fail. This is likely "
|
||||
"not what you want.");
|
||||
}
|
||||
|
||||
return Just<Options>(options);
|
||||
}
|
||||
|
||||
|
@ -617,18 +453,15 @@ std::string TLSContext::Options::ToString() const {
|
|||
auto prefix = indent.Prefix();
|
||||
std::string res("{");
|
||||
res += prefix + "alpn: " + alpn;
|
||||
res += prefix + "hostname: " + hostname;
|
||||
res += prefix + "sni: " + sni;
|
||||
res +=
|
||||
prefix + "keylog: " + (keylog ? std::string("yes") : std::string("no"));
|
||||
res += prefix + "reject_unauthorized: " +
|
||||
(reject_unauthorized ? std::string("yes") : std::string("no"));
|
||||
res += prefix + "verify client: " +
|
||||
(verify_client ? std::string("yes") : std::string("no"));
|
||||
res += prefix + "enable_tls_trace: " +
|
||||
(enable_tls_trace ? std::string("yes") : std::string("no"));
|
||||
res += prefix + "request_peer_certificate: " +
|
||||
(request_peer_certificate ? std::string("yes") : std::string("no"));
|
||||
res += prefix + "verify_hostname_identity: " +
|
||||
(verify_hostname_identity ? std::string("yes") : std::string("no"));
|
||||
res += prefix + "session_id_ctx: " + session_id_ctx;
|
||||
res += prefix + "verify private key: " +
|
||||
(verify_private_key ? std::string("yes") : std::string("no"));
|
||||
res += prefix + "ciphers: " + ciphers;
|
||||
res += prefix + "groups: " + groups;
|
||||
res += prefix + "keys: " + std::to_string(keys.size());
|
||||
|
@ -639,6 +472,179 @@ std::string TLSContext::Options::ToString() const {
|
|||
return res;
|
||||
}
|
||||
|
||||
void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const {
|
||||
tracker->TrackField("keys", keys);
|
||||
tracker->TrackField("certs", certs);
|
||||
tracker->TrackField("ca", ca);
|
||||
tracker->TrackField("crl", crl);
|
||||
}
|
||||
|
||||
const TLSContext::Options TLSContext::Options::kDefault = {};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
const TLSSession& TLSSession::From(const SSL* ssl) {
|
||||
auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
|
||||
CHECK_NOT_NULL(ref);
|
||||
return *static_cast<TLSSession*>(ref->user_data);
|
||||
}
|
||||
|
||||
TLSSession::TLSSession(Session* session,
|
||||
std::shared_ptr<TLSContext> context,
|
||||
const std::optional<SessionTicket>& maybeSessionTicket)
|
||||
: ref_({connection, this}),
|
||||
context_(std::move(context)),
|
||||
session_(session),
|
||||
ssl_(Initialize(maybeSessionTicket)) {
|
||||
Debug(session_, "Created new TLS session for %s", session->config().dcid);
|
||||
}
|
||||
|
||||
TLSSession::operator SSL*() const {
|
||||
CHECK(ssl_);
|
||||
return ssl_.get();
|
||||
}
|
||||
|
||||
bool TLSSession::early_data_was_accepted() const {
|
||||
CHECK_NE(ngtcp2_conn_get_handshake_completed(*session_), 0);
|
||||
return SSL_get_early_data_status(*this) == SSL_EARLY_DATA_ACCEPTED;
|
||||
}
|
||||
|
||||
crypto::SSLPointer TLSSession::Initialize(
|
||||
const std::optional<SessionTicket>& maybeSessionTicket) {
|
||||
auto& ctx = context();
|
||||
auto& options = ctx.options();
|
||||
crypto::SSLPointer ssl(SSL_new(ctx));
|
||||
SSL_set_app_data(ssl.get(), &ref_);
|
||||
ngtcp2_conn_set_tls_native_handle(*session_, ssl.get());
|
||||
|
||||
// Enable tracing if the `--trace-tls` command line flag is used.
|
||||
if (UNLIKELY(session_->env()->options()->trace_tls ||
|
||||
options.enable_tls_trace)) {
|
||||
EnableTrace(session_->env(), &bio_trace_, *this);
|
||||
}
|
||||
|
||||
switch (ctx.side()) {
|
||||
case Side::SERVER: {
|
||||
SSL_set_accept_state(ssl.get());
|
||||
SSL_set_quic_early_data_enabled(ssl.get(), 1);
|
||||
break;
|
||||
}
|
||||
case Side::CLIENT: {
|
||||
SSL_set_connect_state(ssl.get());
|
||||
if (SSL_set_alpn_protos(
|
||||
ssl.get(),
|
||||
reinterpret_cast<const unsigned char*>(options.alpn.data()),
|
||||
options.alpn.size()) != 0) {
|
||||
validation_error_ = "Invalid ALPN";
|
||||
return crypto::SSLPointer();
|
||||
}
|
||||
|
||||
if (!options.sni.empty()) {
|
||||
SSL_set_tlsext_host_name(ssl.get(), options.sni.data());
|
||||
} else {
|
||||
SSL_set_tlsext_host_name(ssl.get(), "localhost");
|
||||
}
|
||||
|
||||
if (maybeSessionTicket.has_value()) {
|
||||
auto sessionTicket = maybeSessionTicket.value();
|
||||
uv_buf_t buf = sessionTicket.ticket();
|
||||
crypto::SSLSessionPointer ticket = crypto::GetTLSSession(
|
||||
reinterpret_cast<unsigned char*>(buf.base), buf.len);
|
||||
|
||||
// The early data will just be ignored if it's invalid.
|
||||
if (crypto::SetTLSSession(ssl, ticket) &&
|
||||
SSL_SESSION_get_max_early_data(ticket.get()) != 0) {
|
||||
ngtcp2_vec rtp = sessionTicket.transport_params();
|
||||
if (ngtcp2_conn_decode_and_set_0rtt_transport_params(
|
||||
*session_, rtp.base, rtp.len) == 0) {
|
||||
SSL_set_quic_early_data_enabled(ssl.get(), 1);
|
||||
session_->SetStreamOpenAllowed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TransportParams tp(ngtcp2_conn_get_local_transport_params(*session_));
|
||||
Store store = tp.Encode(session_->env());
|
||||
if (store && store.length() > 0) {
|
||||
ngtcp2_vec vec = store;
|
||||
SSL_set_quic_transport_params(ssl.get(), vec.base, vec.len);
|
||||
}
|
||||
|
||||
return ssl;
|
||||
}
|
||||
|
||||
std::optional<TLSSession::PeerIdentityValidationError>
|
||||
TLSSession::VerifyPeerIdentity(Environment* env) {
|
||||
int err = crypto::VerifyPeerCertificate(ssl_);
|
||||
if (err == X509_V_OK) return std::nullopt;
|
||||
Local<Value> reason;
|
||||
Local<Value> code;
|
||||
if (!crypto::GetValidationErrorReason(env, err).ToLocal(&reason) ||
|
||||
!crypto::GetValidationErrorCode(env, err).ToLocal(&code)) {
|
||||
// Getting the validation error details failed. We'll return a value but
|
||||
// the fields will be empty.
|
||||
return PeerIdentityValidationError{};
|
||||
}
|
||||
return PeerIdentityValidationError{reason, code};
|
||||
}
|
||||
|
||||
MaybeLocal<Object> TLSSession::cert(Environment* env) const {
|
||||
return crypto::X509Certificate::GetCert(env, ssl_);
|
||||
}
|
||||
|
||||
MaybeLocal<Object> TLSSession::peer_cert(Environment* env) const {
|
||||
crypto::X509Certificate::GetPeerCertificateFlag flag =
|
||||
context_->side() == Side::SERVER
|
||||
? crypto::X509Certificate::GetPeerCertificateFlag::SERVER
|
||||
: crypto::X509Certificate::GetPeerCertificateFlag::NONE;
|
||||
return crypto::X509Certificate::GetPeerCert(env, ssl_, flag);
|
||||
}
|
||||
|
||||
MaybeLocal<Object> TLSSession::ephemeral_key(Environment* env) const {
|
||||
return crypto::GetEphemeralKey(env, ssl_);
|
||||
}
|
||||
|
||||
MaybeLocal<Value> TLSSession::cipher_name(Environment* env) const {
|
||||
return crypto::GetCurrentCipherName(env, ssl_);
|
||||
}
|
||||
|
||||
MaybeLocal<Value> TLSSession::cipher_version(Environment* env) const {
|
||||
return crypto::GetCurrentCipherVersion(env, ssl_);
|
||||
}
|
||||
|
||||
const std::string_view TLSSession::servername() const {
|
||||
const char* servername = crypto::GetServerName(ssl_.get());
|
||||
return servername != nullptr ? std::string_view(servername)
|
||||
: std::string_view();
|
||||
}
|
||||
|
||||
const std::string_view TLSSession::alpn() const {
|
||||
const unsigned char* alpn_buf = nullptr;
|
||||
unsigned int alpnlen;
|
||||
SSL_get0_alpn_selected(ssl_.get(), &alpn_buf, &alpnlen);
|
||||
return alpnlen ? std::string_view(reinterpret_cast<const char*>(alpn_buf),
|
||||
alpnlen)
|
||||
: std::string_view();
|
||||
}
|
||||
|
||||
bool TLSSession::InitiateKeyUpdate() {
|
||||
if (session_->is_destroyed() || in_key_update_) return false;
|
||||
auto leave = OnScopeLeave([this] { in_key_update_ = false; });
|
||||
in_key_update_ = true;
|
||||
|
||||
Debug(session_, "Initiating key update");
|
||||
return ngtcp2_conn_initiate_key_update(*session_, uv_hrtime()) == 0;
|
||||
}
|
||||
|
||||
ngtcp2_conn* TLSSession::connection(ngtcp2_crypto_conn_ref* ref) {
|
||||
CHECK_NOT_NULL(ref->user_data);
|
||||
return static_cast<TLSSession*>(ref->user_data)->session();
|
||||
}
|
||||
|
||||
} // namespace quic
|
||||
} // namespace node
|
||||
|
||||
|
|
|
@ -10,92 +10,162 @@
|
|||
#include <ngtcp2/ngtcp2_crypto.h>
|
||||
#include "bindingdata.h"
|
||||
#include "data.h"
|
||||
#include "defs.h"
|
||||
#include "sessionticket.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
||||
class Session;
|
||||
class TLSContext;
|
||||
|
||||
// Every QUIC Session has exactly one TLSContext that maintains the state
|
||||
// of the TLS handshake and negotiated cipher keys after the handshake has
|
||||
// been completed. It is separated out from the main Session class only as a
|
||||
// Every QUIC Session has exactly one TLSSession that maintains the state
|
||||
// of the TLS handshake and negotiated keys after the handshake has been
|
||||
// completed. It is separated out from the main Session class only as a
|
||||
// convenience to help make the code more maintainable and understandable.
|
||||
class TLSContext final : public MemoryRetainer {
|
||||
// A TLSSession is created from a TLSContext and maintains a reference to
|
||||
// the context.
|
||||
class TLSSession final : public MemoryRetainer {
|
||||
public:
|
||||
enum class EncryptionLevel {
|
||||
INITIAL = NGTCP2_ENCRYPTION_LEVEL_INITIAL,
|
||||
HANDSHAKE = NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE,
|
||||
ONERTT = NGTCP2_ENCRYPTION_LEVEL_1RTT,
|
||||
ZERORTT = NGTCP2_ENCRYPTION_LEVEL_0RTT,
|
||||
static const TLSSession& From(const SSL* ssl);
|
||||
|
||||
// The constructor is public in order to satisify the call to std::make_unique
|
||||
// in TLSContext::NewSession. It should not be called directly.
|
||||
TLSSession(Session* session,
|
||||
std::shared_ptr<TLSContext> context,
|
||||
const std::optional<SessionTicket>& maybeSessionTicket);
|
||||
DISALLOW_COPY_AND_MOVE(TLSSession)
|
||||
|
||||
inline operator bool() const { return ssl_ != nullptr; }
|
||||
inline Session& session() const { return *session_; }
|
||||
inline TLSContext& context() const { return *context_; }
|
||||
|
||||
// Returns true if the handshake has been completed and early data was
|
||||
// accepted by the TLS session. This will assert if the handshake has
|
||||
// not been completed.
|
||||
bool early_data_was_accepted() const;
|
||||
|
||||
v8::MaybeLocal<v8::Object> cert(Environment* env) const;
|
||||
v8::MaybeLocal<v8::Object> peer_cert(Environment* env) const;
|
||||
v8::MaybeLocal<v8::Object> ephemeral_key(Environment* env) const;
|
||||
v8::MaybeLocal<v8::Value> cipher_name(Environment* env) const;
|
||||
v8::MaybeLocal<v8::Value> cipher_version(Environment* env) const;
|
||||
|
||||
// The SNI (server name) negotiated for the session
|
||||
const std::string_view servername() const;
|
||||
|
||||
// The ALPN (protocol name) negotiated for the session
|
||||
const std::string_view alpn() const;
|
||||
|
||||
// Triggers key update to begin. This will fail and return false if either a
|
||||
// previous key update is in progress or if the initial handshake has not yet
|
||||
// been confirmed.
|
||||
bool InitiateKeyUpdate();
|
||||
|
||||
struct PeerIdentityValidationError {
|
||||
v8::MaybeLocal<v8::Value> reason;
|
||||
v8::MaybeLocal<v8::Value> code;
|
||||
};
|
||||
|
||||
// Checks the peer identity against the configured CA and CRL. If the peer
|
||||
// certificate is valid, std::nullopt is returned. Otherwise a
|
||||
// PeerIdentityValidationError is returned with the reason and code for the
|
||||
// failure.
|
||||
std::optional<PeerIdentityValidationError> VerifyPeerIdentity(
|
||||
Environment* env);
|
||||
|
||||
inline const std::string_view validation_error() const {
|
||||
return validation_error_;
|
||||
}
|
||||
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(TLSSession)
|
||||
SET_SELF_SIZE(TLSSession)
|
||||
|
||||
private:
|
||||
operator SSL*() const;
|
||||
crypto::SSLPointer Initialize(
|
||||
const std::optional<SessionTicket>& maybeSessionTicket);
|
||||
|
||||
static ngtcp2_conn* connection(ngtcp2_crypto_conn_ref* ref);
|
||||
|
||||
ngtcp2_crypto_conn_ref ref_;
|
||||
std::shared_ptr<TLSContext> context_;
|
||||
Session* session_;
|
||||
crypto::SSLPointer ssl_;
|
||||
crypto::BIOPointer bio_trace_;
|
||||
std::string validation_error_ = "";
|
||||
bool in_key_update_ = false;
|
||||
};
|
||||
|
||||
// The TLSContext is used to create a TLSSession. For the client, there is
|
||||
// typically only a single TLSContext for each TLSSession. For the server,
|
||||
// there is a single TLSContext for the server and a TLSSession for every
|
||||
// QUIC session created by that server.
|
||||
class TLSContext final : public MemoryRetainer,
|
||||
public std::enable_shared_from_this<TLSContext> {
|
||||
public:
|
||||
static constexpr auto DEFAULT_CIPHERS = "TLS_AES_128_GCM_SHA256:"
|
||||
"TLS_AES_256_GCM_SHA384:"
|
||||
"TLS_CHACHA20_POLY1305_"
|
||||
"SHA256:TLS_AES_128_CCM_SHA256";
|
||||
static constexpr auto DEFAULT_GROUPS = "X25519:P-256:P-384:P-521";
|
||||
|
||||
static inline const TLSContext& From(const SSL* ssl);
|
||||
static inline TLSContext& From(SSL* ssl);
|
||||
|
||||
struct Options final : public MemoryRetainer {
|
||||
// The protocol identifier to be used by this Session.
|
||||
// The SNI servername to use for this session. This option is only used by
|
||||
// the client.
|
||||
std::string sni = "localhost";
|
||||
|
||||
// The ALPN (protocol name) to use for this session. This option is only
|
||||
// used by the client.
|
||||
std::string alpn = NGHTTP3_ALPN_H3;
|
||||
|
||||
// The SNI hostname to be used. This is used only by client Sessions to
|
||||
// identify the SNI host in the TLS client hello message.
|
||||
std::string hostname = "";
|
||||
|
||||
// When true, TLS keylog data will be emitted to the JavaScript session.
|
||||
bool keylog = false;
|
||||
|
||||
// When set, the peer certificate is verified against the list of supplied
|
||||
// CAs. If verification fails, the connection will be refused.
|
||||
bool reject_unauthorized = true;
|
||||
|
||||
// When set, enables TLS tracing for the session. This should only be used
|
||||
// for debugging.
|
||||
bool enable_tls_trace = false;
|
||||
|
||||
// Options only used by server sessions:
|
||||
|
||||
// When set, instructs the server session to request a client authentication
|
||||
// certificate.
|
||||
bool request_peer_certificate = false;
|
||||
|
||||
// Options only used by client sessions:
|
||||
|
||||
// When set, instructs the client session to verify the hostname default.
|
||||
// This is required by QUIC and enabled by default. We allow disabling it
|
||||
// only for debugging.
|
||||
bool verify_hostname_identity = true;
|
||||
|
||||
// The TLS session ID context (only used on the server)
|
||||
std::string session_id_ctx = "Node.js QUIC Server";
|
||||
|
||||
// TLS cipher suite
|
||||
// The list of TLS ciphers to use for this session.
|
||||
std::string ciphers = DEFAULT_CIPHERS;
|
||||
|
||||
// TLS groups
|
||||
// The list of TLS groups to use for this session.
|
||||
std::string groups = DEFAULT_GROUPS;
|
||||
|
||||
// The TLS private key to use for this session.
|
||||
// When true, enables keylog output for the session.
|
||||
bool keylog = false;
|
||||
|
||||
// When true, the peer certificate is verified against the list of supplied
|
||||
// CA. If verification fails, the connection will be refused. When set,
|
||||
// instructs the server session to request a client auth certificate. This
|
||||
// option is only used by the server side.
|
||||
bool verify_client = false;
|
||||
|
||||
// When true, enables TLS tracing for the session. This should only be used
|
||||
// for debugging.
|
||||
// JavaScript option name "tlsTrace".
|
||||
bool enable_tls_trace = false;
|
||||
|
||||
// When true, causes the private key passed in for the session to be
|
||||
// verified.
|
||||
// JavaScript option name "verifyPrivateKey"
|
||||
bool verify_private_key = false;
|
||||
|
||||
// The TLS private key(s) to use for this session.
|
||||
// JavaScript option name "keys"
|
||||
std::vector<std::shared_ptr<crypto::KeyObjectData>> keys;
|
||||
|
||||
// Collection of certificates to use for this session.
|
||||
// JavaScript option name "certs"
|
||||
std::vector<Store> certs;
|
||||
|
||||
// Optional certificate authority overrides to use.
|
||||
// JavaScript option name "ca"
|
||||
std::vector<Store> ca;
|
||||
|
||||
// Optional certificate revocation lists to use.
|
||||
// JavaScript option name "crl"
|
||||
std::vector<Store> crl;
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
SET_MEMORY_INFO_NAME(CryptoContext::Options)
|
||||
SET_MEMORY_INFO_NAME(TLSContext::Options)
|
||||
SET_SELF_SIZE(Options)
|
||||
|
||||
// The default TLS configuration.
|
||||
static const Options kDefault;
|
||||
|
||||
static v8::Maybe<Options> From(Environment* env,
|
||||
|
@ -104,74 +174,51 @@ class TLSContext final : public MemoryRetainer {
|
|||
std::string ToString() const;
|
||||
};
|
||||
|
||||
static const Options kDefaultOptions;
|
||||
static std::shared_ptr<TLSContext> CreateClient(const Options& options);
|
||||
static std::shared_ptr<TLSContext> CreateServer(const Options& options);
|
||||
|
||||
TLSContext(Environment* env,
|
||||
Side side,
|
||||
Session* session,
|
||||
const Options& options);
|
||||
TLSContext(const TLSContext&) = delete;
|
||||
TLSContext(TLSContext&&) = delete;
|
||||
TLSContext& operator=(const TLSContext&) = delete;
|
||||
TLSContext& operator=(TLSContext&&) = delete;
|
||||
TLSContext(Side side, const Options& options);
|
||||
DISALLOW_COPY_AND_MOVE(TLSContext)
|
||||
|
||||
// Start the TLS handshake.
|
||||
void Start();
|
||||
// Each QUIC Session has exactly one TLSSession. Each TLSSession maintains
|
||||
// a reference to the TLSContext used to create it.
|
||||
std::unique_ptr<TLSSession> NewSession(
|
||||
Session* session, const std::optional<SessionTicket>& maybeSessionTicket);
|
||||
|
||||
// TLS Keylogging is enabled per-Session by attaching a handler to the
|
||||
// "keylog" event. Each keylog line is emitted to JavaScript where it can be
|
||||
// routed to whatever destination makes sense. Typically, this will be to a
|
||||
// keylog file that can be consumed by tools like Wireshark to intercept and
|
||||
// decrypt QUIC network traffic.
|
||||
void Keylog(const char* line) const;
|
||||
inline Side side() const { return side_; }
|
||||
inline const Options& options() const { return options_; }
|
||||
inline operator bool() const { return ctx_ != nullptr; }
|
||||
|
||||
v8::MaybeLocal<v8::Object> cert(Environment* env) const;
|
||||
v8::MaybeLocal<v8::Object> peer_cert(Environment* env) const;
|
||||
v8::MaybeLocal<v8::Value> cipher_name(Environment* env) const;
|
||||
v8::MaybeLocal<v8::Value> cipher_version(Environment* env) const;
|
||||
v8::MaybeLocal<v8::Object> ephemeral_key(Environment* env) const;
|
||||
|
||||
// The SNI servername negotiated for the session
|
||||
const std::string_view servername() const;
|
||||
|
||||
// The ALPN (protocol name) negotiated for the session
|
||||
const std::string_view alpn() const;
|
||||
|
||||
// Triggers key update to begin. This will fail and return false if either a
|
||||
// previous key update is in progress and has not been confirmed or if the
|
||||
// initial handshake has not yet been confirmed.
|
||||
bool InitiateKeyUpdate();
|
||||
|
||||
int VerifyPeerIdentity();
|
||||
|
||||
Side side() const;
|
||||
const Options& options() const;
|
||||
|
||||
int OnNewSession(SSL_SESSION* session);
|
||||
|
||||
void MaybeSetEarlySession(const SessionTicket& sessionTicket);
|
||||
bool early_data_was_accepted() const;
|
||||
inline const std::string_view validation_error() const {
|
||||
return validation_error_;
|
||||
}
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
SET_MEMORY_INFO_NAME(CryptoContext)
|
||||
SET_MEMORY_INFO_NAME(TLSContext)
|
||||
SET_SELF_SIZE(TLSContext)
|
||||
|
||||
private:
|
||||
static ngtcp2_conn* getConnection(ngtcp2_crypto_conn_ref* ref);
|
||||
ngtcp2_crypto_conn_ref conn_ref_;
|
||||
crypto::SSLCtxPointer Initialize();
|
||||
operator SSL_CTX*() const;
|
||||
|
||||
static void OnKeylog(const SSL* ssl, const char* line);
|
||||
static int OnNewSession(SSL* ssl, SSL_SESSION* session);
|
||||
static int OnSelectAlpn(SSL* ssl,
|
||||
const unsigned char** out,
|
||||
unsigned char* outlen,
|
||||
const unsigned char* in,
|
||||
unsigned int inlen,
|
||||
void* arg);
|
||||
static int OnVerifyClientCertificate(int preverify_ok, X509_STORE_CTX* ctx);
|
||||
|
||||
Side side_;
|
||||
Environment* env_;
|
||||
Session* session_;
|
||||
const Options options_;
|
||||
BaseObjectPtr<crypto::SecureContext> secure_context_;
|
||||
crypto::SSLPointer ssl_;
|
||||
crypto::BIOPointer bio_trace_;
|
||||
Options options_;
|
||||
crypto::X509Pointer cert_;
|
||||
crypto::X509Pointer issuer_;
|
||||
crypto::SSLCtxPointer ctx_;
|
||||
std::string validation_error_ = "";
|
||||
|
||||
bool in_key_update_ = false;
|
||||
bool early_data_ = false;
|
||||
|
||||
friend class Session;
|
||||
friend class TLSSession;
|
||||
};
|
||||
|
||||
} // namespace quic
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include <node_internals.h>
|
||||
#include <node_sockaddr.h>
|
||||
#include "cid.h"
|
||||
#include "defs.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
@ -35,8 +36,7 @@ class TokenSecret final : public MemoryRetainer {
|
|||
|
||||
TokenSecret(const TokenSecret&) = default;
|
||||
TokenSecret& operator=(const TokenSecret&) = default;
|
||||
TokenSecret(TokenSecret&&) = delete;
|
||||
TokenSecret& operator=(TokenSecret&&) = delete;
|
||||
DISALLOW_MOVE(TokenSecret)
|
||||
|
||||
operator const uint8_t*() const;
|
||||
uint8_t operator[](int pos) const;
|
||||
|
@ -99,7 +99,7 @@ class StatelessResetToken final : public MemoryRetainer {
|
|||
explicit StatelessResetToken(const uint8_t* token);
|
||||
|
||||
StatelessResetToken(const StatelessResetToken& other);
|
||||
StatelessResetToken(StatelessResetToken&&) = delete;
|
||||
DISALLOW_MOVE(StatelessResetToken)
|
||||
|
||||
std::string ToString() const;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
#include <env-inl.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <ngtcp2/ngtcp2.h>
|
||||
#include <quic/cid.h>
|
||||
|
|
132
test/cctest/test_quic_error.cc
Normal file
132
test/cctest/test_quic_error.cc
Normal file
|
@ -0,0 +1,132 @@
|
|||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
#include <env-inl.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <quic/data.h>
|
||||
#include <util-inl.h>
|
||||
#include <string>
|
||||
|
||||
using node::quic::QuicError;
|
||||
|
||||
TEST(QuicError, NoError) {
|
||||
QuicError err;
|
||||
CHECK_EQ(err.code(), QuicError::QUIC_NO_ERROR);
|
||||
CHECK_EQ(err.type(), QuicError::Type::TRANSPORT);
|
||||
CHECK_EQ(err.reason(), "");
|
||||
CHECK_EQ(err, QuicError::TRANSPORT_NO_ERROR);
|
||||
CHECK(!err);
|
||||
|
||||
QuicError err2("a reason");
|
||||
CHECK_EQ(err2.code(), QuicError::QUIC_NO_ERROR);
|
||||
CHECK_EQ(err2.type(), QuicError::Type::TRANSPORT);
|
||||
CHECK_EQ(err2.reason(), "a reason");
|
||||
|
||||
// Equality check ignores the reason
|
||||
CHECK_EQ(err2, QuicError::TRANSPORT_NO_ERROR);
|
||||
|
||||
auto err3 = QuicError::ForTransport(QuicError::QUIC_NO_ERROR);
|
||||
CHECK_EQ(err3.code(), QuicError::QUIC_NO_ERROR);
|
||||
CHECK_EQ(err3.type(), QuicError::Type::TRANSPORT);
|
||||
CHECK_EQ(err3.reason(), "");
|
||||
CHECK_EQ(err3, QuicError::TRANSPORT_NO_ERROR);
|
||||
|
||||
// QuicError's are copy assignable
|
||||
auto err4 = err3;
|
||||
CHECK_EQ(err4, err3);
|
||||
|
||||
// QuicError's are movable
|
||||
auto err5 = std::move(err4);
|
||||
CHECK_EQ(err5, err3);
|
||||
|
||||
// Equality check ignores the reason
|
||||
CHECK(err5 == err2);
|
||||
CHECK(err5 != QuicError::APPLICATION_NO_ERROR);
|
||||
|
||||
const ngtcp2_ccerr& ccerr = err5;
|
||||
CHECK_EQ(ccerr.error_code, NGTCP2_NO_ERROR);
|
||||
CHECK_EQ(ccerr.frame_type, 0);
|
||||
CHECK_EQ(ccerr.type, NGTCP2_CCERR_TYPE_TRANSPORT);
|
||||
CHECK_EQ(ccerr.reasonlen, 0);
|
||||
|
||||
const ngtcp2_ccerr* ccerr2 = err5;
|
||||
CHECK_EQ(ccerr2->error_code, NGTCP2_NO_ERROR);
|
||||
CHECK_EQ(ccerr2->frame_type, 0);
|
||||
CHECK_EQ(ccerr2->type, NGTCP2_CCERR_TYPE_TRANSPORT);
|
||||
CHECK_EQ(ccerr2->reasonlen, 0);
|
||||
|
||||
QuicError err6(ccerr);
|
||||
QuicError err7(ccerr2);
|
||||
CHECK_EQ(err6, err7);
|
||||
|
||||
CHECK_EQ(err.ToString(), "QuicError(TRANSPORT) 0");
|
||||
CHECK_EQ(err2.ToString(), "QuicError(TRANSPORT) 0: a reason");
|
||||
|
||||
ngtcp2_ccerr ccerr3;
|
||||
ngtcp2_ccerr_default(&ccerr3);
|
||||
ccerr3.frame_type = 1;
|
||||
QuicError err8(ccerr3);
|
||||
CHECK_EQ(err8.frame_type(), 1);
|
||||
}
|
||||
|
||||
TEST(QuicError, ApplicationNoError) {
|
||||
CHECK_EQ(QuicError::APPLICATION_NO_ERROR.code(),
|
||||
QuicError::QUIC_APP_NO_ERROR);
|
||||
CHECK_EQ(QuicError::APPLICATION_NO_ERROR.type(),
|
||||
QuicError::Type::APPLICATION);
|
||||
CHECK_EQ(QuicError::APPLICATION_NO_ERROR.reason(), "");
|
||||
|
||||
auto err =
|
||||
QuicError::ForApplication(QuicError::QUIC_APP_NO_ERROR, "a reason");
|
||||
CHECK_EQ(err.code(), QuicError::QUIC_APP_NO_ERROR);
|
||||
CHECK_EQ(err.type(), QuicError::Type::APPLICATION);
|
||||
CHECK_EQ(err.reason(), "a reason");
|
||||
CHECK_EQ(err.ToString(), "QuicError(APPLICATION) 65280: a reason");
|
||||
}
|
||||
|
||||
TEST(QuicError, VersionNegotiation) {
|
||||
CHECK_EQ(QuicError::VERSION_NEGOTIATION.code(), 0);
|
||||
CHECK_EQ(QuicError::VERSION_NEGOTIATION.type(),
|
||||
QuicError::Type::VERSION_NEGOTIATION);
|
||||
CHECK_EQ(QuicError::VERSION_NEGOTIATION.reason(), "");
|
||||
|
||||
auto err = QuicError::ForVersionNegotiation("a reason");
|
||||
CHECK_EQ(err.code(), 0);
|
||||
CHECK_EQ(err.type(), QuicError::Type::VERSION_NEGOTIATION);
|
||||
CHECK_EQ(err.reason(), "a reason");
|
||||
CHECK_EQ(err.ToString(), "QuicError(VERSION_NEGOTIATION) 0: a reason");
|
||||
}
|
||||
|
||||
TEST(QuicError, IdleClose) {
|
||||
CHECK_EQ(QuicError::IDLE_CLOSE.code(), 0);
|
||||
CHECK_EQ(QuicError::IDLE_CLOSE.type(), QuicError::Type::IDLE_CLOSE);
|
||||
CHECK_EQ(QuicError::IDLE_CLOSE.reason(), "");
|
||||
|
||||
auto err = QuicError::ForIdleClose("a reason");
|
||||
CHECK_EQ(err.code(), 0);
|
||||
CHECK_EQ(err.type(), QuicError::Type::IDLE_CLOSE);
|
||||
CHECK_EQ(err.reason(), "a reason");
|
||||
CHECK_EQ(err.ToString(), "QuicError(IDLE_CLOSE) 0: a reason");
|
||||
|
||||
CHECK_EQ(QuicError::IDLE_CLOSE, err);
|
||||
}
|
||||
|
||||
TEST(QuicError, InternalError) {
|
||||
auto err = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL, "a reason");
|
||||
CHECK_EQ(err.code(), NGTCP2_INTERNAL_ERROR);
|
||||
CHECK_EQ(err.type(), QuicError::Type::TRANSPORT);
|
||||
CHECK_EQ(err.reason(), "a reason");
|
||||
CHECK_EQ(err.ToString(), "QuicError(TRANSPORT) 1: a reason");
|
||||
|
||||
printf("%s\n", QuicError::INTERNAL_ERROR.ToString().c_str());
|
||||
CHECK_EQ(err, QuicError::INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
TEST(QuicError, TlsAlert) {
|
||||
auto err = QuicError::ForTlsAlert(1, "a reason");
|
||||
CHECK_EQ(err.code(), 257);
|
||||
CHECK_EQ(err.type(), QuicError::Type::TRANSPORT);
|
||||
CHECK_EQ(err.reason(), "a reason");
|
||||
CHECK(err.is_crypto());
|
||||
CHECK_EQ(err.crypto_error(), 1);
|
||||
}
|
||||
|
||||
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
|
@ -14,7 +14,7 @@ using node::quic::RetryToken;
|
|||
using node::quic::StatelessResetToken;
|
||||
using node::quic::TokenSecret;
|
||||
|
||||
TEST(TokenScret, Basics) {
|
||||
TEST(TokenSecret, Basics) {
|
||||
uint8_t secret[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6};
|
||||
TokenSecret fixed_secret(secret);
|
||||
|
||||
|
|
|
@ -136,14 +136,14 @@ const cases = [
|
|||
{
|
||||
key: 'cc',
|
||||
valid: [
|
||||
quic.QUIC_CC_ALGO_RENO,
|
||||
quic.QUIC_CC_ALGO_CUBIC,
|
||||
quic.QUIC_CC_ALGO_BBR,
|
||||
quic.QUIC_CC_ALGO_BBR2,
|
||||
quic.QUIC_CC_ALGO_RENO_STR,
|
||||
quic.QUIC_CC_ALGO_CUBIC_STR,
|
||||
quic.QUIC_CC_ALGO_BBR_STR,
|
||||
quic.QUIC_CC_ALGO_BBR2_STR,
|
||||
quic.CC_ALGO_RENO,
|
||||
quic.CC_ALGO_CUBIC,
|
||||
quic.CC_ALGO_BBR,
|
||||
quic.CC_ALGO_BBR2,
|
||||
quic.CC_ALGO_RENO_STR,
|
||||
quic.CC_ALGO_CUBIC_STR,
|
||||
quic.CC_ALGO_BBR_STR,
|
||||
quic.CC_ALGO_BBR2_STR,
|
||||
],
|
||||
invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}],
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue