diff --git a/api/src/handler/file.rs b/api/src/handler/file.rs index ae9b41b..514723d 100644 --- a/api/src/handler/file.rs +++ b/api/src/handler/file.rs @@ -28,8 +28,8 @@ pub async fn upload( multipart: Multipart, ) -> ApiResult> { query.validate()?; - let auth = crate::util::http::parse_basic_auth(&headers)?; - let (path, expire_time) = service::file::store(&state, &query, auth, multipart).await?; + let secret = crate::util::http::parse_basic_auth(&headers)?; + let (path, expire_time) = service::file::store(&state, &query, secret, multipart).await?; let url = path.url(&state.config.domain); let qrcode = crate::util::qrcode::encode(&url)?; Ok(Json(UploadResponse { @@ -44,8 +44,8 @@ pub async fn download( Path((code, file_name)): Path<(String, String)>, req: Request, ) -> ApiResult> { - let auth = crate::util::http::parse_basic_auth(req.headers())?; - let file = service::file::fetch(&state, &code, &file_name, auth).await?; + let secret = crate::util::http::parse_basic_auth(req.headers())?; + let file = service::file::fetch(&state, &code, &file_name, secret).await?; Ok(file.oneshot(req).await.unwrap()) } @@ -54,8 +54,8 @@ pub async fn info( Path((code, file_name)): Path<(String, String)>, headers: HeaderMap, ) -> ApiResult> { - let auth = crate::util::http::parse_basic_auth(&headers)?; - let meta = service::file::info(&state, &code, &file_name, auth).await?; + let secret = crate::util::http::parse_basic_auth(&headers)?; + let meta = service::file::info(&state, &code, &file_name, secret).await?; Ok(Json(MetaDataFileResponse::from(&meta))) } @@ -64,8 +64,8 @@ pub async fn delete( Path((code, file_name)): Path<(String, String)>, headers: HeaderMap, ) -> ApiResult> { - let auth = crate::util::http::parse_basic_auth(&headers)?; - service::file::delete(&state, &code, &file_name, auth).await?; + let secret = crate::util::http::parse_basic_auth(&headers)?; + service::file::delete(&state, &code, &file_name, secret).await?; Ok(Json(MessageResponse { message: "OK".to_string(), })) diff --git a/api/src/service/file.rs b/api/src/service/file.rs index 0d0d681..1bf17b9 100644 --- a/api/src/service/file.rs +++ b/api/src/service/file.rs @@ -1,4 +1,6 @@ use crate::error::{ApiError, ApiResult, ToApiResult}; +use crate::util::secret::Secret; +use anyhow::anyhow; use axum::extract::multipart::Field; use axum::extract::Multipart; use chrono::{DateTime, Utc}; @@ -17,10 +19,10 @@ use crate::server::ApiState; pub async fn store( state: &ApiState, query: &UploadParamQuery, - auth: Option, + secret: Option, mut multipart: Multipart, ) -> ApiResult<(FilePath, DateTime)> { - let auth = hash(auth)?; + let auth = secret.map(|s| s.hash()).transpose()?; let expire_secs = query .expire_time .unwrap_or(state.config.default_expire_secs) as i64; @@ -89,7 +91,7 @@ pub async fn info( state: &ApiState, code: &str, file_name: &str, - auth: Option, + auth: Option, ) -> ApiResult { let path = FilePath { code: code.to_string(), @@ -110,7 +112,7 @@ pub async fn fetch( state: &ApiState, code: &str, file_name: &str, - auth: Option, + secret: Option, ) -> ApiResult { let path = FilePath { code: code.to_string(), @@ -123,7 +125,7 @@ pub async fn fetch( return Err(ApiError::NotFound(format!("{} not found", path.url_path()))); } } - authenticate(auth, &meta.auth)?; + authenticate(secret, &meta.auth)?; read_file(&state.config.fs.base_dir.join(&path.url_path())).await } @@ -131,7 +133,7 @@ pub async fn delete( state: &ApiState, code: &str, file_name: &str, - auth: Option, + secret: Option, ) -> ApiResult<()> { let path = FilePath { code: code.to_string(), @@ -139,7 +141,7 @@ pub async fn delete( }; if let Some(meta) = state.db.fetch(&path)? { if meta.delete_manually { - authenticate(auth, &meta.auth)?; + authenticate(secret, &meta.auth)?; let file_path = path.fs_path(&state.config.fs.base_dir); tokio::fs::remove_file(file_path).await?; state.db.delete(path).await?; @@ -158,18 +160,24 @@ pub async fn read_file(file_path: &PathBuf) -> ApiResult { Ok(ServeFile::new(file_path)) } -pub fn authenticate(auth: Option, hash: &Option) -> ApiResult<()> { +pub fn authenticate(secret: Option, hash: &Option) -> ApiResult<()> { if let Some(hash) = hash { - if !matches!( - auth.map(|p| crate::util::hash::argon_verify(p, hash)), - Some(Ok(())) - ) { - return Err(ApiError::PermissionDenied( - "user and password is invalid".to_string(), - )); + match secret.map(|s| s.check(hash)) { + Some(Ok(_)) => return Ok(()), + Some(Err(e)) if e == argon2::password_hash::Error::Password => Err( + ApiError::PermissionDenied("Secret token is invalid".to_string()), + ), + Some(Err(e)) => Err(ApiError::Unknown(anyhow!( + "Unexpected error happened: {}", + e + ))), + None => Err(ApiError::PermissionDenied( + "Authorization header should be set".to_string(), + )), } + } else { + Ok(()) } - Ok(()) } pub fn hash(auth: Option) -> ApiResult> { diff --git a/api/src/util/http.rs b/api/src/util/http.rs index 335232a..cd44a29 100644 --- a/api/src/util/http.rs +++ b/api/src/util/http.rs @@ -1,17 +1,19 @@ use hyper::HeaderMap; -use crate::error::{invalid_input_error, ApiResult}; +use crate::error::{invalid_input_error, ApiError, ApiResult}; -pub fn parse_basic_auth(headers: &HeaderMap) -> ApiResult> { +use super::secret::Secret; + +pub fn parse_basic_auth(headers: &HeaderMap) -> ApiResult> { if let Some(value) = headers.get("Authorization") { let bytes = &value.as_bytes()["Basic ".len()..]; if let Some(non_space_pos) = bytes.iter().position(|b| *b != b' ') { let bytes = &bytes[non_space_pos..]; let value = String::from_utf8(bytes.to_vec()) .map_err(|_e| invalid_input_error("Authorization", "invalid auth header"))?; - Ok(Some(value)) + Ok(Some(Secret::new(value))) } else { - Err(invalid_input_error("Authorization", "invalid auth header")) + Err(invalid_input_error("Authorization", "Invalid auth header")) } } else { Ok(None) diff --git a/api/src/util/mod.rs b/api/src/util/mod.rs index ca5b752..6ac4351 100644 --- a/api/src/util/mod.rs +++ b/api/src/util/mod.rs @@ -3,6 +3,7 @@ pub mod file_name; pub mod hash; pub mod http; pub mod qrcode; +pub mod secret; pub mod string; pub mod test; pub mod tracing; diff --git a/api/src/util/secret.rs b/api/src/util/secret.rs new file mode 100644 index 0000000..2edbdab --- /dev/null +++ b/api/src/util/secret.rs @@ -0,0 +1,20 @@ +use crate::error::{ApiError, ApiResult}; + +#[derive(Debug)] +pub struct Secret { + inner: String, +} + +impl Secret { + pub fn new(secret: String) -> Self { + Self { inner: secret } + } + + pub fn check(&self, hash: &str) -> Result<(), argon2::password_hash::Error> { + crate::util::hash::argon_verify(&self.inner, hash) + } + + pub fn hash(&self) -> ApiResult { + crate::util::hash::argon_hash(&self.inner).map_err(|e| ApiError::HashError(e.to_string())) + } +}