From 5ac1e66026835c73b1c91ab4e29c591ae779f9ca Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Tue, 18 Nov 2025 19:39:53 +0100 Subject: [PATCH] feat: prepare v2 http server --- core/src/http/server/controller/mod.rs | 1 + core/src/http/server/controller/v2.rs | 29 +++++++++ core/src/http/server/mod.rs | 88 ++++++++++++++++++++------ core/src/main.rs | 11 ++-- 4 files changed, 105 insertions(+), 24 deletions(-) create mode 100644 core/src/http/server/controller/v2.rs diff --git a/core/src/http/server/controller/mod.rs b/core/src/http/server/controller/mod.rs index d6560b80..f87961dc 100644 --- a/core/src/http/server/controller/mod.rs +++ b/core/src/http/server/controller/mod.rs @@ -1,2 +1,3 @@ +pub(crate) mod v2; pub(crate) mod v3; pub(crate) mod web; diff --git a/core/src/http/server/controller/v2.rs b/core/src/http/server/controller/v2.rs new file mode 100644 index 00000000..92c197da --- /dev/null +++ b/core/src/http/server/controller/v2.rs @@ -0,0 +1,29 @@ +use hyper::body::Incoming; +use hyper::StatusCode; +use crate::http::dto::{RegisterDto, RegisterResponseDto}; +use crate::http::server::{AppState, JsonResponse, RequestClientInfo}; +use crate::http::server::collect_to_json::CollectToJson; +use crate::http::server::error::AppError; + +pub(crate) async fn register( + body: Incoming, + state: AppState, + client_info: RequestClientInfo, +) -> Result, AppError> { + let payload = body.collect_to_json::().await?; + + let info = state.info.lock().await.clone(); + let has_web_interface = state.web.lock().await.is_some(); + + Ok(JsonResponse { + status: StatusCode::OK, + body: RegisterResponseDto { + alias: info.alias, + version: info.version, + device_model: info.device_model, + device_type: info.device_type, + token: info.token, + has_web_interface, + }, + }) +} diff --git a/core/src/http/server/mod.rs b/core/src/http/server/mod.rs index ab184289..f1602e67 100644 --- a/core/src/http/server/mod.rs +++ b/core/src/http/server/mod.rs @@ -6,6 +6,7 @@ mod error; use crate::crypto::cert::public_key_from_cert_der; use crate::http::dto::ErrorResponse; use crate::http::server::client_cert_verifier::CustomClientCertVerifier; +use crate::http::server::controller::web::WebPageState; use crate::http::server::error::AppError; use crate::http::state::ClientInfo; use bytes::Bytes; @@ -24,7 +25,6 @@ use std::num::NonZeroUsize; use std::ops::Deref; use std::sync::Arc; use tokio::sync::{oneshot, Mutex}; -use crate::http::server::controller::web::WebPageState; #[derive(Clone)] struct AppState { @@ -67,6 +67,7 @@ impl LsHttpServer { port: u16, tls_config: Option, info: ClientInfo, + legacy_enabled: bool, ) -> anyhow::Result { let ipv4_socket_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port); let ipv6_socket_addr = SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), port); @@ -79,11 +80,11 @@ impl LsHttpServer { let state = state.clone(); async move { tokio::select! { - _ = start_server_with_addr(ipv4_socket_addr, tls_config.clone(), state.clone()) => { + _ = start_server_with_addr(ipv4_socket_addr, tls_config.clone(), state.clone(), legacy_enabled) => { tracing::info!("Server stopped on: {}", ipv4_socket_addr); } _ = async { - if start_server_with_addr(ipv6_socket_addr, tls_config, state).await.is_err() { + if start_server_with_addr(ipv6_socket_addr, tls_config, state, legacy_enabled).await.is_err() { tracing::warn!("Failed to start server on: {}", ipv6_socket_addr); // Keep the future running forever, so we continue using "ipv4 only" even if ipv6 fails. @@ -124,6 +125,7 @@ async fn start_server_with_addr( socket_addr: SocketAddr, tls_config: Option, app_state: AppState, + legacy_enabled: bool, ) -> anyhow::Result<()> { let _ = rustls::crypto::ring::default_provider().install_default(); @@ -177,7 +179,7 @@ async fn start_server_with_addr( req.extensions_mut() .insert::(client_info.clone()); req.extensions_mut().insert::(app_state.clone()); - handle_request(req) + handle_request(req, legacy_enabled) }), ) .await @@ -194,7 +196,7 @@ async fn start_server_with_addr( }, ); req.extensions_mut().insert::(app_state.clone()); - handle_request(req) + handle_request(req, legacy_enabled) }), ) .await @@ -251,19 +253,24 @@ impl RequestClientInfo { } } -async fn handle_request(req: Request) -> Result>, hyper::Error> { - Ok(handle_request_inner(req).await.unwrap_or_else(|err| { - tracing::error!("Error handling request: {err:?}"); - JsonResponse { - status: err.status, - body: ErrorResponse { - message: err - .message - .unwrap_or_else(|| "Internal Server Error".to_string()), - }, - } - .into_response() - })) +async fn handle_request( + req: Request, + legacy_enabled: bool, +) -> Result>, hyper::Error> { + Ok(handle_request_inner(req, legacy_enabled) + .await + .unwrap_or_else(|err| { + tracing::error!("Error handling request: {err:?}"); + JsonResponse { + status: err.status, + body: ErrorResponse { + message: err + .message + .unwrap_or_else(|| "Internal Server Error".to_string()), + }, + } + .into_response() + })) } struct JsonResponse { @@ -291,6 +298,7 @@ impl JsonResponse { async fn handle_request_inner( mut req: Request, + legacy_enabled: bool, ) -> Result>, AppError> { let Some(state) = req.extensions_mut().remove::() else { return Err(AppError::status(StatusCode::INTERNAL_SERVER_ERROR, None)); @@ -301,6 +309,50 @@ async fn handle_request_inner( }; match (req.method(), req.uri().path()) { + (&Method::POST, "/api/localsend/v2/register") => { + if !legacy_enabled { + return Err(AppError::status(StatusCode::NOT_FOUND, None)); + } + + Ok( + controller::v2::register(req.into_body(), state, client_info) + .await? + .into_response(), + ) + } + (&Method::POST, "/api/localsend/v2/prepare-upload") => { + if !legacy_enabled { + return Err(AppError::status(StatusCode::NOT_FOUND, None)); + } + + Ok( + controller::v2::register(req.into_body(), state, client_info) + .await? + .into_response(), + ) + } + (&Method::POST, "/api/localsend/v2/upload") => { + if !legacy_enabled { + return Err(AppError::status(StatusCode::NOT_FOUND, None)); + } + + Ok( + controller::v2::register(req.into_body(), state, client_info) + .await? + .into_response(), + ) + } + (&Method::POST, "/api/localsend/v2/cancel") => { + if !legacy_enabled { + return Err(AppError::status(StatusCode::NOT_FOUND, None)); + } + + Ok( + controller::v2::register(req.into_body(), state, client_info) + .await? + .into_response(), + ) + } (&Method::POST, "/api/localsend/v3/nonce") => { Ok( controller::v3::nonce_exchange(req.into_body(), state, client_info) diff --git a/core/src/main.rs b/core/src/main.rs index 678bf7d9..ce5eec38 100644 --- a/core/src/main.rs +++ b/core/src/main.rs @@ -6,6 +6,7 @@ mod webrtc; use crate::crypto::token; use crate::http::client::LsHttpClient; +use crate::http::dto::{PrepareUploadRequestDto, ProtocolType, RegisterDto}; use crate::http::server::TlsConfig; use crate::model::discovery::DeviceType; use crate::webrtc::signaling::{ClientInfo, WsServerMessage}; @@ -19,7 +20,6 @@ use tokio::io; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::sync::{mpsc, oneshot}; use tracing::Level; -use crate::http::dto::{PrepareUploadRequestDto, ProtocolType, RegisterDto}; #[tokio::main] #[cfg(feature = "full")] @@ -141,6 +141,7 @@ async fn server_test() -> Result<()> { private_key: PRIVATE_KEY.to_string(), }), client_info, + true, ) .await?; tokio::time::sleep(std::time::Duration::from_secs(u64::MAX)).await; @@ -151,11 +152,9 @@ async fn server_test() -> Result<()> { async fn client_test() -> Result<()> { let client = LsHttpClient::try_new(PRIVATE_KEY, CERT)?; - let nonce = client.nonce( - &ProtocolType::Https, - "localhost", - 53317, - ).await?; + let nonce = client + .nonce(&ProtocolType::Https, "localhost", 53317) + .await?; println!("Received Nonce: {}", nonce);