diff --git a/.env b/.env index c007da1..e8adc6f 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ HOSTNAME=localhost:8079 PORT=8079 RESTRICTED_MODE=true +API_TOKEN=somesecretpassword # OPENTELEMETRY_URL=http://localhost:4317 diff --git a/README.md b/README.md index 9cca3b9..1506ca2 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ To simply run the server, the command is as follows $ ./relay ``` +#### Administration +> **NOTE:** The server _must be running_ in order to update the lists with the following commands + To learn about any other tasks, the `--help` flag can be passed ```bash An activitypub relay @@ -91,6 +94,7 @@ PRETTY_LOG=false PUBLISH_BLOCKS=true SLED_PATH=./sled/db-0.34 RUST_LOG=warn +API_TOKEN=somepasswordishtoken OPENTELEMETRY_URL=localhost:4317 TELEGRAM_TOKEN=secret TELEGRAM_ADMIN_HANDLE=your_handle @@ -119,6 +123,8 @@ Where to store the on-disk database of connected servers. This defaults to `./sl The log level to print. Available levels are `ERROR`, `WARN`, `INFO`, `DEBUG`, and `TRACE`. You can also specify module paths to enable some logs but not others, such as `RUST_LOG=warn,tracing_actix_web=info,relay=info` ##### `SOURCE_REPO` The URL to the source code for the relay. This defaults to `https://git.asonix.dog/asonix/relay`, but should be changed if you're running a fork hosted elsewhere. +##### `API_TOKEN` +The Secret token used to access the admin APIs. This must be set for the commandline to function ##### `OPENTELEMETRY_URL` A URL for exporting opentelemetry spans. This is mostly useful for debugging. There is no default, since most people probably don't run an opentelemetry collector. ##### `TELEGRAM_TOKEN` diff --git a/src/admin.rs b/src/admin.rs index c3fb836..e7fc665 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -10,15 +10,15 @@ pub(crate) struct Domains { #[derive(serde::Deserialize, serde::Serialize)] pub(crate) struct AllowedDomains { - allowed_domains: Vec, + pub(crate) allowed_domains: Vec, } #[derive(serde::Deserialize, serde::Serialize)] pub(crate) struct BlockedDomains { - blocked_domains: Vec, + pub(crate) blocked_domains: Vec, } #[derive(serde::Deserialize, serde::Serialize)] pub(crate) struct ConnectedActors { - connected_actors: Vec, + pub(crate) connected_actors: Vec, } diff --git a/src/admin/client.rs b/src/admin/client.rs index 3bcfc8b..f63151f 100644 --- a/src/admin/client.rs +++ b/src/admin/client.rs @@ -14,6 +14,14 @@ pub(crate) async fn allow( post_domains(client, config, domains, AdminUrlKind::Allow).await } +pub(crate) async fn disallow( + client: &Client, + config: &Config, + domains: Vec, +) -> Result<(), Error> { + post_domains(client, config, domains, AdminUrlKind::Disallow).await +} + pub(crate) async fn block( client: &Client, config: &Config, @@ -22,6 +30,14 @@ pub(crate) async fn block( post_domains(client, config, domains, AdminUrlKind::Block).await } +pub(crate) async fn unblock( + client: &Client, + config: &Config, + domains: Vec, +) -> Result<(), Error> { + post_domains(client, config, domains, AdminUrlKind::Unblock).await +} + pub(crate) async fn allowed(client: &Client, config: &Config) -> Result { get_results(client, config, AdminUrlKind::Allowed).await } diff --git a/src/admin/routes.rs b/src/admin/routes.rs index 68b78a7..c33efca 100644 --- a/src/admin/routes.rs +++ b/src/admin/routes.rs @@ -14,6 +14,15 @@ pub(crate) async fn allow( Ok(HttpResponse::NoContent().finish()) } +pub(crate) async fn disallow( + admin: Admin, + Json(Domains { domains }): Json, +) -> Result { + admin.db_ref().remove_allows(domains).await?; + + Ok(HttpResponse::NoContent().finish()) +} + pub(crate) async fn block( admin: Admin, Json(Domains { domains }): Json, @@ -23,20 +32,29 @@ pub(crate) async fn block( Ok(HttpResponse::NoContent().finish()) } -pub(crate) async fn allowed(admin: Admin) -> Result { - let allowed_domains = admin.db_ref().allowed_domains().await?; +pub(crate) async fn unblock( + admin: Admin, + Json(Domains { domains }): Json, +) -> Result { + admin.db_ref().remove_blocks(domains).await?; - Ok(HttpResponse::Ok().json(AllowedDomains { allowed_domains })) + Ok(HttpResponse::NoContent().finish()) } -pub(crate) async fn blocked(admin: Admin) -> Result { +pub(crate) async fn allowed(admin: Admin) -> Result, Error> { + let allowed_domains = admin.db_ref().allows().await?; + + Ok(Json(AllowedDomains { allowed_domains })) +} + +pub(crate) async fn blocked(admin: Admin) -> Result, Error> { let blocked_domains = admin.db_ref().blocks().await?; - Ok(HttpResponse::Ok().json(BlockedDomains { blocked_domains })) + Ok(Json(BlockedDomains { blocked_domains })) } -pub(crate) async fn connected(admin: Admin) -> Result { +pub(crate) async fn connected(admin: Admin) -> Result, Error> { let connected_actors = admin.db_ref().connected_ids().await?; - Ok(HttpResponse::Ok().json(ConnectedActors { connected_actors })) + Ok(Json(ConnectedActors { connected_actors })) } diff --git a/src/args.rs b/src/args.rs index 61e093f..6b6c054 100644 --- a/src/args.rs +++ b/src/args.rs @@ -17,6 +17,10 @@ pub(crate) struct Args { } impl Args { + pub(crate) fn any(&self) -> bool { + !self.blocks.is_empty() || !self.allowed.is_empty() || self.list + } + pub(crate) fn new() -> Self { Self::parse() } diff --git a/src/config.rs b/src/config.rs index 750a443..284f807 100644 --- a/src/config.rs +++ b/src/config.rs @@ -71,7 +71,9 @@ pub enum UrlKind { #[derive(Debug)] pub enum AdminUrlKind { Allow, + Disallow, Block, + Unblock, Allowed, Blocked, Connected, @@ -324,8 +326,12 @@ impl Config { let iri = match kind { AdminUrlKind::Allow => FixedBaseResolver::new(self.base_uri.as_ref()) .try_resolve(IriRelativeStr::new("api/v1/admin/allow")?.as_ref())?, + AdminUrlKind::Disallow => FixedBaseResolver::new(self.base_uri.as_ref()) + .try_resolve(IriRelativeStr::new("api/v1/admin/disallow")?.as_ref())?, AdminUrlKind::Block => FixedBaseResolver::new(self.base_uri.as_ref()) .try_resolve(IriRelativeStr::new("api/v1/admin/block")?.as_ref())?, + AdminUrlKind::Unblock => FixedBaseResolver::new(self.base_uri.as_ref()) + .try_resolve(IriRelativeStr::new("api/v1/admin/unblock")?.as_ref())?, AdminUrlKind::Allowed => FixedBaseResolver::new(self.base_uri.as_ref()) .try_resolve(IriRelativeStr::new("api/v1/admin/allowed")?.as_ref())?, AdminUrlKind::Blocked => FixedBaseResolver::new(self.base_uri.as_ref()) diff --git a/src/db.rs b/src/db.rs index fcfadba..0aaf0ac 100644 --- a/src/db.rs +++ b/src/db.rs @@ -281,10 +281,6 @@ impl Db { self.unblock(|inner| Ok(inner.connected().collect())).await } - pub(crate) async fn allowed_domains(&self) -> Result, Error> { - self.unblock(|inner| Ok(inner.allowed().collect())).await - } - pub(crate) async fn save_info(&self, actor_id: IriString, info: Info) -> Result<(), Error> { self.unblock(move |inner| { let vec = serde_json::to_vec(&info)?; diff --git a/src/extractors.rs b/src/extractors.rs index 742f016..9c3c29b 100644 --- a/src/extractors.rs +++ b/src/extractors.rs @@ -1,6 +1,6 @@ use actix_web::{ dev::Payload, - error::ParseError, + error::{BlockingError, ParseError}, http::{ header::{from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue}, StatusCode, @@ -9,12 +9,9 @@ use actix_web::{ FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError, }; use bcrypt::{BcryptError, DEFAULT_COST}; +use futures_util::future::LocalBoxFuture; use http_signature_normalization_actix::prelude::InvalidHeaderValue; -use std::{ - convert::Infallible, - future::{ready, Ready}, - str::FromStr, -}; +use std::{convert::Infallible, str::FromStr}; use tracing_error::SpanTrace; use crate::db::Db; @@ -32,7 +29,7 @@ impl AdminConfig { } fn verify(&self, token: XApiToken) -> Result { - Ok(bcrypt::verify(&self.hashed_api_token, &token.0).map_err(Error::bcrypt_verify)?) + Ok(bcrypt::verify(&token.0, &self.hashed_api_token).map_err(Error::bcrypt_verify)?) } } @@ -41,18 +38,34 @@ pub(crate) struct Admin { } impl Admin { - #[tracing::instrument(level = "debug", skip(req))] - fn verify(req: &HttpRequest) -> Result { + fn prepare_verify( + req: &HttpRequest, + ) -> Result<(Data, Data, XApiToken), Error> { let hashed_api_token = req .app_data::>() - .ok_or_else(Error::missing_config)?; + .ok_or_else(Error::missing_config)? + .clone(); let x_api_token = XApiToken::parse(req).map_err(Error::parse_header)?; - if hashed_api_token.verify(x_api_token)? { - let db = req.app_data::>().ok_or_else(Error::missing_db)?; + let db = req + .app_data::>() + .ok_or_else(Error::missing_db)? + .clone(); - return Ok(Self { db: db.clone() }); + Ok((db, hashed_api_token, x_api_token)) + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn verify( + hashed_api_token: Data, + x_api_token: XApiToken, + ) -> Result<(), Error> { + if actix_web::web::block(move || hashed_api_token.verify(x_api_token)) + .await + .map_err(Error::canceled)?? + { + return Ok(()); } Err(Error::invalid()) @@ -113,6 +126,13 @@ impl Error { kind: ErrorKind::ParseHeader(e), } } + + fn canceled(_: BlockingError) -> Self { + Error { + context: SpanTrace::capture(), + kind: ErrorKind::Canceled, + } + } } #[derive(Debug, thiserror::Error)] @@ -126,6 +146,9 @@ enum ErrorKind { #[error("Missing Db")] MissingDb, + #[error("Panic in verify")] + Canceled, + #[error("Verifying")] BCryptVerify(#[source] BcryptError), @@ -152,10 +175,15 @@ impl ResponseError for Error { impl FromRequest for Admin { type Error = Error; - type Future = Ready>; + type Future = LocalBoxFuture<'static, Result>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - ready(Admin::verify(req)) + let res = Self::prepare_verify(req); + Box::pin(async move { + let (db, c, t) = res?; + Self::verify(c, t).await?; + Ok(Admin { db }) + }) } } diff --git a/src/main.rs b/src/main.rs index 1ef2cd9..1d80a0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,30 +99,50 @@ async fn main() -> Result<(), anyhow::Error> { init_subscriber(Config::software_name(), config.opentelemetry_url())?; - let db = Db::build(&config)?; - let args = Args::new(); - if args.list() { - for domain in db.blocks().await? { - println!("block {}", domain); + if args.any() { + let client = requests::build_client(&config.user_agent()); + + if !args.blocks().is_empty() || !args.allowed().is_empty() { + if args.undo() { + admin::client::unblock(&client, &config, args.blocks().to_vec()).await?; + admin::client::disallow(&client, &config, args.allowed().to_vec()).await?; + } else { + admin::client::block(&client, &config, args.blocks().to_vec()).await?; + admin::client::allow(&client, &config, args.allowed().to_vec()).await?; + } + println!("Updated lists"); } - for domain in db.allows().await? { - println!("allow {}", domain); + + if args.list() { + let (blocked, allowed, connected) = tokio::try_join!( + admin::client::blocked(&client, &config), + admin::client::allowed(&client, &config), + admin::client::connected(&client, &config) + )?; + + let mut report = String::from("Report:\n"); + if !allowed.allowed_domains.is_empty() { + report += "\nAllowed\n\t"; + report += &allowed.allowed_domains.join("\n\t"); + } + if !blocked.blocked_domains.is_empty() { + report += "\n\nBlocked\n\t"; + report += &blocked.blocked_domains.join("\n\t"); + } + if !connected.connected_actors.is_empty() { + report += "\n\nConnected\n\t"; + report += &connected.connected_actors.join("\n\t"); + } + report += "\n"; + println!("{report}"); } + return Ok(()); } - if !args.blocks().is_empty() || !args.allowed().is_empty() { - if args.undo() { - db.remove_blocks(args.blocks().to_vec()).await?; - db.remove_allows(args.allowed().to_vec()).await?; - } else { - db.add_blocks(args.blocks().to_vec()).await?; - db.add_allows(args.allowed().to_vec()).await?; - } - return Ok(()); - } + let db = Db::build(&config)?; let media = MediaCache::new(db.clone()); let state = State::build(db.clone()).await?; @@ -178,7 +198,9 @@ async fn main() -> Result<(), anyhow::Error> { web::scope("/api/v1").service( web::scope("/admin") .route("/allow", web::post().to(admin::routes::allow)) + .route("/disallow", web::post().to(admin::routes::disallow)) .route("/block", web::post().to(admin::routes::block)) + .route("/unblock", web::post().to(admin::routes::unblock)) .route("/allowed", web::get().to(admin::routes::allowed)) .route("/blocked", web::get().to(admin::routes::blocked)) .route("/connected", web::get().to(admin::routes::connected)), diff --git a/src/requests.rs b/src/requests.rs index 3677399..12895e1 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -160,7 +160,7 @@ impl std::fmt::Debug for Requests { } } -fn build_client(user_agent: &str) -> Client { +pub(crate) fn build_client(user_agent: &str) -> Client { Client::builder() .wrap(Tracing) .add_default_header(("User-Agent", user_agent.to_string())) diff --git a/src/telegram.rs b/src/telegram.rs index 39636fb..270e1af 100644 --- a/src/telegram.rs +++ b/src/telegram.rs @@ -105,7 +105,7 @@ async fn answer(bot: Bot, msg: Message, cmd: Command, db: Db) -> ResponseResult< .await?; } Command::ListAllowed => { - if let Ok(allowed) = db.allowed_domains().await { + if let Ok(allowed) = db.allows().await { bot.send_message(msg.chat.id, allowed.join("\n")).await?; } }