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.operation_target(Some(key.as_str()))?;
39 let credentials = self
40 .inner
41 .config
42 .credentials_provider()
43 .provide_credentials()
44 .await?;
45 if credentials.expires_by(OffsetDateTime::now_utc()) {
46 return Err(S3Error::new(
47 ErrorCategory::Authentication,
48 "credential provider returned expired credentials",
49 RetryClassification::Never,
50 ));
51 }
52 let secret = credentials.secret_access_key().expose_secret();
53 let session_token = credentials.session_token().map(ExposeSecret::expose_secret);
54 let signing_credentials = SigningCredentials::new(
55 credentials.access_key_id(),
56 secret.as_bytes(),
57 session_token,
58 );
59 let headers = [Header::new("host", target.authority())];
60 let signing_request = PresigningRequest {
61 method,
62 uri_path: SigningPath::encoded(target.path_and_query()),
63 query: &[],
64 headers: &headers,
65 expires,
66 payload_hash: None,
67 };
68 let query: crate::signing::PresignedQuery = sign_presigned(
69 &signing_credentials,
70 SigningScope::new(self.inner.config.region(), "s3"),
71 &signing_request,
72 OffsetDateTime::now_utc(),
73 )
74 .map_err(|error| {
75 S3Error::new(
76 ErrorCategory::Authentication,
77 "presigned URL generation failed",
78 RetryClassification::Never,
79 )
80 .with_source(error)
81 })?;
82 Ok(PresignedUrl::new(format!(
83 "{}?{}",
84 target.as_str(),
85 query.as_str()
86 )))
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use std::sync::Arc;
93
94 use super::*;
95 use crate::config::S3Config;
96 use crate::credentials::{Credentials, StaticCredentialsProvider};
97
98 #[tokio::test]
99 async fn presigned_output_requires_explicit_exposure() {
100 let credentials = Credentials::new("access", "secret", None).unwrap();
101 let config = S3Config::builder()
102 .bucket("bucket")
103 .credentials_provider(Arc::new(StaticCredentialsProvider::new(credentials)))
104 .build()
105 .unwrap();
106 let client = S3Client::new(config).unwrap();
107 let url = client
108 .presigned_get(&ObjectKey::new("a/../b").unwrap(), Duration::from_secs(60))
109 .await
110 .unwrap();
111 assert!(!format!("{url:?}").contains("X-Amz-Signature"));
112 assert!(url.expose().contains("/bucket/a/../b?"));
113 assert!(url.expose().contains("X-Amz-Signature="));
114 }
115}