rusty-oss 0.2.1

Simple pure Rust Aliyun OSS Client following a Sans-IO approach
Documentation
use std::{iter, str};

use time::OffsetDateTime;
use url::Url;

use crate::sorting_iter::SortingIterator;
use crate::time_::{ISO8601, YYYYMMDD};
use crate::Method;

mod canonical_request;
mod signature;
mod string_to_sign;
pub(crate) mod util;

/// Sign a URL with OSS Signature.
///
/// # Panics
///
/// If date format is invalid.
#[allow(
    clippy::too_many_arguments,
    clippy::map_identity,
    clippy::option_if_let_else,
    clippy::single_match_else
)]
pub fn sign<'a, Q, H>(
    date: &OffsetDateTime,
    method: Method,
    mut url: Url,
    key: &str,
    secret: &str,
    token: Option<&str>,
    region: &str,
    expires_seconds: u64,

    query_string: Q,
    headers: H,
) -> Url
where
    Q: Iterator<Item = (&'a str, &'a str)> + Clone,
    H: Iterator<Item = (&'a str, &'a str)> + Clone,
{
    // Convert `&'a str` into `&str`, in order to later be able to join them to
    // the inner iterators, which because of the references they take to the inner
    // `String`s, have a shorter lifetime than 'a.
    // Thanks to: https://t.me/rustlang_it/61993
    let query_string = query_string.map(|(k, value)| (k, value));
    let headers = headers.map(|(k, value)| (k, value));

    let yyyymmdd = date.format(&YYYYMMDD).expect("invalid format");

    let credential = format!(
        "{}/{}/{}/{}/{}",
        key, yyyymmdd, region, "oss", "aliyun_v4_request"
    );
    let date_str = date.format(&ISO8601).expect("invalid format");
    let expires_seconds_string = expires_seconds.to_string();

    let host = url.host_str().expect("host is known");
    let host_header = match (url.scheme(), url.port()) {
        ("http" | "https", None) | ("http", Some(80)) | ("https", Some(443)) => host.to_owned(),
        ("http" | "https", Some(port)) => {
            format!("{host}:{port}")
        }
        _ => panic!("unsupported url scheme"),
    };

    let standard_headers = iter::once(("host", host_header.as_str()));
    let headers = SortingIterator::new(standard_headers, headers);

    let signed_headers = headers.clone().map(|(k, _)| k);
    let mut signed_headers_str = String::new();
    for header in signed_headers.clone() {
        if !signed_headers_str.is_empty() {
            signed_headers_str.push(';');
        }
        signed_headers_str.push_str(header);
    }

    let a1;
    let a2;
    let standard_query = match token {
        Some(token) => {
            a1 = [
                ("x-oss-additional-headers", signed_headers_str.as_str()),
                ("x-oss-credential", credential.as_str()),
                ("x-oss-date", date_str.as_str()),
                ("x-oss-expires", expires_seconds_string.as_str()),
                ("x-oss-security-token", token),
                ("x-oss-signature-version", "OSS4-HMAC-SHA256"),
            ];
            a1.iter()
        }
        None => {
            a2 = [
                ("x-oss-additional-headers", signed_headers_str.as_str()),
                ("x-oss-credential", credential.as_str()),
                ("x-oss-date", date_str.as_str()),
                ("x-oss-expires", expires_seconds_string.as_str()),
                ("x-oss-signature-version", "OSS4-HMAC-SHA256"),
            ];
            a2.iter()
        }
    };

    let query_string = SortingIterator::new(standard_query.copied(), query_string);

    {
        let mut query_pairs = url.query_pairs_mut();
        query_pairs.clear();

        query_pairs.extend_pairs(query_string.clone());
    }

    let canonical_req =
        canonical_request::canonical_request(method, &url, query_string, headers, signed_headers);
    let signed_string = string_to_sign::string_to_sign(date, region, &canonical_req);
    let signature = signature::signature(date, secret, region, &signed_string);

    url.query_pairs_mut()
        .append_pair("x-oss-signature", &signature);
    url
}

#[cfg(test)]
mod tests {
    use std::iter;

    use pretty_assertions::assert_eq;
    use time::OffsetDateTime;

    use super::Method;
    use super::*;

    #[test]
    fn oss_example() {
        let date = OffsetDateTime::from_unix_timestamp(1738860711).unwrap();

        let method = Method::Get;
        let url = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com"
            .parse()
            .unwrap();
        let key = "accesskeyid";
        let secret = "accesskeysecret";
        let region = "cn-hangzhou";
        let expires_seconds = 86400;

        let expected = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/?x-oss-additional-headers=host&x-oss-credential=accesskeyid%2F20250206%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-date=20250206T165151Z&x-oss-expires=86400&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-signature=e64e70342712d35621fa2a01ff979aa1a904329ae84746c3cfe5f96ecb44a668";

        let got = sign(
            &date,
            method,
            url,
            key,
            secret,
            None,
            region,
            expires_seconds,
            iter::empty(),
            iter::empty(),
        );

        assert_eq!(expected, got.as_str());
    }

    #[test]
    fn oss_example_token() {
        // 2022-12-31 20:48:42 GMT
        let date = OffsetDateTime::from_unix_timestamp(1672519722).unwrap();

        let method = Method::Get;
        let url = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/exampleobject"
            .parse()
            .unwrap();
        let key = "accesskeyid";
        let secret = "accesskeysecret";
        let token = "CAIS******";
        let region = "cn-hangzhou";
        let expires_seconds = 86400;

        let expected = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/exampleobject?x-oss-additional-headers=host&x-oss-credential=accesskeyid%2F20221231%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-date=20221231T204842Z&x-oss-expires=86400&x-oss-security-token=CAIS******&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-signature=68de9dca3035ab3fd4ca7d63017ddf2c059680687927d3afa10b545f43f4fed8";

        let got = sign(
            &date,
            method,
            url,
            key,
            secret,
            Some(token),
            region,
            expires_seconds,
            iter::empty(),
            iter::empty(),
        );

        assert_eq!(expected, got.as_str());
    }

    #[test]
    fn oss_headers_example() {
        let date = OffsetDateTime::from_unix_timestamp(1672519722).unwrap();

        let method = Method::Get;
        let url = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/exampleobject"
            .parse()
            .unwrap();
        let key = "accesskeyid";
        let secret = "accesskeysecret";
        let region = "cn-hangzhou";
        let expires_seconds = 86400;

        let expected = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/exampleobject?x-oss-additional-headers=content-type%3Bhost%3Bx-oss-date&x-oss-credential=accesskeyid%2F20221231%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-date=20221231T204842Z&x-oss-expires=86400&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-signature=ec311fb89afe97a5dad0030181e318d47216dafe7cf747873f937f2aefb4a3b6";

        let headers = [
            (
                "content-type",
                "application/x-www-form-urlencoded; charset=utf-8",
            ),
            ("x-oss-date", "20231203T121212Z"),
        ];

        let got = sign(
            &date,
            method,
            url,
            key,
            secret,
            None,
            region,
            expires_seconds,
            iter::empty(),
            headers.iter().copied(),
        );

        assert_eq!(expected, got.as_str());
    }
}