mirror of
https://github.com/librespot-org/librespot.git
synced 2024-11-08 16:45:43 +00:00
Merge branch 'dev' into log-volume-ctrl-optimisations
This commit is contained in:
commit
3a2455d686
16 changed files with 604 additions and 317 deletions
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -99,8 +99,8 @@ jobs:
|
||||||
- run: cargo hack --workspace --remove-dev-deps
|
- run: cargo hack --workspace --remove-dev-deps
|
||||||
- run: cargo build -p librespot-core --no-default-features
|
- run: cargo build -p librespot-core --no-default-features
|
||||||
- run: cargo build -p librespot-core
|
- run: cargo build -p librespot-core
|
||||||
- run: cargo build -p librespot-connect
|
- run: cargo hack build --each-feature -p librespot-discovery
|
||||||
- run: cargo build -p librespot-connect --no-default-features --features with-dns-sd
|
- run: cargo hack build --each-feature -p librespot-playback
|
||||||
- run: cargo hack build --each-feature
|
- run: cargo hack build --each-feature
|
||||||
|
|
||||||
test-windows:
|
test-windows:
|
||||||
|
|
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -7,32 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`.
|
||||||
- [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves
|
- [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves
|
||||||
- [playback] `alsamixer`: support for querying dB range from Alsa softvol
|
- [playback] `alsamixer`: support for querying dB range from Alsa softvol
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking)
|
- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking)
|
||||||
- [connect, playback] Moved volume controls from `connect` to `playback` crate
|
- [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate
|
||||||
* [connect] Synchronize player volume with mixer volume on playback
|
- [connect] Synchronize player volume with mixer volume on playback
|
||||||
- [playback] Make cubic volume control available to all mixers with `--volume-ctrl cubic`
|
- [playback] Make cubic volume control available to all mixers with `--volume-ctrl cubic`
|
||||||
- [playback] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking)
|
- [playback] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking)
|
||||||
- [playback] `alsamixer`: complete rewrite (breaking)
|
- [playback] `alsamixer`: complete rewrite (breaking)
|
||||||
- [playback] `alsamixer`: query card dB range for the `log` volume control unless specified otherwise
|
- [playback] `alsamixer`: query card dB range for the `log` volume control unless specified otherwise
|
||||||
- [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise
|
- [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise
|
||||||
|
|
||||||
### Fixed
|
### Deprecated
|
||||||
- [connect] Fix step size on volume up/down events
|
- [connect] The `discovery` module
|
||||||
- [playback] Fix `log` and `cubic` volume controls to be mute at zero volume
|
|
||||||
- [playback] `alsamixer`: make `cubic` consistent between cards that report minimum volume as mute, and cards that report some dB value
|
|
||||||
- [playback] `alsamixer`: make `--volume-ctrl {linear|log}` work as expected
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- [connect] Removed no-op mixer started/stopped logic
|
- [connect] Removed no-op mixer started/stopped logic (breaking)
|
||||||
- [playback] `alsamixer`: removed `--mixer-linear-volume` option; use `--volume-ctrl linear` instead
|
- [playback] `alsamixer`: removed `--mixer-linear-volume` option; use `--volume-ctrl linear` instead
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- [connect] Fix step size on volume up/down events
|
||||||
* [librespot-playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream
|
- [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream in librespot-connect was deprecated in favor of the `librespot-discovery` crate.
|
||||||
|
- [playback] Fix `log` and `cubic` volume controls to be mute at zero volume
|
||||||
|
- [playback] `alsamixer`: make `cubic` consistent between cards that report minimum volume as mute, and cards that report some dB value
|
||||||
|
- [playback] `alsamixer`: make `--volume-ctrl {linear|log}` work as expected
|
||||||
|
|
||||||
## [0.2.0] - 2021-05-04
|
## [0.2.0] - 2021-05-04
|
||||||
|
|
||||||
|
|
70
Cargo.lock
generated
70
Cargo.lock
generated
|
@ -237,6 +237,17 @@ dependencies = [
|
||||||
"libloading 0.7.0",
|
"libloading 0.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colored"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59"
|
||||||
|
dependencies = [
|
||||||
|
"atty",
|
||||||
|
"lazy_static",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.5.2"
|
version = "4.5.2"
|
||||||
|
@ -956,9 +967,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jack"
|
name = "jack"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "370ed0ab943f1484a94d6328bf612907f5de2748531b873994f513d8806e9d9d"
|
checksum = "49e720259b4a3e1f33cba335ca524a99a5f2411d405b05f6405fadd69269e2db"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"jack-sys",
|
"jack-sys",
|
||||||
|
@ -1143,6 +1154,7 @@ dependencies = [
|
||||||
"librespot-audio",
|
"librespot-audio",
|
||||||
"librespot-connect",
|
"librespot-connect",
|
||||||
"librespot-core",
|
"librespot-core",
|
||||||
|
"librespot-discovery",
|
||||||
"librespot-metadata",
|
"librespot-metadata",
|
||||||
"librespot-playback",
|
"librespot-playback",
|
||||||
"librespot-protocol",
|
"librespot-protocol",
|
||||||
|
@ -1172,16 +1184,10 @@ dependencies = [
|
||||||
name = "librespot-connect"
|
name = "librespot-connect"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-ctr",
|
|
||||||
"base64",
|
|
||||||
"dns-sd",
|
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-core",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hmac",
|
|
||||||
"hyper",
|
|
||||||
"libmdns",
|
|
||||||
"librespot-core",
|
"librespot-core",
|
||||||
|
"librespot-discovery",
|
||||||
"librespot-playback",
|
"librespot-playback",
|
||||||
"librespot-protocol",
|
"librespot-protocol",
|
||||||
"log",
|
"log",
|
||||||
|
@ -1189,10 +1195,8 @@ dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha-1",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"url",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1235,6 +1239,31 @@ dependencies = [
|
||||||
"vergen",
|
"vergen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "librespot-discovery"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"aes-ctr",
|
||||||
|
"base64",
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"dns-sd",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures",
|
||||||
|
"futures-core",
|
||||||
|
"hex",
|
||||||
|
"hmac",
|
||||||
|
"hyper",
|
||||||
|
"libmdns",
|
||||||
|
"librespot-core",
|
||||||
|
"log",
|
||||||
|
"rand",
|
||||||
|
"serde_json",
|
||||||
|
"sha-1",
|
||||||
|
"simple_logger",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "librespot-metadata"
|
name = "librespot-metadata"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -1260,7 +1289,7 @@ dependencies = [
|
||||||
"glib",
|
"glib",
|
||||||
"gstreamer",
|
"gstreamer",
|
||||||
"gstreamer-app",
|
"gstreamer-app",
|
||||||
"jack 0.7.0",
|
"jack 0.7.1",
|
||||||
"lewton",
|
"lewton",
|
||||||
"libpulse-binding",
|
"libpulse-binding",
|
||||||
"libpulse-simple-binding",
|
"libpulse-simple-binding",
|
||||||
|
@ -1913,9 +1942,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rodio"
|
name = "rodio"
|
||||||
version = "0.13.1"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b65c2eda643191f6d1bb12ea323a9db8d9ba95374e9be3780b5a9fb5cfb8520f"
|
checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cpal",
|
"cpal",
|
||||||
]
|
]
|
||||||
|
@ -2081,6 +2110,19 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple_logger"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd57f17c093ead1d4a1499dc9acaafdd71240908d64775465543b8d9a9f1d198"
|
||||||
|
dependencies = [
|
||||||
|
"atty",
|
||||||
|
"chrono",
|
||||||
|
"colored",
|
||||||
|
"log",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
|
@ -33,6 +33,10 @@ path = "core"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
features = ["apresolve"]
|
features = ["apresolve"]
|
||||||
|
|
||||||
|
[dependencies.librespot-discovery]
|
||||||
|
path = "discovery"
|
||||||
|
version = "0.2.0"
|
||||||
|
|
||||||
[dependencies.librespot-metadata]
|
[dependencies.librespot-metadata]
|
||||||
path = "metadata"
|
path = "metadata"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -72,7 +76,7 @@ gstreamer-backend = ["librespot-playback/gstreamer-backend"]
|
||||||
with-tremor = ["librespot-playback/with-tremor"]
|
with-tremor = ["librespot-playback/with-tremor"]
|
||||||
with-vorbis = ["librespot-playback/with-vorbis"]
|
with-vorbis = ["librespot-playback/with-vorbis"]
|
||||||
|
|
||||||
with-dns-sd = ["librespot-connect/with-dns-sd"]
|
with-dns-sd = ["librespot-discovery/with-dns-sd"]
|
||||||
|
|
||||||
default = ["rodio-backend"]
|
default = ["rodio-backend"]
|
||||||
|
|
||||||
|
|
|
@ -8,25 +8,15 @@ repository = "https://github.com/librespot-org/librespot"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes-ctr = "0.6"
|
|
||||||
base64 = "0.13"
|
|
||||||
form_urlencoded = "1.0"
|
form_urlencoded = "1.0"
|
||||||
futures-core = "0.3"
|
|
||||||
futures-util = { version = "0.3.5", default_features = false }
|
futures-util = { version = "0.3.5", default_features = false }
|
||||||
hmac = "0.11"
|
|
||||||
hyper = { version = "0.14", features = ["server", "http1", "tcp"] }
|
|
||||||
libmdns = "0.6"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
protobuf = "2.14.0"
|
protobuf = "2.14.0"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.25"
|
serde_json = "1.0"
|
||||||
sha-1 = "0.9"
|
tokio = { version = "1.0", features = ["macros", "sync"] }
|
||||||
tokio = { version = "1.0", features = ["macros", "rt", "sync"] }
|
|
||||||
tokio-stream = "0.1.1"
|
tokio-stream = "0.1.1"
|
||||||
url = "2.1"
|
|
||||||
|
|
||||||
dns-sd = { version = "0.1.3", optional = true }
|
|
||||||
|
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "../core"
|
path = "../core"
|
||||||
|
@ -40,6 +30,9 @@ version = "0.2.0"
|
||||||
path = "../protocol"
|
path = "../protocol"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
||||||
[features]
|
[dependencies.librespot-discovery]
|
||||||
with-dns-sd = ["dns-sd"]
|
path = "../discovery"
|
||||||
|
version = "0.2.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
with-dns-sd = ["librespot-discovery/with-dns-sd"]
|
||||||
|
|
|
@ -1,203 +1,19 @@
|
||||||
use aes_ctr::cipher::generic_array::GenericArray;
|
|
||||||
use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher};
|
|
||||||
use aes_ctr::Aes128Ctr;
|
|
||||||
use futures_core::Stream;
|
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
|
||||||
use hyper::{Body, Method, Request, Response, StatusCode};
|
|
||||||
use serde_json::json;
|
|
||||||
use sha1::{Digest, Sha1};
|
|
||||||
use tokio::sync::{mpsc, oneshot};
|
|
||||||
|
|
||||||
#[cfg(feature = "with-dns-sd")]
|
|
||||||
use dns_sd::DNSService;
|
|
||||||
|
|
||||||
use librespot_core::authentication::Credentials;
|
|
||||||
use librespot_core::config::ConnectConfig;
|
|
||||||
use librespot_core::diffie_hellman::DhLocalKeys;
|
|
||||||
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::convert::Infallible;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
type HmacSha1 = Hmac<Sha1>;
|
use futures_util::Stream;
|
||||||
|
use librespot_core::authentication::Credentials;
|
||||||
|
use librespot_core::config::ConnectConfig;
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub struct DiscoveryStream(librespot_discovery::Discovery);
|
||||||
struct Discovery(Arc<DiscoveryInner>);
|
|
||||||
struct DiscoveryInner {
|
|
||||||
config: ConnectConfig,
|
|
||||||
device_id: String,
|
|
||||||
keys: DhLocalKeys,
|
|
||||||
tx: mpsc::UnboundedSender<Credentials>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Discovery {
|
impl Stream for DiscoveryStream {
|
||||||
fn new(
|
type Item = Credentials;
|
||||||
config: ConnectConfig,
|
|
||||||
device_id: String,
|
|
||||||
) -> (Discovery, mpsc::UnboundedReceiver<Credentials>) {
|
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
|
||||||
|
|
||||||
let discovery = Discovery(Arc::new(DiscoveryInner {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
config,
|
Pin::new(&mut self.0).poll_next(cx)
|
||||||
device_id,
|
|
||||||
keys: DhLocalKeys::random(&mut rand::thread_rng()),
|
|
||||||
tx,
|
|
||||||
}));
|
|
||||||
|
|
||||||
(discovery, rx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_get_info(&self, _: BTreeMap<Cow<'_, str>, Cow<'_, str>>) -> Response<hyper::Body> {
|
|
||||||
let public_key = base64::encode(&self.0.keys.public_key());
|
|
||||||
|
|
||||||
let result = json!({
|
|
||||||
"status": 101,
|
|
||||||
"statusString": "ERROR-OK",
|
|
||||||
"spotifyError": 0,
|
|
||||||
"version": "2.7.1",
|
|
||||||
"deviceID": (self.0.device_id),
|
|
||||||
"remoteName": (self.0.config.name),
|
|
||||||
"activeUser": "",
|
|
||||||
"publicKey": (public_key),
|
|
||||||
"deviceType": (self.0.config.device_type.to_string().to_uppercase()),
|
|
||||||
"libraryVersion": "0.1.0",
|
|
||||||
"accountReq": "PREMIUM",
|
|
||||||
"brandDisplayName": "librespot",
|
|
||||||
"modelDisplayName": "librespot",
|
|
||||||
"resolverVersion": "0",
|
|
||||||
"groupStatus": "NONE",
|
|
||||||
"voiceSupport": "NO",
|
|
||||||
});
|
|
||||||
|
|
||||||
let body = result.to_string();
|
|
||||||
Response::new(Body::from(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_add_user(
|
|
||||||
&self,
|
|
||||||
params: BTreeMap<Cow<'_, str>, Cow<'_, str>>,
|
|
||||||
) -> Response<hyper::Body> {
|
|
||||||
let username = params.get("userName").unwrap().as_ref();
|
|
||||||
let encrypted_blob = params.get("blob").unwrap();
|
|
||||||
let client_key = params.get("clientKey").unwrap();
|
|
||||||
|
|
||||||
let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap();
|
|
||||||
|
|
||||||
let shared_key = self
|
|
||||||
.0
|
|
||||||
.keys
|
|
||||||
.shared_secret(&base64::decode(client_key.as_bytes()).unwrap());
|
|
||||||
|
|
||||||
let iv = &encrypted_blob[0..16];
|
|
||||||
let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20];
|
|
||||||
let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()];
|
|
||||||
|
|
||||||
let base_key = Sha1::digest(&shared_key);
|
|
||||||
let base_key = &base_key[..16];
|
|
||||||
|
|
||||||
let checksum_key = {
|
|
||||||
let mut h = HmacSha1::new_from_slice(base_key).expect("HMAC can take key of any size");
|
|
||||||
h.update(b"checksum");
|
|
||||||
h.finalize().into_bytes()
|
|
||||||
};
|
|
||||||
|
|
||||||
let encryption_key = {
|
|
||||||
let mut h = HmacSha1::new_from_slice(&base_key).expect("HMAC can take key of any size");
|
|
||||||
h.update(b"encryption");
|
|
||||||
h.finalize().into_bytes()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut h = HmacSha1::new_from_slice(&checksum_key).expect("HMAC can take key of any size");
|
|
||||||
h.update(encrypted);
|
|
||||||
if h.verify(cksum).is_err() {
|
|
||||||
warn!("Login error for user {:?}: MAC mismatch", username);
|
|
||||||
let result = json!({
|
|
||||||
"status": 102,
|
|
||||||
"spotifyError": 1,
|
|
||||||
"statusString": "ERROR-MAC"
|
|
||||||
});
|
|
||||||
|
|
||||||
let body = result.to_string();
|
|
||||||
return Response::new(Body::from(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
let decrypted = {
|
|
||||||
let mut data = encrypted.to_vec();
|
|
||||||
let mut cipher = Aes128Ctr::new(
|
|
||||||
&GenericArray::from_slice(&encryption_key[0..16]),
|
|
||||||
&GenericArray::from_slice(iv),
|
|
||||||
);
|
|
||||||
cipher.apply_keystream(&mut data);
|
|
||||||
String::from_utf8(data).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let credentials =
|
|
||||||
Credentials::with_blob(username.to_string(), &decrypted, &self.0.device_id);
|
|
||||||
|
|
||||||
self.0.tx.send(credentials).unwrap();
|
|
||||||
|
|
||||||
let result = json!({
|
|
||||||
"status": 101,
|
|
||||||
"spotifyError": 0,
|
|
||||||
"statusString": "ERROR-OK"
|
|
||||||
});
|
|
||||||
|
|
||||||
let body = result.to_string();
|
|
||||||
Response::new(Body::from(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn not_found(&self) -> Response<hyper::Body> {
|
|
||||||
let mut res = Response::default();
|
|
||||||
*res.status_mut() = StatusCode::NOT_FOUND;
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn call(self, request: Request<Body>) -> hyper::Result<Response<Body>> {
|
|
||||||
let mut params = BTreeMap::new();
|
|
||||||
|
|
||||||
let (parts, body) = request.into_parts();
|
|
||||||
|
|
||||||
if let Some(query) = parts.uri.query() {
|
|
||||||
let query_params = url::form_urlencoded::parse(query.as_bytes());
|
|
||||||
params.extend(query_params);
|
|
||||||
}
|
|
||||||
|
|
||||||
if parts.method != Method::GET {
|
|
||||||
debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params);
|
|
||||||
}
|
|
||||||
|
|
||||||
let body = hyper::body::to_bytes(body).await?;
|
|
||||||
|
|
||||||
params.extend(url::form_urlencoded::parse(&body));
|
|
||||||
|
|
||||||
Ok(
|
|
||||||
match (parts.method, params.get("action").map(AsRef::as_ref)) {
|
|
||||||
(Method::GET, Some("getInfo")) => self.handle_get_info(params),
|
|
||||||
(Method::POST, Some("addUser")) => self.handle_add_user(params),
|
|
||||||
_ => self.not_found(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "with-dns-sd")]
|
|
||||||
pub struct DiscoveryStream {
|
|
||||||
credentials: mpsc::UnboundedReceiver<Credentials>,
|
|
||||||
_svc: DNSService,
|
|
||||||
_close_tx: oneshot::Sender<Infallible>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "with-dns-sd"))]
|
|
||||||
pub struct DiscoveryStream {
|
|
||||||
credentials: mpsc::UnboundedReceiver<Credentials>,
|
|
||||||
_svc: libmdns::Service,
|
|
||||||
_close_tx: oneshot::Sender<Infallible>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn discovery(
|
pub fn discovery(
|
||||||
|
@ -205,59 +21,11 @@ pub fn discovery(
|
||||||
device_id: String,
|
device_id: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
) -> io::Result<DiscoveryStream> {
|
) -> io::Result<DiscoveryStream> {
|
||||||
let (discovery, creds_rx) = Discovery::new(config.clone(), device_id);
|
librespot_discovery::Discovery::builder(device_id)
|
||||||
let (close_tx, close_rx) = oneshot::channel();
|
.device_type(config.device_type)
|
||||||
|
.port(port)
|
||||||
let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port);
|
.name(config.name)
|
||||||
|
.launch()
|
||||||
let make_service = make_service_fn(move |_| {
|
.map(DiscoveryStream)
|
||||||
let discovery = discovery.clone();
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||||
async move { Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().call(request))) }
|
|
||||||
});
|
|
||||||
|
|
||||||
let server = hyper::Server::bind(&address).serve(make_service);
|
|
||||||
|
|
||||||
let s_port = server.local_addr().port();
|
|
||||||
debug!("Zeroconf server listening on 0.0.0.0:{}", s_port);
|
|
||||||
|
|
||||||
tokio::spawn(server.with_graceful_shutdown(async {
|
|
||||||
close_rx.await.unwrap_err();
|
|
||||||
debug!("Shutting down discovery server");
|
|
||||||
}));
|
|
||||||
|
|
||||||
#[cfg(feature = "with-dns-sd")]
|
|
||||||
let svc = DNSService::register(
|
|
||||||
Some(&*config.name),
|
|
||||||
"_spotify-connect._tcp",
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
s_port,
|
|
||||||
&["VERSION=1.0", "CPath=/"],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
#[cfg(not(feature = "with-dns-sd"))]
|
|
||||||
let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "with-dns-sd"))]
|
|
||||||
let svc = responder.register(
|
|
||||||
"_spotify-connect._tcp".to_owned(),
|
|
||||||
config.name,
|
|
||||||
s_port,
|
|
||||||
&["VERSION=1.0", "CPath=/"],
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(DiscoveryStream {
|
|
||||||
credentials: creds_rx,
|
|
||||||
_svc: svc,
|
|
||||||
_close_tx: close_tx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Stream for DiscoveryStream {
|
|
||||||
type Item = Credentials;
|
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
||||||
self.credentials.poll_recv(cx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,5 +6,9 @@ use librespot_playback as playback;
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.2.1",
|
||||||
|
note = "Please use the crate `librespot_discovery` instead."
|
||||||
|
)]
|
||||||
pub mod discovery;
|
pub mod discovery;
|
||||||
pub mod spirc;
|
pub mod spirc;
|
||||||
|
|
|
@ -71,30 +71,43 @@ impl FromStr for DeviceType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&DeviceType> for &str {
|
||||||
|
fn from(d: &DeviceType) -> &'static str {
|
||||||
|
use self::DeviceType::*;
|
||||||
|
match d {
|
||||||
|
Unknown => "Unknown",
|
||||||
|
Computer => "Computer",
|
||||||
|
Tablet => "Tablet",
|
||||||
|
Smartphone => "Smartphone",
|
||||||
|
Speaker => "Speaker",
|
||||||
|
Tv => "TV",
|
||||||
|
Avr => "AVR",
|
||||||
|
Stb => "STB",
|
||||||
|
AudioDongle => "AudioDongle",
|
||||||
|
GameConsole => "GameConsole",
|
||||||
|
CastAudio => "CastAudio",
|
||||||
|
CastVideo => "CastVideo",
|
||||||
|
Automobile => "Automobile",
|
||||||
|
Smartwatch => "Smartwatch",
|
||||||
|
Chromebook => "Chromebook",
|
||||||
|
UnknownSpotify => "UnknownSpotify",
|
||||||
|
CarThing => "CarThing",
|
||||||
|
Observer => "Observer",
|
||||||
|
HomeThing => "HomeThing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DeviceType> for &str {
|
||||||
|
fn from(d: DeviceType) -> &'static str {
|
||||||
|
(&d).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for DeviceType {
|
impl fmt::Display for DeviceType {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
use self::DeviceType::*;
|
let str: &str = self.into();
|
||||||
match *self {
|
f.write_str(str)
|
||||||
Unknown => f.write_str("Unknown"),
|
|
||||||
Computer => f.write_str("Computer"),
|
|
||||||
Tablet => f.write_str("Tablet"),
|
|
||||||
Smartphone => f.write_str("Smartphone"),
|
|
||||||
Speaker => f.write_str("Speaker"),
|
|
||||||
Tv => f.write_str("TV"),
|
|
||||||
Avr => f.write_str("AVR"),
|
|
||||||
Stb => f.write_str("STB"),
|
|
||||||
AudioDongle => f.write_str("AudioDongle"),
|
|
||||||
GameConsole => f.write_str("GameConsole"),
|
|
||||||
CastAudio => f.write_str("CastAudio"),
|
|
||||||
CastVideo => f.write_str("CastVideo"),
|
|
||||||
Automobile => f.write_str("Automobile"),
|
|
||||||
Smartwatch => f.write_str("Smartwatch"),
|
|
||||||
Chromebook => f.write_str("Chromebook"),
|
|
||||||
UnknownSpotify => f.write_str("UnknownSpotify"),
|
|
||||||
CarThing => f.write_str("CarThing"),
|
|
||||||
Observer => f.write_str("Observer"),
|
|
||||||
HomeThing => f.write_str("HomeThing"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,20 +6,21 @@ pub struct MercurySender {
|
||||||
mercury: MercuryManager,
|
mercury: MercuryManager,
|
||||||
uri: String,
|
uri: String,
|
||||||
pending: VecDeque<MercuryFuture<MercuryResponse>>,
|
pending: VecDeque<MercuryFuture<MercuryResponse>>,
|
||||||
|
buffered_future: Option<MercuryFuture<MercuryResponse>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MercurySender {
|
impl MercurySender {
|
||||||
// TODO: pub(super) when stable
|
|
||||||
pub(crate) fn new(mercury: MercuryManager, uri: String) -> MercurySender {
|
pub(crate) fn new(mercury: MercuryManager, uri: String) -> MercurySender {
|
||||||
MercurySender {
|
MercurySender {
|
||||||
mercury,
|
mercury,
|
||||||
uri,
|
uri,
|
||||||
pending: VecDeque::new(),
|
pending: VecDeque::new(),
|
||||||
|
buffered_future: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_flushed(&self) -> bool {
|
pub fn is_flushed(&self) -> bool {
|
||||||
self.pending.is_empty()
|
self.buffered_future.is_none() && self.pending.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&mut self, item: Vec<u8>) {
|
pub fn send(&mut self, item: Vec<u8>) {
|
||||||
|
@ -28,8 +29,13 @@ impl MercurySender {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn flush(&mut self) -> Result<(), MercuryError> {
|
pub async fn flush(&mut self) -> Result<(), MercuryError> {
|
||||||
for fut in self.pending.drain(..) {
|
if self.buffered_future.is_none() {
|
||||||
|
self.buffered_future = self.pending.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(fut) = self.buffered_future.as_mut() {
|
||||||
fut.await?;
|
fut.await?;
|
||||||
|
self.buffered_future = self.pending.pop_front();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -41,6 +47,7 @@ impl Clone for MercurySender {
|
||||||
mercury: self.mercury.clone(),
|
mercury: self.mercury.clone(),
|
||||||
uri: self.uri.clone(),
|
uri: self.uri.clone(),
|
||||||
pending: VecDeque::new(),
|
pending: VecDeque::new(),
|
||||||
|
buffered_future: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
40
discovery/Cargo.toml
Normal file
40
discovery/Cargo.toml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
[package]
|
||||||
|
name = "librespot-discovery"
|
||||||
|
version = "0.2.0"
|
||||||
|
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||||
|
description = "The discovery logic for librespot"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/librespot-org/librespot"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aes-ctr = "0.6"
|
||||||
|
base64 = "0.13"
|
||||||
|
cfg-if = "1.0"
|
||||||
|
form_urlencoded = "1.0"
|
||||||
|
futures-core = "0.3"
|
||||||
|
hmac = "0.11"
|
||||||
|
hyper = { version = "0.14", features = ["server", "http1", "tcp"] }
|
||||||
|
libmdns = "0.6"
|
||||||
|
log = "0.4"
|
||||||
|
rand = "0.8"
|
||||||
|
serde_json = "1.0.25"
|
||||||
|
sha-1 = "0.9"
|
||||||
|
thiserror = "1.0"
|
||||||
|
tokio = { version = "1.0", features = ["sync", "rt"] }
|
||||||
|
|
||||||
|
dns-sd = { version = "0.1.3", optional = true }
|
||||||
|
|
||||||
|
[dependencies.librespot-core]
|
||||||
|
path = "../core"
|
||||||
|
default_features = false
|
||||||
|
version = "0.2.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
futures = "0.3"
|
||||||
|
hex = "0.4"
|
||||||
|
simple_logger = "1.11"
|
||||||
|
tokio = { version = "1.0", features = ["macros", "rt"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
with-dns-sd = ["dns-sd"]
|
25
discovery/examples/discovery.rs
Normal file
25
discovery/examples/discovery.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use futures::StreamExt;
|
||||||
|
use librespot_discovery::DeviceType;
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
|
use simple_logger::SimpleLogger;
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
SimpleLogger::new()
|
||||||
|
.with_level(log::LevelFilter::Debug)
|
||||||
|
.init()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let name = "Librespot";
|
||||||
|
let device_id = hex::encode(Sha1::digest(name.as_bytes()));
|
||||||
|
|
||||||
|
let mut server = librespot_discovery::Discovery::builder(device_id)
|
||||||
|
.name(name)
|
||||||
|
.device_type(DeviceType::Computer)
|
||||||
|
.launch()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
while let Some(x) = server.next().await {
|
||||||
|
println!("Received {:?}", x);
|
||||||
|
}
|
||||||
|
}
|
150
discovery/src/lib.rs
Normal file
150
discovery/src/lib.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
//! Advertises this device to Spotify clients in the local network.
|
||||||
|
//!
|
||||||
|
//! This device will show up in the list of "available devices".
|
||||||
|
//! Once it is selected from the list, [`Credentials`] are received.
|
||||||
|
//! Those can be used to establish a new Session with [`librespot_core`].
|
||||||
|
//!
|
||||||
|
//! This library uses mDNS and DNS-SD so that other devices can find it,
|
||||||
|
//! and spawns an http server to answer requests of Spotify clients.
|
||||||
|
|
||||||
|
#![warn(clippy::all, missing_docs, rust_2018_idioms)]
|
||||||
|
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::io;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use futures_core::Stream;
|
||||||
|
use librespot_core as core;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use self::server::DiscoveryServer;
|
||||||
|
|
||||||
|
/// Credentials to be used in [`librespot`](`librespot_core`).
|
||||||
|
pub use crate::core::authentication::Credentials;
|
||||||
|
|
||||||
|
/// Determining the icon in the list of available devices.
|
||||||
|
pub use crate::core::config::DeviceType;
|
||||||
|
|
||||||
|
/// Makes this device visible to Spotify clients in the local network.
|
||||||
|
///
|
||||||
|
/// `Discovery` implements the [`Stream`] trait. Every time this device
|
||||||
|
/// is selected in the list of available devices, it yields [`Credentials`].
|
||||||
|
pub struct Discovery {
|
||||||
|
server: DiscoveryServer,
|
||||||
|
|
||||||
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
|
_svc: libmdns::Service,
|
||||||
|
#[cfg(feature = "with-dns-sd")]
|
||||||
|
_svc: dns_sd::DNSService,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A builder for [`Discovery`].
|
||||||
|
pub struct Builder {
|
||||||
|
server_config: server::Config,
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors that can occur while setting up a [`Discovery`] instance.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Setting up service discovery via DNS-SD failed.
|
||||||
|
#[error("Setting up dns-sd failed: {0}")]
|
||||||
|
DnsSdError(#[from] io::Error),
|
||||||
|
/// Setting up the http server failed.
|
||||||
|
#[error("Setting up the http server failed: {0}")]
|
||||||
|
HttpServerError(#[from] hyper::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Builder {
|
||||||
|
/// Starts a new builder using the provided device id.
|
||||||
|
pub fn new(device_id: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
server_config: server::Config {
|
||||||
|
name: "Librespot".into(),
|
||||||
|
device_type: DeviceType::default(),
|
||||||
|
device_id: device_id.into(),
|
||||||
|
},
|
||||||
|
port: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the name to be displayed. Default is `"Librespot"`.
|
||||||
|
pub fn name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
|
||||||
|
self.server_config.name = name.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the device type which is visible as icon in other Spotify clients. Default is `Speaker`.
|
||||||
|
pub fn device_type(mut self, device_type: DeviceType) -> Self {
|
||||||
|
self.server_config.device_type = device_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the port on which it should listen to incoming connections.
|
||||||
|
/// The default value `0` means any port.
|
||||||
|
pub fn port(mut self, port: u16) -> Self {
|
||||||
|
self.port = port;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up the [`Discovery`] instance.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// If setting up the mdns service or creating the server fails, this function returns an error.
|
||||||
|
pub fn launch(self) -> Result<Discovery, Error> {
|
||||||
|
let mut port = self.port;
|
||||||
|
let name = self.server_config.name.clone().into_owned();
|
||||||
|
let server = DiscoveryServer::new(self.server_config, &mut port)?;
|
||||||
|
|
||||||
|
let svc;
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "with-dns-sd")] {
|
||||||
|
svc = dns_sd::DNSService::register(
|
||||||
|
Some(name.as_ref()),
|
||||||
|
"_spotify-connect._tcp",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
port,
|
||||||
|
&["VERSION=1.0", "CPath=/"],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?;
|
||||||
|
svc = responder.register(
|
||||||
|
"_spotify-connect._tcp".to_owned(),
|
||||||
|
name,
|
||||||
|
port,
|
||||||
|
&["VERSION=1.0", "CPath=/"],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Discovery { server, _svc: svc })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Discovery {
|
||||||
|
/// Starts a [`Builder`] with the provided device id.
|
||||||
|
pub fn builder(device_id: impl Into<String>) -> Builder {
|
||||||
|
Builder::new(device_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new instance with the specified device id and default paramaters.
|
||||||
|
pub fn new(device_id: impl Into<String>) -> Result<Self, Error> {
|
||||||
|
Self::builder(device_id).launch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream for Discovery {
|
||||||
|
type Item = Credentials;
|
||||||
|
|
||||||
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
Pin::new(&mut self.server).poll_next(cx)
|
||||||
|
}
|
||||||
|
}
|
236
discovery/src/server.rs
Normal file
236
discovery/src/server.rs
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
|
use aes_ctr::cipher::generic_array::GenericArray;
|
||||||
|
use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher};
|
||||||
|
use aes_ctr::Aes128Ctr;
|
||||||
|
use futures_core::Stream;
|
||||||
|
use hmac::{Hmac, Mac, NewMac};
|
||||||
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
|
use hyper::{Body, Method, Request, Response, StatusCode};
|
||||||
|
use log::{debug, warn};
|
||||||
|
use serde_json::json;
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
|
use crate::core::authentication::Credentials;
|
||||||
|
use crate::core::config::DeviceType;
|
||||||
|
use crate::core::diffie_hellman::DhLocalKeys;
|
||||||
|
|
||||||
|
type Params<'a> = BTreeMap<Cow<'a, str>, Cow<'a, str>>;
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
pub name: Cow<'static, str>,
|
||||||
|
pub device_type: DeviceType,
|
||||||
|
pub device_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RequestHandler {
|
||||||
|
config: Config,
|
||||||
|
keys: DhLocalKeys,
|
||||||
|
tx: mpsc::UnboundedSender<Credentials>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHandler {
|
||||||
|
fn new(config: Config) -> (Self, mpsc::UnboundedReceiver<Credentials>) {
|
||||||
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
let discovery = Self {
|
||||||
|
config,
|
||||||
|
keys: DhLocalKeys::random(&mut rand::thread_rng()),
|
||||||
|
tx,
|
||||||
|
};
|
||||||
|
|
||||||
|
(discovery, rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_get_info(&self) -> Response<hyper::Body> {
|
||||||
|
let public_key = base64::encode(&self.keys.public_key());
|
||||||
|
let device_type: &str = self.config.device_type.into();
|
||||||
|
|
||||||
|
let body = json!({
|
||||||
|
"status": 101,
|
||||||
|
"statusString": "ERROR-OK",
|
||||||
|
"spotifyError": 0,
|
||||||
|
"version": "2.7.1",
|
||||||
|
"deviceID": (self.config.device_id),
|
||||||
|
"remoteName": (self.config.name),
|
||||||
|
"activeUser": "",
|
||||||
|
"publicKey": (public_key),
|
||||||
|
"deviceType": (device_type),
|
||||||
|
"libraryVersion": crate::core::version::SEMVER,
|
||||||
|
"accountReq": "PREMIUM",
|
||||||
|
"brandDisplayName": "librespot",
|
||||||
|
"modelDisplayName": "librespot",
|
||||||
|
"resolverVersion": "0",
|
||||||
|
"groupStatus": "NONE",
|
||||||
|
"voiceSupport": "NO",
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Response::new(Body::from(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_add_user(&self, params: &Params<'_>) -> Response<hyper::Body> {
|
||||||
|
let username = params.get("userName").unwrap().as_ref();
|
||||||
|
let encrypted_blob = params.get("blob").unwrap();
|
||||||
|
let client_key = params.get("clientKey").unwrap();
|
||||||
|
|
||||||
|
let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
let client_key = base64::decode(client_key.as_bytes()).unwrap();
|
||||||
|
let shared_key = self.keys.shared_secret(&client_key);
|
||||||
|
|
||||||
|
let iv = &encrypted_blob[0..16];
|
||||||
|
let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20];
|
||||||
|
let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()];
|
||||||
|
|
||||||
|
let base_key = Sha1::digest(&shared_key);
|
||||||
|
let base_key = &base_key[..16];
|
||||||
|
|
||||||
|
let checksum_key = {
|
||||||
|
let mut h =
|
||||||
|
Hmac::<Sha1>::new_from_slice(base_key).expect("HMAC can take key of any size");
|
||||||
|
h.update(b"checksum");
|
||||||
|
h.finalize().into_bytes()
|
||||||
|
};
|
||||||
|
|
||||||
|
let encryption_key = {
|
||||||
|
let mut h =
|
||||||
|
Hmac::<Sha1>::new_from_slice(base_key).expect("HMAC can take key of any size");
|
||||||
|
h.update(b"encryption");
|
||||||
|
h.finalize().into_bytes()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut h =
|
||||||
|
Hmac::<Sha1>::new_from_slice(&checksum_key).expect("HMAC can take key of any size");
|
||||||
|
h.update(encrypted);
|
||||||
|
if h.verify(cksum).is_err() {
|
||||||
|
warn!("Login error for user {:?}: MAC mismatch", username);
|
||||||
|
let result = json!({
|
||||||
|
"status": 102,
|
||||||
|
"spotifyError": 1,
|
||||||
|
"statusString": "ERROR-MAC"
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = result.to_string();
|
||||||
|
return Response::new(Body::from(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
let decrypted = {
|
||||||
|
let mut data = encrypted.to_vec();
|
||||||
|
let mut cipher = Aes128Ctr::new(
|
||||||
|
GenericArray::from_slice(&encryption_key[0..16]),
|
||||||
|
GenericArray::from_slice(iv),
|
||||||
|
);
|
||||||
|
cipher.apply_keystream(&mut data);
|
||||||
|
String::from_utf8(data).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let credentials =
|
||||||
|
Credentials::with_blob(username.to_string(), &decrypted, &self.config.device_id);
|
||||||
|
|
||||||
|
self.tx.send(credentials).unwrap();
|
||||||
|
|
||||||
|
let result = json!({
|
||||||
|
"status": 101,
|
||||||
|
"spotifyError": 0,
|
||||||
|
"statusString": "ERROR-OK"
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = result.to_string();
|
||||||
|
Response::new(Body::from(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_found(&self) -> Response<hyper::Body> {
|
||||||
|
let mut res = Response::default();
|
||||||
|
*res.status_mut() = StatusCode::NOT_FOUND;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(self: Arc<Self>, request: Request<Body>) -> hyper::Result<Response<Body>> {
|
||||||
|
let mut params = Params::new();
|
||||||
|
|
||||||
|
let (parts, body) = request.into_parts();
|
||||||
|
|
||||||
|
if let Some(query) = parts.uri.query() {
|
||||||
|
let query_params = form_urlencoded::parse(query.as_bytes());
|
||||||
|
params.extend(query_params);
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts.method != Method::GET {
|
||||||
|
debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = hyper::body::to_bytes(body).await?;
|
||||||
|
|
||||||
|
params.extend(form_urlencoded::parse(&body));
|
||||||
|
|
||||||
|
let action = params.get("action").map(Cow::as_ref);
|
||||||
|
|
||||||
|
Ok(match (parts.method, action) {
|
||||||
|
(Method::GET, Some("getInfo")) => self.handle_get_info(),
|
||||||
|
(Method::POST, Some("addUser")) => self.handle_add_user(¶ms),
|
||||||
|
_ => self.not_found(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiscoveryServer {
|
||||||
|
cred_rx: mpsc::UnboundedReceiver<Credentials>,
|
||||||
|
_close_tx: oneshot::Sender<Infallible>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscoveryServer {
|
||||||
|
pub fn new(config: Config, port: &mut u16) -> hyper::Result<Self> {
|
||||||
|
let (discovery, cred_rx) = RequestHandler::new(config);
|
||||||
|
let discovery = Arc::new(discovery);
|
||||||
|
|
||||||
|
let (close_tx, close_rx) = oneshot::channel();
|
||||||
|
|
||||||
|
let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port);
|
||||||
|
|
||||||
|
let make_service = make_service_fn(move |_| {
|
||||||
|
let discovery = discovery.clone();
|
||||||
|
async move {
|
||||||
|
Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().handle(request)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let server = hyper::Server::try_bind(&address)?.serve(make_service);
|
||||||
|
|
||||||
|
*port = server.local_addr().port();
|
||||||
|
debug!("Zeroconf server listening on 0.0.0.0:{}", *port);
|
||||||
|
|
||||||
|
tokio::spawn(async {
|
||||||
|
let result = server
|
||||||
|
.with_graceful_shutdown(async {
|
||||||
|
close_rx.await.unwrap_err();
|
||||||
|
debug!("Shutting down discovery server");
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
warn!("Discovery server failed: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
cred_rx,
|
||||||
|
_close_tx: close_tx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream for DiscoveryServer {
|
||||||
|
type Item = Credentials;
|
||||||
|
|
||||||
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Credentials>> {
|
||||||
|
self.cred_rx.poll_recv(cx)
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ gstreamer-app = { version = "0.16", optional = true }
|
||||||
glib = { version = "0.10", optional = true }
|
glib = { version = "0.10", optional = true }
|
||||||
|
|
||||||
# Rodio dependencies
|
# Rodio dependencies
|
||||||
rodio = { version = "0.13", optional = true, default-features = false }
|
rodio = { version = "0.14", optional = true, default-features = false }
|
||||||
cpal = { version = "0.13", optional = true }
|
cpal = { version = "0.13", optional = true }
|
||||||
thiserror = { version = "1", optional = true }
|
thiserror = { version = "1", optional = true }
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
pub use librespot_audio as audio;
|
pub use librespot_audio as audio;
|
||||||
pub use librespot_connect as connect;
|
pub use librespot_connect as connect;
|
||||||
pub use librespot_core as core;
|
pub use librespot_core as core;
|
||||||
|
pub use librespot_discovery as discovery;
|
||||||
pub use librespot_metadata as metadata;
|
pub use librespot_metadata as metadata;
|
||||||
pub use librespot_playback as playback;
|
pub use librespot_playback as playback;
|
||||||
pub use librespot_protocol as protocol;
|
pub use librespot_protocol as protocol;
|
||||||
|
|
|
@ -687,11 +687,14 @@ async fn main() {
|
||||||
let mut connecting: Pin<Box<dyn future::FusedFuture<Output = _>>> = Box::pin(future::pending());
|
let mut connecting: Pin<Box<dyn future::FusedFuture<Output = _>>> = Box::pin(future::pending());
|
||||||
|
|
||||||
if setup.enable_discovery {
|
if setup.enable_discovery {
|
||||||
let config = setup.connect_config.clone();
|
|
||||||
let device_id = setup.session_config.device_id.clone();
|
let device_id = setup.session_config.device_id.clone();
|
||||||
|
|
||||||
discovery = Some(
|
discovery = Some(
|
||||||
librespot_connect::discovery::discovery(config, device_id, setup.zeroconf_port)
|
librespot::discovery::Discovery::builder(device_id)
|
||||||
|
.name(setup.connect_config.name.clone())
|
||||||
|
.device_type(setup.connect_config.device_type)
|
||||||
|
.port(setup.zeroconf_port)
|
||||||
|
.launch()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue