Skip to content

Commit

Permalink
refactor(jans-cedarling): replace std::time usage with chrono
Browse files Browse the repository at this point in the history
- replace std::time using with the chrono crate to improve WASM
  compalibility

Signed-off-by: rmarinn <[email protected]>
  • Loading branch information
rmarinn committed Dec 4, 2024
1 parent 1e91d9b commit 3df6e1d
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 44 deletions.
4 changes: 3 additions & 1 deletion jans-cedarling/cedarling/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ base64 = "0.22.1"
url = "2.5.2"
lazy_static = "1.5.0"
jsonwebtoken = "9.3.0"
reqwest = { version = "0.12.8", features = ["blocking", "json"] }
reqwest = { version = "0.12.8", features = ["json"] }
bytes = "1.7.2"
typed-builder = "0.20.0"
semver = { version = "1.0.23", features = ["serde"] }
Expand All @@ -27,6 +27,8 @@ derive_more = { version = "1.0.0", features = [
] }
time = { version = "0.3.36", features = ["wasm-bindgen"] }
regex = "1.11.1"
chrono = "0.4.38"
tokio = { version = "1.42.0", features = ["rt", "time"] }

[dev-dependencies]
# is used in testing
Expand Down
111 changes: 80 additions & 31 deletions jans-cedarling/cedarling/src/jwt/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,63 @@
* Copyright (c) 2024, Gluu, Inc.
*/

use reqwest::blocking::Client;
use std::{thread::sleep, time::Duration};

/// A wrapper around `reqwest::blocking::Client` providing HTTP request functionality
use reqwest::Client;
use serde::de::DeserializeOwned;
use tokio::{
runtime::{Builder as RtBuilder, Runtime},
time::Duration,
};

/// A wrapper around [`reqwest::Client`] providing HTTP request functionality
/// with retry logic.
///
/// The `HttpClient` struct allows for sending GET requests with a retry mechanism
/// that attempts to fetch the requested resource up to a maximum number of times
/// if an error occurs.
#[derive(Debug)]
pub struct HttpClient {
client: reqwest::blocking::Client,
client: reqwest::Client,
max_retries: u32,
retry_delay: Duration,
rt: Runtime,
}

/// A wrapper around [`reqwest::Response`]
#[derive(Debug)]
pub struct Response<'rt> {
rt: &'rt Runtime,
resp: reqwest::Response,
}

impl Response<'_> {
/// Deserializes the response into <T> from JSON.
pub fn json<T>(self) -> Result<T, HttpClientError>
where
T: DeserializeOwned,
{
let resp_json = self
.rt
.block_on(async { self.resp.json::<T>().await })
.map_err(HttpClientError::DeserializeJson)?;
Ok(resp_json)
}

/// Deserializes the response into a String.
pub fn text(self) -> Result<String, HttpClientError> {
let resp_text = self
.rt
.block_on(async { self.resp.text().await })
.map_err(HttpClientError::DeserializeJson)?;
Ok(resp_text)
}
}

impl HttpClient {
pub fn new(max_retries: u32, retry_delay: Duration) -> Result<Self, HttpClientError> {
let rt = RtBuilder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
let client = Client::builder()
.build()
.map_err(HttpClientError::Initialization)?;
Expand All @@ -31,38 +70,43 @@ impl HttpClient {
client,
max_retries,
retry_delay,
rt,
})
}

