grand refactory to multiple list-based control api

This commit is contained in:
yggverse 2026-03-28 04:42:41 +02:00
parent 827cb182f2
commit b93c1e8481
15 changed files with 271 additions and 282 deletions

68
Cargo.lock generated
View file

@ -455,7 +455,7 @@ dependencies = [
"atomic 0.6.1", "atomic 0.6.1",
"pear", "pear",
"serde", "serde",
"toml", "toml 0.8.23",
"uncased", "uncased",
"version_check", "version_check",
] ]
@ -1438,6 +1438,8 @@ dependencies = [
"serde", "serde",
"structopt", "structopt",
"tokio", "tokio",
"toml 1.1.0+spec-1.1.0",
"url",
] ]
[[package]] [[package]]
@ -1979,6 +1981,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
dependencies = [
"serde_core",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@ -2361,11 +2372,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned 0.6.9",
"toml_datetime", "toml_datetime 0.6.11",
"toml_edit", "toml_edit",
] ]
[[package]]
name = "toml"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned 1.1.0",
"toml_datetime 1.1.0+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow 1.0.0",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.11" version = "0.6.11"
@ -2375,6 +2401,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "toml_datetime"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
dependencies = [
"serde_core",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.27" version = "0.22.27"
@ -2383,10 +2418,19 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
"serde_spanned", "serde_spanned 0.6.9",
"toml_datetime", "toml_datetime 0.6.11",
"toml_write", "toml_write",
"winnow", "winnow 0.7.15",
]
[[package]]
name = "toml_parser"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
dependencies = [
"winnow 1.0.0",
] ]
[[package]] [[package]]
@ -2395,6 +2439,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.3" version = "0.5.3"
@ -3126,6 +3176,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winnow"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.51.0" version = "0.51.0"

View file

@ -19,3 +19,5 @@ rocket = { version = "0.5.1", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
structopt = "0.3.26" structopt = "0.3.26"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
toml = "1.1.0"
url = "2.5.8"

View file

@ -16,19 +16,13 @@ Filtering asynchronous SOCKS5 (TCP/UDP) proxy server based on [fast-socks5](http
## Usage ## Usage
``` bash ``` bash
RUST_LOG=trace cargo run -- --allow=http://localhost/allow.txt \ RUST_LOG=trace cargo run -- -c=/path/to/config.toml no-auth
--allow=/path/to/allow.txt \
no-auth
``` ```
* 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/totals` - blocking summary
* `/api/block/<domain.com>` - delete rule from the current session * `/api/list/enable/<ID>` - enable all parsed rules of given list ID (`[list.ID]` in your config)
* `/api/rules` - return active rules (from server memory) * `/api/list/disable/<ID>` - disable all parsed rules of given list ID (`[list.ID]` in your config)
* `/api/lists` - get parsed lists with its ID
* `/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
@ -53,6 +47,7 @@ git clone https://codeberg.org/postscriptum/psocks.git
cd psocks cd psocks
cargo build --release --locked cargo build --release --locked
sudo install target/release/psocks /usr/local/bin sudo install target/release/psocks /usr/local/bin
sudo cp example/config.toml /etc/psocks.toml
sudo useradd -s /usr/sbin/nologin -Mr psocks sudo useradd -s /usr/sbin/nologin -Mr psocks
sudo mkdir /var/log/psocks && sudo chown psocks:psocks /var/log/psocks sudo mkdir /var/log/psocks && sudo chown psocks:psocks /var/log/psocks
``` ```
@ -68,9 +63,7 @@ Wants=network-online.target
User=psocks User=psocks
Group=psocks Group=psocks
ExecStart=/usr/local/bin/psocks \ ExecStart=/usr/local/bin/psocks -c=/etc/psocks.toml no-auth
-a=http://localhost/allow.txt \
no-auth
Restart=always Restart=always

13
src/config.rs Normal file
View file

@ -0,0 +1,13 @@
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize)]
pub struct List {
pub is_enabled: bool,
pub source: String,
}
#[derive(Deserialize)]
pub struct Config {
pub list: HashMap<String, List>,
}

11
src/example/config.toml Normal file
View file

