All files / src/utils network-dispatcher.ts

100% Statements 46/46
100% Branches 30/30
100% Functions 6/6
100% Lines 40/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106265x   265x 265x   265x                                   265x         140x 97x 97x 6x 6x 5x   1x   6x         44x 44x 37x 36x 9x 9x 9x   7x         265x           140x 140x             140x 140x 10x 10x   130x 34x     34x 36x 34x                       34x     20x 20x 2x   18x     106x 106x    
import * as fs from 'node:fs';
import type { Dispatcher } from 'undici';
import { GITLAB_CA_CERT_PATH, HTTP_PROXY, HTTPS_PROXY, NO_PROXY } from '../config';
import { logInfo, logWarn } from '../logger';
 
const undici = require('undici') as {
  Agent: new (options: Record<string, unknown>) => unknown;
  Pool: new (origin: string, options: Record<string, unknown>) => unknown;
  EnvHttpProxyAgent: new (options: Record<string, unknown>) => Dispatcher;
};
 
interface NetworkOptions {
  origin?: string;
  insecureSkipVerify?: boolean;
  connectTimeout: number;
  headersTimeout: number;
  bodyTimeout: number;
  connections?: number;
  keepAliveTimeout?: number;
  keepAliveMaxTimeout?: number;
  pipelining?: number;
}
 
let certificateLoaded = false;
let certificate: Buffer | undefined;
 
/** Load immutable process-level trust configuration once, before creating dispatchers. */
function loadCertificate(): Buffer | undefined {
  if (certificateLoaded) return certificate;
  certificateLoaded = true;
  if (!GITLAB_CA_CERT_PATH) return undefined;
  try {
    certificate = fs.readFileSync(GITLAB_CA_CERT_PATH);
    logInfo(`Custom CA certificate loaded from ${GITLAB_CA_CERT_PATH}`);
  } catch (error: unknown) {
    logWarn(`Failed to load CA certificate from ${GITLAB_CA_CERT_PATH}`, { err: error });
  }
  return certificate;
}
 
/** Remove proxy userinfo from diagnostics without altering connection configuration. */
function proxyUrlForLogging(value: string): string {
  try {
    const parsed = new URL(value);
    if (!parsed.hostname) return '<invalid proxy URL>';
    if (!parsed.username && !parsed.password) return value;
    parsed.username = '';
    parsed.password = '';
    return parsed.toString();
  } catch {
    return '<invalid proxy URL>';
  }
}
 
/** Share proxy routing, GitLab trust and timeouts between startup and instance transports. */
export function createNetworkDispatcher({
  origin,
  insecureSkipVerify,
  connectTimeout,
  ...options
}: NetworkOptions): unknown {
  const ca = loadCertificate();
  const connect = {
    ...(insecureSkipVerify ? { rejectUnauthorized: false } : {}),
    ...(ca ? { ca } : {}),
    timeout: connectTimeout,
    keepAlive: true,
    keepAliveInitialDelay: 30000,
  };
  const proxyUrl = (HTTPS_PROXY === '' ? undefined : HTTPS_PROXY) ?? HTTP_PROXY;
  if (proxyUrl && /^(socks4?|socks5):\/\//.test(proxyUrl)) {
    logInfo(`Using SOCKS proxy: ${proxyUrlForLogging(proxyUrl)}`);
    logWarn('SOCKS proxy not supported with native fetch. Consider HTTP/HTTPS proxy.');
    // SOCKS is unavailable; direct connections still retain GitLab trust and timeouts.
  } else if (proxyUrl) {
    logInfo(`Using proxy: ${proxyUrlForLogging(proxyUrl)}`);
    // Undici's HTTP forwarding and CONNECT-client factories receive only connect options.
    // Carry pool limits and response timeouts into those pools as well as target pools.
    const factory = (poolOrigin: string | URL, poolOptions: Record<string, unknown>) =>
      new undici.Pool(String(poolOrigin), { ...options, ...poolOptions });
    const agent = new undici.EnvHttpProxyAgent({
      ...options,
      httpProxy: HTTP_PROXY,
      httpsProxy: HTTPS_PROXY,
      noProxy: NO_PROXY,
      factory,
      clientFactory: factory,
      connect,
      requestTls: connect,
      // Instance TLS overrides apply to GitLab, not the separate proxy trust boundary.
      proxyTls: { timeout: connectTimeout },
    });
    if (!origin) return agent;
    // Unlike a Pool, an Agent accepts arbitrary origins. Keep instance credentials
    // confined to GitLab even when fetch follows redirects or NO_PROXY bypasses the proxy.
    return agent.compose((dispatch) => (request, handler) => {
      if (new URL(String(request.origin)).origin !== origin) {
        throw new Error('Request origin does not match the configured GitLab instance');
      }
      return dispatch(request, handler);
    });
  }
  const directOptions = { ...options, connect };
  return origin ? new undici.Pool(origin, directOptions) : new undici.Agent(directOptions);
}