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
PORT=8079
RESTRICTED_MODE=true
API_TOKEN=somesecretpassword
# OPENTELEMETRY_URL=http://localhost:4317

View file

@ -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`

View file

@ -10,15 +10,15 @@ pub(crate) struct Domains {
#[derive(serde::Deserialize, serde::Serialize)]
pub(crate) struct AllowedDomains {
allowed_domains: Vec<String>,
pub(crate) allowed_domains: Vec<String>,
}
#[derive(serde::Deserialize, serde::Serialize)]
pub(crate) struct BlockedDomains {
blocked_domains: Vec<String>,
pub(crate) blocked_domains: Vec<String>,
}
#[derive(serde::Deserialize, serde::Serialize)]
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
}
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(
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<String>,
) -> Result<(), Error> {
post_domains(client, config, domains, AdminUrlKind::Unblock).await
}
pub(crate) async fn allowed(client: &Client, config: &Config) -> Result<AllowedDomains, Error> {
get_results(client, config, AdminUrlKind::Allowed).await
}

View file

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

View file

@ -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()
}

View file

@ -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())

View file

@ -281,10 +281,6 @@ impl Db {
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> {
self.unblock(move |inner| {
let vec = serde_json::to_vec(&info)?;

View file

@ -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<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 {
#[tracing::instrument(level = "debug", skip(req))]
fn verify(req: &HttpRequest) -> Result<Self, Error> {
fn prepare_verify(
req: &HttpRequest,
) -> Result<(Data<Db>, Data<AdminConfig>, XApiToken), Error> {
let hashed_api_token = req
.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)?;
if hashed_api_token.verify(x_api_token)? {
let db = req.app_data::<Data<Db>>().ok_or_else(Error::missing_db)?;
let db = req
.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())
@ -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<Result<Self, Self::Error>>;
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
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,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)),

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()
.wrap(Tracing)
.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?;
}
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?;
}
}