This commit is contained in:
zhom
2025-05-29 10:17:16 +04:00
commit 08678dcacc
154 changed files with 29456 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
import { program } from "commander";
import {
startProxyProcess,
stopProxyProcess,
stopAllProxyProcesses,
} from "./proxy-runner";
import { listProxyConfigs } from "./proxy-storage";
import { runProxyWorker } from "./proxy-worker";
// Command for proxy management
program
.command("proxy")
.argument("<action>", "start, stop, or list proxies")
.option(
"-u, --upstream <url>",
"upstream proxy URL (protocol://[username:password@]host:port)"
)
.option(
"-p, --port <number>",
"local port to use (random if not specified)",
parseInt
)
.option("--ignore-certificate", "ignore certificate errors for HTTPS proxies")
.option("--id <id>", "proxy ID for stop command")
.description("manage proxy servers")
.action(async (action, options) => {
if (action === "start") {
if (!options.upstream) {
console.error("Error: Upstream proxy URL is required");
console.log(
"Example: proxy start -u http://username:password@proxy.example.com:8080"
);
return;
}
try {
const config = await startProxyProcess(options.upstream, {
port: options.port,
ignoreProxyCertificate: options.ignoreCertificate,
});
console.log(JSON.stringify(config));
} catch (error: any) {
console.error(`Failed to start proxy: ${error.message}`);
}
} else if (action === "stop") {
if (options.id) {
const stopped = await stopProxyProcess(options.id);
console.log(`{
"success": ${stopped}}`);
} else if (options.upstream) {
// Find proxies with this upstream URL
const configs = listProxyConfigs().filter(
(config) => config.upstreamUrl === options.upstream
);
if (configs.length === 0) {
console.error(`No proxies found for ${options.upstream}`);
return;
}
for (const config of configs) {
const stopped = await stopProxyProcess(config.id);
console.log(`{
"success": ${stopped}}`);
}
} else {
await stopAllProxyProcesses();
console.log(`{
"success": true}`);
}
} else if (action === "list") {
const configs = listProxyConfigs();
console.log(JSON.stringify(configs));
} else {
console.error("Invalid action. Use 'start', 'stop', or 'list'");
}
});
// Command for proxy worker (internal use)
program
.command("proxy-worker")
.argument("<action>", "start a proxy worker")
.requiredOption("--id <id>", "proxy configuration ID")
.description("run a proxy worker process")
.action(async (action, options) => {
if (action === "start") {
await runProxyWorker(options.id);
} else {
console.error("Invalid action for proxy-worker. Use 'start'");
process.exit(1);
}
});
program.parse();
+111
View File
@@ -0,0 +1,111 @@
import { spawn } from "child_process";
import path from "path";
import getPort from "get-port";
import {
ProxyConfig,
saveProxyConfig,
getProxyConfig,
deleteProxyConfig,
isProcessRunning,
generateProxyId,
} from "./proxy-storage";
/**
* Start a proxy in a separate process
* @param upstreamUrl The upstream proxy URL
* @param options Optional configuration
* @returns Promise resolving to the proxy configuration
*/
export async function startProxyProcess(
upstreamUrl: string,
options: { port?: number; ignoreProxyCertificate?: boolean } = {}
): Promise<ProxyConfig> {
// Generate a unique ID for this proxy
const id = generateProxyId();
// Get a random available port if not specified
const port = options.port || (await getPort());
// Create the proxy configuration
const config: ProxyConfig = {
id,
upstreamUrl,
localPort: port,
ignoreProxyCertificate: options.ignoreProxyCertificate || false,
};
// Save the configuration before starting the process
saveProxyConfig(config);
// Build the command arguments
const args = ["proxy-worker", "start", "--id", id];
// Spawn the process
const child = spawn(
process.execPath,
[path.join(__dirname, "index.js"), ...args],
{
detached: true,
stdio: "ignore",
}
);
// Unref the child to allow the parent to exit independently
child.unref();
// Store the process ID
config.pid = child.pid;
config.localUrl = `http://localhost:${port}`;
// Update the configuration with the process ID
saveProxyConfig(config);
// Wait a bit to ensure the proxy has started
await new Promise((resolve) => setTimeout(resolve, 500));
return config;
}
/**
* Stop a proxy process
* @param id The proxy ID to stop
* @returns Promise resolving to true if stopped, false if not found
*/
export async function stopProxyProcess(id: string): Promise<boolean> {
const config = getProxyConfig(id);
if (!config || !config.pid) {
return false;
}
try {
// Check if the process is running
if (isProcessRunning(config.pid)) {
// Send SIGTERM to the process
process.kill(config.pid);
// Wait a bit to ensure the process has terminated
await new Promise((resolve) => setTimeout(resolve, 300));
}
// Delete the configuration
deleteProxyConfig(id);
return true;
} catch (error) {
console.error(`Error stopping proxy ${id}:`, error);
return false;
}
}
/**
* Stop all proxy processes
* @returns Promise resolving when all proxies are stopped
*/
export async function stopAllProxyProcesses(): Promise<void> {
const configs = require("./proxy-storage").listProxyConfigs();
for (const config of configs) {
await stopProxyProcess(config.id);
}
}
+149
View File
@@ -0,0 +1,149 @@
import fs from "fs";
import path from "path";
import os from "os";
// Define the proxy configuration type
export interface ProxyConfig {
id: string;
upstreamUrl: string;
localPort?: number;
ignoreProxyCertificate?: boolean;
localUrl?: string;
pid?: number;
}
// Path to store proxy configurations
const STORAGE_DIR = path.join(os.tmpdir(), "donutbrowser", "proxies");
// Ensure storage directory exists
if (!fs.existsSync(STORAGE_DIR)) {
fs.mkdirSync(STORAGE_DIR, { recursive: true });
}
/**
* Save a proxy configuration to disk
* @param config The proxy configuration to save
*/
export function saveProxyConfig(config: ProxyConfig): void {
const filePath = path.join(STORAGE_DIR, `${config.id}.json`);
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
}
/**
* Get a proxy configuration by ID
* @param id The proxy ID
* @returns The proxy configuration or null if not found
*/
export function getProxyConfig(id: string): ProxyConfig | null {
const filePath = path.join(STORAGE_DIR, `${id}.json`);
if (!fs.existsSync(filePath)) {
return null;
}
try {
const content = fs.readFileSync(filePath, "utf-8");
return JSON.parse(content) as ProxyConfig;
} catch (error) {
console.error(`Error reading proxy config ${id}:`, error);
return null;
}
}
/**
* Delete a proxy configuration
* @param id The proxy ID to delete
* @returns True if deleted, false if not found
*/
export function deleteProxyConfig(id: string): boolean {
const filePath = path.join(STORAGE_DIR, `${id}.json`);
if (!fs.existsSync(filePath)) {
return false;
}
try {
fs.unlinkSync(filePath);
return true;
} catch (error) {
console.error(`Error deleting proxy config ${id}:`, error);
return false;
}
}
/**
* List all saved proxy configurations
* @returns Array of proxy configurations
*/
export function listProxyConfigs(): ProxyConfig[] {
if (!fs.existsSync(STORAGE_DIR)) {
return [];
}
try {
return fs
.readdirSync(STORAGE_DIR)
.filter((file) => file.endsWith(".json"))
.map((file) => {
try {
const content = fs.readFileSync(
path.join(STORAGE_DIR, file),
"utf-8"
);
return JSON.parse(content) as ProxyConfig;
} catch (error) {
console.error(`Error reading proxy config ${file}:`, error);
return null;
}
})
.filter((config): config is ProxyConfig => config !== null);
} catch (error) {
console.error("Error listing proxy configs:", error);
return [];
}
}
/**
* Update a proxy configuration
* @param config The proxy configuration to update
* @returns True if updated, false if not found
*/
export function updateProxyConfig(config: ProxyConfig): boolean {
const filePath = path.join(STORAGE_DIR, `${config.id}.json`);
if (!fs.existsSync(filePath)) {
return false;
}
try {
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
return true;
} catch (error) {
console.error(`Error updating proxy config ${config.id}:`, error);
return false;
}
}
/**
* Check if a proxy process is running
* @param pid The process ID to check
* @returns True if running, false otherwise
*/
export function isProcessRunning(pid: number): boolean {
try {
// The kill method with signal 0 doesn't actually kill the process
// but checks if it exists
process.kill(pid, 0);
return true;
} catch (error) {
return false;
}
}
/**
* Generate a unique ID for a proxy
* @returns A unique ID string
*/
export function generateProxyId(): string {
return `proxy_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
}
+51
View File
@@ -0,0 +1,51 @@
import { Server } from "proxy-chain";
import { getProxyConfig } from "./proxy-storage";
/**
* Run a proxy server as a worker process
* @param id The proxy configuration ID
*/
export async function runProxyWorker(id: string): Promise<void> {
// Get the proxy configuration
const config = getProxyConfig(id);
if (!config) {
console.error(`Proxy configuration ${id} not found`);
process.exit(1);
}
// Create a new proxy server
const server = new Server({
port: config.localPort,
host: "localhost",
prepareRequestFunction: () => {
return {
upstreamProxyUrl: config.upstreamUrl,
ignoreUpstreamProxyCertificate: config.ignoreProxyCertificate || false,
};
},
});
// Handle process termination
process.on("SIGTERM", async () => {
console.log(`Proxy worker ${id} received SIGTERM, shutting down...`);
await server.close(true);
process.exit(0);
});
process.on("SIGINT", async () => {
console.log(`Proxy worker ${id} received SIGINT, shutting down...`);
await server.close(true);
process.exit(0);
});
// Start the server
try {
await server.listen();
console.log(`Proxy worker ${id} started on port ${server.port}`);
console.log(`Forwarding to upstream proxy: ${config.upstreamUrl}`);
} catch (error) {
console.error(`Failed to start proxy worker ${id}:`, error);
process.exit(1);
}
}
+73
View File
@@ -0,0 +1,73 @@
import {
startProxyProcess,
stopProxyProcess,
stopAllProxyProcesses
} from "./proxy-runner";
import { listProxyConfigs } from "./proxy-storage";
// Type definitions
interface ProxyOptions {
port?: number;
ignoreProxyCertificate?: boolean;
}
/**
* Start a local proxy server that forwards to an upstream proxy
* @param upstreamProxyUrl The upstream proxy URL (protocol://[username:password@]host:port)
* @param options Optional configuration
* @returns Promise resolving to the local proxy URL
*/
export async function startProxy(
upstreamProxyUrl: string,
options: ProxyOptions = {}
): Promise<string> {
const config = await startProxyProcess(upstreamProxyUrl, {
port: options.port,
ignoreProxyCertificate: options.ignoreProxyCertificate,
});
return config.localUrl || `http://localhost:${config.localPort}`;
}
/**
* Stop a specific proxy by its upstream URL
* @param upstreamProxyUrl The upstream proxy URL to stop
* @returns Promise resolving to true if proxy was found and stopped, false otherwise
*/
export async function stopProxy(upstreamProxyUrl: string): Promise<boolean> {
// Find all proxies with this upstream URL
const configs = listProxyConfigs().filter(
config => config.upstreamUrl === upstreamProxyUrl
);
if (configs.length === 0) {
return false;
}
// Stop all matching proxies
let success = true;
for (const config of configs) {
const stopped = await stopProxyProcess(config.id);
if (!stopped) {
success = false;
}
}
return success;
}
/**
* Get a list of all active proxy upstream URLs
* @returns Array of upstream proxy URLs
*/
export function getActiveProxies(): string[] {
return listProxyConfigs().map(config => config.upstreamUrl);
}
/**
* Stop all active proxies
* @returns Promise that resolves when all proxies are stopped
*/
export async function stopAllProxies(): Promise<void> {
await stopAllProxyProcesses();
}