Features: - New modern dark UI with gradient accents - Hostname support for custom proxy (e.g. relayup.local) - Full private IP range bypass (10.x, 172.16-31.x, 192.168.x) - WebRTC Leak Protection setting - Kill Switch - block traffic if proxy disconnects - Auto-connect on browser startup - Custom proxy authentication (username/password) - Bypass list for custom exceptions - Local Network Access for printers, NAS, routers, etc - Multiple proxy sources with automatic fallback (Arweave → GitBros → GitHub) - Arweave as default proxy source for decentralized, permanent storage - Auto-update interval for proxy list
433 lines
14 KiB
JavaScript
433 lines
14 KiB
JavaScript
/* ANyONe Extension v2 - Proxy Manager */
|
|
|
|
import { CONFIG } from './config.js';
|
|
import { Utils } from './utils.js';
|
|
import { Storage } from './storage.js';
|
|
|
|
const ProxyManager = {
|
|
// Current state
|
|
_currentProxy: null,
|
|
_isEnabled: false,
|
|
_proxyList: [],
|
|
_currentIndex: 0,
|
|
|
|
/**
|
|
* Initialize proxy manager
|
|
*/
|
|
async init() {
|
|
Utils.log('info', 'Initializing ProxyManager');
|
|
|
|
// Load state from storage
|
|
const [enabled, proxyList, currentProxy] = await Promise.all([
|
|
Storage.isProxyEnabled(),
|
|
Storage.getProxyList(),
|
|
Storage.getCurrentProxy()
|
|
]);
|
|
|
|
this._isEnabled = enabled;
|
|
this._proxyList = proxyList;
|
|
this._currentProxy = currentProxy;
|
|
|
|
Utils.log('info', 'ProxyManager initialized', {
|
|
enabled,
|
|
proxyCount: proxyList.length
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Create SOCKS5 proxy configuration
|
|
* @param {string} host - Proxy host
|
|
* @param {number} port - Proxy port
|
|
* @param {string[]} exceptions - Bypass list
|
|
* @param {boolean} bypassLocal - Allow local network access
|
|
* @returns {object}
|
|
*/
|
|
createProxyConfig(host, port, exceptions = [], bypassLocal = true) {
|
|
// Always include localhost and 127.0.0.1
|
|
let bypassList = exceptions.concat(['localhost', '127.0.0.1']);
|
|
|
|
// Add local network ranges if local network access is enabled
|
|
if (bypassLocal) {
|
|
bypassList.push(
|
|
'<local>', // Simple hostnames (no dots)
|
|
'10.*', // Class A private network
|
|
'172.16.*', // Class B private (172.16.0.0 - 172.31.255.255)
|
|
'172.17.*',
|
|
'172.18.*',
|
|
'172.19.*',
|
|
'172.20.*',
|
|
'172.21.*',
|
|
'172.22.*',
|
|
'172.23.*',
|
|
'172.24.*',
|
|
'172.25.*',
|
|
'172.26.*',
|
|
'172.27.*',
|
|
'172.28.*',
|
|
'172.29.*',
|
|
'172.30.*',
|
|
'172.31.*',
|
|
'192.168.*', // Class C private network
|
|
'169.254.*', // Link-local
|
|
'*.local' // mDNS/Bonjour domains (.local TLD)
|
|
);
|
|
}
|
|
|
|
Utils.log('info', 'Creating proxy config', { host, port, bypassList, bypassLocal });
|
|
return {
|
|
mode: 'fixed_servers',
|
|
rules: {
|
|
singleProxy: {
|
|
scheme: 'socks5',
|
|
host: host,
|
|
port: parseInt(port, 10)
|
|
},
|
|
bypassList: bypassList
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Apply proxy settings
|
|
* @param {string} host - Proxy host
|
|
* @param {number} port - Proxy port
|
|
* @param {string[]} exceptions - Bypass list
|
|
* @param {boolean} bypassLocal - Allow local network access
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async applyProxy(host, port, exceptions = [], bypassLocal = true) {
|
|
return new Promise((resolve) => {
|
|
const config = this.createProxyConfig(host, port, exceptions, bypassLocal);
|
|
|
|
chrome.proxy.settings.set({ value: config, scope: 'regular' }, () => {
|
|
if (chrome.runtime.lastError) {
|
|
Utils.log('error', 'Failed to apply proxy', chrome.runtime.lastError);
|
|
resolve(false);
|
|
} else {
|
|
Utils.log('info', `Proxy applied: ${host}:${port}`);
|
|
this._currentProxy = { host, port };
|
|
this._isEnabled = true;
|
|
resolve(true);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Clear proxy settings
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async clearProxy() {
|
|
return new Promise((resolve) => {
|
|
chrome.proxy.settings.clear({}, () => {
|
|
if (chrome.runtime.lastError) {
|
|
Utils.log('error', 'Failed to clear proxy', chrome.runtime.lastError);
|
|
resolve(false);
|
|
} else {
|
|
Utils.log('info', 'Proxy cleared');
|
|
this._currentProxy = null;
|
|
this._isEnabled = false;
|
|
this._currentIndex = 0;
|
|
resolve(true);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Test proxy connectivity
|
|
* @param {object} proxy - Proxy object {host, port}
|
|
* @returns {Promise<{working: boolean, latency: number|null}>}
|
|
*/
|
|
async testProxy(proxy) {
|
|
Utils.log('debug', `Testing proxy ${proxy.host}:${proxy.port}`);
|
|
|
|
// First, set the proxy
|
|
const config = this.createProxyConfig(proxy.host, proxy.port);
|
|
|
|
return new Promise((resolve) => {
|
|
chrome.proxy.settings.set({ value: config, scope: 'regular' }, async () => {
|
|
if (chrome.runtime.lastError) {
|
|
Utils.log('error', 'Failed to set proxy for test', chrome.runtime.lastError);
|
|
resolve({ working: false, latency: null });
|
|
return;
|
|
}
|
|
|
|
// Longer delay to ensure proxy settings are applied
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), CONFIG.TIMEOUTS.PROXY_CHECK);
|
|
|
|
// Measure latency only for the actual request
|
|
const startTime = Date.now();
|
|
|
|
// Use HEAD request with cache disabled for reliable test
|
|
const response = await fetch(CONFIG.URLS.CHECK_IP, {
|
|
method: 'HEAD',
|
|
cache: 'no-store',
|
|
signal: controller.signal
|
|
});
|
|
|
|
const latency = Date.now() - startTime;
|
|
clearTimeout(timeoutId);
|
|
|
|
if (response.ok || response.type === 'opaque') {
|
|
Utils.log('debug', `Proxy ${proxy.host}:${proxy.port} working, latency: ${latency}ms`);
|
|
resolve({ working: true, latency });
|
|
} else {
|
|
Utils.log('debug', `Proxy ${proxy.host}:${proxy.port} returned ${response.status}`);
|
|
// Clear proxy settings on failure
|
|
chrome.proxy.settings.clear({});
|
|
resolve({ working: false, latency: null });
|
|
}
|
|
} catch (error) {
|
|
Utils.log('debug', `Proxy ${proxy.host}:${proxy.port} failed: ${error.name} - ${error.message}`);
|
|
// Clear proxy settings on failure
|
|
chrome.proxy.settings.clear({});
|
|
resolve({ working: false, latency: null });
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Test multiple proxies in parallel
|
|
* @param {object[]} proxies - Array of proxy objects
|
|
* @param {number} concurrency - Max concurrent tests
|
|
* @returns {Promise<object[]>}
|
|
*/
|
|
async testProxiesParallel(proxies, concurrency = 5) {
|
|
const results = [];
|
|
const chunks = [];
|
|
|
|
// Split into chunks
|
|
for (let i = 0; i < proxies.length; i += concurrency) {
|
|
chunks.push(proxies.slice(i, i + concurrency));
|
|
}
|
|
|
|
// Process chunks sequentially, proxies within chunk in parallel
|
|
for (const chunk of chunks) {
|
|
const chunkResults = await Promise.all(
|
|
chunk.map(async (proxy) => {
|
|
const result = await this.testProxy(proxy);
|
|
return { ...proxy, ...result };
|
|
})
|
|
);
|
|
results.push(...chunkResults);
|
|
}
|
|
|
|
return results;
|
|
},
|
|
|
|
/**
|
|
* Find and connect to fastest working proxy
|
|
* @param {string} country - Country filter (optional)
|
|
* @param {boolean} bypassLocal - Allow local network access
|
|
* @param {string[]} exceptions - Bypass list
|
|
* @returns {Promise<{success: boolean, proxy: object|null, error: string|null}>}
|
|
*/
|
|
async connectToFastest(country = null, bypassLocal = true, exceptions = []) {
|
|
Utils.log('info', 'Finding fastest proxy', { country, exceptions });
|
|
|
|
let proxies = this._proxyList;
|
|
|
|
// Test proxies in parallel
|
|
const results = await this.testProxiesParallel(proxies);
|
|
|
|
// Filter working proxies and sort by latency
|
|
const working = results
|
|
.filter(p => p.working)
|
|
.sort((a, b) => a.latency - b.latency);
|
|
|
|
if (working.length === 0) {
|
|
return { success: false, proxy: null, error: 'No working proxies found' };
|
|
}
|
|
|
|
// Connect to fastest
|
|
const fastest = working[0];
|
|
const applied = await this.applyProxy(fastest.host, fastest.port, exceptions, bypassLocal);
|
|
|
|
if (applied) {
|
|
await Storage.setCurrentProxy(fastest);
|
|
await Storage.setProxyEnabled(true);
|
|
return { success: true, proxy: fastest, error: null };
|
|
}
|
|
|
|
return { success: false, proxy: null, error: 'Failed to apply proxy settings' };
|
|
},
|
|
|
|
/**
|
|
* Connect using custom proxy
|
|
* @param {string} ip - Proxy IP
|
|
* @param {number} port - Proxy port
|
|
* @param {string[]} exceptions - Bypass list
|
|
* @param {boolean} bypassLocal - Allow local network access
|
|
* @returns {Promise<{success: boolean, error: string|null}>}
|
|
*/
|
|
async connectCustom(ip, port, exceptions = [], bypassLocal = true) {
|
|
// Validate
|
|
const validation = Utils.validateProxy(ip, port);
|
|
if (!validation.valid) {
|
|
return { success: false, error: validation.error };
|
|
}
|
|
|
|
// Test connection
|
|
const testResult = await this.testProxy({ host: ip, port });
|
|
if (!testResult.working) {
|
|
return { success: false, error: 'Proxy is not responding' };
|
|
}
|
|
|
|
// Apply
|
|
const applied = await this.applyProxy(ip, port, exceptions, bypassLocal);
|
|
if (applied) {
|
|
const proxy = { host: ip, port, type: 'custom' };
|
|
// Get existing credentials to preserve them when updating storage
|
|
const existingProxy = await Storage.getCustomProxy();
|
|
await Storage.setCustomProxy(ip, port, exceptions, existingProxy.username, existingProxy.password);
|
|
await Storage.setCurrentProxy(proxy);
|
|
await Storage.setProxyEnabled(true);
|
|
return { success: true, proxy, error: null };
|
|
}
|
|
|
|
return { success: false, proxy: null, error: 'Failed to apply proxy settings' };
|
|
},
|
|
|
|
/**
|
|
* Disconnect proxy
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async disconnect() {
|
|
const cleared = await this.clearProxy();
|
|
if (cleared) {
|
|
await Storage.setProxyEnabled(false);
|
|
await Storage.setCurrentProxy(null);
|
|
}
|
|
return cleared;
|
|
},
|
|
|
|
/**
|
|
* Fetch proxy list from source with fallback
|
|
* @param {string} source - Source key from CONFIG.PROXY_SOURCES
|
|
* @returns {Promise<{success: boolean, proxies: object[], error: string|null, usedSource: string|null}>}
|
|
*/
|
|
async fetchProxyList(source = 'arweave') {
|
|
// Define fallback order
|
|
const fallbackOrder = ['arweave', 'git', 'github'];
|
|
|
|
// Start with the requested source, then try others
|
|
const sourcesToTry = [source, ...fallbackOrder.filter(s => s !== source)];
|
|
|
|
for (const currentSource of sourcesToTry) {
|
|
const sourceConfig = CONFIG.PROXY_SOURCES[currentSource];
|
|
if (!sourceConfig) continue;
|
|
|
|
Utils.log('info', `Fetching proxies from ${currentSource}`);
|
|
|
|
try {
|
|
const response = await Utils.fetchWithTimeout(
|
|
sourceConfig.url,
|
|
{},
|
|
CONFIG.TIMEOUTS.FETCH
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Normalize data format
|
|
let proxies = Array.isArray(data) ? data : data.proxies || [];
|
|
|
|
// Ensure each proxy has required fields
|
|
proxies = proxies.map(p => ({
|
|
host: p.host || p.ip,
|
|
port: parseInt(p.port, 10),
|
|
country: p.country || null,
|
|
country_name: p.country_name || null,
|
|
city: p.city || null,
|
|
latency: null
|
|
}));
|
|
|
|
this._proxyList = proxies;
|
|
await Storage.setProxyList(proxies);
|
|
|
|
if (currentSource !== source) {
|
|
Utils.log('info', `Fallback: fetched ${proxies.length} proxies from ${currentSource} (${source} failed)`);
|
|
} else {
|
|
Utils.log('info', `Fetched ${proxies.length} proxies from ${currentSource}`);
|
|
}
|
|
|
|
return { success: true, proxies, error: null, usedSource: currentSource };
|
|
|
|
} catch (error) {
|
|
Utils.log('warn', `Failed to fetch from ${currentSource}: ${error.message}`);
|
|
// Continue to next source
|
|
}
|
|
}
|
|
|
|
// All sources failed
|
|
Utils.log('error', 'All proxy sources failed');
|
|
return { success: false, proxies: [], error: 'All proxy sources failed', usedSource: null };
|
|
},
|
|
|
|
/**
|
|
* Fallback to next proxy
|
|
* @returns {Promise<{success: boolean, proxy: object|null, error: string|null}>}
|
|
*/
|
|
async fallbackToNext() {
|
|
if (this._proxyList.length === 0) {
|
|
return { success: false, proxy: null, error: 'No proxies available. Please refresh the proxy list.' };
|
|
}
|
|
|
|
if (this._proxyList.length === 1) {
|
|
return { success: false, proxy: null, error: 'Only one proxy available' };
|
|
}
|
|
|
|
this._currentIndex = (this._currentIndex + 1) % this._proxyList.length;
|
|
const proxy = this._proxyList[this._currentIndex];
|
|
|
|
Utils.log('info', `Switching to proxy ${this._currentIndex + 1}/${this._proxyList.length}: ${proxy.host}:${proxy.port}`);
|
|
|
|
// Test the proxy first to get latency
|
|
const testResult = await this.testProxy(proxy);
|
|
if (!testResult.working) {
|
|
// Try next proxy if this one doesn't work
|
|
Utils.log('warn', `Proxy ${proxy.host}:${proxy.port} not working, trying next...`);
|
|
return this.fallbackToNext();
|
|
}
|
|
|
|
// Merge test results (latency) with proxy info
|
|
const proxyWithLatency = { ...proxy, latency: testResult.latency };
|
|
|
|
// Get bypass local setting and exceptions
|
|
const bypassLocal = await Storage.getValue(CONFIG.STORAGE_KEYS.BYPASS_LOCAL, true);
|
|
const exceptions = await Storage.getValue(CONFIG.STORAGE_KEYS.EXCEPTIONS, []);
|
|
|
|
const applied = await this.applyProxy(proxy.host, proxy.port, exceptions, bypassLocal);
|
|
if (applied) {
|
|
await Storage.setCurrentProxy(proxyWithLatency);
|
|
return { success: true, proxy: proxyWithLatency, error: null };
|
|
}
|
|
|
|
return { success: false, proxy: null, error: 'Failed to apply proxy settings' };
|
|
},
|
|
|
|
/**
|
|
* Get current status
|
|
* @returns {object}
|
|
*/
|
|
getStatus() {
|
|
return {
|
|
enabled: this._isEnabled,
|
|
currentProxy: this._currentProxy,
|
|
proxyCount: this._proxyList.length
|
|
};
|
|
}
|
|
};
|
|
|
|
// ES Module export
|
|
export { ProxyManager };
|