s3db.js 13.6.0 → 14.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -43
- package/dist/s3db.cjs +72425 -38970
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72177 -38764
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +94 -49
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +180 -41
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/api/utils/template-engine.js +77 -3
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/README.md +126 -126
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProxyManager - Manages proxy pools and session-proxy binding
|
|
3
|
+
*
|
|
4
|
+
* Key Concept: IMMUTABLE BINDING
|
|
5
|
+
* Once a session is created with a proxy, they are bound forever.
|
|
6
|
+
* This prevents fingerprint leakage and maintains consistency.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Proxy pool management
|
|
10
|
+
* - Round-robin, random, or least-used selection
|
|
11
|
+
* - Health monitoring
|
|
12
|
+
* - Session-proxy immutable binding
|
|
13
|
+
* - Proxy authentication support
|
|
14
|
+
*/
|
|
15
|
+
import { BrowserPoolError } from '../puppeteer.errors.js';
|
|
16
|
+
|
|
17
|
+
export class ProxyManager {
|
|
18
|
+
constructor(plugin) {
|
|
19
|
+
this.plugin = plugin;
|
|
20
|
+
this.config = plugin.config.proxy;
|
|
21
|
+
this.storage = null;
|
|
22
|
+
|
|
23
|
+
// Proxy pool
|
|
24
|
+
this.proxies = []; // Array of proxy configs
|
|
25
|
+
this.proxyStats = new Map(); // proxyId -> { requests, failures, lastUsed, healthy }
|
|
26
|
+
|
|
27
|
+
// Session-Proxy binding (IMMUTABLE!)
|
|
28
|
+
this.sessionProxyMap = new Map(); // sessionId -> proxyId
|
|
29
|
+
|
|
30
|
+
// Proxy selection strategy
|
|
31
|
+
this.selectionStrategy = this.config.selectionStrategy || 'round-robin';
|
|
32
|
+
this.currentProxyIndex = 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize proxy manager
|
|
37
|
+
*/
|
|
38
|
+
async initialize() {
|
|
39
|
+
// Load proxies from config
|
|
40
|
+
if (this.config.enabled && this.config.list && this.config.list.length > 0) {
|
|
41
|
+
this.proxies = this.config.list.map((proxy, index) => ({
|
|
42
|
+
id: `proxy_${index}`,
|
|
43
|
+
...this._parseProxy(proxy)
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Initialize stats for each proxy
|
|
47
|
+
for (const proxy of this.proxies) {
|
|
48
|
+
this.proxyStats.set(proxy.id, {
|
|
49
|
+
requests: 0,
|
|
50
|
+
failures: 0,
|
|
51
|
+
successRate: 1.0,
|
|
52
|
+
lastUsed: 0,
|
|
53
|
+
healthy: true,
|
|
54
|
+
createdAt: Date.now()
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.plugin.emit('proxyManager.initialized', {
|
|
59
|
+
count: this.proxies.length,
|
|
60
|
+
strategy: this.selectionStrategy
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Load session-proxy bindings if storage exists
|
|
65
|
+
if (this.plugin.cookieManager && this.plugin.cookieManager.storage) {
|
|
66
|
+
await this._loadSessionProxyBindings();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse proxy string or object
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
_parseProxy(proxy) {
|
|
75
|
+
if (typeof proxy === 'string') {
|
|
76
|
+
// Format: http://user:pass@host:port or http://host:port
|
|
77
|
+
const url = new URL(proxy);
|
|
78
|
+
return {
|
|
79
|
+
protocol: url.protocol.replace(':', ''),
|
|
80
|
+
host: url.hostname,
|
|
81
|
+
port: parseInt(url.port) || (url.protocol === 'https:' ? 443 : 80),
|
|
82
|
+
username: url.username || null,
|
|
83
|
+
password: url.password || null,
|
|
84
|
+
url: proxy
|
|
85
|
+
};
|
|
86
|
+
} else {
|
|
87
|
+
// Object format
|
|
88
|
+
const protocol = proxy.protocol || 'http';
|
|
89
|
+
const host = proxy.host;
|
|
90
|
+
const port = proxy.port || (protocol === 'https' ? 443 : 80);
|
|
91
|
+
const username = proxy.username || null;
|
|
92
|
+
const password = proxy.password || null;
|
|
93
|
+
|
|
94
|
+
// Build URL
|
|
95
|
+
let url = `${protocol}://`;
|
|
96
|
+
if (username && password) {
|
|
97
|
+
url += `${username}:${password}@`;
|
|
98
|
+
}
|
|
99
|
+
url += `${host}:${port}`;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
protocol,
|
|
103
|
+
host,
|
|
104
|
+
port,
|
|
105
|
+
username,
|
|
106
|
+
password,
|
|
107
|
+
url
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Load session-proxy bindings from storage
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
async _loadSessionProxyBindings() {
|
|
117
|
+
const storage = this.plugin.cookieManager.storage;
|
|
118
|
+
const sessions = await storage.list({ limit: 1000 });
|
|
119
|
+
|
|
120
|
+
for (const session of sessions) {
|
|
121
|
+
if (session.proxyId) {
|
|
122
|
+
this.sessionProxyMap.set(session.sessionId, session.proxyId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.plugin.emit('proxyManager.bindingsLoaded', {
|
|
127
|
+
count: this.sessionProxyMap.size
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get proxy for a session (respecting immutable binding)
|
|
133
|
+
* @param {string} sessionId - Session identifier
|
|
134
|
+
* @param {boolean} createIfMissing - Create new binding if session is new
|
|
135
|
+
* @returns {Object|null} - Proxy config or null
|
|
136
|
+
*/
|
|
137
|
+
getProxyForSession(sessionId, createIfMissing = true) {
|
|
138
|
+
if (!this.config.enabled || this.proxies.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if session already has a proxy bound (IMMUTABLE!)
|
|
143
|
+
if (this.sessionProxyMap.has(sessionId)) {
|
|
144
|
+
const proxyId = this.sessionProxyMap.get(sessionId);
|
|
145
|
+
const proxy = this.proxies.find(p => p.id === proxyId);
|
|
146
|
+
|
|
147
|
+
if (!proxy) {
|
|
148
|
+
throw new BrowserPoolError(`Proxy ${proxyId} bound to session ${sessionId} not found in pool`, {
|
|
149
|
+
operation: 'getProxyForSession',
|
|
150
|
+
retriable: false,
|
|
151
|
+
suggestion: 'Ensure proxies remain registered while sessions are active.',
|
|
152
|
+
proxyId,
|
|
153
|
+
sessionId
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Verify proxy is still healthy
|
|
158
|
+
const stats = this.proxyStats.get(proxyId);
|
|
159
|
+
if (!stats || !stats.healthy) {
|
|
160
|
+
throw new BrowserPoolError(`Proxy ${proxyId} bound to session ${sessionId} is unhealthy`, {
|
|
161
|
+
operation: 'getProxyForSession',
|
|
162
|
+
retriable: true,
|
|
163
|
+
suggestion: 'Rebind the session to a healthy proxy or refresh the proxy pool.',
|
|
164
|
+
proxyId,
|
|
165
|
+
sessionId
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return proxy;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// New session - select proxy if createIfMissing
|
|
173
|
+
if (createIfMissing) {
|
|
174
|
+
const proxy = this._selectProxy();
|
|
175
|
+
|
|
176
|
+
if (proxy) {
|
|
177
|
+
// Create IMMUTABLE binding
|
|
178
|
+
this.sessionProxyMap.set(sessionId, proxy.id);
|
|
179
|
+
|
|
180
|
+
this.plugin.emit('proxyManager.sessionBound', {
|
|
181
|
+
sessionId,
|
|
182
|
+
proxyId: proxy.id,
|
|
183
|
+
proxyUrl: this._maskProxyUrl(proxy.url)
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return proxy;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Select proxy based on strategy
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
_selectProxy() {
|
|
198
|
+
// Filter healthy proxies only
|
|
199
|
+
const healthyProxies = this.proxies.filter(proxy => {
|
|
200
|
+
const stats = this.proxyStats.get(proxy.id);
|
|
201
|
+
return stats ? stats.healthy : false;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (healthyProxies.length === 0) {
|
|
205
|
+
throw new BrowserPoolError('No healthy proxies available', {
|
|
206
|
+
operation: '_selectProxy',
|
|
207
|
+
retriable: true,
|
|
208
|
+
suggestion: 'Add healthy proxies to the configuration or allow existing proxies to recover.',
|
|
209
|
+
available: this.proxies.length
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let selectedProxy;
|
|
214
|
+
|
|
215
|
+
switch (this.selectionStrategy) {
|
|
216
|
+
case 'round-robin':
|
|
217
|
+
selectedProxy = healthyProxies[this.currentProxyIndex % healthyProxies.length];
|
|
218
|
+
this.currentProxyIndex++;
|
|
219
|
+
break;
|
|
220
|
+
|
|
221
|
+
case 'random':
|
|
222
|
+
selectedProxy = healthyProxies[Math.floor(Math.random() * healthyProxies.length)];
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case 'least-used':
|
|
226
|
+
// Select proxy with lowest request count
|
|
227
|
+
selectedProxy = healthyProxies.reduce((min, proxy) => {
|
|
228
|
+
const proxyStats = this.proxyStats.get(proxy.id);
|
|
229
|
+
const minStats = this.proxyStats.get(min.id);
|
|
230
|
+
return proxyStats.requests < minStats.requests ? proxy : min;
|
|
231
|
+
});
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
case 'best-performance':
|
|
235
|
+
// Select proxy with highest success rate
|
|
236
|
+
selectedProxy = healthyProxies.reduce((best, proxy) => {
|
|
237
|
+
const proxyStats = this.proxyStats.get(proxy.id);
|
|
238
|
+
const bestStats = this.proxyStats.get(best.id);
|
|
239
|
+
return proxyStats.successRate > bestStats.successRate ? proxy : best;
|
|
240
|
+
});
|
|
241
|
+
break;
|
|
242
|
+
|
|
243
|
+
default:
|
|
244
|
+
selectedProxy = healthyProxies[0];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return selectedProxy;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Record proxy usage
|
|
252
|
+
* @param {string} proxyId - Proxy identifier
|
|
253
|
+
* @param {boolean} success - Whether request succeeded
|
|
254
|
+
*/
|
|
255
|
+
recordProxyUsage(proxyId, success = true) {
|
|
256
|
+
const stats = this.proxyStats.get(proxyId);
|
|
257
|
+
if (!stats) return;
|
|
258
|
+
|
|
259
|
+
stats.requests++;
|
|
260
|
+
stats.lastUsed = Date.now();
|
|
261
|
+
|
|
262
|
+
if (success) {
|
|
263
|
+
// Update success rate with exponential moving average
|
|
264
|
+
stats.successRate = stats.successRate * 0.9 + 0.1;
|
|
265
|
+
} else {
|
|
266
|
+
stats.failures++;
|
|
267
|
+
stats.successRate = stats.successRate * 0.9;
|
|
268
|
+
|
|
269
|
+
// Mark unhealthy if success rate drops below threshold
|
|
270
|
+
const threshold = this.config.healthCheck?.successRateThreshold || 0.3;
|
|
271
|
+
if (stats.successRate < threshold) {
|
|
272
|
+
stats.healthy = false;
|
|
273
|
+
this.plugin.emit('proxyManager.proxyUnhealthy', {
|
|
274
|
+
proxyId,
|
|
275
|
+
successRate: stats.successRate
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get proxy statistics
|
|
283
|
+
* @returns {Array}
|
|
284
|
+
*/
|
|
285
|
+
getProxyStats() {
|
|
286
|
+
return this.proxies.map(proxy => {
|
|
287
|
+
const stats = this.proxyStats.get(proxy.id);
|
|
288
|
+
return {
|
|
289
|
+
proxyId: proxy.id,
|
|
290
|
+
url: this._maskProxyUrl(proxy.url),
|
|
291
|
+
...stats,
|
|
292
|
+
boundSessions: Array.from(this.sessionProxyMap.entries())
|
|
293
|
+
.filter(([_, proxyId]) => proxyId === proxy.id)
|
|
294
|
+
.length
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get session-proxy bindings
|
|
301
|
+
* @returns {Array}
|
|
302
|
+
*/
|
|
303
|
+
getSessionBindings() {
|
|
304
|
+
return Array.from(this.sessionProxyMap.entries()).map(([sessionId, proxyId]) => {
|
|
305
|
+
const proxy = this.proxies.find(p => p.id === proxyId);
|
|
306
|
+
return {
|
|
307
|
+
sessionId,
|
|
308
|
+
proxyId,
|
|
309
|
+
proxyUrl: proxy ? this._maskProxyUrl(proxy.url) : 'unknown'
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Verify session-proxy binding integrity
|
|
316
|
+
* @param {string} sessionId - Session identifier
|
|
317
|
+
* @param {string} proxyId - Proxy identifier
|
|
318
|
+
* @returns {boolean}
|
|
319
|
+
*/
|
|
320
|
+
verifyBinding(sessionId, proxyId) {
|
|
321
|
+
if (!this.sessionProxyMap.has(sessionId)) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const boundProxyId = this.sessionProxyMap.get(sessionId);
|
|
326
|
+
return boundProxyId === proxyId;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get proxy config for browser launch
|
|
331
|
+
* @param {Object} proxy - Proxy object
|
|
332
|
+
* @returns {Object} - Puppeteer proxy config
|
|
333
|
+
*/
|
|
334
|
+
getProxyLaunchArgs(proxy) {
|
|
335
|
+
if (!proxy) return [];
|
|
336
|
+
|
|
337
|
+
const args = [`--proxy-server=${proxy.url}`];
|
|
338
|
+
|
|
339
|
+
// Add proxy bypass list if configured
|
|
340
|
+
if (this.config.bypassList && this.config.bypassList.length > 0) {
|
|
341
|
+
args.push(`--proxy-bypass-list=${this.config.bypassList.join(';')}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return args;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Authenticate proxy on page
|
|
349
|
+
* @param {Page} page - Puppeteer page
|
|
350
|
+
* @param {Object} proxy - Proxy object
|
|
351
|
+
*/
|
|
352
|
+
async authenticateProxy(page, proxy) {
|
|
353
|
+
if (proxy.username && proxy.password) {
|
|
354
|
+
await page.authenticate({
|
|
355
|
+
username: proxy.username,
|
|
356
|
+
password: proxy.password
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Check proxy health
|
|
363
|
+
* @param {string} proxyId - Proxy identifier
|
|
364
|
+
* @returns {Promise<boolean>}
|
|
365
|
+
*/
|
|
366
|
+
async checkProxyHealth(proxyId) {
|
|
367
|
+
const proxy = this.proxies.find(p => p.id === proxyId);
|
|
368
|
+
if (!proxy) return false;
|
|
369
|
+
|
|
370
|
+
const stats = this.proxyStats.get(proxyId);
|
|
371
|
+
if (!stats) return false;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
// Launch browser with this proxy
|
|
375
|
+
const browser = await this.plugin.puppeteer.launch({
|
|
376
|
+
...this.plugin.config.launch,
|
|
377
|
+
args: [
|
|
378
|
+
...this.plugin.config.launch.args,
|
|
379
|
+
...this.getProxyLaunchArgs(proxy)
|
|
380
|
+
]
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const page = await browser.newPage();
|
|
384
|
+
|
|
385
|
+
// Authenticate if needed
|
|
386
|
+
await this.authenticateProxy(page, proxy);
|
|
387
|
+
|
|
388
|
+
// Try to fetch a test page
|
|
389
|
+
const testUrl = this.config.healthCheck?.testUrl || 'https://www.google.com';
|
|
390
|
+
const timeout = this.config.healthCheck?.timeout || 10000;
|
|
391
|
+
|
|
392
|
+
await page.goto(testUrl, { timeout });
|
|
393
|
+
|
|
394
|
+
await browser.close();
|
|
395
|
+
|
|
396
|
+
// Mark as healthy
|
|
397
|
+
stats.healthy = true;
|
|
398
|
+
stats.successRate = Math.min(stats.successRate + 0.1, 1.0);
|
|
399
|
+
|
|
400
|
+
this.plugin.emit('proxyManager.healthCheckPassed', {
|
|
401
|
+
proxyId,
|
|
402
|
+
url: this._maskProxyUrl(proxy.url)
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return true;
|
|
406
|
+
} catch (err) {
|
|
407
|
+
// Mark as unhealthy
|
|
408
|
+
stats.healthy = false;
|
|
409
|
+
stats.failures++;
|
|
410
|
+
|
|
411
|
+
this.plugin.emit('proxyManager.healthCheckFailed', {
|
|
412
|
+
proxyId,
|
|
413
|
+
url: this._maskProxyUrl(proxy.url),
|
|
414
|
+
error: err.message
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Run health checks on all proxies
|
|
423
|
+
* @returns {Promise<Object>}
|
|
424
|
+
*/
|
|
425
|
+
async checkAllProxies() {
|
|
426
|
+
const results = {
|
|
427
|
+
total: this.proxies.length,
|
|
428
|
+
healthy: 0,
|
|
429
|
+
unhealthy: 0,
|
|
430
|
+
checks: []
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
for (const proxy of this.proxies) {
|
|
434
|
+
const isHealthy = await this.checkProxyHealth(proxy.id);
|
|
435
|
+
results.checks.push({
|
|
436
|
+
proxyId: proxy.id,
|
|
437
|
+
url: this._maskProxyUrl(proxy.url),
|
|
438
|
+
healthy: isHealthy
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (isHealthy) {
|
|
442
|
+
results.healthy++;
|
|
443
|
+
} else {
|
|
444
|
+
results.unhealthy++;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return results;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Mask proxy URL for logging (hide credentials)
|
|
453
|
+
* @private
|
|
454
|
+
*/
|
|
455
|
+
_maskProxyUrl(url) {
|
|
456
|
+
try {
|
|
457
|
+
const parsed = new URL(url);
|
|
458
|
+
if (parsed.username) {
|
|
459
|
+
return `${parsed.protocol}//${parsed.username}:***@${parsed.host}`;
|
|
460
|
+
}
|
|
461
|
+
return url;
|
|
462
|
+
} catch {
|
|
463
|
+
return url;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Remove session-proxy binding (only for cleanup/testing)
|
|
469
|
+
* WARNING: This breaks immutability! Only use when deleting sessions.
|
|
470
|
+
* @param {string} sessionId - Session identifier
|
|
471
|
+
*/
|
|
472
|
+
_removeBinding(sessionId) {
|
|
473
|
+
this.sessionProxyMap.delete(sessionId);
|
|
474
|
+
this.plugin.emit('proxyManager.bindingRemoved', { sessionId });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export default ProxyManager;
|