feat: complete nonce exchange

This commit is contained in:
Tien Do Nam
2025-07-16 21:20:45 +02:00
parent 5a7a15a553
commit f57cae5561
6 changed files with 219 additions and 48 deletions
+109 -22
View File
@@ -1,16 +1,29 @@
mod url;
use crate::http::{dto, StatusCodeError};
use crate::model::discovery::{ProtocolType, RegisterDto, RegisterResponseDto};
use crate::model::transfer::{PrepareUploadRequestDto, PrepareUploadResponseDto};
use crate::{crypto, util};
use futures_util::StreamExt;
use lru::LruCache;
use reqwest::{Response, StatusCode};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use std::num::NonZeroUsize;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tokio_stream::wrappers::ReceiverStream;
use crate::http::StatusCodeError;
use crate::http::client::url::{ApiVersion, TargetUrl};
const BASE_PATH: &str = "/api/localsend/v2";
const BASE_PATH: &str = "/api/localsend/v3";
pub struct LsHttpClient {
client: reqwest::Client,
/// Maps client identifiers to nonces that have been received from remote.
received_nonce_map: Arc<Mutex<LruCache<String, Vec<u8>>>>,
/// Maps client identifiers to nonces that are expected to be received from remote.
generated_nonce_map: Arc<Mutex<LruCache<String, Vec<u8>>>>,
}
pub struct RegisterResult {
@@ -39,7 +52,72 @@ impl LsHttpClient {
.identity(identity)
.build()?;
Ok(Self { client })
Ok(Self {
client,
received_nonce_map: Arc::new(Mutex::new(LruCache::new(
NonZeroUsize::new(200).unwrap(),
))),
generated_nonce_map: Arc::new(Mutex::new(LruCache::new(
NonZeroUsize::new(200).unwrap(),
))),
})
}
pub async fn nonce(
&self,
protocol: &ProtocolType,
ip: &str,
port: u16,
) -> anyhow::Result<String> {
// Generate nonce to send to server
let generated_nonce = crypto::nonce::generate_nonce();
let generated_nonce_base64 = util::base64::encode(&generated_nonce);
let request_body = 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())
.body(serde_json::to_string(&request_body)?)
.send()
.await?;
if res.status() != StatusCode::OK {
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?;
// Save the response nonce and our generated nonce
let response_nonce = util::base64::decode(&body.nonce)?;
let mut received_nonce_map = self.received_nonce_map.lock().await;
received_nonce_map.put(remote_key.clone(), response_nonce);
let mut generated_nonce_map = self.generated_nonce_map.lock().await;
generated_nonce_map.put(remote_key.clone(), generated_nonce);
tracing::info!("Nonce exchange successful for server: {ip} (ID: {remote_key})");
tracing::debug!(
"Received map: {:?}",
received_nonce_map.get(&remote_key).unwrap()
);
tracing::debug!(
"Generated map: {:?}",
generated_nonce_map.get(&remote_key).unwrap()
);
Ok(body.nonce)
}
pub async fn register(
@@ -166,24 +244,6 @@ impl LsHttpClient {
}
}
/// Verifies the certificate from the response.
/// Returns the public key extracted from the certificate.
fn verify_cert_from_res(response: &Response, public_key: Option<String>) -> anyhow::Result<String> {
let tls_info_ext = response
.extensions()
.get::<reqwest::tls::TlsInfo>()
.ok_or_else(|| anyhow::anyhow!("TLS info not found"))?;
let cert = tls_info_ext
.peer_certificate()
.ok_or_else(|| anyhow::anyhow!("Certificate not found"))?;
let public_key = match public_key {
Some(public_key) => public_key.to_owned(),
None => crate::crypto::cert::public_key_from_cert_der(cert)?,
};
crate::crypto::cert::verify_cert_from_der(cert, Some(public_key.clone()))?;
Ok(public_key)
}
#[derive(Serialize, Deserialize)]
struct ErrorResponse {
message: String,
@@ -205,3 +265,30 @@ 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> {
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")
}),
}
}
/// Verifies the certificate from the response.
/// Returns the public key extracted from the certificate.
fn verify_cert_from_res(response: &Response, public_key: Option<String>) -> anyhow::Result<String> {
let tls_info_ext = response
.extensions()
.get::<reqwest::tls::TlsInfo>()
.ok_or_else(|| anyhow::anyhow!("TLS info not found"))?;
let cert = tls_info_ext
.peer_certificate()
.ok_or_else(|| anyhow::anyhow!("Certificate not found"))?;
let public_key = match public_key {
Some(public_key) => public_key.to_owned(),
None => crypto::cert::public_key_from_cert_der(cert)?,
};
crypto::cert::verify_cert_from_der(cert, Some(public_key.clone()))?;
Ok(public_key)
}
+34
View File
@@ -0,0 +1,34 @@
use crate::model::discovery::ProtocolType;
use std::borrow::Cow;
pub struct TargetUrl {
pub version: ApiVersion,
pub protocol: ProtocolType,
pub host: String,
pub port: u16,
pub path: &'static str,
}
pub enum ApiVersion {
V2,
V3,
}
impl TargetUrl {
pub fn to_string(&self) -> String {
format!(
"{}://{}:{}/api/localsend/{}{}",
self.protocol.as_str(),
match self.host.contains(':') {
true => Cow::Owned(format!("[{}]", self.host)), // IPv6 addresses need to be enclosed in brackets
false => Cow::Borrowed(&self.host),
},
self.port,
match self.version {
ApiVersion::V2 => "v2",
ApiVersion::V3 => "v3",
},
self.path
)
}
}
+5
View File
@@ -1,3 +1,4 @@
use crate::model::discovery::{RegisterDto, RegisterResponseDto};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
@@ -12,6 +13,10 @@ 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.
+27
View File
@@ -0,0 +1,27 @@
use hyper::body::Incoming;
use hyper::StatusCode;
use http_body_util::BodyExt;
use serde::de::DeserializeOwned;
use crate::http::server::error::AppError;
pub(crate) trait CollectToJson {
async fn collect_to_json<T: DeserializeOwned>(self) -> Result<T, AppError>;
}
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)
}
}
+36 -26
View File
@@ -1,13 +1,14 @@
mod client_cert_verifier;
mod error;
mod collect_to_json;
use crate::crypto::cert::public_key_from_cert_der;
use crate::http::dto::{ErrorResponse, NonceRequest, NonceResponse};
use crate::http::dto::{ErrorResponse, NonceRequest, NonceResponse, RegisterRequest, RegisterResponse};
use crate::http::server::client_cert_verifier::CustomClientCertVerifier;
use crate::http::server::error::AppError;
use crate::{crypto, util};
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
use http_body_util::Full;
use hyper::body::{Body, Incoming};
use hyper::{http, Method, Request, Response, StatusCode};
use hyper_util::rt::{TokioExecutor, TokioIo};
@@ -24,6 +25,7 @@ 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;
#[derive(Clone)]
struct AppState {
@@ -211,13 +213,7 @@ struct ClientInfo {
cert: Option<Vec<u8>>,
}
trait ClientInfoExt {
fn extract_public_key(&self) -> Option<String>;
fn to_remote_key(&self) -> String;
}
impl ClientInfoExt for ClientInfo {
impl ClientInfo {
fn extract_public_key(&self) -> Option<String> {
match &self.cert {
Some(cert) => match public_key_from_cert_der(cert) {
@@ -292,6 +288,11 @@ async fn handle_request_inner(
.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;
@@ -305,19 +306,9 @@ async fn nonce_exchange(
state: AppState,
client_info: ClientInfo,
) -> Result<JsonResponse<NonceResponse>, AppError> {
let bytes = body.collect().await?.to_bytes();
let request = match serde_json::from_slice::<NonceRequest>(&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()),
));
}
};
let payload = body.collect_to_json::<NonceRequest>().await?;
let nonce = util::base64::decode(&request.nonce).map_err(|_| {
let nonce = util::base64::decode(&payload.nonce).map_err(|_| {
tracing::warn!("Failed to decode nonce from base64");
AppError::status(
StatusCode::BAD_REQUEST,
@@ -335,16 +326,20 @@ async fn nonce_exchange(
// Save the nonce
let remote_key = client_info.to_remote_key();
let mut challenged_nonce_map = state.received_nonce_map.lock().await;
challenged_nonce_map.put(remote_key.clone(), nonce);
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 expecting_nonce_map = state.generated_nonce_map.lock().await;
expecting_nonce_map.put(remote_key, 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: {}", client_info.ip);
tracing::info!(
"Nonce exchange successful for client: {} (ID: {})",
client_info.ip,
remote_key
);
Ok(JsonResponse {
status: StatusCode::OK,
@@ -353,3 +348,18 @@ async fn nonce_exchange(
},
})
}
// 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,
// },
// })
// }
+8
View File
@@ -143,6 +143,14 @@ 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?;
println!("Received Nonce: {}", nonce);
let register_dto = RegisterDto {
alias: "test 2".to_string(),
version: "2.3".to_string(),