Skip to content

Commit d714e8d

Browse files
committed
quic: complete the c++ side of the origin frame support
1 parent 5f7901b commit d714e8d

11 files changed

Lines changed: 179 additions & 12 deletions

File tree

doc/api/quic.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1453,13 +1453,20 @@ no other host name matches. Each entry may contain:
14531453
* `crl` {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]}
14541454
Optional certificate revocation lists.
14551455
* `verifyPrivateKey` {boolean} Verify the private key. Default: `false`.
1456+
* `port` {number} The port to advertise in ORIGIN frames (RFC 9412) for
1457+
this host name. **Default:** `443`. Only used for HTTP/3 sessions.
1458+
* `authoritative` {boolean} Whether to include this host name in ORIGIN
1459+
frames. **Default:** `true`. Set to `false` to exclude a host name
1460+
from ORIGIN advertisements. Wildcard (`'*'`) entries are always
1461+
excluded regardless of this setting.
14561462

14571463
```mjs
14581464
const endpoint = await listen(callback, {
14591465
sni: {
14601466
'*': { keys: [defaultKey], certs: [defaultCert] },
1461-
'api.example.com': { keys: [apiKey], certs: [apiCert] },
1467+
'api.example.com': { keys: [apiKey], certs: [apiCert], port: 8443 },
14621468
'www.example.com': { keys: [wwwKey], certs: [wwwCert], ca: [customCA] },
1469+
'internal.example.com': { keys: [intKey], certs: [intCert], authoritative: false },
14631470
},
14641471
});
14651472
```

