Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ anyhow = "1"
async-trait = "0.1"
base64 = "0.22"
bytes = "1"
chrono = "0.4.35"
criterion = { version = "0.7.0", features = ["async_tokio", "html_reports"] }
dotenv = "0.15"
env_logger = "0.11"
form_urlencoded = "1"
hex = "0.4"
hmac = "0.12"
home = "0.5"
http = "1"
jiff = "0.2"
log = "0.4"
macro_rules_attribute = "0.2.0"
once_cell = "1"
Expand Down
2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
bytes.workspace = true
chrono.workspace = true
form_urlencoded.workspace = true
hex.workspace = true
hmac.workspace = true
http.workspace = true
jiff.workspace = true
log.workspace = true
percent-encoding.workspace = true
sha1.workspace = true
Expand Down
83 changes: 25 additions & 58 deletions core/src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,68 +18,26 @@
//! Time related utils.

use crate::Error;
use chrono::format::Fixed;
use chrono::format::Item;
use chrono::format::Numeric;
use chrono::format::Pad;
use chrono::SecondsFormat;
use chrono::Utc;
use std::str::FromStr;

/// DateTime is the alias for chrono::DateTime<Utc>.
pub type DateTime = chrono::DateTime<Utc>;
/// DateTime is the alias for `jiff::Timestamp`.
pub type DateTime = jiff::Timestamp;
Comment thread
tisonkun marked this conversation as resolved.
Outdated

/// Create datetime of now.
pub fn now() -> DateTime {
Utc::now()
jiff::Timestamp::now()
}

/// DATE is a time format like `20220301`
const DATE: &[Item<'static>] = &[
Item::Numeric(Numeric::Year, Pad::Zero),
Item::Numeric(Numeric::Month, Pad::Zero),
Item::Numeric(Numeric::Day, Pad::Zero),
];

/// Format time into date: `20220301`
pub fn format_date(t: DateTime) -> String {
t.format_with_items(DATE.iter()).to_string()
t.strftime("%Y%m%d").to_string()
}

/// ISO8601 is a time format like `20220313T072004Z`.
const ISO8601: &[Item<'static>] = &[
Item::Numeric(Numeric::Year, Pad::Zero),
Item::Numeric(Numeric::Month, Pad::Zero),
Item::Numeric(Numeric::Day, Pad::Zero),
Item::Literal("T"),
Item::Numeric(Numeric::Hour, Pad::Zero),
Item::Numeric(Numeric::Minute, Pad::Zero),
Item::Numeric(Numeric::Second, Pad::Zero),
Item::Literal("Z"),
];

/// Format time into ISO8601: `20220313T072004Z`
pub fn format_iso8601(t: DateTime) -> String {
t.format_with_items(ISO8601.iter()).to_string()
t.strftime("%Y%m%dT%H%M%SZ").to_string()
}

/// HTTP_DATE is a time format like `Sun, 06 Nov 1994 08:49:37 GMT`.
const HTTP_DATE: &[Item<'static>] = &[
Item::Fixed(Fixed::ShortWeekdayName),
Item::Literal(", "),
Item::Numeric(Numeric::Day, Pad::Zero),
Item::Literal(" "),
Item::Fixed(Fixed::ShortMonthName),
Item::Literal(" "),
Item::Numeric(Numeric::Year, Pad::Zero),
Item::Literal(" "),
Item::Numeric(Numeric::Hour, Pad::Zero),
Item::Literal(":"),
Item::Numeric(Numeric::Minute, Pad::Zero),
Item::Literal(":"),
Item::Numeric(Numeric::Second, Pad::Zero),
Item::Literal(" GMT"),
];

