diff --git a/Cargo.lock b/Cargo.lock index e251dc6..edb67d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1169,9 +1169,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 9376d15..0db3723 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,8 @@ futures-util = "*" hex = "*" hkdf = "*" hmac = "*" -hyper = { version = "*", features = ["full"] } -hyper-util = "*" +hyper-util = { version = "*" } +hyper = { version = "1.8.1", features = ["capi", "client", "full", "http1", "http2", "nightly", "server"] } http-body-util = "*" json = "*" native-tls = { version = "*", default-features = false } diff --git a/src/server/api.rs b/src/server/api.rs index 8803cfa..71f38d1 100644 --- a/src/server/api.rs +++ b/src/server/api.rs @@ -17,6 +17,7 @@ pub async fn handle( path: &str, is_local: &bool, headers: HeaderMap, + body_string: Option, ) -> HttpResponse> { if !is_local { return HttpResponse::builder() @@ -26,7 +27,7 @@ pub async fn handle( } let path_parts: Vec<&str> = path.split("/").collect(); - + let body: Option = body_string.map(|s| json::parse(&s).unwrap()); let (status, content, body_text) = if path_parts.len() >= 3 { match path_parts[2] { "app_state" => (StatusCode::OK, "application/json", { @@ -46,25 +47,32 @@ pub async fn handle( if path_parts.len() >= 4 { match path_parts[3] { "add" => { - let username = headers.get("username"); - if let (Some(user), Some(_private_key)) = user_manager::create_user( - &username.unwrap().to_str().unwrap().to_string(), - ) - .await - { - let cv = CommunicationValue::new(CommunicationType::create_user) - .add_data(DataTypes::user, user.frontend()); - cv.to_json().to_string() - } else { + if body.is_none() { "{\"type\":\"error\"}".to_string() + } else { + let username = + body.unwrap()["username"].as_str().unwrap().to_string(); + if let (Some(user), Some(_private_key)) = + user_manager::create_user(&username).await + { + let cv = + CommunicationValue::new(CommunicationType::create_user) + .add_data(DataTypes::user, user.frontend()); + cv.to_json().to_string() + } else { + "{\"type\":\"error\"}".to_string() + } } } "remove" => { - let uuid = - Uuid::parse_str(headers.get("uuid").unwrap().to_str().unwrap()) + if body.is_none() { + "{\"type\":\"error\"}".to_string() + } else { + let uuid = Uuid::parse_str(body.unwrap()["uuid"].as_str().unwrap()) .unwrap(); - user_manager::remove_user(uuid); - "{}".to_string() + user_manager::remove_user(uuid); + "{}".to_string() + } } "get" => { let users = user_manager::get_users(); @@ -89,15 +97,23 @@ pub async fn handle( if path_parts.len() >= 4 { match path_parts[3] { "add" => { - let name = headers.get("name").unwrap().to_str().unwrap(); - let community = Arc::new(Community::create(name.to_string()).await); - community_manager::add_community(community).await; - "{\"type\":\"success\"}".to_string() + if body.is_none() { + "{\"type\":\"error\"}".to_string() + } else { + let name = body.unwrap()["name"].as_str().unwrap().to_string(); + let community = Arc::new(Community::create(name).await); + community_manager::add_community(community).await; + "{\"type\":\"success\"}".to_string() + } } "remove" => { - let name = headers.get("name").unwrap().to_str().unwrap(); - community_manager::remove_community(name).await; - "{\"type\":\"success\"}".to_string() + if body.is_none() { + "{\"type\":\"error\"}".to_string() + } else { + let name = body.unwrap()["name"].as_str().unwrap().to_string(); + community_manager::remove_community(&name).await; + "{\"type\":\"success\"}".to_string() + } } "get" => { let communities = community_manager::get_communities().await; diff --git a/src/server/server.rs b/src/server/server.rs index f78eca6..29c2980 100644 --- a/src/server/server.rs +++ b/src/server/server.rs @@ -2,19 +2,24 @@ use crate::gui::log_panel::log_message; use crate::server::api; use crate::server::socket::handle; use crate::util::file_util::{load_file_buf, load_file_vec}; + use base64::Engine; use base64::engine::general_purpose::STANDARD; -use futures::{StreamExt, TryFutureExt}; +use bytes::Bytes; +use futures::StreamExt; +use futures_util::TryFutureExt; use http_body_util::Full; -use hyper::body::Bytes; +use http_body_util::{BodyExt, Collected}; +use hyper::upgrade::OnUpgrade; +use hyper::{Method, StatusCode}; use hyper::{ - Request as HttpRequest, Response as HttpResponse, StatusCode, body::Incoming, - server::conn::http1, upgrade, + Request as HttpRequest, Response as HttpResponse, body::Incoming, server::conn::http1, upgrade, }; use hyper_util::rt::tokio::TokioIo; use hyper_util::service::TowerToHyperService; use rustls::ServerConfig; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use sha1::digest::generic_array::arr::Inc; use sha1::{Digest, Sha1}; use std::error::Error; use std::io::ErrorKind; @@ -22,12 +27,12 @@ use std::io::{self, BufReader}; use std::net::{IpAddr, SocketAddr}; use std::result::Result::Ok; use std::sync::Arc; +use std::thread::panicking; use std::{future::Future, pin::Pin, time::Duration}; use tokio::net::TcpListener; use tokio_rustls::TlsAcceptor; use tokio_tungstenite::WebSocketStream; use tower::Service; - #[derive(Clone)] struct HttpService { peer_addr: SocketAddr, @@ -46,21 +51,25 @@ impl Service> for HttpService { } fn call(&mut self, req: HttpRequest) -> Self::Future { - let path = req.uri().path().to_string(); - let headers = req.headers().clone(); - let upgrades = upgrade::on(req); - let peer_ip = self.peer_addr.ip(); let is_local = is_local_network(peer_ip); + let (parts, body) = req.into_parts(); + + let method = parts.method.clone(); + let path = parts.uri.path().to_string(); + let headers = parts.headers.clone(); + let fut = async move { - if path.starts_with("/ws") - && headers - .get("connection") - .map(|v| v.to_str().unwrap_or("").contains("Upgrade")) - .unwrap_or(false) - && headers.get("upgrade").map(|v| v.to_str().unwrap_or("")) == Some("websocket") - { + let is_websocket_upgrade = path.starts_with("/ws") + && method == Method::GET // WebSocket upgrades use GET + && headers + .get("connection") + .map(|v| v.to_str().unwrap_or("").contains("Upgrade")) + .unwrap_or(false) + && headers.get("upgrade").map(|v| v.to_str().unwrap_or("")) == Some("websocket"); + + if is_websocket_upgrade { log_message("Attempting WebSocket upgrade on /ws"); if let Some(sec_websocket_key) = headers.get("sec-websocket-key") { @@ -74,29 +83,30 @@ impl Service> for HttpService { .header("Sec-WebSocket-Accept", sec_websocket_accept) .body(Full::new(Bytes::from(""))) .unwrap(); - tokio::spawn(async move { - match upgrades.await { - std::result::Result::Ok(upgraded_stream) => { - let raw_stream = TokioIo::new(upgraded_stream); + let req_for_upgrade = HttpRequest::from_parts(parts, body); + let upgrades = upgrade::on(req_for_upgrade); - let handshake_result = WebSocketStream::from_raw_socket( - raw_stream, - tungstenite::protocol::Role::Server, - None, - ) - .await; - log_message(format!("Handling WebSocket connection",)); - let (writer, reader) = handshake_result.split(); - handle(path, writer, reader); - } - Err(e) => { - log_message(format!( - "WebSocket upgrade failed after response: {:?}", - e - )); - } + match upgrades.await { + std::result::Result::Ok(upgraded_stream) => { + let raw_stream = TokioIo::new(upgraded_stream); + + let handshake_result = WebSocketStream::from_raw_socket( + raw_stream, + tungstenite::protocol::Role::Server, + None, + ) + .await; + log_message(format!("Handling WebSocket connection",)); + let (writer, reader) = handshake_result.split(); + handle(path.clone(), writer, reader); } - }); + Err(e) => { + log_message(format!( + "WebSocket upgrade failed after response: {:?}", + e + )); + } + } Ok(response) } else { log_message("No Sec-WebSocket-Key found in request headers"); @@ -106,62 +116,80 @@ impl Service> for HttpService { .unwrap(); Ok(response) } + } else if path.starts_with("/api") { + let whole_body = match body.collect().await { + Ok(collected) => collected, + Err(e) => { + log_message(format!("Error collecting body: {}", e)); + return Ok(HttpResponse::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Full::new(Bytes::from(format!( + "Failed to read body: {}", + e + )))) + .unwrap()); + } + }; + let bytes = whole_body.to_bytes(); + + let body_string: Option = match String::from_utf8(bytes.to_vec()) { + Ok(s) => Some(s), + Err(_) => None, + }; + + Ok(api::handle(&path, &is_local, headers.clone(), body_string).await) } else { - if path.starts_with("/api") { - Ok(api::handle(&path, &is_local, headers).await) + let mut path_parts: Vec<&str> = path.split("/").collect(); + let name = path_parts.remove(path_parts.len() - 1); + let name = if name.is_empty() { + "index.html" + } else if name.contains(".") && name.contains("?") { + name.split("?").next().unwrap() + } else if name.contains(".") { + name } else { - let mut path_parts: Vec<&str> = path.split("/").collect(); - let name = path_parts.remove(path_parts.len() - 1); - let name = if name.is_empty() { - "index.html" - } else if name.contains(".") && name.contains("?") { - name.split("?").next().unwrap() - } else if name.contains(".") { - name - } else { - &format!("{}.html", name) - }; - let code = if let Some(ext) = name.split(".").last() { - match ext { - "html" => "text/html", - "css" => "text/css", - "ico" => "image/x-icon", - "png" => "image/png", - "js" => "application/javascript", - "json" => "application/json", - _ => "application/octet-stream", - } - } else { - "application/octet-stream" - }; - let (status, content, body_text): (StatusCode, &str, Vec) = { - let content = load_file_vec(&format!("web{}/", path_parts.join("/")), name); + &format!("{}.html", name) + }; + let code = if let Some(ext) = name.split(".").last() { + match ext { + "html" => "text/html", + "css" => "text/css", + "ico" => "image/x-icon", + "png" => "image/png", + "js" => "application/javascript", + "json" => "application/json", + _ => "application/octet-stream", + } + } else { + "application/octet-stream" + }; + let (status, content, body_text): (StatusCode, &str, Vec) = { + let content = load_file_vec(&format!("web{}/", path_parts.join("/")), name); + if content.is_empty() { + let content = load_file_vec("web", "404.html"); if content.is_empty() { - let content = load_file_vec("web", "404.html"); - if content.is_empty() { - ( - StatusCode::NOT_FOUND, - code, - include_str!("../../static/web/404.html") - .as_bytes() - .to_vec(), - ) - } else { - (StatusCode::OK, code, content) - } + ( + StatusCode::NOT_FOUND, + code, + include_str!("../../static/web/404.html") + .as_bytes() + .to_vec(), + ) } else { (StatusCode::OK, code, content) } - }; + } else { + (StatusCode::OK, code, content) + } + }; - let body = Full::new(Bytes::from(body_text.to_vec())); - let response = HttpResponse::builder() - .header("Content-Type", content) - .status(status) - .body(body) - .unwrap(); - Ok(response) - } + let body = Full::new(Bytes::from(body_text.to_vec())); + let response = HttpResponse::builder() + .header("Content-Type", content) + .status(status) + .body(body) + .unwrap(); + Ok(response) } };