ping ssonic server to verify configured auth

This commit is contained in:
Muaz Ahmad 2024-11-27 14:24:21 +05:00
parent f094604f6e
commit 6be3634fdf
10 changed files with 258 additions and 8 deletions

76
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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"),
}
}
}

View file

@ -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(())
}

View file

@ -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
View 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
View 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),
}
}
}

View file

@ -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
View 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 {}

View file

@ -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()
}