@@ -184,6 +184,8 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) {
184184#define SESSION_JS_METHODS (V ) \
185185 V (Destroy, destroy, SIDE_EFFECT ) \
186186 V (GetRemoteAddress, getRemoteAddress, NO_SIDE_EFFECT ) \
187+ V (GetServername, getServername, NO_SIDE_EFFECT ) \
188+ V (GetAlpnProtocol, getAlpnProtocol, NO_SIDE_EFFECT ) \
187189 V (GetLocalAddress, getLocalAddress, NO_SIDE_EFFECT ) \
188190 V (GetCertificate, getCertificate, NO_SIDE_EFFECT ) \
189191 V (GetEphemeralKeyInfo, getEphemeralKey, NO_SIDE_EFFECT ) \
@@ -781,6 +783,8 @@ struct Session::Impl final : public MemoryRetainer {
781783 SocketAddress remote_address_;
782784 std::unique_ptr<Application> application_;
783785 StreamsMap streams_;
786+ // Emits deferred until after session setup is completed
787+ std::vector<std::function<void ()>> deferred_emits_;
784788 TimerWrapHandle timer_;
785789 size_t send_scope_depth_ = 0 ;
786790 QuicError last_error_;
@@ -1001,6 +1005,41 @@ struct Session::Impl final : public MemoryRetainer {
10011005 session->Destroy ();
10021006 }
10031007
1008+ // The SNI servername: null until the TLS parameters are final, then the
1009+ // host name string, or false if the handshake produced no SNI.
1010+ JS_METHOD (GetServername) {
1011+ auto env = Environment::GetCurrent (args);
1012+ Session* session;
1013+ ASSIGN_OR_RETURN_UNWRAP (&session, args.This ());
1014+ if (session->is_destroyed () || !session->tls_info_ready ()) {
1015+ return args.GetReturnValue ().SetNull ();
1016+ }
1017+ auto sn = session->tls_session ().servername ();
1018+ if (sn.empty ()) return args.GetReturnValue ().Set (false );
1019+ Local<Value> ret;
1020+ if (ToV8Value (env->context (), sn).ToLocal (&ret)) {
1021+ args.GetReturnValue ().Set (ret);
1022+ }
1023+ }
1024+
1025+ // The negotiated ALPN protocol: null until the TLS parameters are final,
1026+ // then the protocol string.
1027+ JS_METHOD (GetAlpnProtocol) {
1028+ auto env = Environment::GetCurrent (args);
1029+ Session* session;
1030+ ASSIGN_OR_RETURN_UNWRAP (&session, args.This ());
1031+ if (session->is_destroyed () || !session->tls_info_ready ()) {
1032+ return args.GetReturnValue ().SetNull ();
1033+ }
1034+ auto proto = session->tls_session ().protocol ();
1035+ // QUIC requires ALPN
1036+ DCHECK (!proto.empty ());
1037+ Local<Value> ret;
1038+ if (ToV8Value (env->context (), proto).ToLocal (&ret)) {
1039+ args.GetReturnValue ().Set (ret);
1040+ }
1041+ }
1042+
10041043 JS_METHOD (GetRemoteAddress) {
10051044 auto env = Environment::GetCurrent (args);
10061045 Session* session;
@@ -2649,6 +2688,12 @@ const Session::Options& Session::options() const {
26492688void Session::EmitQlog (uint32_t flags, std::string_view data) {
26502689 if (!env ()->can_call_into_js ()) return ;
26512690
2691+ if (!is_destroyed () && must_defer_emits ()) {
2692+ QueueDeferredEmit (
2693+ [this , flags, held = std::string (data)]() { EmitQlog (flags, held); });
2694+ return ;
2695+ }
2696+
26522697 bool fin = (flags & NGTCP2_QLOG_WRITE_FLAG_FIN ) != 0 ;
26532698
26542699 // Fun fact... ngtcp2 does not emit the final qlog statement until the
@@ -2771,6 +2816,16 @@ bool Session::ReadPacket(const uint8_t* data,
27712816 // Process deferred operations that couldn't run inside callback
27722817 // scopes (e.g., HTTP/3 GOAWAY handling that calls into JS).
27732818 application ().PostReceive ();
2819+ // Surface a server session to JS once its ClientHello has been
2820+ // processed (OnSelectAlpn fired: SNI + ALPN are known and reliable).
2821+ // Held first-flight events - including 0-RTT request streams - replay
2822+ // at emit. The !wrapped guard makes this fire exactly once, on
2823+ // whichever packet completes the ClientHello (so a multi-datagram
2824+ // ClientHello is handled correctly).
2825+ if (is_server () && hello_processed_ && !impl_->state ()->wrapped &&
2826+ !is_destroyed ()) {
2827+ endpoint ().EmitNewSession (BaseObjectPtr<Session>(this ));
2828+ }
27742829 }
27752830 return true ;
27762831 }
@@ -3460,6 +3515,36 @@ void Session::set_wrapped() {
34603515 impl_->state ()->wrapped = 1 ;
34613516}
34623517
3518+ bool Session::must_defer_emits () const {
3519+ // Server sessions are surfaced to JS (via the deferred new-session emit)
3520+ // only after the ClientHello has been processed and wrapped; anything
3521+ // emitted before then has no JS wrapper to receive it and must be held
3522+ // for replay.
3523+ return is_server () && !impl_->state ()->wrapped ;
3524+ }
3525+
3526+ bool Session::tls_info_ready () const {
3527+ // hello_processed_ is set server-side, handshake_completed covers
3528+ // the client. Together they mark the point when SNI/ALPN are final.
3529+ return hello_processed_ || impl_->state ()->handshake_completed ;
3530+ }
3531+
3532+ void Session::QueueDeferredEmit (std::function<void ()> fn) {
3533+ impl_->deferred_emits_ .emplace_back (std::move (fn));
3534+ }
3535+
3536+ void Session::ReplayDeferredEmits () {
3537+ if (is_destroyed ()) return ;
3538+ DCHECK (impl_->state ()->wrapped );
3539+ // Runs synchronously immediately after the new-session callback
3540+ // returns (still within first-flight processing).
3541+ auto emits = std::move (impl_->deferred_emits_ );
3542+ for (auto & emit : emits) {
3543+ if (is_destroyed ()) return ;
3544+ emit ();
3545+ }
3546+ }
3547+
34633548void Session::set_priority_supported (bool on) {
34643549 DCHECK (!is_destroyed ());
34653550 impl_->state ()->priority_supported = on ? 1 : 0 ;
@@ -3769,6 +3854,10 @@ bool Session::HandshakeCompleted() {
37693854
37703855 Debug (this , " Session handshake completed" );
37713856 impl_->state ()->handshake_completed = 1 ;
3857+ // This implies fully completing a handshake without setting hello_processed
3858+ // (set during ALPN negotiation). Should be impossible unless ALPN flow is
3859+ // changed drastically, but good to check as it'd lose sessions.
3860+ DCHECK (!is_server () || hello_processed_);
37723861
37733862 STAT_RECORD_TIMESTAMP (Stats, handshake_completed_at);
37743863 SetStreamOpenAllowed ();
@@ -3967,6 +4056,7 @@ void Session::set_max_datagram_size(uint16_t size) {
39674056
39684057void Session::EmitGoaway (stream_id last_stream_id) {
39694058 if (is_destroyed ()) return ;
4059+ if (DeferEmit ([this , last_stream_id] { EmitGoaway (last_stream_id); })) return ;
39704060 if (!env ()->can_call_into_js ()) return ;
39714061
39724062 CallbackScope<Session> cb_scope (this );
@@ -3981,6 +4071,14 @@ void Session::EmitGoaway(stream_id last_stream_id) {
39814071
39824072void Session::EmitDatagram (Store&& datagram, DatagramReceivedFlags flag) {
39834073 DCHECK (!is_destroyed ());
4074+
4075+ if (must_defer_emits ()) {
4076+ QueueDeferredEmit ([this , datagram = std::move (datagram), flag]() mutable {
4077+ EmitDatagram (std::move (datagram), flag);
4078+ });
4079+ return ;
4080+ }
4081+
39844082 if (!env ()->can_call_into_js ()) return ;
39854083
39864084 CallbackScope<Session> cbv_scope (this );
@@ -3996,6 +4094,8 @@ void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) {
39964094void Session::EmitDatagramStatus (datagram_id id, quic::DatagramStatus status) {
39974095 DCHECK (!is_destroyed ());
39984096
4097+ if (DeferEmit ([this , id, status] { EmitDatagramStatus (id, status); })) return ;
4098+
39994099 if (!env ()->can_call_into_js ()) return ;
40004100
40014101 CallbackScope<Session> cb_scope (this );
@@ -4157,6 +4257,7 @@ void Session::EmitSessionTicket(Store&& ticket) {
41574257
41584258void Session::EmitApplication () {
41594259 if (is_destroyed ()) return ;
4260+ if (DeferEmit ([this ] { EmitApplication (); })) return ;
41604261 if (!env ()->can_call_into_js ()) return ;
41614262
41624263 if (!has_application ()) {
@@ -4227,6 +4328,10 @@ void Session::EmitNewToken(const uint8_t* token, size_t len) {
42274328void Session::EmitStream (const BaseObjectWeakPtr<Stream>& stream) {
42284329 DCHECK (!is_destroyed ());
42294330
4331+ if (DeferEmit ([this , stream] { EmitStream (stream); })) return ;
4332+
4333+ if (!stream) return ;
4334+
42304335 if (!env ()->can_call_into_js ()) return ;
42314336 CallbackScope<Session> cb_scope (this );
42324337
@@ -4282,6 +4387,14 @@ void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd,
42824387
42834388void Session::EmitOrigins (std::vector<std::string>&& origins) {
42844389 DCHECK (!is_destroyed ());
4390+
4391+ if (must_defer_emits ()) {
4392+ QueueDeferredEmit ([this , origins = std::move (origins)]() mutable {
4393+ EmitOrigins (std::move (origins));
4394+ });
4395+ return ;
4396+ }
4397+
42854398 if (!HasListenerFlag (impl_->state ()->listener_flags ,
42864399 SessionListenerFlags::ORIGIN ))
42874400 return ;
@@ -4307,11 +4420,17 @@ void Session::EmitOrigins(std::vector<std::string>&& origins) {
43074420
43084421void Session::EmitKeylog (const char * line) {
43094422 DCHECK (!is_destroyed ());
4423+
4424+ if (must_defer_emits ()) {
4425+ QueueDeferredEmit (
4426+ [this , str = std::string (line)]() { EmitKeylog (str.c_str ()); });
4427+ return ;
4428+ }
4429+
43104430 if (!env ()->can_call_into_js ()) return ;
43114431
4312- auto str = std::string (line);
43134432 Local<Value> argv[] = {Undefined (env ()->isolate ())};
4314- if (!ToV8Value (env ()->context (), str ).ToLocal (&argv[0 ])) {
4433+ if (!ToV8Value (env ()->context (), std::string (line) ).ToLocal (&argv[0 ])) {
43154434 Debug (this , " Failed to convert keylog line to V8 string" );
43164435 return ;
43174436 }
0 commit comments