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.
92 lines
2.6 KiB
Rust
92 lines
2.6 KiB
Rust
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()
|
||
);
|
||
}
|
||
}
|