feat: add scoped music tokens
This commit is contained in:
parent
c829eb8288
commit
a148d24228
@ -9,7 +9,7 @@ use async_tungstenite::{
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{
|
extract::{
|
||||||
ws::{CloseFrame as AxumCloseFrame, Message as AxumMessage, WebSocket, WebSocketUpgrade},
|
ws::{CloseFrame as AxumCloseFrame, Message as AxumMessage, WebSocket, WebSocketUpgrade},
|
||||||
ConnectInfo, Query, State,
|
ConnectInfo, Path, Query, State,
|
||||||
},
|
},
|
||||||
headers::UserAgent,
|
headers::UserAgent,
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
@ -32,9 +32,7 @@ use tower_http::{
|
|||||||
};
|
};
|
||||||
use tracing::{Instrument, Span};
|
use tracing::{Instrument, Span};
|
||||||
|
|
||||||
use crate::{get_conf, AppState};
|
use crate::{get_conf, AppState, B64};
|
||||||
|
|
||||||
const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Auth {
|
struct Auth {
|
||||||
@ -137,8 +135,10 @@ pub(super) async fn handler(state: AppState) -> Result<Router, AppError> {
|
|||||||
.layer(axum::middleware::from_fn(block_external_ips));
|
.layer(axum::middleware::from_fn(block_external_ips));
|
||||||
|
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
|
.route("/token/generate_for_music/:id", get(generate_scoped_token))
|
||||||
.route("/thumbnail/:id", get(http))
|
.route("/thumbnail/:id", get(http))
|
||||||
.route("/audio/external_id/:id", get(http))
|
.route("/audio/external_id/:id", get(http))
|
||||||
|
.route("/audio/scoped/:id", get(get_scoped_music))
|
||||||
.route("/", get(metadata_ws))
|
.route("/", get(metadata_ws))
|
||||||
.layer(SetSensitiveRequestHeadersLayer::new([AUTHORIZATION]))
|
.layer(SetSensitiveRequestHeadersLayer::new([AUTHORIZATION]))
|
||||||
.layer(trace_layer)
|
.layer(trace_layer)
|
||||||
@ -174,6 +174,58 @@ async fn generate_token(State(app): State<AppState>) -> Result<axum::response::R
|
|||||||
Ok(token.into_response())
|
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(
|
async fn http(
|
||||||
State(app): State<AppState>,
|
State(app): State<AppState>,
|
||||||
Query(query): Query<Auth>,
|
Query(query): Query<Auth>,
|
||||||
@ -212,11 +264,8 @@ async fn http(
|
|||||||
.expect("cant fail"));
|
.expect("cant fail"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth = B64.encode(format!("default:{}", app.musikcubed_password));
|
req.headers_mut()
|
||||||
req.headers_mut().insert(
|
.insert(AUTHORIZATION, app.musikcubed_auth_header_value.clone());
|
||||||
AUTHORIZATION,
|
|
||||||
format!("Basic {auth}").parse().expect("valid header value"),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(app.client.request(req).await?)
|
Ok(app.client.request(req).await?)
|
||||||
}
|
}
|
||||||
|
17
src/main.rs
17
src/main.rs
@ -1,10 +1,11 @@
|
|||||||
use std::{net::SocketAddr, process::ExitCode, sync::Arc};
|
use std::{net::SocketAddr, process::ExitCode, sync::Arc};
|
||||||
|
|
||||||
use axum_server::tls_rustls::RustlsConfig;
|
use axum_server::tls_rustls::RustlsConfig;
|
||||||
|
use base64::Engine;
|
||||||
use dotenvy::Error as DotenvError;
|
use dotenvy::Error as DotenvError;
|
||||||
use error::AppError;
|
use error::AppError;
|
||||||
use hyper::{client::HttpConnector, Body};
|
use hyper::{client::HttpConnector, Body};
|
||||||
use token::Tokens;
|
use token::{MusicScopedTokens, Tokens};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
|
|
||||||
@ -82,29 +83,41 @@ type Client = hyper::Client<HttpConnector, Body>;
|
|||||||
|
|
||||||
type AppState = Arc<AppStateInternal>;
|
type AppState = Arc<AppStateInternal>;
|
||||||
|
|
||||||
|
const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppStateInternal {
|
struct AppStateInternal {
|
||||||
client: Client,
|
client: Client,
|
||||||
tokens: Tokens,
|
tokens: Tokens,
|
||||||
|
scoped_tokens: MusicScopedTokens,
|
||||||
tokens_path: String,
|
tokens_path: String,
|
||||||
public_port: u16,
|
public_port: u16,
|
||||||
musikcubed_address: String,
|
musikcubed_address: String,
|
||||||
musikcubed_http_port: u16,
|
musikcubed_http_port: u16,
|
||||||
musikcubed_metadata_port: u16,
|
musikcubed_metadata_port: u16,
|
||||||
musikcubed_password: String,
|
musikcubed_password: String,
|
||||||
|
musikcubed_auth_header_value: http::HeaderValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppStateInternal {
|
impl AppStateInternal {
|
||||||
async fn new(public_port: u16) -> Result<Self, AppError> {
|
async fn new(public_port: u16) -> Result<Self, AppError> {
|
||||||
|
let musikcubed_password = get_conf("MUSIKCUBED_PASSWORD")?;
|
||||||
let tokens_path = get_conf("TOKENS_FILE")?;
|
let tokens_path = get_conf("TOKENS_FILE")?;
|
||||||
let this = Self {
|
let this = Self {
|
||||||
public_port,
|
public_port,
|
||||||
musikcubed_address: get_conf("MUSIKCUBED_ADDRESS")?,
|
musikcubed_address: get_conf("MUSIKCUBED_ADDRESS")?,
|
||||||
musikcubed_http_port: get_conf("MUSIKCUBED_HTTP_PORT")?.parse()?,
|
musikcubed_http_port: get_conf("MUSIKCUBED_HTTP_PORT")?.parse()?,
|
||||||
musikcubed_metadata_port: get_conf("MUSIKCUBED_METADATA_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(),
|
client: Client::new(),
|
||||||
tokens: Tokens::read(&tokens_path).await?,
|
tokens: Tokens::read(&tokens_path).await?,
|
||||||
|
scoped_tokens: MusicScopedTokens::new(get_conf("SCOPED_EXPIRY_DURATION")?.parse()?),
|
||||||
tokens_path,
|
tokens_path,
|
||||||
};
|
};
|
||||||
Ok(this)
|
Ok(this)
|
||||||
|
63
src/token.rs
63
src/token.rs
@ -1,16 +1,29 @@
|
|||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use scc::HashSet;
|
use scc::{HashMap, HashSet};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::UNIX_EPOCH;
|
||||||
|
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|
||||||
|
fn get_current_time() -> u64 {
|
||||||
|
UNIX_EPOCH.elapsed().unwrap().as_secs()
|
||||||
|
}
|
||||||
|
|
||||||
fn hash_string(data: &[u8]) -> Result<String, argon2::Error> {
|
fn hash_string(data: &[u8]) -> Result<String, argon2::Error> {
|
||||||
argon2::hash_encoded(data, "11111111".as_bytes(), &argon2::Config::default())
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Tokens {
|
pub(crate) struct Tokens {
|
||||||
hashed: Arc<HashSet<Cow<'static, str>>>,
|
hashed: Arc<HashSet<Cow<'static, str>>>,
|
||||||
@ -49,11 +62,7 @@ impl Tokens {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate(&self) -> Result<String, AppError> {
|
pub async fn generate(&self) -> Result<String, AppError> {
|
||||||
let token = rand::thread_rng()
|
let token = generate_random_string(30);
|
||||||
.sample_iter(rand::distributions::Alphanumeric)
|
|
||||||
.take(30)
|
|
||||||
.map(|c| c as char)
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let token_hash = hash_string(token.as_bytes())?;
|
let token_hash = hash_string(token.as_bytes())?;
|
||||||
|
|
||||||
@ -71,3 +80,45 @@ impl Tokens {
|
|||||||
self.hashed.clear_async().await;
|
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,
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user