use std::future::Future;
use std::sync::Arc;
use axum_core::body::Body;
use http::Request;
use r402_extensions::siwx::{
EvmVerifier, PaidAddressStore, SIWX_KEY, SiwxChain, SiwxError, SiwxExtension, SiwxOrigin,
SiwxProof, SiwxProofError,
};
use r402_protocol::payment::ExtensionEntry;
use time::OffsetDateTime;
use super::hooks::{GateHooks, ProtectedRequestOutcome};
use crate::headers::SIGN_IN_WITH_X;
pub struct SiwxGate {
extension: SiwxExtension,
store: Arc<dyn PaidAddressStore>,
auth_only: bool,
evm: EvmVerifier,
}
impl Clone for SiwxGate {
#[cfg_attr(
not(feature = "siwx-eip1271"),
allow(
clippy::clone_on_copy,
reason = "EvmVerifier is Copy only without eip1271"
)
)]
fn clone(&self) -> Self {
Self {
extension: self.extension.clone(),
store: Arc::clone(&self.store),
auth_only: self.auth_only,
evm: self.evm.clone(),
}
}
}
impl std::fmt::Debug for SiwxGate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SiwxGate")
.field("origin", self.extension.origin())
.field("auth_only", &self.auth_only)
.finish_non_exhaustive()
}
}
impl SiwxGate {
#[must_use]
pub fn new(origin: SiwxOrigin, store: impl PaidAddressStore + 'static) -> Self {
Self {
extension: SiwxExtension::new(origin),
store: Arc::new(store),
auth_only: false,
evm: EvmVerifier::new(),
}
}
#[must_use]
pub fn with_chain(mut self, chain: SiwxChain) -> Self {
self.extension = self.extension.with_chain(chain);
self
}
#[must_use]
pub fn with_statement(mut self, statement: impl Into<compact_str::CompactString>) -> Self {
self.extension = self.extension.with_statement(statement);
self
}
#[must_use]
pub const fn with_auth_only(mut self) -> Self {
self.auth_only = true;
self
}
#[cfg(feature = "siwx-eip1271")]
#[must_use]
pub fn with_evm_rpc_map(
mut self,
map: impl IntoIterator<Item = (u64, impl Into<String>)>,
timeout: Option<std::time::Duration>,
) -> Self {
let mut evm = EvmVerifier::with_rpc_map(map);
if let Some(timeout) = timeout {
evm = evm.with_rpc_timeout(timeout);
}
self.evm = evm;
self
}
#[must_use]
pub const fn is_auth_only(&self) -> bool {
self.auth_only
}
#[must_use]
pub const fn origin(&self) -> &SiwxOrigin {
self.extension.origin()
}
#[must_use]
pub fn store(&self) -> &dyn PaidAddressStore {
self.store.as_ref()
}
pub fn challenge_entry(&self, path: &str) -> Result<ExtensionEntry, SiwxError> {
self.extension.challenge_now(path)
}
pub fn record_success(&self, path: &str, address: &str) {
let key = self.extension.origin().store_key(path);
self.store.record_success(&key, address);
}
#[must_use]
pub const fn key() -> &'static str {
SIWX_KEY
}
pub(crate) async fn try_grant(&self, header: &str, path: &str) -> bool {
let proof = match SiwxProof::parse_header(header) {
Ok(proof) => proof,
Err(err) => return log_parse_denied(err),
};
if let Err(err) = proof
.verify_at(
self.extension.origin(),
path,
OffsetDateTime::now_utc(),
&self.evm,
)
.await
{
return log_denied(err);
}
let key = self.extension.origin().store_key(path);
if !(self.auth_only || self.store.contains(&key, &proof.address)) {
return log_valid_unpaid();
}
if !self.store.consume_nonce(&proof.nonce) {
return log_nonce_replay();
}
log_granted()
}
}
fn log_parse_denied(err: SiwxProofError) -> bool {
tracing::debug!(error = ?err, "siwx parse denied");
false
}
fn log_denied(err: SiwxError) -> bool {
tracing::debug!(code = err.as_str(), "siwx denied");
false
}
fn log_valid_unpaid() -> bool {
tracing::debug!("siwx valid unpaid");
false
}
fn log_nonce_replay() -> bool {
tracing::debug!("siwx nonce replay");
false
}
fn log_granted() -> bool {
tracing::debug!("siwx granted");
true
}
impl GateHooks for SiwxGate {
fn on_protected_request<'a>(
&'a self,
req: &'a Request<Body>,
) -> impl Future<Output = ProtectedRequestOutcome> + Send + 'a {
let header = req
.headers()
.get(SIGN_IN_WITH_X)
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let path = req.uri().path().to_owned();
async move {
let Some(header) = header else {
return ProtectedRequestOutcome::Continue;
};
if self.try_grant(&header, &path).await {
ProtectedRequestOutcome::GrantAccess
} else {
ProtectedRequestOutcome::Continue
}
}
}
}