Make admin API & client work

This commit is contained in:
asonix 2022-11-17 14:13:41 -06:00
parent fe844a807f
commit ebdc739c84
12 changed files with 145 additions and 48 deletions

1
.env
View file

@ -1,4 +1,5 @@
HOSTNAME=localhost:8079 HOSTNAME=localhost:8079
PORT=8079 PORT=8079
RESTRICTED_MODE=true RESTRICTED_MODE=true
API_TOKEN=somesecretpassword
# OPENTELEMETRY_URL=http://localhost:4317 # OPENTELEMETRY_URL=http://localhost:4317

View file

@ -36,6 +36,9 @@ To simply run the server, the command is as follows
$ ./relay $ ./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 To learn about any other tasks, the `--help` flag can be passed
```bash ```bash
An activitypub relay An activitypub relay
@ -91,6 +94,7 @@ PRETTY_LOG=false
PUBLISH_BLOCKS=true PUBLISH_BLOCKS=true
SLED_PATH=./sled/db-0.34 SLED_PATH=./sled/db-0.34
RUST_LOG=warn RUST_LOG=warn
API_TOKEN=somepasswordishtoken
OPENTELEMETRY_URL=localhost:4317 OPENTELEMETRY_URL=localhost:4317
TELEGRAM_TOKEN=secret TELEGRAM_TOKEN=secret
TELEGRAM_ADMIN_HANDLE=your_handle 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` 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` ##### `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. 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` ##### `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. 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` ##### `TELEGRAM_TOKEN`

View file

