s3_wire/client/
presign.rs1use std::time::Duration;
2
3use secrecy::ExposeSecret;
4use time::OffsetDateTime;
5
6use super::S3Client;
7use crate::error::{ErrorCategory, RetryClassification, S3Error};
8use crate::operation::{ObjectKey, PresignedUrl};
9use crate::signing::{
10 Header, PresigningRequest, SigningCredentials, SigningPath, SigningScope, sign_presigned,
11};
12
13impl S3Client {
14 pub async fn presigned_get(
16 &self,
17 key: &ObjectKey,
18 expires: Duration,
19 ) -> Result<PresignedUrl, S3Error> {
20 self.presign("GET", key, expires).await
21 }
22
23 pub async fn presigned_put(
25 &self,
26 key: &ObjectKey,
27 expires: Duration,
28 ) -> Result<PresignedUrl, S3Error> {
29 self.presign("PUT", key, expires).await
30 }
31
32 async fn presign(
33 &self,
34 method: &'static str,
35 key: &ObjectKey,
36 expires: Duration,
37 ) -> Result<PresignedUrl, S3Error> {
38 let target = self.inner.config.endpoint().object_url(
39 self.inner.config.bucket(),
40 Some(key.as_str()),
41 self.inner.config.addressing_style(),
42 )?;
43 let credentials = self
44 .inner
45 .config
46 .credentials_provider()
47 .provide_credentials()
48 .await?;
49 if credentials.expires_by(OffsetDateTime::now_utc()) {
50 return Err(S3Error::new(
51 ErrorCategory::Authentication,
52 "credential provider returned expired credentials",
53 RetryClassification::Never,
54 ));
55 }
56 let secret = credentials.secret_access_key().expose_secret();
57 let session_token = credentials.session_token().map(ExposeSecret::expose_secret);
58 let signing_credentials = SigningCredentials::new(
59 credentials.access_key_id(),
60 secret.as_bytes(),
61 session_token,
62 );
63 let headers = [Header::new("host", target.authority())];
64 let signing_request = PresigningRequest {
65 method,
66 uri_path: SigningPath::encoded(target.path_and_query()),
67 query: &[],
68 headers: &headers,
69 expires,
70 payload_hash: None,
71 };
72 let query: crate::signing::PresignedQuery = sign_presigned(
73 &signing_credentials,
74 SigningScope::new(self.inner.config.region(), "s3"),
75 &signing_request,
76 OffsetDateTime::now_utc(),
77 )
78 .map_err(|error| {
79 S3Error::new(
80 ErrorCategory::Authentication,
81 "presigned URL generation failed",
82 RetryClassification::Never,
83 )
84 .with_source(error)
85 })?;
86 Ok(PresignedUrl::new(format!(
87 "{}?{}",
88 target.as_str(),
89 query.as_str()
90 )))
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use std::sync::Arc;
97
98 use super::*;
99 use crate::config::S3Config;
100 use crate::credentials::{Credentials, StaticCredentialsProvider};
101
102 #[tokio::test]
103 async fn presigned_output_requires_explicit_exposure() {
104 let credentials = Credentials::new("access", "secret", None).unwrap();
105 let config = S3Config::builder()
106 .bucket("bucket")
107 .credentials_provider(Arc::new(StaticCredentialsProvider::new(credentials)))
108 .build()
109 .unwrap();
110 let client = S3Client::new(config).unwrap();
111 let url = client
112 .presigned_get(&ObjectKey::new("a/../b").unwrap(), Duration::from_secs(60))
113 .await
114 .unwrap();
115 assert!(!format!("{url:?}").contains("X-Amz-Signature"));
116 assert!(url.expose().contains("/bucket/a/../b?"));
117 assert!(url.expose().contains("X-Amz-Signature="));
118 }
119}