2023-01-05 23:39:49 +01: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' ;
2024-07-25 18:55:47 +02:00
import { assert } from './debug' ;
2024-09-17 16:11:21 +02:00
import { monotonicTime } from './time' ;
2023-01-05 23:39:49 +01: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 ;
2024-09-17 16:11:21 +02:00
const kDNSLookupAt = Symbol ( 'kDNSLookupAt' )
const kTCPConnectionAt = Symbol ( 'kTCPConnectionAt' )
2023-01-05 23:39:49 +01:00
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 23:39:49 +01: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 23:39:49 +01: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 23:39:49 +01: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 23:39:49 +01:00
}
}
2024-06-25 19:05:32 +02:00
// These options are aligned with the default Node.js globalAgent options.
export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent ( { keepAlive : true } ) ;
export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent ( { keepAlive : true } ) ;
2023-01-05 23:39:49 +01:00
2023-03-21 22:12:24 +01: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 ) ) ;
}
} ) ;
}
2024-07-25 18:55:47 +02:00
export async function createTLSSocket ( options : tls.ConnectionOptions ) : Promise < tls.TLSSocket > {
return new Promise ( ( resolve , reject ) = > {
assert ( options . host , 'host is required' ) ;
if ( net . isIP ( options . host ) ) {
const socket = tls . connect ( options )
2024-08-15 08:51:40 +02:00
socket . on ( 'secureConnect' , ( ) = > resolve ( socket ) ) ;
2024-07-25 18:55:47 +02:00
socket . on ( 'error' , error = > reject ( error ) ) ;
} else {
createConnectionAsync ( options , ( err , socket ) = > {
if ( err )
reject ( err ) ;
2024-08-15 08:51:40 +02:00
if ( socket ) {
socket . on ( 'secureConnect' , ( ) = > resolve ( socket ) ) ;
socket . on ( 'error' , error = > reject ( error ) ) ;
}
2024-07-25 18:55:47 +02:00
} , true ) . catch ( err = > reject ( err ) ) ;
}
} ) ;
}
export async function createConnectionAsync (
options : http.ClientRequestArgs ,
oncreate : ( ( err : Error | null , socket? : tls.TLSSocket ) = > void ) | undefined ,
useTLS : true
) : Promise < void > ;
export async function createConnectionAsync (
options : http.ClientRequestArgs ,
oncreate : ( ( err : Error | null , socket? : net.Socket ) = > void ) | undefined ,
useTLS : false
) : Promise < void > ;
export async function createConnectionAsync (
options : http.ClientRequestArgs ,
oncreate : ( ( err : Error | null , socket? : any ) = > void ) | undefined ,
useTLS : boolean
) : Promise < void > {
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 ) ;
2024-09-17 16:11:21 +02:00
const dnsLookupAt = monotonicTime ( ) ;
2023-01-05 23:39:49 +01: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 23:39:49 +01: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 23:39:49 +01:00
net . createConnection ( {
. . . options ,
port : options.port as number ,
host : address } ) ;
2024-09-17 16:11:21 +02:00
( socket as any ) [ kDNSLookupAt ] = dnsLookupAt ;
2023-01-05 23:39:49 +01:00
// 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' , ( ) = > {
2024-09-17 16:11:21 +02:00
( socket as any ) [ kTCPConnectionAt ] = monotonicTime ( ) ;
2023-01-05 23:39:49 +01:00
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-05 04:56:42 +02:00
// Alternate ipv6 and ipv4 addresses.
2023-01-05 23:39:49 +01: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 21:14:50 +01:00
return options . host ;
2023-01-30 17:44:26 +01:00
throw new Error ( 'Either options.hostname or options.host must be provided' ) ;
}
2024-09-17 16:11:21 +02:00
export function timingForSocket ( socket : net.Socket | tls . TLSSocket ) {
return {
dnsLookupAt : ( socket as any ) [ kDNSLookupAt ] as number | undefined ,
tcpConnectionAt : ( socket as any ) [ kTCPConnectionAt ] as number | undefined ,
}
}