2023-01-05 14:39:49 -08:00
/ * *
* Copyright ( c ) Microsoft Corporation .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
import * as dns from 'dns' ;
import * as http from 'http' ;
import * as https from 'https' ;
import * as net from 'net' ;
import * as tls from 'tls' ;
2023-02-22 17:09:56 +01:00
import { ManualPromise } from './manualPromise' ;
2023-01-05 14:39:49 -08:00
// Implementation(partial) of Happy Eyeballs 2 algorithm described in
// https://www.rfc-editor.org/rfc/rfc8305
// Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102)
const connectionAttemptDelayMs = 300 ;
class HttpHappyEyeballsAgent extends http . Agent {
createConnection ( options : http.ClientRequestArgs , oncreate ? : ( err : Error | null , socket? : net.Socket ) = > void ) : net . Socket | undefined {
// There is no ambiguity in case of IP address.
2023-01-30 17:44:26 +01:00
if ( net . isIP ( clientRequestArgsToHostName ( options ) ) )
2023-01-05 14:39:49 -08:00
return net . createConnection ( options as net . NetConnectOpts ) ;
2023-01-30 17:44:26 +01:00
createConnectionAsync ( options , oncreate , /* useTLS */ false ) . catch ( err = > oncreate ? . ( err ) ) ;
2023-01-05 14:39:49 -08:00
}
}
class HttpsHappyEyeballsAgent extends https . Agent {
createConnection ( options : http.ClientRequestArgs , oncreate ? : ( err : Error | null , socket? : net.Socket ) = > void ) : net . Socket | undefined {
// There is no ambiguity in case of IP address.
2023-01-30 17:44:26 +01:00
if ( net . isIP ( clientRequestArgsToHostName ( options ) ) )
2023-01-05 14:39:49 -08:00
return tls . connect ( options as tls . ConnectionOptions ) ;
2023-01-30 17:44:26 +01:00
createConnectionAsync ( options , oncreate , /* useTLS */ true ) . catch ( err = > oncreate ? . ( err ) ) ;
2023-01-05 14:39:49 -08:00
}
}
export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent ( ) ;
export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent ( ) ;
2023-03-21 14:12:24 -07:00
export async function createSocket ( host : string , port : number ) : Promise < net.Socket > {
return new Promise ( ( resolve , reject ) = > {
if ( net . isIP ( host ) ) {
const socket = net . createConnection ( { host , port } ) ;
socket . on ( 'connect' , ( ) = > resolve ( socket ) ) ;
socket . on ( 'error' , error = > reject ( error ) ) ;
} else {
createConnectionAsync ( { host , port } , ( err , socket ) = > {
if ( err )
reject ( err ) ;
if ( socket )
resolve ( socket ) ;
} , /* useTLS */ false ) . catch ( err = > reject ( err ) ) ;
}
} ) ;
}
2023-01-30 17:44:26 +01:00
async function createConnectionAsync ( options : http.ClientRequestArgs , oncreate : ( ( err : Error | null , socket? : net.Socket ) = > void ) | undefined , useTLS : boolean ) {
2023-02-22 17:09:56 +01:00
const lookup = ( options as any ) . __testHookLookup || lookupAddresses ;
2023-01-30 17:44:26 +01:00
const hostname = clientRequestArgsToHostName ( options ) ;
const addresses = await lookup ( hostname ) ;
2023-01-05 14:39:49 -08:00
const sockets = new Set < net.Socket > ( ) ;
let firstError ;
let errorCount = 0 ;
const handleError = ( socket : net.Socket , err : Error ) = > {
if ( ! sockets . delete ( socket ) )
return ;
++ errorCount ;
firstError ? ? = err ;
if ( errorCount === addresses . length )
oncreate ? . ( firstError ) ;
} ;
const connected = new ManualPromise ( ) ;
for ( const { address } of addresses ) {
2023-01-30 17:44:26 +01:00
const socket = useTLS ?
2023-01-05 14:39:49 -08:00
tls . connect ( {
. . . ( options as tls . ConnectionOptions ) ,
port : options.port as number ,
host : address ,
2023-01-30 17:44:26 +01:00
servername : hostname } ) :
2023-01-05 14:39:49 -08:00
net . createConnection ( {
. . . options ,
port : options.port as number ,
host : address } ) ;
// Each socket may fire only one of 'connect', 'timeout' or 'error' events.
// None of these events are fired after socket.destroy() is called.
socket . on ( 'connect' , ( ) = > {
connected . resolve ( ) ;
oncreate ? . ( null , socket ) ;
// TODO: Cache the result?
// Close other outstanding sockets.
sockets . delete ( socket ) ;
for ( const s of sockets )
s . destroy ( ) ;
sockets . clear ( ) ;
} ) ;
socket . on ( 'timeout' , ( ) = > {
// Timeout is not an error, so we have to manually close the socket.
socket . destroy ( ) ;
handleError ( socket , new Error ( 'Connection timeout' ) ) ;
} ) ;
socket . on ( 'error' , e = > handleError ( socket , e ) ) ;
sockets . add ( socket ) ;
await Promise . race ( [
connected ,
new Promise ( f = > setTimeout ( f , connectionAttemptDelayMs ) )
] ) ;
if ( connected . isDone ( ) )
break ;
}
}
async function lookupAddresses ( hostname : string ) : Promise < dns.LookupAddress [ ] > {
const addresses = await dns . promises . lookup ( hostname , { all : true , family : 0 , verbatim : true } ) ;
let firstFamily = addresses . filter ( ( { family } ) = > family === 6 ) ;
let secondFamily = addresses . filter ( ( { family } ) = > family === 4 ) ;
// Make sure first address in the list is the same as in the original order.
if ( firstFamily . length && firstFamily [ 0 ] !== addresses [ 0 ] ) {
const tmp = firstFamily ;
firstFamily = secondFamily ;
secondFamily = tmp ;
}
const result = [ ] ;
2023-10-04 22:56:42 -04:00
// Alternate ipv6 and ipv4 addresses.
2023-01-05 14:39:49 -08:00
for ( let i = 0 ; i < Math . max ( firstFamily . length , secondFamily . length ) ; i ++ ) {
if ( firstFamily [ i ] )
result . push ( firstFamily [ i ] ) ;
if ( secondFamily [ i ] )
result . push ( secondFamily [ i ] ) ;
}
return result ;
}
2023-01-30 17:44:26 +01:00
function clientRequestArgsToHostName ( options : http.ClientRequestArgs ) : string {
if ( options . hostname )
return options . hostname ;
if ( options . host )
2023-03-21 13:14:50 -07:00
return options . host ;
2023-01-30 17:44:26 +01:00
throw new Error ( 'Either options.hostname or options.host must be provided' ) ;
}