diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 6b9994cf..61ebc1cb 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -1,11 +1,14 @@ -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + path::Path, +}; use chrono::{NaiveDateTime, Utc}; use num_traits::ToPrimitive; use rocket::{ Route, form::{Form, FromForm}, - fs::TempFile, + fs::{NamedFile, TempFile}, serde::json::Json, }; use serde_json::Value; @@ -13,7 +16,7 @@ use serde_json::Value; use crate::{ CONFIG, api::{self, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, core::log_event}, - auth::ClientVersion, + auth::{ClientVersion, decode_file_download}, auth::{Headers, OrgIdGuard, OwnerHeaders}, config::PathType, crypto, @@ -53,6 +56,7 @@ pub fn routes() -> Vec { post_ciphers_create, post_ciphers_import, get_attachment, + download_attachment, post_attachment_v2, post_attachment_v2_data, post_attachment, // legacy @@ -1104,6 +1108,18 @@ async fn get_attachment( } } +/// Serves a locally stored attachment file using a time-limited, signed token. +#[get("/ciphers/attachment/download?")] +async fn download_attachment(token: String) -> Option { + let Ok(claims) = decode_file_download(&token) else { + return None; + }; + + NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(claims.sub.as_ref()).join(claims.file_id.as_ref())) + .await + .ok() +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct AttachmentRequestData { diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 244f8c27..91bae4ca 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -59,7 +59,7 @@ impl Attachment { if crate::storage::is_fs_operator(&operator) { let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone())); - Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id)) + Ok(attachment_download_url(host, &token)) } else { Ok(operator.presign_read(&self.get_file_path(), Duration::from_mins(5)).await?.uri().to_string()) } @@ -78,6 +78,23 @@ impl Attachment { } } +fn attachment_download_url(host: &str, token: &str) -> String { + format!("{host}/api/ciphers/attachment/download?token={token}") +} + +#[cfg(test)] +mod tests { + use super::attachment_download_url; + + #[test] + fn attachment_download_url_uses_api_endpoint() { + assert_eq!( + attachment_download_url("https://vault.example", "download.token"), + "https://vault.example/api/ciphers/attachment/download?token=download.token" + ); + } +} + /// Database methods impl Attachment { pub async fn save(&self, conn: &DbConn) -> EmptyResult {