/// Sends a GET request to the specified URI with retry logic.
///
/// This method will attempt to fetch the resource up to 3 times, with an increasing delay
/// between each attempt.
pub fn get(&self, uri: &str) -> Result<reqwest::blocking::Response, HttpClientError> {
pub fn get(&self, uri: &str) -> Result<Response, HttpClientError> {
// Fetch the JWKS from the jwks_uri
let mut attempts = 0;
let response = loop {
match self.client.get(uri).send() {
// Exit loop on success
Ok(response) => break response,

Err(e) if attempts < self.max_retries => {
attempts += 1;
// TODO: pass this message to the logger
eprintln!(
"Request failed (attempt {} of {}): {}. Retrying...",
attempts, self.max_retries, e
);
sleep(self.retry_delay * attempts);
},
// Exit if max retries exceeded
Err(e) => return Err(HttpClientError::MaxHttpRetriesReached(e)),
let response = self.rt.block_on(async {
loop {
match self.client.get(uri).send().await {
// Exit loop on success
Ok(response) => return Ok(response),

Err(e) if attempts < self.max_retries => {
attempts += 1;
// TODO: pass this message to the logger
eprintln!(
"Request failed (attempt {} of {}): {}. Retrying...",
attempts, self.max_retries, e
);
tokio::time::sleep(self.retry_delay * attempts).await
},
// Exit if max retries exceeded
Err(e) => return Err(HttpClientError::MaxHttpRetriesReached(e)),
}
}
};
})?;

response
let resp = response
.error_for_status()
.map_err(HttpClientError::HttpStatus)
.map_err(HttpClientError::HttpStatus)?;

Ok(Response { rt: &self.rt, resp })
}
}

Expand All @@ -75,10 +119,15 @@ pub enum HttpClientError {
/// Indicates an HTTP error response received from an endpoint.
#[error("Received error HTTP status: {0}")]
HttpStatus(#[source] reqwest::Error),

/// Indicates a failure to reach the endpoint after 3 attempts.
#[error("Could not reach endpoint after trying 3 times: {0}")]
MaxHttpRetriesReached(#[source] reqwest::Error),
/// Indicates a failure to deserialize the http response into JSON.
#[error("Failed to deserialize response into JSON: {0}")]
DeserializeJson(#[source] reqwest::Error),
/// Indicates a failure to deserialize the http response into JSON.
#[error("Failed to deserialize response into a String: {0}")]
DeserializeText(#[source] reqwest::Error),
}

#[cfg(test)]
Expand All @@ -87,9 +136,9 @@ mod test {

use super::HttpClient;
use mockito::Server;
use serde_json::json;
use std::time::Duration;
use serde_json::{json, Value};
use test_utils::assert_eq;
use tokio::time::Duration;

#[test]
fn can_fetch() {
Expand All @@ -109,16 +158,16 @@ mod test {
.create();

let client =
HttpClient::new(3, Duration::from_millis(1)).expect("Should create HttpClient.");
HttpClient::new(3, Duration::from_millis(10)).expect("Should create HttpClient.");

let response = client
.get(&format!(
"{}/.well-known/openid-configuration",
mock_server.url()
))
.expect("Should get response")
.json::<serde_json::Value>()
.expect("Should deserialize JSON response.");
.json::<Value>()
.expect("Should deserialize response to JSON");

assert_eq!(
response, expected,
Expand Down
16 changes: 10 additions & 6 deletions jans-cedarling/cedarling/src/jwt/jwk_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,17 @@ impl JwkStore {
http_client: &HttpClient,
) -> Result<Self, JwkStoreError> {
// fetch openid configuration
let response = http_client.get(&issuer.openid_configuration_endpoint)?;
let response = http_client
.get(&issuer.openid_configuration_endpoint)
.map_err(JwkStoreError::FetchOpenIdConfig)?;
let openid_config = response
.json::<OpenIdConfig>()
.map_err(JwkStoreError::FetchOpenIdConfig)?;
.map_err(JwkStoreError::DeserializeOpenIdConfig)?;

// fetch jwks
let response = http_client.get(&openid_config.jwks_uri)?;

let jwks = response.text().map_err(JwkStoreError::FetchJwks)?;
let jwks = response.text().map_err(JwkStoreError::DeserializeJwks)?;

let mut store = Self::new_from_jwks_str(store_id, &jwks)?;
store.issuer = Some(openid_config.issuer.into());
Expand Down Expand Up @@ -204,9 +206,11 @@ impl JwkStore {
#[derive(thiserror::Error, Debug)]
pub enum JwkStoreError {
#[error("Failed to fetch OpenIdConfig remote server: {0}")]
FetchOpenIdConfig(#[source] reqwest::Error),
#[error("Failed to fetch JWKS from remote server: {0}")]
FetchJwks(#[source] reqwest::Error),
FetchOpenIdConfig(#[source] HttpClientError),
#[error("Failed to deserialize OpenIdConfig to JSON: {0}")]
DeserializeOpenIdConfig(#[source] HttpClientError),
#[error("Failed to fetch JWKS: {0}")]
DeserializeJwks(#[source] HttpClientError),
#[error("Failed to make HTTP Request: {0}")]
Http(#[from] HttpClientError),
#[error("Failed to create Decoding Key from JWK: {0}")]
Expand Down
11 changes: 5 additions & 6 deletions jans-cedarling/cedarling/src/log/log_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
//! # Log entry
//! The module contains structs for logging events.
use chrono::prelude::*;
use std::collections::HashSet;
use std::fmt::Display;

use std::time::{SystemTime, UNIX_EPOCH};

use uuid7::uuid7;
use uuid7::Uuid;

Expand Down Expand Up @@ -54,10 +53,10 @@ impl LogEntry {
application_id: Option<app_types::ApplicationName>,
log_kind: LogType,
) -> LogEntry {
let unix_time_sec = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
let unix_time_sec = Utc::now()
.timestamp()
.try_into()
.expect("Failed to convert timestamp: value might be negative");

Self {
// We use uuid v7 because it is generated based on the time and sortable.
Expand Down

0 comments on commit 3df6e1d

Please sign in to comment.