SSL Support
Add WSS/TLS (self‑signed) with client cert pinning; auto ws→wss on --tls-ca/-t; add -p/-t flags; harden TLS test; fix clippy; update README. feat: WSS/TLS support (self‑signed + pinning), auto ws→wss when CA provided, new -p/-t flags; tests + clippy cleanup; docs updated. Add TLS: self‑signed certs on agent, client pin via --tls-ca/-t (auto‑upgrade to wss), CLI/tests/README updates, clippy fixes. 12 files changed Cargo.toml README.md Cargo.tomlsocktop_agent main.rssocktop_agent/src tls.rssocktop_agent/src cli_args.rssocktop_agent/tests Add Context... README.md
This commit is contained in:
+79
-59
@@ -10,13 +10,30 @@ mod ws;
|
||||
|
||||
use axum::{routing::get, Router};
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod tls;
|
||||
|
||||
use crate::sampler::{spawn_disks_sampler, spawn_process_sampler, spawn_sampler};
|
||||
use state::AppState;
|
||||
use ws::ws_handler;
|
||||
|
||||
fn arg_flag(name: &str) -> bool {
|
||||
std::env::args().any(|a| a == name)
|
||||
}
|
||||
fn arg_value(name: &str) -> Option<String> {
|
||||
let mut it = std::env::args();
|
||||
while let Some(a) = it.next() {
|
||||
if a == name {
|
||||
return it.next();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// (tests moved to end of file to satisfy clippy::items_after_test_module)
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let state = AppState::new();
|
||||
@@ -29,71 +46,74 @@ async fn main() {
|
||||
// 5s disks
|
||||
let _h_disks = spawn_disks_sampler(state.clone(), std::time::Duration::from_secs(5));
|
||||
|
||||
// Web app
|
||||
let port = resolve_port();
|
||||
// Web app: route /ws to the websocket handler
|
||||
let app = Router::new()
|
||||
.route("/ws", get(ws_handler))
|
||||
.with_state(state);
|
||||
.route("/ws", get(ws::ws_handler))
|
||||
.with_state(state.clone());
|
||||
|
||||
let enable_ssl =
|
||||
arg_flag("--enableSSL") || std::env::var("SOCKTOP_ENABLE_SSL").ok().as_deref() == Some("1");
|
||||
if enable_ssl {
|
||||
// Port can be overridden by --port or SOCKTOP_PORT; default to 8443 when SSL
|
||||
let port = arg_value("--port")
|
||||
.or_else(|| arg_value("-p"))
|
||||
.or_else(|| std::env::var("SOCKTOP_PORT").ok())
|
||||
.and_then(|s| s.parse::<u16>().ok())
|
||||
.unwrap_or(8443);
|
||||
|
||||
let (cert_path, key_path) = tls::ensure_self_signed_cert()?;
|
||||
let cfg = axum_server::tls_rustls::RustlsConfig::from_pem_file(cert_path, key_path).await?;
|
||||
|
||||
let addr = SocketAddr::from_str(&format!("0.0.0.0:{port}"))?;
|
||||
println!("socktop_agent: TLS enabled. Listening on wss://{addr}/ws");
|
||||
axum_server::bind_rustls(addr, cfg)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Non-TLS HTTP/WS path
|
||||
let port = arg_value("--port")
|
||||
.or_else(|| arg_value("-p"))
|
||||
.or_else(|| std::env::var("SOCKTOP_PORT").ok())
|
||||
.and_then(|s| s.parse::<u16>().ok())
|
||||
.unwrap_or(3000);
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
|
||||
//output to console
|
||||
println!("Remote agent running at http://{addr}");
|
||||
println!("WebSocket endpoint: ws://{addr}/ws");
|
||||
|
||||
//trace logging
|
||||
tracing::info!("Remote agent running at http://{} (ws at /ws)", addr);
|
||||
tracing::info!("WebSocket endpoint: ws://{}/ws", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
println!("socktop_agent: Listening on ws://{addr}/ws");
|
||||
axum_server::bind(addr)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Resolve the listening port from CLI args/env with a 3000 default.
|
||||
// Supports: --port <PORT>, -p <PORT>, a bare numeric positional arg, or SOCKTOP_PORT.
|
||||
fn resolve_port() -> u16 {
|
||||
const DEFAULT: u16 = 3000;
|
||||
|
||||
// Env takes precedence over positional, but is overridden by explicit flags if present.
|
||||
if let Ok(s) = std::env::var("SOCKTOP_PORT") {
|
||||
if let Ok(p) = s.parse::<u16>() {
|
||||
if p != 0 {
|
||||
return p;
|
||||
#[cfg(test)]
|
||||
mod tests_cli_agent {
|
||||
// Local helper for testing port parsing
|
||||
fn parse_port<I: IntoIterator<Item = String>>(args: I, default_port: u16) -> u16 {
|
||||
let mut it = args.into_iter();
|
||||
let _ = it.next(); // prog
|
||||
let mut long: Option<String> = None;
|
||||
let mut short: Option<String> = None;
|
||||
while let Some(a) = it.next() {
|
||||
match a.as_str() {
|
||||
"--port" => long = it.next(),
|
||||
"-p" => short = it.next(),
|
||||
_ if a.starts_with("--port=") => {
|
||||
if let Some((_, v)) = a.split_once('=') { long = Some(v.to_string()); }
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
eprintln!("Warning: invalid SOCKTOP_PORT='{s}'; using default {DEFAULT}");
|
||||
long.or(short)
|
||||
.and_then(|s| s.parse::<u16>().ok())
|
||||
.unwrap_or(default_port)
|
||||
}
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--port" | "-p" => {
|
||||
if let Some(v) = args.next() {
|
||||
match v.parse::<u16>() {
|
||||
Ok(p) if p != 0 => return p,
|
||||
_ => {
|
||||
eprintln!("Invalid port '{v}'; using default {DEFAULT}");
|
||||
return DEFAULT;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Missing value for {arg} ; using default {DEFAULT}");
|
||||
return DEFAULT;
|
||||
}
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
println!("Usage: socktop_agent [--port <PORT>] [PORT]\n SOCKTOP_PORT=<PORT> socktop_agent");
|
||||
std::process::exit(0);
|
||||
}
|
||||
s => {
|
||||
if let Ok(p) = s.parse::<u16>() {
|
||||
if p != 0 {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn port_long_short_and_assign() {
|
||||
assert_eq!(parse_port(vec!["agent".into(), "--port".into(), "9001".into()], 8443), 9001);
|
||||
assert_eq!(parse_port(vec!["agent".into(), "-p".into(), "9002".into()], 8443), 9002);
|
||||
assert_eq!(parse_port(vec!["agent".into(), "--port=9003".into()], 8443), 9003);
|
||||
assert_eq!(parse_port(vec!["agent".into()], 8443), 8443);
|
||||
}
|
||||
|
||||
DEFAULT
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
use openssl::asn1::Asn1Time;
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::nid::Nid;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::rsa::Rsa;
|
||||
use openssl::x509::extension::{BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName};
|
||||
use openssl::x509::{X509NameBuilder, X509};
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
fn config_dir() -> PathBuf {
|
||||
std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("HOME").map(|h| Path::new(&h).join(".config")))
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("socktop_agent")
|
||||
.join("tls")
|
||||
}
|
||||
|
||||
pub fn cert_paths() -> (PathBuf, PathBuf) {
|
||||
let dir = config_dir();
|
||||
(dir.join("cert.pem"), dir.join("key.pem"))
|
||||
}
|
||||
|
||||
pub fn ensure_self_signed_cert() -> anyhow::Result<(PathBuf, PathBuf)> {
|
||||
let (cert_path, key_path) = cert_paths();
|
||||
if cert_path.exists() && key_path.exists() {
|
||||
return Ok((cert_path, key_path));
|
||||
}
|
||||
fs::create_dir_all(cert_path.parent().unwrap())?;
|
||||
|
||||
// Key
|
||||
let rsa = Rsa::generate(4096)?;
|
||||
let pkey = PKey::from_rsa(rsa)?;
|
||||
|
||||
// Subject/issuer
|
||||
let hostname = hostname::get()
|
||||
.ok()
|
||||
.and_then(|s| s.into_string().ok())
|
||||
.unwrap_or_else(|| "localhost".to_string());
|
||||
let mut name = X509NameBuilder::new()?;
|
||||
name.append_entry_by_nid(Nid::COMMONNAME, &hostname)?;
|
||||
let name = name.build();
|
||||
|
||||
// Cert builder
|
||||
let mut builder = X509::builder()?;
|
||||
builder.set_version(2)?;
|
||||
builder.set_subject_name(&name)?;
|
||||
builder.set_issuer_name(&name)?;
|
||||
builder.set_pubkey(&pkey)?;
|
||||
|
||||
builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?;
|
||||
builder.set_not_after(Asn1Time::days_from_now(397)?.as_ref())?;
|
||||
|
||||
// SANs: hostname + localhost loopbacks
|
||||
let mut san = SubjectAlternativeName::new();
|
||||
san.dns(&hostname)
|
||||
.dns("localhost")
|
||||
.ip("127.0.0.1")
|
||||
.ip("::1");
|
||||
// Add a generic 0.0.0.0 for convenience; some TLS libs ignore this, but harmless.
|
||||
let _ = san.ip(&IpAddr::V4(Ipv4Addr::UNSPECIFIED).to_string());
|
||||
let san = san.build(&builder.x509v3_context(None, None))?;
|
||||
// End-entity cert: not a CA
|
||||
builder.append_extension(BasicConstraints::new().critical().build()?)?;
|
||||
builder.append_extension(
|
||||
KeyUsage::new()
|
||||
.digital_signature()
|
||||
.key_encipherment()
|
||||
.build()?,
|
||||
)?;
|
||||
// TLS server usage
|
||||
builder.append_extension(ExtendedKeyUsage::new().server_auth().build()?)?;
|
||||
builder.append_extension(san)?;
|
||||
|
||||
builder.sign(&pkey, MessageDigest::sha256())?;
|
||||
let cert: X509 = builder.build();
|
||||
|
||||
let mut f = fs::File::create(&cert_path)?;
|
||||
f.write_all(&cert.to_pem()?)?;
|
||||
let mut k = fs::File::create(&key_path)?;
|
||||
k.write_all(&pkey.private_key_to_pem_pkcs8()?)?;
|
||||
|
||||
println!(
|
||||
"socktop_agent: generated self-signed TLS certificate at {}",
|
||||
cert_path.display()
|
||||
);
|
||||
println!("socktop_agent: private key at {}", key_path.display());
|
||||
Ok((cert_path, key_path))
|
||||
}
|
||||
Reference in New Issue
Block a user