use std::sync::Arc;
use std::time::Duration;
use eggress_core::listener::{TcpListener, TcpListenerConfig};
use eggress_routing::{RouteActionSpec, RouteService, Router};
use eggress_server::ConnectionConfig;
use tokio_util::sync::CancellationToken;
fn require_curl_interop() {
if std::env::var("EGRESS_REQUIRE_CURL_INTEROP").is_err() {
panic!("EGRESS_REQUIRE_CURL_INTEROP not set");
}
}
fn curl_available() -> bool {
std::process::Command::new("curl")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn skip_if_unavailable() {
require_curl_interop();
if !curl_available() {
eprintln!("skipping: curl not available");
panic!("curl not available");
}
}
async fn start_eggress_server(
protocols: Vec<eggress_core::ProtocolId>,
) -> (
std::net::SocketAddr,
CancellationToken,
tokio::task::JoinHandle<()>,
) {
let config = TcpListenerConfig {
bind_addr: "127.0.0.1:0".parse().unwrap(),
protocols,
auth_required: false,
handshake_timeout: Duration::from_secs(5),
connection_limit: 10,
};
let cancel = CancellationToken::new();
let listener = TcpListener::new(&config, cancel.clone()).await.unwrap();
let addr = listener.local_addr().unwrap();
let conn_protocols: std::sync::Arc<[eggress_core::ProtocolId]> =
config.protocols.clone().into();
let jh = tokio::spawn(async move {
loop {
let conn = match listener.accept().await {
Ok(c) => c,
Err(_) => break,
};
let config = ConnectionConfig {
routing: Arc::new(Router::new(vec![], RouteActionSpec::Direct))
as Arc<dyn RouteService>,
context: eggress_server::ConnectionContext::default(),
handshake_timeout: Duration::from_secs(5),
connect_timeout: Duration::from_secs(10),
protocols: conn_protocols.clone(),
authentication: eggress_server::accept::InboundAuthentication::None,
metrics: None,
udp: None,
tls_client_config: None,
shadowsocks: None,
shadowsocks_metrics: None,
trojan: None,
fixed_target: None,
local_bind: None,
};
tokio::spawn(async move {
let _ = eggress_server::serve_connection(conn.stream, config).await;
});
}
});
(addr, cancel, jh)
}
#[tokio::test]
#[ignore = "requires EGRESS_REQUIRE_CURL_INTEROP=1 and curl"]
async fn test_curl_http_connect() {
skip_if_unavailable();
let (origin_addr, origin_jh) = eggress_testkit::start_http_origin_server().await;
let (proxy_addr, cancel, proxy_jh) =
start_eggress_server(vec![eggress_core::ProtocolId::Http]).await;
tokio::time::sleep(Duration::from_millis(50)).await;
let url = format!("http://{}:{}", origin_addr.ip(), origin_addr.port());
let output = tokio::task::spawn_blocking(move || {
std::process::Command::new("curl")
.args([
"--proxy",
&format!("http://{}", proxy_addr),
"--max-time",
"10",
&url,
])
.output()
.expect("failed to execute curl")
})
.await
.unwrap();
assert!(
output.status.success(),
"curl failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("hello from origin"),
"expected 'hello from origin', got: {stdout}"
);
cancel.cancel();
let _ = proxy_jh.await;
origin_jh.abort();
}
#[tokio::test]
#[ignore = "requires EGRESS_REQUIRE_CURL_INTEROP=1 and curl"]
async fn test_curl_socks5() {
skip_if_unavailable();
let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
let (proxy_addr, cancel, proxy_jh) =
start_eggress_server(vec![eggress_core::ProtocolId::Socks5]).await;
tokio::time::sleep(Duration::from_millis(50)).await;
let target = format!("socks5h://{}:{}", echo_addr.ip(), echo_addr.port());
let proxy_url = format!("socks5://{}", proxy_addr);
let output = tokio::task::spawn_blocking(move || {
std::process::Command::new("curl")
.args([
"--proxy",
&proxy_url,
"--max-time",
"10",
"-o",
"/dev/null",
"-w",
"%{http_code}",
&target,
])
.output()
.expect("failed to execute curl")
})
.await
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success()
|| stderr.contains("echo")
|| !stderr.contains("Connection refused"),
"connection through SOCKS5 proxy should succeed, stderr: {stderr}"
);
cancel.cancel();
let _ = proxy_jh.await;
echo_jh.abort();
}
#[tokio::test]
#[ignore = "requires EGRESS_REQUIRE_CURL_INTEROP=1 and curl"]
async fn test_curl_socks4a() {
skip_if_unavailable();
let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
let (proxy_addr, cancel, proxy_jh) =
start_eggress_server(vec![eggress_core::ProtocolId::Socks4]).await;
tokio::time::sleep(Duration::from_millis(50)).await;
let target = format!("socks4a://{}:{}", echo_addr.ip(), echo_addr.port());
let proxy_url = format!("socks4a://{}", proxy_addr);
let output = tokio::task::spawn_blocking(move || {
std::process::Command::new("curl")
.args([
"--proxy",
&proxy_url,
"--max-time",
"10",
"-o",
"/dev/null",
"-w",
"%{http_code}",
&target,
])
.output()
.expect("failed to execute curl")
})
.await
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success()
|| stderr.contains("echo")
|| !stderr.contains("Connection refused"),
"connection through SOCKS4a proxy should succeed, stderr: {stderr}"
);
cancel.cancel();
let _ = proxy_jh.await;
echo_jh.abort();
}