feat: add scoped music tokens

This commit is contained in:
dusk 2023-05-05 09:29:54 +03:00
parent c829eb8288
commit a148d24228
Signed by: dusk
GPG Key ID: 1D8F8FAF2294D6EA
3 changed files with 130 additions and 17 deletions

View File

@ -9,7 +9,7 @@ use async_tungstenite::{
use axum::{
extract::{
ws::{CloseFrame as AxumCloseFrame, Message as AxumMessage, WebSocket, WebSocketUpgrade},
ConnectInfo, Query, State,
ConnectInfo, Path, Query, State,
},
headers::UserAgent,
middleware::Next,
@ -32,9 +32,7 @@ use tower_http::{
};
use tracing::{Instrument, Span};
use crate::{get_conf, AppState};
const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;
use crate::{get_conf, AppState, B64};
#[derive(Deserialize)]
struct Auth {
@ -137,8 +135,10 @@ pub(super) async fn handler(state: AppState) -> Result<Router, AppError> {
.layer(axum::middleware::from_fn(block_external_ips));
let router = Router::new()
.route("/token/generate_for_music/:id", get(generate_scoped_token))
.route("/thumbnail/:id", get(http))
.route("/audio/external_id/:id", get(http))
.route("/audio/scoped/:id", get(get_scoped_music))
.route("/", get(metadata_ws))
.layer(SetSensitiveRequestHeadersLayer::new([AUTHORIZATION]))
.layer(trace_layer)
@ -174,6 +174,58 @@ async fn generate_token(State(app): State<AppState>) -> Result<axum::response::R
Ok(token.into_response())
}
async fn generate_scoped_token(
State(app): State<AppState>,
Query(query): Query<Auth>,
Path(music_id): Path<String>,
) -> Result<axum::response::Response, AppError> {
let maybe_token = query.token;
'ok: {
tracing::debug!("verifying token: {maybe_token:?}");
if let Some(token) = maybe_token {
if app.tokens.verify(token).await? {
break 'ok;
}
}
return Ok((
StatusCode::UNAUTHORIZED,
"Invalid token or token not present",
)
.into_response());
}
// generate token
let token = app.scoped_tokens.generate_for_id(music_id).await;
Ok(token.into_response())
}
async fn get_scoped_music(
State(app): State<AppState>,
Path(token): Path<String>,
) -> Result<Response<Body>, AppError> {
if let Some(music_id) = app.scoped_tokens.verify(token).await {
Ok(app
.client
.request(
Request::builder()
.uri(format!(
"http://{}:{}/audio/external_id/{}",
app.musikcubed_address, app.musikcubed_http_port, music_id
))
.header(AUTHORIZATION, app.musikcubed_auth_header_value.clone())
.body(Body::empty())
.expect("cant fail"),
)
.await?)
} else {
Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body("Invalid scoped token".to_string().into())
.expect("cant fail"))
}
}
async fn http(
State(app): State<AppState>,
Query(query): Query<Auth>,
@ -212,11 +264,8 @@ async fn http(
.expect("cant fail"));
}
let auth = B64.encode(format!("default:{}", app.musikcubed_password));
req.headers_mut().insert(
AUTHORIZATION,
format!("Basic {auth}").parse().expect("valid header value"),
);
req.headers_mut()
.insert(AUTHORIZATION, app.musikcubed_auth_header_value.clone());
Ok(app.client.request(req).await?)
}

View File

