source dump of claude code
at main 426 lines 14 kB view raw
1// @aws-sdk/credential-provider-node and @smithy/node-http-handler are imported 2// dynamically in getAWSClientProxyConfig() to defer ~929KB of AWS SDK. 3// undici is lazy-required inside getProxyAgent/configureGlobalAgents to defer 4// ~1.5MB when no HTTPS_PROXY/mTLS env vars are set (the common case). 5import axios, { type AxiosInstance } from 'axios' 6import type { LookupOptions } from 'dns' 7import type { Agent } from 'http' 8import { HttpsProxyAgent, type HttpsProxyAgentOptions } from 'https-proxy-agent' 9import memoize from 'lodash-es/memoize.js' 10import type * as undici from 'undici' 11import { getCACertificates } from './caCerts.js' 12import { logForDebugging } from './debug.js' 13import { isEnvTruthy } from './envUtils.js' 14import { 15 getMTLSAgent, 16 getMTLSConfig, 17 getTLSFetchOptions, 18 type TLSConfig, 19} from './mtls.js' 20 21// Disable fetch keep-alive after a stale-pool ECONNRESET so retries open a 22// fresh TCP connection instead of reusing the dead pooled socket. Sticky for 23// the process lifetime — once the pool is known-bad, don't trust it again. 24// Works under Bun (native fetch respects keepalive:false for pooling). 25// Under Node/undici, keepalive is a no-op for pooling, but undici 26// naturally evicts dead sockets from the pool on ECONNRESET. 27let keepAliveDisabled = false 28 29export function disableKeepAlive(): void { 30 keepAliveDisabled = true 31} 32 33export function _resetKeepAliveForTesting(): void { 34 keepAliveDisabled = false 35} 36 37/** 38 * Convert dns.LookupOptions.family to a numeric address family value 39 * Handles: 0 | 4 | 6 | 'IPv4' | 'IPv6' | undefined 40 */ 41export function getAddressFamily(options: LookupOptions): 0 | 4 | 6 { 42 switch (options.family) { 43 case 0: 44 case 4: 45 case 6: 46 return options.family 47 case 'IPv6': 48 return 6 49 case 'IPv4': 50 case undefined: 51 return 4 52 default: 53 throw new Error(`Unsupported address family: ${options.family}`) 54 } 55} 56 57type EnvLike = Record<string, string | undefined> 58 59/** 60 * Get the active proxy URL if one is configured 61 * Prefers lowercase variants over uppercase (https_proxy > HTTPS_PROXY > http_proxy > HTTP_PROXY) 62 * @param env Environment variables to check (defaults to process.env for production use) 63 */ 64export function getProxyUrl(env: EnvLike = process.env): string | undefined { 65 return env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY 66} 67 68/** 69 * Get the NO_PROXY environment variable value 70 * Prefers lowercase over uppercase (no_proxy > NO_PROXY) 71 * @param env Environment variables to check (defaults to process.env for production use) 72 */ 73export function getNoProxy(env: EnvLike = process.env): string | undefined { 74 return env.no_proxy || env.NO_PROXY 75} 76 77/** 78 * Check if a URL should bypass the proxy based on NO_PROXY environment variable 79 * Supports: 80 * - Exact hostname matches (e.g., "localhost") 81 * - Domain suffix matches with leading dot (e.g., ".example.com") 82 * - Wildcard "*" to bypass all 83 * - Port-specific matches (e.g., "example.com:8080") 84 * - IP addresses (e.g., "127.0.0.1") 85 * @param urlString URL to check 86 * @param noProxy NO_PROXY value (defaults to getNoProxy() for production use) 87 */ 88export function shouldBypassProxy( 89 urlString: string, 90 noProxy: string | undefined = getNoProxy(), 91): boolean { 92 if (!noProxy) return false 93 94 // Handle wildcard 95 if (noProxy === '*') return true 96 97 try { 98 const url = new URL(urlString) 99 const hostname = url.hostname.toLowerCase() 100 const port = url.port || (url.protocol === 'https:' ? '443' : '80') 101 const hostWithPort = `${hostname}:${port}` 102 103 // Split by comma or space and trim each entry 104 const noProxyList = noProxy.split(/[,\s]+/).filter(Boolean) 105 106 return noProxyList.some(pattern => { 107 pattern = pattern.toLowerCase().trim() 108 109 // Check for port-specific match 110 if (pattern.includes(':')) { 111 return hostWithPort === pattern 112 } 113 114 // Check for domain suffix match (with or without leading dot) 115 if (pattern.startsWith('.')) { 116 // Pattern ".example.com" should match "sub.example.com" and "example.com" 117 // but NOT "notexample.com" 118 const suffix = pattern 119 return hostname === pattern.substring(1) || hostname.endsWith(suffix) 120 } 121 122 // Check for exact hostname match or IP address 123 return hostname === pattern 124 }) 125 } catch { 126 // If URL parsing fails, don't bypass proxy 127 return false 128 } 129} 130 131/** 132 * Create an HttpsProxyAgent with optional mTLS configuration 133 * Skips local DNS resolution to let the proxy handle it 134 */ 135function createHttpsProxyAgent( 136 proxyUrl: string, 137 extra: HttpsProxyAgentOptions<string> = {}, 138): HttpsProxyAgent<string> { 139 const mtlsConfig = getMTLSConfig() 140 const caCerts = getCACertificates() 141 142 const agentOptions: HttpsProxyAgentOptions<string> = { 143 ...(mtlsConfig && { 144 cert: mtlsConfig.cert, 145 key: mtlsConfig.key, 146 passphrase: mtlsConfig.passphrase, 147 }), 148 ...(caCerts && { ca: caCerts }), 149 } 150 151 if (isEnvTruthy(process.env.CLAUDE_CODE_PROXY_RESOLVES_HOSTS)) { 152 // Skip local DNS resolution - let the proxy resolve hostnames 153 // This is needed for environments where DNS is not configured locally 154 // and instead handled by the proxy (as in sandboxes) 155 agentOptions.lookup = (hostname, options, callback) => { 156 callback(null, hostname, getAddressFamily(options)) 157 } 158 } 159 160 return new HttpsProxyAgent(proxyUrl, { ...agentOptions, ...extra }) 161} 162 163/** 164 * Axios instance with its own proxy agent. Same NO_PROXY/mTLS/CA 165 * resolution as the global interceptor, but agent options stay 166 * scoped to this instance. 167 */ 168export function createAxiosInstance( 169 extra: HttpsProxyAgentOptions<string> = {}, 170): AxiosInstance { 171 const proxyUrl = getProxyUrl() 172 const mtlsAgent = getMTLSAgent() 173 const instance = axios.create({ proxy: false }) 174 175 if (!proxyUrl) { 176 if (mtlsAgent) instance.defaults.httpsAgent = mtlsAgent 177 return instance 178 } 179 180 const proxyAgent = createHttpsProxyAgent(proxyUrl, extra) 181 instance.interceptors.request.use(config => { 182 if (config.url && shouldBypassProxy(config.url)) { 183 config.httpsAgent = mtlsAgent 184 config.httpAgent = mtlsAgent 185 } else { 186 config.httpsAgent = proxyAgent 187 config.httpAgent = proxyAgent 188 } 189 return config 190 }) 191 return instance 192} 193 194/** 195 * Get or create a memoized proxy agent for the given URI 196 * Now respects NO_PROXY environment variable 197 */ 198export const getProxyAgent = memoize((uri: string): undici.Dispatcher => { 199 // eslint-disable-next-line @typescript-eslint/no-require-imports 200 const undiciMod = require('undici') as typeof undici 201 const mtlsConfig = getMTLSConfig() 202 const caCerts = getCACertificates() 203 204 // Use EnvHttpProxyAgent to respect NO_PROXY 205 // This agent automatically checks NO_PROXY for each request 206 const proxyOptions: undici.EnvHttpProxyAgent.Options & { 207 requestTls?: { 208 cert?: string | Buffer 209 key?: string | Buffer 210 passphrase?: string 211 ca?: string | string[] | Buffer 212 } 213 } = { 214 // Override both HTTP and HTTPS proxy with the provided URI 215 httpProxy: uri, 216 httpsProxy: uri, 217 noProxy: process.env.NO_PROXY || process.env.no_proxy, 218 } 219 220 // Set both connect and requestTls so TLS options apply to both paths: 221 // - requestTls: used by ProxyAgent for the TLS connection through CONNECT tunnels 222 // - connect: used by Agent for direct (no-proxy) connections 223 if (mtlsConfig || caCerts) { 224 const tlsOpts = { 225 ...(mtlsConfig && { 226 cert: mtlsConfig.cert, 227 key: mtlsConfig.key, 228 passphrase: mtlsConfig.passphrase, 229 }), 230 ...(caCerts && { ca: caCerts }), 231 } 232 proxyOptions.connect = tlsOpts 233 proxyOptions.requestTls = tlsOpts 234 } 235 236 return new undiciMod.EnvHttpProxyAgent(proxyOptions) 237}) 238 239/** 240 * Get an HTTP agent configured for WebSocket proxy support 241 * Returns undefined if no proxy is configured or URL should bypass proxy 242 */ 243export function getWebSocketProxyAgent(url: string): Agent | undefined { 244 const proxyUrl = getProxyUrl() 245 246 if (!proxyUrl) { 247 return undefined 248 } 249 250 // Check if URL should bypass proxy 251 if (shouldBypassProxy(url)) { 252 return undefined 253 } 254 255 return createHttpsProxyAgent(proxyUrl) 256} 257 258/** 259 * Get the proxy URL for WebSocket connections under Bun. 260 * Bun's native WebSocket supports a `proxy` string option instead of Node's `agent`. 261 * Returns undefined if no proxy is configured or URL should bypass proxy. 262 */ 263export function getWebSocketProxyUrl(url: string): string | undefined { 264 const proxyUrl = getProxyUrl() 265 266 if (!proxyUrl) { 267 return undefined 268 } 269 270 if (shouldBypassProxy(url)) { 271 return undefined 272 } 273 274 return proxyUrl 275} 276 277/** 278 * Get fetch options for the Anthropic SDK with proxy and mTLS configuration 279 * Returns fetch options with appropriate dispatcher for proxy and/or mTLS 280 * 281 * @param opts.forAnthropicAPI - Enables ANTHROPIC_UNIX_SOCKET tunneling. This 282 * env var is set by `claude ssh` on the remote CLI to route API calls through 283 * an ssh -R forwarded unix socket to a local auth proxy. It MUST NOT leak 284 * into non-Anthropic-API fetch paths (MCP HTTP/SSE transports, etc.) or those 285 * requests get misrouted to api.anthropic.com. Only the Anthropic SDK client 286 * should pass `true` here. 287 */ 288export function getProxyFetchOptions(opts?: { forAnthropicAPI?: boolean }): { 289 tls?: TLSConfig 290 dispatcher?: undici.Dispatcher 291 proxy?: string 292 unix?: string 293 keepalive?: false 294} { 295 const base = keepAliveDisabled ? ({ keepalive: false } as const) : {} 296 297 // ANTHROPIC_UNIX_SOCKET tunnels through the `claude ssh` auth proxy, which 298 // hardcodes the upstream to the Anthropic API. Scope to the Anthropic API 299 // client so MCP/SSE/other callers don't get their requests misrouted. 300 if (opts?.forAnthropicAPI) { 301 const unixSocket = process.env.ANTHROPIC_UNIX_SOCKET 302 if (unixSocket && typeof Bun !== 'undefined') { 303 return { ...base, unix: unixSocket } 304 } 305 } 306 307 const proxyUrl = getProxyUrl() 308 309 // If we have a proxy, use the proxy agent (which includes mTLS config) 310 if (proxyUrl) { 311 if (typeof Bun !== 'undefined') { 312 return { ...base, proxy: proxyUrl, ...getTLSFetchOptions() } 313 } 314 return { ...base, dispatcher: getProxyAgent(proxyUrl) } 315 } 316 317 // Otherwise, use TLS options directly if available 318 return { ...base, ...getTLSFetchOptions() } 319} 320 321/** 322 * Configure global HTTP agents for both axios and undici 323 * This ensures all HTTP requests use the proxy and/or mTLS if configured 324 */ 325let proxyInterceptorId: number | undefined 326 327export function configureGlobalAgents(): void { 328 const proxyUrl = getProxyUrl() 329 const mtlsAgent = getMTLSAgent() 330 331 // Eject previous interceptor to avoid stacking on repeated calls 332 if (proxyInterceptorId !== undefined) { 333 axios.interceptors.request.eject(proxyInterceptorId) 334 proxyInterceptorId = undefined 335 } 336 337 // Reset proxy-related defaults so reconfiguration is clean 338 axios.defaults.proxy = undefined 339 axios.defaults.httpAgent = undefined 340 axios.defaults.httpsAgent = undefined 341 342 if (proxyUrl) { 343 // workaround for https://github.com/axios/axios/issues/4531 344 axios.defaults.proxy = false 345 346 // Create proxy agent with mTLS options if available 347 const proxyAgent = createHttpsProxyAgent(proxyUrl) 348 349 // Add axios request interceptor to handle NO_PROXY 350 proxyInterceptorId = axios.interceptors.request.use(config => { 351 // Check if URL should bypass proxy based on NO_PROXY 352 if (config.url && shouldBypassProxy(config.url)) { 353 // Bypass proxy - use mTLS agent if configured, otherwise undefined 354 if (mtlsAgent) { 355 config.httpsAgent = mtlsAgent 356 config.httpAgent = mtlsAgent 357 } else { 358 // Remove any proxy agents to use direct connection 359 delete config.httpsAgent 360 delete config.httpAgent 361 } 362 } else { 363 // Use proxy agent 364 config.httpsAgent = proxyAgent 365 config.httpAgent = proxyAgent 366 } 367 return config 368 }) 369 370 // Set global dispatcher that now respects NO_PROXY via EnvHttpProxyAgent 371 // eslint-disable-next-line @typescript-eslint/no-require-imports 372 ;(require('undici') as typeof undici).setGlobalDispatcher( 373 getProxyAgent(proxyUrl), 374 ) 375 } else if (mtlsAgent) { 376 // No proxy but mTLS is configured 377 axios.defaults.httpsAgent = mtlsAgent 378 379 // Set undici global dispatcher with mTLS 380 const mtlsOptions = getTLSFetchOptions() 381 if (mtlsOptions.dispatcher) { 382 // eslint-disable-next-line @typescript-eslint/no-require-imports 383 ;(require('undici') as typeof undici).setGlobalDispatcher( 384 mtlsOptions.dispatcher, 385 ) 386 } 387 } 388} 389 390/** 391 * Get AWS SDK client configuration with proxy support 392 * Returns configuration object that can be spread into AWS service client constructors 393 */ 394export async function getAWSClientProxyConfig(): Promise<object> { 395 const proxyUrl = getProxyUrl() 396 397 if (!proxyUrl) { 398 return {} 399 } 400 401 const [{ NodeHttpHandler }, { defaultProvider }] = await Promise.all([ 402 import('@smithy/node-http-handler'), 403 import('@aws-sdk/credential-provider-node'), 404 ]) 405 406 const agent = createHttpsProxyAgent(proxyUrl) 407 const requestHandler = new NodeHttpHandler({ 408 httpAgent: agent, 409 httpsAgent: agent, 410 }) 411 412 return { 413 requestHandler, 414 credentials: defaultProvider({ 415 clientConfig: { requestHandler }, 416 }), 417 } 418} 419 420/** 421 * Clear proxy agent cache. 422 */ 423export function clearProxyCache(): void { 424 getProxyAgent.cache.clear?.() 425 logForDebugging('Cleared proxy agent cache') 426}