Make TLS handshake a part of Happy Eyeballs
This commit is contained in:
parent
7abd2280bc
commit
18f9512a16
2 changed files with 68 additions and 24 deletions
17
patches/@types+node+18.15.11.patch
Normal file
17
patches/@types+node+18.15.11.patch
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
diff --git a/node_modules/@types/node/tls.d.ts b/node_modules/@types/node/tls.d.ts
|
||||||
|
index 2c55eb9..a594969 100755
|
||||||
|
--- a/node_modules/@types/node/tls.d.ts
|
||||||
|
+++ b/node_modules/@types/node/tls.d.ts
|
||||||
|
@@ -621,6 +621,12 @@ declare module 'tls' {
|
||||||
|
* `identity` must use UTF-8 encoding.
|
||||||
|
*/
|
||||||
|
pskCallback?(hint: string | null): PSKCallbackNegotation | null;
|
||||||
|
+
|
||||||
|
+ /* Node.js documentation says:
|
||||||
|
+ * "...: Any socket.connect() option not already listed."
|
||||||
|
+ * and "signal" is one of them.
|
||||||
|
+ */
|
||||||
|
+ signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Accepts encrypted connections using TLS or SSL.
|
|
@ -4,8 +4,9 @@
|
||||||
import { Agent as HTTPSAgent } from 'https';
|
import { Agent as HTTPSAgent } from 'https';
|
||||||
import type { AgentOptions, RequestOptions } from 'https';
|
import type { AgentOptions, RequestOptions } from 'https';
|
||||||
import type { LookupAddress } from 'dns';
|
import type { LookupAddress } from 'dns';
|
||||||
import net from 'net';
|
import type net from 'net';
|
||||||
import tls from 'tls';
|
import tls from 'tls';
|
||||||
|
import type { ConnectionOptions } from 'tls';
|
||||||
import { callbackify, promisify } from 'util';
|
import { callbackify, promisify } from 'util';
|
||||||
import pTimeout from 'p-timeout';
|
import pTimeout from 'p-timeout';
|
||||||
|
|
||||||
|
@ -16,9 +17,11 @@ import { parseIntOrThrow } from './parseIntOrThrow';
|
||||||
import { sleep } from './sleep';
|
import { sleep } from './sleep';
|
||||||
import { SECOND } from './durations';
|
import { SECOND } from './durations';
|
||||||
import { dropNull } from './dropNull';
|
import { dropNull } from './dropNull';
|
||||||
|
import { explodePromise } from './explodePromise';
|
||||||
|
|
||||||
// See https://www.rfc-editor.org/rfc/rfc8305#section-8
|
// https://www.rfc-editor.org/rfc/rfc8305#section-8 recommends 250ms, but since
|
||||||
const DELAY_MS = 250;
|
// we also try to establish a TLS session - use higher value.
|
||||||
|
const DELAY_MS = 500;
|
||||||
|
|
||||||
// Warning threshold
|
// Warning threshold
|
||||||
const CONNECT_THRESHOLD_MS = SECOND;
|
const CONNECT_THRESHOLD_MS = SECOND;
|
||||||
|
@ -83,16 +86,21 @@ export class Agent extends HTTPSAgent {
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
const { socket, address, v4Attempts, v6Attempts } = await happyEyeballs(
|
const { socket, address, v4Attempts, v6Attempts } = await happyEyeballs({
|
||||||
interleaved,
|
addrs: interleaved,
|
||||||
port
|
port,
|
||||||
);
|
tlsOptions: {
|
||||||
|
ca: options.ca,
|
||||||
|
host: dropNull(options.host),
|
||||||
|
servername: options.servername,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
const logLine =
|
const logLine =
|
||||||
`createHTTPSAgent.createConnection(${host}): connected to ` +
|
`createHTTPSAgent.createConnection(${host}): connected to ` +
|
||||||
`IPv${address.family} addr after ${duration}ms ` +
|
`IPv${address.family} addr after ${duration}ms ` +
|
||||||
`(v4_attempts=${v4Attempts} v6_attempts=${v6Attempts})`;
|
`(attempts v4=${v4Attempts} v6=${v6Attempts})`;
|
||||||
|
|
||||||
if (v4Attempts + v6Attempts > 1 || duration > CONNECT_THRESHOLD_MS) {
|
if (v4Attempts + v6Attempts > 1 || duration > CONNECT_THRESHOLD_MS) {
|
||||||
log.warn(logLine);
|
log.warn(logLine);
|
||||||
|
@ -100,16 +108,17 @@ export class Agent extends HTTPSAgent {
|
||||||
log.info(logLine);
|
log.info(logLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tls.connect({
|
return socket;
|
||||||
socket,
|
|
||||||
ca: options.ca,
|
|
||||||
host: dropNull(options.host),
|
|
||||||
servername: options.servername,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HappyEyeballsOptions = Readonly<{
|
||||||
|
addrs: ReadonlyArray<LookupAddress>;
|
||||||
|
port?: number;
|
||||||
|
tlsOptions: ConnectionOptions;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type HappyEyeballsResult = Readonly<{
|
export type HappyEyeballsResult = Readonly<{
|
||||||
socket: net.Socket;
|
socket: net.Socket;
|
||||||
address: LookupAddress;
|
address: LookupAddress;
|
||||||
|
@ -117,10 +126,11 @@ export type HappyEyeballsResult = Readonly<{
|
||||||
v6Attempts: number;
|
v6Attempts: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export async function happyEyeballs(
|
export async function happyEyeballs({
|
||||||
addrs: ReadonlyArray<LookupAddress>,
|
addrs,
|
||||||
port = 443
|
port = 443,
|
||||||
): Promise<HappyEyeballsResult> {
|
tlsOptions,
|
||||||
|
}: HappyEyeballsOptions): Promise<HappyEyeballsResult> {
|
||||||
const abortControllers = addrs.map(() => new AbortController());
|
const abortControllers = addrs.map(() => new AbortController());
|
||||||
|
|
||||||
let v4Attempts = 0;
|
let v4Attempts = 0;
|
||||||
|
@ -142,9 +152,14 @@ export async function happyEyeballs(
|
||||||
const socket = await connect({
|
const socket = await connect({
|
||||||
address: addr.address,
|
address: addr.address,
|
||||||
port,
|
port,
|
||||||
|
tlsOptions,
|
||||||
abortSignal: abortController.signal,
|
abortSignal: abortController.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (abortController.signal.aborted) {
|
||||||
|
throw new Error('Aborted');
|
||||||
|
}
|
||||||
|
|
||||||
// Abort other connection attempts
|
// Abort other connection attempts
|
||||||
for (const otherController of abortControllers) {
|
for (const otherController of abortControllers) {
|
||||||
if (otherController !== abortController) {
|
if (otherController !== abortController) {
|
||||||
|
@ -186,6 +201,7 @@ export async function happyEyeballs(
|
||||||
type DelayedConnectOptionsType = Readonly<{
|
type DelayedConnectOptionsType = Readonly<{
|
||||||
port: number;
|
port: number;
|
||||||
address: string;
|
address: string;
|
||||||
|
tlsOptions: ConnectionOptions;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}>;
|
}>;
|
||||||
|
@ -193,20 +209,31 @@ type DelayedConnectOptionsType = Readonly<{
|
||||||
async function connect({
|
async function connect({
|
||||||
port,
|
port,
|
||||||
address,
|
address,
|
||||||
|
tlsOptions,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
timeout = CONNECT_TIMEOUT_MS,
|
timeout = CONNECT_TIMEOUT_MS,
|
||||||
}: DelayedConnectOptionsType): Promise<net.Socket> {
|
}: DelayedConnectOptionsType): Promise<net.Socket> {
|
||||||
const socket = new net.Socket({
|
const socket = tls.connect(port, address, {
|
||||||
|
...tlsOptions,
|
||||||
signal: abortSignal,
|
signal: abortSignal,
|
||||||
});
|
});
|
||||||
|
|
||||||
return pTimeout(
|
return pTimeout(
|
||||||
new Promise((resolve, reject) => {
|
(async () => {
|
||||||
socket.once('connect', () => resolve(socket));
|
const { promise: onHandshake, resolve, reject } = explodePromise<void>();
|
||||||
socket.once('error', err => reject(err));
|
|
||||||
|
|
||||||
socket.connect(port, address);
|
socket.once('secureConnect', resolve);
|
||||||
}),
|
socket.once('error', reject);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onHandshake;
|
||||||
|
} finally {
|
||||||
|
socket.removeListener('secureConnect', resolve);
|
||||||
|
socket.removeListener('error', reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
})(),
|
||||||
timeout,
|
timeout,
|
||||||
'createHTTPSAgent.connect: connection timed out'
|
'createHTTPSAgent.connect: connection timed out'
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue