diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dadb99a2..27ec5fce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,20 +53,13 @@ jobs: args: --all-features unit: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: - - ubuntu-latest - - macos-11 - - windows-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install cargo-nextest + run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin - name: Test - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features -- --show-output + run: cargo nextest run --no-fail-fast env: RUST_LOG: DEBUG RUST_BACKTRACE: full diff --git a/Cargo.toml b/Cargo.toml index 04193921..ddddafc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,3 +63,4 @@ reqwest = { version = "0.11", features = ["blocking", "json"] } temp-env = "0.3" tokio = { version = "1", features = ["full"] } ureq = { version = "2.5" } +pretty_assertions = "1.3" diff --git a/src/google/mod.rs b/src/google/mod.rs index d1b32135..056df931 100644 --- a/src/google/mod.rs +++ b/src/google/mod.rs @@ -3,7 +3,6 @@ mod constants; mod credential; mod signer; -mod v4; pub use credential::Token; pub use credential::TokenLoad; pub use signer::Builder; diff --git a/src/google/signer.rs b/src/google/signer.rs index 3a8ff43e..22c0a5db 100644 --- a/src/google/signer.rs +++ b/src/google/signer.rs @@ -1,13 +1,28 @@ +use std::borrow::Cow; + use anyhow::anyhow; use anyhow::Result; use http::header; use log::debug; - +use percent_encoding::percent_decode_str; +use percent_encoding::utf8_percent_encode; +use rsa::pkcs1v15::SigningKey; +use rsa::pkcs8::DecodePrivateKey; +use rsa::signature::RandomizedSigner; + +use super::constants::GOOG_QUERY_ENCODE_SET; +use super::credential::Credential; use super::credential::CredentialLoader; use super::credential::Token; use super::credential::TokenLoad; -use super::v4; +use crate::ctx::SigningContext; +use crate::ctx::SigningMethod; +use crate::hash::hex_sha256; use crate::request::SignableRequest; +use crate::time; +use crate::time::format_date; +use crate::time::format_iso8601; +use crate::time::DateTime; use crate::time::Duration; /// Builder for Signer. @@ -19,6 +34,10 @@ pub struct Builder { credential_path: Option, credential_content: Option, + service: Option, + region: Option, + time: Option, + allow_anonymous: bool, disable_load_from_env: bool, disable_load_from_well_known_location: bool, @@ -87,6 +106,34 @@ impl Builder { self } + /// Set the service name that used for google v4 signing. + /// + /// Default to `storage` + pub fn service(&mut self, service: &str) -> &mut Self { + self.service = Some(service.to_string()); + self + } + + /// Set the region name that used for google v4 signing. + /// + /// Default to `auto` + pub fn region(&mut self, region: &str) -> &mut Self { + self.region = Some(region.to_string()); + self + } + + /// Specify the signing time. + /// + /// # Note + /// + /// We should always take current time to sign requests. + /// Only use this function for testing. + #[cfg(test)] + pub fn time(&mut self, time: DateTime) -> &mut Self { + self.time = Some(time); + self + } + /// Use exising information to build a new signer. /// /// @@ -128,6 +175,12 @@ impl Builder { Ok(Signer { credential_loader: cred_loader, allow_anonymous: self.allow_anonymous, + service: self + .service + .clone() + .unwrap_or_else(|| "storage".to_string()), + region: self.region.clone().unwrap_or_else(|| "auto".to_string()), + time: self.time, }) } } @@ -142,6 +195,10 @@ pub struct Signer { /// Allow anonymous request if credential is not loaded. allow_anonymous: bool, + + service: String, + region: String, + time: Option, } impl Signer { @@ -160,6 +217,93 @@ impl Signer { self.credential_loader.load() } + fn credential(&self) -> Option { + self.credential_loader.load_credential() + } + + fn build_header( + &self, + req: &mut impl SignableRequest, + token: &Token, + ) -> Result { + let mut ctx = req.build()?; + + ctx.headers.insert(header::AUTHORIZATION, { + let mut value: http::HeaderValue = + format!("Bearer {}", token.access_token()).parse()?; + value.set_sensitive(true); + + value + }); + + Ok(ctx) + } + + fn build_query( + &self, + req: &mut impl SignableRequest, + expire: Duration, + cred: &Credential, + ) -> Result { + let mut ctx = req.build()?; + + let now = self.time.unwrap_or_else(time::now); + + // canonicalize context + canonicalize_header(&mut ctx)?; + canonicalize_query( + &mut ctx, + SigningMethod::Query(expire), + cred, + now, + &self.service, + &self.region, + )?; + + // build canonical request and string to sign. + let creq = canonical_request_string(&mut ctx)?; + let encoded_req = hex_sha256(creq.as_bytes()); + + // Scope: "20220313///goog4_request" + let scope = format!( + "{}/{}/{}/goog4_request", + format_date(now), + self.region, + self.service + ); + debug!("calculated scope: {scope}"); + + // StringToSign: + // + // GOOG4-RSA-SHA256 + // 20220313T072004Z + // 20220313///goog4_request + // + let string_to_sign = { + let mut f = String::new(); + f.push_str("GOOG4-RSA-SHA256"); + f.push('\n'); + f.push_str(&format_iso8601(now)); + f.push('\n'); + f.push_str(&scope); + f.push('\n'); + f.push_str(&encoded_req); + + f + }; + debug!("calculated string to sign: {string_to_sign}"); + + let mut rng = rand::thread_rng(); + let private_key = rsa::RsaPrivateKey::from_pkcs8_pem(cred.private_key())?; + let signing_key = SigningKey::::new_with_prefix(private_key); + let signature = signing_key.sign_with_rng(&mut rng, string_to_sign.as_bytes()); + + ctx.query + .push(("X-Goog-Signature".to_string(), signature.to_string())); + + Ok(ctx) + } + /// Signing request. /// /// # Example @@ -197,19 +341,12 @@ impl Signer { /// we can also send API via signed JWT: [Addendum: Service account authorization without OAuth](https://developers.google.com/identity/protocols/oauth2/service-account#jwt-auth) pub fn sign(&self, req: &mut impl SignableRequest) -> Result<()> { if let Some(token) = self.token() { - req.insert_header(header::AUTHORIZATION, { - let mut value: http::HeaderValue = - format!("Bearer {}", token.access_token()).parse()?; - value.set_sensitive(true); - - value - })?; - - return Ok(()); + let ctx = self.build_header(req, &token)?; + return req.apply(ctx); } if self.allow_anonymous { - debug!("token not found and anonymous is allowed, skipping signing."); + debug!("credential not found and anonymous is allowed, skipping signing."); return Ok(()); } @@ -248,22 +385,136 @@ impl Signer { /// Ok(()) /// } /// ``` - pub fn sign_query(&self, query: &mut impl SignableRequest, duration: Duration) -> Result<()> { - let credentials = self.credential_loader.load_credential().unwrap(); - let v4_signer = v4::Signer::builder() - // TODO set this from outside? - .service("storage") - .region("auto") - .credential(credentials) - .build()?; - v4_signer.sign_query(query, duration) + pub fn sign_query(&self, req: &mut impl SignableRequest, duration: Duration) -> Result<()> { + if let Some(cred) = self.credential() { + let ctx = self.build_query(req, duration, &cred)?; + return req.apply(ctx); + } + + if self.allow_anonymous { + debug!("credential not found and anonymous is allowed, skipping signing."); + return Ok(()); + } + + Err(anyhow!("token not found")) + } +} + +fn canonical_request_string(ctx: &mut SigningContext) -> Result { + // 256 is specially chosen to avoid reallocation for most requests. + let mut f = String::with_capacity(256); + + // Insert method + f.push_str(ctx.method.as_str()); + f.push('\n'); + + // Insert encoded path + let path = percent_decode_str(&ctx.path).decode_utf8()?; + f.push_str(&Cow::from(utf8_percent_encode( + &path, + &super::constants::GOOG_URI_ENCODE_SET, + ))); + f.push('\n'); + + // Insert query + f.push_str(&SigningContext::query_to_string( + ctx.query.clone(), + "=", + "&", + )); + f.push('\n'); + + // Insert signed headers + let signed_headers = ctx.header_name_to_vec_sorted(); + for header in signed_headers.iter() { + let value = &ctx.headers[*header]; + f.push_str(header); + f.push(':'); + f.push_str(value.to_str().expect("header value must be valid")); + f.push('\n'); + } + f.push('\n'); + f.push_str(&signed_headers.join(";")); + f.push('\n'); + f.push_str("UNSIGNED-PAYLOAD"); + + debug!("string to sign: {}", f); + Ok(f) +} + +fn canonicalize_header(ctx: &mut SigningContext) -> Result<()> { + for (_, value) in ctx.headers.iter_mut() { + SigningContext::header_value_normalize(value) + } + + // Insert HOST header if not present. + if ctx.headers.get(header::HOST).is_none() { + ctx.headers + .insert(header::HOST, ctx.authority.as_str().parse()?); } + + Ok(()) +} + +fn canonicalize_query( + ctx: &mut SigningContext, + method: SigningMethod, + cred: &Credential, + now: DateTime, + service: &str, + region: &str, +) -> Result<()> { + if let SigningMethod::Query(expire) = method { + ctx.query + .push(("X-Goog-Algorithm".into(), "GOOG4-RSA-SHA256".into())); + ctx.query.push(( + "X-Goog-Credential".into(), + format!( + "{}/{}/{}/{}/goog4_request", + cred.client_email(), + format_date(now), + region, + service + ), + )); + ctx.query.push(("X-Goog-Date".into(), format_iso8601(now))); + ctx.query + .push(("X-Goog-Expires".into(), expire.whole_seconds().to_string())); + ctx.query.push(( + "X-Goog-SignedHeaders".into(), + ctx.header_name_to_vec_sorted().join(";"), + )); + } + + // Return if query is empty. + if ctx.query.is_empty() { + return Ok(()); + } + + // Sort by param name + ctx.query.sort(); + + ctx.query = ctx + .query + .iter() + .map(|(k, v)| { + ( + utf8_percent_encode(k, &GOOG_QUERY_ENCODE_SET).to_string(), + utf8_percent_encode(v, &GOOG_QUERY_ENCODE_SET).to_string(), + ) + }) + .collect(); + + Ok(()) } #[cfg(test)] mod tests { use reqwest::blocking::Client; + use crate::time::parse_rfc2822; + use pretty_assertions::assert_eq; + use super::*; #[derive(Debug)] @@ -318,4 +569,37 @@ mod tests { Ok(()) } + + #[test] + fn test_sign_query_deterministic() -> Result<()> { + let credential_path = format!( + "{}/testdata/services/google/testbucket_credential.json", + std::env::current_dir() + .expect("current_dir must exist") + .to_string_lossy() + ); + + let mut req = http::Request::new(""); + *req.method_mut() = http::Method::GET; + *req.uri_mut() = "https://storage.googleapis.com/testbucket-reqsign/CONTRIBUTING.md" + .parse() + .expect("url must be valid"); + + let time_offset = + parse_rfc2822("Mon, 15 Aug 2022 16:50:12 GMT")?.to_offset(::time::UtcOffset::UTC); + + let signer = Signer::builder() + .credential_path(&credential_path) + .scope("storage") + .time(time_offset) + .build()?; + + signer.sign_query(&mut req, time::Duration::hours(1))?; + + let query = req.query().unwrap(); + assert!(query.contains("X-Goog-Algorithm=GOOG4-RSA-SHA256")); + assert!(query.contains("X-Goog-Credential")); + assert_eq!(query, "X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=testbucket-reqsign-account%40iam-testbucket-reqsign-project.iam.gserviceaccount.com%2F20220815%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220815T165012Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&X-Goog-Signature=9F423139DB223D818F2D4D6BCA4916DD1EE5AEB8E72D99EC60E8B903DC3CF0586C27A0F821C8CB20C6BB76C776E63134DAFF5957E7862BB89926F18E0D3618E4EE40EF8DBEC64D87F5AD4CAF6FE4C2BC3239E1076A33BE3113D6E0D1AF263C16FA5E1C9590C8F8E4E2CA2FED11533607B5AFE84B53E2E00CB320E0BC853C138EBBDCFEC3E9219C73551478EE12AABBD2576686F887738A21DC5AE00DFF3D481BD08F642342C8CCB476E74C8FEA0C02BA6FEFD61300218D6E216EAD4B59F3351E456601DF38D1CC1B4CE639D2748739933672A08B5FEBBED01B5BC0785E81A865EE0252A0C5AE239061F3F5DB4AFD8CC676646750C762A277FBFDE70A85DFDF33"); + Ok(()) + } } diff --git a/src/google/v4.rs b/src/google/v4.rs deleted file mode 100644 index 5e75e88f..00000000 --- a/src/google/v4.rs +++ /dev/null @@ -1,436 +0,0 @@ -//! AWS service sigv4 signer - -use std::borrow::Cow; -use std::fmt::Debug; -use std::fmt::Display; -use std::fmt::Formatter; -use std::fmt::Write; - -use anyhow::anyhow; -use anyhow::Result; -use http::HeaderMap; -use http::HeaderValue; -use log::debug; -use percent_encoding::percent_decode_str; -use percent_encoding::utf8_percent_encode; - -use super::constants::GOOG_QUERY_ENCODE_SET; - -use crate::google::credential::Credential; -use crate::hash::hex_sha256; -use crate::request::SignableRequest; -use crate::time::format_date; -use crate::time::format_iso8601; -use crate::time::DateTime; -use crate::time::Duration; -use crate::time::{self}; - -use rsa::pkcs1v15::SigningKey; -use rsa::signature::RandomizedSigner; - -/// Builder for `Signer`. -#[derive(Default)] -pub struct Builder { - service: Option, - region: Option, - // config_loader: ConfigLoader, - // credential_loader: Option, - credential: Option, - allow_anonymous: bool, - - time: Option, -} - -impl Builder { - /// Specify service like "s3". - pub fn service(&mut self, service: &str) -> &mut Self { - self.service = Some(service.to_string()); - self - } - - /// Specify region like "us-east-1". - /// If not set, use "auto" instead. - pub fn region(&mut self, region: &str) -> &mut Self { - self.region = Some(region.to_string()); - self - } - - /// Allow anonymous request if credential is not loaded. - #[allow(dead_code)] - pub fn allow_anonymous(&mut self) -> &mut Self { - self.allow_anonymous = true; - self - } - - /// Specify the credential - pub fn credential(&mut self, cred: Credential) -> &mut Self { - self.credential = Some(cred); - self - } - - /// Specify the signing time. - /// - /// # Note - /// - /// We should always take current time to sign requests. - /// Only use this function for testing. - #[cfg(test)] - pub fn time(&mut self, time: DateTime) -> &mut Self { - self.time = Some(time); - self - } - - /// Use exising information to build a new signer. - /// - /// The builder should not be used anymore. - pub fn build(&mut self) -> Result { - let service = self - .service - .as_ref() - .ok_or_else(|| anyhow!("service is required"))?; - debug!("signer: service: {:?}", service); - - let region = self.region.take().unwrap_or_else(|| "auto".to_string()); - let credential = self.credential.take(); - - Ok(Signer { - service: service.to_string(), - region, - credential, - allow_anonymous: self.allow_anonymous, - time: self.time, - }) - } -} - -/// Singer that implement Google SigV4. -/// -/// - [Signature Version 4 signing process](https://cloud.google.com/storage/docs/access-control/signing-urls-manually) -pub struct Signer { - service: String, - region: String, - credential: Option, - - /// Allow anonymous request if credential is not loaded. - allow_anonymous: bool, - - time: Option, -} - -impl Signer { - /// Create a builder. - pub fn builder() -> Builder { - Builder::default() - } - - /// Get credential - /// - /// # Note - /// - /// This function should never be exported to avoid credential leaking by - /// mistake. - fn credential(&self) -> &Option { - &self.credential - } - - fn canonicalize( - &self, - req: &impl SignableRequest, - method: SigningMethod, - cred: &Credential, - ) -> Result { - let mut creq = CanonicalRequest::new(req, method, self.time)?; - creq.build_headers()?; - creq.build_query(cred, &self.service, &self.region)?; - - debug!("calculated canonical request: {creq}"); - Ok(creq) - } - - /// Calculate signing requests via SignableRequest. - fn calculate(&self, mut creq: CanonicalRequest, cred: &Credential) -> Result { - let encoded_req = hex_sha256(creq.to_string().as_bytes()); - - // Scope: "20220313///goog4_request" - let scope = format!( - "{}/{}/{}/goog4_request", - format_date(creq.signing_time), - self.region, - self.service - ); - debug!("calculated scope: {scope}"); - - // StringToSign: - // - // GOOG4-RSA-SHA256 - // 20220313T072004Z - // 20220313///goog4_request - // - let string_to_sign = { - let mut f = String::new(); - writeln!(f, "GOOG4-RSA-SHA256")?; - writeln!(f, "{}", format_iso8601(creq.signing_time))?; - writeln!(f, "{}", &scope)?; - write!(f, "{}", &encoded_req)?; - f - }; - debug!("calculated string to sign: {string_to_sign}"); - - use rsa::pkcs8::DecodePrivateKey; - - let mut rng = rand::thread_rng(); - let private_key = rsa::RsaPrivateKey::from_pkcs8_pem(cred.private_key())?; - let signing_key = SigningKey::::new_with_prefix(private_key); - let signature = signing_key.sign_with_rng(&mut rng, string_to_sign.as_bytes()); - let signature = signature.to_string(); - - let mut query = creq - .query - .take() - .expect("query must be valid in query signing"); - write!(query, "&X-Goog-Signature={signature}")?; - - creq.query = Some(query); - - Ok(creq) - } - - /// Apply signed results to requests. - fn apply(&self, req: &mut impl SignableRequest, creq: CanonicalRequest) -> Result<()> { - for (header, value) in creq.headers.into_iter() { - req.insert_header( - header.expect("header must contain only once"), - value.clone(), - )?; - } - - if let Some(query) = creq.query { - req.set_query(&query)?; - } - - Ok(()) - } - - /// Implementation for signing request with query. - /// Example can be found in signer.rs / sign_query - pub fn sign_query(&self, req: &mut impl SignableRequest, expire: Duration) -> Result<()> { - let credential = self.credential().as_ref().unwrap(); - let creq = self.canonicalize(req, SigningMethod::Query(expire), credential)?; - let creq = self.calculate(creq, credential)?; - self.apply(req, creq) - } -} - -impl Debug for Signer { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Signer {{ region: {}, service: {}, allow_anonymous: {} }}", - self.region, self.service, self.allow_anonymous - ) - } -} - -/// SigningMethod is the method that used in signing. -#[derive(Copy, Clone)] -pub enum SigningMethod { - /// Signing with query. - Query(Duration), -} - -#[derive(Clone)] -struct CanonicalRequest { - method: http::Method, - path: String, - query: Option, - headers: HeaderMap, - - signing_host: String, - signing_method: SigningMethod, - signing_time: DateTime, -} - -impl CanonicalRequest { - fn new( - req: &impl SignableRequest, - method: SigningMethod, - now: Option, - ) -> Result { - let mut canonical_headers = HeaderMap::with_capacity(req.headers().len()); - // Header names and values need to be normalized according to Step 4 of https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html - // Using append instead of insert means this will not clobber headers that have the same lowercase name - for (name, value) in req.headers().iter() { - // The user agent header should not be canonical because it may be altered by proxies - if name == http::header::USER_AGENT { - continue; - } - canonical_headers.append(name, normalize_header_value(value)); - } - - Ok(CanonicalRequest { - method: req.method(), - path: percent_decode_str(req.path()).decode_utf8()?.to_string(), - query: req.query().map(|v| v.to_string()), - headers: req.headers(), - - signing_host: req.host_port(), - signing_method: method, - signing_time: now.unwrap_or_else(time::now), - }) - } - - fn build_headers(&mut self) -> Result<()> { - // Insert HOST header if not present. - if self.headers.get(&http::header::HOST).is_none() { - let header = HeaderValue::try_from(self.signing_host.to_string())?; - self.headers.insert(http::header::HOST, header); - } - - Ok(()) - } - - fn signed_headers(&self) -> Vec<&str> { - let mut signed_headers = self.headers.keys().map(|v| v.as_str()).collect::>(); - signed_headers.sort_unstable(); - - signed_headers - } - - fn build_query(&mut self, cred: &Credential, service: &str, region: &str) -> Result<()> { - let query = self.query.take().unwrap_or_default(); - let mut params: Vec<_> = form_urlencoded::parse(query.as_bytes()).collect(); - - let SigningMethod::Query(expire) = self.signing_method; - params.push(("X-Goog-Algorithm".into(), "GOOG4-RSA-SHA256".into())); - params.push(( - "X-Goog-Credential".into(), - Cow::Owned(format!( - "{}/{}/{}/{}/goog4_request", - cred.client_email(), - format_date(self.signing_time), - region, - service - )), - )); - params.push(( - "X-Goog-Date".into(), - Cow::Owned(format_iso8601(self.signing_time)), - )); - params.push(( - "X-Goog-Expires".into(), - Cow::Owned(expire.whole_seconds().to_string()), - )); - params.push(( - "X-Goog-SignedHeaders".into(), - self.signed_headers().join(";").into(), - )); - // Sort by param name - params.sort(); - - if params.is_empty() { - return Ok(()); - } - - let param = params - .iter() - .map(|(k, v)| { - ( - utf8_percent_encode(k, &GOOG_QUERY_ENCODE_SET), - utf8_percent_encode(v, &GOOG_QUERY_ENCODE_SET), - ) - }) - .map(|(k, v)| format!("{k}={v}")) - .collect::>() - .join("&"); - self.query = Some(param); - - Ok(()) - } -} - -impl Display for CanonicalRequest { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "{}", self.method)?; - writeln!( - f, - "{}", - utf8_percent_encode(&self.path, &super::constants::GOOG_URI_ENCODE_SET) - )?; - writeln!(f, "{}", self.query.as_ref().unwrap_or(&"".to_string()))?; - - let signed_headers = self.signed_headers(); - for header in signed_headers.iter() { - let value = &self.headers[*header]; - writeln!( - f, - "{}:{}", - header, - value.to_str().expect("header value must be valid") - )?; - } - writeln!(f)?; - writeln!(f, "{}", signed_headers.join(";"))?; - // TODO: we should support user specify payload hash. - write!(f, "UNSIGNED-PAYLOAD")?; - - Ok(()) - } -} - -fn normalize_header_value(header_value: &HeaderValue) -> HeaderValue { - let bs = header_value.as_bytes(); - - let starting_index = bs.iter().position(|b| *b != b' ').unwrap_or(0); - let ending_offset = bs.iter().rev().position(|b| *b != b' ').unwrap_or(0); - let ending_index = bs.len() - ending_offset; - - // This can't fail because we started with a valid HeaderValue and then only trimmed spaces - HeaderValue::from_bytes(&bs[starting_index..ending_index]).expect("invalid header value") -} - -#[cfg(test)] -mod tests { - use crate::google::{credential::CredentialLoader, v4::Signer}; - use crate::request::SignableRequest; - use crate::time::parse_rfc2822; - use anyhow::Result; - use time::UtcOffset; - - #[test] - fn test_sign_query_deterministic() -> Result<()> { - let credential_path = format!( - "{}/testdata/services/google/testbucket_credential.json", - std::env::current_dir() - .expect("current_dir must exist") - .to_string_lossy() - ); - - let credential_loader = CredentialLoader::from_path(&credential_path).unwrap(); - let credentials = credential_loader.load_credential().unwrap(); - - let mut req = http::Request::new(""); - *req.method_mut() = http::Method::GET; - *req.uri_mut() = "https://storage.googleapis.com/testbucket-reqsign/CONTRIBUTING.md" - .parse() - .expect("url must be valid"); - - let time_offset = parse_rfc2822("Mon, 15 Aug 2022 16:50:12 GMT")? - .to_offset(UtcOffset::from_hms(0, 0, 0)?); - - let v4_signer = Signer::builder() - // TODO set this from outside? - .service("storage") - .region("auto") - .time(time_offset) - .credential(credentials) - .build()?; - - v4_signer.sign_query(&mut req, time::Duration::hours(1))?; - - let query = req.query().unwrap(); - assert!(query.contains("X-Goog-Algorithm=GOOG4-RSA-SHA256")); - assert!(query.contains("X-Goog-Credential")); - assert!(query == "X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=testbucket-reqsign-account%40iam-testbucket-reqsign-project.iam.gserviceaccount.com%2F20220815%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220815T165012Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&X-Goog-Signature=9F423139DB223D818F2D4D6BCA4916DD1EE5AEB8E72D99EC60E8B903DC3CF0586C27A0F821C8CB20C6BB76C776E63134DAFF5957E7862BB89926F18E0D3618E4EE40EF8DBEC64D87F5AD4CAF6FE4C2BC3239E1076A33BE3113D6E0D1AF263C16FA5E1C9590C8F8E4E2CA2FED11533607B5AFE84B53E2E00CB320E0BC853C138EBBDCFEC3E9219C73551478EE12AABBD2576686F887738A21DC5AE00DFF3D481BD08F642342C8CCB476E74C8FEA0C02BA6FEFD61300218D6E216EAD4B59F3351E456601DF38D1CC1B4CE639D2748739933672A08B5FEBBED01B5BC0785E81A865EE0252A0C5AE239061F3F5DB4AFD8CC676646750C762A277FBFDE70A85DFDF33"); - Ok(()) - } -} diff --git a/tests/google/storage.rs b/tests/google/storage.rs index fddbdbde..0b81430c 100644 --- a/tests/google/storage.rs +++ b/tests/google/storage.rs @@ -6,6 +6,7 @@ use log::debug; use log::warn; use reqsign::GoogleSigner; use reqwest::blocking::Client; +use time::Duration; fn init_signer() -> Option { let _ = env_logger::builder().is_test(true).try_init(); @@ -90,3 +91,42 @@ fn test_list_objects() -> Result<()> { assert_eq!(StatusCode::OK, resp.status()); Ok(()) } + +#[test] +fn test_get_object_with_query() -> Result<()> { + let signer = init_signer(); + if signer.is_none() { + warn!("REQSIGN_GOOGLE_TEST is not set, skipped"); + return Ok(()); + } + let signer = signer.unwrap(); + + let url = &env::var("REQSIGN_GOOGLE_CLOUD_STORAGE_URL") + .expect("env REQSIGN_GOOGLE_CLOUD_STORAGE_URL must set"); + + let mut builder = http::Request::builder(); + builder = builder.method(http::Method::GET); + builder = builder.uri(format!( + "{}/{}", + url.replace("storage/v1/b/", ""), + "not_exist_file" + )); + let mut req = builder.body("")?; + + signer + .sign_query(&mut req, Duration::hours(1)) + .expect("sign request must success"); + + debug!("signed request: {:?}", req); + + let client = Client::new(); + let resp = client + .execute(req.try_into()?) + .expect("request must succeed"); + + let code = resp.status(); + debug!("got response: {:?}", resp); + debug!("got body: {}", resp.text().unwrap_or_default()); + assert_eq!(StatusCode::NOT_FOUND, code); + Ok(()) +}