Files
cannaiq/backend/src/services/proxy.ts
Kelly 6eb1babc86 feat: Auto-migrations on startup, worker exit location, proxy improvements
- Add auto-migration system that runs SQL files from migrations/ on server startup
- Track applied migrations in schema_migrations table
- Show proxy exit location in Workers dashboard
- Add "Cleanup Stale" button to remove old workers
- Add remove button for individual workers
- Include proxy location (city, state, country) in worker heartbeats
- Update Proxy interface with location fields
- Re-enable bulk proxy import without ON CONFLICT

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 08:05:24 -07:00

371 lines
10 KiB
TypeScript
Executable File

import axios from 'axios';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { pool } from '../db/pool';
interface ProxyTestResult {
success: boolean;
responseTimeMs?: number;
isAnonymous?: boolean;
error?: string;
}
// In-memory proxy timeout tracking
// Maps proxy ID to timestamp when timeout expires
const proxyTimeouts = new Map<number, number>();
const PROXY_TIMEOUT_MS = 35000; // 35 seconds timeout for bot-detected proxies
// Check if error message indicates bot detection
export function isBotDetectionError(errorMsg: string): boolean {
const botPatterns = [
/bot detection/i,
/captcha/i,
/challenge/i,
/cloudflare/i,
/access denied/i,
/rate limit/i,
/too many requests/i,
/temporarily blocked/i,
/suspicious activity/i,
];
return botPatterns.some(pattern => pattern.test(errorMsg));
}
// Put proxy in timeout (bot detection cooldown)
export function putProxyInTimeout(proxyId: number, reason: string): void {
const timeoutUntil = Date.now() + PROXY_TIMEOUT_MS;
proxyTimeouts.set(proxyId, timeoutUntil);
console.log(`🚫 Proxy ${proxyId} in timeout for ${PROXY_TIMEOUT_MS/1000}s: ${reason}`);
}
// Check if proxy is currently in timeout
export function isProxyInTimeout(proxyId: number): boolean {
const timeoutUntil = proxyTimeouts.get(proxyId);
if (!timeoutUntil) return false;
if (Date.now() >= timeoutUntil) {
// Timeout expired, remove it
proxyTimeouts.delete(proxyId);
console.log(`✅ Proxy ${proxyId} timeout expired, back in rotation`);
return false;
}
return true;
}
// Get active proxy that's not in timeout
export async function getActiveProxy(): Promise<{ id: number; host: string; port: number; protocol: string; username?: string; password?: string } | null> {
const result = await pool.query(`
SELECT id, host, port, protocol, username, password
FROM proxies
WHERE active = true
ORDER BY RANDOM()
`);
// Filter out proxies in timeout
for (const proxy of result.rows) {
if (!isProxyInTimeout(proxy.id)) {
return proxy;
}
}
// All proxies are in timeout, wait for first one to expire
if (proxyTimeouts.size > 0) {
const nextAvailable = Math.min(...Array.from(proxyTimeouts.values()));
const waitTime = Math.max(0, nextAvailable - Date.now());
console.log(`⏳ All proxies in timeout, waiting ${Math.ceil(waitTime/1000)}s for next available...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
// Try again after waiting
return getActiveProxy();
}
console.log('⚠️ No active proxies available');
return null;
}
async function getSettings(): Promise<{ timeout: number; testUrl: string }> {
const result = await pool.query(`
SELECT key, value FROM settings
WHERE key IN ('proxy_timeout_ms', 'proxy_test_url')
`);
const settings: Record<string, string> = {};
result.rows.forEach((row: { key: string; value: string }) => {
settings[row.key] = row.value;
});
return {
timeout: parseInt(settings.proxy_timeout_ms || '3000'),
testUrl: settings.proxy_test_url || 'https://httpbin.org/ip'
};
}
export async function testProxy(
host: string,
port: number,
protocol: string,
username?: string,
password?: string
): Promise<ProxyTestResult> {
try {
const { timeout, testUrl } = await getSettings();
const startTime = Date.now();
// Construct proxy URL
let proxyUrl: string;
if (username && password) {
proxyUrl = `${protocol}://${username}:${password}@${host}:${port}`;
} else {
proxyUrl = `${protocol}://${host}:${port}`;
}
// Create appropriate agent based on protocol
let agent;
if (protocol === 'socks5' || protocol === 'socks') {
agent = new SocksProxyAgent(proxyUrl);
} else if (protocol === 'http' || protocol === 'https') {
agent = new HttpsProxyAgent(proxyUrl);
} else {
return {
success: false,
error: `Unsupported protocol: ${protocol}`
};
}
// Make test request
const response = await axios.get(testUrl, {
httpAgent: agent,
httpsAgent: agent,
timeout,
});
const responseTimeMs = Date.now() - startTime;
// Check anonymity - the test URL should return our IP
// If it returns the proxy's IP, we're anonymous
let isAnonymous = false;
if (response.data && response.data.origin) {
// If the returned IP is different from our actual IP, the proxy is working
// For simplicity, we'll consider it anonymous if we get a response
isAnonymous = true;
}
return {
success: true,
responseTimeMs,
isAnonymous
};
} catch (error: any) {
return {
success: false,
error: error.message || 'Unknown error'
};
}
}
export async function saveProxyTestResult(
proxyId: number,
result: ProxyTestResult
): Promise<void> {
await pool.query(`
UPDATE proxies
SET last_tested_at = CURRENT_TIMESTAMP,
test_result = $1,
response_time_ms = $2,
is_anonymous = $3,
active = $4,
updated_at = CURRENT_TIMESTAMP
WHERE id = $5
`, [
result.success ? 'success' : 'failed',
result.responseTimeMs || null,
result.isAnonymous || false,
result.success,
proxyId
]);
}
export async function testAllProxies(): Promise<void> {
console.log('🔍 Testing all proxies...');
const result = await pool.query(`
SELECT id, host, port, protocol, username, password
FROM proxies
`);
for (const proxy of result.rows) {
console.log(`Testing proxy: ${proxy.protocol}://${proxy.host}:${proxy.port}`);
const testResult = await testProxy(
proxy.host,
proxy.port,
proxy.protocol,
proxy.username,
proxy.password
);
await saveProxyTestResult(proxy.id, testResult);
if (testResult.success) {
console.log(`✅ Proxy OK (${testResult.responseTimeMs}ms, anonymous: ${testResult.isAnonymous})`);
} else {
console.log(`❌ Proxy failed: ${testResult.error}`);
}
// Small delay between tests
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log('✅ Proxy testing complete');
}
export async function addProxy(
host: string,
port: number,
protocol: string,
username?: string,
password?: string
): Promise<number> {
// Test the proxy first
const testResult = await testProxy(host, port, protocol, username, password);
if (!testResult.success) {
throw new Error(`Proxy test failed: ${testResult.error}`);
}
// Insert into database
const result = await pool.query(`
INSERT INTO proxies (host, port, protocol, username, password, active, is_anonymous, test_result, response_time_ms, last_tested_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP)
RETURNING id
`, [
host,
port,
protocol,
username,
password,
testResult.success,
testResult.isAnonymous,
'success',
testResult.responseTimeMs
]);
return result.rows[0].id;
}
export async function addProxiesFromList(proxies: Array<{
host: string;
port: number;
protocol: string;
username?: string;
password?: string;
}>): Promise<{ added: number; failed: number; duplicates: number; errors: string[] }> {
let added = 0;
let failed = 0;
let duplicates = 0;
const errors: string[] = [];
console.log(`📥 Importing ${proxies.length} proxies without testing...`);
for (const proxy of proxies) {
try {
// Insert without testing first
await pool.query(`
INSERT INTO proxies (host, port, protocol, username, password, active)
VALUES ($1, $2, $3, $4, $5, false)
`, [
proxy.host,
proxy.port,
proxy.protocol,
proxy.username,
proxy.password
]);
added++;
if (added % 100 === 0) {
console.log(`📥 Imported ${added} proxies...`);
}
} catch (error: any) {
failed++;
const errorMsg = `${proxy.host}:${proxy.port} - ${error.message}`;
errors.push(errorMsg);
console.log(`❌ Failed to add proxy: ${errorMsg}`);
}
}
console.log(`✅ Import complete: ${added} added, ${duplicates} duplicates, ${failed} failed`);
return { added, failed, duplicates, errors };
}
export async function moveProxyToFailed(proxyId: number, errorMsg: string): Promise<void> {
// Get proxy details
const proxyResult = await pool.query(`
SELECT host, port, protocol, username, password, failure_count
FROM proxies
WHERE id = $1
`, [proxyId]);
if (proxyResult.rows.length === 0) {
return;
}
const proxy = proxyResult.rows[0];
// Insert into failed_proxies table
await pool.query(`
INSERT INTO failed_proxies (host, port, protocol, username, password, failure_count, last_error)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (host, port, protocol)
DO UPDATE SET
failure_count = $6,
last_error = $7,
failed_at = CURRENT_TIMESTAMP
`, [
proxy.host,
proxy.port,
proxy.protocol,
proxy.username,
proxy.password,
proxy.failure_count,
errorMsg
]);
// Delete from active proxies
await pool.query(`DELETE FROM proxies WHERE id = $1`, [proxyId]);
console.log(`🔴 Moved proxy to failed: ${proxy.protocol}://${proxy.host}:${proxy.port} (${proxy.failure_count} failures)`);
}
export async function incrementProxyFailure(proxyId: number, errorMsg: string): Promise<boolean> {
// Increment failure count
const result = await pool.query(`
UPDATE proxies
SET failure_count = failure_count + 1,
active = false,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING failure_count, host, port, protocol
`, [proxyId]);
if (result.rows.length === 0) {
return false;
}
const proxy = result.rows[0];
const failureCount = proxy.failure_count;
console.log(`⚠️ Proxy failure #${failureCount}: ${proxy.protocol}://${proxy.host}:${proxy.port}`);
// If failed 3 times, move to failed table
if (failureCount >= 3) {
await moveProxyToFailed(proxyId, errorMsg);
return true; // Moved to failed
}
return false; // Still in active proxies
}