From 18509e6d1a17829c82d8271f2735a5b73e50f6b1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 27 Mar 2026 15:58:24 +0200 Subject: [PATCH] draft multi-list rules api --- README.md | 2 + src/main.rs | 60 ++++++++++++++------ src/rules.rs | 142 +++++++++++++++++++++++++++------------------- src/rules/item.rs | 5 ++ 4 files changed, 133 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index b434b13..e1b66ef 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ RUST_LOG=trace cargo run -- --allow=http://localhost/allow.txt \ * `/api/rules` - return active rules (from server memory) * `/api/lists` - get parsed lists with its ID * `/api/list/` - get all parsed rules for list ID (see `/api/lists`) + * `/api/list/enable/` - enable all parsed rules of given list ID (see `/api/lists`) + * `/api/list/disable/` - disable all parsed rules of given list ID (see `/api/lists`) ### Allow list example diff --git a/src/main.rs b/src/main.rs index fe71e00..b945436 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,12 +33,9 @@ async fn api_allow( totals: &State>, ) -> Result, Status> { let result = rules.allow(rule).await; - totals.set_entries(rules.total().await); + totals.set_entries(rules.total(true).await); // @TODO separate active/inactive totals info!("Delete `{rule}` from the in-memory rules (operation status: {result:?})"); - Ok(Json(result.map_err(|e| { - error!("Allow request handle error for `{rule}`: `{e}`"); - Status::InternalServerError - })?)) + Ok(Json(result)) } #[rocket::get("/api/block/")] @@ -46,14 +43,11 @@ async fn api_block( rule: &str, rules: &State>, totals: &State>, -) -> Result, Status> { +) -> Result, Status> { let result = rules.block(rule).await; - totals.set_entries(rules.total().await); + totals.set_entries(rules.total(true).await); // @TODO separate active/inactive totals info!("Add `{rule}` to the in-memory rules (operation status: {result:?})"); - Ok(Json(result.map_err(|e| { - error!("Block request handle error for `{rule}`: `{e}`"); - Status::InternalServerError - })?)) + Ok(Json(result)) } #[rocket::get("/api/rules")] @@ -65,7 +59,7 @@ async fn api_rules(rules: &State>) -> Result>, Statu #[rocket::get("/api/lists")] async fn api_lists(rules: &State>) -> Result>, Status> { - let lists = rules.lists(); + let lists = rules.lists().await; debug!("Get lists index (total: {})", lists.len()); Ok(Json(lists)) } @@ -74,15 +68,39 @@ async fn api_lists(rules: &State>) -> Result>, -) -> Result>, Status> { - let list = rules.list(&id); +) -> Result>, Status> { + let list = rules.list(&id).await; debug!( "Get list #{id} rules (total: {:?})", - list.map(|l| l.items.len()) + list.as_ref().map(|l| l.items.len()) ); Ok(Json(list)) } +#[rocket::get("/api/list/enable/")] +async fn api_list_enable( + id: usize, + rules: &State>, + totals: &State>, +) -> Result>, Status> { + let affected = rules.enable(&id, true).await; + totals.set_entries(rules.total(true).await); // @TODO separate active/inactive totals + info!("Enabled {affected:?} rules from the active rule set"); + Ok(Json(affected)) // @TODO handle empty result +} + +#[rocket::get("/api/list/disable/")] +async fn api_list_disable( + id: usize, + rules: &State>, + totals: &State>, +) -> Result>, Status> { + let affected = rules.enable(&id, false).await; + totals.set_entries(rules.total(true).await); // @TODO separate active/inactive totals + info!("Disabled {affected:?} rules from the active rule set"); + Ok(Json(affected)) // @TODO handle empty result +} + #[rocket::launch] async fn rocket() -> _ { env_logger::init(); @@ -91,7 +109,7 @@ async fn rocket() -> _ { let rules = Arc::new(Rules::from_opt(&opt.allow_list).await.unwrap()); - let totals = Arc::new(Total::with_rules(rules.total().await)); + let totals = Arc::new(Total::with_rules(rules.total(true).await)); // @TODO separate active/inactive totals tokio::spawn({ let socks_rules = rules.clone(); @@ -115,7 +133,15 @@ async fn rocket() -> _ { .mount( "/", rocket::routes![ - index, api_totals, api_allow, api_block, api_rules, api_lists, api_list + index, + api_totals, + api_allow, + api_block, + api_rules, + api_lists, + api_list, + api_list_enable, + api_list_disable ], ) } diff --git a/src/rules.rs b/src/rules.rs index eb7d4ac..aca1abe 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -6,25 +6,25 @@ use log::*; use std::collections::{HashMap, HashSet}; use tokio::{fs, sync::RwLock}; -/// In-memory registry, based on the `--allow-list` -pub struct Rules { - /// Active, unique rules for this session - active: RwLock>, - /// Hold original lists rules asset with unique ID generated once on server init - /// * it allows to enable/disable specified rules in-memory by index ID - lists: HashMap, -} +pub struct Rules(RwLock>); impl Rules { - /// Build new List object pub async fn from_opt(list: &[String]) -> Result { - let mut active = HashSet::new(); - let mut lists = HashMap::new(); - - let mut rules_total = 0; - + let mut index = HashMap::with_capacity(list.len()); + assert!( + index + .insert( + 0, + List { + alias: "session".into(), + items: HashSet::new(), + status: true + } + ) + .is_none() + ); for (i, l) in list.iter().enumerate() { - let mut list_items = HashSet::new(); + let mut items = HashSet::new(); let list = if l.contains("://") { let response = reqwest::get(l).await?; let status = response.status(); @@ -40,77 +40,78 @@ impl Rules { if line.starts_with("/") || line.starts_with("#") || line.is_empty() { continue; // skip comments } - let item = Item::from_line(line); - if !active.insert(item.clone()) { - warn!("Ruleset index `{l}` contains duplicated entry: `{line}`") - } - if !list_items.insert(item) { + if !items.insert(Item::from_line(line)) { warn!("List `{l}` contains duplicated entry: `{line}`") } - rules_total += 1 } assert!( - lists + index .insert( - i, + i + 1, List { alias: l.clone(), - items: list_items + items, + status: true // @TODO implement config file } ) .is_none() ) } - - info!( - "Total rules parsed: {} (added: {rules_total}) / lists parsed: {} (added: {})", - active.len(), - list.len(), - lists.len() - ); - - Ok(Self { - active: RwLock::new(active), - lists, - }) + Ok(Self(RwLock::new(index))) } /// Check if rule is exist in the (allow) index pub async fn any(&self, value: &str) -> bool { - self.active.read().await.iter().any(|item| match item { - Item::Exact(v) => v == value, - Item::Ending(v) => value.ends_with(v), - }) + self.0.read().await.values().any(|list| list.any(value)) } /// Get total rules from the current session - pub async fn total(&self) -> u64 { - self.active.read().await.len() as u64 + pub async fn total(&self, status: bool) -> u64 { + self.0 + .read() + .await + .values() + .filter(|list| list.status == status) + .map(|list| list.total()) + .sum() } - /// Allow given `rule` + /// Allow given `rule`(in the session index) /// * return `false` if the `rule` is exist - pub async fn allow(&self, rule: &str) -> Result { - Ok(self.active.write().await.insert(Item::from_line(rule))) + pub async fn allow(&self, rule: &str) -> bool { + self.0 + .write() + .await + .get_mut(&0) + .unwrap() + .items + .insert(Item::from_line(rule)) } - /// Block given `rule` - /// * return `false` if the `rule` is not exist - pub async fn block(&self, rule: &str) -> Result { - Ok(self.active.write().await.remove(&Item::from_line(rule))) + /// Block given `rule` (in the session index) + pub async fn block(&self, rule: &str) { + self.0 + .write() + .await + .get_mut(&0) + .unwrap() + .items + .retain(|item| rule == item.as_str()) } /// Return active rules pub async fn active(&self) -> Vec { let mut rules: Vec = self - .active + .0 .read() .await - .iter() - .map(|item| item.to_string()) + .values() + .filter(|list| list.status) + .flat_map(|list| list.items.iter().map(|item| item.to_string())) .collect(); rules.sort(); // HashSet does not keep the order rules } - /// Return original list references - pub fn lists(&self) -> Vec { - let mut list = Vec::with_capacity(self.lists.len()); - for l in self.lists.iter() { + /// Return list references + pub async fn lists(&self) -> Vec { + let this = self.0.read().await; + let mut list = Vec::with_capacity(this.len()); + for l in this.iter() { list.push(ListEntry { id: *l.0, alias: l.1.alias.clone(), @@ -119,8 +120,16 @@ impl Rules { list } /// Return original list references - pub fn list(&self, id: &usize) -> Option<&List> { - self.lists.get(id) + pub async fn list(&self, id: &usize) -> Option { + self.0.read().await.get(id).cloned() + } + /// Change rule set status by list ID + pub async fn enable(&self, list_id: &usize, status: bool) -> Option<()> { + self.0 + .write() + .await + .get_mut(list_id) + .map(|this| this.status = status) } } @@ -130,8 +139,23 @@ pub struct ListEntry { pub alias: String, } -#[derive(serde::Serialize)] +#[derive(serde::Serialize, Clone)] pub struct List { pub alias: String, pub items: HashSet, + pub status: bool, +} + +impl List { + /// Check if rule is exist in the items index + pub fn any(&self, value: &str) -> bool { + self.items.iter().any(|item| match item { + Item::Exact(v) => v == value, + Item::Ending(v) => value.ends_with(v), + }) + } + /// Get total rules in list + pub fn total(&self) -> u64 { + self.items.len() as u64 + } } diff --git a/src/rules/item.rs b/src/rules/item.rs index fad747b..23678fc 100644 --- a/src/rules/item.rs +++ b/src/rules/item.rs @@ -16,6 +16,11 @@ impl Item { Self::Exact(rule.to_string()) } } + pub fn as_str(&self) -> &str { + match self { + Item::Ending(s) | Item::Exact(s) => s, + } + } } impl std::fmt::Display for Item {