1use std::ops::Deref;
2
3use ring::{rand, signature};
4
5use token_source::{NoopTokenSourceProvider, TokenSourceProvider};
6
7use crate::http::service_account_client::ServiceAccountClient;
8use crate::http::storage_client::StorageClient;
9use crate::sign::SignBy::PrivateKey;
10use crate::sign::{create_signed_buffer, RsaKeyPair, SignBy, SignedURLError, SignedURLOptions};
11
12#[derive(Debug)]
42pub struct ClientConfig {
43 pub http: Option<reqwest_middleware::ClientWithMiddleware>,
44 pub storage_endpoint: String,
45 pub service_account_endpoint: String,
46 pub token_source_provider: Option<Box<dyn TokenSourceProvider>>,
47 pub default_google_access_id: Option<String>,
48 pub default_sign_by: Option<SignBy>,
49 pub project_id: Option<String>,
50}
51
52impl Default for ClientConfig {
53 fn default() -> Self {
54 Self {
55 http: None,
56 storage_endpoint: "https://storage.googleapis.com".to_string(),
57 token_source_provider: Some(Box::new(NoopTokenSourceProvider {})),
58 service_account_endpoint: "https://iamcredentials.googleapis.com".to_string(),
59 default_google_access_id: None,
60 default_sign_by: None,
61 project_id: None,
62 }
63 }
64}
65
66impl ClientConfig {
67 pub fn anonymous(mut self) -> Self {
68 self.token_source_provider = None;
69 self
70 }
71}
72
73#[cfg(feature = "auth")]
74pub use google_cloud_auth;
75
76#[cfg(feature = "auth")]
77impl ClientConfig {
78 pub async fn with_auth(self) -> Result<Self, google_cloud_auth::error::Error> {
79 let ts = google_cloud_auth::token::DefaultTokenSourceProvider::new(Self::auth_config()).await?;
80 Ok(self.with_token_source(ts).await)
81 }
82
83 pub async fn with_credentials(
84 self,
85 credentials: google_cloud_auth::credentials::CredentialsFile,
86 ) -> Result<Self, google_cloud_auth::error::Error> {
87 let ts = google_cloud_auth::token::DefaultTokenSourceProvider::new_with_credentials(
88 Self::auth_config(),
89 Box::new(credentials),
90 )
91 .await?;
92 Ok(self.with_token_source(ts).await)
93 }
94
95 async fn with_token_source(mut self, ts: google_cloud_auth::token::DefaultTokenSourceProvider) -> Self {
96 match &ts.source_credentials {
97 Some(cred) => {
99 self.project_id.clone_from(&cred.project_id);
100 if let Some(pk) = &cred.private_key {
101 self.default_sign_by = Some(PrivateKey(pk.clone().into_bytes()));
102 }
103 self.default_google_access_id.clone_from(&cred.client_email);
104 }
105 None => {
107 self.project_id = Some(google_cloud_metadata::project_id().await);
108 self.default_sign_by = Some(SignBy::SignBytes);
109 self.default_google_access_id = google_cloud_metadata::email("default").await.ok();
110 }
111 }
112 self.token_source_provider = Some(Box::new(ts));
113 self
114 }
115
116 fn auth_config() -> google_cloud_auth::project::Config<'static> {
117 google_cloud_auth::project::Config::default()
118 .with_audience(&crate::http::storage_client::AUDIENCE)
119 .with_scopes(&crate::http::storage_client::SCOPES)
120 }
121}
122
123#[derive(Clone)]
124pub struct Client {
125 default_google_access_id: Option<String>,
126 default_sign_by: Option<SignBy>,
127 storage_client: StorageClient,
128 service_account_client: ServiceAccountClient,
129}
130
131impl Deref for Client {
132 type Target = StorageClient;
133
134 fn deref(&self) -> &Self::Target {
135 &self.storage_client
136 }
137}
138
139impl Client {
140 pub fn new(config: ClientConfig) -> Self {
142 let ts = match config.token_source_provider {
143 Some(tsp) => Some(tsp.token_source()),
144 None => {
145 tracing::trace!("Use anonymous access due to lack of token");
146 None
147 }
148 };
149 let http = config
150 .http
151 .unwrap_or_else(|| reqwest_middleware::ClientBuilder::new(reqwest::Client::default()).build());
152
153 let service_account_client =
154 ServiceAccountClient::new(ts.clone(), config.service_account_endpoint.as_str(), http.clone());
155 let storage_client = StorageClient::new(ts, config.storage_endpoint.as_str(), http);
156
157 Self {
158 default_google_access_id: config.default_google_access_id,
159 default_sign_by: config.default_sign_by,
160 storage_client,
161 service_account_client,
162 }
163 }
164
165 #[cfg_attr(feature = "trace", tracing::instrument(skip_all))]
201 pub async fn signed_url(
202 &self,
203 bucket: &str,
204 object: &str,
205 google_access_id: Option<String>,
206 sign_by: Option<SignBy>,
207 opts: SignedURLOptions,
208 ) -> Result<String, SignedURLError> {
209 let google_access_id = match &google_access_id {
212 Some(overwritten_gai) => overwritten_gai.to_owned(),
213 None => {
214 let default_gai = &self
215 .default_google_access_id
216 .clone()
217 .ok_or(SignedURLError::InvalidOption("No default google_access_id is found"))?;
218
219 default_gai.to_owned()
220 }
221 };
222
223 let sign_by = match &sign_by {
225 Some(overwritten_sign_by) => overwritten_sign_by.to_owned(),
226 None => {
227 let default_sign_by = &self
228 .default_sign_by
229 .clone()
230 .ok_or(SignedURLError::InvalidOption("No default sign_by is found"))?;
231
232 default_sign_by.to_owned()
233 }
234 };
235
236 let (signed_buffer, mut builder) = create_signed_buffer(bucket, object, &google_access_id, &opts)?;
237 tracing::trace!("signed_buffer={:?}", String::from_utf8_lossy(&signed_buffer));
238
239 let signature = match &sign_by {
241 PrivateKey(private_key) => {
242 if private_key.is_empty() {
244 return Err(SignedURLError::InvalidOption("No keys present"));
245 }
246 let key_pair = &RsaKeyPair::try_from(private_key)?;
247 let mut signed = vec![0; key_pair.public().modulus_len()];
248 key_pair
249 .sign(
250 &signature::RSA_PKCS1_SHA256,
251 &rand::SystemRandom::new(),
252 signed_buffer.as_slice(),
253 &mut signed,
254 )
255 .map_err(|e| SignedURLError::CertError(e.to_string()))?;
256 signed
257 }
258 SignBy::SignBytes => {
259 let path = format!("projects/-/serviceAccounts/{google_access_id}");
260 self.service_account_client
261 .sign_blob(&path, signed_buffer.as_slice())
262 .await
263 .map_err(SignedURLError::SignBlob)?
264 }
265 };
266 builder
267 .query_pairs_mut()
268 .append_pair("X-Goog-Signature", &hex::encode(signature));
269 Ok(builder.to_string())
270 }
271}
272
273#[cfg(test)]
274mod test {
275
276 use serial_test::serial;
277
278 use crate::client::{Client, ClientConfig};
279 use crate::http::buckets::get::GetBucketRequest;
280
281 use crate::http::storage_client::test::bucket_name;
282 use crate::sign::{SignedURLMethod, SignedURLOptions};
283
284 async fn create_client() -> (Client, String) {
285 let config = ClientConfig::default().with_auth().await.unwrap();
286 let project_id = config.project_id.clone();
287 (Client::new(config), project_id.unwrap())
288 }
289
290 #[tokio::test]
291 #[serial]
292 async fn test_sign() {
293 let (client, project) = create_client().await;
294 let bucket_name = bucket_name(&project, "object");
295 let data = "aiueo";
296 let content_type = "application/octet-stream";
297
298 let option = SignedURLOptions {
300 method: SignedURLMethod::PUT,
301 content_type: Some(content_type.to_string()),
302 ..SignedURLOptions::default()
303 };
304 let url = client
305 .signed_url(&bucket_name, "signed_uploadtest", None, None, option)
306 .await
307 .unwrap();
308 println!("uploading={url:?}");
309 let request = reqwest::Client::default()
310 .put(url)
311 .header("content-type", content_type)
312 .body(data.as_bytes());
313 let result = request.send().await.unwrap();
314 let status = result.status();
315 assert!(status.is_success(), "{:?}", result.text().await.unwrap());
316
317 let option = SignedURLOptions {
319 content_type: Some(content_type.to_string()),
320 ..SignedURLOptions::default()
321 };
322 let url = client
323 .signed_url(&bucket_name, "signed_uploadtest", None, None, option)
324 .await
325 .unwrap();
326 println!("downloading={url:?}");
327 let result = reqwest::Client::default()
328 .get(url)
329 .header("content-type", content_type)
330 .send()
331 .await
332 .unwrap()
333 .text()
334 .await
335 .unwrap();
336 assert_eq!(result, data);
337 }
338
339 #[tokio::test]
340 #[serial]
341 async fn test_sign_with_overwrites() {
342 let (client, project) = create_client().await;
343 let bucket_name = bucket_name(&project, "object");
344 let data = "aiueo";
345 let content_type = "application/octet-stream";
346 let overwritten_gai = client.default_google_access_id.as_ref().unwrap();
347 let overwritten_sign_by = client.default_sign_by.as_ref().unwrap();
348
349 let option = SignedURLOptions {
351 method: SignedURLMethod::PUT,
352 content_type: Some(content_type.to_string()),
353 ..SignedURLOptions::default()
354 };
355 let url = client
356 .signed_url(
357 &bucket_name,
358 "signed_uploadtest",
359 Some(overwritten_gai.to_owned()),
360 Some(overwritten_sign_by.to_owned()),
361 option,
362 )
363 .await
364 .unwrap();
365 println!("uploading={url:?}");
366 let request = reqwest::Client::default()
367 .put(url)
368 .header("content-type", content_type)
369 .body(data.as_bytes());
370 let result = request.send().await.unwrap();
371 let status = result.status();
372 assert!(status.is_success(), "{:?}", result.text().await.unwrap());
373
374 let option = SignedURLOptions {
376 content_type: Some(content_type.to_string()),
377 ..SignedURLOptions::default()
378 };
379
380 let url = client
381 .signed_url(
382 &bucket_name,
383 "signed_uploadtest",
384 Some(overwritten_gai.to_owned()),
385 Some(overwritten_sign_by.to_owned()),
386 option,
387 )
388 .await
389 .unwrap();
390 println!("downloading={url:?}");
391 let result = reqwest::Client::default()
392 .get(url)
393 .header("content-type", content_type)
394 .send()
395 .await
396 .unwrap()
397 .text()
398 .await
399 .unwrap();
400 assert_eq!(result, data);
401 }
402
403 #[tokio::test]
404 #[serial]
405 async fn test_anonymous() {
406 let project = ClientConfig::default().with_auth().await.unwrap().project_id.unwrap();
407 let bucket = bucket_name(&project, "anonymous");
408
409 let config = ClientConfig::default().anonymous();
410 let client = Client::new(config);
411 let result = client
412 .get_bucket(&GetBucketRequest {
413 bucket: bucket.clone(),
414 ..Default::default()
415 })
416 .await
417 .unwrap();
418 assert_eq!(result.name, bucket);
419 }
420}