1use std::ops::Deref;
2
3use ring::{rand, signature};
4
5use google_cloud_token::{NopeTokenSourceProvider, 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(NopeTokenSourceProvider {})),
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().with_scopes(&crate::http::storage_client::SCOPES)
118 }
119}
120
121#[derive(Clone)]
122pub struct Client {
123 default_google_access_id: Option<String>,
124 default_sign_by: Option<SignBy>,
125 storage_client: StorageClient,
126 service_account_client: ServiceAccountClient,
127}
128
129impl Deref for Client {
130 type Target = StorageClient;
131
132 fn deref(&self) -> &Self::Target {
133 &self.storage_client
134 }
135}
136
137impl Client {
138 pub fn new(config: ClientConfig) -> Self {
140 let ts = match config.token_source_provider {
141 Some(tsp) => Some(tsp.token_source()),
142 None => {
143 tracing::trace!("Use anonymous access due to lack of token");
144 None
145 }
146 };
147 let http = config
148 .http
149 .unwrap_or_else(|| reqwest_middleware::ClientBuilder::new(reqwest::Client::default()).build());
150
151 let service_account_client =
152 ServiceAccountClient::new(ts.clone(), config.service_account_endpoint.as_str(), http.clone());
153 let storage_client = StorageClient::new(ts, config.storage_endpoint.as_str(), http);
154
155 Self {
156 default_google_access_id: config.default_google_access_id,
157 default_sign_by: config.default_sign_by,
158 storage_client,
159 service_account_client,
160 }
161 }
162
163 #[cfg_attr(feature = "trace", tracing::instrument(skip_all))]
199 pub async fn signed_url(
200 &self,
201 bucket: &str,
202 object: &str,
203 google_access_id: Option<String>,
204 sign_by: Option<SignBy>,
205 opts: SignedURLOptions,
206 ) -> Result<String, SignedURLError> {
207 let google_access_id = match &google_access_id {
210 Some(overwritten_gai) => overwritten_gai.to_owned(),
211 None => {
212 let default_gai = &self
213 .default_google_access_id
214 .clone()
215 .ok_or(SignedURLError::InvalidOption("No default google_access_id is found"))?;
216
217 default_gai.to_owned()
218 }
219 };
220
221 let sign_by = match &sign_by {
223 Some(overwritten_sign_by) => overwritten_sign_by.to_owned(),
224 None => {
225 let default_sign_by = &self
226 .default_sign_by
227 .clone()
228 .ok_or(SignedURLError::InvalidOption("No default sign_by is found"))?;
229
230 default_sign_by.to_owned()
231 }
232 };
233
234 let (signed_buffer, mut builder) = create_signed_buffer(bucket, object, &google_access_id, &opts)?;
235 tracing::trace!("signed_buffer={:?}", String::from_utf8_lossy(&signed_buffer));
236
237 let signature = match &sign_by {
239 PrivateKey(private_key) => {
240 if private_key.is_empty() {
242 return Err(SignedURLError::InvalidOption("No keys present"));
243 }
244 let key_pair = &RsaKeyPair::try_from(private_key)?;
245 let mut signed = vec![0; key_pair.public().modulus_len()];
246 key_pair
247 .sign(
248 &signature::RSA_PKCS1_SHA256,
249 &rand::SystemRandom::new(),
250 signed_buffer.as_slice(),
251 &mut signed,
252 )
253 .map_err(|e| SignedURLError::CertError(e.to_string()))?;
254 signed
255 }
256 SignBy::SignBytes => {
257 let path = format!("projects/-/serviceAccounts/{}", google_access_id);
258 self.service_account_client
259 .sign_blob(&path, signed_buffer.as_slice())
260 .await
261 .map_err(SignedURLError::SignBlob)?
262 }
263 };
264 builder
265 .query_pairs_mut()
266 .append_pair("X-Goog-Signature", &hex::encode(signature));
267 Ok(builder.to_string())
268 }
269}
270
271#[cfg(test)]
272mod test {
273
274 use serial_test::serial;
275
276 use crate::client::{Client, ClientConfig};
277 use crate::http::buckets::get::GetBucketRequest;
278
279 use crate::http::storage_client::test::bucket_name;
280 use crate::sign::{SignedURLMethod, SignedURLOptions};
281
282 async fn create_client() -> (Client, String) {
283 let config = ClientConfig::default().with_auth().await.unwrap();
284 let project_id = config.project_id.clone();
285 (Client::new(config), project_id.unwrap())
286 }
287
288 #[tokio::test]
289 #[serial]
290 async fn test_sign() {
291 let (client, project) = create_client().await;
292 let bucket_name = bucket_name(&project, "object");
293 let data = "aiueo";
294 let content_type = "application/octet-stream";
295
296 let option = SignedURLOptions {
298 method: SignedURLMethod::PUT,
299 content_type: Some(content_type.to_string()),
300 ..SignedURLOptions::default()
301 };
302 let url = client
303 .signed_url(&bucket_name, "signed_uploadtest", None, None, option)
304 .await
305 .unwrap();
306 println!("uploading={url:?}");
307 let request = reqwest::Client::default()
308 .put(url)
309 .header("content-type", content_type)
310 .body(data.as_bytes());
311 let result = request.send().await.unwrap();
312 let status = result.status();
313 assert!(status.is_success(), "{:?}", result.text().await.unwrap());
314
315 let option = SignedURLOptions {
317 content_type: Some(content_type.to_string()),
318 ..SignedURLOptions::default()
319 };
320 let url = client
321 .signed_url(&bucket_name, "signed_uploadtest", None, None, option)
322 .await
323 .unwrap();
324 println!("downloading={url:?}");
325 let result = reqwest::Client::default()
326 .get(url)
327 .header("content-type", content_type)
328 .send()
329 .await
330 .unwrap()
331 .text()
332 .await
333 .unwrap();
334 assert_eq!(result, data);
335 }
336
337 #[tokio::test]
338 #[serial]
339 async fn test_sign_with_overwrites() {
340 let (client, project) = create_client().await;
341 let bucket_name = bucket_name(&project, "object");
342 let data = "aiueo";
343 let content_type = "application/octet-stream";
344 let overwritten_gai = client.default_google_access_id.as_ref().unwrap();
345 let overwritten_sign_by = client.default_sign_by.as_ref().unwrap();
346
347 let option = SignedURLOptions {
349 method: SignedURLMethod::PUT,
350 content_type: Some(content_type.to_string()),
351 ..SignedURLOptions::default()
352 };
353 let url = client
354 .signed_url(
355 &bucket_name,
356 "signed_uploadtest",
357 Some(overwritten_gai.to_owned()),
358 Some(overwritten_sign_by.to_owned()),
359 option,
360 )
361 .await
362 .unwrap();
363 println!("uploading={url:?}");
364 let request = reqwest::Client::default()
365 .put(url)
366 .header("content-type", content_type)
367 .body(data.as_bytes());
368 let result = request.send().await.unwrap();
369 let status = result.status();
370 assert!(status.is_success(), "{:?}", result.text().await.unwrap());
371
372 let option = SignedURLOptions {
374 content_type: Some(content_type.to_string()),
375 ..SignedURLOptions::default()
376 };
377
378 let url = client
379 .signed_url(
380 &bucket_name,
381 "signed_uploadtest",
382 Some(overwritten_gai.to_owned()),
383 Some(overwritten_sign_by.to_owned()),
384 option,
385 )
386 .await
387 .unwrap();
388 println!("downloading={url:?}");
389 let result = reqwest::Client::default()
390 .get(url)
391 .header("content-type", content_type)
392 .send()
393 .await
394 .unwrap()
395 .text()
396 .await
397 .unwrap();
398 assert_eq!(result, data);
399 }
400
401 #[tokio::test]
402 #[serial]
403 async fn test_anonymous() {
404 let project = ClientConfig::default().with_auth().await.unwrap().project_id.unwrap();
405 let bucket = bucket_name(&project, "anonymous");
406
407 let config = ClientConfig::default().anonymous();
408 let client = Client::new(config);
409 let result = client
410 .get_bucket(&GetBucketRequest {
411 bucket: bucket.clone(),
412 ..Default::default()
413 })
414 .await
415 .unwrap();
416 assert_eq!(result.name, bucket);
417 }
418}