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 =

Reply via email to