'use strict' ; import { debuglog } from 'util' ; import { MaxReceivePacketSize , MaxPacketSizeIPv4 , MaxPacketSizeIPv6 } from './internal/constant' ; import { dnsLookup , BufferVisitor } from './internal/common' ; import { QuicError , QUICError } from './internal/error' ; import { parsePacket , NegotiationPacket , RegularPacket } from './internal/packet' ; import { FamilyType , ConnectionID , SocketAddress , SessionType , getVersion , chooseVersion , } from './internal/protocol' ; import { kHS , kSocket , kState , kVersion , kClientState , kIntervalCheck , kUnackedPackets , } from './internal/symbol' ; import { createSocket , AddressInfo , Socket } from './socket' ; import { ClientHandShake } from './handshake' ; import { Session } from './session' ; import { Stream } from './stream' ; const debug = debuglog ( 'quic' ) ; export declare interface Client { addListener ( event , listener ) : this ; addListener ( event , listener ) : this ; addListener ( event , listener ) : this ; addListener ( event , listener ) : this ; addListener ( event , listener ) : this ; addListener ( event , listener ) : this ; emit ( event , err ) : boolean ; emit ( event , err ) : boolean ; emit ( event , err ? ) : boolean ; emit ( event ) : boolean ; emit ( event , stream ) : boolean ; emit ( event , ver ) : boolean ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; once ( event , listener ) : this ; once ( event , listener ) : this ; once ( event , listener ) : this ; once ( event , listener ) : this ; once ( event , listener ) : this ; once ( event , listener ) : this ; } export class Client extends Session { [ kClientState ] : ClientState ; [ kHS ] : ClientHandShake ; constructor ( ) { super ( ConnectionID . random ( ) , SessionType . CLIENT ) ; this [ kVersion ] = getVersion ( ) ; this [ kHS ] = new ClientHandShake ( this ) ; this [ kClientState ] = new ClientState ( ) ; this [ kIntervalCheck ] = setInterval ( ( ) => { const time = Date . now ( ) ; if ( time - this [ kState ] . lastNetworkActivityTime > this [ kState ] . idleTimeout ) { this . emit ( 'timeout' ) ; this . close ( QuicError . fromError ( QuicError . QUIC_NETWORK_IDLE_TIMEOUT ) ) ; return ; } this . _intervalCheck ( time ) ; } , 512 ) ; } _resendPacketsForNegotiation ( ) { const packets = this [ kUnackedPackets ] . toArray ( ) ; this [ kUnackedPackets ] . reset ( ) ; for ( const packet of packets ) { this . _sendPacket ( packet , ( err ) => { if ( err != null && ! this . destroyed ) { this . destroy ( err ) ; } } ) ; } } setKeepAlive ( enable , _initialDelay ? ) { this [ kState ] . keepAlivePingSent = enable ; } ref ( ) { const socket = this [ kSocket ] ; if ( socket == null ) { throw new Error ( 'Client not connect' ) ; } socket . ref ( ) ; } unref ( ) { const socket = this [ kSocket ] ; if ( socket == null ) { throw new Error ( 'Client not connect' ) ; } socket . unref ( ) ; } async spawn ( port , address = 'localhost' ) { if ( this [ kState ] . destroyed ) { throw new Error ( 'Client destroyed' ) ; } const socket = this [ kSocket ] ; if ( socket == null || socket [ kState ] . destroyed ) { throw new Error ( 'the underlying socket destroyed' ) ; } const addr = await dnsLookup ( address ) ; debug ( `client connect: %s, %d, %j` , address , port , addr ) ; const client = new Client ( ) ; socket [ kState ] . conns . set ( client . id , client ) ; socket [ kState ] . exclusive = false ; client [ kSocket ] = socket ; client [ kState ] . localFamily = this [ kState ] . localFamily ; client [ kState ] . localAddress = this [ kState ] . localAddress ; client [ kState ] . localPort = this [ kState ] . localPort ; client [ kState ] . localAddr = new SocketAddress ( socket . address ( ) as AddressInfo ) ; client [ kState ] . remotePort = port ; client [ kState ] . remoteAddress = addr . address ; client [ kState ] . remoteFamily = 'IPv' + addr . family ; client [ kState ] . remoteAddr = new SocketAddress ( { port , address : addr . address , family : `IPv${ addr . family }` } ) ; client [ kState ] . maxPacketSize = this [ kState ] . maxPacketSize ; await new Promise ( ( resolve , reject ) => { client [ kHS ] . once ( 'secureConnection' , ( ) => { client . removeListener ( 'error' , reject ) ; resolve ( ) ; } ) ; client . once ( 'error' , reject ) ; client [ kHS ] . setup ( ) ; } ) ; return client ; } async connect ( port , address = 'localhost' ) { if ( this [ kState ] . destroyed ) { throw new Error ( 'Client destroyed' ) ; } if ( this [ kSocket ] != null ) { throw new Error ( 'Client connecting duplicated' ) ; } const addr = await dnsLookup ( address ) ; debug ( `client connect: %s, %d, %j` , address , port , addr ) ; this [ kState ] . remotePort = port ; this [ kState ] . remoteAddress = addr . address ; this [ kState ] . remoteFamily = 'IPv' + addr . family ; this [ kState ] . remoteAddr = new SocketAddress ( { port , address : addr . address , family : `IPv${ addr . family }` } ) ; this [ kState ] . maxPacketSize = this [ kState ] . localFamily === FamilyType . IPv6 ? MaxPacketSizeIPv6 : MaxPacketSizeIPv4 ; const socket = this [ kSocket ] = createSocket < Client > ( addr . family ) ; socket [ kState ] . conns . set ( this . id , this ) ; socket . on ( 'error' , ( err ) => this . emit ( 'error' , err ) ) . on ( 'close' , ( ) => this . destroy ( new Error ( 'the underlying socket closed' ) ) ) . on ( 'message' , socketOnMessage ) ; const res = new Promise ( ( resolve , reject ) => { socket . once ( 'listening' , ( ) => { socket . removeListener ( 'error' , reject ) ; const localAddr = socket . address ( ) as AddressInfo ; this [ kState ] . localFamily = localAddr . family ; this [ kState ] . localAddress = localAddr . address ; this [ kState ] . localPort = localAddr . port ; this [ kState ] . localAddr = new SocketAddress ( localAddr ) ; this [ kHS ] . once ( 'secureConnection' , ( ) => { this . removeListener ( 'error' , reject ) ; process . nextTick ( ( ) => this . emit ( 'connect' ) ) ; resolve ( ) ; } ) ; this [ kHS ] . setup ( ) ; } ) ; this . once ( 'error' , reject ) ; socket . once ( 'error' , reject ) ; } ) ; socket . bind ( { exclusive : true , port : 0 } ) ; await res ; } } export class ClientState { hostname : string ; receivedNegotiationPacket : boolean ; constructor ( ) { this . hostname = '' ; this . receivedNegotiationPacket = false ; } } function socketOnMessage ( this , msg , rinfo ) { if ( msg . length === 0 || this [ kState ] . destroyed ) { return ; } if ( msg . length > MaxReceivePacketSize ) { debug ( `client message - receive too large data: %d bytes` , msg . length ) ; } const senderAddr = new SocketAddress ( rinfo ) ; const rcvTime = Date . now ( ) ; const bufv = new BufferVisitor ( msg ) ; let packet = null ; try { packet = parsePacket ( bufv , SessionType . SERVER ) ; } catch ( err ) { debug ( `client message - parsing packet error: %o` , err ) ; return ; } const connectionID = packet . connectionID . valueOf ( ) ; const client = this [ kState ] . conns . get ( connectionID ) ; if ( client == null ) { debug ( `client message - received a spoofed packet with wrong ID: %s` , connectionID ) ; return ; } else if ( client . destroyed ) { return ; } if ( packet . isReset ( ) ) { const remoteAddr = client [ kState ] . remoteAddr ; if ( remoteAddr == null || ! remoteAddr . equals ( senderAddr ) ) { debug ( `session %s - received a spoofed Public Reset: %j` , client . id , senderAddr ) ; return ; } debug ( `session %s - Public Reset, rejected packet number: %j` , client . id , packet ) ; client . destroy ( QuicError . fromError ( QuicError . QUIC_PUBLIC_RESET ) ) ; return ; } if ( packet . isNegotiation ( ) ) { if ( client [ kClientState ] . receivedNegotiationPacket || client [ kState ] . versionNegotiated ) { return ; } const versions = ( packet as NegotiationPacket ) . versions ; if ( client [ kVersion ] !== '' && versions . includes ( client [ kVersion ] ) ) { return ; } const newVersion = chooseVersion ( versions ) ; client [ kClientState ] . receivedNegotiationPacket = true ; debug ( `session %s - received Public Negotiation: %s` , client . id , newVersion ) ; if ( newVersion !== '' ) { client [ kVersion ] = newVersion ; client . _resendPacketsForNegotiation ( ) ; } else { client . destroy ( QuicError . fromError ( QuicError . QUIC_INVALID_VERSION ) ) ; } return ; } if ( ! client [ kState ] . versionNegotiated ) { client [ kState ] . versionNegotiated = true ; client . emit ( 'version' , client . version ) ; } client [ kState ] . bytesRead += msg . length ; try { client . _handleRegularPacket ( packet as RegularPacket , rcvTime , bufv ) ; } catch ( err ) { debug ( `CLIENT session %s - handle RegularPacket error: %o` , client . id , err ) ; client . destroy ( QuicError . fromError ( err ) ) ; } }