| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- /**
- * Network Proxy Configuration Module
- *
- * Provides proxy configuration for all outbound HTTP/HTTPS requests from the Roo Code extension.
- * When running in debug mode (F5), a proxy can be enabled for outbound traffic.
- * Optionally, TLS certificate verification can be disabled (debug only) to allow
- * MITM proxy inspection.
- *
- * Uses global-agent to globally route all HTTP/HTTPS traffic through the proxy,
- * which works with axios, fetch, and most SDKs that use native Node.js http/https.
- */
- import * as vscode from "vscode"
- import { Package } from "../shared/package"
- /**
- * Proxy configuration state
- */
- export interface ProxyConfig {
- /** Whether the debug proxy is enabled */
- enabled: boolean
- /** The proxy server URL (e.g., http://127.0.0.1:8888) */
- serverUrl: string
- /** Accept self-signed/insecure TLS certificates from the proxy (required for MITM) */
- tlsInsecure: boolean
- /** Whether running in debug/development mode */
- isDebugMode: boolean
- }
- let extensionContext: vscode.ExtensionContext | null = null
- let proxyInitialized = false
- let undiciProxyInitialized = false
- let fetchPatched = false
- let originalFetch: typeof fetch | undefined
- let outputChannel: vscode.OutputChannel | null = null
- let loggingEnabled = false
- let consoleLoggingEnabled = false
- let tlsVerificationOverridden = false
- let originalNodeTlsRejectUnauthorized: string | undefined
- function redactProxyUrl(proxyUrl: string | undefined): string {
- if (!proxyUrl) {
- return "(not set)"
- }
- try {
- const url = new URL(proxyUrl)
- url.username = ""
- url.password = ""
- return url.toString()
- } catch {
- // Fallback for invalid URLs: redact basic auth if present.
- return proxyUrl.replace(/\/\/[^@/]+@/g, "//REDACTED@")
- }
- }
- function restoreGlobalFetchPatch(): void {
- if (!fetchPatched) {
- return
- }
- if (originalFetch) {
- globalThis.fetch = originalFetch
- }
- fetchPatched = false
- originalFetch = undefined
- }
- function restoreTlsVerificationOverride(): void {
- if (!tlsVerificationOverridden) {
- return
- }
- if (typeof originalNodeTlsRejectUnauthorized === "string") {
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalNodeTlsRejectUnauthorized
- } else {
- delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
- }
- tlsVerificationOverridden = false
- originalNodeTlsRejectUnauthorized = undefined
- }
- function applyTlsVerificationOverride(config: ProxyConfig): void {
- // Only relevant in debug mode with an active proxy.
- if (!config.isDebugMode || !config.enabled) {
- restoreTlsVerificationOverride()
- return
- }
- if (!config.tlsInsecure) {
- restoreTlsVerificationOverride()
- return
- }
- if (!tlsVerificationOverridden) {
- originalNodeTlsRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED
- }
- // CodeQL: debug-only opt-in for MITM debugging.
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" // lgtm[js/disabling-certificate-validation]
- tlsVerificationOverridden = true
- }
- /**
- * Initialize the network proxy module with the extension context.
- * Must be called early in extension activation before any network requests.
- *
- * @param context The VS Code extension context
- * @param channel Optional output channel for logging
- */
- export async function initializeNetworkProxy(
- context: vscode.ExtensionContext,
- channel?: vscode.OutputChannel,
- ): Promise<void> {
- extensionContext = context
- // extensionMode is immutable for the process lifetime - exit early if not in debug mode.
- // This avoids any overhead (listeners, logging, etc.) in production.
- const isDebugMode = context.extensionMode === vscode.ExtensionMode.Development
- if (!isDebugMode) {
- return
- }
- outputChannel = channel ?? null
- loggingEnabled = true
- consoleLoggingEnabled = !outputChannel
- const config = getProxyConfig()
- log(`Initializing network proxy module...`)
- log(
- `Proxy config: enabled=${config.enabled}, serverUrl=${redactProxyUrl(config.serverUrl)}, tlsInsecure=${config.tlsInsecure}`,
- )
- // Listen for configuration changes to allow toggling proxy during a debug session.
- // Guard for test environments where onDidChangeConfiguration may not be mocked.
- if (typeof vscode.workspace.onDidChangeConfiguration === "function") {
- context.subscriptions.push(
- vscode.workspace.onDidChangeConfiguration((e) => {
- if (
- e.affectsConfiguration(`${Package.name}.debugProxy.enabled`) ||
- e.affectsConfiguration(`${Package.name}.debugProxy.serverUrl`) ||
- e.affectsConfiguration(`${Package.name}.debugProxy.tlsInsecure`)
- ) {
- const newConfig = getProxyConfig()
- if (newConfig.enabled) {
- applyTlsVerificationOverride(newConfig)
- configureGlobalProxy(newConfig)
- configureUndiciProxy(newConfig)
- } else {
- // Proxy disabled - but we can't easily un-bootstrap global-agent or reset undici dispatcher safely.
- // We *can* restore any global fetch patch immediately.
- restoreGlobalFetchPatch()
- restoreTlsVerificationOverride()
- log("Debug proxy disabled. Restart VS Code to fully disable proxy routing.")
- }
- }
- }),
- )
- }
- // Ensure we restore any overrides when the extension unloads.
- context.subscriptions.push({
- dispose: () => {
- restoreGlobalFetchPatch()
- restoreTlsVerificationOverride()
- },
- })
- if (config.enabled) {
- applyTlsVerificationOverride(config)
- await configureGlobalProxy(config)
- await configureUndiciProxy(config)
- } else {
- log(`Debug proxy not enabled.`)
- }
- }
- /**
- * Get the current proxy configuration based on VS Code settings and extension mode.
- */
- export function getProxyConfig(): ProxyConfig {
- const defaultServerUrl = "http://127.0.0.1:8888"
- if (!extensionContext) {
- // Fallback if called before initialization
- return {
- enabled: false,
- serverUrl: defaultServerUrl,
- tlsInsecure: false,
- isDebugMode: false,
- }
- }
- const config = vscode.workspace.getConfiguration(Package.name)
- const enabled = Boolean(config.get<unknown>("debugProxy.enabled"))
- const rawServerUrl = config.get<unknown>("debugProxy.serverUrl")
- const serverUrl = typeof rawServerUrl === "string" && rawServerUrl.trim() ? rawServerUrl.trim() : defaultServerUrl
- const tlsInsecure = Boolean(config.get<unknown>("debugProxy.tlsInsecure"))
- // Debug mode only.
- const isDebugMode = extensionContext.extensionMode === vscode.ExtensionMode.Development
- return {
- enabled,
- serverUrl,
- tlsInsecure,
- isDebugMode,
- }
- }
- /**
- * Configure global-agent to route all HTTP/HTTPS traffic through the proxy.
- */
- async function configureGlobalProxy(config: ProxyConfig): Promise<void> {
- if (proxyInitialized) {
- // global-agent can only be bootstrapped once
- // Update environment variables for any new connections
- log(`Proxy already initialized, updating env vars only`)
- updateProxyEnvVars(config)
- return
- }
- // Set up environment variables before bootstrapping
- log(`Setting proxy environment variables before bootstrap (values redacted)...`)
- updateProxyEnvVars(config)
- let bootstrap: (() => void) | undefined
- try {
- const mod = (await import("global-agent")) as typeof import("global-agent")
- bootstrap = mod.bootstrap
- } catch (error) {
- log(
- `Failed to load global-agent (proxy support is only available in debug/dev builds): ${error instanceof Error ? error.message : String(error)}`,
- )
- return
- }
- // Bootstrap global-agent to intercept all HTTP/HTTPS requests
- log(`Calling global-agent bootstrap()...`)
- try {
- bootstrap()
- proxyInitialized = true
- log(`global-agent bootstrap() completed successfully`)
- } catch (error) {
- log(`global-agent bootstrap() FAILED: ${error instanceof Error ? error.message : String(error)}`)
- return
- }
- log(`Network proxy configured: ${redactProxyUrl(config.serverUrl)}`)
- }
- /**
- * Configure undici's global dispatcher so Node's built-in `fetch()` and any undici-based
- * clients route through the proxy.
- */
- async function configureUndiciProxy(config: ProxyConfig): Promise<void> {
- if (!config.enabled || !config.serverUrl) {
- return
- }
- if (undiciProxyInitialized) {
- log(`undici global dispatcher already configured; restart VS Code to change proxy safely`)
- return
- }
- try {
- const {
- ProxyAgent,
- setGlobalDispatcher,
- fetch: undiciFetch,
- } = (await import("undici")) as typeof import("undici")
- const proxyAgent = new ProxyAgent({
- uri: config.serverUrl,
- // If the user enabled TLS insecure mode (debug only), apply it to undici.
- requestTls: config.tlsInsecure
- ? ({ rejectUnauthorized: false } satisfies import("tls").ConnectionOptions) // lgtm[js/disabling-certificate-validation]
- : undefined,
- proxyTls: config.tlsInsecure
- ? ({ rejectUnauthorized: false } satisfies import("tls").ConnectionOptions) // lgtm[js/disabling-certificate-validation]
- : undefined,
- })
- setGlobalDispatcher(proxyAgent)
- undiciProxyInitialized = true
- log(`undici global dispatcher configured for proxy: ${redactProxyUrl(config.serverUrl)}`)
- // Node's built-in `fetch()` (Node 18+) is powered by an internal undici copy.
- // Setting a dispatcher on our `undici` dependency does NOT affect that internal fetch.
- // To ensure Roo Code's `fetch()` calls are proxied, patch global fetch in debug mode.
- // This patch is scoped to the extension lifecycle (restored on deactivate) and can be restored
- // immediately if the proxy is disabled.
- if (!fetchPatched) {
- if (typeof globalThis.fetch === "function") {
- originalFetch = globalThis.fetch
- }
- globalThis.fetch = undiciFetch as unknown as typeof fetch
- fetchPatched = true
- log(`globalThis.fetch patched to undici.fetch (debug proxy mode)`)
- if (extensionContext) {
- extensionContext.subscriptions.push({
- dispose: () => restoreGlobalFetchPatch(),
- })
- }
- }
- } catch (error) {
- log(`Failed to configure undici proxy dispatcher: ${error instanceof Error ? error.message : String(error)}`)
- }
- }
- /**
- * Update environment variables for proxy configuration.
- * global-agent reads from GLOBAL_AGENT_* environment variables.
- */
- function updateProxyEnvVars(config: ProxyConfig): void {
- if (config.serverUrl) {
- // global-agent uses these environment variables
- process.env.GLOBAL_AGENT_HTTP_PROXY = config.serverUrl
- process.env.GLOBAL_AGENT_HTTPS_PROXY = config.serverUrl
- process.env.GLOBAL_AGENT_NO_PROXY = "" // Proxy all requests
- }
- }
- /**
- * Check if a proxy is currently configured and active.
- */
- export function isProxyEnabled(): boolean {
- const config = getProxyConfig()
- // Active proxy is only applied in debug mode.
- return config.enabled && config.isDebugMode
- }
- /**
- * Check if we're running in debug mode.
- */
- export function isDebugMode(): boolean {
- if (!extensionContext) {
- return false
- }
- return extensionContext.extensionMode === vscode.ExtensionMode.Development
- }
- /**
- * Log a message to the output channel if available.
- */
- function log(message: string): void {
- if (!loggingEnabled) {
- return
- }
- const logMessage = `[NetworkProxy] ${message}`
- if (outputChannel) {
- outputChannel.appendLine(logMessage)
- }
- if (consoleLoggingEnabled) {
- console.log(logMessage)
- }
- }
|