disallow server start if at least one list parsed with errors; add initial listing api

This commit is contained in:
yggverse 2026-03-27 13:09:08 +02:00
parent 0b08744f8f
commit 752ea23f80
4 changed files with 111 additions and 36 deletions

View file

@ -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 * set `socks5://127.0.0.1:1080` proxy in your application
* use http://127.0.0.1:8010 for API: * use http://127.0.0.1:8010 for API:
* `/api/allow/{domain.com}` - add rule to the current session * `/api/allow/<domain.com>` - add rule to the current session
* `/api/block/{domain.com}` - delete rule from the current session * `/api/block/<domain.com>` - delete rule from the current session
* `/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/list/<ID>` - get all parsed rules for list ID (see `/api/lists`)
### Allow list example ### Allow list example

View file

@ -59,10 +59,30 @@ async fn api_block(
#[rocket::get("/api/rules")] #[rocket::get("/api/rules")]
async fn api_rules(rules: &State<Arc<Rules>>) -> Result<Json<Vec<String>>, Status> { async fn api_rules(rules: &State<Arc<Rules>>) -> Result<Json<Vec<String>>, Status> {
let active = rules.active().await; let active = rules.active().await;
info!("Get rules (total: {})", active.len()); debug!("Get rules (total: {})", active.len());
Ok(Json(active)) Ok(Json(active))
} }
#[rocket::get("/api/lists")]
async fn api_lists(rules: &State<Arc<Rules>>) -> Result<Json<Vec<rules::ListEntry>>, Status> {
let lists = rules.lists();
debug!("Get lists index (total: {})", lists.len());
Ok(Json(lists))
}
#[rocket::get("/api/list/<id>")]
async fn api_list(
id: usize,
rules: &State<Arc<Rules>>,
) -> Result<Json<Option<&rules::List>>, Status> {
let list = rules.list(&id);
debug!(
"Get list #{id} rules (total: {:?})",
list.map(|l| l.items.len())
);
Ok(Json(list))
}
#[rocket::launch] #[rocket::launch]
async fn rocket() -> _ { async fn rocket() -> _ {
env_logger::init(); env_logger::init();
@ -94,7 +114,9 @@ async fn rocket() -> _ {
.manage(Instant::now()) .manage(Instant::now())
.mount( .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
],
) )
} }

View file

@ -1,80 +1,104 @@
mod item; mod item;
use anyhow::Result; use anyhow::{Result, bail};
use item::Item; use item::Item;
use log::*; use log::*;
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use tokio::{fs, sync::RwLock}; use tokio::{fs, sync::RwLock};
/// In-memory registry, based on `--allow-list` /// In-memory registry, based on the `--allow-list`
pub struct Rules(RwLock<HashSet<Item>>); 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 /// Build new List object
pub async fn from_opt(list: &Vec<String>) -> Result<Self> { pub async fn from_opt(list: &[String]) -> Result<Self> {
fn handle(this: &mut HashSet<Item>, line: &str) -> Option<bool> { let mut active = HashSet::new();
if line.starts_with("/") || line.starts_with("#") || line.is_empty() { let mut lists = HashMap::new();
return None;
}
Some(this.insert(Item::from_line(line)))
}
let mut index = HashSet::new();
let mut rules_total = 0; let mut rules_total = 0;
for l in list { for (i, l) in list.iter().enumerate() {
for line in if l.contains("://") { let mut list_items = HashSet::new();
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();
if status.is_success() { if status.is_success() {
response.text().await? response.text().await?
} else { } else {
warn!("Could not receive remote list `{l}`: `{status}`"); bail!("Could not receive remote list `{l}`: `{status}`")
continue;
} }
} else { } else {
fs::read_to_string(l).await? fs::read_to_string(l).await?
};
for line in list.lines() {
if line.starts_with("/") || line.starts_with("#") || line.is_empty() {
continue; // skip comments
} }
.lines() let item = Item::from_line(line);
{ if !active.insert(item.clone()) {
if handle(&mut index, line).is_some_and(|status| !status) { warn!("Ruleset index `{l}` contains duplicated entry: `{line}`")
warn!("Ruleset `{l}` contains duplicated entry: `{line}`") }
if !list_items.insert(item) {
warn!("List `{l}` contains duplicated entry: `{line}`")
} }
rules_total += 1 rules_total += 1
} }
assert!(
lists
.insert(
i,
List {
alias: l.clone(),
items: list_items
}
)
.is_none()
)
} }
let len = index.len(); info!(
info!("Total rules parsed: {len} (added: {rules_total})",); "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 /// 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.0.read().await.iter().any(|item| match item { self.active.read().await.iter().any(|item| match item {
Item::Exact(v) => v == value, Item::Exact(v) => v == value,
Item::Ending(v) => value.ends_with(v), 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) -> u64 {
self.0.read().await.len() as u64 self.active.read().await.len() as u64
} }
/// Allow given `rule` /// Allow given `rule`
/// * 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) -> Result<bool> {
Ok(self.0.write().await.insert(Item::from_line(rule))) Ok(self.active.write().await.insert(Item::from_line(rule)))
} }
/// Block given `rule` /// Block given `rule`
/// * return `false` if the `rule` is not exist /// * return `false` if the `rule` is not exist
pub async fn block(&self, rule: &str) -> Result<bool> { pub async fn block(&self, rule: &str) -> Result<bool> {
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<String> { pub async fn active(&self) -> Vec<String> {
let mut rules: Vec<String> = self let mut rules: Vec<String> = self
.0 .active
.read() .read()
.await .await
.iter() .iter()
@ -83,4 +107,31 @@ impl Rules {
rules.sort(); // HashSet does not keep the order rules.sort(); // HashSet does not keep the order
rules rules
} }
/// Return original list references
pub fn lists(&self) -> Vec<ListEntry> {
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<Item>,
} }

View file

@ -1,6 +1,6 @@
use log::debug; use log::debug;
#[derive(PartialEq, Eq, Hash)] #[derive(PartialEq, Eq, Hash, Clone, serde::Serialize)]
pub enum Item { pub enum Item {
Ending(String), Ending(String),
Exact(String), Exact(String),