|
|
@@ -0,0 +1,364 @@
|
|
|
+/**
|
|
|
+ * 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)
|
|
|
+ }
|
|
|
+}
|