mirror of
https://github.com/localsend/localsend.git
synced 2026-06-23 04:10:07 +00:00
feat: complete nonce exchange
This commit is contained in:
+109
-22
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user