LoVisual/backend/gateway/src/rate_limit/rules.rs
loki5512344 44353e893c
feat: mod platform integration (showcase, cloud slots, cloud screen, server-side unlink)
Mod:
- %config slots/pull/publish/unpublish for the four cloud slots
- %showcase [new|popular] [page] | load | copy, with number references
- %cloud opens a CloudScreen (link/unlink, slots, showcase) on vanilla widgets
- %config load IDDQD works offline (everything off except ChinaHat)
- default backend URL is now the production gateway
- shared ConfigRemoteApplier and ClientThread replace per-class copies
- %link unlink revokes the link on the server, then clears the local token

Backend:
- POST /device/revoke (RFC 7009 style self-revoke, always 204), rate limited
  to 10/min per IP at the gateway

CloudScreen is compiled but has not been opened in a running client yet.
2026-10-02 09:23:44 +02:00

92 lines
2.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use axum::http::Method;
use governor::Quota;
use std::num::NonZeroU32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyBy {
Ip,
Account,
}
pub struct Rule {
pub method: Method,
/// Exact path, or a prefix when it ends with '*'.
pub pattern: &'static str,
pub quota: Quota,
pub key_by: KeyBy,
}
impl Rule {
pub fn matches(&self, method: &Method, path: &str) -> bool {
if self.method != *method {
return false;
}
match self.pattern.strip_suffix('*') {
Some(prefix) => path.starts_with(prefix),
None => path == self.pattern,
}
}
}
fn n(v: u32) -> NonZeroU32 {
NonZeroU32::new(v).expect("rate-limit constants are non-zero")
}
fn rule(method: Method, pattern: &'static str, quota: Quota, key_by: KeyBy) -> Rule {
Rule {
method,
pattern,
quota,
key_by,
}
}
pub fn rules() -> Vec<Rule> {
use KeyBy::*;
vec![
rule(Method::POST, "/auth/login", Quota::per_minute(n(5)), Ip),
rule(Method::POST, "/auth/register", Quota::per_hour(n(3)), Ip),
rule(Method::POST, "/auth/refresh", Quota::per_minute(n(30)), Ip),
rule(Method::POST, "/device/code", Quota::per_minute(n(10)), Ip),
// The mod polls every 2–3 s for up to 10 min: 10/min would break linking.
rule(Method::POST, "/device/token", Quota::per_minute(n(30)), Ip),
rule(Method::POST, "/device/revoke", Quota::per_minute(n(10)), Ip),
rule(Method::PUT, "/configs/*", Quota::per_minute(n(20)), Account),
rule(
Method::GET,
"/configs/shared/*",
Quota::per_minute(n(30)),
Ip,
),
rule(Method::POST, "/avatars", Quota::per_hour(n(5)), Account),
rule(Method::GET, "/showcase*", Quota::per_minute(n(60)), Ip),
]
}
pub fn global_quota() -> Quota {
Quota::per_minute(n(300))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_and_prefix_matching() {
let all = rules();
let find = |m: Method, p: &str| all.iter().position(|r| r.matches(&m, p));
assert_eq!(find(Method::POST, "/auth/login"), Some(0));
assert_eq!(find(Method::GET, "/auth/login"), None);
assert_eq!(find(Method::POST, "/auth/login/x"), None);
assert!(find(Method::PUT, "/configs/3").is_some());
assert!(find(Method::GET, "/configs/shared/ABCD").is_some());
assert!(find(Method::GET, "/showcase").is_some());
assert!(
find(
Method::GET,
"/showcase/11111111-1111-1111-1111-111111111111"
)
.is_some()
);
}
}