feat: update proxy ui to accept credential outside url too

This commit is contained in:
zhom
2025-06-11 01:55:54 +04:00
parent 4d7bbe719f
commit 29c329b432
10 changed files with 375 additions and 236 deletions
+2
View File
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"ahooks",
"appimage",
"appindicator",
"applescript",
@@ -10,6 +11,7 @@
"CFURL",
"checkin",
"clippy",
"cmdk",
"codegen",
"devedition",
"donutbrowser",
+71 -51
View File
@@ -1,8 +1,8 @@
import { program } from "commander";
import {
startProxyProcess,
stopProxyProcess,
stopAllProxyProcesses,
stopProxyProcess,
} from "./proxy-runner";
import { listProxyConfigs } from "./proxy-storage";
import { runProxyWorker } from "./proxy-worker";
@@ -11,22 +11,22 @@ import { runProxyWorker } from "./proxy-worker";
program
.command("proxy")
.argument("<action>", "start, stop, or list proxies")
.option("-h, --host <host>", "upstream proxy host")
.option("-P, --proxy-port <port>", "upstream proxy port", Number.parseInt)
.option(
"-t, --type <type>",
"upstream proxy type (http, https, socks4, socks5)",
"http"
)
.option("-u, --username <username>", "upstream proxy username")
.option("-w, --password <password>", "upstream proxy password")
.option("--host <host>", "upstream proxy host")
.option("--proxy-port <port>", "upstream proxy port", Number.parseInt)
.option("--type <type>", "proxy type (http, https, socks4, socks5)")
.option("--username <username>", "proxy username")
.option("--password <password>", "proxy password")
.option(
"-p, --port <number>",
"local port to use (random if not specified)",
Number.parseInt
Number.parseInt,
)
.option("--ignore-certificate", "ignore certificate errors for HTTPS proxies")
.option("--id <id>", "proxy ID for stop command")
.option(
"-u, --upstream <url>",
"upstream proxy URL (protocol://[username:password@]host:port)",
)
.description("manage proxy servers")
.action(
async (
@@ -40,78 +40,98 @@ program
port?: number;
ignoreCertificate?: boolean;
id?: string;
}
upstream?: string;
},
) => {
if (action === "start") {
if (!options.host || !options.proxyPort) {
console.error("Error: Upstream proxy host and port are required");
console.log(
"Example: proxy start -h proxy.example.com -P 8080 -t http -u username -w password"
let upstreamUrl: string;
// Build upstream URL from individual components if provided
if (options.host && options.proxyPort && options.type) {
const protocol =
options.type === "socks4" || options.type === "socks5"
? options.type
: "http";
const auth =
options.username && options.password
? `${encodeURIComponent(options.username)}:${encodeURIComponent(
options.password,
)}@`
: "";
upstreamUrl = `${protocol}://${auth}${options.host}:${options.proxyPort}`;
} else if (options.upstream) {
upstreamUrl = options.upstream;
} else {
console.error(
"Error: Either --upstream URL or --host, --proxy-port, and --type are required",
);
console.log(
"Example: proxy start --host datacenter.proxyempire.io --proxy-port 9000 --type http --username user --password pass",
);
process.exit(1);
return;
}
try {
// Construct the upstream URL with credentials if provided
let upstreamProxyUrl: string;
if (options.username && options.password) {
upstreamProxyUrl = `${options.type}://${options.username}:${options.password}@${options.host}:${options.proxyPort}`;
} else {
upstreamProxyUrl = `${options.type}://${options.host}:${options.proxyPort}`;
}
const config = await startProxyProcess(upstreamProxyUrl, {
const config = await startProxyProcess(upstreamUrl, {
port: options.port,
ignoreProxyCertificate: options.ignoreCertificate,
});
console.log(JSON.stringify(config));
// Output the configuration as JSON for the Rust side to parse
console.log(
JSON.stringify({
id: config.id,
localPort: config.localPort,
localUrl: config.localUrl,
upstreamUrl: config.upstreamUrl,
}),
);
// Exit successfully to allow the process to detach
process.exit(0);
} catch (error: unknown) {
console.error(`Failed to start proxy: ${JSON.stringify(error)}`);
console.error(
`Failed to start proxy: ${
error instanceof Error ? error.message : JSON.stringify(error)
}`,
);
process.exit(1);
}
} else if (action === "stop") {
if (options.id) {
const stopped = await stopProxyProcess(options.id);
console.log(`{
"success": ${stopped}}`);
} else if (options.host && options.proxyPort && options.type) {
// Find proxies with matching upstream details
const configs = listProxyConfigs().filter((config) => {
try {
const url = new URL(config.upstreamUrl);
return (
url.hostname === options.host &&
Number.parseInt(url.port) === options.proxyPort &&
url.protocol.replace(":", "") === options.type
);
} catch {
return false;
}
});
console.log(JSON.stringify({ 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.host}:${options.proxyPort}`
);
console.error(`No proxies found for ${options.upstream}`);
process.exit(1);
return;
}
for (const config of configs) {
const stopped = await stopProxyProcess(config.id);
console.log(`{
"success": ${stopped}}`);
console.log(JSON.stringify({ success: stopped }));
}
} else {
await stopAllProxyProcesses();
console.log(`{
"success": true}`);
console.log(JSON.stringify({ success: true }));
}
process.exit(0);
} else if (action === "list") {
const configs = listProxyConfigs();
console.log(JSON.stringify(configs));
process.exit(0);
} else {
console.error("Invalid action. Use 'start', 'stop', or 'list'");
process.exit(1);
}
}
},
);
// Command for proxy worker (internal use)
+37 -25
View File
@@ -3,12 +3,12 @@ import path from "node:path";
import getPort from "get-port";
import {
type ProxyConfig,
saveProxyConfig,
getProxyConfig,
deleteProxyConfig,
isProcessRunning,
generateProxyId,
getProxyConfig,
isProcessRunning,
listProxyConfigs,
saveProxyConfig,
} from "./proxy-storage";
/**
@@ -19,50 +19,53 @@ import {
*/
export async function startProxyProcess(
upstreamUrl: string,
options: { port?: number; ignoreProxyCertificate?: boolean } = {}
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());
const port = options.port ?? (await getPort());
// Create the proxy configuration
const config: ProxyConfig = {
id,
upstreamUrl,
localPort: port,
ignoreProxyCertificate: options.ignoreProxyCertificate || false,
ignoreProxyCertificate: options.ignoreProxyCertificate ?? false,
};
// Save the configuration before starting the process
saveProxyConfig(config);
// Build the command arguments
const args = ["proxy-worker", "start", "--id", id];
const args = [
path.join(__dirname, "index.js"),
"proxy-worker",
"start",
"--id",
id,
];
// Spawn the process
const child = spawn(
process.execPath,
[path.join(__dirname, "index.js"), ...args],
{
detached: true,
stdio: "ignore",
}
);
// Spawn the process with proper detachment
const child = spawn(process.execPath, args, {
detached: true,
stdio: ["ignore", "ignore", "ignore"], // Completely ignore all stdio
cwd: process.cwd(),
});
// Unref the child to allow the parent to exit independently
child.unref();
// Store the process ID
// Store the process ID and local URL
config.pid = child.pid;
config.localUrl = `http://localhost:${port}`;
config.localUrl = `http://127.0.0.1:${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));
// Give the worker a moment to start before returning
await new Promise((resolve) => setTimeout(resolve, 100));
return config;
}
@@ -76,6 +79,8 @@ export async function stopProxyProcess(id: string): Promise<boolean> {
const config = getProxyConfig(id);
if (!config || !config.pid) {
// Try to delete the config anyway in case it exists without a PID
deleteProxyConfig(id);
return false;
}
@@ -83,10 +88,16 @@ export async function stopProxyProcess(id: string): Promise<boolean> {
// Check if the process is running
if (isProcessRunning(config.pid)) {
// Send SIGTERM to the process
process.kill(config.pid);
process.kill(config.pid, "SIGTERM");
// Wait a bit to ensure the process has terminated
await new Promise((resolve) => setTimeout(resolve, 300));
await new Promise((resolve) => setTimeout(resolve, 500));
// If still running, send SIGKILL
if (isProcessRunning(config.pid)) {
process.kill(config.pid, "SIGKILL");
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
// Delete the configuration
@@ -95,6 +106,8 @@ export async function stopProxyProcess(id: string): Promise<boolean> {
return true;
} catch (error) {
console.error(`Error stopping proxy ${id}:`, error);
// Delete the configuration even if stopping failed
deleteProxyConfig(id);
return false;
}
}
@@ -106,7 +119,6 @@ export async function stopProxyProcess(id: string): Promise<boolean> {
export async function stopAllProxyProcesses(): Promise<void> {
const configs = listProxyConfigs();
for (const config of configs) {
await stopProxyProcess(config.id);
}
const stopPromises = configs.map((config) => stopProxyProcess(config.id));
await Promise.all(stopPromises);
}
+41 -17
View File
@@ -1,5 +1,5 @@
import { Server } from "proxy-chain";
import { getProxyConfig } from "./proxy-storage";
import { getProxyConfig, updateProxyConfig } from "./proxy-storage";
/**
* Run a proxy server as a worker process
@@ -8,44 +8,68 @@ import { getProxyConfig } from "./proxy-storage";
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",
host: "127.0.0.1",
prepareRequestFunction: () => {
return {
upstreamProxyUrl: config.upstreamUrl,
ignoreUpstreamProxyCertificate: config.ignoreProxyCertificate || false,
ignoreUpstreamProxyCertificate: config.ignoreProxyCertificate ?? false,
};
},
});
// Handle process termination
process.on("SIGTERM", async () => {
console.log(`Proxy worker ${id} received SIGTERM, shutting down...`);
await server.close(true);
// Handle process termination gracefully
const gracefulShutdown = async (signal: string) => {
console.log(`Proxy worker ${id} received ${signal}, shutting down...`);
try {
await server.close(true);
console.log(`Proxy worker ${id} shut down successfully`);
} catch (error) {
console.error(`Error during shutdown for proxy ${id}:`, error);
}
process.exit(0);
};
process.on("SIGTERM", () => void gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => void gracefulShutdown("SIGINT"));
// Handle uncaught exceptions
process.on("uncaughtException", (error) => {
console.error(`Uncaught exception in proxy worker ${id}:`, error);
process.exit(1);
});
process.on("SIGINT", async () => {
console.log(`Proxy worker ${id} received SIGINT, shutting down...`);
await server.close(true);
process.exit(0);
process.on("unhandledRejection", (reason) => {
console.error(`Unhandled rejection in proxy worker ${id}:`, reason);
process.exit(1);
});
// Start the server
try {
await server.listen();
// Update the config with the actual port (in case it was auto-assigned)
config.localPort = server.port;
config.localUrl = `http://127.0.0.1:${server.port}`;
updateProxyConfig(config);
console.log(`Proxy worker ${id} started on port ${server.port}`);
console.log(`Forwarding to upstream proxy: ${config.upstreamUrl}`);
// Keep the process alive
setInterval(() => {
// Do nothing, just keep the process alive
}, 60000);
} catch (error) {
console.error(`Failed to start proxy worker ${id}:`, error);
process.exit(1);
}
}
}
-103
View File
@@ -1,103 +0,0 @@
import {
startProxyProcess,
stopProxyProcess,
stopAllProxyProcesses,
} from "./proxy-runner";
import { listProxyConfigs } from "./proxy-storage";
// Type definitions
interface ProxyOptions {
port?: number;
ignoreProxyCertificate?: boolean;
username?: string;
password?: string;
}
/**
* Start a local proxy server that forwards to an upstream proxy
* @param upstreamProxyHost The upstream proxy host
* @param upstreamProxyPort The upstream proxy port
* @param upstreamProxyType The upstream proxy type (http, https, socks4, socks5)
* @param options Optional configuration including credentials
* @returns Promise resolving to the local proxy URL
*/
export async function startProxy(
upstreamProxyHost: string,
upstreamProxyPort: number,
upstreamProxyType: string,
options: ProxyOptions = {}
): Promise<string> {
// Construct the upstream proxy URL with credentials if provided
let upstreamProxyUrl: string;
if (options.username && options.password) {
upstreamProxyUrl = `${upstreamProxyType}://${options.username}:${options.password}@${upstreamProxyHost}:${upstreamProxyPort}`;
} else {
upstreamProxyUrl = `${upstreamProxyType}://${upstreamProxyHost}:${upstreamProxyPort}`;
}
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 host, port, and type
* @param upstreamProxyHost The upstream proxy host
* @param upstreamProxyPort The upstream proxy port
* @param upstreamProxyType The upstream proxy type
* @returns Promise resolving to true if proxy was found and stopped, false otherwise
*/
export async function stopProxy(
upstreamProxyHost: string,
upstreamProxyPort: number,
upstreamProxyType: string
): Promise<boolean> {
// Find all proxies with matching upstream details (ignoring credentials in URL)
const configs = listProxyConfigs().filter((config) => {
// Parse the upstream URL to extract host, port, and type
try {
const url = new URL(config.upstreamUrl);
return (
url.hostname === upstreamProxyHost &&
Number.parseInt(url.port) === upstreamProxyPort &&
url.protocol.replace(":", "") === upstreamProxyType
);
} catch {
return false;
}
});
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();
}
+1 -1
View File
@@ -76,7 +76,7 @@
},
"packageManager": "pnpm@10.11.1",
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,md}": [
"**/*.{js,jsx,ts,tsx,json,css,md}": [
"biome check --fix",
"eslint --cache --fix"
],
+2
View File
@@ -6,6 +6,8 @@
<string>Donut Browser needs camera access to enable camera functionality in web browsers. Each website will still ask for your permission individually.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Donut Browser needs microphone access to enable microphone functionality in web browsers. Each website will still ask for your permission individually.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Donut Browser has proxy functionality that requires local network access. You can deny this functionality if you don't plan on setting proxies for browser profiles.</string>
<key>CFBundleDisplayName</key>
<string>Donut Browser</string>
<key>CFBundleName</key>
+1 -12
View File
@@ -1,14 +1,3 @@
function FindProxyForURL(url, host) {
const proxyString = "{{proxy_url}}";
// Split the proxy string to get the credentials part
const parts = proxyString.split(" ")[1].split("@");
if (parts.length > 1) {
const credentials = parts[0];
const encodedCredentials = encodeURIComponent(credentials);
// Replace the original credentials with encoded ones
return proxyString.replace(credentials, encodedCredentials);
}
return proxyString;
return "{{proxy_url}}";
}
+219 -26
View File
@@ -47,7 +47,7 @@ impl ProxyManager {
return Ok(ProxySettings {
enabled: true,
proxy_type: proxy.upstream_type.clone(),
host: "localhost".to_string(),
host: "127.0.0.1".to_string(), // Use 127.0.0.1 instead of localhost for better compatibility
port: proxy.local_port,
username: None,
password: None,
@@ -74,11 +74,11 @@ impl ProxyManager {
None
};
// Start a new proxy using the nodecar binary
// Start a new proxy using the nodecar binary with the correct CLI interface
let mut nodecar = app_handle
.shell()
.sidecar("nodecar")
.unwrap()
.map_err(|e| format!("Failed to create sidecar: {e}"))?
.arg("proxy")
.arg("start")
.arg("--host")
@@ -101,11 +101,19 @@ impl ProxyManager {
nodecar = nodecar.arg("--port").arg(port.to_string());
}
let output = nodecar.output().await.unwrap();
// Execute the command and wait for it to complete
// The nodecar binary should start the worker and then exit
let output = nodecar
.output()
.await
.map_err(|e| format!("Failed to execute nodecar: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Proxy start failed: {stderr}"));
let stdout = String::from_utf8_lossy(&output.stdout);
return Err(format!(
"Proxy start failed - stdout: {stdout}, stderr: {stderr}"
));
}
let json_string =
@@ -148,7 +156,7 @@ impl ProxyManager {
Ok(ProxySettings {
enabled: true,
proxy_type: "http".to_string(),
host: "localhost".to_string(),
host: "127.0.0.1".to_string(), // Use 127.0.0.1 instead of localhost for better compatibility
port: proxy_info.local_port,
username: None,
password: None,
@@ -196,7 +204,7 @@ impl ProxyManager {
proxies.get(&browser_pid).map(|proxy| ProxySettings {
enabled: true,
proxy_type: "http".to_string(),
host: "localhost".to_string(),
host: "127.0.0.1".to_string(), // Use 127.0.0.1 instead of localhost for better compatibility
port: proxy.local_port,
username: None,
password: None,
@@ -355,7 +363,7 @@ mod tests {
assert!(proxy_settings.is_some());
let settings = proxy_settings.unwrap();
assert!(settings.enabled);
assert_eq!(settings.host, "localhost");
assert_eq!(settings.host, "127.0.0.1");
assert_eq!(settings.port, 8080);
// Test non-existent browser PID
@@ -407,7 +415,7 @@ mod tests {
let browser_pid = (1000 + i) as u32;
let proxy_info = ProxyInfo {
id: format!("proxy_{i}"),
local_url: format!("http://localhost:{}", 8000 + i),
local_url: format!("http://127.0.0.1:{}", 8000 + i),
upstream_host: "127.0.0.1".to_string(),
upstream_port: 3128,
upstream_type: "http".to_string(),
@@ -454,21 +462,19 @@ mod tests {
let upstream_addr = upstream_listener.local_addr()?;
// Spawn upstream server
tokio::spawn(async move {
loop {
if let Ok((stream, _)) = upstream_listener.accept().await {
let io = TokioIo::new(stream);
tokio::task::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(
io,
service_fn(|_req| async {
Ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from("Upstream OK"))))
}),
)
.await;
});
}
let server_handle = tokio::spawn(async move {
while let Ok((stream, _)) = upstream_listener.accept().await {
let io = TokioIo::new(stream);
tokio::task::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(
io,
service_fn(|_req| async {
Ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from("Upstream OK"))))
}),
)
.await;
});
}
});
@@ -487,7 +493,8 @@ mod tests {
.arg("--type")
.arg("http");
let output = cmd.output()?;
// Set a timeout for the command
let output = tokio::time::timeout(Duration::from_secs(10), async { cmd.output() }).await??;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
@@ -499,12 +506,33 @@ mod tests {
assert!(config["localUrl"].is_string());
let proxy_id = config["id"].as_str().unwrap();
let local_port = config["localPort"].as_u64().unwrap();
// Wait for proxy worker to start
println!("Waiting for proxy worker to start...");
tokio::time::sleep(Duration::from_secs(3)).await;
// Test that the local port is listening
let mut port_test = Command::new("nc");
port_test
.arg("-z")
.arg("127.0.0.1")
.arg(local_port.to_string());
let port_output = port_test.output()?;
if port_output.status.success() {
println!("Proxy is listening on port {local_port}");
} else {
println!("Warning: Proxy port {local_port} is not listening");
}
// Test stopping the proxy
let mut stop_cmd = Command::new(&nodecar_path);
stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
let stop_output = stop_cmd.output()?;
let stop_output =
tokio::time::timeout(Duration::from_secs(5), async { stop_cmd.output() }).await??;
assert!(stop_output.status.success());
println!("Integration test passed: nodecar proxy start/stop works correctly");
@@ -514,6 +542,9 @@ mod tests {
return Err(format!("Nodecar command failed: {stderr}").into());
}
// Clean up server
server_handle.abort();
Ok(())
}
@@ -552,4 +583,166 @@ mod tests {
assert_eq!(proxy_settings.username.as_ref().unwrap(), "user");
assert_eq!(proxy_settings.password.as_ref().unwrap(), "pass");
}
// Test the CLI detachment specifically - ensure the CLI exits properly
#[tokio::test]
async fn test_cli_exits_after_proxy_start() -> Result<(), Box<dyn std::error::Error>> {
let nodecar_path = ensure_nodecar_binary().await?;
// Test that the CLI exits quickly with a mock upstream
let mut cmd = Command::new(&nodecar_path);
cmd
.arg("proxy")
.arg("start")
.arg("--host")
.arg("httpbin.org")
.arg("--proxy-port")
.arg("80")
.arg("--type")
.arg("http");
let start_time = std::time::Instant::now();
let output = tokio::time::timeout(Duration::from_secs(3), async { cmd.output() }).await;
match output {
Ok(Ok(cmd_output)) => {
let execution_time = start_time.elapsed();
println!("CLI completed in {execution_time:?}");
// Should complete very quickly if properly detached
assert!(
execution_time < Duration::from_secs(3),
"CLI took too long ({execution_time:?}), should exit immediately after starting worker"
);
if cmd_output.status.success() {
let stdout = String::from_utf8(cmd_output.stdout)?;
let config: serde_json::Value = serde_json::from_str(&stdout)?;
// Clean up - try to stop the proxy
if let Some(proxy_id) = config["id"].as_str() {
let mut stop_cmd = Command::new(&nodecar_path);
stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
let _ = stop_cmd.output();
}
}
println!("CLI detachment test passed - CLI exited in {execution_time:?}");
}
Ok(Err(e)) => {
return Err(format!("Command execution failed: {e}").into());
}
Err(_) => {
return Err("CLI command timed out - this indicates improper detachment".into());
}
}
Ok(())
}
// Test that validates proper CLI detachment behavior
#[tokio::test]
async fn test_cli_detachment_behavior() -> Result<(), Box<dyn std::error::Error>> {
let nodecar_path = ensure_nodecar_binary().await?;
// Test that the CLI command exits quickly even with a real upstream
let mut cmd = Command::new(&nodecar_path);
cmd
.arg("proxy")
.arg("start")
.arg("--host")
.arg("httpbin.org") // Use a known good endpoint
.arg("--proxy-port")
.arg("80")
.arg("--type")
.arg("http");
let start_time = std::time::Instant::now();
let output = tokio::time::timeout(Duration::from_secs(5), async { cmd.output() }).await??;
let execution_time = start_time.elapsed();
// Command should complete very quickly if properly detached
assert!(
execution_time < Duration::from_secs(5),
"CLI command took {execution_time:?}, should complete in under 5 seconds for proper detachment"
);
println!("CLI detachment test: command completed in {execution_time:?}");
if output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
let config: serde_json::Value = serde_json::from_str(&stdout)?;
let proxy_id = config["id"].as_str().unwrap();
// Clean up
let mut stop_cmd = Command::new(&nodecar_path);
stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
let _ = stop_cmd.output();
println!("CLI detachment test passed");
} else {
// Even if the upstream fails, the CLI should still exit quickly
println!("CLI command failed but exited quickly as expected");
}
Ok(())
}
// Test that validates URL encoding for special characters in credentials
#[tokio::test]
async fn test_proxy_credentials_encoding() -> Result<(), Box<dyn std::error::Error>> {
let nodecar_path = ensure_nodecar_binary().await?;
// Test with credentials that include special characters
let mut cmd = Command::new(&nodecar_path);
cmd
.arg("proxy")
.arg("start")
.arg("--host")
.arg("test.example.com")
.arg("--proxy-port")
.arg("8080")
.arg("--type")
.arg("http")
.arg("--username")
.arg("user@domain.com") // Contains @ symbol
.arg("--password")
.arg("pass word!"); // Contains space and special character
let output = tokio::time::timeout(Duration::from_secs(5), async { cmd.output() }).await??;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
let config: serde_json::Value = serde_json::from_str(&stdout)?;
let upstream_url = config["upstreamUrl"].as_str().unwrap();
println!("Generated upstream URL: {upstream_url}");
// Verify that special characters are properly encoded
assert!(upstream_url.contains("user%40domain.com"));
// The password may be encoded as "pass%20word!" or "pass%20word%21" depending on implementation
assert!(upstream_url.contains("pass%20word"));
println!("URL encoding test passed - special characters handled correctly");
// Clean up
let proxy_id = config["id"].as_str().unwrap();
let mut stop_cmd = Command::new(&nodecar_path);
stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
let _ = stop_cmd.output();
} else {
// This test might fail if the upstream doesn't exist, but we mainly care about URL construction
let stdout = String::from_utf8(output.stdout)?;
let stderr = String::from_utf8(output.stderr)?;
println!("Command failed (expected for non-existent upstream):");
println!("Stdout: {stdout}");
println!("Stderr: {stderr}");
// The important thing is that the command completed quickly
println!("URL encoding test completed - credentials should be properly encoded");
}
Ok(())
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ export function WindowDragArea() {
return (
<div
className="fixed top-0 right-0 left-0 z-50 h-8 cursor-move"
className="fixed top-0 right-0 left-0 h-10 z-9999"
style={{
// Ensure it's above all other content
zIndex: 9999,