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			PenaltyReason::AuthFailure | PenaltyReason::TokenVerificationFailure => {
60				Duration::from_hours(1)
61			}
62			PenaltyReason::SuspiciousActivity => Duration::from_hours(2),
63			PenaltyReason::RepeatedViolation => Duration::from_hours(24),
64		}
65	}
66}
67
68/// Reason for incrementing the PoW counter
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum PowPenaltyReason {
71	/// CONN action failed signature verification
72	ConnSignatureFailure,
73	/// CONN received while another pending from same issuer
74	ConnDuplicatePending,
75	/// CONN action was rejected by user or policy
76	ConnRejected,
77	/// CONN action failed PoW check (insufficient proof of work)
78	ConnPowCheckFailed,
79}
80
81impl PowPenaltyReason {
82	/// Whether this reason should affect network-level counter too
83	pub fn affects_network(&self) -> bool {
84		match self {
85			PowPenaltyReason::ConnRejected => false, // Individual only
86			// Network-affecting reasons
87			PowPenaltyReason::ConnSignatureFailure
88			| PowPenaltyReason::ConnDuplicatePending
89			| PowPenaltyReason::ConnPowCheckFailed => true,
90		}
91	}
92}
93
94/// Ban entry stored in the ban list
95#[derive(Debug, Clone)]
96pub struct BanEntry {
97	/// Address key that is banned
98	pub key: AddressKey,
99	/// Reason for the ban
100	pub reason: PenaltyReason,
101	/// When the ban was created
102	pub created_at: Instant,
103	/// When the ban expires (None = permanent)
104	pub expires_at: Option<Instant>,
105}
106
107impl BanEntry {
108	/// Check if this ban has expired
109	pub fn is_expired(&self) -> bool {
110		self.expires_at.is_some_and(|exp| Instant::now() >= exp)
111	}
112
113	/// Get remaining duration until ban expires
114	pub fn remaining_duration(&self) -> Option<Duration> {
115		self.expires_at.map(|exp| {
116			let now = Instant::now();
117			if now >= exp { Duration::ZERO } else { exp - now }
118		})
119	}
120}
121
122/// Statistics about the rate limiter
123#[derive(Debug, Clone, Default)]
124pub struct RateLimiterStats {
125	/// Number of tracked addresses
126	pub tracked_addresses: usize,
127	/// Number of active bans
128	pub active_bans: usize,
129	/// Total requests that were rate limited
130	pub total_requests_limited: u64,
131	/// Total bans issued
132	pub total_bans_issued: u64,
133	/// Current PoW counter entries (individual level)
134	pub pow_individual_entries: usize,
135	/// Current PoW counter entries (network level)
136	pub pow_network_entries: usize,
137}
138
139/// Internal API for programmatic rate limit management
140pub trait RateLimitApi: Send + Sync {
141	/// Query current limit status for an address at all hierarchical levels
142	fn get_status(
143		&self,
144		addr: &IpAddr,
145		category: &str,
146	) -> ClResult<Vec<(AddressKey, RateLimitStatus)>>;
147
148	/// Manually consume quota (increase usage) - e.g., after auth failure
149	fn penalize(&self, addr: &IpAddr, reason: PenaltyReason, amount: u32) -> ClResult<()>;
150
151	/// Decrease penalty count (grant extra quota) - e.g., after successful CAPTCHA
152	fn grant(&self, addr: &IpAddr, amount: u32) -> ClResult<()>;
153
154	/// Reset limits for an address at all levels
155	fn reset(&self, addr: &IpAddr) -> ClResult<()>;
156
157	/// Temporarily ban an address (all hierarchical levels)
158	fn ban(&self, addr: &IpAddr, duration: Duration, reason: PenaltyReason) -> ClResult<()>;
159
160	/// Unban an address
161	fn unban(&self, addr: &IpAddr) -> ClResult<()>;
162
163	/// Check if an address is banned
164	fn is_banned(&self, addr: &IpAddr) -> bool;
165
166	/// List all currently banned addresses
167	fn list_bans(&self) -> Vec<BanEntry>;
168
169	/// Get statistics about rate limiter state
170	fn stats(&self) -> RateLimiterStats;
171
172	// === Proof-of-Work Counter API ===
173
174	/// Get current PoW requirement for address (max of individual + network level)
175	fn get_pow_requirement(&self, addr: &IpAddr) -> u32;
176
177	/// Increment PoW counter for address
178	fn increment_pow_counter(&self, addr: &IpAddr, reason: PowPenaltyReason) -> ClResult<()>;
179
180	/// Decrement PoW counter (after successful CONN, with decay over time)
181	fn decrement_pow_counter(&self, addr: &IpAddr, amount: u32) -> ClResult<()>;
182
183	/// Verify proof-of-work on action token
184	fn verify_pow(&self, addr: &IpAddr, token: &str) -> Result<(), PowError>;
185}
186
187// vim: ts=4