ping ssonic server to verify configured auth
This commit is contained in:
parent
f094604f6e
commit
6be3634fdf
10 changed files with 258 additions and 8 deletions
76
Cargo.lock
generated
76
Cargo.lock
generated
|
@ -114,6 +114,12 @@ version = "3.16.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.8.0"
|
||||
|
@ -951,6 +957,12 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
|
@ -1169,6 +1181,15 @@ version = "0.3.31"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
|
@ -1197,6 +1218,36 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
|
@ -1265,6 +1316,7 @@ dependencies = [
|
|||
"base64",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
|
@ -1593,10 +1645,13 @@ name = "subtails"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"md5",
|
||||
"pipewire",
|
||||
"rand",
|
||||
"ratatui",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml",
|
||||
]
|
||||
|
||||
|
@ -2196,6 +2251,27 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.5"
|
||||
|
|
|
@ -5,8 +5,11 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
crossterm = "0.28.1"
|
||||
md5 = "0.7.0"
|
||||
pipewire = "0.8.0"
|
||||
rand = "0.8.5"
|
||||
ratatui = "0.29.0"
|
||||
reqwest = "0.12.9"
|
||||
reqwest = { version = "0.12.9", features = ["blocking", "json"] }
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
toml = "0.8.19"
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::fmt::Display;
|
|||
pub enum ConfigError {
|
||||
ConfigNotFound,
|
||||
MissingConfigValue(&'static str),
|
||||
InvalidServerAddress,
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
@ -15,7 +16,8 @@ impl Display for ConfigError {
|
|||
f,
|
||||
"Config File was not found in either the current directory or ~/.config."
|
||||
),
|
||||
Self::MissingConfigValue(var_name) => write!(f, "Neither the Env variable \"{}\" was set, nor the equivalent field in the config file was found.", var_name)
|
||||
Self::MissingConfigValue(var_name) => write!(f, "Neither the Env variable \"{}\" was set, nor the equivalent field in the config file was found.", var_name),
|
||||
Self::InvalidServerAddress => write!(f, "The configured server address is not a valid url"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use std::env::var;
|
||||
|
||||
use reqwest::Url;
|
||||
|
||||
use crate::{config::errors::ConfigError, utils::Error};
|
||||
|
||||
use super::Settings;
|
||||
|
@ -14,6 +16,17 @@ pub fn validate_config(settings: &mut Settings) -> Result<(), Error> {
|
|||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let url = Url::parse(settings.subsonic.server_address.as_str());
|
||||
match url {
|
||||
Err(_) => return Err(Box::new(ConfigError::InvalidServerAddress)),
|
||||
Ok(u) => {
|
||||
if u.scheme() != "http" && u.scheme() != "https" {
|
||||
return Err(Box::new(ConfigError::InvalidServerAddress));
|
||||
}
|
||||
settings.subsonic.server_address =
|
||||
format!("{}{}", settings.subsonic.server_address, "/rest")
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ fn init() -> Result<Receiver<utils::Error>, utils::Error> {
|
|||
audio::init(settings.clone(), error_in.clone(), player_events_in.clone())?;
|
||||
let api_event_chan =
|
||||
ssonic::init(settings.clone(), error_in.clone(), player_events_in.clone())?;
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
let mut player = player::init(
|
||||
settings.clone(),
|
||||
audio_event_chan,
|
||||
|
|
71
src/ssonic/client.rs
Normal file
71
src/ssonic/client.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use std::io::Read;
|
||||
|
||||
use reqwest::{blocking::Response, header::ACCEPT};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
ssonic::{errors::APIError, response},
|
||||
utils::{generate_random_salt, Error},
|
||||
};
|
||||
|
||||
const SSONIC_CLIENT: &str = "subtails";
|
||||
const SSONIC_VERSION: &str = "1.16.1";
|
||||
|
||||
use super::APIClient;
|
||||
|
||||
impl APIClient {
|
||||
pub fn begin(&mut self) -> Result<(), Error> {
|
||||
self.validate()?;
|
||||
loop {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(&mut self) -> Result<(), Error> {
|
||||
let response = self.request::<[(&str, String); 0]>("/ping.view", None)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(Box::new(APIError::StatusError(
|
||||
response.status(),
|
||||
"/ping.view",
|
||||
)));
|
||||
}
|
||||
let ssonic_response: response::Response = response.json()?;
|
||||
if ssonic_response.subsonic_response.status != "ok" {
|
||||
return Err(Box::new(APIError::SubsonicError(
|
||||
ssonic_response.subsonic_response.error.unwrap().message,
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_random_token(&self) -> (String, String) {
|
||||
let salt = generate_random_salt();
|
||||
let hash = md5::compute(format!("{}{}", self.settings.subsonic.password, salt));
|
||||
(format!("{:x}", hash), salt)
|
||||
}
|
||||
|
||||
fn request<T: Serialize + ?Sized>(
|
||||
&mut self,
|
||||
uri: &str,
|
||||
extra_params: Option<&T>,
|
||||
) -> Result<Response, Error> {
|
||||
let (token, salt) = self.generate_random_token();
|
||||
let mut req = self
|
||||
.ssonic_client
|
||||
.get(format!("{}{}", self.settings.subsonic.server_address, uri));
|
||||
req = req.query(&[
|
||||
("u", self.settings.subsonic.username.clone()),
|
||||
("t", token),
|
||||
("s", salt),
|
||||
("c", String::from(SSONIC_CLIENT)),
|
||||
("v", String::from(SSONIC_VERSION)),
|
||||
("f", String::from("json")),
|
||||
]);
|
||||
if let Some(params) = extra_params {
|
||||
req = req.query(params);
|
||||
}
|
||||
req = req.header(ACCEPT, "application/json");
|
||||
Ok(req.send()?)
|
||||
}
|
||||
}
|
22
src/ssonic/errors.rs
Normal file
22
src/ssonic/errors.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use reqwest::StatusCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum APIError {
|
||||
StatusError(StatusCode, &'static str),
|
||||
SubsonicError(String),
|
||||
}
|
||||
|
||||
impl std::error::Error for APIError {}
|
||||
|
||||
impl std::fmt::Display for APIError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::StatusError(code, path) => write!(
|
||||
f,
|
||||
"Encountered an error code of {} while executing request on: {}",
|
||||
code, path
|
||||
),
|
||||
Self::SubsonicError(message) => write!(f, "Subsonic API error: {}", message),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@ use std::{
|
|||
thread,
|
||||
};
|
||||
|
||||
use reqwest::blocking::Client;
|
||||
|
||||
use crate::{config::Settings, player::PlayerEvent, utils::Error};
|
||||
|
||||
pub enum APIEvent {}
|
||||
|
@ -15,6 +17,7 @@ pub struct APIClient {
|
|||
error_chan: Sender<Error>,
|
||||
api_requests: Receiver<APIEvent>,
|
||||
player_chan: Sender<PlayerEvent>,
|
||||
ssonic_client: Client,
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
|
@ -23,11 +26,26 @@ pub fn init(
|
|||
player_chan: Sender<PlayerEvent>,
|
||||
) -> Result<Sender<APIEvent>, Error> {
|
||||
let (api_requests_in, api_requests_out) = channel();
|
||||
thread::spawn(|| APIClient {
|
||||
settings,
|
||||
error_chan,
|
||||
api_requests: api_requests_out,
|
||||
player_chan,
|
||||
thread::spawn(move || {
|
||||
let mut client = APIClient {
|
||||
settings,
|
||||
error_chan: error_chan.clone(),
|
||||
api_requests: api_requests_out,
|
||||
player_chan,
|
||||
ssonic_client: Client::new(),
|
||||
};
|
||||
match client.begin() {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
error_chan.send(err).unwrap();
|
||||
thread::park();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(api_requests_in)
|
||||
}
|
||||
|
||||
mod client;
|
||||
mod errors;
|
||||
mod response;
|
||||
|
|
34
src/ssonic/response.rs
Normal file
34
src/ssonic/response.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Response {
|
||||
#[serde(alias = "subsonic-response")]
|
||||
pub subsonic_response: SSonicResponse,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SSonicResponse {
|
||||
pub status: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<Error>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub random_songs: Option<Vec<Song>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub play_queue: Option<PlayQueue>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Error {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Song {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub cover_art: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PlayQueue {}
|
10
src/utils.rs
10
src/utils.rs
|
@ -1 +1,11 @@
|
|||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
pub fn generate_random_salt() -> String {
|
||||
thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(8)
|
||||
.map(|c| c as char)
|
||||
.collect()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue