use std::io::{Read, Write}; use std::net::TcpStream; #[derive(Debug, Clone, PartialEq, Eq)] pub struct HttpUrl { pub host: String, pub port: u16, pub base_path: String, } impl HttpUrl { pub fn parse(server_url: &str) -> Result { let without_scheme = server_url .strip_prefix("http://") .ok_or_else(|| "ce premier client supporte seulement http://".to_string())?; let (host_port, base_path) = match without_scheme.split_once('/') { Some((host_port, path)) => (host_port, format!("/{path}")), None => (without_scheme, String::new()), }; let (host, port) = match host_port.rsplit_once(':') { Some((host, port)) => { let parsed_port = port .parse::() .map_err(|_| "port serveur invalide".to_string())?; (host.to_string(), parsed_port) } None => (host_port.to_string(), 80), }; if host.is_empty() { return Err("hôte serveur manquant".to_string()); } Ok(Self { host, port, base_path, }) } pub fn path(&self, endpoint: &str) -> String { let base = self.base_path.trim_end_matches('/'); let endpoint = endpoint.trim_start_matches('/'); if base.is_empty() { format!("/{endpoint}") } else { format!("{base}/{endpoint}") } } } pub struct ApiClient { server: HttpUrl, token: Option, } impl ApiClient { pub fn new(server_url: &str, token: Option) -> Result { Ok(Self { server: HttpUrl::parse(server_url)?, token, }) } pub fn get_capabilities(&self) -> Result { self.get("/api/capabilities") } pub fn get_system_status(&self) -> Result { self.get("/api/system/status") } pub fn get_system_metrics(&self) -> Result { self.get("/api/system/metrics") } pub fn get_machines(&self) -> Result { self.get("/api/machines") } fn get(&self, endpoint: &str) -> Result { let path = self.server.path(endpoint); let mut request = format!( "GET {path} HTTP/1.1\r\nHost: {}\r\nUser-Agent: system-update-gnome/0.1\r\nAccept: application/json\r\nConnection: close\r\n", self.server.host ); if let Some(token) = &self.token { request.push_str(&format!("Authorization: Bearer {token}\r\n")); } request.push_str("\r\n"); let mut stream = TcpStream::connect((&*self.server.host, self.server.port)) .map_err(|err| format!("connexion serveur échouée: {err}"))?; stream .write_all(request.as_bytes()) .map_err(|err| format!("envoi requête échoué: {err}"))?; let mut response = String::new(); stream .read_to_string(&mut response) .map_err(|err| format!("lecture réponse échouée: {err}"))?; split_http_response(&response) } } fn split_http_response(response: &str) -> Result { let (headers, body) = response .split_once("\r\n\r\n") .ok_or_else(|| "réponse HTTP invalide".to_string())?; let status_line = headers.lines().next().unwrap_or_default(); if !status_line.contains(" 200 ") { return Err(format!("réponse serveur inattendue: {status_line}")); } Ok(body.to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn parses_default_http_port() { let url = HttpUrl::parse("http://localhost").expect("url"); assert_eq!(url.host, "localhost"); assert_eq!(url.port, 80); assert_eq!(url.path("/api/capabilities"), "/api/capabilities"); } #[test] fn parses_explicit_http_port_and_base_path() { let url = HttpUrl::parse("http://10.0.0.80:8787/system-update").expect("url"); assert_eq!(url.host, "10.0.0.80"); assert_eq!(url.port, 8787); assert_eq!( url.path("/api/capabilities"), "/system-update/api/capabilities" ); } #[test] fn rejects_https_until_tls_client_is_added() { assert!(HttpUrl::parse("https://10.0.0.80:8787").is_err()); } #[test] fn extracts_success_body() { let body = split_http_response( "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"ok\":true}", ) .expect("body"); assert_eq!(body, "{\"ok\":true}"); } }