mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: verify sha256
This commit is contained in:
@@ -87,6 +87,6 @@ async fn read_and_hash_from_file(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_hex(bytes: &[u8]) -> String {
|
||||
pub(crate) fn to_hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
@@ -203,6 +203,7 @@ impl LsHttpClientV2 {
|
||||
/// * 400 - Missing parameters
|
||||
/// * 403 - Invalid token or IP address
|
||||
/// * 409 - Blocked by another session
|
||||
/// * 422 - Checksum mismatch
|
||||
/// * 500 - Unknown error
|
||||
pub async fn upload(
|
||||
&self,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::crypto;
|
||||
use bytes::Bytes;
|
||||
use http_body_util::BodyExt;
|
||||
use hyper::body::Incoming;
|
||||
@@ -58,11 +59,28 @@ pub enum FileUploadTarget {
|
||||
},
|
||||
}
|
||||
|
||||
/// Outcome of receiving an uploaded file.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SaveResult {
|
||||
/// The file has been received and, if a checksum was given, it matched.
|
||||
Success,
|
||||
|
||||
/// The body could not be read or the target failed to process it.
|
||||
Failed,
|
||||
|
||||
/// The received bytes do not match the expected SHA-256 checksum.
|
||||
HashMismatch,
|
||||
}
|
||||
|
||||
/// Forwards the body of `req` to `target`.
|
||||
pub(crate) async fn save_req_to_target(
|
||||
req: Request<Incoming>,
|
||||
target: FileUploadTarget,
|
||||
file_size: u64,
|
||||
) -> bool {
|
||||
expected_sha256: Option<&str>,
|
||||
) -> SaveResult {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
// Resolve the target into a chunk sender and a result receiver.
|
||||
let (binary_tx, result_rx) = match target {
|
||||
FileUploadTarget::Stream {
|
||||
@@ -103,7 +121,8 @@ pub(crate) async fn save_req_to_target(
|
||||
),
|
||||
};
|
||||
|
||||
// Forward the request body to the target.
|
||||
// Forward the request body to the target, hashing it on the way if requested.
|
||||
let mut hasher = expected_sha256.map(|_| Sha256::new());
|
||||
let mut body = req.into_body();
|
||||
let mut stream_error = false;
|
||||
while let Some(frame) = body.frame().await {
|
||||
@@ -115,6 +134,9 @@ pub(crate) async fn save_req_to_target(
|
||||
if data.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(hasher) = &mut hasher {
|
||||
hasher.update(&data);
|
||||
}
|
||||
if binary_tx.send(data).await.is_err() {
|
||||
// The receiver is gone (dropped by the application or
|
||||
// closed by the file writer after an error).
|
||||
@@ -133,17 +155,28 @@ pub(crate) async fn save_req_to_target(
|
||||
// Signal end of file to the receiving side.
|
||||
drop(binary_tx);
|
||||
|
||||
match stream_error {
|
||||
true => false,
|
||||
false => match result_rx.await {
|
||||
Ok(Ok(())) => true,
|
||||
Ok(Err(err)) => {
|
||||
tracing::warn!("Failed to process file: {err}");
|
||||
false
|
||||
}
|
||||
Err(_) => false,
|
||||
},
|
||||
if stream_error {
|
||||
return SaveResult::Failed;
|
||||
}
|
||||
|
||||
match result_rx.await {
|
||||
Ok(Ok(())) => (),
|
||||
Ok(Err(err)) => {
|
||||
tracing::warn!("Failed to process file: {err}");
|
||||
return SaveResult::Failed;
|
||||
}
|
||||
Err(_) => return SaveResult::Failed,
|
||||
}
|
||||
|
||||
if let (Some(hasher), Some(expected)) = (hasher, expected_sha256) {
|
||||
let actual = crypto::hash::to_hex(&hasher.finalize());
|
||||
if !actual.eq_ignore_ascii_case(expected) {
|
||||
tracing::warn!("Checksum mismatch: expected {expected}, got {actual}");
|
||||
return SaveResult::HashMismatch;
|
||||
}
|
||||
}
|
||||
|
||||
SaveResult::Success
|
||||
}
|
||||
|
||||
/// Spawns a task that writes incoming chunks to a file provided by `open`.
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::http::server::common::error::AppError;
|
||||
use crate::http::server::common::pin::check_pin;
|
||||
use crate::http::server::common::query::parse_query;
|
||||
use crate::http::server::common::response::{empty_body, BoxedBody, JsonResponse};
|
||||
use crate::http::server::common::save::FileUploadTarget;
|
||||
use crate::http::server::common::save::{FileUploadTarget, SaveResult};
|
||||
use crate::http::server::common::session::{
|
||||
FileStatusV2, SessionFileV2, SessionStateV2, UploadSessionV2,
|
||||
};
|
||||
@@ -359,6 +359,7 @@ pub(crate) async fn upload(
|
||||
let mut upload_guard = UploadGuard::new(v2.clone(), session_id.clone(), file_id.clone());
|
||||
|
||||
let file_size = file_dto.size;
|
||||
let expected_sha256 = file_dto.sha256.clone();
|
||||
let (target_tx, target_rx) = oneshot::channel::<FileUploadTarget>();
|
||||
|
||||
let event = ServerEventV2::FileUpload {
|
||||
@@ -377,13 +378,18 @@ pub(crate) async fn upload(
|
||||
return Err(AppError::Status(StatusCode::INTERNAL_SERVER_ERROR));
|
||||
};
|
||||
|
||||
let success = common::save::save_req_to_target(req, target, file_size).await;
|
||||
let result =
|
||||
common::save::save_req_to_target(req, target, file_size, expected_sha256.as_deref()).await;
|
||||
|
||||
upload_guard.finish(success).await;
|
||||
upload_guard.finish(result == SaveResult::Success).await;
|
||||
|
||||
match success {
|
||||
true => Ok(Response::new(empty_body())),
|
||||
false => Err(AppError::Status(StatusCode::INTERNAL_SERVER_ERROR)),
|
||||
match result {
|
||||
SaveResult::Success => Ok(Response::new(empty_body())),
|
||||
SaveResult::Failed => Err(AppError::Status(StatusCode::INTERNAL_SERVER_ERROR)),
|
||||
SaveResult::HashMismatch => Err(AppError::Message(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"Checksum mismatch".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_util::StreamExt;
|
||||
use localsend::crypto::hash::sha256_hex;
|
||||
use localsend::http::client::{ClientError, LsHttpClientV2};
|
||||
use localsend::http::dto::ProtocolType;
|
||||
use localsend::http::dto_v2::{PrepareUploadRequestDtoV2, ProtocolTypeV2, RegisterDtoV2};
|
||||
@@ -368,6 +369,78 @@ async fn test_full_upload_flow() {
|
||||
assert_status(result, 403);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_upload_with_matching_sha256() {
|
||||
let server = start_test_server(None, true, None).await;
|
||||
let client = LsHttpClientV2::try_new_without_cert().unwrap();
|
||||
|
||||
let bytes = b"hello".to_vec();
|
||||
let mut file = file_dto("file-a", "a.bin", bytes.len() as u64);
|
||||
file.sha256 = Some(sha256_hex(&bytes));
|
||||
|
||||
let response = client
|
||||
.prepare_upload(
|
||||
ProtocolType::Http,
|
||||
"127.0.0.1",
|
||||
server.port,
|
||||
None,
|
||||
prepare_upload_request(&[file]),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.response
|
||||
.unwrap();
|
||||
|
||||
upload_bytes(
|
||||
&client,
|
||||
server.port,
|
||||
&response.session_id,
|
||||
"file-a",
|
||||
&response.files["file-a"],
|
||||
&bytes,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(server.received.lock().await["file-a"], bytes);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_upload_with_mismatched_sha256() {
|
||||
let server = start_test_server(None, true, None).await;
|
||||
let client = LsHttpClientV2::try_new_without_cert().unwrap();
|
||||
|
||||
let bytes = b"hello".to_vec();
|
||||
let mut file = file_dto("file-a", "a.bin", bytes.len() as u64);
|
||||
file.sha256 = Some(sha256_hex(b"something else"));
|
||||
|
||||
let response = client
|
||||
.prepare_upload(
|
||||
ProtocolType::Http,
|
||||
"127.0.0.1",
|
||||
server.port,
|
||||
None,
|
||||
prepare_upload_request(&[file]),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.response
|
||||
.unwrap();
|
||||
|
||||
let result = upload_bytes(
|
||||
&client,
|
||||
server.port,
|
||||
&response.session_id,
|
||||
"file-a",
|
||||
&response.files["file-a"],
|
||||
&bytes,
|
||||
)
|
||||
.await;
|
||||
assert_status(result, 422);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_upload_saved_to_path_by_server() {
|
||||
let save_dir = std::env::temp_dir().join(format!("localsend-test-{}", uuid::Uuid::new_v4()));
|
||||
|
||||
Reference in New Issue
Block a user