@ -0,0 +1,11 @@
[list.google]
is_enabled = true
source = "https://codeberg.org/postscriptum/psocks-list/src/branch/main/allow/google.txt"
[list.github]
is_enabled = false
source = "https://codeberg.org/postscriptum/psocks-list/src/branch/main/allow/github.txt"
#[[list.common]]
#is_enabled = false
#source = "/path/to/common.txt"

View file

@ -0,0 +1,6 @@
// Github list example
.github.com
.github.io
.githubassets.com
.githubusercontent.com

View file

@ -0,0 +1,13 @@
// Google list example
.gmail.com
.google
.google.ch
.google.com
.google.com.ua
.google.dev
.googlevideo.com
.gstatic.com
.withgoogle.com
.youtube.com
.ytimg.com

View file

@ -1,8 +1,10 @@
mod config;
mod opt; mod opt;
mod rules; mod rules;
mod stats; mod stats;
use anyhow::Context; use anyhow::Context;
use config::Config;
use fast_socks5::{ use fast_socks5::{
ReplyError, Result, Socks5Command, SocksError, ReplyError, Result, Socks5Command, SocksError,
server::{DnsResolveHelper as _, Socks5ServerProtocol, run_tcp_proxy, run_udp_proxy}, server::{DnsResolveHelper as _, Socks5ServerProtocol, run_tcp_proxy, run_udp_proxy},
@ -14,7 +16,7 @@ use rules::Rules;
use stats::{Snap, Total}; use stats::{Snap, Total};
use std::{future::Future, sync::Arc, time::Instant}; use std::{future::Future, sync::Arc, time::Instant};
use structopt::StructOpt; use structopt::StructOpt;
use tokio::{net::TcpListener, task}; use tokio::{net::TcpListener, sync::RwLock, task};
#[rocket::get("/")] #[rocket::get("/")]
async fn index(totals: &State<Arc<Total>>, startup_time: &State<Instant>) -> Json<Snap> { async fn index(totals: &State<Arc<Total>>, startup_time: &State<Instant>) -> Json<Snap> {
@ -26,79 +28,20 @@ async fn api_totals(totals: &State<Arc<Total>>, startup_time: &State<Instant>) -
Json(totals.inner().snap(startup_time.elapsed().as_secs())) Json(totals.inner().snap(startup_time.elapsed().as_secs()))
} }
#[rocket::get("/api/allow/<rule>")] #[rocket::get("/api/list/enable/<alias>")]
async fn api_allow(
rule: &str,
rules: &State<Arc<Rules>>,
totals: &State<Arc<Total>>,
) -> Result<Json<bool>, Status> {
let result = rules.allow(rule).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))
}
#[rocket::get("/api/block/<rule>")]
async fn api_block(
rule: &str,
rules: &State<Arc<Rules>>,
totals: &State<Arc<Total>>,
) -> Result<Json<()>, Status> {
let result = rules.block(rule).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))
}
#[rocket::get("/api/rules")]
async fn api_rules(rules: &State<Arc<Rules>>) -> Result<Json<Vec<String>>, Status> {
let active = rules.active().await;
debug!("Get rules (total: {})", active.len());
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().await;
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).await;
debug!(
"Get list #{id} rules (total: {:?})",
list.as_ref().map(|l| l.items.len())
);
Ok(Json(list))
}
#[rocket::get("/api/list/enable/<id>")]
async fn api_list_enable( async fn api_list_enable(
id: usize, alias: &str,
rules: &State<Arc<Rules>>, rules: &State<Arc<RwLock<Rules>>>,
totals: &State<Arc<Total>>, ) -> Result<Json<bool>, Status> {
) -> Result<Json<Option<()>>, Status> { Ok(Json(rules.write().await.set_status(alias, true)))
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>")] #[rocket::get("/api/list/disable/<alias>")]
async fn api_list_disable( async fn api_list_disable(
id: usize, alias: &str,
rules: &State<Arc<Rules>>, rules: &State<Arc<RwLock<Rules>>>,
totals: &State<Arc<Total>>, ) -> Result<Json<bool>, Status> {
) -> Result<Json<Option<()>>, Status> { Ok(Json(rules.write().await.set_status(alias, false)))
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]
@ -106,10 +49,20 @@ async fn rocket() -> _ {
env_logger::init(); env_logger::init();
let opt: &'static Opt = Box::leak(Box::new(Opt::from_args())); let opt: &'static Opt = Box::leak(Box::new(Opt::from_args()));
let config: Config = toml::from_str(&std::fs::read_to_string(&opt.config).unwrap()).unwrap();
let rules = Arc::new(Rules::from_opt(&opt.allow_list).await.unwrap()); let totals = Arc::new(Total::default());
let rules = Arc::new(RwLock::new({
let totals = Arc::new(Total::with_rules(rules.total(true).await)); // @TODO separate active/inactive totals let mut rules = Rules::new();
for (alias, rule) in config.list {
assert!(
rules
.push(&alias, &rule.source, rule.is_enabled)
.await
.unwrap()
)
}
rules
}));
tokio::spawn({ tokio::spawn({
let socks_rules = rules.clone(); let socks_rules = rules.clone();
@ -132,23 +85,13 @@ async fn rocket() -> _ {
.manage(Instant::now()) .manage(Instant::now())
.mount( .mount(
"/", "/",
rocket::routes![ rocket::routes![index, api_totals, api_list_enable, api_list_disable],
index,
api_totals,
api_allow,
api_block,
api_rules,
api_lists,
api_list,
api_list_enable,
api_list_disable
],
) )
} }
async fn spawn_socks_server( async fn spawn_socks_server(
opt: &'static Opt, opt: &'static Opt,
rules: Arc<Rules>, rules: Arc<RwLock<Rules>>,
totals: Arc<Total>, totals: Arc<Total>,
) -> Result<()> { ) -> Result<()> {
if opt.allow_udp && opt.public_addr.is_none() { if opt.allow_udp && opt.public_addr.is_none() {
@ -179,7 +122,7 @@ async fn spawn_socks_server(
async fn serve_socks5( async fn serve_socks5(
opt: &Opt, opt: &Opt,
socket: tokio::net::TcpStream, socket: tokio::net::TcpStream,
rules: Arc<Rules>, rules: Arc<RwLock<Rules>>,
totals: Arc<Total>, totals: Arc<Total>,
) -> Result<(), SocksError> { ) -> Result<(), SocksError> {
totals.increase_request(); totals.increase_request();
@ -201,7 +144,7 @@ async fn serve_socks5(
let (host, _) = request.2.clone().into_string_and_port(); let (host, _) = request.2.clone().into_string_and_port();
if !rules.any(&host).await { if !rules.read().await.any(&host) {
totals.increase_blocked(); totals.increase_blocked();
info!("Blocked connection attempt to: {host}"); info!("Blocked connection attempt to: {host}");
request request

View file

@ -1,4 +1,4 @@
use std::{net::SocketAddr, num::ParseFloatError, time::Duration}; use std::{net::SocketAddr, num::ParseFloatError, path::PathBuf, time::Duration};
use structopt::StructOpt; use structopt::StructOpt;
/// # How to use it: /// # How to use it:
@ -45,11 +45,9 @@ pub struct Opt {
#[structopt(short = "U", long)] #[structopt(short = "U", long)]
pub allow_udp: bool, pub allow_udp: bool,
/// Allow list: /// Path to config file
/// * local filename #[structopt(short = "c", long)]
/// * remote URL pub config: PathBuf,
#[structopt(short = "a", long)]
pub allow_list: Vec<String>,
} }
/// Choose the authentication type /// Choose the authentication type

View file

@ -1,162 +1,39 @@
mod item; mod list;
use anyhow::{Result, bail}; use anyhow::Result;
use item::Item; use list::List;
use log::*; use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use tokio::{fs, sync::RwLock};
pub struct Rules(RwLock<HashMap<usize, List>>); pub struct Rules(HashMap<String, List>);
impl Rules { impl Rules {
pub async fn from_opt(list: &[String]) -> Result<Self> { pub fn new() -> Self {
let mut index = HashMap::with_capacity(list.len()); Self(HashMap::new())
assert!(
index
.insert(
0,
List {
alias: "session".into(),
items: HashSet::new(),
status: true
}
)
.is_none()
);
for (i, l) in list.iter().enumerate() {
let mut items = HashSet::new();
for line in if l.contains("://") {
let response = reqwest::get(l).await?;
let status = response.status();
if status.is_success() {
response.text().await?
} else {
bail!("Could not receive remote list `{l}`: `{status}`")
}
} else {
fs::read_to_string(l).await?
}
.lines()
{
if line.starts_with("/") || line.starts_with("#") || line.is_empty() {
continue; // skip comments
}
if !items.insert(Item::from_line(line)) {
warn!("List `{l}` contains duplicated entry: `{line}`")
}
}
assert!(
index
.insert(
i + 1,
List {
alias: l.clone(),
items,
status: true // @TODO implement config file
}
)
.is_none()
)
}
Ok(Self(RwLock::new(index)))
} }
/// Check if rule is exist in the (allow) index pub async fn push(&mut self, list_alias: &str, src: &str, status: bool) -> Result<bool> {
pub async fn any(&self, value: &str) -> bool { Ok(self
self.0.read().await.values().any(|list| list.any(value))
}
/// Get total rules from the current session
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`(in the session index)
/// * return `false` if the `rule` is exist
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` (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<String> {
let mut rules: Vec<String> = self
.0 .0
.read() .insert(list_alias.into(), List::init(src, status).await?)
.await .is_none())
.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 list references
pub async fn lists(&self) -> Vec<ListEntry> {
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(),
})
}
list
}
/// Return original list references
pub async fn list(&self, id: &usize) -> Option<List> {
self.0.read().await.get(id).cloned()
} }
/// Change rule set status by list ID /// Change rule set status by list ID
pub async fn enable(&self, list_id: &usize, status: bool) -> Option<()> { pub fn set_status(&mut self, list_alias: &str, status: bool) -> bool {
self.0 self.0
.write() .get_mut(list_alias)
.await .map(|list| list.set_status(status))
.get_mut(list_id) .is_some()
.map(|this| this.status = status)
} }
} /// Check if rule is exist in the index
#[derive(serde::Serialize)]
pub struct ListEntry {
pub id: usize,
pub alias: String,
}
#[derive(serde::Serialize, Clone)]
pub struct List {
pub alias: String,
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 { pub fn any(&self, value: &str) -> bool {
self.items.iter().any(|item| match item { self.0.values().any(|list| list.contains(value))
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
} }
/*
/// Get total rules in session by `status`
pub fn total(&self, status: bool) -> u64 {
self.0
.values()
.filter(|list| status == list.is_enabled())
.map(|list| list.total())
.sum()
}*/
} }

44
src/rules/list.rs Normal file
View file

@ -0,0 +1,44 @@
mod rule;
mod source;
use anyhow::Result;
use log::warn;
use rule::Rule;
use source::Source;
use std::collections::HashSet;
pub struct List {
pub is_enabled: bool,
//pub source: Source,
pub rules: HashSet<Rule>,
}
impl List {
pub async fn init(src: &str, is_enabled: bool) -> Result<Self> {
let mut rules = HashSet::new();
let source = Source::from_str(src)?;
if is_enabled {
for line in source.get().await?.lines() {
if line.starts_with("/") || line.starts_with("#") || line.is_empty() {
continue; // skip comments
}
if !rules.insert(Rule::from_line(line)) {
warn!("List `{src}` contains duplicated entry: `{line}`")
}
}
}
Ok(Self {
rules,
//source,
is_enabled,
})
}
/// Change rule set status by list ID
pub fn set_status(&mut self, is_enabled: bool) {
self.is_enabled = is_enabled;
}
/// Check if rule is exist in the items index
pub fn contains(&self, value: &str) -> bool {
self.is_enabled && self.rules.iter().any(|rule| rule.contains(value))
}
}

View file

@ -1,12 +1,12 @@
use log::debug; use log::debug;
#[derive(PartialEq, Eq, Hash, Clone, serde::Serialize)] #[derive(PartialEq, Eq, Hash)]
pub enum Item { pub enum Rule {
Ending(String), Ending(String),
Exact(String), Exact(String),
} }
impl Item { impl Rule {
pub fn from_line(rule: &str) -> Self { pub fn from_line(rule: &str) -> Self {
if let Some(item) = rule.strip_prefix(".") { if let Some(item) = rule.strip_prefix(".") {
debug!("Init `{rule}` rule"); debug!("Init `{rule}` rule");
@ -16,14 +16,20 @@ impl Item {
Self::Exact(rule.to_string()) Self::Exact(rule.to_string())
} }
} }
pub fn as_str(&self) -> &str { /*pub fn as_str(&self) -> &str {
match self { match self {
Item::Ending(s) | Item::Exact(s) => s, Self::Ending(s) | Self::Exact(s) => s,
}
}*/
pub fn contains(&self, value: &str) -> bool {
match self {
Rule::Exact(v) => v == value,
Rule::Ending(v) => value.ends_with(v),
} }
} }
} }
impl std::fmt::Display for Item { impl std::fmt::Display for Rule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Ending(s) => write!(f, ".{}", s), Self::Ending(s) => write!(f, ".{}", s),

40
src/rules/list/source.rs Normal file
View file

@ -0,0 +1,40 @@
use anyhow::{Result, bail};
use std::{path::PathBuf, str::FromStr};
use url::Url;
pub enum Source {
Path(PathBuf),
Url(Url),
}
impl Source {
pub fn from_str(source: &str) -> Result<Self> {
Ok(if source.contains("://") {
Self::Url(Url::from_str(source)?)
} else {
Self::Path(PathBuf::from_str(source)?.canonicalize()?)
})
}
pub async fn get(&self) -> Result<String> {
Ok(match self {
Source::Path(path) => tokio::fs::read_to_string(path).await?,
Source::Url(url) => {
let request = url.as_str();
let response = reqwest::get(request).await?;
let status = response.status();
if status.is_success() {
response.text().await?
} else {
bail!("Could not receive remote list `{request}`: `{status}`")
}
}
})
}
}
impl FromStr for Source {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Source::from_str(s)
}
}

View file

@ -6,29 +6,18 @@ pub use snap::Snap;
#[derive(Default)] #[derive(Default)]
pub struct Total { pub struct Total {
entries: AtomicU64,
blocked: AtomicU64, blocked: AtomicU64,
request: AtomicU64, request: AtomicU64,
} }
impl Total { impl Total {
pub fn with_rules(entries: u64) -> Self {
Self {
entries: entries.into(),
..Self::default()
}
}
pub fn snap(&self, seconds_from_startup: u64) -> Snap { pub fn snap(&self, seconds_from_startup: u64) -> Snap {
Snap::shot( Snap::shot(
self.entries.load(Ordering::Relaxed),
self.request.load(Ordering::Relaxed), self.request.load(Ordering::Relaxed),
self.blocked.load(Ordering::Relaxed), self.blocked.load(Ordering::Relaxed),
seconds_from_startup, seconds_from_startup,
) )
} }
pub fn set_entries(&self, value: u64) {
self.entries.store(value, Ordering::Relaxed);
}
pub fn increase_blocked(&self) { pub fn increase_blocked(&self) {
self.blocked.fetch_add(1, Ordering::Relaxed); self.blocked.fetch_add(1, Ordering::Relaxed);
} }

View file

@ -40,20 +40,18 @@ pub struct Request {
#[derive(Serialize)] #[derive(Serialize)]
pub struct Snap { pub struct Snap {
rules: u64,
request: Request, request: Request,
up: Up, up: Up,
} }
impl Snap { impl Snap {
pub fn shot(rules: u64, request: u64, blocked: u64, seconds_from_startup: u64) -> Self { pub fn shot(request: u64, blocked: u64, seconds_from_startup: u64) -> Self {
let blocked_percent = if request > 0 { let blocked_percent = if request > 0 {
blocked as f32 * 100.0 / request as f32 blocked as f32 * 100.0 / request as f32
} else { } else {
0.0 0.0
}; };
Self { Self {
rules,
request: Request { request: Request {
total: request, total: request,
allowed: Sum { allowed: Sum {