#![allow(dead_code)]
use std::{
io::{BufRead, BufReader},
net::TcpStream,
path::PathBuf,
process::Child,
sync::Once,
thread,
time::{Duration, Instant},
};
use base64::Engine;
use rama::telemetry::tracing::{
level_filters::LevelFilter,
subscriber::{self, EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt},
};
#[derive(Debug)]
pub(super) struct RamaService {
process: Child,
}
#[derive(Debug, Clone)]
pub(super) enum EchoMode {
Tcp,
Udp,
Tls,
Http,
Https,
HttpsWithCertIssuer {
remote_addr: String,
remote_ca: Option<Vec<u8>>,
remote_auth: Option<String>,
},
}
impl RamaService {
pub(super) fn serve_ip(port: u16, transport: bool, secure: bool) -> Self {
let mut builder = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command();
builder
.stdout(std::process::Stdio::piped())
.arg("serve")
.arg("ip")
.arg("--bind")
.arg(format!("127.0.0.1:{port}"))
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
);
if secure {
const BASE64: base64::engine::GeneralPurpose =
base64::engine::general_purpose::STANDARD;
builder.env(
"RAMA_TLS_CRT",
BASE64.encode(include_bytes!("./example_tls.crt")),
);
builder.env(
"RAMA_TLS_KEY",
BASE64.encode(include_bytes!("./example_tls.key")),
);
builder.arg("-s");
}
if transport {
builder.arg("-T");
}
let mut process = builder.spawn().unwrap();
let stdout = process.stdout.take().unwrap();
let mut stdout = BufReader::new(stdout).lines();
for line in &mut stdout {
let line = line.unwrap();
if line.contains("ip service ready") {
break;
}
}
thread::spawn(move || {
for line in stdout {
let line = line.unwrap();
eprintln!("rama ip >> {line}");
}
});
Self { process }
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn serve_echo(port: u16, mode: EchoMode) -> Self {
let mut builder = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command();
const BASE64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;
if matches!(mode, EchoMode::Tls | EchoMode::Https) {
builder.env(
"RAMA_TLS_CRT",
BASE64.encode(include_bytes!("./example_tls.crt")),
);
builder.env(
"RAMA_TLS_KEY",
BASE64.encode(include_bytes!("./example_tls.key")),
);
} else if let EchoMode::HttpsWithCertIssuer {
remote_addr,
remote_ca,
remote_auth,
} = &mode
{
builder.env("RAMA_TLS_REMOTE", remote_addr);
if let Some(remote_ca) = remote_ca {
builder.env("RAMA_TLS_REMOTE_CA", BASE64.encode(remote_ca));
}
if let Some(remote_auth) = remote_auth {
builder.env("RAMA_TLS_REMOTE_AUTH", remote_auth);
}
}
builder
.stdout(std::process::Stdio::piped())
.arg("serve")
.arg("echo")
.arg("--bind")
.arg(format!("127.0.0.1:{port}"))
.arg("--mode")
.arg(match &mode {
EchoMode::Tcp => "tcp",
EchoMode::Udp => "udp",
EchoMode::Tls => "tls",
EchoMode::Http => "http",
EchoMode::Https | EchoMode::HttpsWithCertIssuer { .. } => "https",
})
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
);
if matches!(
&mode,
EchoMode::Http | EchoMode::Https | EchoMode::HttpsWithCertIssuer { .. }
) {
builder.arg("--ws");
}
let mut process = builder.spawn().unwrap();
let stdout = process.stdout.take().unwrap();
let mut stdout = BufReader::new(stdout).lines();
for line in &mut stdout {
let line = line.unwrap();
if line.contains("echo service ready") {
break;
}
}
thread::spawn(move || {
for line in stdout {
let line = line.unwrap();
println!("rama echo >> {line}");
}
});
Self { process }
}
pub(super) fn serve_fp(port: u16, secure: bool) -> Self {
let mut builder = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command();
if secure {
const BASE64: base64::engine::GeneralPurpose =
base64::engine::general_purpose::STANDARD;
builder.env(
"RAMA_TLS_CRT",
BASE64.encode(include_bytes!("./example_tls.crt")),
);
builder.env(
"RAMA_TLS_KEY",
BASE64.encode(include_bytes!("./example_tls.key")),
);
}
builder
.stdout(std::process::Stdio::piped())
.arg("serve")
.arg("fp")
.arg("--bind")
.arg(format!("127.0.0.1:{port}"))
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
);
if secure {
builder.arg("--secure");
}
let mut process = builder.spawn().unwrap();
let stdout = process.stdout.take().unwrap();
let mut stdout = BufReader::new(stdout).lines();
for line in &mut stdout {
let line = line.unwrap();
if line.contains("FP Service (auto) listening") {
break;
}
}
thread::spawn(move || {
for line in stdout {
let line = line.unwrap();
println!("rama fp >> {line}");
}
});
Self { process }
}
pub(super) fn serve_proxy(port: u16) -> Self {
let mut builder = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command();
builder
.stdout(std::process::Stdio::piped())
.arg("serve")
.arg("proxy")
.arg("--bind")
.arg(format!("127.0.0.1:{port}"))
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
);
let mut process = builder.spawn().unwrap();
let stdout = process.stdout.take().unwrap();
let mut stdout = BufReader::new(stdout).lines();
for line in &mut stdout {
let line = line.unwrap();
if line.contains("proxy ready") {
break;
}
}
thread::spawn(move || {
for line in stdout {
let line = line.unwrap();
println!("rama proxy >> {line}");
}
});
Self { process }
}
pub(super) fn serve_discard(port: u16, mode: &'static str) -> Self {
let mut builder = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command();
if mode.eq_ignore_ascii_case("tls") {
const BASE64: base64::engine::GeneralPurpose =
base64::engine::general_purpose::STANDARD;
builder.env(
"RAMA_TLS_CRT",
BASE64.encode(include_bytes!("./example_tls.crt")),
);
builder.env(
"RAMA_TLS_KEY",
BASE64.encode(include_bytes!("./example_tls.key")),
);
}
builder
.stdout(std::process::Stdio::piped())
.arg("serve")
.arg("discard")
.arg("--bind")
.arg(format!("127.0.0.1:{port}"))
.arg("--mode")
.arg(mode)
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
);
let mut process = builder.spawn().unwrap();
let stdout = process.stdout.take().unwrap();
let mut stdout = BufReader::new(stdout).lines();
for line in &mut stdout {
let line = line.unwrap();
if line.contains("discard service ready") {
break;
}
}
thread::spawn(move || {
for line in stdout {
let line = line.unwrap();
println!("rama discard >> {line}");
}
});
Self { process }
}
pub(super) fn serve_http_test(port: u16, secure: bool) -> Self {
let mut builder = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command();
if secure {
const BASE64: base64::engine::GeneralPurpose =
base64::engine::general_purpose::STANDARD;
builder.env(
"RAMA_TLS_CRT",
BASE64.encode(include_bytes!("./example_tls.crt")),
);
builder.env(
"RAMA_TLS_KEY",
BASE64.encode(include_bytes!("./example_tls.key")),
);
}
builder
.stdout(std::process::Stdio::piped())
.arg("serve")
.arg("http-test")
.arg("--bind")
.arg(format!("127.0.0.1:{port}"))
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
);
if secure {
builder.arg("--secure");
}
let mut process = builder.spawn().unwrap();
let stdout = process.stdout.take().unwrap();
thread::spawn(move || {
let stdout = BufReader::new(stdout).lines();
for line in stdout {
let line = line.unwrap();
println!("rama http-test >> {line}");
}
});
wait_for_tcp_listener(&mut process, port, "http-test");
Self { process }
}
pub(super) fn run(args: &[&'static str]) -> Result<String, Box<dyn std::error::Error>> {
let child = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command()
.stderr(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.args(args)
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
)
.spawn()
.unwrap();
let output = child.wait_with_output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
panic!(
"rama exited unsuccessfully\n args: {args:?}\n status: {status}\n --- stderr ---\n{stderr}\n --- stdout ---\n{stdout}",
status = output.status,
);
}
let mut s = String::from_utf8(output.stderr)?;
s.push_str(&String::from_utf8(output.stdout)?);
Ok(s)
}
#[allow(clippy::type_complexity)]
pub(super) fn run_capture(
args: &[&str],
) -> Result<(bool, String, String), Box<dyn std::error::Error>> {
let child = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command()
.stderr(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.args(args)
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
)
.spawn()
.unwrap();
let output = child.wait_with_output()?;
Ok((
output.status.success(),
String::from_utf8(output.stdout)?,
String::from_utf8(output.stderr)?,
))
}
pub(super) fn http(
input_args: Vec<&'static str>,
) -> Result<String, Box<dyn std::error::Error>> {
let mut args = vec!["--verbose", "-L", "-k"];
args.extend(input_args);
Self::run(&args)
}
pub(super) fn probe_tls(addr: &'static str) -> Result<String, Box<dyn std::error::Error>> {
let args = vec!["probe", "tls", "-k", addr];
Self::run(&args)
}
pub(super) fn probe_tcp(addr: &'static str) -> Result<String, Box<dyn std::error::Error>> {
let args = vec!["probe", "tcp", addr];
Self::run(&args)
}
pub(super) fn resolve(
domain: &'static str,
record_type: Option<&'static str>,
) -> Result<String, Box<dyn std::error::Error>> {
let mut args = vec!["resolve", domain];
if let Some(rt) = record_type {
args.push(rt);
}
Self::run(&args)
}
pub(super) fn serve_fs(port: u16, path: Option<PathBuf>) -> Self {
let secure = true;
let mut builder = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command();
if secure {
const BASE64: base64::engine::GeneralPurpose =
base64::engine::general_purpose::STANDARD;
builder.env(
"RAMA_TLS_CRT",
BASE64.encode(include_bytes!("./example_tls.crt")),
);
builder.env(
"RAMA_TLS_KEY",
BASE64.encode(include_bytes!("./example_tls.key")),
);
}
builder
.stdout(std::process::Stdio::piped())
.arg("serve")
.arg("fs")
.arg("--bind")
.arg(format!("127.0.0.1:{port}"))
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
);
if secure {
builder.arg("-s");
}
if let Some(path) = path {
builder.arg(path);
}
let mut process = builder.spawn().unwrap();
let stdout = process.stdout.take().unwrap();
let mut stdout = BufReader::new(stdout).lines();
for line in &mut stdout {
let line = line.unwrap();
if line.contains("ready to serve") {
break;
}
}
thread::spawn(move || {
for line in stdout {
let line = line.unwrap();
println!("rama serve >> {line}");
}
});
Self { process }
}
pub(super) fn serve_stunnel_exit(bind: &str, forward: &str) -> Self {
let mut builder = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command();
builder
.stdout(std::process::Stdio::piped())
.arg("serve")
.arg("stunnel")
.arg("exit")
.arg("--bind")
.arg(bind)
.arg("--forward")
.arg(forward)
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
);
let mut process = builder.spawn().unwrap();
let stdout = process.stdout.take().unwrap();
let mut stdout = BufReader::new(stdout).lines();
for line in &mut stdout {
let line = line.unwrap();
if line.contains("Stunnel exit node is running") {
break;
}
}
thread::spawn(move || {
for line in stdout {
let line = line.unwrap();
eprintln!("rama stunnel-server >> {line}");
}
});
Self { process }
}
pub(super) fn serve_stunnel_entry_insecure(bind: &str, connect: &str) -> Self {
let mut builder = escargot::CargoBuild::new()
.package("rama-cli")
.bin("rama")
.target_dir("./target/")
.run()
.unwrap()
.command();
builder
.stdout(std::process::Stdio::piped())
.arg("serve")
.arg("stunnel")
.arg("entry")
.arg("--insecure")
.arg("--bind")
.arg(bind)
.arg("--connect")
.arg(connect)
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or("info".into()),
);
let mut process = builder.spawn().unwrap();
let stdout = process.stdout.take().unwrap();
let mut stdout = BufReader::new(stdout).lines();
for line in &mut stdout {
let line = line.unwrap();
if line.contains("Stunnel entry node is running") {
break;
}
}
thread::spawn(move || {
for line in stdout {
let line = line.unwrap();
eprintln!("rama stunnel-client >> {line}");
}
});
Self { process }
}
}
fn wait_for_tcp_listener(process: &mut Child, port: u16, name: &str) {
let deadline = Instant::now() + Duration::from_secs(10);
let addr = format!("127.0.0.1:{port}");
loop {
if TcpStream::connect(&addr).is_ok() {
return;
}
if let Some(status) = process.try_wait().expect("check service status") {
panic!("{name} service exited before listening on {addr}: {status}");
}
assert!(
Instant::now() < deadline,
"{name} service did not listen on {addr} before timeout"
);
thread::sleep(Duration::from_millis(25));
}
}
impl Drop for RamaService {
fn drop(&mut self) {
self.process.kill().expect("kill server process");
}
}
static INIT_TRACING_ONCE: Once = Once::new();
pub(super) fn init_tracing() {
INIT_TRACING_ONCE.call_once(|| {
_ = subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::TRACE.into())
.from_env_lossy(),
)
.try_init();
});
}