#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
#[cfg(feature = "api")]
pub mod api;
mod db;
use actix_web::{FromRequest, HttpRequest, dev::Payload, error::ErrorUnauthorized, http};
use futures::future::{Ready, err, ok};
use moosicbox_json_utils::{ParseError, database::DatabaseFetchError, serde_json::ToValue};
use serde_json::Value;
use switchy_database::config::ConfigDatabase;
use thiserror::Error;
use uuid::Uuid;
use crate::db::{create_client_access_token, get_client_access_token};
#[derive(Debug, Error)]
pub enum AuthError {
#[error(transparent)]
DatabaseFetch(#[from] DatabaseFetchError),
#[error(transparent)]
Parse(#[from] ParseError),
#[error(transparent)]
Http(#[from] switchy_http::Error),
#[error("Failed to register client")]
RegisterClient,
#[error("Unauthorized")]
Unauthorized,
}
#[cfg(feature = "api")]
pub(crate) async fn get_credentials_from_magic_token(
db: &ConfigDatabase,
magic_token: &str,
) -> Result<Option<(String, String)>, DatabaseFetchError> {
if let Some((client_id, access_token)) =
crate::db::get_credentials_from_magic_token(db, magic_token).await?
{
Ok(Some((client_id, access_token)))
} else {
Ok(None)
}
}
#[cfg(feature = "api")]
async fn tunnel_magic_token(
tunnel_host: &str,
client_id: &str,
access_token: &str,
magic_token: &str,
) -> Result<bool, AuthError> {
let url =
format!("{tunnel_host}/auth/magic-token?clientId={client_id}&magicToken={magic_token}");
let value: Value = switchy_http::Client::new()
.post(&url)
.header(switchy_http::Header::Authorization.as_ref(), access_token)
.send()
.await?
.json()
.await?;
Ok(value
.get("success")
.is_some_and(|success| success.as_bool().unwrap_or(false)))
}
#[cfg(feature = "api")]
pub(crate) async fn create_magic_token(
db: &ConfigDatabase,
tunnel_host: Option<String>,
) -> Result<String, AuthError> {
let magic_token = Uuid::new_v4().to_string();
if let Some((client_id, access_token)) = { get_client_access_token(db).await? } {
if let Some(tunnel_host) = tunnel_host {
tunnel_magic_token(&tunnel_host, &client_id, &access_token, &magic_token).await?;
}
crate::db::save_magic_token(db, &magic_token, &client_id, &access_token).await?;
}
Ok(magic_token)
}
fn create_client_id() -> String {
Uuid::new_v4().to_string()
}
pub async fn get_client_id_and_access_token(
db: &ConfigDatabase,
host: &str,
) -> Result<(String, String), AuthError> {
if let Ok(Some((client_id, token))) = get_client_access_token(db).await {
Ok((client_id, token))
} else {
let client_id = create_client_id();
let token = register_client(host, &client_id)
.await?
.ok_or(AuthError::RegisterClient)?;
create_client_access_token(db, &client_id, &token).await?;
Ok((client_id, token))
}
}
async fn register_client(host: &str, client_id: &str) -> Result<Option<String>, AuthError> {
let url = format!("{host}/auth/register-client?clientId={client_id}");
Ok(switchy_http::Client::new()
.post(&url)
.header(
switchy_http::Header::Authorization.as_ref(),
&std::env::var("TUNNEL_ACCESS_TOKEN").expect("TUNNEL_ACCESS_TOKEN not set"),
)
.send()
.await?
.json::<Value>()
.await?
.to_value("token")?)
}
pub struct NonTunnelRequestAuthorized;
impl FromRequest for NonTunnelRequestAuthorized {
type Error = actix_web::Error;
type Future = Ready<Result<Self, actix_web::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
if is_authorized(req) {
ok(Self)
} else {
log::warn!(
"Unauthorized NonTunnelRequestAuthorized request to '{}'",
req.path()
);
err(ErrorUnauthorized("Unauthorized"))
}
}
}
fn is_authorized(req: &HttpRequest) -> bool {
if let Some(user_agent) = req.headers().get(http::header::USER_AGENT) {
if let Ok(user_agent) = user_agent.to_str() {
return user_agent != "MOOSICBOX_TUNNEL";
}
}
true
}
pub async fn fetch_signature_token(
host: &str,
client_id: &str,
access_token: &str,
) -> Result<Option<String>, AuthError> {
let url = format!("{host}/auth/signature-token?clientId={client_id}");
log::debug!("Fetching signature token for client_id={client_id}");
let response = switchy_http::Client::new()
.post(&url)
.header(switchy_http::Header::Authorization.as_ref(), access_token)
.send()
.await?;
if response.status() == switchy_http::models::StatusCode::Unauthorized {
return Err(AuthError::Unauthorized);
}
Ok(response.json::<Value>().await?.to_value("token")?)
}