fix: enable proxy support to resolve (#1125)
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Pre-commit / pre-commit (push) Waiting to run
Test / Run Python Tests (push) Waiting to run

Co-authored-by: a7m-1st <Ahmed.jimi.awelkeir500@gmail.com>
Co-authored-by: Ahmed Awelkair A <108264625+a7m-1st@users.noreply.github.com>
This commit is contained in:
BitToby 2026-02-11 08:04:17 -03:00 committed by GitHub
parent ad44e59485
commit 53d88308df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 483 additions and 67 deletions

View file

@ -11,3 +11,8 @@ VITE_USE_LOCAL_PROXY=false
VITE_STACK_PROJECT_ID=dummy_project_id
VITE_STACK_PUBLISHABLE_CLIENT_KEY=dummy_publishable_key
VITE_STACK_SECRET_SERVER_KEY=dummy_secret_server_key
# HTTP proxy settings for local development
# HTTP_PROXY=http://127.0.0.1:9090
# HTTPS_PROXY=https://127.0.0.1:9090
# NO_PROXY=localhost,127.0.0.1

View file

@ -52,6 +52,7 @@ import {
getEmailFolderPath,
getEnvPath,
maskProxyUrl,
readEnvValueWithPriority,
readGlobalEnvKey,
removeEnvKey,
updateEnvBlock,
@ -138,11 +139,31 @@ app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
app.commandLine.appendSwitch('renderer-process-limit', '8');
// ==================== Proxy configuration ====================
// Read proxy from global .env file on startup
proxyUrl = readGlobalEnvKey('HTTP_PROXY');
// Read proxy from multiple sources with priority:
// 1. Process environment (inline: SET HTTP_PROXY=... && eigent.exe)
// 2. .env.development (development mode)
// 3. Global ~/.eigent/.env file
// Check both HTTP_PROXY and HTTPS_PROXY (with lowercase variants)
const httpProxy =
readEnvValueWithPriority('HTTP_PROXY') ||
readEnvValueWithPriority('http_proxy');
const httpsProxy =
readEnvValueWithPriority('HTTPS_PROXY') ||
readEnvValueWithPriority('https_proxy');
// Prefer HTTPS proxy if available, fallback to HTTP proxy
proxyUrl = httpsProxy || httpProxy;
if (proxyUrl) {
log.info(`[PROXY] Applying proxy configuration: ${maskProxyUrl(proxyUrl)}`);
app.commandLine.appendSwitch('proxy-server', proxyUrl);
// Log which proxy type is being used
if (httpsProxy) {
log.info('[PROXY] Using HTTPS_PROXY configuration');
} else if (httpProxy) {
log.info('[PROXY] Using HTTP_PROXY configuration');
}
} else {
log.info('[PROXY] No proxy configured');
}
@ -1159,17 +1180,39 @@ function registerIpcHandlers() {
log.error('global env-remove error:', error);
}
// Also remove from .env.development file (remove from anywhere in file, not just MCP block)
const DEV_ENV_PATH = path.join(process.cwd(), '.env.development');
try {
if (fs.existsSync(DEV_ENV_PATH)) {
let devContent = fs.readFileSync(DEV_ENV_PATH, 'utf-8');
let devLines = devContent.split(/\r?\n/);
// Remove key from anywhere in the file (not limited to MCP block)
devLines = devLines.filter((line) => !line.trim().startsWith(key + '='));
fs.writeFileSync(DEV_ENV_PATH, devLines.join('\n'), 'utf-8');
log.info(`env-remove: removed ${key} from .env.development`);
}
} catch (error) {
log.error('.env.development env-remove error:', error);
}
return { success: true };
});
// ==================== read global env handler ====================
const ALLOWED_GLOBAL_ENV_KEYS = new Set(['HTTP_PROXY', 'HTTPS_PROXY']);
const ALLOWED_GLOBAL_ENV_KEYS = new Set([
'HTTP_PROXY',
'HTTPS_PROXY',
'NO_PROXY',
'http_proxy',
'https_proxy',
'no_proxy',
]);
ipcMain.handle('read-global-env', async (_event, key: string) => {
if (!ALLOWED_GLOBAL_ENV_KEYS.has(key)) {
log.warn(`[ENV] Blocked read of disallowed global env key: ${key}`);
return { value: null };
}
return { value: readGlobalEnvKey(key) };
return { value: readEnvValueWithPriority(key) };
});
// ==================== new window handler ====================

