diff --git a/Cargo.lock b/Cargo.lock index f495ac4..c28cee2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,7 +30,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash", - "base64", + "base64 0.21.7", "bitflags 2.4.2", "brotli", "bytes", @@ -189,7 +189,7 @@ checksum = "1d613edf08a42ccc6864c941d30fe14e1b676a77d16f1dbadc1174d065a0a775" dependencies = [ "actix-utils", "actix-web", - "base64", + "base64 0.21.7", "futures-core", "futures-util", "log", @@ -371,6 +371,22 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + +[[package]] +name = "base64-serde" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba368df5de76a5bea49aaf0cf1b39ccfbbef176924d1ba5db3e4135216cbe3c7" +dependencies = [ + "base64 0.21.7", + "serde", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -1029,6 +1045,8 @@ dependencies = [ "actix-web-httpauth", "anyhow", "argon2", + "base64 0.22.0", + "base64-serde", "chrono", "compile-time-run", "dotenvy", @@ -1038,6 +1056,7 @@ dependencies = [ "serde", "sqlx", "thiserror", + "tokio", "uuid", ] @@ -1895,7 +1914,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bitflags 2.4.2", "byteorder", "bytes", @@ -1938,7 +1957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bitflags 2.4.2", "byteorder", "chrono", diff --git a/Cargo.toml b/Cargo.toml index ba2d37a..3c7e86e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ license = "MIT" [dependencies] anyhow = "1.0.81" argon2 = "0.5.3" +base64 = "0.22.0" +base64-serde = "0.7.0" chrono = "0.4.35" compile-time-run = "0.2.12" dotenvy = "0.15.7" @@ -19,6 +21,7 @@ rand = "0.8.5" serde = { version = "1.0.197", features = ["default"] } thiserror = "1.0.58" uuid = { version = "1.7.0", features = ["v4"] } +tokio = { version = "1.36.0", features = ["sync"] } actix-web = "4.5.1" actix-web-httpauth = "0.8.1" diff --git a/src/api/endpoints/account/mod.rs b/src/api/endpoints/account/mod.rs index b73eee6..dbe5ec3 100644 --- a/src/api/endpoints/account/mod.rs +++ b/src/api/endpoints/account/mod.rs @@ -2,7 +2,6 @@ pub mod invite; use crate::backend::Backend; use actix_web::{post, web, HttpResponse, Responder}; -use log::error; use serde::{Deserialize, Serialize}; use std::str::FromStr; use uuid::Uuid; @@ -59,7 +58,7 @@ pub async fn auth(backend: web::Data, body: web::Json) -> match backend.authenticate(userid, body.password).await { Err(e) => { - error!("{e}"); + log::error!("{e}"); HttpResponse::InternalServerError().finish() } Ok(res) => match res { diff --git a/src/api/endpoints/relay.rs b/src/api/endpoints/relay/mod.rs similarity index 97% rename from src/api/endpoints/relay.rs rename to src/api/endpoints/relay/mod.rs index ca6826c..d8b2d05 100644 --- a/src/api/endpoints/relay.rs +++ b/src/api/endpoints/relay/mod.rs @@ -1,3 +1,5 @@ +pub mod relay_id; + use crate::backend::Backend; use actix_web::{post, web, HttpResponse, Responder}; use actix_web_httpauth::extractors::bearer::BearerAuth; diff --git a/src/api/endpoints/relay/relay_id/mod.rs b/src/api/endpoints/relay/relay_id/mod.rs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/src/api/endpoints/relay/relay_id/mod.rs @@ -0,0 +1 @@ +pub mod user; diff --git a/src/api/endpoints/relay/relay_id/user/mod.rs b/src/api/endpoints/relay/relay_id/user/mod.rs new file mode 100644 index 0000000..698e8be --- /dev/null +++ b/src/api/endpoints/relay/relay_id/user/mod.rs @@ -0,0 +1 @@ +pub mod user_id; diff --git a/src/api/endpoints/relay/relay_id/user/user_id.rs b/src/api/endpoints/relay/relay_id/user/user_id.rs new file mode 100644 index 0000000..ecf7c20 --- /dev/null +++ b/src/api/endpoints/relay/relay_id/user/user_id.rs @@ -0,0 +1,43 @@ +use crate::{backend::Backend, Base64Encoding}; +use actix_web::{get, web, HttpResponse, Responder}; +use actix_web_httpauth::extractors::bearer::BearerAuth; +use log::error; +use serde::Serialize; +use std::str::FromStr; +use uuid::Uuid; + +#[derive(Debug, Serialize)] +struct PublicKeyResponse { + #[serde(with = "Base64Encoding")] + public_key: [u8; 32], +} + +#[get("/relay/{relay_id}/user/{user_id}/public_key")] +pub async fn public_key( + backend: web::Data, + auth: BearerAuth, + path: web::Path<(String, String)>, +) -> impl Responder { + let relay_id = match Uuid::from_str(path.0.as_str()) { + Err(_) => return HttpResponse::BadRequest().finish(), + Ok(uuid) => uuid, + }; + let user_id = match Uuid::from_str(path.1.as_str()) { + Err(_) => return HttpResponse::BadRequest().finish(), + Ok(uuid) => uuid, + }; + + match backend + .get_public_key_relay(auth.token(), relay_id, user_id) + .await + { + Err(e) => { + error!("{e}"); + HttpResponse::InternalServerError().finish() + } + Ok(res) => match res { + Err(e) => e.into(), + Ok(public_key) => HttpResponse::Ok().json(PublicKeyResponse { public_key }), + }, + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index e736ca5..02bd6bc 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,7 +1,6 @@ use crate::{backend::Backend, config::Config}; use actix_web::{web, App, HttpServer}; use anyhow::Result; -use log::info; mod endpoints; @@ -14,10 +13,11 @@ pub async fn start(config: &Config, backend: Backend) -> Result<()> { .service(endpoints::account::auth) .service(endpoints::account::invite::new) .service(endpoints::relay::create) + .service(endpoints::relay::relay_id::user::user_id::public_key) }) .bind((config.addr.as_str(), config.port))?; - info!("API starting"); + log::info!("API starting"); if let Some(threads) = config.threads { server.workers(threads as usize).run().await?; } else { diff --git a/src/backend/db_structures.rs b/src/backend/db_structures.rs index 40281c7..ee62e06 100644 --- a/src/backend/db_structures.rs +++ b/src/backend/db_structures.rs @@ -1,4 +1,4 @@ -use sqlx::types::chrono::NaiveDateTime; +use sqlx::{types::chrono::NaiveDateTime, FromRow}; pub struct InviteTokensRow { pub token: String, @@ -22,9 +22,10 @@ pub struct RelaysRow { pub secret: String, } +#[derive(FromRow)] pub struct RelayIndexRow { + pub private_id: String, pub public_id: String, - pub public_key: [u8; 32], - pub auth: String, + pub public_key: Vec, pub name: String, } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 99e872f..0b51918 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -5,17 +5,18 @@ mod relay; mod tokens; mod user; -use crate::{backend::relay::Relay, config::Config}; +use crate::config::Config; use anyhow::Result; -use log::info; use sqlx::MySqlPool; -use std::collections::HashMap; -use uuid::Uuid; +use tokio::sync::{mpsc, oneshot}; #[derive(Debug)] pub struct Backend { pub pool: MySqlPool, - pub relays: HashMap, + pub relays_manager: mpsc::UnboundedSender<( + relay::manager::Request, + oneshot::Sender, + )>, } impl Backend { @@ -60,11 +61,13 @@ impl Backend { .execute(&pool) .await?; - info!("Backend initialized"); + log::info!("Database initialized"); + + let tx = relay::manager::start(pool.to_owned()).await; Ok(Self { pool, - relays: HashMap::new(), + relays_manager: tx, }) } } diff --git a/src/backend/relay/manager.rs b/src/backend/relay/manager.rs new file mode 100644 index 0000000..13cf53b --- /dev/null +++ b/src/backend/relay/manager.rs @@ -0,0 +1,154 @@ +use crate::backend::{ + db_structures::{RelayIndexRow, RelaysRow}, + error::Error, + relay::{Relay, UserIndex}, +}; +use anyhow::{anyhow, bail, Result}; +use sqlx::MySqlPool; +use std::{ + collections::{HashMap, VecDeque}, + fmt::{Display, Formatter}, + str::FromStr, +}; +use tokio::sync::{ + mpsc, + mpsc::{UnboundedReceiver, UnboundedSender}, + oneshot, +}; +use uuid::Uuid; + +pub enum Request { + FetchUserPublicKey { + requester: Uuid, + relay: Uuid, + user: Uuid, + }, +} + +#[derive(Debug)] +pub enum Response { + FetchUserPublicKey(Result>), +} + +impl Display for Response { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Response::{}", + match self { + Response::FetchUserPublicKey(_) => "FetchUserPublicKey", + } + )?; + Ok(()) + } +} + +pub struct Manager { + pub pool: MySqlPool, + pub relays: HashMap, +} + +impl Manager { + /// Returns a reference to a relay. If the relay is not loaded into the + /// runtime manager, it will be loaded by this function. + pub async fn get_relay(&mut self, relay_id: Uuid) -> Result> { + if self.relays.get(&relay_id).is_none() { + match sqlx::query_as!( + RelaysRow, + r#"SELECT * FROM Relays WHERE id = ? LIMIT 1;"#, + relay_id.as_bytes().as_slice() + ) + .fetch_one(&self.pool) + .await + { + Err(sqlx::Error::RowNotFound) => return Ok(Err(Error::RelayNotFound)), + Err(e) => return Err(e.into()), + Ok(_) => { + let mut user_index = HashMap::new(); + let mut user_map = HashMap::new(); + + let rows: Vec = + sqlx::query_as(&format!("SELECT * FROM RelayIndex_{};", relay_id.simple())) + .fetch_all(&self.pool) + .await?; + + for row in rows { + let public_id = Uuid::from_str(&row.public_id)?; + + user_map.insert(Uuid::from_str(&row.private_id)?, public_id); + user_index.insert( + public_id, + UserIndex { + public_key: row.public_key.as_slice().try_into()?, + name: row.name, + messages: VecDeque::new(), + }, + ); + } + + let relay = Relay { + user_index, + user_map, + }; + + self.relays.insert(relay_id, relay); + } + } + } + + if let Some(relay) = self.relays.get(&relay_id) { + Ok(Ok(relay)) + } else { + bail!("!!! This should never happen !!! relay not found in runtime manager, even tho it was there before.") + } + } + + async fn handle_request( + &mut self, + request: Request, + tx: oneshot::Sender, + ) -> Result<()> { + match request { + Request::FetchUserPublicKey { + requester, + relay, + user, + } => tx + .send(Response::FetchUserPublicKey( + self.get_public_key(requester, relay, user).await, + )) + .map_err(|_| anyhow!("Failed to send response back to oneshot"))?, + }; + + Ok(()) + } +} + +async fn manager(pool: MySqlPool, mut rx: UnboundedReceiver<(Request, oneshot::Sender)>) { + let mut manager = Manager { + pool, + relays: HashMap::new(), + }; + + log::info!("Relays runtime manager initialized"); + + loop { + let (request, tx) = match rx.recv().await { + Some(msg) => msg, + None => panic!("The relays runtime manager message queue lost all senders!"), + }; + + match manager.handle_request(request, tx).await { + Ok(_) => (), + Err(e) => log::error!("Relays runtime manager: {e}"), + }; + } +} + +pub async fn start(pool: MySqlPool) -> UnboundedSender<(Request, oneshot::Sender)> { + let (tx, rx) = mpsc::unbounded_channel(); + + tokio::task::spawn(manager(pool, rx)); + + tx +} diff --git a/src/backend/relay.rs b/src/backend/relay/mod.rs similarity index 52% rename from src/backend/relay.rs rename to src/backend/relay/mod.rs index 9ec4545..679e756 100644 --- a/src/backend/relay.rs +++ b/src/backend/relay/mod.rs @@ -1,9 +1,7 @@ -use crate::backend::{ - db_structures::{RelayIndexRow, RelaysRow}, - error::Error, - user::IntoUser, - Backend, -}; +pub mod manager; +pub mod user; + +use crate::backend::{error::Error, relay, user::IntoUser, Backend}; use anyhow::{bail, Result}; use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, @@ -11,6 +9,7 @@ use argon2::{ }; use rand::distributions::DistString; use std::collections::{HashMap, VecDeque}; +use tokio::sync::oneshot; use uuid::Uuid; #[derive(Debug)] @@ -32,13 +31,23 @@ pub struct Relay { user_map: HashMap, } +impl Relay { + pub fn is_user_in_relay(&self, private_id: &Uuid) -> bool { + self.user_map.contains_key(private_id) + } + + pub fn get_user_index(&self, public_id: &Uuid) -> Result<&UserIndex, Error> { + match self.user_index.get(public_id) { + Some(index) => Ok(index), + None => Err(Error::UserNotFound), + } + } +} + impl Backend { /// Creates the structures for a new relay - pub async fn create_relay( - &self, - user: impl IntoUser, - ) -> anyhow::Result> { - let _user = match user.into_user(&self).await? { + pub async fn create_relay(&self, user: impl IntoUser) -> Result> { + let _user = match user.into_user(&self.pool).await? { Ok(user) => user, Err(e) => return Ok(Err(e)), }; @@ -64,8 +73,8 @@ impl Backend { sqlx::query(&format!( "CREATE TABLE RelayIndex_{} ( public_id UUID NOT NULL PRIMARY KEY, + private_id UUID NOT NULL, public_key BINARY(32) NOT NULL, - auth VARCHAR(128) NOT NULL, name VARCHAR(32) );", relay_id.simple() @@ -76,35 +85,31 @@ impl Backend { Ok(Ok((relay_id, secret))) } - pub async fn get_relay(&mut self, relay_id: Uuid) -> Result> { - // can't use early return pattern, because of the borrow checker :/ - if self.relays.get(&relay_id).is_none() { - match sqlx::query_as!( - RelaysRow, - r#"SELECT * FROM Relays WHERE id = ? LIMIT 1;"#, - relay_id.as_bytes().as_slice() - ) - .fetch_one(&self.pool) - .await - { - Err(sqlx::Error::RowNotFound) => return Ok(Err(Error::RelayNotFound)), - Err(e) => return Err(e.into()), - Ok(_) => { - // the hashmaps will be filled, as soon, as clients subscribe to the relay - let relay = Relay { - user_index: HashMap::new(), - user_map: HashMap::new(), - }; + pub async fn get_public_key_relay( + &self, + requester: impl IntoUser, + relay: Uuid, + user: Uuid, + ) -> Result> { + let requester = match requester.into_user(&self.pool).await? { + Err(e) => return Ok(Err(e)), + Ok(user) => user, + }; - self.relays.insert(relay_id, relay); - } - } - } + let (tx, rx) = oneshot::channel::(); - if let Some(relay) = self.relays.get(&relay_id) { - Ok(Ok(relay)) - } else { - bail!("!!! This should never happen !!! relay not found in runtime manager, even tho it was there before.") + self.relays_manager.send(( + manager::Request::FetchUserPublicKey { + requester: requester.uuid, + relay, + user, + }, + tx, + ))?; + + match rx.await? { + manager::Response::FetchUserPublicKey(response) => response, + resp => bail!("Relay runtime wrong response error: expected Response::FetchUserPublicKey, got: {resp}") } } } diff --git a/src/backend/relay/user.rs b/src/backend/relay/user.rs new file mode 100644 index 0000000..dfadd69 --- /dev/null +++ b/src/backend/relay/user.rs @@ -0,0 +1,40 @@ +use crate::backend::{ + error::Error, + relay::manager::{self, Manager}, + user::IntoUser, + Backend, +}; +use anyhow::Result; +use uuid::Uuid; + +impl Manager { + pub async fn get_public_key( + &mut self, + user: impl IntoUser, + relay_id: Uuid, + user_id: Uuid, + ) -> Result> { + let requester = match user.into_user(&self.pool).await? { + Ok(user) => user, + Err(e) => return Ok(Err(e)), + }; + + let requested_relay = match self.get_relay(relay_id).await? { + Err(e) => return Ok(Err(e)), + Ok(relay) => relay, + }; + + if !requested_relay.is_user_in_relay(&requester.uuid) { + return Ok(Err(Error::PermissionDenied( + "You're not part of the relay.", + ))); + } + + let requested_user = match requested_relay.get_user_index(&user_id) { + Err(e) => return Ok(Err(e)), + Ok(user) => user, + }; + + Ok(Ok(requested_user.public_key)) + } +} diff --git a/src/backend/tokens.rs b/src/backend/tokens.rs index a897a70..33ed228 100644 --- a/src/backend/tokens.rs +++ b/src/backend/tokens.rs @@ -8,40 +8,41 @@ use crate::backend::{ use argon2::password_hash::rand_core::OsRng; use chrono::{Days, Utc}; use rand::distributions::DistString; +use sqlx::MySqlPool; use std::str::FromStr; use uuid::Uuid; -impl Backend { - /// Returns the UUID of the user who owns the auth token. - pub async fn resolve_auth_token( - &self, - token: &str, - ) -> anyhow::Result> { - match sqlx::query_as!( - AuthTokensRow, - r#"SELECT * FROM AuthTokens WHERE token = ? LIMIT 1;"#, - token - ) - .fetch_one(&self.pool) - .await - { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(Err(Error::InvalidToken)), - _ => Err(e.into()), - }, - Ok(row) => { - if row.expire > Utc::now().naive_utc() { - Ok(Ok(Uuid::from_str(&row.userid)?)) - } else { - sqlx::query!(r#"DELETE FROM AuthTokens WHERE token = ?;"#, token) - .execute(&self.pool) - .await?; - Ok(Err(Error::TokenExpired)) - } +/// Returns the UUID of the user who owns the auth token. +pub async fn resolve_auth_token( + pool: &MySqlPool, + token: &str, +) -> anyhow::Result> { + match sqlx::query_as!( + AuthTokensRow, + r#"SELECT * FROM AuthTokens WHERE token = ? LIMIT 1;"#, + token + ) + .fetch_one(pool) + .await + { + Err(e) => match e { + sqlx::Error::RowNotFound => Ok(Err(Error::InvalidToken)), + _ => Err(e.into()), + }, + Ok(row) => { + if row.expire > Utc::now().naive_utc() { + Ok(Ok(Uuid::from_str(&row.userid)?)) + } else { + sqlx::query!(r#"DELETE FROM AuthTokens WHERE token = ?;"#, token) + .execute(pool) + .await?; + Ok(Err(Error::TokenExpired)) } } } +} +impl Backend { /// Check whether an invite-token is valid or not. pub async fn check_invite_token( &self, @@ -77,7 +78,7 @@ impl Backend { &self, user: impl IntoUser, ) -> anyhow::Result> { - let user = match user.into_user(&self).await? { + let user = match user.into_user(&self.pool).await? { Ok(user) => user, Err(e) => return Ok(Err(e)), }; diff --git a/src/backend/user.rs b/src/backend/user.rs index a9160d0..e1b5bb2 100644 --- a/src/backend/user.rs +++ b/src/backend/user.rs @@ -1,4 +1,7 @@ -use crate::backend::{db_structures::UsersRow, error::Error, permissions::Permission, Backend}; +use crate::backend::{ + db_structures::UsersRow, error::Error, permissions::Permission, tokens::resolve_auth_token, + Backend, +}; use anyhow::{bail, Result}; use argon2::{ password_hash::{rand_core::OsRng, SaltString}, @@ -6,12 +9,12 @@ use argon2::{ }; use chrono::Days; use rand::distributions::DistString; -use sqlx::types::chrono::Utc; +use sqlx::{types::chrono::Utc, MySqlPool}; use std::str::FromStr; use uuid::Uuid; pub struct User { - uuid: Uuid, + pub uuid: Uuid, password: String, permissions: u16, } @@ -60,50 +63,50 @@ impl User { } pub trait IntoUser { - async fn into_user(self, backend: &Backend) -> Result>; + async fn into_user(self, pool: &MySqlPool) -> Result>; } impl IntoUser for Uuid { - async fn into_user(self, backend: &Backend) -> Result> { - backend.get_user(self).await + async fn into_user(self, pool: &MySqlPool) -> Result> { + get_user(pool, self).await } } impl IntoUser for &str { - async fn into_user(self, backend: &Backend) -> Result> { - let userid = match backend.resolve_auth_token(self).await? { + async fn into_user(self, pool: &MySqlPool) -> Result> { + let userid = match resolve_auth_token(pool, self).await? { Ok(userid) => userid, Err(e) => return Ok(Err(e)), }; - userid.into_user(backend).await + userid.into_user(pool).await } } impl IntoUser for User { - async fn into_user(self, _backend: &Backend) -> Result> { + async fn into_user(self, _pool: &MySqlPool) -> Result> { Ok(Ok(self)) } } -impl Backend { - /// Returns detailed information about the user identified by the UUID. - async fn get_user(&self, userid: Uuid) -> Result> { - match sqlx::query_as!( - UsersRow, - r#"SELECT * FROM Users WHERE userid = ? LIMIT 1;"#, - userid.as_bytes().as_slice() - ) - .fetch_one(&self.pool) - .await - { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(Err(Error::UserNotFound)), - _ => Err(e.into()), - }, - Ok(row) => Ok(Ok(row.try_into()?)), - } +/// Returns detailed information about the user identified by the UUID. +async fn get_user(pool: &MySqlPool, userid: Uuid) -> Result> { + match sqlx::query_as!( + UsersRow, + r#"SELECT * FROM Users WHERE userid = ? LIMIT 1;"#, + userid.as_bytes().as_slice() + ) + .fetch_one(pool) + .await + { + Err(e) => match e { + sqlx::Error::RowNotFound => Ok(Err(Error::UserNotFound)), + _ => Err(e.into()), + }, + Ok(row) => Ok(Ok(row.try_into()?)), } +} +impl Backend { /// Creates a new account and returns its UUID. pub async fn account_register( &self, @@ -140,7 +143,7 @@ impl Backend { userid: Uuid, password: String, ) -> Result> { - let user = match userid.into_user(&self).await? { + let user = match userid.into_user(&self.pool).await? { Ok(user) => user, Err(e) => return Ok(Err(e)), }; @@ -152,7 +155,7 @@ impl Backend { let mut token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); // just for the case, that there's some duplication loop { - match self.resolve_auth_token(&token).await? { + match resolve_auth_token(&self.pool, &token).await? { Ok(_) => token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48), Err(Error::InvalidToken) | Err(Error::TokenExpired) => break, Err(e) => bail!("!THIS ERROR SHOULDN'T BE HERE! -> {e}"), diff --git a/src/main.rs b/src/main.rs index 238a3ce..951fdfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,16 +4,18 @@ mod config; use crate::backend::Backend; use anyhow::Result; +use base64_serde::base64_serde_type; use compile_time_run::run_command_str; use config::Config; -use log::info; + +base64_serde_type!(pub Base64Encoding, base64::engine::general_purpose::STANDARD); #[actix_web::main] async fn main() -> Result<()> { dotenvy::dotenv()?; env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - info!( + log::info!( "Starting Cipher Relay (v{} - git: {}/{})", env!("CARGO_PKG_VERSION"), run_command_str!("git", "branch", "--show-current"),