@ -10,15 +10,15 @@ pub(crate) struct Domains {
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub(crate) struct AllowedDomains { pub(crate) struct AllowedDomains {
allowed_domains: Vec<String>, pub(crate) allowed_domains: Vec<String>,
} }
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub(crate) struct BlockedDomains { pub(crate) struct BlockedDomains {
blocked_domains: Vec<String>, pub(crate) blocked_domains: Vec<String>,
} }
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub(crate) struct ConnectedActors { pub(crate) struct ConnectedActors {
connected_actors: Vec<IriString>, pub(crate) connected_actors: Vec<IriString>,
} }

View file

@ -14,6 +14,14 @@ pub(crate) async fn allow(
post_domains(client, config, domains, AdminUrlKind::Allow).await post_domains(client, config, domains, AdminUrlKind::Allow).await
} }
pub(crate) async fn disallow(
client: &Client,
config: &Config,
domains: Vec<String>,
) -> Result<(), Error> {
post_domains(client, config, domains, AdminUrlKind::Disallow).await
}
pub(crate) async fn block( pub(crate) async fn block(
client: &Client, client: &Client,
config: &Config, config: &Config,
@ -22,6 +30,14 @@ pub(crate) async fn block(
post_domains(client, config, domains, AdminUrlKind::Block).await post_domains(client, config, domains, AdminUrlKind::Block).await
} }
pub(crate) async fn unblock(
client: &Client,
config: &Config,
domains: Vec<String>,
) -> Result<(), Error> {
post_domains(client, config, domains, AdminUrlKind::Unblock).await
}
pub(crate) async fn allowed(client: &Client, config: &Config) -> Result<AllowedDomains, Error> { pub(crate) async fn allowed(client: &Client, config: &Config) -> Result<AllowedDomains, Error> {
get_results(client, config, AdminUrlKind::Allowed).await get_results(client, config, AdminUrlKind::Allowed).await
} }

View file

@ -14,6 +14,15 @@ pub(crate) async fn allow(
Ok(HttpResponse::NoContent().finish()) Ok(HttpResponse::NoContent().finish())
} }
pub(crate) async fn disallow(
admin: Admin,
Json(Domains { domains }): Json<Domains>,
) -> Result<HttpResponse, Error> {
admin.db_ref().remove_allows(domains).await?;
Ok(HttpResponse::NoContent().finish())
}
pub(crate) async fn block( pub(crate) async fn block(
admin: Admin, admin: Admin,
Json(Domains { domains }): Json<Domains>, Json(Domains { domains }): Json<Domains>,
@ -23,20 +32,29 @@ pub(crate) async fn block(
Ok(HttpResponse::NoContent().finish()) Ok(HttpResponse::NoContent().finish())
} }
pub(crate) async fn allowed(admin: Admin) -> Result<HttpResponse, Error> { pub(crate) async fn unblock(
let allowed_domains = admin.db_ref().allowed_domains().await?; admin: Admin,
Json(Domains { domains }): Json<Domains>,
) -> Result<HttpResponse, Error> {
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<HttpResponse, Error> { pub(crate) async fn allowed(admin: Admin) -> Result<Json<AllowedDomains>, Error> {
let allowed_domains = admin.db_ref().allows().await?;
Ok(Json(AllowedDomains { allowed_domains }))
}
pub(crate) async fn blocked(admin: Admin) -> Result<Json<BlockedDomains>, Error> {
let blocked_domains = admin.db_ref().blocks().await?; 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<HttpResponse, Error> { pub(crate) async fn connected(admin: Admin) -> Result<Json<ConnectedActors>, Error> {
let connected_actors = admin.db_ref().connected_ids().await?; let connected_actors = admin.db_ref().connected_ids().await?;
Ok(HttpResponse::Ok().json(ConnectedActors { connected_actors })) Ok(Json(ConnectedActors { connected_actors }))
} }

View file

@ -17,6 +17,10 @@ pub(crate) struct Args {
} }
impl Args { impl Args {
pub(crate) fn any(&self) -> bool {
!self.blocks.is_empty() || !self.allowed.is_empty() || self.list
}
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Self::parse() Self::parse()
} }

View file

@ -71,7 +71,9 @@ pub enum UrlKind {
#[derive(Debug)] #[derive(Debug)]
pub enum AdminUrlKind { pub enum AdminUrlKind {
Allow, Allow,
Disallow,
Block, Block,
Unblock,
Allowed, Allowed,
Blocked, Blocked,
Connected, Connected,
@ -324,8 +326,12 @@ impl Config {
let iri = match kind { let iri = match kind {
AdminUrlKind::Allow => FixedBaseResolver::new(self.base_uri.as_ref()) AdminUrlKind::Allow => FixedBaseResolver::new(self.base_uri.as_ref())
.try_resolve(IriRelativeStr::new("api/v1/admin/allow")?.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()) AdminUrlKind::Block => FixedBaseResolver::new(self.base_uri.as_ref())
.try_resolve(IriRelativeStr::new("api/v1/admin/block")?.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()) AdminUrlKind::Allowed => FixedBaseResolver::new(self.base_uri.as_ref())
.try_resolve(IriRelativeStr::new("api/v1/admin/allowed")?.as_ref())?, .try_resolve(IriRelativeStr::new("api/v1/admin/allowed")?.as_ref())?,
AdminUrlKind::Blocked => FixedBaseResolver::new(self.base_uri.as_ref()) AdminUrlKind::Blocked => FixedBaseResolver::new(self.base_uri.as_ref())

View file

@ -281,10 +281,6 @@ impl Db {
self.unblock(|inner| Ok(inner.connected().collect())).await self.unblock(|inner| Ok(inner.connected().collect())).await
} }
pub(crate) async fn allowed_domains(&self) -> Result<Vec<String>, Error> {
self.unblock(|inner| Ok(inner.allowed().collect())).await
}
pub(crate) async fn save_info(&self, actor_id: IriString, info: Info) -> Result<(), Error> { pub(crate) async fn save_info(&self, actor_id: IriString, info: Info) -> Result<(), Error> {
self.unblock(move |inner| { self.unblock(move |inner| {
let vec = serde_json::to_vec(&info)?; let vec = serde_json::to_vec(&info)?;

View file

@ -1,6 +1,6 @@
use actix_web::{ use actix_web::{
dev::Payload, dev::Payload,
error::ParseError, error::{BlockingError, ParseError},
http::{ http::{
header::{from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue}, header::{from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue},
StatusCode, StatusCode,
@ -9,12 +9,9 @@ use actix_web::{
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError, FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
}; };
use bcrypt::{BcryptError, DEFAULT_COST}; use bcrypt::{BcryptError, DEFAULT_COST};
use futures_util::future::LocalBoxFuture;
use http_signature_normalization_actix::prelude::InvalidHeaderValue; use http_signature_normalization_actix::prelude::InvalidHeaderValue;
use std::{ use std::{convert::Infallible, str::FromStr};
convert::Infallible,
future::{ready, Ready},
str::FromStr,
};
use tracing_error::SpanTrace; use tracing_error::SpanTrace;
use crate::db::Db; use crate::db::Db;
@ -32,7 +29,7 @@ impl AdminConfig {
} }
fn verify(&self, token: XApiToken) -> Result<bool, Error> { fn verify(&self, token: XApiToken) -> Result<bool, Error> {
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 { impl Admin {
#[tracing::instrument(level = "debug", skip(req))] fn prepare_verify(
fn verify(req: &HttpRequest) -> Result<Self, Error> { req: &HttpRequest,
) -> Result<(Data<Db>, Data<AdminConfig>, XApiToken), Error> {
let hashed_api_token = req let hashed_api_token = req
.app_data::<Data<AdminConfig>>() .app_data::<Data<AdminConfig>>()
.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)?; let x_api_token = XApiToken::parse(req).map_err(Error::parse_header)?;
if hashed_api_token.verify(x_api_token)? { let db = req
let db = req.app_data::<Data<Db>>().ok_or_else(Error::missing_db)?; .app_data::<Data<Db>>()
.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<AdminConfig>,
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()) Err(Error::invalid())
@ -113,6 +126,13 @@ impl Error {
kind: ErrorKind::ParseHeader(e), kind: ErrorKind::ParseHeader(e),
} }
} }
fn canceled(_: BlockingError) -> Self {
Error {
context: SpanTrace::capture(),
kind: ErrorKind::Canceled,
}
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -126,6 +146,9 @@ enum ErrorKind {
#[error("Missing Db")] #[error("Missing Db")]
MissingDb, MissingDb,
#[error("Panic in verify")]
Canceled,
#[error("Verifying")] #[error("Verifying")]
BCryptVerify(#[source] BcryptError), BCryptVerify(#[source] BcryptError),
@ -152,10 +175,15 @@ impl ResponseError for Error {
impl FromRequest for Admin { impl FromRequest for Admin {
type Error = Error; type Error = Error;
type Future = Ready<Result<Self, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { 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 })
})
} }
} }

View file

@ -99,31 +99,51 @@ async fn main() -> Result<(), anyhow::Error> {
init_subscriber(Config::software_name(), config.opentelemetry_url())?; init_subscriber(Config::software_name(), config.opentelemetry_url())?;
let db = Db::build(&config)?;
let args = Args::new(); let args = Args::new();
if args.list() { if args.any() {
for domain in db.blocks().await? { let client = requests::build_client(&config.user_agent());
println!("block {}", domain);
}
for domain in db.allows().await? {
println!("allow {}", domain);
}
return Ok(());
}
if !args.blocks().is_empty() || !args.allowed().is_empty() { if !args.blocks().is_empty() || !args.allowed().is_empty() {
if args.undo() { if args.undo() {
db.remove_blocks(args.blocks().to_vec()).await?; admin::client::unblock(&client, &config, args.blocks().to_vec()).await?;
db.remove_allows(args.allowed().to_vec()).await?; admin::client::disallow(&client, &config, args.allowed().to_vec()).await?;
} else { } else {
db.add_blocks(args.blocks().to_vec()).await?; admin::client::block(&client, &config, args.blocks().to_vec()).await?;
db.add_allows(args.allowed().to_vec()).await?; admin::client::allow(&client, &config, args.allowed().to_vec()).await?;
} }
println!("Updated lists");
}
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(()); return Ok(());
} }
let db = Db::build(&config)?;
let media = MediaCache::new(db.clone()); let media = MediaCache::new(db.clone());
let state = State::build(db.clone()).await?; let state = State::build(db.clone()).await?;
let actors = ActorCache::new(db.clone()); let actors = ActorCache::new(db.clone());
@ -178,7 +198,9 @@ async fn main() -> Result<(), anyhow::Error> {
web::scope("/api/v1").service( web::scope("/api/v1").service(
web::scope("/admin") web::scope("/admin")
.route("/allow", web::post().to(admin::routes::allow)) .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("/block", web::post().to(admin::routes::block))
.route("/unblock", web::post().to(admin::routes::unblock))
.route("/allowed", web::get().to(admin::routes::allowed)) .route("/allowed", web::get().to(admin::routes::allowed))
.route("/blocked", web::get().to(admin::routes::blocked)) .route("/blocked", web::get().to(admin::routes::blocked))
.route("/connected", web::get().to(admin::routes::connected)), .route("/connected", web::get().to(admin::routes::connected)),

View file

@ -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() Client::builder()
.wrap(Tracing) .wrap(Tracing)
.add_default_header(("User-Agent", user_agent.to_string())) .add_default_header(("User-Agent", user_agent.to_string()))

View file

@ -105,7 +105,7 @@ async fn answer(bot: Bot, msg: Message, cmd: Command, db: Db) -> ResponseResult<
.await?; .await?;
} }
Command::ListAllowed => { 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?; bot.send_message(msg.chat.id, allowed.join("\n")).await?;
} }
} }