@@ -16,6 +16,7 @@ const {
16
16
Error,
17
17
Map,
18
18
Number,
19
+ Promise,
19
20
RegExp,
20
21
Set,
21
22
Symbol,
@@ -104,6 +105,7 @@ const {
104
105
ERR_QUICSESSION_VERSION_NEGOTIATION ,
105
106
ERR_TLS_DH_PARAM_SIZE ,
106
107
} ,
108
+ hideStackFrames,
107
109
errnoException,
108
110
exceptionWithHostPort
109
111
} = require ( 'internal/errors' ) ;
@@ -200,10 +202,14 @@ const {
200
202
201
203
const emit = EventEmitter . prototype . emit ;
202
204
205
+ // TODO(@jasnell): Temporary while converting to Promises-based API
206
+ const { lookup } = require ( 'dns' ) . promises ;
207
+
203
208
const kAfterLookup = Symbol ( 'kAfterLookup' ) ;
204
209
const kAfterPreferredAddressLookup = Symbol ( 'kAfterPreferredAddressLookup' ) ;
205
210
const kAddSession = Symbol ( 'kAddSession' ) ;
206
211
const kAddStream = Symbol ( 'kAddStream' ) ;
212
+ const kBind = Symbol ( 'kBind' ) ;
207
213
const kClose = Symbol ( 'kClose' ) ;
208
214
const kCert = Symbol ( 'kCert' ) ;
209
215
const kClientHello = Symbol ( 'kClientHello' ) ;
@@ -255,6 +261,14 @@ const kSocketDestroyed = 4;
255
261
let diagnosticPacketLossWarned = false ;
256
262
let warnedVerifyHostnameIdentity = false ;
257
263
264
+ let DOMException ;
265
+
266
+ const lazyDOMException = hideStackFrames ( ( message ) => {
267
+ if ( DOMException === undefined )
268
+ DOMException = internalBinding ( 'messaging' ) . DOMException ;
269
+ return new DOMException ( message ) ;
270
+ } ) ;
271
+
258
272
assert ( process . versions . ngtcp2 !== undefined ) ;
259
273
260
274
// Called by the C++ internals when the QuicSocket is closed with
@@ -589,12 +603,27 @@ function lookupOrDefault(lookup, type) {
589
603
return lookup || ( type === AF_INET6 ? lookup6 : lookup4 ) ;
590
604
}
591
605
606
+ function deferredClosePromise ( state ) {
607
+ return state . closePromise = new Promise ( ( resolve , reject ) => {
608
+ state . closePromiseResolve = resolve ;
609
+ state . closePromiseReject = reject ;
610
+ } ) . finally ( ( ) => {
611
+ state . closePromise = undefined ;
612
+ state . closePromiseResolve = undefined ;
613
+ state . closePromiseReject = undefined ;
614
+ } ) ;
615
+ }
616
+
592
617
// QuicEndpoint wraps a UDP socket and is owned
593
618
// by a QuicSocket. It does not exist independently
594
619
// of the QuicSocket.
595
620
class QuicEndpoint {
596
621
[ kInternalState ] = {
597
622
state : kSocketUnbound ,
623
+ bindPromise : undefined ,
624
+ closePromise : undefined ,
625
+ closePromiseResolve : undefined ,
626
+ closePromiseReject : undefined ,
598
627
socket : undefined ,
599
628
udpSocket : undefined ,
600
629
address : undefined ,
@@ -645,15 +674,14 @@ class QuicEndpoint {
645
674
return customInspect ( this , {
646
675
address : this . address ,
647
676
fd : this . fd ,
648
- type : this [ kInternalState ] . type === AF_INET6 ? 'udp6' : 'udp4'
677
+ type : this [ kInternalState ] . type === AF_INET6 ? 'udp6' : 'udp4' ,
678
+ destroyed : this . destroyed ,
679
+ bound : this . bound ,
680
+ pending : this . pending ,
649
681
} , depth , options ) ;
650
682
}
651
683
652
- // afterLookup is invoked when binding a QuicEndpoint. The first
653
- // step to binding is to resolve the given hostname into an ip
654
- // address. Once resolution is complete, the ip address needs to
655
- // be passed on to the [kContinueBind] function or the QuicEndpoint
656
- // needs to be destroyed.
684
+ // TODO(@jasnell): Remove once migration to Promise API is complete
657
685
static [ kAfterLookup ] ( err , ip ) {
658
686
if ( err ) {
659
687
this . destroy ( err ) ;
@@ -662,10 +690,7 @@ class QuicEndpoint {
662
690
this [ kContinueBind ] ( ip ) ;
663
691
}
664
692
665
- // kMaybeBind binds the endpoint on-demand if it is not already
666
- // bound. If it is bound, we return immediately, otherwise put
667
- // the endpoint into the pending state and initiate the binding
668
- // process by calling the lookup to resolve the IP address.
693
+ // TODO(@jasnell): Remove once migration to Promise API is complete
669
694
[ kMaybeBind ] ( ) {
670
695
const state = this [ kInternalState ] ;
671
696
if ( state . state !== kSocketUnbound )
@@ -674,8 +699,7 @@ class QuicEndpoint {
674
699
state . lookup ( state . address , QuicEndpoint [ kAfterLookup ] . bind ( this ) ) ;
675
700
}
676
701
677
- // IP address resolution is completed and we're ready to finish
678
- // binding to the local port.
702
+ // TODO(@jasnell): Remove once migration to Promise API is complete
679
703
[ kContinueBind ] ( ip ) {
680
704
const state = this [ kInternalState ] ;
681
705
const udpHandle = state . udpSocket [ internalDgram . kStateSymbol ] . handle ;
@@ -704,6 +728,95 @@ class QuicEndpoint {
704
728
state . socket [ kEndpointBound ] ( this ) ;
705
729
}
706
730
731
+ bind ( options ) {
732
+ const state = this [ kInternalState ] ;
733
+ if ( state . bindPromise !== undefined )
734
+ return state . bindPromise ;
735
+
736
+ return state . bindPromise = this [ kBind ] ( ) . finally ( ( ) => {
737
+ state . bindPromise = undefined ;
738
+ } ) ;
739
+ }
740
+
741
+ // Binds the QuicEndpoint to the local port. Returns a Promise
742
+ // that is resolved once the QuicEndpoint binds, or rejects if
743
+ // binding was not successful. Calling bind() multiple times
744
+ // before the Promise is resolved will return the same Promise.
745
+ // Calling bind() after the endpoint is already bound will
746
+ // immediately return a resolved promise. Calling bind() after
747
+ // the endpoint has been destroyed will cause the Promise to
748
+ // be rejected.
749
+ async [ kBind ] ( options ) {
750
+ const state = this [ kInternalState ] ;
751
+ if ( this . destroyed )
752
+ throw new ERR_INVALID_STATE ( 'QuicEndpoint is already destroyed' ) ;
753
+
754
+ if ( state . state !== kSocketUnbound )
755
+ return this . address ;
756
+
757
+ const { signal } = { ...options } ;
758
+ if ( signal != null && ! ( 'aborted' in signal ) )
759
+ throw new ERR_INVALID_ARG_TYPE ( 'options.signal' , 'AbortSignal' , signal ) ;
760
+
761
+ // If an AbotSignal was passed in, check to make sure it is not already
762
+ // aborted before we continue on to do any work.
763
+ if ( signal && signal . aborted )
764
+ throw new lazyDOMException ( 'AbortError' ) ;
765
+
766
+ state . state = kSocketPending ;
767
+
768
+ // TODO(@jasnell): Use passed in lookup function once everything
769
+ // has been converted to Promises-based API
770
+ const {
771
+ address : ip
772
+ } = await lookup ( state . address , state . type === AF_INET6 ? 6 : 4 ) ;
773
+
774
+ // It's possible for the QuicEndpoint to have been destroyed while
775
+ // we were waiting for the DNS lookup to complete. If so, reject
776
+ // the Promise.
777
+ if ( this . destroyed )
778
+ throw new ERR_INVALID_STATE ( 'QuicEndpoint was destroyed' ) ;
779
+
780
+ // If an AbortSignal was passed in, check to see if it was triggered
781
+ // while we were waiting.
782
+ if ( signal && signal . aborted ) {
783
+ state . state = kSocketUnbound ;
784
+ throw new lazyDOMException ( 'AbortError' ) ;
785
+ }
786
+
787
+ // From here on, any errors are fatal for the QuicEndpoint. Keep in
788
+ // mind that this means that the Bind Promise will be rejected *and*
789
+ // the QuicEndpoint will be destroyed with an error.
790
+ try {
791
+ const udpHandle = state . udpSocket [ internalDgram . kStateSymbol ] . handle ;
792
+ if ( udpHandle == null ) {
793
+ // It's not clear what cases trigger this but it is possible.
794
+ throw new ERR_OPERATION_FAILED ( 'Acquiring UDP socket handle failed' ) ;
795
+ }
796
+
797
+ const flags =
798
+ ( state . reuseAddr ? UV_UDP_REUSEADDR : 0 ) |
799
+ ( state . ipv6Only ? UV_UDP_IPV6ONLY : 0 ) ;
800
+
801
+ const ret = udpHandle . bind ( ip , state . port , flags ) ;
802
+ if ( ret )
803
+ throw exceptionWithHostPort ( ret , 'bind' , ip , state . port ) ;
804
+
805
+ // On Windows, the fd will be meaningless, but we always record it.
806
+ state . fd = udpHandle . fd ;
807
+ state . state = kSocketBound ;
808
+
809
+ // Notify the owning socket that the QuicEndpoint has been successfully
810
+ // bound to the local UDP port.
811
+ state . socket [ kEndpointBound ] ( this ) ;
812
+
813
+ return this . address ;
814
+ } catch ( error ) {
815
+ this . destroy ( error ) ;
816
+ throw error ;
817
+ }
818
+ }
819
+
707
820
destroy ( error ) {
708
821
if ( this . destroyed )
709
822
return ;
@@ -727,12 +840,35 @@ class QuicEndpoint {
727
840
handle . ondone = ( ) => {
728
841
state . udpSocket . close ( ( err ) => {
729
842
if ( err ) error = err ;
843
+ if ( error && typeof state . closePromiseReject === 'function' )
844
+ state . closePromiseReject ( error ) ;
845
+ else if ( typeof state . closePromiseResolve === 'function' )
846
+ state . closePromiseResolve ( ) ;
730
847
state . socket [ kEndpointClose ] ( this , error ) ;
731
848
} ) ;
732
849
} ;
733
850
handle . waitForPendingCallbacks ( ) ;
734
851
}
735
852
853
+ // Closes the QuicEndpoint. Returns a Promise that is resolved
854
+ // once the QuicEndpoint closes, or rejects if it closes with
855
+ // an error. Calling close() multiple times before the Promise
856
+ // is resolved will return the same Promise. Calling close()
857
+ // after will return a rejected Promise.
858
+ close ( ) {
859
+ return this [ kInternalState ] . closePromise || this [ kClose ] ( ) ;
860
+ }
861
+
862
+ [ kClose ] ( ) {
863
+ if ( this . destroyed ) {
864
+ return Promise . reject (
865
+ new ERR_INVALID_STATE ( 'QuicEndpoint is already destroyed' ) ) ;
866
+ }
867
+ const promise = deferredClosePromise ( this [ kInternalState ] ) ;
868
+ this . destroy ( ) ;
869
+ return promise ;
870
+ }
871
+
736
872
// If the QuicEndpoint is bound, returns an object detailing
737
873
// the local IP address, port, and address type to which it
738
874
// is bound. Otherwise, returns an empty object.
0 commit comments