target-manager.js•11.5 kB
/**
* Target Manager
* Manages proxy target sites and routing rules
*/
export class TargetManager {
constructor() {
this.targets = new Map(); // Map<domain, config>
this.pacContent = '';
this.lastUpdated = new Date();
}
/**
* Add a target site for proxy monitoring
* @param {string} domain - Domain to monitor (e.g., 'example.com')
* @param {Object} config - Configuration options
* @returns {boolean} Success status
*/
addTarget(domain, config = {}) {
// Pre-conditions
if (!domain || typeof domain !== 'string') {
throw new Error('Domain is required and must be a string');
}
const targetConfig = {
domain: domain.toLowerCase(),
enabled: config.enabled !== false,
includeSubdomains: config.includeSubdomains !== false,
description: config.description || '',
captureHeaders: config.captureHeaders !== false, // Default to true
captureBody: config.captureBody !== false, // Default to true
addedAt: new Date(),
...config
};
// Post-conditions: validate critical fields
console.assert(typeof targetConfig.captureHeaders === 'boolean', 'captureHeaders must be boolean');
console.assert(typeof targetConfig.captureBody === 'boolean', 'captureBody must be boolean');
console.assert(typeof targetConfig.enabled === 'boolean', 'enabled must be boolean');
this.targets.set(domain.toLowerCase(), targetConfig);
this._updatePacFile();
console.log(`✅ Target added: ${domain} (captureHeaders=${targetConfig.captureHeaders}, captureBody=${targetConfig.captureBody})`);
return true;
}
/**
* Remove a target site
* @param {string} domain - Domain to remove
* @returns {boolean} Success status
*/
removeTarget(domain) {
if (!domain) {
throw new Error('Domain is required');
}
const removed = this.targets.delete(domain.toLowerCase());
if (removed) {
this._updatePacFile();
}
return removed;
}
/**
* Toggle target site on/off
* @param {string} domain - Domain to toggle
* @returns {boolean} New enabled status
*/
toggleTarget(domain) {
const target = this.targets.get(domain.toLowerCase());
if (!target) {
throw new Error(`Target domain '${domain}' not found`);
}
target.enabled = !target.enabled;
target.lastModified = new Date();
this._updatePacFile();
return target.enabled;
}
/**
* Get all target sites
* @param {string} statusFilter - Filter by status: 'all', 'active', 'inactive'
* @returns {Array} List of target configurations
*/
listTargets(statusFilter = 'all') {
const targets = Array.from(this.targets.values()).map(target => ({
...target,
status: target.enabled ? 'active' : 'inactive'
}));
if (statusFilter === 'active') {
return targets.filter(t => t.enabled);
} else if (statusFilter === 'inactive') {
return targets.filter(t => !t.enabled);
}
return targets;
}
/**
* Check if a request should be proxied
* @param {string} hostname - Request hostname (not full URL)
* @returns {boolean} Should proxy this request
*/
shouldProxy(hostname) {
try {
const normalizedHostname = hostname.toLowerCase();
for (const [domain, config] of this.targets) {
if (!config.enabled) continue;
if (config.includeSubdomains) {
if (normalizedHostname === domain || normalizedHostname.endsWith(`.${domain}`)) {
return true;
}
} else {
if (normalizedHostname === domain) {
return true;
}
}
}
return false;
} catch (error) {
console.error('Error checking proxy rule:', error);
return false;
}
}
/**
* Generate PAC (Proxy Auto-Configuration) file content
* @param {string} proxyHost - Proxy server host (default: localhost)
* @param {number} proxyPort - Proxy server port (default: 8080)
* @returns {string} PAC file content
*/
generatePacFile(proxyHost = 'localhost', proxyPort = 8080) {
const enabledTargets = Array.from(this.targets.values())
.filter(target => target.enabled);
const domainChecks = enabledTargets.map(target => {
if (target.includeSubdomains) {
return ` if (host === "${target.domain}" || host.endsWith(".${target.domain}")) return "PROXY ${proxyHost}:${proxyPort}";`;
} else {
return ` if (host === "${target.domain}") return "PROXY ${proxyHost}:${proxyPort}";`;
}
}).join('\n');
return `function FindProxyForURL(url, host) {
host = host.toLowerCase();
// Proxy specific domains through our proxy server
${domainChecks}
// All other traffic goes direct
return "DIRECT";
}
// Generated by Web Proxy MCP Server
// Last updated: ${new Date().toISOString()}
// Active targets: ${enabledTargets.length}`;
}
/**
* Update internal PAC file content
* @private
*/
_updatePacFile() {
this.pacContent = this.generatePacFile();
this.lastUpdated = new Date();
}
/**
* Get current PAC file content
* @returns {string} PAC file content
*/
getPacContent() {
return this.pacContent;
}
/**
* Get statistics about targets
* @returns {Object} Statistics
*/
getStats() {
const targets = Array.from(this.targets.values());
return {
total: targets.length,
enabled: targets.filter(t => t.enabled).length,
disabled: targets.filter(t => !t.enabled).length,
lastUpdated: this.lastUpdated,
domains: targets.map(t => t.domain)
};
}
/**
* Export all targets as JSON
* @returns {string} JSON string of all targets
*/
exportTargets() {
const exportData = {
version: '1.0',
exportedAt: new Date().toISOString(),
targets: Object.fromEntries(this.targets)
};
return JSON.stringify(exportData, null, 2);
}
/**
* Import targets from JSON
* @param {string} jsonData - JSON string to import
* @returns {number} Number of targets imported
*/
importTargets(jsonData) {
try {
const data = JSON.parse(jsonData);
let imported = 0;
if (data.targets && typeof data.targets === 'object') {
for (const [domain, config] of Object.entries(data.targets)) {
this.addTarget(domain, config);
imported++;
}
}
return imported;
} catch (error) {
throw new Error(`Failed to import targets: ${error.message}`);
}
}
/**
* Load targets from file
* @param {string} filepath - File path to load from
*/
async loadTargets(filepath = './data/targets.json') {
try {
const fs = await import('fs/promises');
const data = await fs.readFile(filepath, 'utf-8');
const parsed = JSON.parse(data);
this.targets.clear();
if (parsed.targets && typeof parsed.targets === 'object') {
for (const [domain, config] of Object.entries(parsed.targets)) {
this.targets.set(domain.toLowerCase(), {
domain: config.domain,
description: config.description || '',
enabled: config.enabled !== false,
includeSubdomains: config.includeSubdomains !== false,
captureHeaders: config.captureHeaders !== false,
captureBody: config.captureBody || false,
createdAt: config.createdAt ? new Date(config.createdAt) : new Date(),
lastModified: config.lastModified ? new Date(config.lastModified) : new Date()
});
}
this._updatePacFile();
console.log(`📋 Loaded ${this.targets.size} targets from ${filepath}`);
}
} catch (error) {
if (error.code === 'ENOENT') {
console.log(`📋 No existing targets file found at ${filepath}, starting fresh`);
} else {
console.error(`Failed to load targets: ${error.message}`);
}
}
}
/**
* Save targets to file
* @param {string} filepath - File path to save to
*/
async saveTargets(filepath = './data/targets.json') {
try {
const fs = await import('fs/promises');
const path = await import('path');
// Ensure directory exists
const dir = path.dirname(filepath);
await fs.mkdir(dir, { recursive: true });
const exportData = {
version: '1.0',
savedAt: new Date().toISOString(),
targets: Object.fromEntries(this.targets)
};
await fs.writeFile(filepath, JSON.stringify(exportData, null, 2));
console.log(`💾 Saved ${this.targets.size} targets to ${filepath}`);
} catch (error) {
console.error(`Failed to save targets: ${error.message}`);
throw error;
}
}
/**
* Import targets from file
* @param {string} filepath - File path to import from
* @param {boolean} merge - Whether to merge with existing targets
* @returns {Object} Import results
*/
async importFromFile(filepath, merge = false) {
try {
const fs = await import('fs/promises');
const data = await fs.readFile(filepath, 'utf-8');
if (!merge) {
this.targets.clear();
}
const imported = this.importTargets(data);
return { imported, merge };
} catch (error) {
throw new Error(`Failed to import from file: ${error.message}`);
}
}
/**
* Export targets to file
* @param {string} filepath - File path to export to
* @param {Object} additionalData - Additional data to include
* @returns {Object} Export results
*/
async exportToFile(filepath, additionalData = {}) {
try {
const fs = await import('fs/promises');
const path = await import('path');
// Ensure directory exists
const dir = path.dirname(filepath);
await fs.mkdir(dir, { recursive: true });
const exportData = {
version: '1.0',
exportedAt: new Date().toISOString(),
targets: Object.fromEntries(this.targets),
...additionalData
};
const content = JSON.stringify(exportData, null, 2);
await fs.writeFile(filepath, content);
return {
filepath,
targetCount: this.targets.size,
fileSize: content.length
};
} catch (error) {
throw new Error(`Failed to export to file: ${error.message}`);
}
}
/**
* Find target by domain
* @param {string} domain - Domain to find
* @returns {Object|null} Target configuration or null
*/
findTarget(domain) {
return this.targets.get(domain.toLowerCase()) || null;
}
/**
* Update target configuration
* @param {string} domain - Domain to update
* @param {Object} updates - Updates to apply
* @returns {boolean} Whether target was updated
*/
updateTarget(domain, updates) {
const target = this.targets.get(domain.toLowerCase());
if (!target) {
return false;
}
// Apply updates
if ('enabled' in updates) target.enabled = updates.enabled;
if ('description' in updates) target.description = updates.description;
if ('includeSubdomains' in updates) target.includeSubdomains = updates.includeSubdomains;
if ('captureHeaders' in updates) target.captureHeaders = updates.captureHeaders;
if ('captureBody' in updates) target.captureBody = updates.captureBody;
target.lastModified = new Date();
this._updatePacFile();
return true;
}
}