use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use daylight_core::{Config, Engine, ObserveTarget};
use serde_json::json;
const INDEX_HTML: &str = include_str!("../static/index.html");
struct AppState {
engine: Engine,
handle: Option<daylight_core::ObservationHandle>,
}
fn main() {
tracing_subscriber::fmt::init();
let listener = match TcpListener::bind("127.0.0.1:0") {
Ok(l) => l,
Err(e) => {
eprintln!("Failed to start server: {e}");
std::process::exit(1);
}
};
let port = match listener.local_addr() {
Ok(addr) => addr.port(),
Err(e) => {
eprintln!("Failed to get server address: {e}");
std::process::exit(1);
}
};
let url = format!("http://127.0.0.1:{port}");
eprintln!("Daylight GUI running at {url}");
if let Err(e) = open_browser(&url) {
eprintln!("Could not open browser: {e}");
eprintln!("Open manually: {url}");
}
let state = Arc::new(Mutex::new(AppState {
engine: Engine::new(),
handle: None,
}));
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let state = Arc::clone(&state);
std::thread::spawn(move || {
if let Err(e) = handle_connection(stream, &state) {
tracing::debug!("connection error: {e}");
}
});
}
Err(e) => {
tracing::debug!("accept error: {e}");
}
}
}
}
fn handle_connection(
mut stream: TcpStream,
state: &Arc<Mutex<AppState>>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut reader = BufReader::new(stream.try_clone()?);
let mut request_line = String::new();
reader.read_line(&mut request_line)?;
let parts: Vec<&str> = request_line.trim().split_whitespace().collect();
if parts.len() < 2 {
return Ok(());
}
let method = parts[0];
let path = parts[1];
let mut content_length: usize = 0;
loop {
let mut line = String::new();
reader.read_line(&mut line)?;
if line.trim().is_empty() {
break;
}
if let Some(rest) = line.to_lowercase().strip_prefix("content-length:") {
content_length = rest.trim().parse().unwrap_or(0);
}
}
const MAX_BODY: usize = 1 * 1024 * 1024; if content_length > MAX_BODY {
respond_json(&mut stream, 413, &json!({"error": "request body too large"}))?;
return Ok(());
}
let body = if content_length > 0 {
let mut buf = vec![0u8; content_length];
reader.read_exact(&mut buf)?;
String::from_utf8_lossy(&buf).to_string()
} else {
String::new()
};
match (method, path) {
("GET", "/") | ("GET", "/index.html") => {
respond_html(&mut stream, INDEX_HTML)?;
}
("GET", "/api/processes") => {
handle_processes(&mut stream, state)?;
}
("GET", "/api/browser-tabs") => {
handle_browser_tabs(&mut stream, state)?;
}
("POST", "/api/start") => {
handle_start(&mut stream, state, &body)?;
}
("POST", "/api/stop") => {
handle_stop(&mut stream, state)?;
}
("GET", "/api/status") => {
handle_status(&mut stream, state)?;
}
_ => {
respond_json(&mut stream, 404, &json!({"error": "not found"}))?;
}
}
Ok(())
}
fn handle_processes(
stream: &mut TcpStream,
state: &Arc<Mutex<AppState>>,
) -> Result<(), Box<dyn std::error::Error>> {
let state = state.lock().map_err(|e| format!("lock: {e}"))?;
match state.engine.list_processes() {
Ok(processes) => {
let top: Vec<_> = processes.into_iter().take(50).collect();
respond_json(stream, 200, &serde_json::to_value(top)?)?;
}
Err(e) => {
respond_json(stream, 500, &json!({"error": e.to_string()}))?;
}
}
Ok(())
}
fn handle_browser_tabs(
stream: &mut TcpStream,
state: &Arc<Mutex<AppState>>,
) -> Result<(), Box<dyn std::error::Error>> {
let state = state.lock().map_err(|e| format!("lock: {e}"))?;
match state.engine.discover_browser_tabs(9222) {
Ok(tabs) => {
respond_json(stream, 200, &serde_json::to_value(tabs)?)?;
}
Err(e) => {
respond_json(stream, 200, &json!({"error": e.to_string()}))?;
}
}
Ok(())
}
fn handle_start(
stream: &mut TcpStream,
state: &Arc<Mutex<AppState>>,
body: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let target: ObserveTarget = serde_json::from_str(body).map_err(|e| format!("parse: {e}"))?;
let mut state = state.lock().map_err(|e| format!("lock: {e}"))?;
if state.handle.is_some() {
respond_json(stream, 400, &json!({"error": "observation already running"}))?;
return Ok(());
}
let config = Config {
classify: true,
..Config::default()
};
match state.engine.start(target, config) {
Ok(handle) => {
state.handle = Some(handle);
respond_json(stream, 200, &json!({"status": "started"}))?;
}
Err(e) => {
respond_json(stream, 500, &json!({"error": e.to_string()}))?;
}
}
Ok(())
}
fn handle_stop(
stream: &mut TcpStream,
state: &Arc<Mutex<AppState>>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut state = state.lock().map_err(|e| format!("lock: {e}"))?;
let handle = match state.handle.take() {
Some(h) => h,
None => {
respond_json(stream, 400, &json!({"error": "no observation running"}))?;
return Ok(());
}
};
match handle.stop() {
Ok(session) => {
let report = state.engine.render_text(&session);
let result = json!({
"status": "stopped",
"report": report,
"event_count": session.events.len(),
});
respond_json(stream, 200, &result)?;
}
Err(e) => {
respond_json(stream, 500, &json!({"error": e.to_string()}))?;
}
}
Ok(())
}
fn handle_status(
stream: &mut TcpStream,
state: &Arc<Mutex<AppState>>,
) -> Result<(), Box<dyn std::error::Error>> {
let state = state.lock().map_err(|e| format!("lock: {e}"))?;
match &state.handle {
Some(handle) => {
let events = handle.events_snapshot();
let running = handle.is_running();
let event_summaries: Vec<serde_json::Value> = events
.iter()
.map(|e| {
let dest_summary = e.destination.summary();
let hostname = match &e.destination {
daylight_types::Destination::Network { hostname, .. } => hostname.clone(),
_ => None,
};
json!({
"sequence": e.sequence,
"timestamp": e.timestamp.to_rfc3339(),
"kind": e.kind.syscall_name(),
"destination": {
"address": dest_summary,
"hostname": hostname,
},
"classifications": e.classifications.iter().map(|c| {
json!({
"category": c.category.as_str(),
"rule": c.rule_name,
})
}).collect::<Vec<_>>(),
})
})
.collect();
respond_json(stream, 200, &json!({
"running": running,
"event_count": events.len(),
"events": event_summaries,
}))?;
}
None => {
respond_json(stream, 200, &json!({
"running": false,
"event_count": 0,
"events": [],
}))?;
}
}
Ok(())
}
fn respond_html(stream: &mut TcpStream, body: &str) -> Result<(), std::io::Error> {
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: text/html; charset=utf-8\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
\r\n\
{body}",
body.len()
);
stream.write_all(response.as_bytes())
}
fn respond_json(
stream: &mut TcpStream,
status: u16,
body: &serde_json::Value,
) -> Result<(), std::io::Error> {
let body_str = body.to_string();
let status_text = match status {
200 => "OK",
400 => "Bad Request",
404 => "Not Found",
500 => "Internal Server Error",
_ => "OK",
};
let response = format!(
"HTTP/1.1 {status} {status_text}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
Access-Control-Allow-Origin: *\r\n\
\r\n\
{body_str}",
body_str.len()
);
stream.write_all(response.as_bytes())
}
fn open_browser(url: &str) -> Result<(), std::io::Error> {
std::process::Command::new("xdg-open")
.arg(url)
.spawn()
.map(|_| ())
}