@ -1,10 +1,11 @@
use std::{net::SocketAddr, process::ExitCode, sync::Arc};
use axum_server::tls_rustls::RustlsConfig;
use base64::Engine;
use dotenvy::Error as DotenvError;
use error::AppError;
use hyper::{client::HttpConnector, Body};
use token::Tokens;
use token::{MusicScopedTokens, Tokens};
use tracing::{info, warn};
use tracing_subscriber::prelude::*;
@ -82,29 +83,41 @@ type Client = hyper::Client<HttpConnector, Body>;
type AppState = Arc<AppStateInternal>;
const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;
#[derive(Clone)]
struct AppStateInternal {
client: Client,
tokens: Tokens,
scoped_tokens: MusicScopedTokens,
tokens_path: String,
public_port: u16,
musikcubed_address: String,
musikcubed_http_port: u16,
musikcubed_metadata_port: u16,
musikcubed_password: String,
musikcubed_auth_header_value: http::HeaderValue,
}
impl AppStateInternal {
async fn new(public_port: u16) -> Result<Self, AppError> {
let musikcubed_password = get_conf("MUSIKCUBED_PASSWORD")?;
let tokens_path = get_conf("TOKENS_FILE")?;
let this = Self {
public_port,
musikcubed_address: get_conf("MUSIKCUBED_ADDRESS")?,
musikcubed_http_port: get_conf("MUSIKCUBED_HTTP_PORT")?.parse()?,
musikcubed_metadata_port: get_conf("MUSIKCUBED_METADATA_PORT")?.parse()?,
musikcubed_password: get_conf("MUSIKCUBED_PASSWORD")?,
musikcubed_auth_header_value: format!(
"Basic {}",
B64.encode(format!("default:{}", musikcubed_password))
)
.parse()
.expect("valid header value"),
musikcubed_password,
client: Client::new(),
tokens: Tokens::read(&tokens_path).await?,
scoped_tokens: MusicScopedTokens::new(get_conf("SCOPED_EXPIRY_DURATION")?.parse()?),
tokens_path,
};
Ok(this)

View File

@ -1,16 +1,29 @@
use rand::Rng;
use scc::HashSet;
use scc::{HashMap, HashSet};
use std::borrow::Cow;
use std::fmt::Write;
use std::path::Path;
use std::sync::Arc;
use std::time::UNIX_EPOCH;
use crate::error::AppError;
fn get_current_time() -> u64 {
UNIX_EPOCH.elapsed().unwrap().as_secs()
}
fn hash_string(data: &[u8]) -> Result<String, argon2::Error> {
argon2::hash_encoded(data, "11111111".as_bytes(), &argon2::Config::default())
}
fn generate_random_string(len: usize) -> String {
rand::thread_rng()
.sample_iter(rand::distributions::Alphanumeric)
.take(len)
.map(|c| c as char)
.collect::<String>()
}
#[derive(Debug, Clone)]
pub(crate) struct Tokens {
hashed: Arc<HashSet<Cow<'static, str>>>,
@ -49,11 +62,7 @@ impl Tokens {
}
pub async fn generate(&self) -> Result<String, AppError> {
let token = rand::thread_rng()
.sample_iter(rand::distributions::Alphanumeric)
.take(30)
.map(|c| c as char)
.collect::<String>();
let token = generate_random_string(30);
let token_hash = hash_string(token.as_bytes())?;
@ -71,3 +80,45 @@ impl Tokens {
self.hashed.clear_async().await;
}
}
#[derive(Clone)]
pub(crate) struct MusicScopedTokens {
map: Arc<HashMap<String, MusicScopedToken>>,
expiry_time: u64,
}
impl MusicScopedTokens {
pub fn new(expiry_time: u64) -> Self {
Self {
map: Arc::new(HashMap::new()),
expiry_time,
}
}
pub async fn generate_for_id(&self, music_id: String) -> String {
let data = MusicScopedToken {
creation: get_current_time(),
music_id,
};
let token = generate_random_string(12);
let _ = self.map.insert_async(token.clone(), data).await;
return token;
}
pub async fn verify(&self, token: impl AsRef<str>) -> Option<String> {
let token = token.as_ref();
let data = self.map.read_async(token, |_, v| v.clone()).await?;
if get_current_time() - data.creation > self.expiry_time {
self.map.remove_async(token).await;
return None;
}
Some(data.music_id)
}
}
#[derive(Clone)]
pub(crate) struct MusicScopedToken {
creation: u64,
music_id: String,
}