This is an automated email from the ASF dual-hosted git repository. xuanwo pushed a commit to branch xuanwo/oss-pr8-signature-v2 in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git
commit 38d5c9fa3b64df8b1e58a03c1e4756b15f56516c Author: Xuanwo <[email protected]> AuthorDate: Thu Mar 19 02:03:24 2026 +0800 feat(aliyun-oss): add signature v2 signing support --- services/aliyun-oss/README.md | 20 +- services/aliyun-oss/src/lib.rs | 24 +- services/aliyun-oss/src/sign_request.rs | 545 +++++++++++++++++++++++++++++++- 3 files changed, 549 insertions(+), 40 deletions(-) diff --git a/services/aliyun-oss/README.md b/services/aliyun-oss/README.md index 29c1a4f..83c0952 100644 --- a/services/aliyun-oss/README.md +++ b/services/aliyun-oss/README.md @@ -10,14 +10,9 @@ This crate provides signing support for Alibaba Cloud Object Storage Service (OS ```rust use reqsign_aliyun_oss::{ -<<<<<<< HEAD - AssumeRoleWithOidcCredentialProvider, DefaultCredentialProvider, EnvCredentialProvider, - RequestSigner, SigningVersion, StaticCredentialProvider, -======= AssumeRoleWithOidcCredentialProvider, ConfigFileCredentialProvider, CredentialsFileCredentialProvider, DefaultCredentialProvider, EnvCredentialProvider, OssProfileCredentialProvider, RequestSigner, SigningVersion, StaticCredentialProvider, ->>>>>>> origin/main }; use reqsign_core::{Context, Result, Signer}; use reqsign_file_read_tokio::TokioFileRead; @@ -66,13 +61,8 @@ async fn main() -> Result<()> { ## Features -- **V1 and V4 Signing**: Supports both legacy OSS V1 signatures and Signature V4 -<<<<<<< HEAD -- **Multiple Credential Sources**: Environment variables and OIDC-based STS exchange -======= +- **V1, V2, and V4 Signing**: Supports legacy OSS V1, SHA256-based V2, and region-aware V4 - **Multiple Credential Sources**: Environment variables, OSS profile files, Alibaba shared credential/config files, and OIDC-based STS exchange -- **V1 and V4 Signing**: Supports both legacy OSS V1 signatures and Signature V4 ->>>>>>> origin/main - **STS Support**: Temporary credentials via Security Token Service - **All OSS Operations**: Object, bucket, and multipart operations @@ -80,6 +70,14 @@ async fn main() -> Result<()> { `RequestSigner::new("bucket")` keeps the current V1 behavior. +To opt into V2 signing: + +```rust +use reqsign_aliyun_oss::{RequestSigner, SigningVersion}; + +let signer = RequestSigner::new("bucket").with_signing_version(SigningVersion::V2); +``` + To opt into V4 signing, configure both the region and signing version: ```rust diff --git a/services/aliyun-oss/src/lib.rs b/services/aliyun-oss/src/lib.rs index c81feab..9da4e2f 100644 --- a/services/aliyun-oss/src/lib.rs +++ b/services/aliyun-oss/src/lib.rs @@ -22,29 +22,20 @@ //! //! ## Overview //! -//! Aliyun OSS uses a custom signing algorithm based on HMAC-SHA1. This crate implements -//! the complete signing process along with credential loading from various sources -//! including environment variables, configuration files, and STS tokens. +//! Aliyun OSS supports multiple signature generations. This crate keeps V1 as the +//! default and also supports V2 and V4 signing for callers that need stronger hashing +//! or modern region-aware signing. //! -//! `RequestSigner` defaults to V1 signing and supports opting into V4 signing -<<<<<<< HEAD -//! when a region is configured. -======= -//! when both region and signing version are configured. ->>>>>>> origin/main +//! `RequestSigner` defaults to V1 signing and supports opting into V2 or V4 signing. +//! V4 additionally requires a configured region. //! //! ## Quick Start //! //! ```no_run //! use reqsign_aliyun_oss::{ -<<<<<<< HEAD -//! AssumeRoleWithOidcCredentialProvider, DefaultCredentialProvider, EnvCredentialProvider, -//! RequestSigner, SigningVersion, StaticCredentialProvider, -======= //! AssumeRoleWithOidcCredentialProvider, ConfigFileCredentialProvider, //! CredentialsFileCredentialProvider, DefaultCredentialProvider, EnvCredentialProvider, //! OssProfileCredentialProvider, RequestSigner, SigningVersion, StaticCredentialProvider, ->>>>>>> origin/main //! }; //! use reqsign_core::{Context, Signer, Result}; //! use reqsign_file_read_tokio::TokioFileRead; @@ -75,7 +66,10 @@ //! //! // Create request builder //! let builder = RequestSigner::new("bucket"); -//! // Or opt into V4: +//! // Or opt into V2/V4: +//! // let builder = RequestSigner::new("bucket") +//! // .with_signing_version(SigningVersion::V2); +//! //! // let builder = RequestSigner::new("bucket") //! // .with_region("cn-beijing") //! // .with_signing_version(SigningVersion::V4); diff --git a/services/aliyun-oss/src/sign_request.rs b/services/aliyun-oss/src/sign_request.rs index 39a1a17..4661db0 100644 --- a/services/aliyun-oss/src/sign_request.rs +++ b/services/aliyun-oss/src/sign_request.rs @@ -20,7 +20,9 @@ use http::HeaderValue; use http::header::{AUTHORIZATION, CONTENT_TYPE, DATE, HOST}; use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode}; use reqsign_core::Result; -use reqsign_core::hash::{base64_hmac_sha1, hex_hmac_sha256, hex_sha256, hmac_sha256}; +use reqsign_core::hash::{ + base64_hmac_sha1, base64_hmac_sha256, hex_hmac_sha256, hex_sha256, hmac_sha256, +}; use reqsign_core::time::Timestamp; use reqsign_core::{Context, Error, SignRequest, SigningRequest}; use std::collections::HashSet; @@ -29,11 +31,15 @@ use std::sync::LazyLock; use std::time::Duration; const CONTENT_MD5: &str = "content-md5"; +const IF_MODIFIED_SINCE: &str = "if-modified-since"; +const OSS_V2_ALGORITHM: &str = "OSS2"; const OSS_V4_ALGORITHM: &str = "OSS4-HMAC-SHA256"; const OSS_V4_REQUEST: &str = "aliyun_v4_request"; const OSS_V4_SERVICE: &str = "oss"; +const RANGE: &str = "range"; const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; const X_OSS_ADDITIONAL_HEADERS: &str = "x-oss-additional-headers"; +const X_OSS_ACCESS_KEY_ID: &str = "x-oss-access-key-id"; const X_OSS_CONTENT_SHA256: &str = "x-oss-content-sha256"; const X_OSS_CREDENTIAL: &str = "x-oss-credential"; const X_OSS_DATE: &str = "x-oss-date"; @@ -55,11 +61,20 @@ static OSS_V4_QUERY_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC .remove(b'_') .remove(b'~'); +static OSS_V2_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC + .remove(b'-') + .remove(b'.') + .remove(b'_') + .remove(b'~'); + +const OSS_V2_DEFAULT_ADDITIONAL_HEADERS: &[&str] = &[IF_MODIFIED_SINCE, RANGE]; + /// SigningVersion controls which OSS signing algorithm the signer uses. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum SigningVersion { V1, + V2, V4, } @@ -85,7 +100,7 @@ impl RequestSigner { /// Set the OSS region. /// - /// Signature V4 requires this value. V1 keeps ignoring it. + /// Signature V4 requires this value. V1 and V2 keep ignoring it. pub fn with_region(mut self, region: impl Into<String>) -> Self { self.region = Some(region.into()); self @@ -93,8 +108,8 @@ impl RequestSigner { /// Set the signing version. /// - /// The signer defaults to V1. Use V4 together with [`RequestSigner::with_region`] - /// to opt into OSS Signature Version 4. + /// The signer defaults to V1. Use V2 for SHA256-based legacy signing, + /// or V4 together with [`RequestSigner::with_region`] for region-aware signing. pub fn with_signing_version(mut self, signing_version: SigningVersion) -> Self { self.signing_version = signing_version; self @@ -140,6 +155,9 @@ impl SignRequest for RequestSigner { self.sign_header(req, cred, signing_time)?; } } + SigningVersion::V2 => { + self.sign_v2(req, cred, signing_time, expires_in)?; + } SigningVersion::V4 => { self.sign_v4(req, cred, signing_time, expires_in)?; } @@ -150,6 +168,93 @@ impl SignRequest for RequestSigner { } impl RequestSigner { + fn sign_v2( + &self, + req: &mut http::request::Parts, + cred: &Credential, + signing_time: Timestamp, + expires_in: Option<Duration>, + ) -> Result<()> { + match expires_in { + Some(expires) => self.sign_v2_query(req, cred, signing_time, expires), + None => self.sign_v2_header(req, cred, signing_time), + } + } + + fn sign_v2_header( + &self, + req: &mut http::request::Parts, + cred: &Credential, + signing_time: Timestamp, + ) -> Result<()> { + let date = signing_time.format_http_date(); + req.headers.insert(DATE, date.parse()?); + + if let Some(token) = &cred.security_token { + req.headers.insert(X_OSS_SECURITY_TOKEN, token.parse()?); + } + + let additional_headers = self.v2_additional_headers(req, false); + let query_pairs = self.query_pairs(req); + let string_to_sign = + self.build_v2_string_to_sign(req, &query_pairs, &date, &additional_headers)?; + let signature = + base64_hmac_sha256(cred.access_key_secret.as_bytes(), string_to_sign.as_bytes()); + + let authorization = if additional_headers.is_empty() { + format!( + "{OSS_V2_ALGORITHM} AccessKeyId:{},Signature:{signature}", + cred.access_key_id + ) + } else { + format!( + "{OSS_V2_ALGORITHM} AccessKeyId:{},AdditionalHeaders:{},Signature:{signature}", + cred.access_key_id, + additional_headers.join(";") + ) + }; + let mut value: HeaderValue = authorization.parse()?; + value.set_sensitive(true); + req.headers.insert(AUTHORIZATION, value); + + Ok(()) + } + + fn sign_v2_query( + &self, + req: &mut http::request::Parts, + cred: &Credential, + signing_time: Timestamp, + expires: Duration, + ) -> Result<()> { + let expiration_time = (signing_time + expires).as_second().to_string(); + let additional_headers = self.v2_additional_headers(req, true); + let mut query_pairs = self.query_pairs(req); + query_pairs.push(( + X_OSS_SIGNATURE_VERSION.to_string(), + OSS_V2_ALGORITHM.to_string(), + )); + query_pairs.push((X_OSS_EXPIRES.to_string(), expiration_time.clone())); + query_pairs.push((X_OSS_ACCESS_KEY_ID.to_string(), cred.access_key_id.clone())); + if !additional_headers.is_empty() { + query_pairs.push(( + X_OSS_ADDITIONAL_HEADERS.to_string(), + additional_headers.join(";"), + )); + } + if let Some(token) = &cred.security_token { + query_pairs.push(("security-token".to_string(), token.clone())); + } + + let string_to_sign = + self.build_v2_string_to_sign(req, &query_pairs, &expiration_time, &additional_headers)?; + let signature = + base64_hmac_sha256(cred.access_key_secret.as_bytes(), string_to_sign.as_bytes()); + query_pairs.push((X_OSS_SIGNATURE.to_string(), signature)); + + self.apply_query_pairs(req, &query_pairs) + } + fn sign_header( &self, req: &mut http::request::Parts, @@ -250,6 +355,47 @@ impl RequestSigner { Ok(()) } + fn build_v2_string_to_sign( + &self, + req: &http::request::Parts, + query_pairs: &[(String, String)], + date_or_expires: &str, + additional_headers: &[String], + ) -> Result<String> { + let mut s = String::new(); + writeln!(&mut s, "{}", req.method)?; + writeln!( + &mut s, + "{}", + req.headers + .get(CONTENT_MD5) + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + )?; + writeln!( + &mut s, + "{}", + req.headers + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + )?; + writeln!(&mut s, "{date_or_expires}")?; + write!( + &mut s, + "{}", + self.v2_canonicalized_headers(req, additional_headers)? + )?; + writeln!(&mut s, "{}", additional_headers.join(";"))?; + write!( + &mut s, + "{}", + self.v2_canonicalized_resource(req, query_pairs)? + )?; + + Ok(s) + } + fn build_string_to_sign( &self, req: &http::request::Parts, @@ -620,16 +766,157 @@ impl RequestSigner { Ok(utf8_percent_encode(&resource_path, &OSS_V4_URI_ENCODE_SET).to_string()) } -<<<<<<< HEAD - fn canonicalize_headers(&self, req: &http::request::Parts, cred: &Credential) -> String { -======= + fn v2_additional_headers(&self, req: &http::request::Parts, is_presign: bool) -> Vec<String> { + let defaults = if is_presign { + &[][..] + } else { + OSS_V2_DEFAULT_ADDITIONAL_HEADERS + }; + + let mut headers = defaults + .iter() + .copied() + .filter(|name| req.headers.contains_key(*name)) + .map(str::to_string) + .collect::<Vec<_>>(); + headers.sort(); + headers.dedup(); + headers + } + + fn v2_canonicalized_headers( + &self, + req: &http::request::Parts, + additional_headers: &[String], + ) -> Result<String> { + let mut headers = Vec::new(); + + for (name, value) in &req.headers { + let name = name.as_str().to_ascii_lowercase(); + if name == AUTHORIZATION.as_str() { + continue; + } + if name.starts_with("x-oss-") || additional_headers.binary_search(&name).is_ok() { + let value = value + .to_str() + .map_err(|e| { + Error::request_invalid("invalid header value for V2 signing").with_source(e) + })? + .trim() + .to_string(); + headers.push((name, value)); + } + } + + headers.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut s = String::new(); + for (name, value) in headers { + writeln!(&mut s, "{name}:{value}")?; + } + Ok(s) + } + + fn v2_canonicalized_resource( + &self, + req: &http::request::Parts, + query_pairs: &[(String, String)], + ) -> Result<String> { + let decoded_path = percent_decode_str(req.uri.path()) + .decode_utf8() + .map_err(|e| Error::request_invalid("invalid request path").with_source(e))?; + let authority = req.uri.authority().map(|v| v.as_str()).unwrap_or(""); + let resource_path = if authority.starts_with(&format!("{}.", self.bucket)) { + format!("/{}{}", self.bucket, decoded_path) + } else { + decoded_path.into_owned() + }; + + let mut resource = utf8_percent_encode(&resource_path, &OSS_V2_ENCODE_SET).to_string(); + let canonical_query = self.v2_canonicalized_query(query_pairs); + if !canonical_query.is_empty() { + resource.push('?'); + resource.push_str(&canonical_query); + } + + Ok(resource) + } + + fn v2_canonicalized_query(&self, query_pairs: &[(String, String)]) -> String { + let mut encoded_pairs = query_pairs + .iter() + .enumerate() + .map(|(idx, (key, value))| { + ( + idx, + utf8_percent_encode(key, &OSS_V2_ENCODE_SET).to_string(), + utf8_percent_encode(value, &OSS_V2_ENCODE_SET).to_string(), + ) + }) + .collect::<Vec<_>>(); + encoded_pairs.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.cmp(&b.2)).then(a.0.cmp(&b.0))); + + encoded_pairs + .into_iter() + .map(|(_, key, value)| { + if value.is_empty() { + key + } else { + format!("{key}={value}") + } + }) + .collect::<Vec<_>>() + .join("&") + } + + fn query_pairs(&self, req: &http::request::Parts) -> Vec<(String, String)> { + req.uri + .query() + .map(|query| { + form_urlencoded::parse(query.as_bytes()) + .map(|(key, value)| (key.into_owned(), value.into_owned())) + .collect() + }) + .unwrap_or_default() + } + + fn apply_query_pairs( + &self, + req: &mut http::request::Parts, + query_pairs: &[(String, String)], + ) -> Result<()> { + let query_string = query_pairs + .iter() + .map(|(key, value)| { + let key = utf8_percent_encode(key, &OSS_V2_ENCODE_SET).to_string(); + if value.is_empty() { + key + } else { + format!("{key}={}", utf8_percent_encode(value, &OSS_V2_ENCODE_SET)) + } + }) + .collect::<Vec<_>>() + .join("&"); + + let path = req.uri.path(); + let new_path_and_query = if query_string.is_empty() { + path.to_string() + } else { + format!("{path}?{query_string}") + }; + let mut parts = req.uri.clone().into_parts(); + parts.path_and_query = Some(new_path_and_query.try_into()?); + req.uri = http::Uri::from_parts(parts)?; + + Ok(()) + } + fn canonicalize_headers( &self, req: &http::request::Parts, include_implicit_security_token: bool, cred: &Credential, ) -> String { ->>>>>>> origin/main let mut oss_headers = Vec::new(); // Collect x-oss-* headers @@ -827,10 +1114,6 @@ static SUBRESOURCES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| { mod tests { use super::*; use crate::Credential; -<<<<<<< HEAD - use reqsign_core::{Context, SigningRequest}; - -======= use http::Request; use reqsign_core::{Context, SigningRequest}; @@ -921,8 +1204,6 @@ mod tests { ); assert!(!string_to_sign.contains("x-oss-security-token:sts-token")); } - ->>>>>>> origin/main #[test] fn test_request_signer_accepts_region_configuration() { let signer = RequestSigner::new("bucket") @@ -977,6 +1258,242 @@ mod tests { ); } + #[test] + fn test_v2_header_signature_matches_official_put_object_example() { + let credential = Credential { + access_key_id: "44CF9590006BF252F707".to_string(), + access_key_secret: "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV".to_string(), + security_token: None, + expires_in: None, + }; + let time = Timestamp::from_second(1_487_151_431).expect("timestamp must build"); + let signer = RequestSigner::new("oss-example") + .with_signing_version(SigningVersion::V2) + .with_time(time); + + let req = http::Request::put("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson") + .header(CONTENT_MD5, "FxqG8Ca0qEJPOghSihJ8Ew==") + .header(CONTENT_TYPE, "text/plain") + .header("x-oss-object-acl", "private") + .header(DATE, "Wed, 15 Feb 2017 09:37:11 GMT") + .body(()) + .expect("request must build") + .into_parts() + .0; + + let additional_headers = signer.v2_additional_headers(&req, false); + let string_to_sign = signer + .build_v2_string_to_sign( + &req, + &signer.query_pairs(&req), + "Wed, 15 Feb 2017 09:37:11 GMT", + &additional_headers, + ) + .expect("string to sign must build"); + assert_eq!( + string_to_sign, + "PUT\nFxqG8Ca0qEJPOghSihJ8Ew==\ntext/plain\nWed, 15 Feb 2017 09:37:11 GMT\nx-oss-object-acl:private\n\n%2Foss-example%2Fnelson" + ); + + let mut signed_req = + http::Request::put("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson") + .header(CONTENT_MD5, "FxqG8Ca0qEJPOghSihJ8Ew==") + .header(CONTENT_TYPE, "text/plain") + .header("x-oss-object-acl", "private") + .body(()) + .expect("request must build") + .into_parts() + .0; + signer + .sign_v2_header(&mut signed_req, &credential, time) + .expect("v2 header signing must succeed"); + + assert_eq!( + signed_req.headers.get(DATE).and_then(|v| v.to_str().ok()), + Some("Wed, 15 Feb 2017 09:37:11 GMT") + ); + assert_eq!( + signed_req + .headers + .get(AUTHORIZATION) + .and_then(|v| v.to_str().ok()), + Some( + "OSS2 AccessKeyId:44CF9590006BF252F707,Signature:5Am2ewK1tL0gXX7GV6dwybZtj7efOEtc0Mo2FR6CkM8=" + ) + ); + } + + #[test] + fn test_v2_header_signature_matches_official_additional_headers_example() { + let credential = Credential { + access_key_id: "44CF9590006BF252F707".to_string(), + access_key_secret: "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV".to_string(), + security_token: None, + expires_in: None, + }; + let time = Timestamp::from_second(1_487_210_979).expect("timestamp must build"); + let signer = RequestSigner::new("oss-example") + .with_signing_version(SigningVersion::V2) + .with_time(time); + + let req = http::Request::get("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson") + .header(RANGE, "bytes=0-7") + .header(IF_MODIFIED_SINCE, "Thu, 16 Feb 2017 02:10:39 GMT") + .header(DATE, "Thu, 16 Feb 2017 02:09:39 GMT") + .body(()) + .expect("request must build") + .into_parts() + .0; + + let additional_headers = signer.v2_additional_headers(&req, false); + assert_eq!( + additional_headers, + vec![IF_MODIFIED_SINCE.to_string(), RANGE.to_string()] + ); + let string_to_sign = signer + .build_v2_string_to_sign( + &req, + &signer.query_pairs(&req), + "Thu, 16 Feb 2017 02:09:39 GMT", + &additional_headers, + ) + .expect("string to sign must build"); + assert_eq!( + string_to_sign, + "GET\n\n\nThu, 16 Feb 2017 02:09:39 GMT\nif-modified-since:Thu, 16 Feb 2017 02:10:39 GMT\nrange:bytes=0-7\nif-modified-since;range\n%2Foss-example%2Fnelson" + ); + + let mut signed_req = + http::Request::get("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson") + .header(RANGE, "bytes=0-7") + .header(IF_MODIFIED_SINCE, "Thu, 16 Feb 2017 02:10:39 GMT") + .body(()) + .expect("request must build") + .into_parts() + .0; + signer + .sign_v2_header(&mut signed_req, &credential, time) + .expect("v2 header signing must succeed"); + + assert_eq!( + signed_req + .headers + .get(AUTHORIZATION) + .and_then(|v| v.to_str().ok()), + Some( + "OSS2 AccessKeyId:44CF9590006BF252F707,AdditionalHeaders:if-modified-since;range,Signature:YG9mKO3m4S0Jx9Hk6Lq64VchJg/TOTkyCX4DaeeOYxE=" + ) + ); + } + + #[test] + fn test_v2_presign_signature_matches_official_example() { + let credential = Credential { + access_key_id: "44CF9590006BF252F707".to_string(), + access_key_secret: "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV".to_string(), + security_token: None, + expires_in: None, + }; + let time = Timestamp::from_second(1_487_151_431).expect("timestamp must build"); + let signer = RequestSigner::new("oss-example") + .with_signing_version(SigningVersion::V2) + .with_time(time); + + let req = http::Request::get("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson") + .body(()) + .expect("request must build") + .into_parts() + .0; + let expiration_time = "1487152431".to_string(); + let mut query_pairs = signer.query_pairs(&req); + query_pairs.push(( + X_OSS_SIGNATURE_VERSION.to_string(), + OSS_V2_ALGORITHM.to_string(), + )); + query_pairs.push((X_OSS_EXPIRES.to_string(), expiration_time.clone())); + query_pairs.push(( + X_OSS_ACCESS_KEY_ID.to_string(), + credential.access_key_id.clone(), + )); + let additional_headers = signer.v2_additional_headers(&req, true); + let string_to_sign = signer + .build_v2_string_to_sign(&req, &query_pairs, &expiration_time, &additional_headers) + .expect("string to sign must build"); + assert_eq!( + string_to_sign, + "GET\n\n\n1487152431\n\n%2Foss-example%2Fnelson?x-oss-access-key-id=44CF9590006BF252F707&x-oss-expires=1487152431&x-oss-signature-version=OSS2" + ); + + let mut signed_req = + http::Request::get("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson") + .body(()) + .expect("request must build") + .into_parts() + .0; + signer + .sign_v2_query( + &mut signed_req, + &credential, + time, + Duration::from_secs(1_000), + ) + .expect("v2 presign must succeed"); + + assert_eq!( + signed_req.uri.query(), + Some( + "x-oss-signature-version=OSS2&x-oss-expires=1487152431&x-oss-access-key-id=44CF9590006BF252F707&x-oss-signature=ps%2F%2BMLhd1WKkVi%2FQlOiliJsTaBMBk93f6UYVscDNHCQ%3D" + ) + ); + } + + #[test] + fn test_v2_presign_signs_all_query_parameters() { + let credential = Credential { + access_key_id: "44CF9590006BF252F707".to_string(), + access_key_secret: "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV".to_string(), + security_token: None, + expires_in: None, + }; + let time = Timestamp::from_second(1_487_210_979).expect("timestamp must build"); + let signer = RequestSigner::new("oss-example") + .with_signing_version(SigningVersion::V2) + .with_time(time); + + let req = http::Request::get( + "https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson?x-oss-signature-version=OSS2&extra-query=1&x-oss-access-key-id=44CF9590006BF252F707&x-oss-expires=1487211619", + ) + .body(()) + .expect("request must build") + .into_parts() + .0; + let string_to_sign = signer + .build_v2_string_to_sign(&req, &signer.query_pairs(&req), "1487211619", &[]) + .expect("string to sign must build"); + assert_eq!( + string_to_sign, + "GET\n\n\n1487211619\n\n%2Foss-example%2Fnelson?extra-query=1&x-oss-access-key-id=44CF9590006BF252F707&x-oss-expires=1487211619&x-oss-signature-version=OSS2" + ); + + let mut signed_req = http::Request::get( + "https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson?extra-query=1", + ) + .body(()) + .expect("request must build") + .into_parts() + .0; + signer + .sign_v2_query(&mut signed_req, &credential, time, Duration::from_secs(640)) + .expect("v2 presign must succeed"); + + assert_eq!( + signed_req.uri.query(), + Some( + "extra-query=1&x-oss-signature-version=OSS2&x-oss-expires=1487211619&x-oss-access-key-id=44CF9590006BF252F707&x-oss-signature=wsARTPqvZdbdPjYpZfDZ%2FjisUaacYq7gGOdB3f1BgTE%3D" + ) + ); + } + #[tokio::test] async fn test_v4_signing_requires_region() { let mut req =
