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
+131
View File
@@ -0,0 +1,131 @@
import { FlatCompat } from "@eslint/eslintrc";
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
});
const eslintConfig = tseslint.config(
eslint.configs.recommended,
...compat.extends("next/core-web-vitals"),
{
// Disabled rules taken from https://biomejs.dev/linter/rules-sources for ones that
// are already handled by Biome
rules: {
// eslint-plugin-jsx-a11y rules replaced by Biome
"jsx-a11y/alt-text": "off",
"jsx-a11y/anchor-has-content": "off",
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/aria-activedescendant-has-tabindex": "off",
"jsx-a11y/aria-props": "off",
"jsx-a11y/aria-proptypes": "off",
"jsx-a11y/aria-role": "off",
"jsx-a11y/aria-unsupported-elements": "off",
"jsx-a11y/autocomplete-valid": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/heading-has-content": "off",
"jsx-a11y/html-has-lang": "off",
"jsx-a11y/iframe-has-title": "off",
"jsx-a11y/img-redundant-alt": "off",
"jsx-a11y/interactive-supports-focus": "off",
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/lang": "off",
"jsx-a11y/media-has-caption": "off",
"jsx-a11y/mouse-events-have-key-events": "off",
"jsx-a11y/no-access-key": "off",
"jsx-a11y/no-aria-hidden-on-focusable": "off",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-distracting-elements": "off",
"jsx-a11y/no-interactive-element-to-noninteractive-role": "off",
"jsx-a11y/no-noninteractive-element-to-interactive-role": "off",
"jsx-a11y/no-noninteractive-tabindex": "off",
"jsx-a11y/no-redundant-roles": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/role-has-required-aria-props": "off",
"jsx-a11y/role-supports-aria-props": "off",
"jsx-a11y/scope": "off",
"jsx-a11y/tabindex-no-positive": "off",
// eslint-plugin-react rules replaced by Biome
"react/button-has-type": "off",
"react/jsx-boolean-value": "off",
"react/jsx-curly-brace-presence": "off",
"react/jsx-fragments": "off",
"react/jsx-key": "off",
"react/jsx-no-comment-textnodes": "off",
"react/jsx-no-duplicate-props": "off",
"react/jsx-no-target-blank": "off",
"react/jsx-no-useless-fragment": "off",
"react/no-array-index-key": "off",
"react/no-children-prop": "off",
"react/no-danger": "off",
"react/no-danger-with-children": "off",
"react/void-dom-elements-no-children": "off",
// eslint-plugin-react-hooks rules replaced by Biome
"react-hooks/exhaustive-deps": "off",
"react-hooks/rules-of-hooks": "off",
// typescript-eslint rules replaced by Biome
"@typescript-eslint/adjacent-overload-signatures": "off",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/consistent-type-exports": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/default-param-last": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-dupe-class-members": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extra-non-null-assertion": "off",
"@typescript-eslint/no-extraneous-class": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-invalid-void-type": "off",
"@typescript-eslint/no-loss-of-precision": "off",
"@typescript-eslint/no-misused-new": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-redeclare": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-restricted-imports": "off",
"@typescript-eslint/no-restricted-types": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/no-unsafe-declaration-merging": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-useless-constructor": "off",
"@typescript-eslint/no-useless-empty-export": "off",
"@typescript-eslint/only-throw-error": "off",
"@typescript-eslint/parameter-properties": "off",
"@typescript-eslint/prefer-as-const": "off",
"@typescript-eslint/prefer-enum-initializers": "off",
"@typescript-eslint/prefer-for-of": "off",
"@typescript-eslint/prefer-function-type": "off",
"@typescript-eslint/prefer-literal-enum-member": "off",
"@typescript-eslint/prefer-namespace-keyword": "off",
"@typescript-eslint/prefer-optional-chain": "off",
"@typescript-eslint/require-await": "off",
// Custom rules
"@typescript-eslint/restrict-template-expressions": [
"error",
{
allowNumber: true,
allowBoolean: true,
allowNever: true,
},
],
},
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
}
);
export default eslintConfig;
+33
View File
@@ -0,0 +1,33 @@
{
"name": "nodecar",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"watch": "nodemon --exec ts-node --esm ./src/index.ts --watch src",
"dev": "node --loader ts-node/esm ./src/index.ts",
"start": "node --loader ts-node/esm ./src/index.ts",
"build": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar",
"build:aarch64": "tsc && pkg ./dist/index.js --targets latest-macos-arm64 --output dist/nodecar",
"build:x86_64": "tsc && pkg ./dist/index.js --targets latest-macos-x64 --output dist/nodecar",
"build:linux-x64": "tsc && pkg ./dist/index.js --targets latest-linux-x64 --output dist/nodecar",
"build:linux-arm64": "tsc && pkg ./dist/index.js --targets latest-linux-arm64 --output dist/nodecar",
"build:win-x64": "tsc && pkg ./dist/index.js --targets latest-win-x64 --output dist/nodecar",
"build:win-arm64": "tsc && pkg ./dist/index.js --targets latest-win-arm64 --output dist/nodecar"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.6.1",
"dependencies": {
"@types/node": "^22.15.17",
"@yao-pkg/pkg": "^6.4.1",
"commander": "^13.1.0",
"dotenv": "^16.5.0",
"get-port": "^7.1.0",
"nodemon": "^3.1.10",
"proxy-chain": "^2.5.8",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}
+1304
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
import { execSync } from "child_process";
import fs from "fs";
const ext = process.platform === "win32" ? ".exe" : "";
const rustInfo = execSync("rustc -vV");
const targetTriple = /host: (\S+)/g.exec(rustInfo)[1];
if (!targetTriple) {
console.error("Failed to determine platform target triple");
}
fs.renameSync(
`dist/nodecar${ext}`,
`../src-tauri/binaries/nodecar-${targetTriple}${ext}`
);
+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();
}
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"lib": ["dom", "es6", "es2017", "esnext.asynciterable"],
"sourceMap": false,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"types": ["node"],
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"baseUrl": ".",
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"removeComments": true
}
}