networkProxy.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. /**
  2. * Network Proxy Configuration Module
  3. *
  4. * Provides proxy configuration for all outbound HTTP/HTTPS requests from the Roo Code extension.
  5. * When running in debug mode (F5), a proxy can be enabled for outbound traffic.
  6. * Optionally, TLS certificate verification can be disabled (debug only) to allow
  7. * MITM proxy inspection.
  8. *
  9. * Uses global-agent to globally route all HTTP/HTTPS traffic through the proxy,
  10. * which works with axios, fetch, and most SDKs that use native Node.js http/https.
  11. */
  12. import * as vscode from "vscode"
  13. import { Package } from "../shared/package"
  14. /**
  15. * Proxy configuration state
  16. */
  17. export interface ProxyConfig {
  18. /** Whether the debug proxy is enabled */
  19. enabled: boolean
  20. /** The proxy server URL (e.g., http://127.0.0.1:8888) */
  21. serverUrl: string
  22. /** Accept self-signed/insecure TLS certificates from the proxy (required for MITM) */
  23. tlsInsecure: boolean
  24. /** Whether running in debug/development mode */
  25. isDebugMode: boolean
  26. }
  27. let extensionContext: vscode.ExtensionContext | null = null
  28. let proxyInitialized = false
  29. let undiciProxyInitialized = false
  30. let fetchPatched = false
  31. let originalFetch: typeof fetch | undefined
  32. let outputChannel: vscode.OutputChannel | null = null
  33. let loggingEnabled = false
  34. let consoleLoggingEnabled = false
  35. let tlsVerificationOverridden = false
  36. let originalNodeTlsRejectUnauthorized: string | undefined
  37. function redactProxyUrl(proxyUrl: string | undefined): string {
  38. if (!proxyUrl) {
  39. return "(not set)"
  40. }
  41. try {
  42. const url = new URL(proxyUrl)
  43. url.username = ""
  44. url.password = ""
  45. return url.toString()
  46. } catch {
  47. // Fallback for invalid URLs: redact basic auth if present.
  48. return proxyUrl.replace(/\/\/[^@/]+@/g, "//REDACTED@")
  49. }
  50. }
  51. function restoreGlobalFetchPatch(): void {
  52. if (!fetchPatched) {
  53. return
  54. }
  55. if (originalFetch) {
  56. globalThis.fetch = originalFetch
  57. }
  58. fetchPatched = false
  59. originalFetch = undefined
  60. }
  61. function restoreTlsVerificationOverride(): void {
  62. if (!tlsVerificationOverridden) {
  63. return
  64. }
  65. if (typeof originalNodeTlsRejectUnauthorized === "string") {
  66. process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalNodeTlsRejectUnauthorized
  67. } else {
  68. delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
  69. }
  70. tlsVerificationOverridden = false
  71. originalNodeTlsRejectUnauthorized = undefined
  72. }
  73. function applyTlsVerificationOverride(config: ProxyConfig): void {
  74. // Only relevant in debug mode with an active proxy.
  75. if (!config.isDebugMode || !config.enabled) {
  76. restoreTlsVerificationOverride()
  77. return
  78. }
  79. if (!config.tlsInsecure) {
  80. restoreTlsVerificationOverride()
  81. return
  82. }
  83. if (!tlsVerificationOverridden) {
  84. originalNodeTlsRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED
  85. }
  86. // CodeQL: debug-only opt-in for MITM debugging.
  87. process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" // lgtm[js/disabling-certificate-validation]
  88. tlsVerificationOverridden = true
  89. }
  90. /**
  91. * Initialize the network proxy module with the extension context.
  92. * Must be called early in extension activation before any network requests.
  93. *
  94. * @param context The VS Code extension context
  95. * @param channel Optional output channel for logging
  96. */
  97. export async function initializeNetworkProxy(
  98. context: vscode.ExtensionContext,
  99. channel?: vscode.OutputChannel,
  100. ): Promise<void> {
  101. extensionContext = context
  102. // extensionMode is immutable for the process lifetime - exit early if not in debug mode.
  103. // This avoids any overhead (listeners, logging, etc.) in production.
  104. const isDebugMode = context.extensionMode === vscode.ExtensionMode.Development
  105. if (!isDebugMode) {
  106. return
  107. }
  108. outputChannel = channel ?? null
  109. loggingEnabled = true
  110. consoleLoggingEnabled = !outputChannel
  111. const config = getProxyConfig()
  112. log(`Initializing network proxy module...`)
  113. log(
  114. `Proxy config: enabled=${config.enabled}, serverUrl=${redactProxyUrl(config.serverUrl)}, tlsInsecure=${config.tlsInsecure}`,
  115. )
  116. // Listen for configuration changes to allow toggling proxy during a debug session.
  117. // Guard for test environments where onDidChangeConfiguration may not be mocked.
  118. if (typeof vscode.workspace.onDidChangeConfiguration === "function") {
  119. context.subscriptions.push(
  120. vscode.workspace.onDidChangeConfiguration((e) => {
  121. if (
  122. e.affectsConfiguration(`${Package.name}.debugProxy.enabled`) ||
  123. e.affectsConfiguration(`${Package.name}.debugProxy.serverUrl`) ||
  124. e.affectsConfiguration(`${Package.name}.debugProxy.tlsInsecure`)
  125. ) {
  126. const newConfig = getProxyConfig()
  127. if (newConfig.enabled) {
  128. applyTlsVerificationOverride(newConfig)
  129. configureGlobalProxy(newConfig)
  130. configureUndiciProxy(newConfig)
  131. } else {
  132. // Proxy disabled - but we can't easily un-bootstrap global-agent or reset undici dispatcher safely.
  133. // We *can* restore any global fetch patch immediately.
  134. restoreGlobalFetchPatch()
  135. restoreTlsVerificationOverride()
  136. log("Debug proxy disabled. Restart VS Code to fully disable proxy routing.")
  137. }
  138. }
  139. }),
  140. )
  141. }
  142. // Ensure we restore any overrides when the extension unloads.
  143. context.subscriptions.push({
  144. dispose: () => {
  145. restoreGlobalFetchPatch()
  146. restoreTlsVerificationOverride()
  147. },
  148. })
  149. if (config.enabled) {
  150. applyTlsVerificationOverride(config)
  151. await configureGlobalProxy(config)
  152. await configureUndiciProxy(config)
  153. } else {
  154. log(`Debug proxy not enabled.`)
  155. }
  156. }
  157. /**
  158. * Get the current proxy configuration based on VS Code settings and extension mode.
  159. */
  160. export function getProxyConfig(): ProxyConfig {
  161. const defaultServerUrl = "http://127.0.0.1:8888"
  162. if (!extensionContext) {
  163. // Fallback if called before initialization
  164. return {
  165. enabled: false,
  166. serverUrl: defaultServerUrl,
  167. tlsInsecure: false,
  168. isDebugMode: false,
  169. }
  170. }
  171. const config = vscode.workspace.getConfiguration(Package.name)
  172. const enabled = Boolean(config.get<unknown>("debugProxy.enabled"))
  173. const rawServerUrl = config.get<unknown>("debugProxy.serverUrl")
  174. const serverUrl = typeof rawServerUrl === "string" && rawServerUrl.trim() ? rawServerUrl.trim() : defaultServerUrl
  175. const tlsInsecure = Boolean(config.get<unknown>("debugProxy.tlsInsecure"))
  176. // Debug mode only.
  177. const isDebugMode = extensionContext.extensionMode === vscode.ExtensionMode.Development
  178. return {
  179. enabled,
  180. serverUrl,
  181. tlsInsecure,
  182. isDebugMode,
  183. }
  184. }
  185. /**
  186. * Configure global-agent to route all HTTP/HTTPS traffic through the proxy.
  187. */
  188. async function configureGlobalProxy(config: ProxyConfig): Promise<void> {
  189. if (proxyInitialized) {
  190. // global-agent can only be bootstrapped once
  191. // Update environment variables for any new connections
  192. log(`Proxy already initialized, updating env vars only`)
  193. updateProxyEnvVars(config)
  194. return
  195. }
  196. // Set up environment variables before bootstrapping
  197. log(`Setting proxy environment variables before bootstrap (values redacted)...`)
  198. updateProxyEnvVars(config)
  199. let bootstrap: (() => void) | undefined
  200. try {
  201. const mod = (await import("global-agent")) as typeof import("global-agent")
  202. bootstrap = mod.bootstrap
  203. } catch (error) {
  204. log(
  205. `Failed to load global-agent (proxy support is only available in debug/dev builds): ${error instanceof Error ? error.message : String(error)}`,
  206. )
  207. return
  208. }
  209. // Bootstrap global-agent to intercept all HTTP/HTTPS requests
  210. log(`Calling global-agent bootstrap()...`)
  211. try {
  212. bootstrap()
  213. proxyInitialized = true
  214. log(`global-agent bootstrap() completed successfully`)
  215. } catch (error) {
  216. log(`global-agent bootstrap() FAILED: ${error instanceof Error ? error.message : String(error)}`)
  217. return
  218. }
  219. log(`Network proxy configured: ${redactProxyUrl(config.serverUrl)}`)
  220. }
  221. /**
  222. * Configure undici's global dispatcher so Node's built-in `fetch()` and any undici-based
  223. * clients route through the proxy.
  224. */
  225. async function configureUndiciProxy(config: ProxyConfig): Promise<void> {
  226. if (!config.enabled || !config.serverUrl) {
  227. return
  228. }
  229. if (undiciProxyInitialized) {
  230. log(`undici global dispatcher already configured; restart VS Code to change proxy safely`)
  231. return
  232. }
  233. try {
  234. const {
  235. ProxyAgent,
  236. setGlobalDispatcher,
  237. fetch: undiciFetch,
  238. } = (await import("undici")) as typeof import("undici")
  239. const proxyAgent = new ProxyAgent({
  240. uri: config.serverUrl,
  241. // If the user enabled TLS insecure mode (debug only), apply it to undici.
  242. requestTls: config.tlsInsecure
  243. ? ({ rejectUnauthorized: false } satisfies import("tls").ConnectionOptions) // lgtm[js/disabling-certificate-validation]
  244. : undefined,
  245. proxyTls: config.tlsInsecure
  246. ? ({ rejectUnauthorized: false } satisfies import("tls").ConnectionOptions) // lgtm[js/disabling-certificate-validation]
  247. : undefined,
  248. })
  249. setGlobalDispatcher(proxyAgent)
  250. undiciProxyInitialized = true
  251. log(`undici global dispatcher configured for proxy: ${redactProxyUrl(config.serverUrl)}`)
  252. // Node's built-in `fetch()` (Node 18+) is powered by an internal undici copy.
  253. // Setting a dispatcher on our `undici` dependency does NOT affect that internal fetch.
  254. // To ensure Roo Code's `fetch()` calls are proxied, patch global fetch in debug mode.
  255. // This patch is scoped to the extension lifecycle (restored on deactivate) and can be restored
  256. // immediately if the proxy is disabled.
  257. if (!fetchPatched) {
  258. if (typeof globalThis.fetch === "function") {
  259. originalFetch = globalThis.fetch
  260. }
  261. globalThis.fetch = undiciFetch as unknown as typeof fetch
  262. fetchPatched = true
  263. log(`globalThis.fetch patched to undici.fetch (debug proxy mode)`)
  264. if (extensionContext) {
  265. extensionContext.subscriptions.push({
  266. dispose: () => restoreGlobalFetchPatch(),
  267. })
  268. }
  269. }
  270. } catch (error) {
  271. log(`Failed to configure undici proxy dispatcher: ${error instanceof Error ? error.message : String(error)}`)
  272. }
  273. }
  274. /**
  275. * Update environment variables for proxy configuration.
  276. * global-agent reads from GLOBAL_AGENT_* environment variables.
  277. */
  278. function updateProxyEnvVars(config: ProxyConfig): void {
  279. if (config.serverUrl) {
  280. // global-agent uses these environment variables
  281. process.env.GLOBAL_AGENT_HTTP_PROXY = config.serverUrl
  282. process.env.GLOBAL_AGENT_HTTPS_PROXY = config.serverUrl
  283. process.env.GLOBAL_AGENT_NO_PROXY = "" // Proxy all requests
  284. }
  285. }
  286. /**
  287. * Check if a proxy is currently configured and active.
  288. */
  289. export function isProxyEnabled(): boolean {
  290. const config = getProxyConfig()
  291. // Active proxy is only applied in debug mode.
  292. return config.enabled && config.isDebugMode
  293. }
  294. /**
  295. * Check if we're running in debug mode.
  296. */
  297. export function isDebugMode(): boolean {
  298. if (!extensionContext) {
  299. return false
  300. }
  301. return extensionContext.extensionMode === vscode.ExtensionMode.Development
  302. }
  303. /**
  304. * Log a message to the output channel if available.
  305. */
  306. function log(message: string): void {
  307. if (!loggingEnabled) {
  308. return
  309. }
  310. const logMessage = `[NetworkProxy] ${message}`
  311. if (outputChannel) {
  312. outputChannel.appendLine(logMessage)
  313. }
  314. if (consoleLoggingEnabled) {
  315. console.log(logMessage)
  316. }
  317. }