lib/internal/quic/quic.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ const {
138138
kNewToken,
139139
kOnHeaders,
140140
kOnTrailers,
141+
kOrigin,
141142
kPathValidation,
142143
kPrivateConstructor,
143144
kReset,
@@ -186,6 +187,7 @@ const onSessionPathValidationChannel = dc.channel('quic.session.path.validation'
186187
const onSessionNewTokenChannel = dc.channel('quic.session.new.token');
187188
const onSessionTicketChannel = dc.channel('quic.session.ticket');
188189
const onSessionVersionNegotiationChannel = dc.channel('quic.session.version.negotiation');
190+
const onSessionOriginChannel = dc.channel('quic.session.receive.origin');
189191
const onSessionHandshakeChannel = dc.channel('quic.session.handshake');
190192

191193
/**
@@ -504,6 +506,15 @@ setCallbacks({
504506
this[kOwner][kNewToken](token, address);
505507
},
506508

509+
/**
510+
* Called when the session receives an ORIGIN frame from the peer (RFC 9412).
511+
* @param {string[]} origins The list of origins the peer claims authority for
512+
*/
513+
onSessionOrigin(origins) {
514+
debug('session origin callback', this[kOwner]);
515+
this[kOwner][kOrigin](origins);
516+
},
517+
507518
/**
508519
* Called when the session receives a session version negotiation request
509520
* @param {*} version
@@ -1628,6 +1639,20 @@ class QuicSession {
16281639
}
16291640
}
16301641

1642+
/**
1643+
* Called when the session receives an ORIGIN frame (RFC 9412).
1644+
* @param {string[]} origins
1645+
*/
1646+
[kOrigin](origins) {
1647+
if (this.destroyed) return;
1648+
if (onSessionOriginChannel.hasSubscribers) {
1649+
onSessionOriginChannel.publish({
1650+
origins,
1651+
session: this,
1652+
});
1653+
}
1654+
}
1655+
16311656
/**
16321657
* @param {string} servername
16331658
* @param {string} protocol
@@ -2422,11 +2447,18 @@ function processTlsOptions(tls, forServer) {
24222447
if (identity.certs === undefined) {
24232448
throw new ERR_MISSING_ARGS(`options.sni['${hostname}'].certs`);
24242449
}
2425-
// Build a full TLS options object: shared + identity.
2450+
// Extract ORIGIN frame options from the SNI entry.
2451+
const {
2452+
port,
2453+
authoritative,
2454+
} = sni[hostname];
2455+
// Build a full TLS options object: shared + identity + origin options.
24262456
sniEntries[hostname] = {
24272457
__proto__: null,
24282458
...shared,
24292459
...identity,
2460+
...(port !== undefined ? { port } : {}),
2461+
...(authoritative !== undefined ? { authoritative } : {}),
24302462
};
24312463
}
24322464

lib/internal/quic/symbols.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const kNewStream = Symbol('kNewStream');
3636
const kNewToken = Symbol('kNewToken');
3737
const kOnHeaders = Symbol('kOnHeaders');
3838
const kOnTrailers = Symbol('kOwnTrailers');
39+
const kOrigin = Symbol('kOrigin');
3940
const kOwner = Symbol('kOwner');
4041
const kPathValidation = Symbol('kPathValidation');
4142
const kPrivateConstructor = Symbol('kPrivateConstructor');
@@ -65,6 +66,7 @@ module.exports = {
6566
kNewToken,
6667
kOnHeaders,
6768
kOnTrailers,
69+
kOrigin,
6870
kOwner,
6971
kPathValidation,
7072
kPrivateConstructor,

src/quic/bindingdata.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Packet;
4141
V(session_handshake, SessionHandshake) \
4242
V(session_new, SessionNew) \
4343
V(session_new_token, SessionNewToken) \
44+
V(session_origin, SessionOrigin) \
4445
V(session_path_validation, SessionPathValidation) \
4546
V(session_ticket, SessionTicket) \
4647
V(session_version_negotiation, SessionVersionNegotiation) \
@@ -59,6 +60,7 @@ class Packet;
5960
V(active_connection_id_limit, "activeConnectionIDLimit") \
6061
V(address_lru_size, "addressLRUSize") \
6162
V(application, "application") \
63+
V(authoritative, "authoritative") \
6264
V(bbr, "bbr") \
6365
V(ca, "ca") \
6466
V(cc_algorithm, "cc") \
@@ -103,6 +105,7 @@ class Packet;
103105
V(max_stream_window, "maxStreamWindow") \
104106
V(max_window, "maxWindow") \
105107
V(min_version, "minVersion") \
108+
V(port, "port") \
106109
V(preferred_address_strategy, "preferredAddressPolicy") \
107110
V(alpn, "alpn") \
108111
V(qlog, "qlog") \

src/quic/defs.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,39 @@ bool SetOption(Environment* env,
8383
return true;
8484
}
8585

86+
template <typename Opt, uint16_t Opt::*member>
87+
bool SetOption(Environment* env,
88+
Opt* options,
89+
const v8::Local<v8::Object>& object,
90+
const v8::Local<v8::String>& name) {
91+
v8::Local<v8::Value> value;
92+
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
93+
if (!value->IsUndefined()) {
94+
if (!value->IsUint32()) {
95+
Utf8Value nameStr(env->isolate(), name);
96+
THROW_ERR_INVALID_ARG_VALUE(
97+
env, "The %s option must be an uint16", nameStr);
98+
return false;
99+
}
100+
v8::Local<v8::Uint32> num;
101+
if (!value->ToUint32(env->context()).ToLocal(&num)) {
102+
Utf8Value nameStr(env->isolate(), name);
103+
THROW_ERR_INVALID_ARG_VALUE(
104+
env, "The %s option must be an uint16", nameStr);
105+
return false;
106+
}
107+
uint32_t val = num->Value();
108+
if (val > 0xFFFF) {
109+
Utf8Value nameStr(env->isolate(), name);
110+
THROW_ERR_INVALID_ARG_VALUE(
111+
env, "The %s option must fit in a uint16", nameStr);
112+
return false;
113+
}
114+
options->*member = static_cast<uint16_t>(val);
115+
}
116+
return true;
117+
}
118+
86119
template <typename Opt, uint64_t Opt::*member>
87120
bool SetOption(Environment* env,
88121
Opt* options,

src/quic/http3.cc

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,14 @@ class Http3ApplicationImpl final : public Session::Application {
144144
: Application(session, options),
145145
allocator_(BindingData::Get(env())),
146146
options_(options),
147-
conn_(InitializeConnection()) {
147+
conn_(nullptr) {
148+
// Build the ORIGIN frame payload from the SNI configuration before
149+
// creating the nghttp3 connection, since InitializeConnection needs
150+
// the origin_vec_ to be ready for settings.origin_list.
151+
if (session->is_server()) {
152+
BuildOriginPayload();
153+
}
154+
conn_ = InitializeConnection();
148155
session->set_priority_supported();
149156
}
150157

@@ -613,9 +620,38 @@ class Http3ApplicationImpl final : public Session::Application {
613620
id == qpack_enc_stream_id_;
614621
}
615622

623+
void BuildOriginPayload() {
624+
// Build the serialized ORIGIN frame payload from the SNI configuration.
625+
// Each origin entry is: 2-byte BE length + origin string.
626+
// Wildcard ('*') entries and entries with authoritative=false are skipped.
627+
auto& sni = session().config().options.sni;
628+
for (auto& [hostname, opts] : sni) {
629+
if (hostname == "*" || !opts.authoritative) continue;
630+
std::string origin = "https://";
631+
origin += hostname;
632+
if (opts.port != 443) {
633+
origin += ":";
634+
origin += std::to_string(opts.port);
635+
}
636+
// 2-byte BE length prefix
637+
uint16_t len = static_cast<uint16_t>(origin.size());
638+
origin_payload_.push_back(static_cast<uint8_t>((len >> 8) & 0xff));
639+
origin_payload_.push_back(static_cast<uint8_t>(len & 0xff));
640+
// Origin string bytes
641+
origin_payload_.insert(
642+
origin_payload_.end(), origin.begin(), origin.end());
643+
}
644+
if (!origin_payload_.empty()) {
645+
origin_vec_ = {origin_payload_.data(), origin_payload_.size()};
646+
}
647+
}
648+
616649
Http3ConnectionPointer InitializeConnection() {
617650
nghttp3_conn* conn = nullptr;
618651
nghttp3_settings settings = options_;
652+
if (!origin_payload_.empty()) {
653+
settings.origin_list = &origin_vec_;
654+
}
619655
if (session().is_server()) {
620656
CHECK_EQ(nghttp3_conn_server_new(
621657
&conn, &kCallbacks, &settings, &allocator_, this),
@@ -803,6 +839,14 @@ class Http3ApplicationImpl final : public Session::Application {
803839
int64_t qpack_dec_stream_id_ = -1;
804840
int64_t qpack_enc_stream_id_ = -1;
805841

842+
// ORIGIN frame support (RFC 9412).
843+
// origin_payload_ holds the serialized ORIGIN frame payload for sending.
844+
// origin_vec_ points into origin_payload_ for nghttp3_settings.origin_list.
845+
// received_origins_ accumulates origins from received ORIGIN frames.
846+
std::vector<uint8_t> origin_payload_;
847+
nghttp3_vec origin_vec_{nullptr, 0};
848+
std::vector<std::string> received_origins_;
849+
806850
// ==========================================================================
807851
// Static callbacks
808852

@@ -1083,14 +1127,18 @@ class Http3ApplicationImpl final : public Session::Application {
10831127
const uint8_t* origin,
10841128
size_t originlen,
10851129
void* conn_user_data) {
1086-
// ORIGIN frames (RFC 8336) are used for connection coalescing
1087-
// across multiple origins. Not yet implemented u2014 requires
1088-
// connection pooling and multi-origin reuse support.
1130+
NGHTTP3_CALLBACK_SCOPE(app);
1131+
app.received_origins_.emplace_back(reinterpret_cast<const char*>(origin),
1132+
originlen);
10891133
return NGTCP2_SUCCESS;
10901134
}
10911135

10921136
static int on_end_origin(nghttp3_conn* conn, void* conn_user_data) {
1093-
// See on_receive_origin above.
1137+
NGHTTP3_CALLBACK_SCOPE(app);
1138+
if (!app.received_origins_.empty()) {
1139+
app.session().EmitOrigins(std::move(app.received_origins_));
1140+
app.received_origins_.clear();
1141+
}
10941142
return NGTCP2_SUCCESS;
10951143
}
10961144

src/quic/session.cc

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,7 +1240,11 @@ struct Session::Impl final : public MemoryRetainer {
12401240
on_receive_rx_key,
12411241
nullptr,
12421242
on_early_data_rejected,
1243-
on_begin_path_validation};
1243+
on_begin_path_validation,
1244+
nullptr,
1245+
nullptr,
1246+
nullptr,
1247+
nullptr};
12441248

12451249
static constexpr ngtcp2_callbacks SERVER = {
12461250
nullptr,
@@ -1283,7 +1287,11 @@ struct Session::Impl final : public MemoryRetainer {
12831287
nullptr,
12841288
on_receive_tx_key,
12851289
on_early_data_rejected,
1286-
on_begin_path_validation};
1290+
on_begin_path_validation,
1291+
nullptr,
1292+
nullptr,
1293+
nullptr,
1294+
nullptr};
12871295
};
12881296

12891297
#undef NGTCP2_CALLBACK_SCOPE
@@ -2914,6 +2922,28 @@ void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd,
29142922
argv);
29152923
}
29162924

2925+
void Session::EmitOrigins(std::vector<std::string>&& origins) {
2926+
DCHECK(!is_destroyed());
2927+
if (!env()->can_call_into_js()) return;
2928+
2929+
CallbackScope<Session> cb_scope(this);
2930+
2931+
auto isolate = env()->isolate();
2932+
2933+
LocalVector<Value> elements(env()->isolate(), origins.size());
2934+
for (size_t i = 0; i < origins.size(); i++) {
2935+
Local<Value> str;
2936+
if (!ToV8Value(env()->context(), origins[i]).ToLocal(&str)) [[unlikely]] {
2937+
return;
2938+
}
2939+
elements[i] = str;
2940+
}
2941+
2942+
Local<Value> argv[] = {Array::New(isolate, elements.data(), elements.size())};
2943+
MakeCallback(
2944+
BindingData::Get(env()).session_origin_callback(), arraysize(argv), argv);
2945+
}
2946+
29172947
void Session::EmitKeylog(const char* line) {
29182948
if (!env()->can_call_into_js()) return;
29192949
if (keylog_stream_) {

src/quic/session.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
480480
void EmitDatagramStatus(datagram_id id, DatagramStatus status);
481481
void EmitHandshakeComplete();
482482
void EmitKeylog(const char* line);
483+
void EmitOrigins(std::vector<std::string>&& origins);
483484

484485
struct ValidatedPath {
485486
std::shared_ptr<SocketAddress> local;

src/quic/tlscontext.cc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -697,9 +697,10 @@ Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
697697
if (!SET(verify_client) || !SET(reject_unauthorized) ||
698698
!SET(enable_early_data) || !SET(enable_tls_trace) || !SET(alpn) ||
699699
!SET(servername) || !SET(ciphers) || !SET(groups) ||
700-
!SET(verify_private_key) || !SET(keylog) ||
701-
!SET_VECTOR(crypto::KeyObjectData, keys) || !SET_VECTOR(Store, certs) ||
702-
!SET_VECTOR(Store, ca) || !SET_VECTOR(Store, crl)) {
700+
!SET(verify_private_key) || !SET(keylog) || !SET(port) ||
701+
!SET(authoritative) || !SET_VECTOR(crypto::KeyObjectData, keys) ||
702+
!SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) ||
703+
!SET_VECTOR(Store, crl)) {
703704
return Nothing<Options>();
704705
}
705706

src/quic/tlscontext.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,15 @@ class TLSContext final : public MemoryRetainer,
241241
// JavaScript option name "crl"
242242
std::vector<Store> crl;
243243

244+
// The port to advertise in ORIGIN frames for this hostname.
245+
// Defaults to 443 (the standard HTTPS port). Only relevant for
246+
// server-side SNI entries used with HTTP/3.
247+
uint16_t port = 443;
248+
249+
// Whether this hostname should be included in ORIGIN frames.
250+
// Only relevant for server-side SNI entries.
251+
bool authoritative = true;
252+
244253
void MemoryInfo(MemoryTracker* tracker) const override;
245254
SET_MEMORY_INFO_NAME(TLSContext::Options)
246255
SET_SELF_SIZE(Options)

0 commit comments

Comments
 (0)