/// Format time into http date: `Sun, 06 Nov 1994 08:49:37 GMT`
///
/// ## Note
Expand All @@ -89,12 +47,12 @@ const HTTP_DATE: &[Item<'static>] = &[
/// - Timezone is fixed to GMT.
/// - Day must be 2 digit.
pub fn format_http_date(t: DateTime) -> String {
t.format_with_items(HTTP_DATE.iter()).to_string()
t.strftime("%a, %d %b %Y %T GMT").to_string()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use an explicit RFC 9110 printer instead?

Note that I can't immediately spot anything obviously wrong with what you have here. Probably the dedicated printer is faster, but I'm not sure if that matters given the String alloc here anyway.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know. Updating.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a closer look, I'd prefer the current consistent usage of strftime as it's correct.

Would you elaborate a bit on how switching to the RFC 9110 printer can benefit?

@tisonkun tisonkun Sep 25, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the dedicated printer is faster, but I'm not sure if that matters given the String alloc here anyway

Yeah .. I'd prefer code consistency for understandability unless a break has a strong reason.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly perf. And I think it makes the code clearer to use a dedicated printer. But that's your call.

}

/// Format time into RFC3339: `2022-03-13T07:20:04Z`
pub fn format_rfc3339(t: DateTime) -> String {
t.to_rfc3339_opts(SecondsFormat::Secs, true)
t.strftime("%FT%TZ").to_string()
}

/// Parse time from RFC3339.
Expand All @@ -105,21 +63,30 @@ pub fn format_rfc3339(t: DateTime) -> String {
/// - `2022-03-01T08:12:34+00:00`
/// - `2022-03-01T08:12:34.00+00:00`
pub fn parse_rfc3339(s: &str) -> crate::Result<DateTime> {
Comment thread
tisonkun marked this conversation as resolved.
Outdated
Ok(chrono::DateTime::parse_from_rfc3339(s)
.map_err(|err| {
Error::unexpected(format!("parse '{s}' into rfc3339 failed")).with_source(err)
})?
.with_timezone(&Utc))
FromStr::from_str(s).map_err(|err| {
Comment thread
tisonkun marked this conversation as resolved.
Outdated
Error::unexpected(format!("parse '{s}' into rfc3339 failed")).with_source(err)
})
}

/// Parse time from RFC2822.
///
/// All of them are valid time:
///
/// - `Sat, 13 Jul 2024 15:09:59 -0400`
/// - `Mon, 15 Aug 2022 16:50:12 GMT`
pub fn parse_rfc2822(s: &str) -> crate::Result<DateTime> {
let zoned = jiff::fmt::rfc2822::parse(s).map_err(|err| {
Error::unexpected(format!("parse '{s}' into rfc2822 failed")).with_source(err)
})?;
Ok(zoned.timestamp())
}

#[cfg(test)]
mod tests {
use chrono::TimeZone;

use super::*;

fn test_time() -> DateTime {
Utc.with_ymd_and_hms(2022, 3, 1, 8, 12, 34).unwrap()
"2022-03-01T08:12:34Z".parse().unwrap()
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion licenserc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@

headerPath = "Apache-2.0-ASF.txt"

includes = ['**/*.rs', '**/*.yml', '**/*.yaml', '**/*.toml']
includes = ['**/*.rs', '**/*.yml', '**/*.yaml', '**/*.toml']
2 changes: 1 addition & 1 deletion services/aliyun-oss/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ repository.workspace = true
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
http.workspace = true
jiff.workspace = true
log.workspace = true
once_cell.workspace = true
percent-encoding.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion services/aliyun-oss/src/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl SigningCredential for Credential {
// Take 120s as buffer to avoid edge cases.
if let Some(valid) = self
.expires_in
.map(|v| v > now() + chrono::TimeDelta::try_minutes(2).expect("in bounds"))
.map(|v| v > now() + jiff::SignedDuration::from_mins(2))
{
return valid;
}
Expand Down
8 changes: 4 additions & 4 deletions services/aliyun-oss/src/sign_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ impl RequestSigner {
expires: Duration,
) -> Result<()> {
let expiration_time = signing_time
+ chrono::TimeDelta::from_std(expires).map_err(|e| {
+ jiff::SignedDuration::try_from(expires).map_err(|e| {
reqsign_core::Error::request_invalid(format!("Invalid expiration duration: {e}"))
})?;
let string_to_sign = self.build_string_to_sign(req, cred, signing_time, Some(expires))?;
Expand All @@ -154,7 +154,7 @@ impl RequestSigner {
query_pairs.push(("OSSAccessKeyId".to_string(), cred.access_key_id.clone()));
query_pairs.push((
"Expires".to_string(),
expiration_time.timestamp().to_string(),
expiration_time.as_second().to_string(),
));
query_pairs.push((
"Signature".to_string(),
Expand Down Expand Up @@ -229,12 +229,12 @@ impl RequestSigner {
match expires {
Some(expires_duration) => {
let expiration_time = signing_time
+ chrono::TimeDelta::from_std(expires_duration).map_err(|e| {
+ jiff::SignedDuration::try_from(expires_duration).map_err(|e| {
reqsign_core::Error::request_invalid(format!(
"Invalid expiration duration: {e}"
))
})?;
writeln!(&mut s, "{}", expiration_time.timestamp())?;
writeln!(&mut s, "{}", expiration_time.as_second())?;
}
None => {
writeln!(&mut s, "{}", format_http_date(signing_time))?;
Expand Down
2 changes: 1 addition & 1 deletion services/aws-v4/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ name = "aws"
anyhow.workspace = true
async-trait.workspace = true
bytes = "1.7.2"
chrono.workspace = true
form_urlencoded.workspace = true
http.workspace = true
jiff.workspace = true
log.workspace = true
percent-encoding.workspace = true
quick-xml.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion services/aws-v4/src/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl SigningCredential for Credential {
// Take 120s as buffer to avoid edge cases.
if let Some(valid) = self
.expires_in
.map(|v| v > now() + chrono::TimeDelta::try_minutes(2).expect("in bounds"))
.map(|v| v > now() + jiff::SignedDuration::from_mins(2))
{
return valid;
}
Expand Down
6 changes: 3 additions & 3 deletions services/aws-v4/src/provide_credential/cognito.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

use crate::Credential;
use async_trait::async_trait;
use chrono::DateTime;
use http::{Method, Request, StatusCode};
use log::debug;
use reqsign_core::time::DateTime;
use reqsign_core::{Context, Error, ProvideCredential, Result};
use serde::Deserialize;
use serde_json::json;
Expand Down Expand Up @@ -224,8 +224,8 @@ impl CognitoIdentityCredentialProvider {
.map_err(|e| Error::unexpected(format!("failed to parse credentials response: {e}")))?;

let creds = result.credentials;
let expires_in = DateTime::from_timestamp(creds.expiration, 0)
.ok_or_else(|| Error::unexpected("invalid expiration timestamp".to_string()))?;
let expires_in = DateTime::new(creds.expiration, 0)
Comment thread
tisonkun marked this conversation as resolved.
Outdated
.map_err(|e| Error::unexpected(format!("invalid expiration date: {e}")))?;

Ok(Credential {
access_key_id: creds.access_key_id,
Expand Down
4 changes: 2 additions & 2 deletions services/aws-v4/src/provide_credential/imds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ impl IMDSv2CredentialProvider {
}
let ec2_token = resp.into_body();
// Set expires_in to 10 minutes to enforce re-read.
let expires_in = now() + chrono::TimeDelta::try_seconds(21600).expect("in bounds")
- chrono::TimeDelta::try_seconds(600).expect("in bounds");
let expires_in =
now() + jiff::SignedDuration::from_secs(21600) - jiff::SignedDuration::from_secs(600);

{
*self.token.lock().expect("lock poisoned") = (ec2_token.clone(), expires_in);
Expand Down
4 changes: 2 additions & 2 deletions services/aws-v4/src/provide_credential/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

use crate::Credential;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use ini::Ini;
use log::debug;
use reqsign_core::time::DateTime;
use reqsign_core::{Context, Error, ProvideCredential, Result};
use serde::Deserialize;

Expand Down Expand Up @@ -213,7 +213,7 @@ impl ProvideCredential for ProcessCredentialProvider {

let expires_in =
if let Some(exp_str) = &output.expiration {
Some(exp_str.parse::<DateTime<Utc>>().map_err(|e| {
Some(exp_str.parse::<DateTime>().map_err(|e| {
Error::unexpected(format!("failed to parse expiration time: {e}"))
})?)
} else {
Expand Down
16 changes: 8 additions & 8 deletions services/aws-v4/src/provide_credential/s3_express_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use async_trait::async_trait;
use bytes::Bytes;
use http::{header, Method, Request};
use log::debug;
use reqsign_core::time::parse_rfc3339;
use reqsign_core::{Context, Error, ProvideCredential, Result, SignRequest};
use serde::Deserialize;

Expand Down Expand Up @@ -160,21 +161,20 @@ impl S3ExpressSessionProvider {

// Parse expiration time from ISO8601 format
let expiration =
chrono::DateTime::parse_from_rfc3339(&create_session_resp.credentials.expiration)
.map_err(|e| {
Error::unexpected(format!(
"Failed to parse expiration time '{}': {e}",
create_session_resp.credentials.expiration
))
})?;
parse_rfc3339(&create_session_resp.credentials.expiration).map_err(|e| {
Error::unexpected(format!(
"Failed to parse expiration time '{}': {e}",
create_session_resp.credentials.expiration
))
})?;

// Convert to Credential with expiration time
let creds = create_session_resp.credentials;
Ok(Credential {
access_key_id: creds.access_key_id,
secret_access_key: creds.secret_access_key,
session_token: Some(creds.session_token),
expires_in: Some(expiration.into()),
expires_in: Some(expiration),
})
}

Expand Down
10 changes: 5 additions & 5 deletions services/aws-v4/src/provide_credential/sso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@

use crate::Credential;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use http::{Method, Request, StatusCode};
use ini::Ini;
use log::{debug, warn};
use reqsign_core::time::{now, DateTime};
use reqsign_core::{Context, Error, ProvideCredential, Result};
use serde::Deserialize;

Expand Down Expand Up @@ -218,11 +218,11 @@ impl SSOCredentialProvider {
})?;

// Check if token is expired
let expires_at: DateTime<Utc> = token.expires_at.parse().map_err(|e| {
let expires_at = token.expires_at.parse::<DateTime>().map_err(|e| {
Comment thread
tisonkun marked this conversation as resolved.
Outdated
Error::unexpected(format!("failed to parse expiration time: {e}"))
})?;

if expires_at <= Utc::now() {
if expires_at <= now() {
warn!("SSO token is expired");
return Ok(None);
}
Expand Down Expand Up @@ -286,8 +286,8 @@ impl SSOCredentialProvider {
.map_err(|e| Error::unexpected(format!("failed to parse SSO credentials: {e}")))?;

let role_creds = creds.role_credentials;
let expires_in = DateTime::from_timestamp_millis(role_creds.expiration)
.ok_or_else(|| Error::unexpected("invalid expiration timestamp".to_string()))?;
let expires_in = DateTime::from_millisecond(role_creds.expiration)
.map_err(|e| Error::unexpected(format!("invalid expiration timestamp: {e}")))?;

Ok(Credential {
access_key_id: role_creds.access_key_id,
Expand Down
2 changes: 1 addition & 1 deletion services/azure-storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
bytes.workspace = true
chrono.workspace = true
form_urlencoded.workspace = true
http.workspace = true
jiff.workspace = true
log.workspace = true
percent-encoding.workspace = true
reqsign-core.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion services/azure-storage/src/account_sas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ mod tests {
#[test]
fn test_can_generate_sas_token() {
let key = hash::base64_encode("key".as_bytes());
let expiry = test_time() + chrono::TimeDelta::try_minutes(5).expect("in bounds");
let expiry = test_time() + jiff::SignedDuration::from_mins(5);
let sign = AccountSharedAccessSignature::new("account".to_string(), key, expiry);
let token_content = sign.token().expect("token decode failed");
let token = token_content
Expand Down
2 changes: 1 addition & 1 deletion services/azure-storage/src/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ impl SigningCredential for Credential {
}
// Check expiration for bearer tokens (take 20s as buffer to avoid edge cases)
if let Some(expires) = expires_in {
*expires > now() + chrono::TimeDelta::try_seconds(20).expect("in bounds")
*expires > now() + jiff::SignedDuration::from_secs(20)
} else {
true
}
Expand Down
Loading
Loading