Skip to main content

cloudillo_core/rate_limit/
api.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! Rate Limiting Internal API
5//!
6//! Traits and types for programmatic rate limit management.
7
8use std::net::IpAddr;
9use std::time::{Duration, Instant};
10
11use super::error::PowError;
12use super::extractors::AddressKey;
13use crate::prelude::*;
14
15/// Current status of rate limiting for an address at a specific level
16#[derive(Debug, Clone)]
17pub struct RateLimitStatus {
18	/// Whether this address is currently rate limited
19	pub is_limited: bool,
20	/// Remaining requests before limit kicks in (if not limited)
21	pub remaining: Option<u32>,
22	/// When the limit will reset (if limited)
23	pub reset_at: Option<Instant>,
24	/// Total quota for this period
25	pub quota: u32,
26	/// Whether address is currently banned
27	pub is_banned: bool,
28	/// When ban expires (if banned)
29	pub ban_expires_at: Option<Instant>,
30}
31
32/// Reason for a penalty
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum PenaltyReason {
35	/// Failed authentication attempt
36	AuthFailure,
37	/// Invalid action token
38	TokenVerificationFailure,
39	/// Suspicious request pattern
40	SuspiciousActivity,
41	/// Rate limit exceeded multiple times
42	RepeatedViolation,
43}
44
45impl PenaltyReason {
46	/// Get the number of failures before auto-ban for this reason
47	pub fn failures_to_ban(&self) -> u32 {
48		match self {
49			PenaltyReason::AuthFailure => 5,
50			PenaltyReason::TokenVerificationFailure => 3,
51			PenaltyReason::SuspiciousActivity => 2,
52			PenaltyReason::RepeatedViolation => 1,
53		}
54	}
55
56	/// Get the default ban duration for this reason
57	pub fn ban_duration(&self) -> Duration {
58		match self {
59			// 1 hour
60			PenaltyReason::AuthFailure | PenaltyReason::TokenVerificationFailure => {
61				Duration::from_secs(3600)
62			}
63			PenaltyReason::SuspiciousActivity => Duration::from_secs(7200), // 2 hours
64			PenaltyReason::RepeatedViolation => Duration::from_secs(86400), // 24 hours
65		}
66	}
67}
68
69/// Reason for incrementing the PoW counter
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum PowPenaltyReason {
72	/// CONN action failed signature verification
73	ConnSignatureFailure,
74	/// CONN received while another pending from same issuer
75	ConnDuplicatePending,
76	/// CONN action was rejected by user or policy
77	ConnRejected,
78	/// CONN action failed PoW check (insufficient proof of work)
79	ConnPowCheckFailed,
80}
81
82impl PowPenaltyReason {
83	/// Whether this reason should affect network-level counter too
84	pub fn affects_network(&self) -> bool {
85		match self {
86			PowPenaltyReason::ConnRejected => false, // Individual only
87			// Network-affecting reasons
88			PowPenaltyReason::ConnSignatureFailure
89			| PowPenaltyReason::ConnDuplicatePending
90			| PowPenaltyReason::ConnPowCheckFailed => true,
91		}
92	}
93}
94
95/// Ban entry stored in the ban list
96#[derive(Debug, Clone)]
97pub struct BanEntry {
98	/// Address key that is banned
99	pub key: AddressKey,
100	/// Reason for the ban
101	pub reason: PenaltyReason,
102	/// When the ban was created
103	pub created_at: Instant,
104	/// When the ban expires (None = permanent)
105	pub expires_at: Option<Instant>,
106}
107
108impl BanEntry {
109	/// Check if this ban has expired
110	pub fn is_expired(&self) -> bool {
111		self.expires_at.is_some_and(|exp| Instant::now() >= exp)
112	}
113
114	/// Get remaining duration until ban expires
115	pub fn remaining_duration(&self) -> Option<Duration> {
116		self.expires_at.map(|exp| {
117			let now = Instant::now();
118			if now >= exp {
119				Duration::ZERO
120			} else {
121				exp - now
122			}
123		})
124	}
125}
126
127/// Statistics about the rate limiter
128#[derive(Debug, Clone, Default)]
129pub struct RateLimiterStats {
130	/// Number of tracked addresses
131	pub tracked_addresses: usize,
132	/// Number of active bans
133	pub active_bans: usize,
134	/// Total requests that were rate limited
135	pub total_requests_limited: u64,
136	/// Total bans issued
137	pub total_bans_issued: u64,
138	/// Current PoW counter entries (individual level)
139	pub pow_individual_entries: usize,
140	/// Current PoW counter entries (network level)
141	pub pow_network_entries: usize,
142}
143
144/// Internal API for programmatic rate limit management
145pub trait RateLimitApi: Send + Sync {
146	/// Query current limit status for an address at all hierarchical levels
147	fn get_status(
148		&self,
149		addr: &IpAddr,
150		category: &str,
151	) -> ClResult<Vec<(AddressKey, RateLimitStatus)>>;
152
153	/// Manually consume quota (increase usage) - e.g., after auth failure
154	fn penalize(&self, addr: &IpAddr, reason: PenaltyReason, amount: u32) -> ClResult<()>;
155
156	/// Decrease penalty count (grant extra quota) - e.g., after successful CAPTCHA
157	fn grant(&self, addr: &IpAddr, amount: u32) -> ClResult<()>;
158
159	/// Reset limits for an address at all levels
160	fn reset(&self, addr: &IpAddr) -> ClResult<()>;
161
162	/// Temporarily ban an address (all hierarchical levels)
163	fn ban(&self, addr: &IpAddr, duration: Duration, reason: PenaltyReason) -> ClResult<()>;
164
165	/// Unban an address
166	fn unban(&self, addr: &IpAddr) -> ClResult<()>;
167
168	/// Check if an address is banned
169	fn is_banned(&self, addr: &IpAddr) -> bool;
170
171	/// List all currently banned addresses
172	fn list_bans(&self) -> Vec<BanEntry>;
173
174	/// Get statistics about rate limiter state
175	fn stats(&self) -> RateLimiterStats;
176
177	// === Proof-of-Work Counter API ===
178
179	/// Get current PoW requirement for address (max of individual + network level)
180	fn get_pow_requirement(&self, addr: &IpAddr) -> u32;
181
182	/// Increment PoW counter for address
183	fn increment_pow_counter(&self, addr: &IpAddr, reason: PowPenaltyReason) -> ClResult<()>;
184
185	/// Decrement PoW counter (after successful CONN, with decay over time)
186	fn decrement_pow_counter(&self, addr: &IpAddr, amount: u32) -> ClResult<()>;
187
188	/// Verify proof-of-work on action token
189	fn verify_pow(&self, addr: &IpAddr, token: &str) -> Result<(), PowError>;
190}
191
192// vim: ts=4