1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-04 18:29:45 +02:00

Change panics into Result<_, librespot_core::Error>

This commit is contained in:
Roderick van Domburg 2021-12-26 21:18:42 +01:00
parent a297c68913
commit 62461be1fc
No known key found for this signature in database
GPG key ID: A9EF5222A26F0451
69 changed files with 2041 additions and 1331 deletions

View file

@ -1,7 +1,9 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use hyper::{Body, Method, Request};
use serde::Deserialize;
use std::error::Error;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::Error;
pub type SocketAddress = (String, u16);
@ -67,7 +69,7 @@ impl ApResolver {
.collect()
}
pub async fn try_apresolve(&self) -> Result<ApResolveData, Box<dyn Error>> {
pub async fn try_apresolve(&self) -> Result<ApResolveData, Error> {
let req = Request::builder()
.method(Method::GET)
.uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient")

View file

@ -1,54 +1,85 @@
use std::{collections::HashMap, io::Write};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use bytes::Bytes;
use std::collections::HashMap;
use std::io::Write;
use thiserror::Error;
use tokio::sync::oneshot;
use crate::file_id::FileId;
use crate::packet::PacketType;
use crate::spotify_id::SpotifyId;
use crate::util::SeqGenerator;
use crate::{packet::PacketType, util::SeqGenerator, Error, FileId, SpotifyId};
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct AudioKey(pub [u8; 16]);
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct AudioKeyError;
#[derive(Debug, Error)]
pub enum AudioKeyError {
#[error("audio key error")]
AesKey,
#[error("other end of channel disconnected")]
Channel,
#[error("unexpected packet type {0}")]
Packet(u8),
#[error("sequence {0} not pending")]
Sequence(u32),
}
impl From<AudioKeyError> for Error {
fn from(err: AudioKeyError) -> Self {
match err {
AudioKeyError::AesKey => Error::unavailable(err),
AudioKeyError::Channel => Error::aborted(err),
AudioKeyError::Sequence(_) => Error::aborted(err),
AudioKeyError::Packet(_) => Error::unimplemented(err),
}
}
}
component! {
AudioKeyManager : AudioKeyManagerInner {
sequence: SeqGenerator<u32> = SeqGenerator::new(0),
pending: HashMap<u32, oneshot::Sender<Result<AudioKey, AudioKeyError>>> = HashMap::new(),
pending: HashMap<u32, oneshot::Sender<Result<AudioKey, Error>>> = HashMap::new(),
}
}
impl AudioKeyManager {
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) {
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
let seq = BigEndian::read_u32(data.split_to(4).as_ref());
let sender = self.lock(|inner| inner.pending.remove(&seq));
let sender = self
.lock(|inner| inner.pending.remove(&seq))
.ok_or(AudioKeyError::Sequence(seq))?;
if let Some(sender) = sender {
match cmd {
PacketType::AesKey => {
let mut key = [0u8; 16];
key.copy_from_slice(data.as_ref());
let _ = sender.send(Ok(AudioKey(key)));
}
PacketType::AesKeyError => {
warn!(
"error audio key {:x} {:x}",
data.as_ref()[0],
data.as_ref()[1]
);
let _ = sender.send(Err(AudioKeyError));
}
_ => (),
match cmd {
PacketType::AesKey => {
let mut key = [0u8; 16];
key.copy_from_slice(data.as_ref());
sender
.send(Ok(AudioKey(key)))
.map_err(|_| AudioKeyError::Channel)?
}
PacketType::AesKeyError => {
error!(
"error audio key {:x} {:x}",
data.as_ref()[0],
data.as_ref()[1]
);
sender
.send(Err(AudioKeyError::AesKey.into()))
.map_err(|_| AudioKeyError::Channel)?
}
_ => {
trace!(
"Did not expect {:?} AES key packet with data {:#?}",
cmd,
data
);
return Err(AudioKeyError::Packet(cmd as u8).into());
}
}
Ok(())
}
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, AudioKeyError> {
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, Error> {
let (tx, rx) = oneshot::channel();
let seq = self.lock(move |inner| {
@ -57,16 +88,16 @@ impl AudioKeyManager {
seq
});
self.send_key_request(seq, track, file);
rx.await.map_err(|_| AudioKeyError)?
self.send_key_request(seq, track, file)?;
rx.await?
}
fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) {
fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) -> Result<(), Error> {
let mut data: Vec<u8> = Vec::new();
data.write_all(&file.0).unwrap();
data.write_all(&track.to_raw()).unwrap();
data.write_u32::<BigEndian>(seq).unwrap();
data.write_u16::<BigEndian>(0x0000).unwrap();
data.write_all(&file.0)?;
data.write_all(&track.to_raw())?;
data.write_u32::<BigEndian>(seq)?;
data.write_u16::<BigEndian>(0x0000)?;
self.session().send_packet(PacketType::RequestKey, data)
}

View file

@ -7,8 +7,21 @@ use pbkdf2::pbkdf2;
use protobuf::ProtobufEnum;
use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1};
use thiserror::Error;
use crate::protocol::authentication::AuthenticationType;
use crate::{protocol::authentication::AuthenticationType, Error};
#[derive(Debug, Error)]
pub enum AuthenticationError {
#[error("unknown authentication type {0}")]
AuthType(u32),
}
impl From<AuthenticationError> for Error {
fn from(err: AuthenticationError) -> Self {
Error::invalid_argument(err)
}
}
/// The credentials are used to log into the Spotify API.
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -46,7 +59,7 @@ impl Credentials {
username: impl Into<String>,
encrypted_blob: impl AsRef<[u8]>,
device_id: impl AsRef<[u8]>,
) -> Credentials {
) -> Result<Credentials, Error> {
fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> {
let mut data = [0u8];
stream.read_exact(&mut data)?;
@ -91,7 +104,7 @@ impl Credentials {
use aes::cipher::generic_array::GenericArray;
use aes::cipher::{BlockCipher, NewBlockCipher};
let mut data = base64::decode(encrypted_blob).unwrap();
let mut data = base64::decode(encrypted_blob)?;
let cipher = Aes192::new(GenericArray::from_slice(&key));
let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize();
@ -109,19 +122,20 @@ impl Credentials {
};
let mut cursor = io::Cursor::new(blob.as_slice());
read_u8(&mut cursor).unwrap();
read_bytes(&mut cursor).unwrap();
read_u8(&mut cursor).unwrap();
let auth_type = read_int(&mut cursor).unwrap();
let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap();
read_u8(&mut cursor).unwrap();
let auth_data = read_bytes(&mut cursor).unwrap();
read_u8(&mut cursor)?;
read_bytes(&mut cursor)?;
read_u8(&mut cursor)?;
let auth_type = read_int(&mut cursor)?;
let auth_type = AuthenticationType::from_i32(auth_type as i32)
.ok_or(AuthenticationError::AuthType(auth_type))?;
read_u8(&mut cursor)?;
let auth_data = read_bytes(&mut cursor)?;
Credentials {
Ok(Credentials {
username,
auth_type,
auth_data,
}
})
}
}

View file

@ -1,15 +1,29 @@
use std::cmp::Reverse;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{self, Error, ErrorKind, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use std::{
cmp::Reverse,
collections::HashMap,
fs::{self, File},
io::{self, Read, Write},
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::SystemTime,
};
use priority_queue::PriorityQueue;
use thiserror::Error;
use crate::authentication::Credentials;
use crate::file_id::FileId;
use crate::{authentication::Credentials, error::ErrorKind, Error, FileId};
#[derive(Debug, Error)]
pub enum CacheError {
#[error("audio cache location is not configured")]
Path,
}
impl From<CacheError> for Error {
fn from(err: CacheError) -> Self {
Error::failed_precondition(err)
}
}
/// Some kind of data structure that holds some paths, the size of these files and a timestamp.
/// It keeps track of the file sizes and is able to pop the path with the oldest timestamp if
@ -57,16 +71,17 @@ impl SizeLimiter {
/// to delete the file in the file system.
fn pop(&mut self) -> Option<PathBuf> {
if self.exceeds_limit() {
let (next, _) = self
.queue
.pop()
.expect("in_use was > 0, so the queue should have contained an item.");
let size = self
.sizes
.remove(&next)
.expect("`queue` and `sizes` should have the same keys.");
self.in_use -= size;
Some(next)
if let Some((next, _)) = self.queue.pop() {
if let Some(size) = self.sizes.remove(&next) {
self.in_use -= size;
} else {
error!("`queue` and `sizes` should have the same keys.");
}
Some(next)
} else {
error!("in_use was > 0, so the queue should have contained an item.");
None
}
} else {
None
}
@ -85,11 +100,11 @@ impl SizeLimiter {
return false;
}
let size = self
.sizes
.remove(file)
.expect("`queue` and `sizes` should have the same keys.");
self.in_use -= size;
if let Some(size) = self.sizes.remove(file) {
self.in_use -= size;
} else {
error!("`queue` and `sizes` should have the same keys.");
}
true
}
@ -172,56 +187,70 @@ impl FsSizeLimiter {
}
}
fn add(&self, file: &Path, size: u64) {
fn add(&self, file: &Path, size: u64) -> Result<(), Error> {
self.limiter
.lock()
.unwrap()
.add(file, size, SystemTime::now());
Ok(())
}
fn touch(&self, file: &Path) -> bool {
self.limiter.lock().unwrap().update(file, SystemTime::now())
fn touch(&self, file: &Path) -> Result<bool, Error> {
Ok(self.limiter.lock().unwrap().update(file, SystemTime::now()))
}
fn remove(&self, file: &Path) {
self.limiter.lock().unwrap().remove(file);
fn remove(&self, file: &Path) -> Result<bool, Error> {
Ok(self.limiter.lock().unwrap().remove(file))
}
fn prune_internal<F: FnMut() -> Option<PathBuf>>(mut pop: F) {
fn prune_internal<F: FnMut() -> Result<Option<PathBuf>, Error>>(
mut pop: F,
) -> Result<(), Error> {
let mut first = true;
let mut count = 0;
let mut last_error = None;
while let Some(file) = pop() {
if first {
debug!("Cache dir exceeds limit, removing least recently used files.");
first = false;
while let Ok(result) = pop() {
if let Some(file) = result {
if first {
debug!("Cache dir exceeds limit, removing least recently used files.");
first = false;
}
let res = fs::remove_file(&file);
if let Err(e) = res {
warn!("Could not remove file {:?} from cache dir: {}", file, e);
last_error = Some(e);
} else {
count += 1;
}
}
if let Err(e) = fs::remove_file(&file) {
warn!("Could not remove file {:?} from cache dir: {}", file, e);
} else {
count += 1;
if count > 0 {
info!("Removed {} cache files.", count);
}
}
if count > 0 {
info!("Removed {} cache files.", count);
if let Some(err) = last_error {
Err(err.into())
} else {
Ok(())
}
}
fn prune(&self) {
Self::prune_internal(|| self.limiter.lock().unwrap().pop())
fn prune(&self) -> Result<(), Error> {
Self::prune_internal(|| Ok(self.limiter.lock().unwrap().pop()))
}
fn new(path: &Path, limit: u64) -> Self {
fn new(path: &Path, limit: u64) -> Result<Self, Error> {
let mut limiter = SizeLimiter::new(limit);
Self::init_dir(&mut limiter, path);
Self::prune_internal(|| limiter.pop());
Self::prune_internal(|| Ok(limiter.pop()))?;
Self {
Ok(Self {
limiter: Mutex::new(limiter),
}
})
}
}
@ -234,15 +263,13 @@ pub struct Cache {
size_limiter: Option<Arc<FsSizeLimiter>>,
}
pub struct RemoveFileError(());
impl Cache {
pub fn new<P: AsRef<Path>>(
credentials_path: Option<P>,
volume_path: Option<P>,
audio_path: Option<P>,
size_limit: Option<u64>,
) -> io::Result<Self> {
) -> Result<Self, Error> {
let mut size_limiter = None;
if let Some(location) = &credentials_path {
@ -263,8 +290,7 @@ impl Cache {
fs::create_dir_all(location)?;
if let Some(limit) = size_limit {
let limiter = FsSizeLimiter::new(location.as_ref(), limit);
let limiter = FsSizeLimiter::new(location.as_ref(), limit)?;
size_limiter = Some(Arc::new(limiter));
}
}
@ -285,11 +311,11 @@ impl Cache {
let location = self.credentials_location.as_ref()?;
// This closure is just convencience to enable the question mark operator
let read = || {
let read = || -> Result<Credentials, Error> {
let mut file = File::open(location)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
serde_json::from_str(&contents).map_err(|e| Error::new(ErrorKind::InvalidData, e))
Ok(serde_json::from_str(&contents)?)
};
match read() {
@ -297,7 +323,7 @@ impl Cache {
Err(e) => {
// If the file did not exist, the file was probably not written
// before. Otherwise, log the error.
if e.kind() != ErrorKind::NotFound {
if e.kind != ErrorKind::NotFound {
warn!("Error reading credentials from cache: {}", e);
}
None
@ -321,19 +347,17 @@ impl Cache {
pub fn volume(&self) -> Option<u16> {
let location = self.volume_location.as_ref()?;
let read = || {
let read = || -> Result<u16, Error> {
let mut file = File::open(location)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
contents
.parse()
.map_err(|e| Error::new(ErrorKind::InvalidData, e))
Ok(contents.parse()?)
};
match read() {
Ok(v) => Some(v),
Err(e) => {
if e.kind() != ErrorKind::NotFound {
if e.kind != ErrorKind::NotFound {
warn!("Error reading volume from cache: {}", e);
}
None
@ -364,12 +388,14 @@ impl Cache {
match File::open(&path) {
Ok(file) => {
if let Some(limiter) = self.size_limiter.as_deref() {
limiter.touch(&path);
if let Err(e) = limiter.touch(&path) {
error!("limiter could not touch {:?}: {}", path, e);
}
}
Some(file)
}
Err(e) => {
if e.kind() != ErrorKind::NotFound {
if e.kind() != io::ErrorKind::NotFound {
warn!("Error reading file from cache: {}", e)
}
None
@ -377,7 +403,7 @@ impl Cache {
}
}
pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> bool {
pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> Result<(), Error> {
if let Some(path) = self.file_path(file) {
if let Some(parent) = path.parent() {
if let Ok(size) = fs::create_dir_all(parent)
@ -385,28 +411,25 @@ impl Cache {
.and_then(|mut file| io::copy(contents, &mut file))
{
if let Some(limiter) = self.size_limiter.as_deref() {
limiter.add(&path, size);
limiter.prune();
limiter.add(&path, size)?;
limiter.prune()?
}
return true;
return Ok(());
}
}
}
false
Err(CacheError::Path.into())
}
pub fn remove_file(&self, file: FileId) -> Result<(), RemoveFileError> {
let path = self.file_path(file).ok_or(RemoveFileError(()))?;
pub fn remove_file(&self, file: FileId) -> Result<(), Error> {
let path = self.file_path(file).ok_or(CacheError::Path)?;
if let Err(err) = fs::remove_file(&path) {
warn!("Unable to remove file from cache: {}", err);
Err(RemoveFileError(()))
} else {
if let Some(limiter) = self.size_limiter.as_deref() {
limiter.remove(&path);
}
Ok(())
fs::remove_file(&path)?;
if let Some(limiter) = self.size_limiter.as_deref() {
limiter.remove(&path)?;
}
Ok(())
}
}

View file

@ -1,34 +1,19 @@
use std::{
convert::{TryFrom, TryInto},
ops::{Deref, DerefMut},
};
use chrono::Local;
use protobuf::{Message, ProtobufError};
use protobuf::Message;
use thiserror::Error;
use url::Url;
use std::convert::{TryFrom, TryInto};
use std::ops::{Deref, DerefMut};
use super::date::Date;
use super::file_id::FileId;
use super::session::Session;
use super::spclient::SpClientError;
use super::{date::Date, Error, FileId, Session};
use librespot_protocol as protocol;
use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage;
use protocol::storage_resolve::StorageResolveResponse_Result;
#[derive(Error, Debug)]
pub enum CdnUrlError {
#[error("no URLs available")]
Empty,
#[error("all tokens expired")]
Expired,
#[error("error parsing response")]
Parsing,
#[error("could not parse protobuf: {0}")]
Protobuf(#[from] ProtobufError),
#[error("could not complete API request: {0}")]
SpClient(#[from] SpClientError),
}
#[derive(Debug, Clone)]
pub struct MaybeExpiringUrl(pub String, pub Option<Date>);
@ -48,10 +33,27 @@ impl DerefMut for MaybeExpiringUrls {
}
}
#[derive(Debug, Error)]
pub enum CdnUrlError {
#[error("all URLs expired")]
Expired,
#[error("resolved storage is not for CDN")]
Storage,
}
impl From<CdnUrlError> for Error {
fn from(err: CdnUrlError) -> Self {
match err {
CdnUrlError::Expired => Error::deadline_exceeded(err),
CdnUrlError::Storage => Error::unavailable(err),
}
}
}
#[derive(Debug, Clone)]
pub struct CdnUrl {
pub file_id: FileId,
pub urls: MaybeExpiringUrls,
urls: MaybeExpiringUrls,
}
impl CdnUrl {
@ -62,7 +64,7 @@ impl CdnUrl {
}
}
pub async fn resolve_audio(&self, session: &Session) -> Result<Self, CdnUrlError> {
pub async fn resolve_audio(&self, session: &Session) -> Result<Self, Error> {
let file_id = self.file_id;
let response = session.spclient().get_audio_urls(file_id).await?;
let msg = CdnUrlMessage::parse_from_bytes(&response)?;
@ -75,37 +77,26 @@ impl CdnUrl {
Ok(cdn_url)
}
pub fn get_url(&mut self) -> Result<&str, CdnUrlError> {
if self.urls.is_empty() {
return Err(CdnUrlError::Empty);
}
// prune expired URLs until the first one is current, or none are left
pub fn try_get_url(&self) -> Result<&str, Error> {
let now = Local::now();
while !self.urls.is_empty() {
let maybe_expiring = self.urls[0].1;
if let Some(expiry) = maybe_expiring {
if now < expiry.as_utc() {
break;
} else {
self.urls.remove(0);
}
}
}
let url = self.urls.iter().find(|url| match url.1 {
Some(expiry) => now < expiry.as_utc(),
None => true,
});
if let Some(cdn_url) = self.urls.first() {
Ok(&cdn_url.0)
if let Some(url) = url {
Ok(&url.0)
} else {
Err(CdnUrlError::Expired)
Err(CdnUrlError::Expired.into())
}
}
}
impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
type Error = CdnUrlError;
type Error = crate::Error;
fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> {
if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) {
return Err(CdnUrlError::Parsing);
return Err(CdnUrlError::Storage.into());
}
let is_expiring = !msg.get_fileid().is_empty();
@ -114,7 +105,7 @@ impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
.get_cdnurl()
.iter()
.map(|cdn_url| {
let url = Url::parse(cdn_url).map_err(|_| CdnUrlError::Parsing)?;
let url = Url::parse(cdn_url)?;
if is_expiring {
let expiry_str = if let Some(token) = url
@ -122,29 +113,47 @@ impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
.into_iter()
.find(|(key, _value)| key == "__token__")
{
let start = token.1.find("exp=").ok_or(CdnUrlError::Parsing)?;
let slice = &token.1[start + 4..];
let end = slice.find('~').ok_or(CdnUrlError::Parsing)?;
String::from(&slice[..end])
if let Some(mut start) = token.1.find("exp=") {
start += 4;
if token.1.len() >= start {
let slice = &token.1[start..];
if let Some(end) = slice.find('~') {
// this is the only valid invariant for akamaized.net
String::from(&slice[..end])
} else {
String::from(slice)
}
} else {
String::new()
}
} else {
String::new()
}
} else if let Some(query) = url.query() {
let mut items = query.split('_');
String::from(items.next().ok_or(CdnUrlError::Parsing)?)
if let Some(first) = items.next() {
// this is the only valid invariant for scdn.co
String::from(first)
} else {
String::new()
}
} else {
return Err(CdnUrlError::Parsing);
String::new()
};
let mut expiry: i64 = expiry_str.parse().map_err(|_| CdnUrlError::Parsing)?;
let mut expiry: i64 = expiry_str.parse()?;
expiry -= 5 * 60; // seconds
Ok(MaybeExpiringUrl(
cdn_url.to_owned(),
Some(expiry.try_into().map_err(|_| CdnUrlError::Parsing)?),
Some(expiry.try_into()?),
))
} else {
Ok(MaybeExpiringUrl(cdn_url.to_owned(), None))
}
})
.collect::<Result<Vec<MaybeExpiringUrl>, CdnUrlError>>()?;
.collect::<Result<Vec<MaybeExpiringUrl>, Error>>()?;
Ok(Self(result))
}

View file

@ -1,18 +1,20 @@
use std::collections::HashMap;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Instant;
use std::{
collections::HashMap,
fmt,
pin::Pin,
task::{Context, Poll},
time::Instant,
};
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
use futures_core::Stream;
use futures_util::lock::BiLock;
use futures_util::{ready, StreamExt};
use futures_util::{lock::BiLock, ready, StreamExt};
use num_traits::FromPrimitive;
use thiserror::Error;
use tokio::sync::mpsc;
use crate::packet::PacketType;
use crate::util::SeqGenerator;
use crate::{packet::PacketType, util::SeqGenerator, Error};
component! {
ChannelManager : ChannelManagerInner {
@ -27,9 +29,21 @@ component! {
const ONE_SECOND_IN_MS: usize = 1000;
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)]
pub struct ChannelError;
impl From<ChannelError> for Error {
fn from(err: ChannelError) -> Self {
Error::aborted(err)
}
}
impl fmt::Display for ChannelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "channel error")
}
}
pub struct Channel {
receiver: mpsc::UnboundedReceiver<(u8, Bytes)>,
state: ChannelState,
@ -70,7 +84,7 @@ impl ChannelManager {
(seq, channel)
}
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) {
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
use std::collections::hash_map::Entry;
let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref());
@ -94,9 +108,14 @@ impl ChannelManager {
inner.download_measurement_bytes += data.len();
if let Entry::Occupied(entry) = inner.channels.entry(id) {
let _ = entry.get().send((cmd as u8, data));
entry
.get()
.send((cmd as u8, data))
.map_err(|_| ChannelError)?;
}
});
Ok(())
})
}
pub fn get_download_rate_estimate(&self) -> usize {
@ -142,7 +161,11 @@ impl Stream for Channel {
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
match self.state.clone() {
ChannelState::Closed => panic!("Polling already terminated channel"),
ChannelState::Closed => {
error!("Polling already terminated channel");
return Poll::Ready(None);
}
ChannelState::Header(mut data) => {
if data.is_empty() {
data = ready!(self.recv_packet(cx))?;

View file

@ -14,7 +14,7 @@ macro_rules! component {
#[allow(dead_code)]
fn lock<F: FnOnce(&mut $inner) -> R, R>(&self, f: F) -> R {
let mut inner = (self.0).1.lock().expect("Mutex poisoned");
let mut inner = (self.0).1.lock().unwrap();
f(&mut inner)
}

View file

@ -1,6 +1,5 @@
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use std::{fmt, path::PathBuf, str::FromStr};
use url::Url;
#[derive(Clone, Debug)]

View file

@ -1,12 +1,20 @@
use std::io;
use byteorder::{BigEndian, ByteOrder};
use bytes::{BufMut, Bytes, BytesMut};
use shannon::Shannon;
use std::io;
use thiserror::Error;
use tokio_util::codec::{Decoder, Encoder};
const HEADER_SIZE: usize = 3;
const MAC_SIZE: usize = 4;
#[derive(Debug, Error)]
pub enum ApCodecError {
#[error("payload was malformed")]
Payload,
}
#[derive(Debug)]
enum DecodeState {
Header,
@ -87,7 +95,10 @@ impl Decoder for ApCodec {
let mut payload = buf.split_to(size + MAC_SIZE);
self.decode_cipher.decrypt(payload.get_mut(..size).unwrap());
self.decode_cipher
.decrypt(payload.get_mut(..size).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, ApCodecError::Payload)
})?);
let mac = payload.split_off(size);
self.decode_cipher.check_mac(mac.as_ref())?;

View file

@ -1,20 +1,28 @@
use std::{env::consts::ARCH, io};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use hmac::{Hmac, Mac, NewMac};
use protobuf::{self, Message};
use rand::{thread_rng, RngCore};
use sha1::Sha1;
use std::env::consts::ARCH;
use std::io;
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio_util::codec::{Decoder, Framed};
use super::codec::ApCodec;
use crate::diffie_hellman::DhLocalKeys;
use crate::{diffie_hellman::DhLocalKeys, version};
use crate::protocol;
use crate::protocol::keyexchange::{
APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags,
};
use crate::version;
#[derive(Debug, Error)]
pub enum HandshakeError {
#[error("invalid key length")]
InvalidLength,
}
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
mut connection: T,
@ -31,7 +39,7 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
.to_owned();
let shared_secret = local_keys.shared_secret(&remote_key);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?;
let codec = ApCodec::new(&send_key, &recv_key);
client_response(&mut connection, challenge).await?;
@ -112,8 +120,8 @@ where
let mut buffer = vec![0, 4];
let size = 2 + 4 + packet.compute_size();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
packet.write_to_vec(&mut buffer).unwrap();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
packet.write_to_vec(&mut buffer)?;
connection.write_all(&buffer[..]).await?;
Ok(buffer)
@ -133,8 +141,8 @@ where
let mut buffer = vec![];
let size = 4 + packet.compute_size();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
packet.write_to_vec(&mut buffer).unwrap();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
packet.write_to_vec(&mut buffer)?;
connection.write_all(&buffer[..]).await?;
Ok(())
@ -148,7 +156,7 @@ where
let header = read_into_accumulator(connection, 4, acc).await?;
let size = BigEndian::read_u32(header) as usize;
let data = read_into_accumulator(connection, size - 4, acc).await?;
let message = M::parse_from_bytes(data).unwrap();
let message = M::parse_from_bytes(data)?;
Ok(message)
}
@ -164,24 +172,26 @@ async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>(
Ok(&mut acc[offset..])
}
fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> io::Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
type HmacSha1 = Hmac<Sha1>;
let mut data = Vec::with_capacity(0x64);
for i in 1..6 {
let mut mac =
HmacSha1::new_from_slice(shared_secret).expect("HMAC can take key of any size");
let mut mac = HmacSha1::new_from_slice(shared_secret).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength)
})?;
mac.update(packets);
mac.update(&[i]);
data.extend_from_slice(&mac.finalize().into_bytes());
}
let mut mac = HmacSha1::new_from_slice(&data[..0x14]).expect("HMAC can take key of any size");
let mut mac = HmacSha1::new_from_slice(&data[..0x14])
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength))?;
mac.update(packets);
(
Ok((
mac.finalize().into_bytes().to_vec(),
data[0x14..0x34].to_vec(),
data[0x34..0x54].to_vec(),
)
))
}

View file

@ -1,23 +1,21 @@
mod codec;
mod handshake;
pub use self::codec::ApCodec;
pub use self::handshake::handshake;
pub use self::{codec::ApCodec, handshake::handshake};
use std::io::{self, ErrorKind};
use std::io;
use futures_util::{SinkExt, StreamExt};
use num_traits::FromPrimitive;
use protobuf::{self, Message, ProtobufError};
use protobuf::{self, Message};
use thiserror::Error;
use tokio::net::TcpStream;
use tokio_util::codec::Framed;
use url::Url;
use crate::authentication::Credentials;
use crate::packet::PacketType;
use crate::{authentication::Credentials, packet::PacketType, version, Error};
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
use crate::version;
pub type Transport = Framed<TcpStream, ApCodec>;
@ -42,13 +40,19 @@ fn login_error_message(code: &ErrorCode) -> &'static str {
pub enum AuthenticationError {
#[error("Login failed with reason: {}", login_error_message(.0))]
LoginFailed(ErrorCode),
#[error("Authentication failed: {0}")]
IoError(#[from] io::Error),
#[error("invalid packet {0}")]
Packet(u8),
#[error("transport returned no data")]
Transport,
}
impl From<ProtobufError> for AuthenticationError {
fn from(e: ProtobufError) -> Self {
io::Error::new(ErrorKind::InvalidData, e).into()
impl From<AuthenticationError> for Error {
fn from(err: AuthenticationError) -> Self {
match err {
AuthenticationError::LoginFailed(_) => Error::permission_denied(err),
AuthenticationError::Packet(_) => Error::unimplemented(err),
AuthenticationError::Transport => Error::unavailable(err),
}
}
}
@ -68,7 +72,7 @@ pub async fn authenticate(
transport: &mut Transport,
credentials: Credentials,
device_id: &str,
) -> Result<Credentials, AuthenticationError> {
) -> Result<Credentials, Error> {
use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
let cpu_family = match std::env::consts::ARCH {
@ -119,12 +123,15 @@ pub async fn authenticate(
packet.set_version_string(format!("librespot {}", version::SEMVER));
let cmd = PacketType::Login;
let data = packet.write_to_bytes().unwrap();
let data = packet.write_to_bytes()?;
transport.send((cmd as u8, data)).await?;
let (cmd, data) = transport.next().await.expect("EOF")?;
let (cmd, data) = transport
.next()
.await
.ok_or(AuthenticationError::Transport)??;
let packet_type = FromPrimitive::from_u8(cmd);
match packet_type {
let result = match packet_type {
Some(PacketType::APWelcome) => {
let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;
@ -141,8 +148,13 @@ pub async fn authenticate(
Err(error_data.into())
}
_ => {
let msg = format!("Received invalid packet: {}", cmd);
Err(io::Error::new(ErrorKind::InvalidData, msg).into())
trace!(
"Did not expect {:?} AES key packet with data {:#?}",
cmd,
data
);
Err(AuthenticationError::Packet(cmd))
}
}
};
Ok(result?)
}

View file

@ -1,18 +1,23 @@
use std::convert::TryFrom;
use std::fmt::Debug;
use std::ops::Deref;
use std::{convert::TryFrom, fmt::Debug, ops::Deref};
use chrono::{DateTime, Utc};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use thiserror::Error;
use crate::Error;
use librespot_protocol as protocol;
use protocol::metadata::Date as DateMessage;
#[derive(Debug, Error)]
pub enum DateError {
#[error("item has invalid date")]
InvalidTimestamp,
#[error("item has invalid timestamp {0}")]
Timestamp(i64),
}
impl From<DateError> for Error {
fn from(err: DateError) -> Self {
Error::invalid_argument(err)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@ -30,11 +35,11 @@ impl Date {
self.0.timestamp()
}
pub fn from_timestamp(timestamp: i64) -> Result<Self, DateError> {
pub fn from_timestamp(timestamp: i64) -> Result<Self, Error> {
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) {
Ok(Self::from_utc(date_time))
} else {
Err(DateError::InvalidTimestamp)
Err(DateError::Timestamp(timestamp).into())
}
}
@ -67,7 +72,7 @@ impl From<DateTime<Utc>> for Date {
}
impl TryFrom<i64> for Date {
type Error = DateError;
type Error = crate::Error;
fn try_from(timestamp: i64) -> Result<Self, Self::Error> {
Self::from_timestamp(timestamp)
}

View file

@ -1,7 +1,20 @@
use std::collections::HashMap;
#[derive(Debug)]
pub struct AlreadyHandledError(());
use thiserror::Error;
use crate::Error;
#[derive(Debug, Error)]
pub enum HandlerMapError {
#[error("request was already handled")]
AlreadyHandled,
}
impl From<HandlerMapError> for Error {
fn from(err: HandlerMapError) -> Self {
Error::aborted(err)
}
}
pub enum HandlerMap<T> {
Leaf(T),
@ -19,9 +32,9 @@ impl<T> HandlerMap<T> {
&mut self,
mut path: impl Iterator<Item = &'a str>,
handler: T,
) -> Result<(), AlreadyHandledError> {
) -> Result<(), Error> {
match self {
Self::Leaf(_) => Err(AlreadyHandledError(())),
Self::Leaf(_) => Err(HandlerMapError::AlreadyHandled.into()),
Self::Branch(children) => {
if let Some(component) = path.next() {
let node = children.entry(component.to_owned()).or_default();
@ -30,7 +43,7 @@ impl<T> HandlerMap<T> {
*self = Self::Leaf(handler);
Ok(())
} else {
Err(AlreadyHandledError(()))
Err(HandlerMapError::AlreadyHandled.into())
}
}
}

View file

@ -1,29 +1,40 @@
mod maps;
pub mod protocol;
use std::iter;
use std::pin::Pin;
use std::sync::atomic::AtomicBool;
use std::sync::{atomic, Arc, Mutex};
use std::task::Poll;
use std::time::Duration;
use std::{
iter,
pin::Pin,
sync::{
atomic::{self, AtomicBool},
Arc, Mutex,
},
task::Poll,
time::Duration,
};
use futures_core::{Future, Stream};
use futures_util::future::join_all;
use futures_util::{SinkExt, StreamExt};
use futures_util::{future::join_all, SinkExt, StreamExt};
use thiserror::Error;
use tokio::select;
use tokio::sync::mpsc::{self, UnboundedReceiver};
use tokio::sync::Semaphore;
use tokio::task::JoinHandle;
use tokio::{
select,
sync::{
mpsc::{self, UnboundedReceiver},
Semaphore,
},
task::JoinHandle,
};
use tokio_tungstenite::tungstenite;
use tungstenite::error::UrlError;
use url::Url;
use self::maps::*;
use self::protocol::*;
use crate::socket;
use crate::util::{keep_flushing, CancelOnDrop, TimeoutOnDrop};
use crate::{
socket,
util::{keep_flushing, CancelOnDrop, TimeoutOnDrop},
Error,
};
type WsMessage = tungstenite::Message;
type WsError = tungstenite::Error;
@ -164,24 +175,38 @@ fn split_uri(s: &str) -> Option<impl Iterator<Item = &'_ str>> {
pub enum AddHandlerError {
#[error("There is already a handler for the given uri")]
AlreadyHandled,
#[error("The specified uri is invalid")]
InvalidUri,
#[error("The specified uri {0} is invalid")]
InvalidUri(String),
}
impl From<AddHandlerError> for Error {
fn from(err: AddHandlerError) -> Self {
match err {
AddHandlerError::AlreadyHandled => Error::aborted(err),
AddHandlerError::InvalidUri(_) => Error::invalid_argument(err),
}
}
}
#[derive(Debug, Clone, Error)]
pub enum SubscriptionError {
#[error("The specified uri is invalid")]
InvalidUri,
InvalidUri(String),
}
impl From<SubscriptionError> for Error {
fn from(err: SubscriptionError) -> Self {
Error::invalid_argument(err)
}
}
fn add_handler(
map: &mut HandlerMap<Box<dyn RequestHandler>>,
uri: &str,
handler: impl RequestHandler,
) -> Result<(), AddHandlerError> {
let split = split_uri(uri).ok_or(AddHandlerError::InvalidUri)?;
) -> Result<(), Error> {
let split = split_uri(uri).ok_or_else(|| AddHandlerError::InvalidUri(uri.to_string()))?;
map.insert(split, Box::new(handler))
.map_err(|_| AddHandlerError::AlreadyHandled)
}
fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
@ -191,11 +216,11 @@ fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
fn subscribe(
map: &mut SubscriberMap<MessageHandler>,
uris: &[&str],
) -> Result<Subscription, SubscriptionError> {
) -> Result<Subscription, Error> {
let (tx, rx) = mpsc::unbounded_channel();
for &uri in uris {
let split = split_uri(uri).ok_or(SubscriptionError::InvalidUri)?;
let split = split_uri(uri).ok_or_else(|| SubscriptionError::InvalidUri(uri.to_string()))?;
map.insert(split, tx.clone());
}
@ -237,15 +262,11 @@ impl Builder {
Self::default()
}
pub fn add_handler(
&mut self,
uri: &str,
handler: impl RequestHandler,
) -> Result<(), AddHandlerError> {
pub fn add_handler(&mut self, uri: &str, handler: impl RequestHandler) -> Result<(), Error> {
add_handler(&mut self.request_handlers, uri, handler)
}
pub fn subscribe(&mut self, uris: &[&str]) -> Result<Subscription, SubscriptionError> {
pub fn subscribe(&mut self, uris: &[&str]) -> Result<Subscription, Error> {
subscribe(&mut self.message_handlers, uris)
}
@ -342,7 +363,7 @@ pub struct Dealer {
}
impl Dealer {
pub fn add_handler<H>(&self, uri: &str, handler: H) -> Result<(), AddHandlerError>
pub fn add_handler<H>(&self, uri: &str, handler: H) -> Result<(), Error>
where
H: RequestHandler,
{
@ -357,7 +378,7 @@ impl Dealer {
remove_handler(&mut self.shared.request_handlers.lock().unwrap(), uri)
}
pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, SubscriptionError> {
pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, Error> {
subscribe(&mut self.shared.message_handlers.lock().unwrap(), uris)
}
@ -367,7 +388,9 @@ impl Dealer {
self.shared.notify_drop.close();
if let Some(handle) = self.handle.take() {
CancelOnDrop(handle).await.unwrap();
if let Err(e) = CancelOnDrop(handle).await {
error!("error aborting dealer operations: {}", e);
}
}
}
}
@ -556,11 +579,15 @@ async fn run<F, Fut>(
select! {
() = shared.closed() => break,
r = t0 => {
r.unwrap(); // Whatever has gone wrong (probably panicked), we can't handle it, so let's panic too.
if let Err(e) = r {
error!("timeout on task 0: {}", e);
}
tasks.0.take();
},
r = t1 => {
r.unwrap();
if let Err(e) = r {
error!("timeout on task 1: {}", e);
}
tasks.1.take();
}
}
@ -576,7 +603,7 @@ async fn run<F, Fut>(
match connect(&url, proxy.as_ref(), &shared).await {
Ok((s, r)) => tasks = (init_task(s), init_task(r)),
Err(e) => {
warn!("Error while connecting: {}", e);
error!("Error while connecting: {}", e);
tokio::time::sleep(RECONNECT_INTERVAL).await;
}
}

437
core/src/error.rs Normal file
View file

@ -0,0 +1,437 @@
use std::{error, fmt, num::ParseIntError, str::Utf8Error, string::FromUtf8Error};
use base64::DecodeError;
use http::{
header::{InvalidHeaderName, InvalidHeaderValue, ToStrError},
method::InvalidMethod,
status::InvalidStatusCode,
uri::{InvalidUri, InvalidUriParts},
};
use protobuf::ProtobufError;
use thiserror::Error;
use tokio::sync::{mpsc::error::SendError, oneshot::error::RecvError};
use url::ParseError;
#[derive(Debug)]
pub struct Error {
pub kind: ErrorKind,
pub error: Box<dyn error::Error + Send + Sync>,
}
#[derive(Clone, Copy, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
pub enum ErrorKind {
#[error("The operation was cancelled by the caller")]
Cancelled = 1,
#[error("Unknown error")]
Unknown = 2,
#[error("Client specified an invalid argument")]
InvalidArgument = 3,
#[error("Deadline expired before operation could complete")]
DeadlineExceeded = 4,
#[error("Requested entity was not found")]
NotFound = 5,
#[error("Attempt to create entity that already exists")]
AlreadyExists = 6,
#[error("Permission denied")]
PermissionDenied = 7,
#[error("No valid authentication credentials")]
Unauthenticated = 16,
#[error("Resource has been exhausted")]
ResourceExhausted = 8,
#[error("Invalid state")]
FailedPrecondition = 9,
#[error("Operation aborted")]
Aborted = 10,
#[error("Operation attempted past the valid range")]
OutOfRange = 11,
#[error("Not implemented")]
Unimplemented = 12,
#[error("Internal error")]
Internal = 13,
#[error("Service unavailable")]
Unavailable = 14,
#[error("Unrecoverable data loss or corruption")]
DataLoss = 15,
#[error("Operation must not be used")]
DoNotUse = -1,
}
#[derive(Debug, Error)]
struct ErrorMessage(String);
impl fmt::Display for ErrorMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error {
pub fn new<E>(kind: ErrorKind, error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind,
error: error.into(),
}
}
pub fn aborted<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Aborted,
error: error.into(),
}
}
pub fn already_exists<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::AlreadyExists,
error: error.into(),
}
}
pub fn cancelled<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Cancelled,
error: error.into(),
}
}
pub fn data_loss<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::DataLoss,
error: error.into(),
}
}
pub fn deadline_exceeded<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::DeadlineExceeded,
error: error.into(),
}
}
pub fn do_not_use<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::DoNotUse,
error: error.into(),
}
}
pub fn failed_precondition<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::FailedPrecondition,
error: error.into(),
}
}
pub fn internal<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Internal,
error: error.into(),
}
}
pub fn invalid_argument<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::InvalidArgument,
error: error.into(),
}
}
pub fn not_found<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::NotFound,
error: error.into(),
}
}
pub fn out_of_range<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::OutOfRange,
error: error.into(),
}
}
pub fn permission_denied<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::PermissionDenied,
error: error.into(),
}
}
pub fn resource_exhausted<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::ResourceExhausted,
error: error.into(),
}
}
pub fn unauthenticated<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unauthenticated,
error: error.into(),
}
}
pub fn unavailable<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unavailable,
error: error.into(),
}
}
pub fn unimplemented<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unimplemented,
error: error.into(),
}
}
pub fn unknown<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unknown,
error: error.into(),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.error.source()
}
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "{} {{ ", self.kind)?;
self.error.fmt(fmt)?;
write!(fmt, " }}")
}
}
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
if err.is::<InvalidHeaderName>()
|| err.is::<InvalidHeaderValue>()
|| err.is::<InvalidMethod>()
|| err.is::<InvalidUri>()
|| err.is::<InvalidUriParts>()
{
return Self::new(ErrorKind::InvalidArgument, err);
}
if err.is::<InvalidStatusCode>() {
return Self::new(ErrorKind::FailedPrecondition, err);
}
Self::new(ErrorKind::Unknown, err)
}
}
impl From<hyper::Error> for Error {
fn from(err: hyper::Error) -> Self {
if err.is_parse() || err.is_parse_too_large() || err.is_parse_status() || err.is_user() {
return Self::new(ErrorKind::Internal, err);
}
if err.is_canceled() {
return Self::new(ErrorKind::Cancelled, err);
}
if err.is_connect() {
return Self::new(ErrorKind::Unavailable, err);
}
if err.is_incomplete_message() {
return Self::new(ErrorKind::DataLoss, err);
}
if err.is_body_write_aborted() || err.is_closed() {
return Self::new(ErrorKind::Aborted, err);
}
if err.is_timeout() {
return Self::new(ErrorKind::DeadlineExceeded, err);
}
Self::new(ErrorKind::Unknown, err)
}
}
impl From<quick_xml::Error> for Error {
fn from(err: quick_xml::Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
use std::io::ErrorKind as IoErrorKind;
match err.kind() {
IoErrorKind::NotFound => Self::new(ErrorKind::NotFound, err),
IoErrorKind::PermissionDenied => Self::new(ErrorKind::PermissionDenied, err),
IoErrorKind::AddrInUse | IoErrorKind::AlreadyExists => {
Self::new(ErrorKind::AlreadyExists, err)
}
IoErrorKind::AddrNotAvailable
| IoErrorKind::ConnectionRefused
| IoErrorKind::NotConnected => Self::new(ErrorKind::Unavailable, err),
IoErrorKind::BrokenPipe
| IoErrorKind::ConnectionReset
| IoErrorKind::ConnectionAborted => Self::new(ErrorKind::Aborted, err),
IoErrorKind::Interrupted | IoErrorKind::WouldBlock => {
Self::new(ErrorKind::Cancelled, err)
}
IoErrorKind::InvalidData | IoErrorKind::UnexpectedEof => {
Self::new(ErrorKind::FailedPrecondition, err)
}
IoErrorKind::TimedOut => Self::new(ErrorKind::DeadlineExceeded, err),
IoErrorKind::InvalidInput => Self::new(ErrorKind::InvalidArgument, err),
IoErrorKind::WriteZero => Self::new(ErrorKind::ResourceExhausted, err),
_ => Self::new(ErrorKind::Unknown, err),
}
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<InvalidHeaderValue> for Error {
fn from(err: InvalidHeaderValue) -> Self {
Self::new(ErrorKind::InvalidArgument, err)
}
}
impl From<InvalidUri> for Error {
fn from(err: InvalidUri) -> Self {
Self::new(ErrorKind::InvalidArgument, err)
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<ParseIntError> for Error {
fn from(err: ParseIntError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<ProtobufError> for Error {
fn from(err: ProtobufError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<RecvError> for Error {
fn from(err: RecvError) -> Self {
Self::new(ErrorKind::Internal, err)
}
}
impl<T> From<SendError<T>> for Error {
fn from(err: SendError<T>) -> Self {
Self {
kind: ErrorKind::Internal,
error: ErrorMessage(err.to_string()).into(),
}
}
}
impl From<ToStrError> for Error {
fn from(err: ToStrError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<Utf8Error> for Error {
fn from(err: Utf8Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}

View file

@ -1,7 +1,7 @@
use librespot_protocol as protocol;
use std::fmt;
use librespot_protocol as protocol;
use crate::spotify_id::to_base16;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]

View file

@ -1,49 +1,82 @@
use std::env::consts::OS;
use bytes::Bytes;
use futures_util::future::IntoStream;
use futures_util::FutureExt;
use futures_util::{future::IntoStream, FutureExt};
use http::header::HeaderValue;
use http::uri::InvalidUri;
use hyper::client::{HttpConnector, ResponseFuture};
use hyper::header::USER_AGENT;
use hyper::{Body, Client, Request, Response, StatusCode};
use hyper::{
client::{HttpConnector, ResponseFuture},
header::USER_AGENT,
Body, Client, Request, Response, StatusCode,
};
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
use hyper_rustls::HttpsConnector;
use rustls::{ClientConfig, RootCertStore};
use thiserror::Error;
use url::Url;
use std::env::consts::OS;
use crate::version::{
FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING,
use crate::{
version::{FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING},
Error,
};
#[derive(Debug, Error)]
pub enum HttpClientError {
#[error("Response status code: {0}")]
StatusCode(hyper::StatusCode),
}
impl From<HttpClientError> for Error {
fn from(err: HttpClientError) -> Self {
match err {
HttpClientError::StatusCode(code) => {
// not exhaustive, but what reasonably could be expected
match code {
StatusCode::GATEWAY_TIMEOUT | StatusCode::REQUEST_TIMEOUT => {
Error::deadline_exceeded(err)
}
StatusCode::GONE
| StatusCode::NOT_FOUND
| StatusCode::MOVED_PERMANENTLY
| StatusCode::PERMANENT_REDIRECT
| StatusCode::TEMPORARY_REDIRECT => Error::not_found(err),
StatusCode::FORBIDDEN | StatusCode::PAYMENT_REQUIRED => {
Error::permission_denied(err)
}
StatusCode::NETWORK_AUTHENTICATION_REQUIRED
| StatusCode::PROXY_AUTHENTICATION_REQUIRED
| StatusCode::UNAUTHORIZED => Error::unauthenticated(err),
StatusCode::EXPECTATION_FAILED
| StatusCode::PRECONDITION_FAILED
| StatusCode::PRECONDITION_REQUIRED => Error::failed_precondition(err),
StatusCode::RANGE_NOT_SATISFIABLE => Error::out_of_range(err),
StatusCode::INTERNAL_SERVER_ERROR
| StatusCode::MISDIRECTED_REQUEST
| StatusCode::SERVICE_UNAVAILABLE
| StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => Error::unavailable(err),
StatusCode::BAD_REQUEST
| StatusCode::HTTP_VERSION_NOT_SUPPORTED
| StatusCode::LENGTH_REQUIRED
| StatusCode::METHOD_NOT_ALLOWED
| StatusCode::NOT_ACCEPTABLE
| StatusCode::PAYLOAD_TOO_LARGE
| StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE
| StatusCode::UNSUPPORTED_MEDIA_TYPE
| StatusCode::URI_TOO_LONG => Error::invalid_argument(err),
StatusCode::TOO_MANY_REQUESTS => Error::resource_exhausted(err),
StatusCode::NOT_IMPLEMENTED => Error::unimplemented(err),
_ => Error::unknown(err),
}
}
}
}
}
pub struct HttpClient {
user_agent: HeaderValue,
proxy: Option<Url>,
tls_config: ClientConfig,
}
#[derive(Error, Debug)]
pub enum HttpClientError {
#[error("could not parse request: {0}")]
Parsing(#[from] http::Error),
#[error("could not send request: {0}")]
Request(hyper::Error),
#[error("could not read response: {0}")]
Response(hyper::Error),
#[error("status code: {0}")]
NotOK(u16),
#[error("could not build proxy connector: {0}")]
ProxyBuilder(#[from] std::io::Error),
}
impl From<InvalidUri> for HttpClientError {
fn from(err: InvalidUri) -> Self {
Self::Parsing(err.into())
}
}
impl HttpClient {
pub fn new(proxy: Option<&Url>) -> Self {
let spotify_version = match OS {
@ -53,7 +86,7 @@ impl HttpClient {
let spotify_platform = match OS {
"android" => "Android/31",
"ios" => "iOS/15.1.1",
"ios" => "iOS/15.2",
"macos" => "OSX/0",
"windows" => "Win32/0",
_ => "Linux/0",
@ -95,37 +128,32 @@ impl HttpClient {
}
}
pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, HttpClientError> {
pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, Error> {
debug!("Requesting {:?}", req.uri().to_string());
let request = self.request_fut(req)?;
{
let response = request.await;
if let Ok(response) = &response {
let status = response.status();
if status != StatusCode::OK {
return Err(HttpClientError::NotOK(status.into()));
}
let response = request.await;
if let Ok(response) = &response {
let code = response.status();
if code != StatusCode::OK {
return Err(HttpClientError::StatusCode(code).into());
}
response.map_err(HttpClientError::Response)
}
Ok(response?)
}
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, HttpClientError> {
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, Error> {
let response = self.request(req).await?;
hyper::body::to_bytes(response.into_body())
.await
.map_err(HttpClientError::Response)
Ok(hyper::body::to_bytes(response.into_body()).await?)
}
pub fn request_stream(
&self,
req: Request<Body>,
) -> Result<IntoStream<ResponseFuture>, HttpClientError> {
pub fn request_stream(&self, req: Request<Body>) -> Result<IntoStream<ResponseFuture>, Error> {
Ok(self.request_fut(req)?.into_stream())
}
pub fn request_fut(&self, mut req: Request<Body>) -> Result<ResponseFuture, HttpClientError> {
pub fn request_fut(&self, mut req: Request<Body>) -> Result<ResponseFuture, Error> {
let mut http = HttpConnector::new();
http.enforce_http(false);
let connector = HttpsConnector::from((http, self.tls_config.clone()));

View file

@ -20,6 +20,7 @@ pub mod date;
mod dealer;
#[doc(hidden)]
pub mod diffie_hellman;
pub mod error;
pub mod file_id;
mod http_client;
pub mod mercury;
@ -34,3 +35,9 @@ pub mod token;
#[doc(hidden)]
pub mod util;
pub mod version;
pub use config::SessionConfig;
pub use error::Error;
pub use file_id::FileId;
pub use session::Session;
pub use spotify_id::SpotifyId;

View file

@ -1,9 +1,10 @@
use std::collections::HashMap;
use std::future::Future;
use std::mem;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
use std::{
collections::HashMap,
future::Future,
mem,
pin::Pin,
task::{Context, Poll},
};
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
@ -11,9 +12,7 @@ use futures_util::FutureExt;
use protobuf::Message;
use tokio::sync::{mpsc, oneshot};
use crate::packet::PacketType;
use crate::protocol;
use crate::util::SeqGenerator;
use crate::{packet::PacketType, protocol, util::SeqGenerator, Error};
mod types;
pub use self::types::*;
@ -33,18 +32,18 @@ component! {
pub struct MercuryPending {
parts: Vec<Vec<u8>>,
partial: Option<Vec<u8>>,
callback: Option<oneshot::Sender<Result<MercuryResponse, MercuryError>>>,
callback: Option<oneshot::Sender<Result<MercuryResponse, Error>>>,
}
pub struct MercuryFuture<T> {
receiver: oneshot::Receiver<Result<T, MercuryError>>,
receiver: oneshot::Receiver<Result<T, Error>>,
}
impl<T> Future for MercuryFuture<T> {
type Output = Result<T, MercuryError>;
type Output = Result<T, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.receiver.poll_unpin(cx).map_err(|_| MercuryError)?
self.receiver.poll_unpin(cx)?
}
}
@ -55,7 +54,7 @@ impl MercuryManager {
seq
}
fn request(&self, req: MercuryRequest) -> MercuryFuture<MercuryResponse> {
fn request(&self, req: MercuryRequest) -> Result<MercuryFuture<MercuryResponse>, Error> {
let (tx, rx) = oneshot::channel();
let pending = MercuryPending {
@ -72,13 +71,13 @@ impl MercuryManager {
});
let cmd = req.method.command();
let data = req.encode(&seq);
let data = req.encode(&seq)?;
self.session().send_packet(cmd, data);
MercuryFuture { receiver: rx }
self.session().send_packet(cmd, data)?;
Ok(MercuryFuture { receiver: rx })
}
pub fn get<T: Into<String>>(&self, uri: T) -> MercuryFuture<MercuryResponse> {
pub fn get<T: Into<String>>(&self, uri: T) -> Result<MercuryFuture<MercuryResponse>, Error> {
self.request(MercuryRequest {
method: MercuryMethod::Get,
uri: uri.into(),
@ -87,7 +86,11 @@ impl MercuryManager {
})
}
pub fn send<T: Into<String>>(&self, uri: T, data: Vec<u8>) -> MercuryFuture<MercuryResponse> {
pub fn send<T: Into<String>>(
&self,
uri: T,
data: Vec<u8>,
) -> Result<MercuryFuture<MercuryResponse>, Error> {
self.request(MercuryRequest {
method: MercuryMethod::Send,
uri: uri.into(),
@ -103,7 +106,7 @@ impl MercuryManager {
pub fn subscribe<T: Into<String>>(
&self,
uri: T,
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, MercuryError>> + 'static
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, Error>> + 'static
{
let uri = uri.into();
let request = self.request(MercuryRequest {
@ -115,7 +118,7 @@ impl MercuryManager {
let manager = self.clone();
async move {
let response = request.await?;
let response = request?.await?;
let (tx, rx) = mpsc::unbounded_channel();
@ -125,13 +128,18 @@ impl MercuryManager {
if !response.payload.is_empty() {
// Old subscription protocol, watch the provided list of URIs
for sub in response.payload {
let mut sub =
protocol::pubsub::Subscription::parse_from_bytes(&sub).unwrap();
let sub_uri = sub.take_uri();
match protocol::pubsub::Subscription::parse_from_bytes(&sub) {
Ok(mut sub) => {
let sub_uri = sub.take_uri();
debug!("subscribed sub_uri={}", sub_uri);
debug!("subscribed sub_uri={}", sub_uri);
inner.subscriptions.push((sub_uri, tx.clone()));
inner.subscriptions.push((sub_uri, tx.clone()));
}
Err(e) => {
error!("could not subscribe to {}: {}", uri, e);
}
}
}
} else {
// New subscription protocol, watch the requested URI
@ -165,7 +173,7 @@ impl MercuryManager {
}
}
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) {
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
let seq = data.split_to(seq_len).as_ref().to_owned();
@ -185,7 +193,7 @@ impl MercuryManager {
}
} else {
warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8);
return;
return Err(MercuryError::Command(cmd).into());
}
}
};
@ -205,10 +213,12 @@ impl MercuryManager {
}
if flags == 0x1 {
self.complete_request(cmd, pending);
self.complete_request(cmd, pending)?;
} else {
self.lock(move |inner| inner.pending.insert(seq, pending));
}
Ok(())
}
fn parse_part(data: &mut Bytes) -> Vec<u8> {
@ -216,9 +226,9 @@ impl MercuryManager {
data.split_to(size).as_ref().to_owned()
}
fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) {
fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) -> Result<(), Error> {
let header_data = pending.parts.remove(0);
let header = protocol::mercury::Header::parse_from_bytes(&header_data).unwrap();
let header = protocol::mercury::Header::parse_from_bytes(&header_data)?;
let response = MercuryResponse {
uri: header.get_uri().to_string(),
@ -226,13 +236,17 @@ impl MercuryManager {
payload: pending.parts,
};
if response.status_code >= 500 {
panic!("Spotify servers returned an error. Restart librespot.");
} else if response.status_code >= 400 {
warn!("error {} for uri {}", response.status_code, &response.uri);
let status_code = response.status_code;
if status_code >= 500 {
error!("error {} for uri {}", status_code, &response.uri);
Err(MercuryError::Response(response).into())
} else if status_code >= 400 {
error!("error {} for uri {}", status_code, &response.uri);
if let Some(cb) = pending.callback {
let _ = cb.send(Err(MercuryError));
cb.send(Err(MercuryError::Response(response.clone()).into()))
.map_err(|_| MercuryError::Channel)?;
}
Err(MercuryError::Response(response).into())
} else if let PacketType::MercuryEvent = cmd {
self.lock(|inner| {
let mut found = false;
@ -242,7 +256,7 @@ impl MercuryManager {
// before sending while saving the subscription under its unencoded form.
let mut uri_split = response.uri.split('/');
let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string())
let encoded_uri = std::iter::once(uri_split.next().unwrap_or_default().to_string())
.chain(uri_split.map(|component| {
form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>()
}))
@ -263,12 +277,19 @@ impl MercuryManager {
});
if !found {
debug!("unknown subscription uri={}", response.uri);
debug!("unknown subscription uri={}", &response.uri);
trace!("response pushed over Mercury: {:?}", response);
Err(MercuryError::Response(response).into())
} else {
Ok(())
}
})
} else if let Some(cb) = pending.callback {
let _ = cb.send(Ok(response));
cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?;
Ok(())
} else {
error!("can't handle Mercury response: {:?}", response);
Err(MercuryError::Response(response).into())
}
}

View file

@ -1,6 +1,8 @@
use std::collections::VecDeque;
use super::*;
use super::{MercuryFuture, MercuryManager, MercuryResponse};
use crate::Error;
pub struct MercurySender {
mercury: MercuryManager,
@ -23,12 +25,13 @@ impl MercurySender {
self.buffered_future.is_none() && self.pending.is_empty()
}
pub fn send(&mut self, item: Vec<u8>) {
let task = self.mercury.send(self.uri.clone(), item);
pub fn send(&mut self, item: Vec<u8>) -> Result<(), Error> {
let task = self.mercury.send(self.uri.clone(), item)?;
self.pending.push_back(task);
Ok(())
}
pub async fn flush(&mut self) -> Result<(), MercuryError> {
pub async fn flush(&mut self) -> Result<(), Error> {
if self.buffered_future.is_none() {
self.buffered_future = self.pending.pop_front();
}

View file

@ -1,11 +1,10 @@
use std::io::Write;
use byteorder::{BigEndian, WriteBytesExt};
use protobuf::Message;
use std::fmt;
use std::io::Write;
use thiserror::Error;
use crate::packet::PacketType;
use crate::protocol;
use crate::{packet::PacketType, protocol, Error};
#[derive(Debug, PartialEq, Eq)]
pub enum MercuryMethod {
@ -30,12 +29,23 @@ pub struct MercuryResponse {
pub payload: Vec<Vec<u8>>,
}
#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)]
pub struct MercuryError;
#[derive(Debug, Error)]
pub enum MercuryError {
#[error("callback receiver was disconnected")]
Channel,
#[error("error handling packet type: {0:?}")]
Command(PacketType),
#[error("error handling Mercury response: {0:?}")]
Response(MercuryResponse),
}
impl fmt::Display for MercuryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Mercury error")
impl From<MercuryError> for Error {
fn from(err: MercuryError) -> Self {
match err {
MercuryError::Channel => Error::aborted(err),
MercuryError::Command(_) => Error::unimplemented(err),
MercuryError::Response(_) => Error::unavailable(err),
}
}
}
@ -63,15 +73,12 @@ impl MercuryMethod {
}
impl MercuryRequest {
// TODO: change into Result and remove unwraps
pub fn encode(&self, seq: &[u8]) -> Vec<u8> {
pub fn encode(&self, seq: &[u8]) -> Result<Vec<u8>, Error> {
let mut packet = Vec::new();
packet.write_u16::<BigEndian>(seq.len() as u16).unwrap();
packet.write_all(seq).unwrap();
packet.write_u8(1).unwrap(); // Flags: FINAL
packet
.write_u16::<BigEndian>(1 + self.payload.len() as u16)
.unwrap(); // Part count
packet.write_u16::<BigEndian>(seq.len() as u16)?;
packet.write_all(seq)?;
packet.write_u8(1)?; // Flags: FINAL
packet.write_u16::<BigEndian>(1 + self.payload.len() as u16)?; // Part count
let mut header = protocol::mercury::Header::new();
header.set_uri(self.uri.clone());
@ -81,16 +88,14 @@ impl MercuryRequest {
header.set_content_type(content_type.clone());
}
packet
.write_u16::<BigEndian>(header.compute_size() as u16)
.unwrap();
header.write_to_writer(&mut packet).unwrap();
packet.write_u16::<BigEndian>(header.compute_size() as u16)?;
header.write_to_writer(&mut packet)?;
for p in &self.payload {
packet.write_u16::<BigEndian>(p.len() as u16).unwrap();
packet.write_all(p).unwrap();
packet.write_u16::<BigEndian>(p.len() as u16)?;
packet.write_all(p)?;
}
packet
Ok(packet)
}
}

View file

@ -2,7 +2,7 @@
use num_derive::{FromPrimitive, ToPrimitive};
#[derive(Debug, FromPrimitive, ToPrimitive)]
#[derive(Debug, Copy, Clone, FromPrimitive, ToPrimitive)]
pub enum PacketType {
SecretBlock = 0x02,
Ping = 0x04,

View file

@ -1,13 +1,16 @@
use std::collections::HashMap;
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::process::exit;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, RwLock, Weak};
use std::task::Context;
use std::task::Poll;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{
collections::HashMap,
future::Future,
io,
pin::Pin,
process::exit,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, RwLock, Weak,
},
task::{Context, Poll},
time::{SystemTime, UNIX_EPOCH},
};
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
@ -20,18 +23,21 @@ use thiserror::Error;
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use crate::apresolve::ApResolver;
use crate::audio_key::AudioKeyManager;
use crate::authentication::Credentials;
use crate::cache::Cache;
use crate::channel::ChannelManager;
use crate::config::SessionConfig;
use crate::connection::{self, AuthenticationError};
use crate::http_client::HttpClient;
use crate::mercury::MercuryManager;
use crate::packet::PacketType;
use crate::spclient::SpClient;
use crate::token::TokenProvider;
use crate::{
apresolve::ApResolver,
audio_key::AudioKeyManager,
authentication::Credentials,
cache::Cache,
channel::ChannelManager,
config::SessionConfig,
connection::{self, AuthenticationError},
http_client::HttpClient,
mercury::MercuryManager,
packet::PacketType,
spclient::SpClient,
token::TokenProvider,
Error,
};
#[derive(Debug, Error)]
pub enum SessionError {
@ -39,6 +45,18 @@ pub enum SessionError {
AuthenticationError(#[from] AuthenticationError),
#[error("Cannot create session: {0}")]
IoError(#[from] io::Error),
#[error("packet {0} unknown")]
Packet(u8),
}
impl From<SessionError> for Error {
fn from(err: SessionError) -> Self {
match err {
SessionError::AuthenticationError(_) => Error::unauthenticated(err),
SessionError::IoError(_) => Error::unavailable(err),
SessionError::Packet(_) => Error::unimplemented(err),
}
}
}
pub type UserAttributes = HashMap<String, String>;
@ -88,7 +106,7 @@ impl Session {
config: SessionConfig,
credentials: Credentials,
cache: Option<Cache>,
) -> Result<Session, SessionError> {
) -> Result<Session, Error> {
let http_client = HttpClient::new(config.proxy.as_ref());
let (sender_tx, sender_rx) = mpsc::unbounded_channel();
let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
@ -214,9 +232,18 @@ impl Session {
}
}
fn dispatch(&self, cmd: u8, data: Bytes) {
fn dispatch(&self, cmd: u8, data: Bytes) -> Result<(), Error> {
use PacketType::*;
let packet_type = FromPrimitive::from_u8(cmd);
let cmd = match packet_type {
Some(cmd) => cmd,
None => {
trace!("Ignoring unknown packet {:x}", cmd);
return Err(SessionError::Packet(cmd).into());
}
};
match packet_type {
Some(Ping) => {
let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64;
@ -229,24 +256,21 @@ impl Session {
self.0.data.write().unwrap().time_delta = server_timestamp - timestamp;
self.debug_info();
self.send_packet(Pong, vec![0, 0, 0, 0]);
self.send_packet(Pong, vec![0, 0, 0, 0])
}
Some(CountryCode) => {
let country = String::from_utf8(data.as_ref().to_owned()).unwrap();
let country = String::from_utf8(data.as_ref().to_owned())?;
info!("Country: {:?}", country);
self.0.data.write().unwrap().user_data.country = country;
Ok(())
}
Some(StreamChunkRes) | Some(ChannelError) => {
self.channel().dispatch(packet_type.unwrap(), data);
}
Some(AesKey) | Some(AesKeyError) => {
self.audio_key().dispatch(packet_type.unwrap(), data);
}
Some(StreamChunkRes) | Some(ChannelError) => self.channel().dispatch(cmd, data),
Some(AesKey) | Some(AesKeyError) => self.audio_key().dispatch(cmd, data),
Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => {
self.mercury().dispatch(packet_type.unwrap(), data);
self.mercury().dispatch(cmd, data)
}
Some(ProductInfo) => {
let data = std::str::from_utf8(&data).unwrap();
let data = std::str::from_utf8(&data)?;
let mut reader = quick_xml::Reader::from_str(data);
let mut buf = Vec::new();
@ -256,8 +280,7 @@ impl Session {
loop {
match reader.read_event(&mut buf) {
Ok(Event::Start(ref element)) => {
current_element =
std::str::from_utf8(element.name()).unwrap().to_owned()
current_element = std::str::from_utf8(element.name())?.to_owned()
}
Ok(Event::End(_)) => {
current_element = String::new();
@ -266,7 +289,7 @@ impl Session {
if !current_element.is_empty() {
let _ = user_attributes.insert(
current_element.clone(),
value.unescape_and_decode(&reader).unwrap(),
value.unescape_and_decode(&reader)?,
);
}
}
@ -284,24 +307,23 @@ impl Session {
Self::check_catalogue(&user_attributes);
self.0.data.write().unwrap().user_data.attributes = user_attributes;
Ok(())
}
Some(PongAck)
| Some(SecretBlock)
| Some(LegacyWelcome)
| Some(UnknownDataAllZeros)
| Some(LicenseVersion) => {}
| Some(LicenseVersion) => Ok(()),
_ => {
if let Some(packet_type) = PacketType::from_u8(cmd) {
trace!("Ignoring {:?} packet with data {:#?}", packet_type, data);
} else {
trace!("Ignoring unknown packet {:x}", cmd);
}
trace!("Ignoring {:?} packet with data {:#?}", cmd, data);
Err(SessionError::Packet(cmd as u8).into())
}
}
}
pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) {
self.0.tx_connection.send((cmd as u8, data)).unwrap();
pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) -> Result<(), Error> {
self.0.tx_connection.send((cmd as u8, data))?;
Ok(())
}
pub fn cache(&self) -> Option<&Arc<Cache>> {
@ -393,7 +415,7 @@ impl SessionWeak {
}
pub(crate) fn upgrade(&self) -> Session {
self.try_upgrade().expect("Session died")
self.try_upgrade().expect("Session died") // TODO
}
}
@ -434,7 +456,9 @@ where
}
};
session.dispatch(cmd, data);
if let Err(e) = session.dispatch(cmd, data) {
error!("could not dispatch command: {}", e);
}
}
}
}

View file

@ -1,5 +1,4 @@
use std::io;
use std::net::ToSocketAddrs;
use std::{io, net::ToSocketAddrs};
use tokio::net::TcpStream;
use url::Url;

View file

@ -1,22 +1,25 @@
use crate::apresolve::SocketAddress;
use crate::file_id::FileId;
use crate::http_client::HttpClientError;
use crate::mercury::MercuryError;
use crate::protocol::canvaz::EntityCanvazRequest;
use crate::protocol::connect::PutStateRequest;
use crate::protocol::extended_metadata::BatchedEntityRequest;
use crate::spotify_id::SpotifyId;
use std::time::Duration;
use bytes::Bytes;
use futures_util::future::IntoStream;
use http::header::HeaderValue;
use hyper::client::ResponseFuture;
use hyper::header::{InvalidHeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE};
use hyper::{Body, HeaderMap, Method, Request};
use hyper::{
client::ResponseFuture,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE},
Body, HeaderMap, Method, Request,
};
use protobuf::Message;
use rand::Rng;
use std::time::Duration;
use thiserror::Error;
use crate::{
apresolve::SocketAddress,
error::ErrorKind,
protocol::{
canvaz::EntityCanvazRequest, connect::PutStateRequest,
extended_metadata::BatchedEntityRequest,
},
Error, FileId, SpotifyId,
};
component! {
SpClient : SpClientInner {
@ -25,23 +28,7 @@ component! {
}
}
pub type SpClientResult = Result<Bytes, SpClientError>;
#[derive(Error, Debug)]
pub enum SpClientError {
#[error("could not get authorization token")]
Token(#[from] MercuryError),
#[error("could not parse request: {0}")]
Parsing(#[from] http::Error),
#[error("could not complete request: {0}")]
Network(#[from] HttpClientError),
}
impl From<InvalidHeaderValue> for SpClientError {
fn from(err: InvalidHeaderValue) -> Self {
Self::Parsing(err.into())
}
}
pub type SpClientResult = Result<Bytes, Error>;
#[derive(Copy, Clone, Debug)]
pub enum RequestStrategy {
@ -157,12 +144,8 @@ impl SpClient {
))?,
);
last_response = self
.session()
.http_client()
.request_body(request)
.await
.map_err(SpClientError::Network);
last_response = self.session().http_client().request_body(request).await;
if last_response.is_ok() {
return last_response;
}
@ -177,9 +160,9 @@ impl SpClient {
// Reconnection logic: drop the current access point if we are experiencing issues.
// This will cause the next call to base_url() to resolve a new one.
if let Err(SpClientError::Network(ref network_error)) = last_response {
match network_error {
HttpClientError::Response(_) | HttpClientError::Request(_) => {
if let Err(ref network_error) = last_response {
match network_error.kind {
ErrorKind::Unavailable | ErrorKind::DeadlineExceeded => {
// Keep trying the current access point three times before dropping it.
if tries % 3 == 0 {
self.flush_accesspoint().await
@ -244,7 +227,7 @@ impl SpClient {
}
pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult {
let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62(),);
let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62());
self.request_as_json(&Method::GET, &endpoint, None, None)
.await
@ -291,7 +274,7 @@ impl SpClient {
url: &str,
offset: usize,
length: usize,
) -> Result<IntoStream<ResponseFuture>, SpClientError> {
) -> Result<IntoStream<ResponseFuture>, Error> {
let req = Request::builder()
.method(&Method::GET)
.uri(url)

View file

@ -1,13 +1,17 @@
use librespot_protocol as protocol;
use std::{
convert::{TryFrom, TryInto},
fmt,
ops::Deref,
};
use thiserror::Error;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::ops::Deref;
use crate::Error;
use librespot_protocol as protocol;
// re-export FileId for historic reasons, when it was part of this mod
pub use crate::file_id::FileId;
pub use crate::FileId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpotifyItemType {
@ -64,8 +68,14 @@ pub enum SpotifyIdError {
InvalidRoot,
}
pub type SpotifyIdResult = Result<SpotifyId, SpotifyIdError>;
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, SpotifyIdError>;
impl From<SpotifyIdError> for Error {
fn from(err: SpotifyIdError) -> Self {
Error::invalid_argument(err)
}
}
pub type SpotifyIdResult = Result<SpotifyId, Error>;
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, Error>;
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
@ -95,7 +105,7 @@ impl SpotifyId {
let p = match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
_ => return Err(SpotifyIdError::InvalidId),
_ => return Err(SpotifyIdError::InvalidId.into()),
} as u128;
dst <<= 4;
@ -121,7 +131,7 @@ impl SpotifyId {
b'0'..=b'9' => c - b'0',
b'a'..=b'z' => c - b'a' + 10,
b'A'..=b'Z' => c - b'A' + 36,
_ => return Err(SpotifyIdError::InvalidId),
_ => return Err(SpotifyIdError::InvalidId.into()),
} as u128;
dst *= 62;
@ -143,7 +153,7 @@ impl SpotifyId {
id: u128::from_be_bytes(dst),
item_type: SpotifyItemType::Unknown,
}),
Err(_) => Err(SpotifyIdError::InvalidId),
Err(_) => Err(SpotifyIdError::InvalidId.into()),
}
}
@ -161,20 +171,20 @@ impl SpotifyId {
// At minimum, should be `spotify:{type}:{id}`
if uri_parts.len() < 3 {
return Err(SpotifyIdError::InvalidFormat);
return Err(SpotifyIdError::InvalidFormat.into());
}
if uri_parts[0] != "spotify" {
return Err(SpotifyIdError::InvalidRoot);
return Err(SpotifyIdError::InvalidRoot.into());
}
let id = uri_parts.pop().unwrap();
let id = uri_parts.pop().unwrap_or_default();
if id.len() != Self::SIZE_BASE62 {
return Err(SpotifyIdError::InvalidId);
return Err(SpotifyIdError::InvalidId.into());
}
Ok(Self {
item_type: uri_parts.pop().unwrap().into(),
item_type: uri_parts.pop().unwrap_or_default().into(),
..Self::from_base62(id)?
})
}
@ -285,15 +295,15 @@ impl NamedSpotifyId {
// At minimum, should be `spotify:user:{username}:{type}:{id}`
if uri_parts.len() < 5 {
return Err(SpotifyIdError::InvalidFormat);
return Err(SpotifyIdError::InvalidFormat.into());
}
if uri_parts[0] != "spotify" {
return Err(SpotifyIdError::InvalidRoot);
return Err(SpotifyIdError::InvalidRoot.into());
}
if uri_parts[1] != "user" {
return Err(SpotifyIdError::InvalidFormat);
return Err(SpotifyIdError::InvalidFormat.into());
}
Ok(Self {
@ -344,35 +354,35 @@ impl fmt::Display for NamedSpotifyId {
}
impl TryFrom<&[u8]> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
Self::from_raw(src)
}
}
impl TryFrom<&str> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(src: &str) -> Result<Self, Self::Error> {
Self::from_base62(src)
}
}
impl TryFrom<String> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(src: String) -> Result<Self, Self::Error> {
Self::try_from(src.as_str())
}
}
impl TryFrom<&Vec<u8>> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(src.as_slice())
}
}
impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
match SpotifyId::from_raw(track.get_gid()) {
Ok(mut id) => {
@ -385,7 +395,7 @@ impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Album> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Album,
@ -395,7 +405,7 @@ impl TryFrom<&protocol::metadata::Album> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Artist,
@ -405,7 +415,7 @@ impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Episode,
@ -415,7 +425,7 @@ impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Track> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Track,
@ -425,7 +435,7 @@ impl TryFrom<&protocol::metadata::Track> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Show> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Show,
@ -435,7 +445,7 @@ impl TryFrom<&protocol::metadata::Show> for SpotifyId {
}
impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Artist,
@ -445,7 +455,7 @@ impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
}
impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Track,
@ -457,7 +467,7 @@ impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
// Note that this is the unique revision of an item's metadata on a playlist,
// not the ID of that item or playlist.
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
Self::try_from(item.get_revision())
}
@ -465,7 +475,7 @@ impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
// Note that this is the unique revision of a playlist, not the ID of that playlist.
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(
playlist: &protocol::playlist4_external::SelectedListContent,
) -> Result<Self, Self::Error> {
@ -477,7 +487,7 @@ impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
// which is why we now don't create a separate `Playlist` enum value yet and choose
// to discard any item type.
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(
picture: &protocol::playlist_annotate3::TranscodedPicture,
) -> Result<Self, Self::Error> {
@ -565,7 +575,7 @@ mod tests {
id: 0,
kind: SpotifyItemType::Unknown,
// Invalid ID in the URI.
uri_error: Some(SpotifyIdError::InvalidId),
uri_error: SpotifyIdError::InvalidId,
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
base62: "!!!!!Ys0csV6RS48xBl0tH",
@ -578,7 +588,7 @@ mod tests {
id: 0,
kind: SpotifyItemType::Unknown,
// Missing colon between ID and type.
uri_error: Some(SpotifyIdError::InvalidFormat),
uri_error: SpotifyIdError::InvalidFormat,
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
base16: "--------------------",
base62: "....................",
@ -591,7 +601,7 @@ mod tests {
id: 0,
kind: SpotifyItemType::Unknown,
// Uri too short
uri_error: Some(SpotifyIdError::InvalidId),
uri_error: SpotifyIdError::InvalidId,
uri: "spotify:azb:aRS48xBl0tH",
base16: "--------------------",
base62: "....................",

View file

@ -8,12 +8,12 @@
// user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming,
// app-remote-control
use crate::mercury::MercuryError;
use std::time::{Duration, Instant};
use serde::Deserialize;
use thiserror::Error;
use std::error::Error;
use std::time::{Duration, Instant};
use crate::Error;
component! {
TokenProvider : TokenProviderInner {
@ -21,6 +21,18 @@ component! {
}
}
#[derive(Debug, Error)]
pub enum TokenError {
#[error("no tokens available")]
Empty,
}
impl From<TokenError> for Error {
fn from(err: TokenError) -> Self {
Error::unavailable(err)
}
}
#[derive(Clone, Debug)]
pub struct Token {
pub access_token: String,
@ -54,11 +66,7 @@ impl TokenProvider {
}
// scopes must be comma-separated
pub async fn get_token(&self, scopes: &str) -> Result<Token, MercuryError> {
if scopes.is_empty() {
return Err(MercuryError);
}
pub async fn get_token(&self, scopes: &str) -> Result<Token, Error> {
if let Some(index) = self.find_token(scopes.split(',').collect()) {
let cached_token = self.lock(|inner| inner.tokens[index].clone());
if cached_token.is_expired() {
@ -79,14 +87,10 @@ impl TokenProvider {
Self::KEYMASTER_CLIENT_ID,
self.session().device_id()
);
let request = self.session().mercury().get(query_uri);
let request = self.session().mercury().get(query_uri)?;
let response = request.await?;
let data = response
.payload
.first()
.expect("No tokens received")
.to_vec();
let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?;
let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec();
let token = Token::new(String::from_utf8(data)?)?;
trace!("Got token: {:#?}", token);
self.lock(|inner| inner.tokens.push(token.clone()));
Ok(token)
@ -96,7 +100,7 @@ impl TokenProvider {
impl Token {
const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10);
pub fn new(body: String) -> Result<Self, Box<dyn Error>> {
pub fn new(body: String) -> Result<Self, Error> {
let data: TokenData = serde_json::from_slice(body.as_ref())?;
Ok(Self {
access_token: data.access_token,

View file

@ -1,15 +1,13 @@
use std::future::Future;
use std::mem;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
use std::{
future::Future,
mem,
pin::Pin,
task::{Context, Poll},
};
use futures_core::ready;
use futures_util::FutureExt;
use futures_util::Sink;
use futures_util::{future, SinkExt};
use tokio::task::JoinHandle;
use tokio::time::timeout;
use futures_util::{future, FutureExt, Sink, SinkExt};
use tokio::{task::JoinHandle, time::timeout};
/// Returns a future that will flush the sink, even if flushing is temporarily completed.
/// Finishes only if the sink throws an error.