use crate::settings;
use postrust_auth::jwks::{
FetchError, FetchResponse, JwksStore, StoredJwks, FETCH_TIMEOUT, MAX_JWKS_BYTES,
};
use std::time::Duration;
use worker::async_trait::async_trait;
pub(crate) struct WorkerFetch;
fn failed(e: worker::Error) -> FetchError {
FetchError::new(e.to_string())
}
async fn exchange(url: &str, signal: &worker::AbortSignal) -> Result<FetchResponse, FetchError> {
use futures::StreamExt;
let headers = worker::Headers::new();
headers
.set("accept", "application/jwk-set+json, application/json")
.map_err(failed)?;
let mut init = worker::RequestInit::new();
init.with_redirect(worker::RequestRedirect::Manual)
.with_headers(headers);
let request = worker::Request::new_with_init(url, &init).map_err(failed)?;
let mut response = worker::Fetch::Request(request)
.send_with_signal(signal)
.await
.map_err(failed)?;
let status = response.status_code();
let header = |name: &str| response.headers().get(name).ok().flatten();
let location = header("location");
let cache_control = header("cache-control");
let too_large = || {
FetchError::new(format!(
"the response is larger than {MAX_JWKS_BYTES} bytes"
))
};
let mut body = Vec::new();
if status == 200 {
if header("content-length")
.and_then(|length| length.parse::<u64>().ok())
.is_some_and(|length| length > MAX_JWKS_BYTES as u64)
{
return Err(too_large());
}
let mut stream = response.stream().map_err(failed)?;
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(failed)?;
if body.len() + chunk.len() > MAX_JWKS_BYTES {
return Err(too_large());
}
body.extend_from_slice(&chunk);
}
}
Ok(FetchResponse::new(status, body)
.with_location(location)
.with_cache_control(cache_control))
}
#[async_trait(?Send)]
impl postrust_auth::jwks::Fetch for WorkerFetch {
async fn get(&self, url: &str) -> Result<FetchResponse, FetchError> {
let controller = worker::AbortController::default();
let signal = controller.signal();
let fetch = std::pin::pin!(exchange(url, &signal));
let timeout = worker::Delay::from(FETCH_TIMEOUT);
match futures::future::select(fetch, timeout).await {
futures::future::Either::Left((outcome, _)) => outcome,
futures::future::Either::Right(_) => {
controller.abort();
Err(FetchError::new(format!(
"no answer within {} s",
FETCH_TIMEOUT.as_secs()
)))
}
}
}
}
pub(crate) struct KvJwks(pub(crate) worker::KvStore);
#[async_trait(?Send)]
impl JwksStore for KvJwks {
async fn load(&self, url: &str) -> Option<StoredJwks> {
match self.0.get(&settings::jwks_key(url)).text().await {
Ok(Some(text)) => serde_json::from_str(&text).ok(),
Ok(None) => None,
Err(e) => {
worker::console_warn!("postrust: the JWKS in KV could not be read: {e}");
None
}
}
}
async fn save(&self, url: &str, jwks: &StoredJwks, ttl: Duration) {
let written = match serde_json::to_string(jwks) {
Ok(json) => match self.0.put(&settings::jwks_key(url), json) {
Ok(put) => put
.expiration_ttl(settings::kv_expiration_secs(ttl.as_secs_f64()))
.execute()
.await
.map_err(|e| e.to_string()),
Err(e) => Err(e.to_string()),
},
Err(e) => Err(e.to_string()),
};
if let Err(e) = written {
worker::console_error!("postrust: could not write the JWKS to KV: {e}");
}
}
}