diff --git a/README.md b/README.md index 30b8599..b434b13 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,11 @@ RUST_LOG=trace cargo run -- --allow=http://localhost/allow.txt \ ``` * set `socks5://127.0.0.1:1080` proxy in your application * use http://127.0.0.1:8010 for API: - * `/api/allow/{domain.com}` - add rule to the current session - * `/api/block/{domain.com}` - delete rule from the current session + * `/api/allow/` - add rule to the current session + * `/api/block/` - delete rule from the current session * `/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`) ### Allow list example diff --git a/src/main.rs b/src/main.rs index c511bc0..fe71e00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,10 +59,30 @@ async fn api_block( #[rocket::get("/api/rules")] async fn api_rules(rules: &State>) -> Result>, Status> { let active = rules.active().await; - info!("Get rules (total: {})", active.len()); + debug!("Get rules (total: {})", active.len()); Ok(Json(active)) } +#[rocket::get("/api/lists")] +async fn api_lists(rules: &State>) -> Result>, Status> { + let lists = rules.lists(); + debug!("Get lists index (total: {})", lists.len()); + Ok(Json(lists)) +} + +#[rocket::get("/api/list/")] +async fn api_list( + id: usize, + rules: &State>, +) -> Result>, Status> { + let list = rules.list(&id); + debug!( + "Get list #{id} rules (total: {:?})", + list.map(|l| l.items.len()) + ); + Ok(Json(list)) +} + #[rocket::launch] async fn rocket() -> _ { env_logger::init(); @@ -94,7 +114,9 @@ async fn rocket() -> _ { .manage(Instant::now()) .mount( "/", - rocket::routes![index, api_totals, api_allow, api_block, api_rules], + rocket::routes![ + index, api_totals, api_allow, api_block, api_rules, api_lists, api_list + ], ) } diff --git a/src/rules.rs b/src/rules.rs index 0aaa466..eb7d4ac 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -1,80 +1,104 @@ mod item; -use anyhow::Result; +use anyhow::{Result, bail}; use item::Item; use log::*; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use tokio::{fs, sync::RwLock}; -/// In-memory registry, based on `--allow-list` -pub struct Rules(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, +} impl Rules { /// Build new List object - pub async fn from_opt(list: &Vec) -> Result { - fn handle(this: &mut HashSet, line: &str) -> Option { - if line.starts_with("/") || line.starts_with("#") || line.is_empty() { - return None; - } - Some(this.insert(Item::from_line(line))) - } - - let mut index = HashSet::new(); + pub async fn from_opt(list: &[String]) -> Result { + let mut active = HashSet::new(); + let mut lists = HashMap::new(); let mut rules_total = 0; - for l in list { - for line in if l.contains("://") { + for (i, l) in list.iter().enumerate() { + let mut list_items = HashSet::new(); + let list = if l.contains("://") { let response = reqwest::get(l).await?; let status = response.status(); if status.is_success() { response.text().await? } else { - warn!("Could not receive remote list `{l}`: `{status}`"); - continue; + bail!("Could not receive remote list `{l}`: `{status}`") } } else { fs::read_to_string(l).await? - } - .lines() - { - if handle(&mut index, line).is_some_and(|status| !status) { - warn!("Ruleset `{l}` contains duplicated entry: `{line}`") + }; + for line in list.lines() { + 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) { + warn!("List `{l}` contains duplicated entry: `{line}`") } rules_total += 1 } + assert!( + lists + .insert( + i, + List { + alias: l.clone(), + items: list_items + } + ) + .is_none() + ) } - let len = index.len(); - info!("Total rules parsed: {len} (added: {rules_total})",); + info!( + "Total rules parsed: {} (added: {rules_total}) / lists parsed: {} (added: {})", + active.len(), + list.len(), + lists.len() + ); - Ok(Self(RwLock::new(index))) + Ok(Self { + active: RwLock::new(active), + lists, + }) } /// Check if rule is exist in the (allow) index pub async fn any(&self, value: &str) -> bool { - self.0.read().await.iter().any(|item| match item { + self.active.read().await.iter().any(|item| match item { Item::Exact(v) => v == value, Item::Ending(v) => value.ends_with(v), }) } /// Get total rules from the current session pub async fn total(&self) -> u64 { - self.0.read().await.len() as u64 + self.active.read().await.len() as u64 } /// Allow given `rule` /// * return `false` if the `rule` is exist pub async fn allow(&self, rule: &str) -> Result { - Ok(self.0.write().await.insert(Item::from_line(rule))) + Ok(self.active.write().await.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.0.write().await.remove(&Item::from_line(rule))) + Ok(self.active.write().await.remove(&Item::from_line(rule))) } - /// Return active rules (from server memory) + /// Return active rules pub async fn active(&self) -> Vec { let mut rules: Vec = self - .0 + .active .read() .await .iter() @@ -83,4 +107,31 @@ impl Rules { 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() { + list.push(ListEntry { + id: *l.0, + alias: l.1.alias.clone(), + }) + } + list + } + /// Return original list references + pub fn list(&self, id: &usize) -> Option<&List> { + self.lists.get(id) + } +} + +#[derive(serde::Serialize)] +pub struct ListEntry { + pub id: usize, + pub alias: String, +} + +#[derive(serde::Serialize)] +pub struct List { + pub alias: String, + pub items: HashSet, } diff --git a/src/rules/item.rs b/src/rules/item.rs index 583e381..fad747b 100644 --- a/src/rules/item.rs +++ b/src/rules/item.rs @@ -1,6 +1,6 @@ use log::debug; -#[derive(PartialEq, Eq, Hash)] +#[derive(PartialEq, Eq, Hash, Clone, serde::Serialize)] pub enum Item { Ending(String), Exact(String),