Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"title": "Install Local Analysis",
"description": "Get instant feedback as you type by analyzing your code locally.\n\n[Install Codacy CLI](command:codacy.installCLI)\n\nInstalls all required dependencies: Node, Python, Java",
"media": {
"image": {
"image": {
"light": "resources/walkthrough/light/local-analysis.svg",
"dark": "resources/walkthrough/dark/local-analysis.svg",
"hc": "resources/walkthrough/dark/local-analysis.svg",
Expand Down Expand Up @@ -586,6 +586,7 @@
"@types/sarif": "2.1.7",
"@types/sinon": "^10.0.16",
"@types/temp": "^0.9.1",
"@types/tunnel": "^0.0.7",
"@types/vscode": "^1.102.0",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
Expand Down
2 changes: 2 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import {
import { Config } from '../common/config'
import { Tools } from '../codacy/Tools'
import { detectEditor } from '../auth'
import { configureAxiosProxy } from '../common/proxy'

export const initializeApi = () => {
configureAxiosProxy()
const ide = detectEditor()

// set up OpenAPI client
Expand Down
131 changes: 131 additions & 0 deletions src/common/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as vscode from 'vscode'
import * as tunnel from 'tunnel'
import { HttpProxyAgent } from 'http-proxy-agent'
import axios from 'axios'
import type { HTTPClient, HTTPClientRequest, HTTPResponse } from '@segment/analytics-node'

function resolveProxyUrl(): string | undefined {
// VS Code setting takes precedence; an empty string means "no proxy"
const vscodeProxy = vscode.workspace.getConfiguration('http').get<string>('proxy')
if (vscodeProxy !== undefined) {
return vscodeProxy
}

return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy
}

function resolveStrictSSL(): boolean {
return vscode.workspace.getConfiguration('http').get<boolean>('proxyStrictSSL', true)
}

function resolveProxyAuthorization(): string | undefined {
return vscode.workspace.getConfiguration('http').get<string | null>('proxyAuthorization') ?? undefined
}

function resolveNoProxy(): string[] {
const raw = process.env.NO_PROXY || process.env.no_proxy || ''
return raw
.split(',')
.map((s) => s.trim())
.filter(Boolean)
}

function hostMatchesNoProxy(hostname: string, entry: string): boolean {
if (entry === '*') return true
const normalized = entry.startsWith('.') ? entry.slice(1) : entry
return hostname === normalized || hostname.endsWith('.' + normalized)
}

function shouldBypassProxy(url: string, noProxyList: string[]): boolean {
try {
const { hostname } = new URL(url)
return noProxyList.some((entry) => hostMatchesNoProxy(hostname, entry))
} catch {
return false
}
}

// Ensures the URL has a protocol so `new URL()` doesn't throw for bare host:port strings
function normalizeProxyUrl(url: string): string {
return url.includes('://') ? url : `http://${url}`
}

let noProxyInterceptorId: number | null = null

/**
* Applies proxy settings from VS Code config and environment variables to the global axios instance
*/
export function configureAxiosProxy(): void {

Check warning on line 58 in src/common/proxy.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/common/proxy.ts#L58

Method configureAxiosProxy has a cyclomatic complexity of 9 (limit is 7)
Comment thread
nedaKaighobadi marked this conversation as resolved.
if (noProxyInterceptorId !== null) {
axios.interceptors.request.eject(noProxyInterceptorId)
noProxyInterceptorId = null
}

const proxyUrl = resolveProxyUrl()

if (proxyUrl) {
const strictSSL = resolveStrictSSL()
const authHeader = resolveProxyAuthorization()

const normalizedUrl = normalizeProxyUrl(proxyUrl)
const parsed = new URL(normalizedUrl)
const proxyHost = parsed.hostname
const proxyPort = parseInt(parsed.port || (parsed.protocol === 'https:' ? '443' : '80'), 10)
const proxyHeaders = authHeader ? { 'Proxy-Authorization': authHeader } : undefined
const proxyOpts = { host: proxyHost, port: proxyPort, ...(proxyHeaders ? { headers: proxyHeaders } : {}) }

// Branch on the proxy protocol so TLS proxies work too.
if (parsed.protocol === 'https:') {
axios.defaults.httpsAgent = tunnel.httpsOverHttps({ rejectUnauthorized: strictSSL, proxy: proxyOpts })
axios.defaults.httpAgent = tunnel.httpOverHttps({ proxy: proxyOpts })
} else {
axios.defaults.httpsAgent = tunnel.httpsOverHttp({ rejectUnauthorized: strictSSL, proxy: proxyOpts })
axios.defaults.httpAgent = new HttpProxyAgent(normalizedUrl)
}
// Disable axios's built-in proxy parsing so the agents take full control
axios.defaults.proxy = false

// Per-connection rejectUnauthorized on the tunnel agent is not sufficient
// in all Node.js versions to suppress TLS errors from an intercepting
// proxy cert. Mirror the VS Code proxyStrictSSL setting via the
// process-level flag so it is reliably honoured.
if (!strictSSL) {
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'
Comment thread
nedaKaighobadi marked this conversation as resolved.
} else {
delete process.env['NODE_TLS_REJECT_UNAUTHORIZED']
}

const noProxyList = resolveNoProxy()
if (noProxyList.length > 0) {
noProxyInterceptorId = axios.interceptors.request.use((config) => {
if (config.url && shouldBypassProxy(config.url, noProxyList)) {
config.httpsAgent = undefined
config.httpAgent = undefined
config.proxy = undefined
}
return config
})
}
} else {
axios.defaults.httpsAgent = undefined
axios.defaults.httpAgent = undefined
axios.defaults.proxy = undefined
// Restore TLS verification when proxy is removed or strictSSL is re-enabled
delete process.env['NODE_TLS_REJECT_UNAUTHORIZED']
}
}

/**
* An HTTPClient implementation for @segment/analytics-node that routes
* requests through the proxy configured in axios defaults (if any).
*/
export class ProxiedSegmentHTTPClient implements HTTPClient {
async makeRequest(options: HTTPClientRequest): Promise<HTTPResponse> {
const response = await axios.post(options.url, options.data, {
headers: options.headers,
timeout: options.httpRequestTimeout,
validateStatus: () => true,
})
return { status: response.status, statusText: response.statusText }
Comment thread
nedaKaighobadi marked this conversation as resolved.
}
Comment thread
nedaKaighobadi marked this conversation as resolved.
}
6 changes: 5 additions & 1 deletion src/common/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EventProperties } from '@segment/analytics-core'
import { Organization, User } from '../api/client'
import { SEGMENT_WRITE_KEY } from '../env-secrets'
import { v4 as uuidv4 } from 'uuid'
import { ProxiedSegmentHTTPClient } from './proxy'

class TelemetryClient {
private analytics: Analytics | undefined
Expand All @@ -13,7 +14,10 @@ class TelemetryClient {

constructor() {
if (SEGMENT_WRITE_KEY) {
this.analytics = new Analytics({ writeKey: SEGMENT_WRITE_KEY })
this.analytics = new Analytics({
writeKey: SEGMENT_WRITE_KEY,
httpClient: new ProxiedSegmentHTTPClient(),
})
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as os from 'os'
import { CommandType, wrapExtensionCommand } from './common/utils'
import Logger from './common/logger'
import { initializeApi } from './api'
import { configureAxiosProxy } from './common/proxy'
import { GitProvider } from './git/GitProvider'
import { CodacyCloud } from './git/CodacyCloud'
import { PullRequestSummaryTree } from './views/PullRequestSummaryTree'
Expand Down Expand Up @@ -209,6 +210,9 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(new SupportTree(context))
const setupViewProvider = activateWebview(context)

// Apply proxy settings before any outbound calls (telemetry, API, etc.)
configureAxiosProxy()

// Initialize telemetry with anonymous ID
Telemetry.init(context)

Expand All @@ -221,6 +225,15 @@ export async function activate(context: vscode.ExtensionContext) {
context.globalState.update('codacy.hasBeenActivated', true)
}

// Re-apply proxy settings when the VS Code http configuration changes
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('http')) {
configureAxiosProxy()
}
})
)
Comment thread
nedaKaighobadi marked this conversation as resolved.

// Listen for workspace folder changes
context.subscriptions.push(
vscode.workspace.onDidChangeWorkspaceFolders(() => {
Expand Down
Loading