mirror of
https://github.com/localsend/localsend.git
synced 2026-06-22 20:00:07 +00:00
refactor: http module
This commit is contained in:
@@ -51,6 +51,8 @@ pub fn public_key_from_cert_pem(cert: String) -> anyhow::Result<String> {
|
||||
public_key_from_cert(parsed_cert)
|
||||
}
|
||||
|
||||
/// Extracts the public key from the certificate which is in DER format.
|
||||
/// Encodes the public key in PEM format.
|
||||
pub fn public_key_from_cert_der(cert: &[u8]) -> anyhow::Result<String> {
|
||||
let (_, parsed_cert) = X509Certificate::from_der(&cert)?;
|
||||
public_key_from_cert(parsed_cert)
|
||||
|
||||
+32
-24
@@ -1,8 +1,9 @@
|
||||
mod url;
|
||||
|
||||
use crate::http::{dto, StatusCodeError};
|
||||
use crate::model::discovery::{ProtocolType, RegisterDto, RegisterResponseDto};
|
||||
use crate::model::transfer::{PrepareUploadRequestDto, PrepareUploadResponseDto};
|
||||
use crate::http;
|
||||
use crate::http::client::url::{ApiVersion, TargetUrl};
|
||||
use crate::http::dto::ProtocolType;
|
||||
use crate::http::StatusCodeError;
|
||||
use crate::{crypto, util};
|
||||
use futures_util::StreamExt;
|
||||
use lru::LruCache;
|
||||
@@ -12,7 +13,6 @@ use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use crate::http::client::url::{ApiVersion, TargetUrl};
|
||||
|
||||
const BASE_PATH: &str = "/api/localsend/v3";
|
||||
|
||||
@@ -33,7 +33,7 @@ pub struct RegisterResult {
|
||||
pub public_key: Option<String>,
|
||||
|
||||
/// The response body from the register request.
|
||||
pub body: RegisterResponseDto,
|
||||
pub body: http::dto::RegisterResponseDto,
|
||||
}
|
||||
|
||||
impl LsHttpClient {
|
||||
@@ -73,19 +73,22 @@ impl LsHttpClient {
|
||||
let generated_nonce = crypto::nonce::generate_nonce();
|
||||
let generated_nonce_base64 = util::base64::encode(&generated_nonce);
|
||||
|
||||
let request_body = dto::NonceRequest {
|
||||
let request_body = http::dto::NonceRequest {
|
||||
nonce: generated_nonce_base64,
|
||||
};
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.clone(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/nonce",
|
||||
}.to_string())
|
||||
.post(
|
||||
TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.clone(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/nonce",
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
.body(serde_json::to_string(&request_body)?)
|
||||
.send()
|
||||
.await?;
|
||||
@@ -94,8 +97,8 @@ impl LsHttpClient {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
}
|
||||
|
||||
let remote_key = to_remote_key(&res, protocol == &ProtocolType::Https, None)?;
|
||||
let body = res.json::<dto::NonceResponse>().await?;
|
||||
let remote_key = to_identifier(&res, protocol == &ProtocolType::Https, None)?;
|
||||
let body = res.json::<http::dto::NonceResponse>().await?;
|
||||
|
||||
// Save the response nonce and our generated nonce
|
||||
let response_nonce = util::base64::decode(&body.nonce)?;
|
||||
@@ -125,7 +128,7 @@ impl LsHttpClient {
|
||||
protocol: &ProtocolType,
|
||||
ip: &str,
|
||||
port: u16,
|
||||
payload: RegisterDto,
|
||||
payload: http::dto::RegisterDto,
|
||||
) -> anyhow::Result<RegisterResult> {
|
||||
let res = self
|
||||
.client
|
||||
@@ -145,7 +148,7 @@ impl LsHttpClient {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let body = res.json::<RegisterResponseDto>().await?;
|
||||
let body = res.json::<http::dto::RegisterResponseDto>().await?;
|
||||
|
||||
Ok(RegisterResult { public_key, body })
|
||||
}
|
||||
@@ -156,8 +159,8 @@ impl LsHttpClient {
|
||||
ip: &str,
|
||||
port: u16,
|
||||
public_key: Option<String>,
|
||||
payload: PrepareUploadRequestDto,
|
||||
) -> anyhow::Result<PrepareUploadResponseDto> {
|
||||
payload: http::dto::PrepareUploadRequestDto,
|
||||
) -> anyhow::Result<http::dto::PrepareUploadResponseDto> {
|
||||
let res = self
|
||||
.client
|
||||
.post(format!(
|
||||
@@ -179,7 +182,7 @@ impl LsHttpClient {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
}
|
||||
|
||||
let body = res.json::<PrepareUploadResponseDto>().await?;
|
||||
let body = res.json::<http::dto::PrepareUploadResponseDto>().await?;
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
@@ -266,12 +269,17 @@ async fn status_code_error_from_res(response: Response) -> anyhow::Result<anyhow
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_remote_key(response: &Response, require_cert: bool, public_key: Option<String>) -> anyhow::Result<String> {
|
||||
fn to_identifier(
|
||||
response: &Response,
|
||||
require_cert: bool,
|
||||
public_key: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
match require_cert {
|
||||
true => verify_cert_from_res(response, public_key),
|
||||
false => response.remote_addr().map(|addr| addr.ip().to_string()).ok_or_else(|| {
|
||||
anyhow::anyhow!("Remote address not found in response")
|
||||
}),
|
||||
false => response
|
||||
.remote_addr()
|
||||
.map(|addr| addr.ip().to_string())
|
||||
.ok_or_else(|| anyhow::anyhow!("Remote address not found in response")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::model::discovery::ProtocolType;
|
||||
use crate::http::dto::ProtocolType;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub struct TargetUrl {
|
||||
|
||||
+80
-5
@@ -1,5 +1,7 @@
|
||||
use crate::model::discovery::{RegisterDto, RegisterResponseDto};
|
||||
use crate::model::discovery::DeviceType;
|
||||
use crate::model::transfer::FileDto;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct NonceRequest {
|
||||
@@ -13,12 +15,85 @@ pub struct NonceResponse {
|
||||
pub nonce: String,
|
||||
}
|
||||
|
||||
pub type RegisterRequest = RegisterDto;
|
||||
|
||||
pub type RegisterResponse = RegisterResponseDto;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
/// The error message.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterDto {
|
||||
pub alias: String,
|
||||
|
||||
pub version: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_model: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_type: Option<DeviceType>,
|
||||
|
||||
pub token: String,
|
||||
|
||||
pub port: u16,
|
||||
|
||||
pub protocol: ProtocolType,
|
||||
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub has_web_interface: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ProtocolType {
|
||||
Http,
|
||||
Https,
|
||||
}
|
||||
|
||||
impl ProtocolType {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
ProtocolType::Http => "http",
|
||||
ProtocolType::Https => "https",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to `RegisterDto`, but without `port` and `protocol` (those are already known).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterResponseDto {
|
||||
pub alias: String,
|
||||
|
||||
pub version: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_model: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_type: Option<DeviceType>,
|
||||
|
||||
pub token: String,
|
||||
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub has_web_interface: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PrepareUploadRequestDto {
|
||||
pub info: RegisterDto,
|
||||
pub files: HashMap<String, FileDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PrepareUploadResponseDto {
|
||||
pub session_id: String,
|
||||
pub files: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
|
||||
t == &T::default()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
pub mod dto;
|
||||
pub mod server;
|
||||
pub mod state;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{status};{message:?}")]
|
||||
|
||||
@@ -11,17 +11,9 @@ pub(crate) trait CollectToJson {
|
||||
impl CollectToJson for Incoming {
|
||||
async fn collect_to_json<T: DeserializeOwned>(self) -> Result<T, AppError> {
|
||||
let bytes = self.collect().await?.to_bytes();
|
||||
let request = match serde_json::from_slice::<T>(&bytes) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse JSON body: {err:#}");
|
||||
return Err(AppError::status(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some("Invalid JSON body".to_string()),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(request)
|
||||
serde_json::from_slice::<T>(&bytes).map_err(|err| {
|
||||
tracing::warn!("Failed to parse JSON body: {err:#}");
|
||||
AppError::status(StatusCode::BAD_REQUEST, Some("Invalid JSON body".to_string()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod v3;
|
||||
pub(crate) mod web;
|
||||
@@ -0,0 +1,78 @@
|
||||
use hyper::body::Incoming;
|
||||
use hyper::StatusCode;
|
||||
use crate::http::dto::{NonceRequest, NonceResponse, RegisterDto, RegisterResponseDto};
|
||||
use crate::http::server::{AppState, RequestClientInfo, JsonResponse};
|
||||
use crate::http::server::error::AppError;
|
||||
use crate::{crypto, util};
|
||||
use crate::http::server::collect_to_json::CollectToJson;
|
||||
|
||||
pub(crate) async fn nonce_exchange(
|
||||
body: Incoming,
|
||||
state: AppState,
|
||||
client_info: RequestClientInfo,
|
||||
) -> Result<JsonResponse<NonceResponse>, AppError> {
|
||||
let payload = body.collect_to_json::<NonceRequest>().await?;
|
||||
|
||||
let nonce = util::base64::decode(&payload.nonce).map_err(|_| {
|
||||
tracing::warn!("Failed to decode nonce from base64");
|
||||
AppError::status(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some("Invalid nonce format".to_string()),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !crypto::nonce::validate_nonce(&nonce) {
|
||||
tracing::warn!("Invalid nonce received");
|
||||
return Err(AppError::status(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some("Invalid nonce".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
// Save the nonce
|
||||
let remote_key = client_info.identifier();
|
||||
let mut received_nonce_map = state.received_nonce_map.lock().await;
|
||||
received_nonce_map.put(remote_key.clone(), nonce);
|
||||
|
||||
// Generate new nonce for the client
|
||||
let new_nonce = crypto::nonce::generate_nonce();
|
||||
let new_nonce_base64 = util::base64::encode(&new_nonce);
|
||||
let mut generated_nonce_map = state.generated_nonce_map.lock().await;
|
||||
generated_nonce_map.put(remote_key.clone(), new_nonce);
|
||||
|
||||
tracing::info!(
|
||||
"Nonce exchange successful for client: {} (ID: {})",
|
||||
client_info.ip,
|
||||
remote_key
|
||||
);
|
||||
|
||||
Ok(JsonResponse {
|
||||
status: StatusCode::OK,
|
||||
body: NonceResponse {
|
||||
nonce: new_nonce_base64,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn register(
|
||||
body: Incoming,
|
||||
state: AppState,
|
||||
client_info: RequestClientInfo,
|
||||
) -> Result<JsonResponse<RegisterResponseDto>, AppError> {
|
||||
let payload = body.collect_to_json::<RegisterDto>().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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WebPageState;
|
||||
+63
-104
@@ -1,34 +1,39 @@
|
||||
mod client_cert_verifier;
|
||||
mod error;
|
||||
mod collect_to_json;
|
||||
mod controller;
|
||||
mod error;
|
||||
|
||||
use crate::crypto::cert::public_key_from_cert_der;
|
||||
use crate::http::dto::{ErrorResponse, NonceRequest, NonceResponse, RegisterRequest, RegisterResponse};
|
||||
use crate::http::dto::ErrorResponse;
|
||||
use crate::http::server::client_cert_verifier::CustomClientCertVerifier;
|
||||
use crate::http::server::error::AppError;
|
||||
use crate::{crypto, util};
|
||||
use crate::http::state::ClientInfo;
|
||||
use bytes::Bytes;
|
||||
use http_body_util::Full;
|
||||
use hyper::body::{Body, Incoming};
|
||||
use hyper::body::Incoming;
|
||||
use hyper::{http, Method, Request, Response, StatusCode};
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use hyper_util::server::conn::auto::Builder;
|
||||
use lru::LruCache;
|
||||
use rustls::pki_types::pem::PemObject;
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use std::fmt::Debug;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{oneshot, Mutex};
|
||||
use uuid::Uuid;
|
||||
use x509_parser::nom::Parser;
|
||||
use crate::http::server::collect_to_json::CollectToJson;
|
||||
use crate::http::server::controller::web::WebPageState;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
/// Information about server's device.
|
||||
info: Arc<Mutex<ClientInfo>>,
|
||||
|
||||
/// State for serving web pages.
|
||||
web: Arc<Mutex<Option<WebPageState>>>,
|
||||
|
||||
/// Maps client identifiers to nonces that have been received from remote.
|
||||
received_nonce_map: Arc<Mutex<LruCache<String, Vec<u8>>>>,
|
||||
|
||||
@@ -37,8 +42,10 @@ struct AppState {
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn new() -> Self {
|
||||
fn new(info: Arc<Mutex<ClientInfo>>) -> Self {
|
||||
Self {
|
||||
info,
|
||||
web: Arc::new(Mutex::new(None)),
|
||||
received_nonce_map: Arc::new(Mutex::new(LruCache::new(
|
||||
NonZeroUsize::new(200).unwrap(),
|
||||
))),
|
||||
@@ -50,37 +57,46 @@ impl AppState {
|
||||
}
|
||||
|
||||
pub struct LsHttpServer {
|
||||
state: AppState,
|
||||
stop_tx: Arc<Mutex<Option<oneshot::Sender<()>>>>,
|
||||
}
|
||||
|
||||
impl LsHttpServer {
|
||||
/// Binds the server to the specified port on both IPv4 and IPv6 addresses.
|
||||
pub async fn start_with_port(
|
||||
port: u16,
|
||||
tls_config: Option<TlsConfig>,
|
||||
info: ClientInfo,
|
||||
) -> anyhow::Result<LsHttpServer> {
|
||||
let ipv4_socket_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port);
|
||||
let ipv6_socket_addr = SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), port);
|
||||
let info = Arc::new(Mutex::new(info));
|
||||
let state = AppState::new(info.clone());
|
||||
|
||||
let (stop_tx, stop_rx) = oneshot::channel::<()>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
result = start_server_with_addr(ipv4_socket_addr, tls_config.clone()) => {
|
||||
tracing::info!("Server stopped on: {}, {:?}", ipv4_socket_addr, result);
|
||||
}
|
||||
_ = async {
|
||||
if start_server_with_addr(ipv6_socket_addr, tls_config).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.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(u64::MAX)).await;
|
||||
tokio::spawn({
|
||||
let state = state.clone();
|
||||
async move {
|
||||
tokio::select! {
|
||||
_ = start_server_with_addr(ipv4_socket_addr, tls_config.clone(), state.clone()) => {
|
||||
tracing::info!("Server stopped on: {}", ipv4_socket_addr);
|
||||
}
|
||||
} => {}
|
||||
_ = stop_rx => {}
|
||||
_ = async {
|
||||
if start_server_with_addr(ipv6_socket_addr, tls_config, state).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.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(u64::MAX)).await;
|
||||
}
|
||||
} => {}
|
||||
_ = stop_rx => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(LsHttpServer {
|
||||
state,
|
||||
stop_tx: Arc::new(Mutex::new(Some(stop_tx))),
|
||||
})
|
||||
}
|
||||
@@ -107,11 +123,11 @@ pub struct TlsConfig {
|
||||
async fn start_server_with_addr(
|
||||
socket_addr: SocketAddr,
|
||||
tls_config: Option<TlsConfig>,
|
||||
app_state: AppState,
|
||||
) -> anyhow::Result<()> {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let incoming = tokio::net::TcpListener::bind(socket_addr).await?;
|
||||
let app_state = AppState::new();
|
||||
|
||||
let tls_acceptor = match tls_config {
|
||||
Some(tls_config) => Some(create_tls_config(&tls_config).inspect_err(|err| {
|
||||
@@ -144,7 +160,7 @@ async fn start_server_with_addr(
|
||||
|
||||
let client_info = {
|
||||
let (_, server_connection) = tls_stream.get_ref();
|
||||
ClientInfo {
|
||||
RequestClientInfo {
|
||||
ip: remote_addr.ip(),
|
||||
cert: server_connection
|
||||
.deref()
|
||||
@@ -159,7 +175,7 @@ async fn start_server_with_addr(
|
||||
TokioIo::new(tls_stream),
|
||||
hyper::service::service_fn(move |mut req: Request<Incoming>| {
|
||||
req.extensions_mut()
|
||||
.insert::<ClientInfo>(client_info.clone());
|
||||
.insert::<RequestClientInfo>(client_info.clone());
|
||||
req.extensions_mut().insert::<AppState>(app_state.clone());
|
||||
handle_request(req)
|
||||
}),
|
||||
@@ -171,10 +187,12 @@ async fn start_server_with_addr(
|
||||
.serve_connection(
|
||||
TokioIo::new(tcp_stream),
|
||||
hyper::service::service_fn(move |mut req: Request<Incoming>| {
|
||||
req.extensions_mut().insert::<ClientInfo>(ClientInfo {
|
||||
ip: remote_addr.ip(),
|
||||
cert: None,
|
||||
});
|
||||
req.extensions_mut().insert::<RequestClientInfo>(
|
||||
RequestClientInfo {
|
||||
ip: remote_addr.ip(),
|
||||
cert: None,
|
||||
},
|
||||
);
|
||||
req.extensions_mut().insert::<AppState>(app_state.clone());
|
||||
handle_request(req)
|
||||
}),
|
||||
@@ -205,7 +223,7 @@ fn create_tls_config(tls_config: &TlsConfig) -> anyhow::Result<tokio_rustls::Tls
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ClientInfo {
|
||||
struct RequestClientInfo {
|
||||
/// The IP address of the client.
|
||||
ip: IpAddr,
|
||||
|
||||
@@ -213,7 +231,7 @@ struct ClientInfo {
|
||||
cert: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl ClientInfo {
|
||||
impl RequestClientInfo {
|
||||
fn extract_public_key(&self) -> Option<String> {
|
||||
match &self.cert {
|
||||
Some(cert) => match public_key_from_cert_der(cert) {
|
||||
@@ -227,7 +245,7 @@ impl ClientInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_remote_key(&self) -> String {
|
||||
fn identifier(&self) -> String {
|
||||
self.extract_public_key()
|
||||
.unwrap_or_else(|| self.ip.to_string())
|
||||
}
|
||||
@@ -278,21 +296,25 @@ async fn handle_request_inner(
|
||||
return Err(AppError::status(StatusCode::INTERNAL_SERVER_ERROR, None));
|
||||
};
|
||||
|
||||
let Some(client_info) = req.extensions_mut().remove::<ClientInfo>() else {
|
||||
let Some(client_info) = req.extensions_mut().remove::<RequestClientInfo>() else {
|
||||
return Err(AppError::status(StatusCode::INTERNAL_SERVER_ERROR, None));
|
||||
};
|
||||
|
||||
match (req.method(), req.uri().path()) {
|
||||
(&Method::POST, "/api/localsend/v3/nonce") => {
|
||||
Ok(nonce_exchange(req.into_body(), state, client_info)
|
||||
.await?
|
||||
.into_response())
|
||||
Ok(
|
||||
controller::v3::nonce_exchange(req.into_body(), state, client_info)
|
||||
.await?
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
(&Method::POST, "/api/localsend/v3/register") => {
|
||||
Ok(
|
||||
controller::v3::register(req.into_body(), state, client_info)
|
||||
.await?
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
// (&Method::POST, "/api/localsend/v3/register") => {
|
||||
// Ok(register(req.into_body(), state, client_info)
|
||||
// .await?
|
||||
// .into_response())
|
||||
// }
|
||||
_ => {
|
||||
let mut res = Response::new(Full::default());
|
||||
*res.status_mut() = StatusCode::NOT_FOUND;
|
||||
@@ -300,66 +322,3 @@ async fn handle_request_inner(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn nonce_exchange(
|
||||
body: Incoming,
|
||||
state: AppState,
|
||||
client_info: ClientInfo,
|
||||
) -> Result<JsonResponse<NonceResponse>, AppError> {
|
||||
let payload = body.collect_to_json::<NonceRequest>().await?;
|
||||
|
||||
let nonce = util::base64::decode(&payload.nonce).map_err(|_| {
|
||||
tracing::warn!("Failed to decode nonce from base64");
|
||||
AppError::status(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some("Invalid nonce format".to_string()),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !crypto::nonce::validate_nonce(&nonce) {
|
||||
tracing::warn!("Invalid nonce received");
|
||||
return Err(AppError::status(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some("Invalid nonce".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
// Save the nonce
|
||||
let remote_key = client_info.to_remote_key();
|
||||
let mut received_nonce_map = state.received_nonce_map.lock().await;
|
||||
received_nonce_map.put(remote_key.clone(), nonce);
|
||||
|
||||
// Generate new nonce for the client
|
||||
let new_nonce = crypto::nonce::generate_nonce();
|
||||
let new_nonce_base64 = util::base64::encode(&new_nonce);
|
||||
let mut generated_nonce_map = state.generated_nonce_map.lock().await;
|
||||
generated_nonce_map.put(remote_key.clone(), new_nonce);
|
||||
|
||||
tracing::info!(
|
||||
"Nonce exchange successful for client: {} (ID: {})",
|
||||
client_info.ip,
|
||||
remote_key
|
||||
);
|
||||
|
||||
Ok(JsonResponse {
|
||||
status: StatusCode::OK,
|
||||
body: NonceResponse {
|
||||
nonce: new_nonce_base64,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// async fn register(
|
||||
// body: Incoming,
|
||||
// state: AppState,
|
||||
// client_info: ClientInfo,
|
||||
// ) -> Result<JsonResponse<RegisterResponse>, AppError> {
|
||||
// let payload = body.collect_to_json::<RegisterRequest>().await?;
|
||||
//
|
||||
// Ok(JsonResponse {
|
||||
// status: StatusCode::OK,
|
||||
// body: RegisterResponse {
|
||||
// token: payload.token,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::model::discovery::DeviceType;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientInfo {
|
||||
/// The name of the peer.
|
||||
pub alias: String,
|
||||
|
||||
/// Client Protocol Version (major.minor)
|
||||
pub version: String,
|
||||
|
||||
/// The device model of the peer.
|
||||
/// Windows, macOS, iPhone, Samsung, etc.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_model: Option<String>,
|
||||
|
||||
/// The device type of the peer.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_type: Option<DeviceType>,
|
||||
|
||||
/// A token generated by the client.
|
||||
/// Used to merge the same peers detected on different channels (LAN, WebRTC, etc.).
|
||||
pub token: String,
|
||||
}
|
||||
+13
-5
@@ -7,8 +7,7 @@ mod webrtc;
|
||||
use crate::crypto::token;
|
||||
use crate::http::client::LsHttpClient;
|
||||
use crate::http::server::TlsConfig;
|
||||
use crate::model::discovery::{DeviceType, ProtocolType, RegisterDto};
|
||||
use crate::model::transfer::PrepareUploadRequestDto;
|
||||
use crate::model::discovery::DeviceType;
|
||||
use crate::webrtc::signaling::{ClientInfo, WsServerMessage};
|
||||
use crate::webrtc::webrtc::{PinConfig, RTCFile, RTCFileError, RTCSendFileResponse, RTCStatus};
|
||||
use anyhow::Result;
|
||||
@@ -20,6 +19,7 @@ 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")]
|
||||
@@ -127,12 +127,20 @@ MCowBQYDK2VwAyEAZmdXP230oqK92o65ra3XaF2F8r3+fK5DEBK4c40qVts=
|
||||
}
|
||||
|
||||
async fn server_test() -> Result<()> {
|
||||
let client_info = http::state::ClientInfo {
|
||||
alias: "Server-Test".to_string(),
|
||||
version: "1.2.3".to_string(),
|
||||
device_model: None,
|
||||
device_type: None,
|
||||
token: "456".to_string(),
|
||||
};
|
||||
let server = http::server::LsHttpServer::start_with_port(
|
||||
53317,
|
||||
Some(TlsConfig {
|
||||
cert: CERT.to_string(),
|
||||
private_key: PRIVATE_KEY.to_string(),
|
||||
}),
|
||||
client_info,
|
||||
)
|
||||
.await?;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(u64::MAX)).await;
|
||||
@@ -156,10 +164,10 @@ async fn client_test() -> Result<()> {
|
||||
version: "2.3".to_string(),
|
||||
device_model: Some("test".to_string()),
|
||||
device_type: Some(DeviceType::Headless),
|
||||
fingerprint: "test".to_string(),
|
||||
token: "test".to_string(),
|
||||
port: 53317,
|
||||
protocol: ProtocolType::Https,
|
||||
download: false,
|
||||
has_web_interface: false,
|
||||
};
|
||||
|
||||
let response = client
|
||||
@@ -179,7 +187,7 @@ async fn client_test() -> Result<()> {
|
||||
files: {
|
||||
let mut map = HashMap::new();
|
||||
let id = "test-123-id".to_string();
|
||||
let file = crate::model::transfer::FileDto {
|
||||
let file = model::transfer::FileDto {
|
||||
id: id.clone(),
|
||||
file_name: "test.mp4".to_string(),
|
||||
size: 1000,
|
||||
|
||||
@@ -1,34 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterDto {
|
||||
pub alias: String,
|
||||
pub version: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_model: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_type: Option<DeviceType>,
|
||||
pub fingerprint: String,
|
||||
pub port: u16,
|
||||
pub protocol: ProtocolType,
|
||||
pub download: bool,
|
||||
}
|
||||
|
||||
/// Similar to `RegisterDto`, but without `port` and `protocol` (those are already known).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterResponseDto {
|
||||
pub alias: String,
|
||||
pub version: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_model: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_type: Option<DeviceType>,
|
||||
pub fingerprint: String,
|
||||
pub download: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum DeviceType {
|
||||
@@ -38,19 +9,3 @@ pub enum DeviceType {
|
||||
Headless,
|
||||
Server,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ProtocolType {
|
||||
Http,
|
||||
Https,
|
||||
}
|
||||
|
||||
impl ProtocolType {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
ProtocolType::Http => "http",
|
||||
ProtocolType::Https => "https",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::model::discovery::RegisterDto;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -24,17 +22,3 @@ pub struct FileMetadata {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub accessed: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PrepareUploadRequestDto {
|
||||
pub info: RegisterDto,
|
||||
pub files: HashMap<String, FileDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PrepareUploadResponseDto {
|
||||
pub session_id: String,
|
||||
pub files: HashMap<String, String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user