draft multi-list rules api

This commit is contained in:
yggverse 2026-03-27 15:58:24 +02:00
parent 752ea23f80
commit 18509e6d1a
4 changed files with 133 additions and 76 deletions

View file

@ -27,6 +27,8 @@ RUST_LOG=trace cargo run -- --allow=http://localhost/allow.txt \
* `/api/rules` - return active rules (from server memory) * `/api/rules` - return active rules (from server memory)
* `/api/lists` - get parsed lists with its ID * `/api/lists` - get parsed lists with its ID
* `/api/list/<ID>` - get all parsed rules for list ID (see `/api/lists`) * `/api/list/<ID>` - get all parsed rules for list ID (see `/api/lists`)
* `/api/list/enable/<ID>` - enable all parsed rules of given list ID (see `/api/lists`)
* `/api/list/disable/<ID>` - disable all parsed rules of given list ID (see `/api/lists`)
### Allow list example ### Allow list example

View file

@ -33,12 +33,9 @@ async fn api_allow(
totals: &State<Arc<Total>>, totals: &State<Arc<Total>>,
) -> Result<Json<bool>, Status> { ) -> Result<Json<bool>, Status> {
let result = rules.allow(rule).await; 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:?})"); info!("Delete `{rule}` from the in-memory rules (operation status: {result:?})");
Ok(Json(result.map_err(|e| { Ok(Json(result))
error!("Allow request handle error for `{rule}`: `{e}`");
Status::InternalServerError
})?))
} }
#[rocket::get("/api/block/<rule>")] #[rocket::get("/api/block/<rule>")]
@ -46,14 +43,11 @@ async fn api_block(
rule: &str, rule: &str,
rules: &State<Arc<Rules>>, rules: &State<Arc<Rules>>,
totals: &State<Arc<Total>>, totals: &State<Arc<Total>>,
) -> Result<Json<bool>, Status> { ) -> Result<Json<()>, Status> {
let result = rules.block(rule).await; 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:?})"); info!("Add `{rule}` to the in-memory rules (operation status: {result:?})");
Ok(Json(result.map_err(|e| { Ok(Json(result))
error!("Block request handle error for `{rule}`: `{e}`");
Status::InternalServerError
})?))
} }
#[rocket::get("/api/rules")] #[rocket::get("/api/rules")]
@ -65,7 +59,7 @@ async fn api_rules(rules: &State<Arc<Rules>>) -> Result<Json<Vec<String>>, Statu
#[rocket::get("/api/lists")] #[rocket::get("/api/lists")]
async fn api_lists(rules: &State<Arc<Rules>>) -> Result<Json<Vec<rules::ListEntry>>, Status> { async fn api_lists(rules: &State<Arc<Rules>>) -> Result<Json<Vec<rules::ListEntry>>, Status> {
let lists = rules.lists(); let lists = rules.lists().await;
debug!("Get lists index (total: {})", lists.len()); debug!("Get lists index (total: {})", lists.len());
Ok(Json(lists)) Ok(Json(lists))
} }
@ -74,15 +68,39 @@ async fn api_lists(rules: &State<Arc<Rules>>) -> Result<Json<Vec<rules::ListEntr
async fn api_list( async fn api_list(
id: usize, id: usize,
rules: &State<Arc<Rules>>, rules: &State<Arc<Rules>>,
) -> Result<Json<Option<&rules::List>>, Status> { ) -> Result<Json<Option<rules::List>>, Status> {
let list = rules.list(&id); let list = rules.list(&id).await;
debug!( debug!(
"Get list #{id} rules (total: {:?})", "Get list #{id} rules (total: {:?})",
list.map(|l| l.items.len()) list.as_ref().map(|l| l.items.len())
); );
Ok(Json(list)) Ok(Json(list))
} }
#[rocket::get("/api/list/enable/<id>")]
async fn api_list_enable(
id: usize,
rules: &State<Arc<Rules>>,
totals: &State<Arc<Total>>,
) -> Result<Json<Option<()>>, 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/<id>")]
async fn api_list_disable(
id: usize,
rules: &State<Arc<Rules>>,
totals: &State<Arc<Total>>,
) -> Result<Json<Option<()>>, 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] #[rocket::launch]
async fn rocket() -> _ { async fn rocket() -> _ {
env_logger::init(); env_logger::init();
@ -91,7 +109,7 @@ async fn rocket() -> _ {
let rules = Arc::new(Rules::from_opt(&opt.allow_list).await.unwrap()); 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({ tokio::spawn({
let socks_rules = rules.clone(); let socks_rules = rules.clone();
@ -115,7 +133,15 @@ async fn rocket() -> _ {
.mount( .mount(
"/", "/",
rocket::routes![ 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
], ],
) )
} }

View file

@ -6,25 +6,25 @@ use log::*;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use tokio::{fs, sync::RwLock}; use tokio::{fs, sync::RwLock};
/// In-memory registry, based on the `--allow-list` pub struct Rules(RwLock<HashMap<usize, List>>);
pub struct Rules {
/// Active, unique rules for this session
active: RwLock<HashSet<Item>>,
/// 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<usize, List>,
}
impl Rules { impl Rules {
/// Build new List object
pub async fn from_opt(list: &[String]) -> Result<Self> { pub async fn from_opt(list: &[String]) -> Result<Self> {
let mut active = HashSet::new(); let mut index = HashMap::with_capacity(list.len());
let mut lists = HashMap::new(); assert!(
index
let mut rules_total = 0; .insert(
0,
List {
alias: "session".into(),
items: HashSet::new(),
status: true
}
)
.is_none()
);
for (i, l) in list.iter().enumerate() { for (i, l) in list.iter().enumerate() {
let mut list_items = HashSet::new(); let mut items = HashSet::new();
let list = if l.contains("://") { let list = if l.contains("://") {
let response = reqwest::get(l).await?; let response = reqwest::get(l).await?;
let status = response.status(); let status = response.status();
@ -40,77 +40,78 @@ impl Rules {
if line.starts_with("/") || line.starts_with("#") || line.is_empty() { if line.starts_with("/") || line.starts_with("#") || line.is_empty() {
continue; // skip comments continue; // skip comments
} }
let item = Item::from_line(line); if !items.insert(Item::from_line(line)) {
if !active.insert(item.clone()) {
warn!("Ruleset index `{l}` contains duplicated entry: `{line}`")
}
if !list_items.insert(item) {
warn!("List `{l}` contains duplicated entry: `{line}`") warn!("List `{l}` contains duplicated entry: `{line}`")
} }
rules_total += 1
} }
assert!( assert!(
lists index
.insert( .insert(
i, i + 1,
List { List {
alias: l.clone(), alias: l.clone(),
items: list_items items,
status: true // @TODO implement config file
} }
) )
.is_none() .is_none()
) )
} }
Ok(Self(RwLock::new(index)))
info!(
"Total rules parsed: {} (added: {rules_total}) / lists parsed: {} (added: {})",
active.len(),
list.len(),
lists.len()
);
Ok(Self {
active: RwLock::new(active),
lists,
})
} }
/// Check if rule is exist in the (allow) index /// Check if rule is exist in the (allow) index
pub async fn any(&self, value: &str) -> bool { pub async fn any(&self, value: &str) -> bool {
self.active.read().await.iter().any(|item| match item { self.0.read().await.values().any(|list| list.any(value))
Item::Exact(v) => v == value,
Item::Ending(v) => value.ends_with(v),
})
} }
/// Get total rules from the current session /// Get total rules from the current session
pub async fn total(&self) -> u64 { pub async fn total(&self, status: bool) -> u64 {
self.active.read().await.len() as 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 /// * return `false` if the `rule` is exist
pub async fn allow(&self, rule: &str) -> Result<bool> { pub async fn allow(&self, rule: &str) -> bool {
Ok(self.active.write().await.insert(Item::from_line(rule))) self.0
.write()
.await
.get_mut(&0)
.unwrap()
.items
.insert(Item::from_line(rule))
} }
/// Block given `rule` /// Block given `rule` (in the session index)
/// * return `false` if the `rule` is not exist pub async fn block(&self, rule: &str) {
pub async fn block(&self, rule: &str) -> Result<bool> { self.0
Ok(self.active.write().await.remove(&Item::from_line(rule))) .write()
.await
.get_mut(&0)
.unwrap()
.items
.retain(|item| rule == item.as_str())
} }
/// Return active rules /// Return active rules
pub async fn active(&self) -> Vec<String> { pub async fn active(&self) -> Vec<String> {
let mut rules: Vec<String> = self let mut rules: Vec<String> = self
.active .0
.read() .read()
.await .await
.iter() .values()
.map(|item| item.to_string()) .filter(|list| list.status)
.flat_map(|list| list.items.iter().map(|item| item.to_string()))
.collect(); .collect();
rules.sort(); // HashSet does not keep the order rules.sort(); // HashSet does not keep the order
rules rules
} }
/// Return original list references /// Return list references
pub fn lists(&self) -> Vec<ListEntry> { pub async fn lists(&self) -> Vec<ListEntry> {
let mut list = Vec::with_capacity(self.lists.len()); let this = self.0.read().await;
for l in self.lists.iter() { let mut list = Vec::with_capacity(this.len());
for l in this.iter() {
list.push(ListEntry { list.push(ListEntry {
id: *l.0, id: *l.0,
alias: l.1.alias.clone(), alias: l.1.alias.clone(),
@ -119,8 +120,16 @@ impl Rules {
list list
} }
/// Return original list references /// Return original list references
pub fn list(&self, id: &usize) -> Option<&List> { pub async fn list(&self, id: &usize) -> Option<List> {
self.lists.get(id) 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, pub alias: String,
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize, Clone)]
pub struct List { pub struct List {
pub alias: String, pub alias: String,
pub items: HashSet<Item>, pub items: HashSet<Item>,
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
}
} }

View file

@ -16,6 +16,11 @@ impl Item {
Self::Exact(rule.to_string()) 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 { impl std::fmt::Display for Item {