View file

@ -22,7 +22,7 @@ import os from 'os';
import path from 'path';
import { promisify } from 'util';
import { PromiseReturnType } from './install-deps';
import { maskProxyUrl, readGlobalEnvKey } from './utils/envUtil';
import { maskProxyUrl, readEnvValueWithPriority } from './utils/envUtil';
import {
ensureTerminalVenvAtUserPath,
findNodejsWheelBinPath,
@ -42,7 +42,10 @@ const execAsync = promisify(exec);
const DEFAULT_SERVER_URL = 'https://dev.eigent.ai/api';
function readEnvValue(filePath: string, key: string): string | undefined {
export function readEnvValue(
filePath: string,
key: string
): string | undefined {
try {
if (!fs.existsSync(filePath)) return undefined;
const content = fs.readFileSync(filePath, 'utf-8');
@ -236,22 +239,22 @@ export async function startBackend(
const uvEnv = getUvEnv(currentVersion);
const globalEnvPath = path.join(os.homedir(), '.eigent', '.env');
// Load proxy configuration from global .env file
const proxyUrl = readGlobalEnvKey('HTTP_PROXY');
// Build proxy env vars if configured
const proxyEnv = proxyUrl
? {
HTTP_PROXY: proxyUrl,
HTTPS_PROXY: proxyUrl,
http_proxy: proxyUrl,
https_proxy: proxyUrl,
}
: {};
const proxyEnv = {
HTTP_PROXY: readEnvValueWithPriority('HTTP_PROXY'),
HTTPS_PROXY: readEnvValueWithPriority('HTTPS_PROXY'),
http_proxy: readEnvValueWithPriority('http_proxy'),
https_proxy: readEnvValueWithPriority('https_proxy'),
// Ensure local connections bypass proxy
NO_PROXY:
readEnvValueWithPriority('NO_PROXY') || 'localhost,127.0.0.1,.local',
no_proxy:
readEnvValueWithPriority('no_proxy') || 'localhost,127.0.0.1,.local',
};
if (proxyUrl) {
if (proxyEnv.HTTP_PROXY || proxyEnv.HTTPS_PROXY) {
log.info(
`[BACKEND] Proxy configured for backend: ${maskProxyUrl(proxyUrl)}`
`[BACKEND] Proxy configured for backend: ${maskProxyUrl((proxyEnv.HTTP_PROXY || proxyEnv.HTTPS_PROXY) as string)}`
);
}

View file

@ -15,6 +15,7 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { readEnvValue } from '../init';
export const ENV_START = '# === MCP INTEGRATION ENV START ===';
export const ENV_END = '# === MCP INTEGRATION ENV END ===';
@ -116,6 +117,34 @@ export function readGlobalEnvKey(key: string): string | null {
return null;
}
/**
* Read environment variable value with priority system.
*
* Priority order (highest to lowest):
* 1. Process environment variables (inline/system)
* 2. .env.development file (development mode only)
* 3. Global ~/.eigent/.env file
*
* @param key - The environment variable key to read
* @returns The value if found, null otherwise
*/
export function readEnvValueWithPriority(key: string): string | null {
// Priority 1: Process environment variables (highest priority)
if (process.env[key]) {
return process.env[key]!;
}
// Priority 2: .env.development file (development mode only)
if (process.env.NODE_ENV === 'development') {
const devEnvPath = path.join(process.cwd(), '.env.development');
const value = readEnvValue(devEnvPath, key);
if (value) return value;
}
// Priority 3: Global ~/.eigent/.env file
return readGlobalEnvKey(key);
}
/**
* Mask credentials in a proxy URL for safe logging.
* e.g. "http://user:pass@host:port" "http://***:***@host:port"

View file

@ -18,6 +18,7 @@ import log from 'electron-log';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { maskProxyUrl, readEnvValueWithPriority } from './envUtil';
export function getResourcePath() {
return path.join(app.getAppPath(), 'resources');
@ -33,6 +34,69 @@ export function getBackendPath() {
}
}
/**
* Get proxy environment variables with priority:
* 1. Process environment variables (inline/system)
* 2. .env.development file (development mode only)
* 3. Global ~/.eigent/.env config
*
* Returns an object with HTTP_PROXY, HTTPS_PROXY, NO_PROXY and lowercase variants
* if a proxy is configured, or an empty object if not.
* Supports separate HTTP and HTTPS proxy configurations.
*/
function getProxyEnvVars(): Record<string, string> {
// Check both uppercase and lowercase variants
const httpProxy =
readEnvValueWithPriority('HTTP_PROXY') ||
readEnvValueWithPriority('http_proxy');
const httpsProxy =
readEnvValueWithPriority('HTTPS_PROXY') ||
readEnvValueWithPriority('https_proxy');
// Return empty object if no proxy configured
if (!httpProxy && !httpsProxy) {
return {};
}
// Log configured proxies
if (httpProxy) {
log.info(
`[INSTALL SCRIPT] HTTP Proxy configured: ${maskProxyUrl(httpProxy)}`
);
}
if (httpsProxy) {
log.info(
`[INSTALL SCRIPT] HTTPS Proxy configured: ${maskProxyUrl(httpsProxy)}`
);
}
// Get NO_PROXY configuration (with default for local connections)
const noProxy = readEnvValueWithPriority('NO_PROXY') ||
readEnvValueWithPriority('no_proxy') ||
'localhost,127.0.0.1,.local';
// Return all variants (some tools need uppercase, others lowercase)
// Filter out undefined values
const result: Record<string, string> = {};
if (httpProxy) {
result.HTTP_PROXY = httpProxy;
result.http_proxy = httpProxy;
}
if (httpsProxy) {
result.HTTPS_PROXY = httpsProxy;
result.https_proxy = httpsProxy;
}
// Always set NO_PROXY when proxy is configured to avoid issues with local connections
result.NO_PROXY = noProxy;
result.no_proxy = noProxy;
return result;
}
export function runInstallScript(scriptPath: string): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
const installScriptPath = path.join(
@ -42,8 +106,15 @@ export function runInstallScript(scriptPath: string): Promise<boolean> {
);
log.info(`Running script at: ${installScriptPath}`);
// Get proxy configuration from global .env file
const proxyEnv = getProxyEnvVars();
const nodeProcess = spawn(process.execPath, [installScriptPath], {
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
env: {
...process.env,
...proxyEnv,
ELECTRON_RUN_AS_NODE: '1',
},
});
let stderrOutput = '';

View file

@ -12,13 +12,250 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
/* global console, setTimeout, clearTimeout, require */
/* global console, process */
// @ts-check
import fs from 'fs';
import http from 'http';
import https from 'https';
import tls from 'tls';
import { URL } from 'url';
/**
* Downloads a file from a URL with redirect handling
* Check if the target URL should bypass proxy based on NO_PROXY env var.
* @param {string} targetUrl - The target URL
* @returns {boolean} True if proxy should be bypassed
*/
function shouldBypassProxy(targetUrl) {
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
if (!noProxy) return false;
try {
const targetHost = new URL(targetUrl).hostname.toLowerCase();
const noProxyList = noProxy.split(',').map((s) => s.trim().toLowerCase());
for (const pattern of noProxyList) {
if (!pattern) continue;
if (pattern === '*') return true;
if (targetHost === pattern) return true;
if (pattern.startsWith('.') && targetHost.endsWith(pattern)) return true;
if (targetHost.endsWith('.' + pattern)) return true;
}
} catch (error) {
console.warn(`Warning: Failed to parse NO_PROXY: ${error.message}`);
}
return false;
}
/**
* Get proxy URL from environment variables.
* @param {string} targetUrl - The target URL to determine which proxy to use
* @returns {string | null} The proxy URL or null if not configured
*/
function getProxyUrl(targetUrl) {
// Check NO_PROXY first
if (shouldBypassProxy(targetUrl)) {
return null;
}
const isHttps = targetUrl.startsWith('https://');
// Priority order for proxy env vars (check both uppercase and lowercase)
const envVars = isHttps
? ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy']
: ['HTTP_PROXY', 'http_proxy'];
for (const envVar of envVars) {
const value = process.env[envVar];
if (value && value.trim()) {
return value.trim();
}
}
return null;
}
/**
* Mask credentials in a proxy URL for safe logging.
* @param {string} url - The URL to mask
* @returns {string} The masked URL
*/
function maskProxyUrl(url) {
try {
const parsed = new URL(url);
if (parsed.username || parsed.password) {
parsed.username = parsed.username ? '***' : '';
parsed.password = parsed.password ? '***' : '';
return parsed.toString();
}
} catch (error) {
// Not a valid URL, return as-is
console.warn(`Warning: Failed to parse proxy URL: ${error.message}`);
}
return url;
}
/**
* Make an HTTP GET request with optional proxy support.
* For HTTPS URLs through HTTP proxy, uses CONNECT tunnel with TLS.
* @param {string} url - The URL to request
* @param {(response: http.IncomingMessage) => void} callback - Response callback
* @param {(error: Error) => void} onError - Error callback
*/
function makeRequest(url, callback, onError) {
const proxyUrl = getProxyUrl(url);
const isHttps = url.startsWith('https://');
const targetUrl = new URL(url);
const targetPort = parseInt(targetUrl.port, 10) || (isHttps ? 443 : 80);
if (!proxyUrl) {
// Direct connection (no proxy)
const httpModule = isHttps ? https : http;
const req = httpModule.get(url, callback);
req.on('error', onError);
return;
}
console.log(`Using proxy: ${maskProxyUrl(proxyUrl)}`);
const proxy = new URL(proxyUrl);
const proxyPort = parseInt(proxy.port, 10) || 80;
// Build proxy auth header if credentials provided
const proxyAuthHeader =
proxy.username || proxy.password
? {
'Proxy-Authorization': `Basic ${Buffer.from(
`${decodeURIComponent(proxy.username || '')}:${decodeURIComponent(proxy.password || '')}`
).toString('base64')}`,
}
: {};
if (isHttps) {
// HTTPS through HTTP proxy: Use CONNECT tunnel
const connectReq = http.request({
host: proxy.hostname,
port: proxyPort,
method: 'CONNECT',
path: `${targetUrl.hostname}:${targetPort}`,
headers: {
Host: `${targetUrl.hostname}:${targetPort}`,
...proxyAuthHeader,
},
});
// Track resources for cleanup
let tlsSocket = null;
let httpsReq = null;
// Cleanup function to destroy all connections
const cleanup = () => {
if (httpsReq && !httpsReq.destroyed) {
httpsReq.destroy();
}
if (tlsSocket && !tlsSocket.destroyed) {
tlsSocket.destroy();
}
if (connectReq && !connectReq.destroyed) {
connectReq.destroy();
}
};
connectReq.on('connect', (res, socket) => {
if (res.statusCode !== 200) {
socket.destroy();
cleanup();
onError(
new Error(
`Proxy CONNECT failed with status ${res.statusCode}: ${res.statusMessage}`
)
);
return;
}
// Upgrade socket to TLS
tlsSocket = tls.connect(
{
host: targetUrl.hostname,
port: targetPort,
socket: socket,
servername: targetUrl.hostname, // SNI
},
() => {
// Make HTTPS request over TLS socket
httpsReq = https.request(
{
hostname: targetUrl.hostname,
port: targetPort,
path: targetUrl.pathname + targetUrl.search,
method: 'GET',
headers: { Host: targetUrl.host },
agent: false,
// Use createConnection to provide the pre-established TLS socket
createConnection: () => tlsSocket,
},
(response) => {
// Cleanup connections when response ends
response.on('end', cleanup);
response.on('close', cleanup);
callback(response);
}
);
httpsReq.on('error', (err) => {
cleanup();
onError(new Error(`HTTPS request error: ${err.message}`));
});
httpsReq.end();
}
);
tlsSocket.on('error', (err) => {
cleanup();
onError(new Error(`TLS connection error: ${err.message}`));
});
});
connectReq.on('error', (err) => {
cleanup();
onError(new Error(`Proxy connection error: ${err.message}`));
});
connectReq.setTimeout(30000, () => {
cleanup();
onError(new Error('Proxy connection timeout after 30 seconds'));
});
connectReq.end();
} else {
// HTTP through HTTP proxy: Use proxy as target with full URL as path
const req = http.request(
{
host: proxy.hostname,
port: proxyPort,
path: url, // Full URL for HTTP proxy
method: 'GET',
headers: {
Host: targetUrl.host,
...proxyAuthHeader,
},
},
callback
);
req.on('error', (err) => {
onError(new Error(`HTTP proxy request error: ${err.message}`));
});
req.end();
}
}
/**
* Downloads a file from a URL with redirect handling and proxy support.
* Proxy is automatically detected from environment variables:
* - HTTPS_PROXY / https_proxy (for HTTPS URLs)
* - HTTP_PROXY / http_proxy (for HTTP URLs, or as fallback for HTTPS)
* - NO_PROXY / no_proxy (to bypass proxy for specific hosts)
*
* @param {string} url The URL to download from
* @param {string} destinationPath The path to save the file to
* @returns {Promise<void>} Promise that resolves when download is complete
@ -26,9 +263,7 @@ import https from 'https';
export async function downloadWithRedirects(url, destinationPath) {
return new Promise((resolve, reject) => {
const timeoutMs = 10 * 60 * 1000; // 10 minutes
const timeout = setTimeout(() => {
reject(new Error(`timeout${timeoutMs / 1000} seconds`));
}, timeoutMs);
let timeoutId = null;
// Use flag to prevent multiple resolve/reject calls
let settled = false;
@ -36,7 +271,7 @@ export async function downloadWithRedirects(url, destinationPath) {
const safeReject = (error) => {
if (!settled) {
settled = true;
clearTimeout(timeout);
if (timeoutId) clearTimeout(timeoutId);
reject(error);
}
};
@ -44,17 +279,19 @@ export async function downloadWithRedirects(url, destinationPath) {
const safeResolve = () => {
if (!settled) {
settled = true;
clearTimeout(timeout);
if (timeoutId) clearTimeout(timeoutId);
resolve();
}
};
const request = (url) => {
// Support both http and https
const httpModule = url.startsWith('https://') ? https : require('http');
timeoutId = setTimeout(() => {
safeReject(new Error(`Download timeout after ${timeoutMs / 1000} seconds`));
}, timeoutMs);
httpModule
.get(url, (response) => {
const request = (requestUrl) => {
makeRequest(
requestUrl,
(response) => {
const statusCode = response.statusCode || 0;
// Handle redirects (301, 302, 307, 308)
@ -63,15 +300,28 @@ export async function downloadWithRedirects(url, destinationPath) {
statusCode <= 308 &&
response.headers.location
) {
const redirectUrl = response.headers.location;
let redirectUrl = response.headers.location;
// Handle relative redirects
if (redirectUrl.startsWith('/')) {
try {
const originalUrl = new URL(requestUrl);
redirectUrl = `${originalUrl.protocol}//${originalUrl.host}${redirectUrl}`;
} catch (error) {
safeReject(new Error(`Failed to parse redirect URL: ${error.message}`));
return;
}
}
console.log(`Following redirect to: ${redirectUrl}`);
request(redirectUrl);
return;
}
if (statusCode !== 200) {
safeReject(
new Error(
`Download failed: ${statusCode} ${response.statusMessage || 'Unknown error'}`
`Download failed with status ${statusCode}: ${response.statusMessage || 'Unknown error'}`
)
);
return;
@ -80,7 +330,8 @@ export async function downloadWithRedirects(url, destinationPath) {
const file = fs.createWriteStream(destinationPath);
let downloadedBytes = 0;
const expectedBytes = parseInt(
response.headers['content-length'] || '0'
response.headers['content-length'] || '0',
10
);
const startTime = Date.now();
let lastProgressTime = Date.now();
@ -131,8 +382,10 @@ export async function downloadWithRedirects(url, destinationPath) {
if (fs.existsSync(destinationPath)) {
fs.unlinkSync(destinationPath);
}
} catch (err) {
console.error('Failed to delete incomplete file:', err);
} catch (cleanupError) {
console.warn(
`Warning: Failed to delete incomplete file: ${cleanupError.message}`
);
}
safeReject(
new Error(
@ -155,9 +408,9 @@ export async function downloadWithRedirects(url, destinationPath) {
} else {
safeReject(new Error('Downloaded file does not exist'));
}
} catch (err) {
} catch (verifyError) {
safeReject(
new Error(`Failed to verify download: ${err.message}`)
new Error(`Failed to verify download: ${verifyError.message}`)
);
}
});
@ -168,16 +421,18 @@ export async function downloadWithRedirects(url, destinationPath) {
if (fs.existsSync(destinationPath)) {
fs.unlinkSync(destinationPath);
}
} catch (deleteErr) {
console.error('Failed to delete file after error:', deleteErr);
} catch (cleanupError) {
console.warn(
`Warning: Failed to delete file after error: ${cleanupError.message}`
);
}
safeReject(err);
safeReject(new Error(`File write error: ${err.message}`));
});
})
.on('error', (err) => {
safeReject(err);
});
},
safeReject
);
};
request(url);
});
}

View file

@ -77,6 +77,7 @@ export default function SettingGeneral() {
// Proxy configuration state
const [proxyUrl, setProxyUrl] = useState('');
const [loadedProxyUrl, setLoadedProxyUrl] = useState(''); // Track the initially loaded value
const [isProxySaving, setIsProxySaving] = useState(false);
const [proxyNeedsRestart, setProxyNeedsRestart] = useState(false);
@ -158,16 +159,20 @@ export default function SettingGeneral() {
];
useEffect(() => {
// Load proxy configuration from global env
// Load proxy configuration from env (with priority system)
const loadProxyConfig = async () => {
if (window.electronAPI?.readGlobalEnv) {
try {
const result = await window.electronAPI.readGlobalEnv('HTTP_PROXY');
if (result?.value) {
setProxyUrl(result.value);
}
const result =
(await window.electronAPI.readGlobalEnv('HTTPS_PROXY')) ||
(await window.electronAPI.readGlobalEnv('HTTP_PROXY'));
const value = result?.value || '';
setProxyUrl(value);
setLoadedProxyUrl(value); // Remember the loaded value
} catch (_error) {
console.log('No proxy configured');
setProxyUrl('');
setLoadedProxyUrl('');
}
}
};
@ -229,6 +234,9 @@ export default function SettingGeneral() {
}
};
// Check if proxy value has changed from loaded value
const hasProxyChanged = proxyUrl.trim() !== loadedProxyUrl.trim();
if (!chatStore) {
return <div>Loading...</div>;
}
@ -372,22 +380,24 @@ export default function SettingGeneral() {
size="default"
note={proxyNeedsRestart ? t('setting.proxy-restart-hint') : undefined}
trailingButton={
<Button
variant={proxyNeedsRestart ? 'outline' : 'primary'}
size="sm"
onClick={
proxyNeedsRestart
? () => window.electronAPI?.restartApp()
: handleSaveProxy
}
disabled={!proxyNeedsRestart && isProxySaving}
>
{proxyNeedsRestart
? t('setting.restart-to-apply')
: isProxySaving
? t('setting.saving')
: t('setting.save')}
</Button>
proxyNeedsRestart ? (
<Button
variant="outline"
size="sm"
onClick={() => window.electronAPI?.restartApp()}
>
{t('setting.restart-to-apply')}
</Button>
) : hasProxyChanged ? (
<Button
variant="primary"
size="sm"
onClick={handleSaveProxy}
disabled={isProxySaving}
>
{isProxySaving ? t('setting.saving') : t('setting.save')}
</Button>
) : undefined
}
/>
</div>