├── .dockerignore ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── audio ├── Cargo.toml └── src │ ├── decrypt.rs │ ├── fetch.rs │ ├── lewton_decoder.rs │ ├── lib.rs │ └── libvorbis_decoder.rs ├── build.rs ├── cache └── .gitignore ├── contrib ├── Dockerfile ├── docker-build.sh └── librespot.service ├── core ├── Cargo.toml ├── build.rs └── src │ ├── apresolve.rs │ ├── audio_key.rs │ ├── authentication.rs │ ├── cache │ └── mod.rs │ ├── channel.rs │ ├── component.rs │ ├── config.rs │ ├── connection │ ├── codec.rs │ ├── handshake.rs │ └── mod.rs │ ├── diffie_hellman.rs │ ├── lib.in.rs │ ├── lib.rs │ ├── mercury │ ├── mod.rs │ ├── sender.rs │ └── types.rs │ ├── session.rs │ ├── util │ ├── int128.rs │ ├── mod.rs │ ├── spotify_id.rs │ └── subfile.rs │ └── version.rs ├── docs ├── authentication.md └── connection.md ├── examples └── play.rs ├── metadata ├── Cargo.toml └── src │ ├── cover.rs │ ├── lib.rs │ └── metadata.rs ├── protocol ├── Cargo.toml ├── build.rs ├── build.sh ├── files.rs ├── proto │ ├── ad-hermes-proxy.proto │ ├── appstore.proto │ ├── authentication.proto │ ├── facebook-publish.proto │ ├── facebook.proto │ ├── keyexchange.proto │ ├── mercury.proto │ ├── mergedprofile.proto │ ├── metadata.proto │ ├── playlist4changes.proto │ ├── playlist4content.proto │ ├── playlist4issues.proto │ ├── playlist4meta.proto │ ├── playlist4ops.proto │ ├── popcount.proto │ ├── presence.proto │ ├── pubsub.proto │ ├── radio.proto │ ├── search.proto │ ├── social.proto │ ├── socialgraph.proto │ ├── spirc.proto │ ├── suggest.proto │ └── toplist.proto └── src │ ├── authentication.rs │ ├── keyexchange.rs │ ├── lib.rs │ ├── mercury.rs │ ├── metadata.rs │ ├── pubsub.rs │ └── spirc.rs └── src ├── audio_backend ├── alsa.rs ├── mod.rs ├── pipe.rs ├── portaudio.rs └── pulseaudio.rs ├── discovery.rs ├── keymaster.rs ├── lib.in.rs ├── lib.rs ├── main.rs ├── mixer ├── mod.rs └── softmixer.rs ├── player.rs └── spirc.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | cache 3 | protocol/target 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .cargo 3 | spotify_appkey.key 4 | .vagrant/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - 1.17.0 4 | - stable 5 | - beta 6 | - nightly 7 | 8 | cache: cargo 9 | 10 | addons: 11 | apt: 12 | packages: 13 | - gcc-arm-linux-gnueabihf 14 | - libc6-dev-armhf-cross 15 | - libpulse-dev 16 | - portaudio19-dev 17 | 18 | before_script: 19 | - mkdir -p ~/.cargo 20 | - echo '[target.armv7-unknown-linux-gnueabihf]' > ~/.cargo/config 21 | - echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config 22 | - rustup target add armv7-unknown-linux-gnueabihf 23 | 24 | script: 25 | - cargo build --no-default-features 26 | - cargo build --no-default-features --features "with-tremor" 27 | - cargo build --no-default-features --features "portaudio-backend" 28 | - cargo build --no-default-features --features "pulseaudio-backend" 29 | - cargo build --no-default-features --features "alsa-backend" 30 | - cargo build --no-default-features --target armv7-unknown-linux-gnueabihf 31 | - if [[ $TRAVIS_RUST_VERSION != *"1.17.0"* ]]; then 32 | cargo build --no-default-features --features "with-lewton"; 33 | fi 34 | 35 | notifications: 36 | email: false 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot" 3 | version = "0.1.0" 4 | authors = ["Paul Liétar "] 5 | build = "build.rs" 6 | license = "MIT" 7 | description = "Open Source Spotify client library" 8 | keywords = ["spotify"] 9 | repository = "https://github.com/plietar/librespot" 10 | readme = "README.md" 11 | 12 | [workspace] 13 | 14 | [lib] 15 | name = "librespot" 16 | path = "src/lib.rs" 17 | 18 | [[bin]] 19 | name = "librespot" 20 | path = "src/main.rs" 21 | doc = false 22 | 23 | [dependencies.librespot-audio] 24 | path = "audio" 25 | [dependencies.librespot-core] 26 | path = "core" 27 | [dependencies.librespot-metadata] 28 | path = "metadata" 29 | [dependencies.librespot-protocol] 30 | path = "protocol" 31 | 32 | [dependencies] 33 | base64 = "0.5.0" 34 | env_logger = "0.4.0" 35 | futures = "0.1.8" 36 | getopts = "0.2.14" 37 | hyper = "0.11.2" 38 | log = "0.3.5" 39 | mdns = { git = "https://github.com/plietar/rust-mdns" } 40 | num-bigint = "0.1.35" 41 | protobuf = "1.1" 42 | rand = "0.3.13" 43 | rpassword = "0.3.0" 44 | rust-crypto = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" } 45 | serde = "0.9.6" 46 | serde_derive = "0.9.6" 47 | serde_json = "0.9.5" 48 | tokio-core = "0.1.2" 49 | tokio-signal = "0.1.2" 50 | url = "1.3" 51 | 52 | alsa = { git = "https://github.com/plietar/rust-alsa", optional = true } 53 | portaudio-rs = { version = "0.3.0", optional = true } 54 | libpulse-sys = { version = "0.0.0", optional = true } 55 | 56 | [build-dependencies] 57 | rand = "0.3.13" 58 | vergen = "0.1.0" 59 | protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros", features = ["with-syntex"] } 60 | 61 | [features] 62 | alsa-backend = ["alsa"] 63 | portaudio-backend = ["portaudio-rs"] 64 | pulseaudio-backend = ["libpulse-sys"] 65 | 66 | with-tremor = ["librespot-audio/with-tremor"] 67 | with-lewton = ["librespot-audio/with-lewton"] 68 | 69 | default = ["portaudio-backend"] 70 | 71 | [package.metadata.deb] 72 | maintainer = "nobody" 73 | copyright = "2016 Paul Liétar" 74 | license_file = ["LICENSE", "4"] 75 | depends = "$auto" 76 | extended_description = """\ 77 | librespot is an open source client library for Spotify. It enables applications \ 78 | to use Spotify's service, without using the official but closed-source \ 79 | libspotify. Additionally, it will provide extra features which are not \ 80 | available in the official library.""" 81 | section = "sound" 82 | priority = "optional" 83 | assets = [ 84 | ["target/release/librespot", "usr/bin/", "755"], 85 | ["contrib/librespot.service", "lib/systemd/system/", "644"] 86 | ] 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Paul Lietar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # librespot 2 | *librespot* is an open source client library for Spotify. It enables 3 | applications to use Spotify's service, without using the official but 4 | closed-source libspotify. Additionally, it will provide extra features 5 | which are not available in the official library. 6 | 7 | Note: librespot only works with Spotify Premium. 8 | 9 | # Unmaintained 10 | Unfortunately I am unable to maintain librespot anymore. It should still work, 11 | but issues and Pull requests will be ignored. Feel free to fork it and continue 12 | development there. If a fork gains traction I will happily point to it from the 13 | README. 14 | 15 | ## Building 16 | Rust 1.17.0 or later is required to build librespot. 17 | 18 | **If you are building librespot on macOS, the homebrew provided rust may fail due to the way in which homebrew installs rust. In this case, uninstall the homebrew version of rust and use [rustup](https://www.rustup.rs/), and librespot should then build.** 19 | 20 | It also requires a C, with portaudio. 21 | 22 | On debian / ubuntu, the following command will install these dependencies : 23 | ```shell 24 | sudo apt-get install build-essential portaudio19-dev 25 | ``` 26 | 27 | On Fedora systems, the following command will install these dependencies : 28 | ```shell 29 | sudo dnf install portaudio-devel make gcc 30 | ``` 31 | 32 | On macOS, using homebrew : 33 | ```shell 34 | brew install portaudio 35 | ``` 36 | 37 | Once you've cloned this repository you can build *librespot* using `cargo`. 38 | ```shell 39 | cargo build --release 40 | ``` 41 | 42 | ## Usage 43 | A sample program implementing a headless Spotify Connect receiver is provided. 44 | Once you've built *librespot*, run it using : 45 | ```shell 46 | target/release/librespot --username USERNAME --cache CACHEDIR --name DEVICENAME 47 | ``` 48 | 49 | ## Discovery mode 50 | *librespot* can be run in discovery mode, in which case no password is required at startup. 51 | For that, simply omit the `--username` argument. 52 | 53 | ## Audio Backends 54 | *librespot* supports various audio backends. Multiple backends can be enabled at compile time by enabling the 55 | corresponding cargo feature. By default, only PortAudio is enabled. 56 | 57 | A specific backend can selected at runtime using the `--backend` switch. 58 | 59 | ```shell 60 | cargo build --features portaudio-backend 61 | target/release/librespot [...] --backend portaudio 62 | ``` 63 | 64 | The following backends are currently available : 65 | - ALSA 66 | - PortAudio 67 | - PulseAudio 68 | 69 | ## Cross-compiling 70 | A cross compilation environment is provided as a docker image. 71 | Build the image from the root of the project with the following command : 72 | 73 | ``` 74 | $ docker build -t librespot-cross -f contrib/Dockerfile . 75 | ``` 76 | 77 | The resulting image can be used to build librespot for linux x86_64, armhf (compatible e. g. with Raspberry Pi 2 or 3, but not with Raspberry Pi 1 or Zero) and armel. 78 | The compiled binaries will be located in /tmp/librespot-build 79 | 80 | ``` 81 | docker run -v /tmp/librespot-build:/build librespot-cross 82 | ``` 83 | 84 | If only one architecture is desired, cargo can be invoked directly with the appropriate options : 85 | ```shell 86 | docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features alsa-backend 87 | docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend 88 | docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend 89 | ``` 90 | 91 | Don't forget to set the `with-tremor` feature flag if your target device does not have floating-point capabilities. 92 | 93 | ## Development 94 | When developing *librespot*, it is preferable to use Rust nightly, and build it using the following : 95 | ```shell 96 | cargo build --no-default-features --features "nightly portaudio-backend" 97 | ``` 98 | 99 | This produces better compilation error messages than with the default configuration. 100 | 101 | ## Disclaimer 102 | Using this code to connect to Spotify's API is probably forbidden by them. 103 | Use at your own risk. 104 | 105 | ## Contact 106 | Come and hang out on gitter if you need help or want to offer some. 107 | https://gitter.im/sashahilton00/spotify-connect-resources 108 | 109 | ## License 110 | Everything in this repository is licensed under the MIT license. 111 | 112 | -------------------------------------------------------------------------------- /audio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-audio" 3 | version = "0.1.0" 4 | authors = ["Paul Lietar "] 5 | 6 | [dependencies.librespot-core] 7 | path = "../core" 8 | 9 | [dependencies] 10 | bit-set = "0.4.0" 11 | byteorder = "1.0" 12 | futures = "0.1.8" 13 | log = "0.3.5" 14 | num-bigint = "0.1.35" 15 | num-traits = "0.1.36" 16 | rust-crypto = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" } 17 | tempfile = "2.1" 18 | vorbis = "0.1.0" 19 | 20 | tremor = { git = "https://github.com/plietar/rust-tremor", optional = true } 21 | lewton = { version = "0.6.2", optional = true } 22 | 23 | [features] 24 | with-tremor = ["tremor"] 25 | with-lewton = ["lewton"] 26 | -------------------------------------------------------------------------------- /audio/src/decrypt.rs: -------------------------------------------------------------------------------- 1 | use crypto::aes; 2 | use crypto::symmetriccipher::SynchronousStreamCipher; 3 | use num_bigint::BigUint; 4 | use num_traits::FromPrimitive; 5 | use std::io; 6 | use std::ops::Add; 7 | 8 | use core::audio_key::AudioKey; 9 | 10 | const AUDIO_AESIV: &'static [u8] = &[0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 11 | 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93]; 12 | 13 | pub struct AudioDecrypt { 14 | cipher: Box, 15 | key: AudioKey, 16 | reader: T, 17 | } 18 | 19 | impl AudioDecrypt { 20 | pub fn new(key: AudioKey, reader: T) -> AudioDecrypt { 21 | let cipher = aes::ctr(aes::KeySize::KeySize128, &key.0, AUDIO_AESIV); 22 | AudioDecrypt { 23 | cipher: cipher, 24 | key: key, 25 | reader: reader, 26 | } 27 | } 28 | } 29 | 30 | impl io::Read for AudioDecrypt { 31 | fn read(&mut self, output: &mut [u8]) -> io::Result { 32 | let mut buffer = vec![0u8; output.len()]; 33 | let len = try!(self.reader.read(&mut buffer)); 34 | 35 | self.cipher.process(&buffer[..len], &mut output[..len]); 36 | 37 | Ok(len) 38 | } 39 | } 40 | 41 | impl io::Seek for AudioDecrypt { 42 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 43 | let newpos = try!(self.reader.seek(pos)); 44 | let skip = newpos % 16; 45 | 46 | let iv = BigUint::from_bytes_be(AUDIO_AESIV) 47 | .add(BigUint::from_u64(newpos / 16).unwrap()) 48 | .to_bytes_be(); 49 | self.cipher = aes::ctr(aes::KeySize::KeySize128, &self.key.0, &iv); 50 | 51 | let buf = vec![0u8; skip as usize]; 52 | let mut buf2 = vec![0u8; skip as usize]; 53 | self.cipher.process(&buf, &mut buf2); 54 | 55 | Ok(newpos as u64) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /audio/src/fetch.rs: -------------------------------------------------------------------------------- 1 | use bit_set::BitSet; 2 | use byteorder::{ByteOrder, BigEndian, WriteBytesExt}; 3 | use futures::Stream; 4 | use futures::sync::{oneshot, mpsc}; 5 | use futures::{Poll, Async, Future}; 6 | use std::cmp::min; 7 | use std::fs; 8 | use std::io::{self, Read, Write, Seek, SeekFrom}; 9 | use std::sync::{Arc, Condvar, Mutex}; 10 | use tempfile::NamedTempFile; 11 | 12 | use core::channel::{Channel, ChannelData, ChannelError, ChannelHeaders}; 13 | use core::session::Session; 14 | use core::util::FileId; 15 | 16 | const CHUNK_SIZE: usize = 0x20000; 17 | 18 | pub enum AudioFile { 19 | Cached(fs::File), 20 | Streaming(AudioFileStreaming), 21 | } 22 | 23 | pub enum AudioFileOpen { 24 | Cached(Option), 25 | Streaming(AudioFileOpenStreaming), 26 | } 27 | 28 | pub struct AudioFileOpenStreaming { 29 | session: Session, 30 | data_rx: Option, 31 | headers: ChannelHeaders, 32 | file_id: FileId, 33 | complete_tx: Option>, 34 | } 35 | 36 | pub struct AudioFileStreaming { 37 | read_file: fs::File, 38 | 39 | position: u64, 40 | seek: mpsc::UnboundedSender, 41 | 42 | shared: Arc, 43 | } 44 | 45 | struct AudioFileShared { 46 | file_id: FileId, 47 | chunk_count: usize, 48 | cond: Condvar, 49 | bitmap: Mutex, 50 | } 51 | 52 | impl AudioFileOpenStreaming { 53 | fn finish(&mut self, size: usize) -> AudioFileStreaming { 54 | let chunk_count = (size + CHUNK_SIZE - 1) / CHUNK_SIZE; 55 | 56 | let shared = Arc::new(AudioFileShared { 57 | file_id: self.file_id, 58 | chunk_count: chunk_count, 59 | cond: Condvar::new(), 60 | bitmap: Mutex::new(BitSet::with_capacity(chunk_count)), 61 | }); 62 | 63 | let mut write_file = NamedTempFile::new().unwrap(); 64 | write_file.set_len(size as u64).unwrap(); 65 | write_file.seek(SeekFrom::Start(0)).unwrap(); 66 | 67 | let read_file = write_file.reopen().unwrap(); 68 | 69 | let data_rx = self.data_rx.take().unwrap(); 70 | let complete_tx = self.complete_tx.take().unwrap(); 71 | let (seek_tx, seek_rx) = mpsc::unbounded(); 72 | 73 | let fetcher = AudioFileFetch::new( 74 | self.session.clone(), shared.clone(), data_rx, write_file, seek_rx, complete_tx 75 | ); 76 | self.session.spawn(move |_| fetcher); 77 | 78 | AudioFileStreaming { 79 | read_file: read_file, 80 | 81 | position: 0, 82 | seek: seek_tx, 83 | 84 | shared: shared, 85 | } 86 | } 87 | } 88 | 89 | impl Future for AudioFileOpen { 90 | type Item = AudioFile; 91 | type Error = ChannelError; 92 | 93 | fn poll(&mut self) -> Poll { 94 | match *self { 95 | AudioFileOpen::Streaming(ref mut open) => { 96 | let file = try_ready!(open.poll()); 97 | Ok(Async::Ready(AudioFile::Streaming(file))) 98 | } 99 | AudioFileOpen::Cached(ref mut file) => { 100 | let file = file.take().unwrap(); 101 | Ok(Async::Ready(AudioFile::Cached(file))) 102 | } 103 | } 104 | } 105 | } 106 | 107 | impl Future for AudioFileOpenStreaming { 108 | type Item = AudioFileStreaming; 109 | type Error = ChannelError; 110 | 111 | fn poll(&mut self) -> Poll { 112 | loop { 113 | let (id, data) = try_ready!(self.headers.poll()).unwrap(); 114 | 115 | if id == 0x3 { 116 | let size = BigEndian::read_u32(&data) as usize * 4; 117 | let file = self.finish(size); 118 | 119 | return Ok(Async::Ready(file)); 120 | } 121 | } 122 | } 123 | } 124 | 125 | impl AudioFile { 126 | pub fn open(session: &Session, file_id: FileId) -> AudioFileOpen { 127 | let cache = session.cache().cloned(); 128 | 129 | if let Some(file) = cache.as_ref().and_then(|cache| cache.file(file_id)) { 130 | debug!("File {} already in cache", file_id); 131 | return AudioFileOpen::Cached(Some(file)); 132 | } 133 | 134 | debug!("Downloading file {}", file_id); 135 | 136 | let (complete_tx, complete_rx) = oneshot::channel(); 137 | let (headers, data) = request_chunk(session, file_id, 0).split(); 138 | 139 | let open = AudioFileOpenStreaming { 140 | session: session.clone(), 141 | file_id: file_id, 142 | 143 | headers: headers, 144 | data_rx: Some(data), 145 | 146 | complete_tx: Some(complete_tx), 147 | }; 148 | 149 | let session_ = session.clone(); 150 | session.spawn(move |_| { 151 | complete_rx.map(move |mut file| { 152 | if let Some(cache) = session_.cache() { 153 | cache.save_file(file_id, &mut file); 154 | debug!("File {} complete, saving to cache", file_id); 155 | } else { 156 | debug!("File {} complete", file_id); 157 | } 158 | }).or_else(|oneshot::Canceled| Ok(())) 159 | }); 160 | 161 | AudioFileOpen::Streaming(open) 162 | } 163 | } 164 | 165 | fn request_chunk(session: &Session, file: FileId, index: usize) -> Channel { 166 | trace!("requesting chunk {}", index); 167 | 168 | let start = (index * CHUNK_SIZE / 4) as u32; 169 | let end = ((index + 1) * CHUNK_SIZE / 4) as u32; 170 | 171 | let (id, channel) = session.channel().allocate(); 172 | 173 | let mut data: Vec = Vec::new(); 174 | data.write_u16::(id).unwrap(); 175 | data.write_u8(0).unwrap(); 176 | data.write_u8(1).unwrap(); 177 | data.write_u16::(0x0000).unwrap(); 178 | data.write_u32::(0x00000000).unwrap(); 179 | data.write_u32::(0x00009C40).unwrap(); 180 | data.write_u32::(0x00020000).unwrap(); 181 | data.write(&file.0).unwrap(); 182 | data.write_u32::(start).unwrap(); 183 | data.write_u32::(end).unwrap(); 184 | 185 | session.send_packet(0x8, data); 186 | 187 | channel 188 | } 189 | 190 | struct AudioFileFetch { 191 | session: Session, 192 | shared: Arc, 193 | output: Option, 194 | 195 | index: usize, 196 | data_rx: ChannelData, 197 | 198 | seek_rx: mpsc::UnboundedReceiver, 199 | complete_tx: Option>, 200 | } 201 | 202 | impl AudioFileFetch { 203 | fn new(session: Session, shared: Arc, 204 | data_rx: ChannelData, output: NamedTempFile, 205 | seek_rx: mpsc::UnboundedReceiver, 206 | complete_tx: oneshot::Sender) -> AudioFileFetch 207 | { 208 | AudioFileFetch { 209 | session: session, 210 | shared: shared, 211 | output: Some(output), 212 | 213 | index: 0, 214 | data_rx: data_rx, 215 | 216 | seek_rx: seek_rx, 217 | complete_tx: Some(complete_tx), 218 | } 219 | } 220 | 221 | fn download(&mut self, mut new_index: usize) { 222 | assert!(new_index < self.shared.chunk_count); 223 | 224 | { 225 | let bitmap = self.shared.bitmap.lock().unwrap(); 226 | while bitmap.contains(new_index) { 227 | new_index = (new_index + 1) % self.shared.chunk_count; 228 | } 229 | } 230 | 231 | if self.index != new_index { 232 | self.index = new_index; 233 | 234 | let offset = self.index * CHUNK_SIZE; 235 | 236 | self.output.as_mut().unwrap() 237 | .seek(SeekFrom::Start(offset as u64)).unwrap(); 238 | 239 | let (_headers, data) = request_chunk(&self.session, self.shared.file_id, self.index).split(); 240 | self.data_rx = data; 241 | } 242 | } 243 | 244 | fn finish(&mut self) { 245 | let mut output = self.output.take().unwrap(); 246 | let complete_tx = self.complete_tx.take().unwrap(); 247 | 248 | output.seek(SeekFrom::Start(0)).unwrap(); 249 | let _ = complete_tx.send(output); 250 | } 251 | } 252 | 253 | impl Future for AudioFileFetch { 254 | type Item = (); 255 | type Error = (); 256 | 257 | fn poll(&mut self) -> Poll<(), ()> { 258 | loop { 259 | let mut progress = false; 260 | 261 | match self.seek_rx.poll() { 262 | Ok(Async::Ready(None)) => { 263 | return Ok(Async::Ready(())); 264 | } 265 | Ok(Async::Ready(Some(offset))) => { 266 | progress = true; 267 | let index = offset as usize / CHUNK_SIZE; 268 | self.download(index); 269 | } 270 | Ok(Async::NotReady) => (), 271 | Err(()) => unreachable!(), 272 | } 273 | 274 | match self.data_rx.poll() { 275 | Ok(Async::Ready(Some(data))) => { 276 | progress = true; 277 | 278 | self.output.as_mut().unwrap() 279 | .write_all(data.as_ref()).unwrap(); 280 | } 281 | Ok(Async::Ready(None)) => { 282 | progress = true; 283 | 284 | trace!("chunk {} / {} complete", self.index, self.shared.chunk_count); 285 | 286 | let full = { 287 | let mut bitmap = self.shared.bitmap.lock().unwrap(); 288 | bitmap.insert(self.index as usize); 289 | self.shared.cond.notify_all(); 290 | 291 | bitmap.len() >= self.shared.chunk_count 292 | }; 293 | 294 | if full { 295 | self.finish(); 296 | return Ok(Async::Ready(())); 297 | } 298 | 299 | let new_index = (self.index + 1) % self.shared.chunk_count; 300 | self.download(new_index); 301 | } 302 | Ok(Async::NotReady) => (), 303 | Err(ChannelError) => { 304 | warn!("error from channel"); 305 | return Ok(Async::Ready(())); 306 | }, 307 | } 308 | 309 | if !progress { 310 | return Ok(Async::NotReady); 311 | } 312 | } 313 | } 314 | } 315 | 316 | impl Read for AudioFileStreaming { 317 | fn read(&mut self, output: &mut [u8]) -> io::Result { 318 | let index = self.position as usize / CHUNK_SIZE; 319 | let offset = self.position as usize % CHUNK_SIZE; 320 | let len = min(output.len(), CHUNK_SIZE - offset); 321 | 322 | let mut bitmap = self.shared.bitmap.lock().unwrap(); 323 | while !bitmap.contains(index) { 324 | bitmap = self.shared.cond.wait(bitmap).unwrap(); 325 | } 326 | drop(bitmap); 327 | 328 | let read_len = try!(self.read_file.read(&mut output[..len])); 329 | 330 | self.position += read_len as u64; 331 | 332 | Ok(read_len) 333 | } 334 | } 335 | 336 | impl Seek for AudioFileStreaming { 337 | fn seek(&mut self, pos: SeekFrom) -> io::Result { 338 | self.position = try!(self.read_file.seek(pos)); 339 | 340 | // Notify the fetch thread to get the correct block 341 | // This can fail if fetch thread has completed, in which case the 342 | // block is ready. Just ignore the error. 343 | let _ = self.seek.send(self.position); 344 | Ok(self.position) 345 | } 346 | } 347 | 348 | impl Read for AudioFile { 349 | fn read(&mut self, output: &mut [u8]) -> io::Result { 350 | match *self { 351 | AudioFile::Cached(ref mut file) => file.read(output), 352 | AudioFile::Streaming(ref mut file) => file.read(output), 353 | } 354 | } 355 | } 356 | 357 | impl Seek for AudioFile { 358 | fn seek(&mut self, pos: SeekFrom) -> io::Result { 359 | match *self { 360 | AudioFile::Cached(ref mut file) => file.seek(pos), 361 | AudioFile::Streaming(ref mut file) => file.seek(pos), 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /audio/src/lewton_decoder.rs: -------------------------------------------------------------------------------- 1 | extern crate lewton; 2 | 3 | use self::lewton::inside_ogg::OggStreamReader; 4 | 5 | use std::io::{Read, Seek}; 6 | use std::fmt; 7 | use std::error; 8 | 9 | pub struct VorbisDecoder(OggStreamReader); 10 | pub struct VorbisPacket(Vec); 11 | pub struct VorbisError(lewton::VorbisError); 12 | 13 | impl VorbisDecoder 14 | where R: Read + Seek 15 | { 16 | pub fn new(input: R) -> Result, VorbisError> { 17 | Ok(VorbisDecoder(OggStreamReader::new(input)?)) 18 | } 19 | 20 | pub fn seek(&mut self, ms: i64) -> Result<(), VorbisError> { 21 | let absgp = ms * 44100 / 1000; 22 | self.0.seek_absgp_pg(absgp as u64)?; 23 | Ok(()) 24 | } 25 | 26 | pub fn next_packet(&mut self) -> Result, VorbisError> { 27 | use self::lewton::VorbisError::BadAudio; 28 | use self::lewton::audio::AudioReadError::AudioIsHeader; 29 | loop { 30 | match self.0.read_dec_packet_itl() { 31 | Ok(Some(packet)) => return Ok(Some(VorbisPacket(packet))), 32 | Ok(None) => return Ok(None), 33 | 34 | Err(BadAudio(AudioIsHeader)) => (), 35 | Err(err) => return Err(err.into()), 36 | } 37 | } 38 | } 39 | } 40 | 41 | impl VorbisPacket { 42 | pub fn data(&self) -> &[i16] { 43 | &self.0 44 | } 45 | 46 | pub fn data_mut(&mut self) -> &mut [i16] { 47 | &mut self.0 48 | } 49 | } 50 | 51 | impl From for VorbisError { 52 | fn from(err: lewton::VorbisError) -> VorbisError { 53 | VorbisError(err) 54 | } 55 | } 56 | 57 | impl fmt::Debug for VorbisError { 58 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 59 | fmt::Debug::fmt(&self.0, f) 60 | } 61 | } 62 | 63 | impl fmt::Display for VorbisError { 64 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 65 | fmt::Display::fmt(&self.0, f) 66 | } 67 | } 68 | 69 | impl error::Error for VorbisError { 70 | fn description(&self) -> &str { 71 | error::Error::description(&self.0) 72 | } 73 | 74 | fn cause(&self) -> Option<&error::Error> { 75 | error::Error::cause(&self.0) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /audio/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] extern crate log; 2 | #[macro_use] extern crate futures; 3 | 4 | extern crate bit_set; 5 | extern crate byteorder; 6 | extern crate crypto; 7 | extern crate num_traits; 8 | extern crate num_bigint; 9 | extern crate tempfile; 10 | 11 | extern crate librespot_core as core; 12 | 13 | mod fetch; 14 | mod decrypt; 15 | 16 | #[cfg(not(feature = "with-lewton"))] 17 | mod libvorbis_decoder; 18 | #[cfg(feature = "with-lewton")] 19 | mod lewton_decoder; 20 | 21 | pub use fetch::{AudioFile, AudioFileOpen}; 22 | pub use decrypt::AudioDecrypt; 23 | 24 | #[cfg(not(feature = "with-lewton"))] 25 | pub use libvorbis_decoder::{VorbisDecoder, VorbisPacket, VorbisError}; 26 | #[cfg(feature = "with-lewton")] 27 | pub use lewton_decoder::{VorbisDecoder, VorbisPacket, VorbisError}; 28 | -------------------------------------------------------------------------------- /audio/src/libvorbis_decoder.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "with-tremor"))] extern crate vorbis; 2 | #[cfg(feature = "with-tremor")] extern crate tremor as vorbis; 3 | 4 | use std::io::{Read, Seek}; 5 | use std::fmt; 6 | use std::error; 7 | 8 | pub struct VorbisDecoder(vorbis::Decoder); 9 | pub struct VorbisPacket(vorbis::Packet); 10 | pub struct VorbisError(vorbis::VorbisError); 11 | 12 | impl VorbisDecoder 13 | where R: Read + Seek 14 | { 15 | pub fn new(input: R) -> Result, VorbisError> { 16 | Ok(VorbisDecoder(vorbis::Decoder::new(input)?)) 17 | } 18 | 19 | #[cfg(not(feature = "with-tremor"))] 20 | pub fn seek(&mut self, ms: i64) -> Result<(), VorbisError> { 21 | self.0.time_seek(ms as f64 / 1000f64)?; 22 | Ok(()) 23 | } 24 | 25 | #[cfg(feature = "with-tremor")] 26 | pub fn seek(&mut self, ms: i64) -> Result<(), VorbisError> { 27 | self.0.time_seek(ms)?; 28 | Ok(()) 29 | } 30 | 31 | pub fn next_packet(&mut self) -> Result, VorbisError> { 32 | loop { 33 | match self.0.packets().next() { 34 | Some(Ok(packet)) => return Ok(Some(VorbisPacket(packet))), 35 | None => return Ok(None), 36 | 37 | Some(Err(vorbis::VorbisError::Hole)) => (), 38 | Some(Err(err)) => return Err(err.into()), 39 | } 40 | } 41 | } 42 | } 43 | 44 | impl VorbisPacket { 45 | pub fn data(&self) -> &[i16] { 46 | &self.0.data 47 | } 48 | 49 | pub fn data_mut(&mut self) -> &mut [i16] { 50 | &mut self.0.data 51 | } 52 | } 53 | 54 | impl From for VorbisError { 55 | fn from(err: vorbis::VorbisError) -> VorbisError { 56 | VorbisError(err) 57 | } 58 | } 59 | 60 | impl fmt::Debug for VorbisError { 61 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 62 | fmt::Debug::fmt(&self.0, f) 63 | } 64 | } 65 | 66 | impl fmt::Display for VorbisError { 67 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 68 | fmt::Display::fmt(&self.0, f) 69 | } 70 | } 71 | 72 | impl error::Error for VorbisError { 73 | fn description(&self) -> &str { 74 | error::Error::description(&self.0) 75 | } 76 | 77 | fn cause(&self) -> Option<&error::Error> { 78 | error::Error::cause(&self.0) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate protobuf_macros; 2 | 3 | use std::env; 4 | use std::path::PathBuf; 5 | 6 | fn main() { 7 | let out = PathBuf::from(env::var("OUT_DIR").unwrap()); 8 | 9 | protobuf_macros::expand("src/lib.in.rs", &out.join("lib.rs")).unwrap(); 10 | 11 | println!("cargo:rerun-if-changed=src/lib.in.rs"); 12 | println!("cargo:rerun-if-changed=src/spirc.rs"); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /contrib/Dockerfile: -------------------------------------------------------------------------------- 1 | # Cross compilation environment for librespot 2 | # Build the docker image from the root of the project with the following command : 3 | # $ docker build -t librespot-cross -f contrib/Dockerfile . 4 | # 5 | # The resulting image can be used to build librespot for linux x86_64, armhf and armel. 6 | # $ docker run -v /tmp/librespot-build:/build librespot-cross 7 | # 8 | # The compiled binaries will be located in /tmp/librespot-build 9 | # 10 | # If only one architecture is desired, cargo can be invoked directly with the appropriate options : 11 | # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend" 12 | # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" 13 | # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend" 14 | # 15 | 16 | FROM debian:stretch 17 | 18 | RUN dpkg --add-architecture arm64 19 | RUN dpkg --add-architecture armhf 20 | RUN dpkg --add-architecture armel 21 | RUN dpkg --add-architecture mipsel 22 | RUN apt-get update 23 | 24 | RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 crossbuild-essential-armel crossbuild-essential-armhf crossbuild-essential-mipsel 25 | RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf libasound2-dev:mipsel 26 | 27 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 28 | ENV PATH="/root/.cargo/bin/:${PATH}" 29 | RUN rustup target add aarch64-unknown-linux-gnu 30 | RUN rustup target add arm-unknown-linux-gnueabi 31 | RUN rustup target add arm-unknown-linux-gnueabihf 32 | RUN rustup target add mipsel-unknown-linux-gnu 33 | 34 | RUN mkdir /.cargo && \ 35 | echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \ 36 | echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config && \ 37 | echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config && \ 38 | echo '[target.mipsel-unknown-linux-gnu]\nlinker = "mipsel-linux-gnu-gcc"' >> /.cargo/config 39 | 40 | RUN mkdir /build 41 | ENV CARGO_TARGET_DIR /build 42 | ENV CARGO_HOME /build/cache 43 | 44 | ADD . /src 45 | WORKDIR /src 46 | CMD ["/src/contrib/docker-build.sh"] 47 | -------------------------------------------------------------------------------- /contrib/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | cargo build --release --no-default-features --features alsa-backend 5 | cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend 6 | cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend 7 | cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend 8 | cargo build --release --target mipsel-unknown-linux-gnu --no-default-features --features alsa-backend 9 | -------------------------------------------------------------------------------- /contrib/librespot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Librespot 3 | Requires=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | User=nobody 8 | Group=audio 9 | Restart=always 10 | RestartSec=10 11 | ExecStart=/usr/bin/librespot -n "%p on %H" 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | 16 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-core" 3 | version = "0.1.0" 4 | authors = ["Paul Lietar "] 5 | build = "build.rs" 6 | 7 | [dependencies.librespot-protocol] 8 | path = "../protocol" 9 | 10 | [dependencies] 11 | base64 = "0.5.0" 12 | byteorder = "1.0" 13 | error-chain = { version = "0.9.0", default_features = false } 14 | futures = "0.1.8" 15 | hyper = "0.11.2" 16 | lazy_static = "0.2.0" 17 | log = "0.3.5" 18 | num-bigint = "0.1.35" 19 | num-integer = "0.1.32" 20 | num-traits = "0.1.36" 21 | protobuf = "1.1" 22 | rand = "0.3.13" 23 | rpassword = "0.3.0" 24 | rust-crypto = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" } 25 | serde = "0.9.6" 26 | serde_derive = "0.9.6" 27 | serde_json = "0.9.5" 28 | shannon = "0.2.0" 29 | tokio-core = "0.1.2" 30 | uuid = { version = "0.4", features = ["v4"] } 31 | 32 | [build-dependencies] 33 | protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros", features = ["with-syntex"] } 34 | rand = "0.3.13" 35 | vergen = "0.1.0" 36 | -------------------------------------------------------------------------------- /core/build.rs: -------------------------------------------------------------------------------- 1 | extern crate vergen; 2 | extern crate protobuf_macros; 3 | extern crate rand; 4 | 5 | use rand::Rng; 6 | use std::env; 7 | use std::path::PathBuf; 8 | use std::fs::OpenOptions; 9 | use std::io::Write; 10 | 11 | fn main() { 12 | let out = PathBuf::from(env::var("OUT_DIR").unwrap()); 13 | 14 | vergen::vergen(vergen::OutputFns::all()).unwrap(); 15 | 16 | let build_id: String = rand::thread_rng() 17 | .gen_ascii_chars() 18 | .take(8) 19 | .collect(); 20 | 21 | let mut version_file = 22 | OpenOptions::new() 23 | .write(true) 24 | .append(true) 25 | .open(&out.join("version.rs")) 26 | .unwrap(); 27 | 28 | let build_id_fn = format!(" 29 | /// Generate a random build id. 30 | pub fn build_id() -> &'static str {{ 31 | \"{}\" 32 | }} 33 | ", build_id); 34 | 35 | if let Err(e) = version_file.write_all(build_id_fn.as_bytes()) { 36 | println!("{}", e); 37 | } 38 | 39 | protobuf_macros::expand("src/lib.in.rs", &out.join("lib.rs")).unwrap(); 40 | 41 | println!("cargo:rerun-if-changed=src/lib.in.rs"); 42 | println!("cargo:rerun-if-changed=src/connection"); 43 | } 44 | -------------------------------------------------------------------------------- /core/src/apresolve.rs: -------------------------------------------------------------------------------- 1 | const AP_FALLBACK : &'static str = "ap.spotify.com:80"; 2 | const APRESOLVE_ENDPOINT : &'static str = "http://apresolve.spotify.com/"; 3 | 4 | use std::str::FromStr; 5 | use futures::{Future, Stream}; 6 | use hyper::{self, Uri, Client}; 7 | use serde_json; 8 | use tokio_core::reactor::Handle; 9 | 10 | error_chain! { } 11 | 12 | #[derive(Clone, Debug, Serialize, Deserialize)] 13 | pub struct APResolveData { 14 | ap_list: Vec 15 | } 16 | 17 | pub fn apresolve(handle: &Handle) -> Box> { 18 | let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL"); 19 | 20 | let client = Client::new(handle); 21 | let response = client.get(url); 22 | 23 | let body = response.and_then(|response| { 24 | response.body().fold(Vec::new(), |mut acc, chunk| { 25 | acc.extend_from_slice(chunk.as_ref()); 26 | Ok::<_, hyper::Error>(acc) 27 | }) 28 | }); 29 | let body = body.then(|result| result.chain_err(|| "HTTP error")); 30 | let body = body.and_then(|body| { 31 | String::from_utf8(body).chain_err(|| "invalid UTF8 in response") 32 | }); 33 | 34 | let data = body.and_then(|body| { 35 | serde_json::from_str::(&body) 36 | .chain_err(|| "invalid JSON") 37 | }); 38 | 39 | let ap = data.and_then(|data| { 40 | let ap = data.ap_list.first().ok_or("empty AP List")?; 41 | Ok(ap.clone()) 42 | }); 43 | 44 | Box::new(ap) 45 | } 46 | 47 | pub fn apresolve_or_fallback(handle: &Handle) 48 | -> Box> 49 | where E: 'static 50 | { 51 | let ap = apresolve(handle).or_else(|e| { 52 | warn!("Failed to resolve Access Point: {}", e.description()); 53 | warn!("Using fallback \"{}\"", AP_FALLBACK); 54 | Ok(AP_FALLBACK.into()) 55 | }); 56 | 57 | Box::new(ap) 58 | } 59 | -------------------------------------------------------------------------------- /core/src/audio_key.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; 2 | use futures::sync::oneshot; 3 | use futures::{Async, Future, Poll}; 4 | use std::collections::HashMap; 5 | use std::io::Write; 6 | use tokio_core::io::EasyBuf; 7 | 8 | use util::SeqGenerator; 9 | use util::{SpotifyId, FileId}; 10 | 11 | #[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] 12 | pub struct AudioKey(pub [u8; 16]); 13 | 14 | #[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] 15 | pub struct AudioKeyError; 16 | 17 | component! { 18 | AudioKeyManager : AudioKeyManagerInner { 19 | sequence: SeqGenerator = SeqGenerator::new(0), 20 | pending: HashMap>> = HashMap::new(), 21 | } 22 | } 23 | 24 | impl AudioKeyManager { 25 | pub fn dispatch(&self, cmd: u8, mut data: EasyBuf) { 26 | let seq = BigEndian::read_u32(data.drain_to(4).as_ref()); 27 | 28 | let sender = self.lock(|inner| inner.pending.remove(&seq)); 29 | 30 | if let Some(sender) = sender { 31 | match cmd { 32 | 0xd => { 33 | let mut key = [0u8; 16]; 34 | key.copy_from_slice(data.as_ref()); 35 | sender.complete(Ok(AudioKey(key))); 36 | } 37 | 0xe => { 38 | warn!("error audio key {:x} {:x}", data.as_ref()[0], data.as_ref()[1]); 39 | sender.complete(Err(AudioKeyError)); 40 | } 41 | _ => (), 42 | } 43 | } 44 | } 45 | 46 | pub fn request(&self, track: SpotifyId, file: FileId) -> AudioKeyFuture { 47 | let (tx, rx) = oneshot::channel(); 48 | 49 | let seq = self.lock(move |inner| { 50 | let seq = inner.sequence.get(); 51 | inner.pending.insert(seq, tx); 52 | seq 53 | }); 54 | 55 | self.send_key_request(seq, track, file); 56 | AudioKeyFuture(rx) 57 | } 58 | 59 | fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) { 60 | let mut data: Vec = Vec::new(); 61 | data.write(&file.0).unwrap(); 62 | data.write(&track.to_raw()).unwrap(); 63 | data.write_u32::(seq).unwrap(); 64 | data.write_u16::(0x0000).unwrap(); 65 | 66 | self.session().send_packet(0xc, data) 67 | } 68 | } 69 | 70 | pub struct AudioKeyFuture(oneshot::Receiver>); 71 | impl Future for AudioKeyFuture { 72 | type Item = T; 73 | type Error = AudioKeyError; 74 | 75 | fn poll(&mut self) -> Poll { 76 | match self.0.poll() { 77 | Ok(Async::Ready(Ok(value))) => Ok(Async::Ready(value)), 78 | Ok(Async::Ready(Err(err))) => Err(err), 79 | Ok(Async::NotReady) => Ok(Async::NotReady), 80 | Err(oneshot::Canceled) => Err(AudioKeyError), 81 | } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /core/src/authentication.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use byteorder::{BigEndian, ByteOrder}; 3 | use crypto; 4 | use crypto::aes; 5 | use crypto::digest::Digest; 6 | use crypto::hmac::Hmac; 7 | use crypto::pbkdf2::pbkdf2; 8 | use crypto::sha1::Sha1; 9 | use protobuf::ProtobufEnum; 10 | use rpassword; 11 | use serde; 12 | use serde_json; 13 | use std::io::{self, stderr, Read, Write}; 14 | use std::fs::File; 15 | use std::path::Path; 16 | 17 | use protocol::authentication::AuthenticationType; 18 | 19 | #[derive(Debug, Clone)] 20 | #[derive(Serialize, Deserialize)] 21 | pub struct Credentials { 22 | pub username: String, 23 | 24 | #[serde(serialize_with="serialize_protobuf_enum")] 25 | #[serde(deserialize_with="deserialize_protobuf_enum")] 26 | pub auth_type: AuthenticationType, 27 | 28 | #[serde(serialize_with="serialize_base64")] 29 | #[serde(deserialize_with="deserialize_base64")] 30 | pub auth_data: Vec, 31 | } 32 | 33 | impl Credentials { 34 | pub fn with_password(username: String, password: String) -> Credentials { 35 | Credentials { 36 | username: username, 37 | auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, 38 | auth_data: password.into_bytes(), 39 | } 40 | } 41 | 42 | pub fn with_blob(username: String, encrypted_blob: &str, device_id: &str) -> Credentials { 43 | fn read_u8(stream: &mut R) -> io::Result { 44 | let mut data = [0u8]; 45 | try!(stream.read_exact(&mut data)); 46 | Ok(data[0]) 47 | } 48 | 49 | fn read_int(stream: &mut R) -> io::Result { 50 | let lo = try!(read_u8(stream)) as u32; 51 | if lo & 0x80 == 0 { 52 | return Ok(lo); 53 | } 54 | 55 | let hi = try!(read_u8(stream)) as u32; 56 | Ok(lo & 0x7f | hi << 7) 57 | } 58 | 59 | fn read_bytes(stream: &mut R) -> io::Result> { 60 | let length = try!(read_int(stream)); 61 | let mut data = vec![0u8; length as usize]; 62 | try!(stream.read_exact(&mut data)); 63 | 64 | Ok(data) 65 | } 66 | 67 | let encrypted_blob = base64::decode(encrypted_blob).unwrap(); 68 | 69 | let secret = { 70 | let mut data = [0u8; 20]; 71 | let mut h = crypto::sha1::Sha1::new(); 72 | h.input(device_id.as_bytes()); 73 | h.result(&mut data); 74 | data 75 | }; 76 | 77 | let key = { 78 | let mut data = [0u8; 24]; 79 | let mut mac = Hmac::new(Sha1::new(), &secret); 80 | pbkdf2(&mut mac, username.as_bytes(), 0x100, &mut data[0..20]); 81 | 82 | let mut hash = Sha1::new(); 83 | hash.input(&data[0..20]); 84 | hash.result(&mut data[0..20]); 85 | BigEndian::write_u32(&mut data[20..], 20); 86 | data 87 | }; 88 | 89 | let blob = { 90 | // Anyone know what this block mode is ? 91 | let mut data = vec![0u8; encrypted_blob.len()]; 92 | let mut cipher = aes::ecb_decryptor(aes::KeySize::KeySize192, 93 | &key, 94 | crypto::blockmodes::NoPadding); 95 | cipher.decrypt(&mut crypto::buffer::RefReadBuffer::new(&encrypted_blob), 96 | &mut crypto::buffer::RefWriteBuffer::new(&mut data), 97 | true) 98 | .unwrap(); 99 | 100 | let l = encrypted_blob.len(); 101 | for i in 0..l - 0x10 { 102 | data[l - i - 1] ^= data[l - i - 0x11]; 103 | } 104 | 105 | data 106 | }; 107 | 108 | let mut cursor = io::Cursor::new(&blob); 109 | read_u8(&mut cursor).unwrap(); 110 | read_bytes(&mut cursor).unwrap(); 111 | read_u8(&mut cursor).unwrap(); 112 | let auth_type = read_int(&mut cursor).unwrap(); 113 | let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap(); 114 | read_u8(&mut cursor).unwrap(); 115 | let auth_data = read_bytes(&mut cursor).unwrap();; 116 | 117 | Credentials { 118 | username: username, 119 | auth_type: auth_type, 120 | auth_data: auth_data, 121 | } 122 | } 123 | 124 | pub fn from_reader(mut reader: R) -> Credentials { 125 | let mut contents = String::new(); 126 | reader.read_to_string(&mut contents).unwrap(); 127 | 128 | serde_json::from_str(&contents).unwrap() 129 | } 130 | 131 | pub fn from_file>(path: P) -> Option { 132 | File::open(path).ok().map(Credentials::from_reader) 133 | } 134 | 135 | pub fn save_to_writer(&self, writer: &mut W) { 136 | let contents = serde_json::to_string(&self.clone()).unwrap(); 137 | writer.write_all(contents.as_bytes()).unwrap(); 138 | } 139 | 140 | pub fn save_to_file>(&self, path: P) { 141 | let mut file = File::create(path).unwrap(); 142 | self.save_to_writer(&mut file) 143 | } 144 | } 145 | 146 | fn serialize_protobuf_enum(v: &T, ser: S) -> Result 147 | where T: ProtobufEnum, S: serde::Serializer { 148 | 149 | serde::Serialize::serialize(&v.value(), ser) 150 | } 151 | 152 | fn deserialize_protobuf_enum(de: D) -> Result 153 | where T: ProtobufEnum, D: serde::Deserializer { 154 | 155 | let v : i32 = try!(serde::Deserialize::deserialize(de)); 156 | T::from_i32(v).ok_or_else(|| serde::de::Error::custom("Invalid enum value")) 157 | } 158 | 159 | fn serialize_base64(v: &T, ser: S) -> Result 160 | where T: AsRef<[u8]>, S: serde::Serializer { 161 | 162 | serde::Serialize::serialize(&base64::encode(v.as_ref()), ser) 163 | } 164 | 165 | fn deserialize_base64(de: D) -> Result, D::Error> 166 | where D: serde::Deserializer { 167 | 168 | let v : String = try!(serde::Deserialize::deserialize(de)); 169 | base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string())) 170 | } 171 | 172 | pub fn get_credentials(username: Option, password: Option, 173 | cached_credentials: Option) 174 | -> Option 175 | { 176 | match (username, password, cached_credentials) { 177 | 178 | (Some(username), Some(password), _) 179 | => Some(Credentials::with_password(username, password)), 180 | 181 | (Some(ref username), _, Some(ref credentials)) 182 | if *username == credentials.username => Some(credentials.clone()), 183 | 184 | (Some(username), None, _) => { 185 | write!(stderr(), "Password for {}: ", username).unwrap(); 186 | stderr().flush().unwrap(); 187 | let password = rpassword::read_password().unwrap(); 188 | Some(Credentials::with_password(username.clone(), password)) 189 | } 190 | 191 | (None, _, Some(credentials)) 192 | => Some(credentials), 193 | 194 | (None, _, None) => None, 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /core/src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::io::Read; 3 | use std::fs::File; 4 | 5 | use util::{FileId, mkdir_existing}; 6 | use authentication::Credentials; 7 | 8 | #[derive(Clone)] 9 | pub struct Cache { 10 | root: PathBuf, 11 | use_audio_cache: bool, 12 | } 13 | 14 | impl Cache { 15 | pub fn new(location: PathBuf, use_audio_cache: bool) -> Cache { 16 | mkdir_existing(&location).unwrap(); 17 | mkdir_existing(&location.join("files")).unwrap(); 18 | 19 | Cache { 20 | root: location, 21 | use_audio_cache: use_audio_cache 22 | } 23 | } 24 | } 25 | 26 | impl Cache { 27 | fn credentials_path(&self) -> PathBuf { 28 | self.root.join("credentials.json") 29 | } 30 | 31 | pub fn credentials(&self) -> Option { 32 | let path = self.credentials_path(); 33 | Credentials::from_file(path) 34 | } 35 | 36 | pub fn save_credentials(&self, cred: &Credentials) { 37 | let path = self.credentials_path(); 38 | cred.save_to_file(&path); 39 | } 40 | } 41 | 42 | impl Cache { 43 | fn file_path(&self, file: FileId) -> PathBuf { 44 | let name = file.to_base16(); 45 | self.root.join("files").join(&name[0..2]).join(&name[2..]) 46 | } 47 | 48 | pub fn file(&self, file: FileId) -> Option { 49 | File::open(self.file_path(file)).ok() 50 | } 51 | 52 | pub fn save_file(&self, file: FileId, contents: &mut Read) { 53 | if self.use_audio_cache { 54 | let path = self.file_path(file); 55 | 56 | mkdir_existing(path.parent().unwrap()).unwrap(); 57 | 58 | let mut cache_file = File::create(path).unwrap(); 59 | ::std::io::copy(contents, &mut cache_file).unwrap(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/channel.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder}; 2 | use futures::sync::{BiLock, mpsc}; 3 | use futures::{Poll, Async, Stream}; 4 | use std::collections::HashMap; 5 | use tokio_core::io::EasyBuf; 6 | 7 | use util::SeqGenerator; 8 | 9 | component! { 10 | ChannelManager : ChannelManagerInner { 11 | sequence: SeqGenerator = SeqGenerator::new(0), 12 | channels: HashMap> = HashMap::new(), 13 | } 14 | } 15 | 16 | #[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] 17 | pub struct ChannelError; 18 | 19 | pub struct Channel { 20 | receiver: mpsc::UnboundedReceiver<(u8, EasyBuf)>, 21 | state: ChannelState, 22 | } 23 | 24 | pub struct ChannelHeaders(BiLock); 25 | pub struct ChannelData(BiLock); 26 | 27 | pub enum ChannelEvent { 28 | Header(u8, Vec), 29 | Data(EasyBuf), 30 | } 31 | 32 | #[derive(Clone)] 33 | enum ChannelState { 34 | Header(EasyBuf), 35 | Data, 36 | Closed, 37 | } 38 | 39 | impl ChannelManager { 40 | pub fn allocate(&self) -> (u16, Channel) { 41 | let (tx, rx) = mpsc::unbounded(); 42 | 43 | let seq = self.lock(|inner| { 44 | let seq = inner.sequence.get(); 45 | inner.channels.insert(seq, tx); 46 | seq 47 | }); 48 | 49 | let channel = Channel { 50 | receiver: rx, 51 | state: ChannelState::Header(EasyBuf::new()), 52 | }; 53 | 54 | (seq, channel) 55 | } 56 | 57 | pub fn dispatch(&self, cmd: u8, mut data: EasyBuf) { 58 | use std::collections::hash_map::Entry; 59 | 60 | let id: u16 = BigEndian::read_u16(data.drain_to(2).as_ref()); 61 | 62 | self.lock(|inner| { 63 | if let Entry::Occupied(entry) = inner.channels.entry(id) { 64 | let _ = entry.get().send((cmd, data)); 65 | } 66 | }); 67 | } 68 | } 69 | 70 | impl Channel { 71 | fn recv_packet(&mut self) -> Poll { 72 | let (cmd, packet) = match self.receiver.poll() { 73 | Ok(Async::Ready(t)) => t.expect("channel closed"), 74 | Ok(Async::NotReady) => return Ok(Async::NotReady), 75 | Err(()) => unreachable!(), 76 | }; 77 | 78 | if cmd == 0xa { 79 | let code = BigEndian::read_u16(&packet.as_ref()[..2]); 80 | error!("channel error: {} {}", packet.len(), code); 81 | 82 | self.state = ChannelState::Closed; 83 | 84 | Err(ChannelError) 85 | } else { 86 | Ok(Async::Ready(packet)) 87 | } 88 | } 89 | 90 | pub fn split(self) -> (ChannelHeaders, ChannelData) { 91 | let (headers, data) = BiLock::new(self); 92 | 93 | (ChannelHeaders(headers), ChannelData(data)) 94 | } 95 | } 96 | 97 | impl Stream for Channel { 98 | type Item = ChannelEvent; 99 | type Error = ChannelError; 100 | 101 | fn poll(&mut self) -> Poll, Self::Error> { 102 | loop { 103 | match self.state.clone() { 104 | ChannelState::Closed => panic!("Polling already terminated channel"), 105 | ChannelState::Header(mut data) => { 106 | if data.len() == 0 { 107 | data = try_ready!(self.recv_packet()); 108 | } 109 | 110 | let length = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize; 111 | if length == 0 { 112 | assert_eq!(data.len(), 0); 113 | self.state = ChannelState::Data; 114 | } else { 115 | let header_id = data.drain_to(1).as_ref()[0]; 116 | let header_data = data.drain_to(length - 1).as_ref().to_owned(); 117 | 118 | self.state = ChannelState::Header(data); 119 | 120 | let event = ChannelEvent::Header(header_id, header_data); 121 | return Ok(Async::Ready(Some(event))); 122 | } 123 | } 124 | 125 | ChannelState::Data => { 126 | let data = try_ready!(self.recv_packet()); 127 | if data.len() == 0 { 128 | self.receiver.close(); 129 | self.state = ChannelState::Closed; 130 | return Ok(Async::Ready(None)); 131 | } else { 132 | let event = ChannelEvent::Data(data); 133 | return Ok(Async::Ready(Some(event))); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | impl Stream for ChannelData { 142 | type Item = EasyBuf; 143 | type Error = ChannelError; 144 | 145 | fn poll(&mut self) -> Poll, Self::Error> { 146 | let mut channel = match self.0.poll_lock() { 147 | Async::Ready(c) => c, 148 | Async::NotReady => return Ok(Async::NotReady), 149 | }; 150 | 151 | loop { 152 | match try_ready!(channel.poll()) { 153 | Some(ChannelEvent::Header(..)) => (), 154 | Some(ChannelEvent::Data(data)) => return Ok(Async::Ready(Some(data))), 155 | None => return Ok(Async::Ready(None)), 156 | } 157 | } 158 | } 159 | } 160 | 161 | impl Stream for ChannelHeaders { 162 | type Item = (u8, Vec); 163 | type Error = ChannelError; 164 | 165 | fn poll(&mut self) -> Poll, Self::Error> { 166 | let mut channel = match self.0.poll_lock() { 167 | Async::Ready(c) => c, 168 | Async::NotReady => return Ok(Async::NotReady), 169 | }; 170 | 171 | match try_ready!(channel.poll()) { 172 | Some(ChannelEvent::Header(id, data)) => Ok(Async::Ready(Some((id, data)))), 173 | Some(ChannelEvent::Data(..)) | None => Ok(Async::Ready(None)), 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /core/src/component.rs: -------------------------------------------------------------------------------- 1 | macro_rules! component { 2 | ($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => { 3 | #[derive(Clone)] 4 | pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::std::sync::Mutex<$inner>)>); 5 | impl $name { 6 | #[allow(dead_code)] 7 | pub fn new(session: $crate::session::SessionWeak) -> $name { 8 | debug!(target:"librespot::component", "new {}", stringify!($name)); 9 | 10 | $name(::std::sync::Arc::new((session, ::std::sync::Mutex::new($inner { 11 | $($key : $value,)* 12 | })))) 13 | } 14 | 15 | #[allow(dead_code)] 16 | fn lock R, R>(&self, f: F) -> R { 17 | let mut inner = (self.0).1.lock().expect("Mutex poisoned"); 18 | f(&mut inner) 19 | } 20 | 21 | #[allow(dead_code)] 22 | fn session(&self) -> $crate::session::Session { 23 | (self.0).0.upgrade() 24 | } 25 | } 26 | 27 | struct $inner { 28 | $($key : $ty,)* 29 | } 30 | 31 | impl Drop for $inner { 32 | fn drop(&mut self) { 33 | debug!(target:"librespot::component", "drop {}", stringify!($name)); 34 | } 35 | } 36 | } 37 | } 38 | 39 | use std::sync::Mutex; 40 | use std::cell::UnsafeCell; 41 | 42 | pub struct Lazy(Mutex, UnsafeCell>); 43 | unsafe impl Sync for Lazy {} 44 | unsafe impl Send for Lazy {} 45 | 46 | #[cfg_attr(feature = "cargo-clippy", allow(mutex_atomic))] 47 | impl Lazy { 48 | pub fn new() -> Lazy { 49 | Lazy(Mutex::new(false), UnsafeCell::new(None)) 50 | } 51 | 52 | pub fn get T>(&self, f: F) -> &T { 53 | let mut inner = self.0.lock().unwrap(); 54 | if !*inner { 55 | unsafe { 56 | *self.1.get() = Some(f()); 57 | } 58 | *inner = true; 59 | } 60 | 61 | unsafe { &*self.1.get() }.as_ref().unwrap() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/config.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | use std::str::FromStr; 3 | use std::fmt; 4 | 5 | use version; 6 | 7 | #[derive(Clone,Debug)] 8 | pub struct SessionConfig { 9 | pub user_agent: String, 10 | pub device_id: String, 11 | } 12 | 13 | impl Default for SessionConfig { 14 | fn default() -> SessionConfig { 15 | let device_id = Uuid::new_v4().hyphenated().to_string(); 16 | SessionConfig { 17 | user_agent: version::version_string(), 18 | device_id: device_id, 19 | } 20 | } 21 | } 22 | 23 | 24 | #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] 25 | pub enum Bitrate { 26 | Bitrate96, 27 | Bitrate160, 28 | Bitrate320, 29 | } 30 | 31 | impl FromStr for Bitrate { 32 | type Err = (); 33 | fn from_str(s: &str) -> Result { 34 | match s { 35 | "96" => Ok(Bitrate::Bitrate96), 36 | "160" => Ok(Bitrate::Bitrate160), 37 | "320" => Ok(Bitrate::Bitrate320), 38 | _ => Err(()), 39 | } 40 | } 41 | } 42 | 43 | impl Default for Bitrate { 44 | fn default() -> Bitrate { 45 | Bitrate::Bitrate160 46 | } 47 | } 48 | 49 | #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] 50 | pub enum DeviceType { 51 | Unknown = 0, 52 | Computer = 1, 53 | Tablet = 2, 54 | Smartphone = 3, 55 | Speaker = 4, 56 | TV = 5, 57 | AVR = 6, 58 | STB = 7, 59 | AudioDongle = 8, 60 | } 61 | 62 | impl FromStr for DeviceType { 63 | type Err = (); 64 | fn from_str(s: &str) -> Result { 65 | use self::DeviceType::*; 66 | match s.to_lowercase().as_ref() { 67 | "computer" => Ok(Computer), 68 | "tablet" => Ok(Tablet), 69 | "smartphone" => Ok(Smartphone), 70 | "speaker" => Ok(Speaker), 71 | "tv" => Ok(TV), 72 | "avr" => Ok(AVR), 73 | "stb" => Ok(STB), 74 | "audiodongle" => Ok(AudioDongle), 75 | _ => Err(()), 76 | } 77 | } 78 | } 79 | 80 | impl fmt::Display for DeviceType { 81 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 82 | use self::DeviceType::*; 83 | match *self { 84 | Unknown => f.write_str("Unknown"), 85 | Computer => f.write_str("Computer"), 86 | Tablet => f.write_str("Tablet"), 87 | Smartphone => f.write_str("Smartphone"), 88 | Speaker => f.write_str("Speaker"), 89 | TV => f.write_str("TV"), 90 | AVR => f.write_str("AVR"), 91 | STB => f.write_str("STB"), 92 | AudioDongle => f.write_str("AudioDongle"), 93 | } 94 | } 95 | } 96 | 97 | impl Default for DeviceType { 98 | fn default() -> DeviceType { 99 | DeviceType::Speaker 100 | } 101 | } 102 | 103 | #[derive(Clone,Debug)] 104 | pub struct PlayerConfig { 105 | pub bitrate: Bitrate, 106 | pub onstart: Option, 107 | pub onstop: Option, 108 | } 109 | 110 | impl Default for PlayerConfig { 111 | fn default() -> PlayerConfig { 112 | PlayerConfig { 113 | bitrate: Bitrate::default(), 114 | onstart: None, 115 | onstop: None, 116 | } 117 | } 118 | } 119 | 120 | #[derive(Clone,Debug)] 121 | pub struct ConnectConfig { 122 | pub name: String, 123 | pub device_type: DeviceType, 124 | } 125 | -------------------------------------------------------------------------------- /core/src/connection/codec.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; 2 | use shannon::Shannon; 3 | use std::io; 4 | use tokio_core::io::{Codec, EasyBuf}; 5 | 6 | const HEADER_SIZE: usize = 3; 7 | const MAC_SIZE: usize = 4; 8 | 9 | #[derive(Debug)] 10 | enum DecodeState { 11 | Header, 12 | Payload(u8, usize), 13 | } 14 | 15 | pub struct APCodec { 16 | encode_nonce: u32, 17 | encode_cipher: Shannon, 18 | 19 | decode_nonce: u32, 20 | decode_cipher: Shannon, 21 | decode_state: DecodeState, 22 | } 23 | 24 | impl APCodec { 25 | pub fn new(send_key: &[u8], recv_key: &[u8]) -> APCodec { 26 | APCodec { 27 | encode_nonce: 0, 28 | encode_cipher: Shannon::new(send_key), 29 | 30 | decode_nonce: 0, 31 | decode_cipher: Shannon::new(recv_key), 32 | decode_state: DecodeState::Header, 33 | } 34 | } 35 | } 36 | 37 | impl Codec for APCodec { 38 | type Out = (u8, Vec); 39 | type In = (u8, EasyBuf); 40 | 41 | fn encode(&mut self, item: (u8, Vec), buf: &mut Vec) -> io::Result<()> { 42 | let (cmd, payload) = item; 43 | let offset = buf.len(); 44 | 45 | buf.write_u8(cmd).unwrap(); 46 | buf.write_u16::(payload.len() as u16).unwrap(); 47 | buf.extend_from_slice(&payload); 48 | 49 | self.encode_cipher.nonce_u32(self.encode_nonce); 50 | self.encode_nonce += 1; 51 | 52 | self.encode_cipher.encrypt(&mut buf[offset..]); 53 | 54 | let mut mac = [0u8; MAC_SIZE]; 55 | self.encode_cipher.finish(&mut mac); 56 | buf.extend_from_slice(&mac); 57 | 58 | Ok(()) 59 | } 60 | 61 | fn decode(&mut self, buf: &mut EasyBuf) -> io::Result> { 62 | if let DecodeState::Header = self.decode_state { 63 | if buf.len() >= HEADER_SIZE { 64 | let mut header = [0u8; HEADER_SIZE]; 65 | header.copy_from_slice(buf.drain_to(HEADER_SIZE).as_slice()); 66 | 67 | self.decode_cipher.nonce_u32(self.decode_nonce); 68 | self.decode_nonce += 1; 69 | 70 | self.decode_cipher.decrypt(&mut header); 71 | 72 | let cmd = header[0]; 73 | let size = BigEndian::read_u16(&header[1..]) as usize; 74 | self.decode_state = DecodeState::Payload(cmd, size); 75 | } 76 | } 77 | 78 | if let DecodeState::Payload(cmd, size) = self.decode_state { 79 | if buf.len() >= size + MAC_SIZE { 80 | self.decode_state = DecodeState::Header; 81 | 82 | let mut payload = buf.drain_to(size + MAC_SIZE); 83 | 84 | self.decode_cipher.decrypt(&mut payload.get_mut()[..size]); 85 | let mac = payload.split_off(size); 86 | self.decode_cipher.check_mac(mac.as_slice())?; 87 | 88 | return Ok(Some((cmd, payload))); 89 | } 90 | } 91 | 92 | 93 | Ok(None) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /core/src/connection/handshake.rs: -------------------------------------------------------------------------------- 1 | use crypto::sha1::Sha1; 2 | use crypto::hmac::Hmac; 3 | use crypto::mac::Mac;use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; 4 | use protobuf::{self, Message, MessageStatic}; 5 | use rand::thread_rng; 6 | use std::io::{self, Read, Write}; 7 | use std::marker::PhantomData; 8 | use tokio_core::io::{Io, Framed, write_all, WriteAll, read_exact, ReadExact, Window}; 9 | use futures::{Poll, Async, Future}; 10 | 11 | use diffie_hellman::DHLocalKeys; 12 | use protocol; 13 | use protocol::keyexchange::{ClientHello, APResponseMessage, ClientResponsePlaintext}; 14 | use util; 15 | use super::codec::APCodec; 16 | 17 | pub struct Handshake { 18 | keys: DHLocalKeys, 19 | state: HandshakeState, 20 | } 21 | 22 | enum HandshakeState { 23 | ClientHello(WriteAll>), 24 | APResponse(RecvPacket), 25 | ClientResponse(Option, WriteAll>), 26 | } 27 | 28 | pub fn handshake(connection: T) -> Handshake { 29 | let local_keys = DHLocalKeys::random(&mut thread_rng()); 30 | let client_hello = client_hello(connection, local_keys.public_key()); 31 | 32 | Handshake { 33 | keys: local_keys, 34 | state: HandshakeState::ClientHello(client_hello), 35 | } 36 | } 37 | 38 | impl Future for Handshake { 39 | type Item = Framed; 40 | type Error = io::Error; 41 | 42 | fn poll(&mut self) -> Poll { 43 | use self::HandshakeState::*; 44 | loop { 45 | self.state = match self.state { 46 | ClientHello(ref mut write) => { 47 | let (connection, accumulator) = try_ready!(write.poll()); 48 | 49 | let read = recv_packet(connection, accumulator); 50 | APResponse(read) 51 | } 52 | 53 | APResponse(ref mut read) => { 54 | let (connection, message, accumulator) = try_ready!(read.poll()); 55 | let remote_key = message.get_challenge() 56 | .get_login_crypto_challenge() 57 | .get_diffie_hellman() 58 | .get_gs() 59 | .to_owned(); 60 | 61 | let shared_secret = self.keys.shared_secret(&remote_key); 62 | let (challenge, send_key, recv_key) = compute_keys(&shared_secret, 63 | &accumulator); 64 | let codec = APCodec::new(&send_key, &recv_key); 65 | 66 | let write = client_response(connection, challenge); 67 | ClientResponse(Some(codec), write) 68 | } 69 | 70 | ClientResponse(ref mut codec, ref mut write) => { 71 | let (connection, _) = try_ready!(write.poll()); 72 | let codec = codec.take().unwrap(); 73 | let framed = connection.framed(codec); 74 | return Ok(Async::Ready(framed)); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | fn client_hello(connection: T, gc: Vec) -> WriteAll> { 82 | let packet = protobuf_init!(ClientHello::new(), { 83 | build_info => { 84 | product: protocol::keyexchange::Product::PRODUCT_PARTNER, 85 | platform: protocol::keyexchange::Platform::PLATFORM_LINUX_X86, 86 | version: 0x10800000000, 87 | }, 88 | cryptosuites_supported => [ 89 | protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON, 90 | ], 91 | login_crypto_hello.diffie_hellman => { 92 | gc: gc, 93 | server_keys_known: 1, 94 | }, 95 | client_nonce: util::rand_vec(&mut thread_rng(), 0x10), 96 | padding: vec![0x1e], 97 | }); 98 | 99 | let mut buffer = vec![0, 4]; 100 | let size = 2 + 4 + packet.compute_size(); 101 | buffer.write_u32::(size).unwrap(); 102 | packet.write_to_vec(&mut buffer).unwrap(); 103 | 104 | write_all(connection, buffer) 105 | } 106 | 107 | fn client_response(connection: T, challenge: Vec) -> WriteAll> { 108 | let packet = protobuf_init!(ClientResponsePlaintext::new(), { 109 | login_crypto_response.diffie_hellman => { 110 | hmac: challenge 111 | }, 112 | pow_response => {}, 113 | crypto_response => {}, 114 | }); 115 | 116 | let mut buffer = vec![]; 117 | let size = 4 + packet.compute_size(); 118 | buffer.write_u32::(size).unwrap(); 119 | packet.write_to_vec(&mut buffer).unwrap(); 120 | 121 | write_all(connection, buffer) 122 | } 123 | 124 | enum RecvPacket { 125 | Header(ReadExact>>, PhantomData), 126 | Body(ReadExact>>, PhantomData), 127 | } 128 | 129 | fn recv_packet(connection: T, acc: Vec) -> RecvPacket 130 | where T: Read, 131 | M: MessageStatic 132 | { 133 | RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData) 134 | } 135 | 136 | impl Future for RecvPacket 137 | where T: Read, 138 | M: MessageStatic 139 | { 140 | type Item = (T, M, Vec); 141 | type Error = io::Error; 142 | 143 | fn poll(&mut self) -> Poll { 144 | use self::RecvPacket::*; 145 | loop { 146 | *self = match *self { 147 | Header(ref mut read, _) => { 148 | let (connection, header) = try_ready!(read.poll()); 149 | let size = BigEndian::read_u32(header.as_ref()) as usize; 150 | 151 | let acc = header.into_inner(); 152 | let read = read_into_accumulator(connection, size - 4, acc); 153 | RecvPacket::Body(read, PhantomData) 154 | } 155 | 156 | Body(ref mut read, _) => { 157 | let (connection, data) = try_ready!(read.poll()); 158 | let message = protobuf::parse_from_bytes(data.as_ref()).unwrap(); 159 | 160 | let acc = data.into_inner(); 161 | return Ok(Async::Ready((connection, message, acc))); 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | fn read_into_accumulator(connection: T, size: usize, mut acc: Vec) -> ReadExact>> { 169 | let offset = acc.len(); 170 | acc.resize(offset + size, 0); 171 | 172 | let mut window = Window::new(acc); 173 | window.set_start(offset); 174 | 175 | read_exact(connection, window) 176 | } 177 | 178 | fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec, Vec, Vec) { 179 | let mut data = Vec::with_capacity(0x64); 180 | let mut mac = Hmac::new(Sha1::new(), &shared_secret); 181 | 182 | for i in 1..6 { 183 | mac.input(packets); 184 | mac.input(&[i]); 185 | data.extend_from_slice(&mac.result().code()); 186 | mac.reset(); 187 | } 188 | 189 | mac = Hmac::new(Sha1::new(), &data[..0x14]); 190 | mac.input(packets); 191 | 192 | (mac.result().code().to_vec(), data[0x14..0x34].to_vec(), data[0x34..0x54].to_vec()) 193 | } 194 | -------------------------------------------------------------------------------- /core/src/connection/mod.rs: -------------------------------------------------------------------------------- 1 | mod codec; 2 | mod handshake; 3 | 4 | pub use self::codec::APCodec; 5 | pub use self::handshake::handshake; 6 | 7 | use futures::{Future, Sink, Stream, BoxFuture}; 8 | use std::io; 9 | use std::net::ToSocketAddrs; 10 | use tokio_core::net::TcpStream; 11 | use tokio_core::reactor::Handle; 12 | use tokio_core::io::Framed; 13 | use protobuf::{self, Message}; 14 | 15 | use authentication::Credentials; 16 | use version; 17 | 18 | pub type Transport = Framed; 19 | 20 | pub fn connect(addr: A, handle: &Handle) -> BoxFuture { 21 | let addr = addr.to_socket_addrs().unwrap().next().unwrap(); 22 | let socket = TcpStream::connect(&addr, handle); 23 | let connection = socket.and_then(|socket| { 24 | handshake(socket) 25 | }); 26 | 27 | connection.boxed() 28 | } 29 | 30 | pub fn authenticate(transport: Transport, credentials: Credentials, device_id: String) 31 | -> BoxFuture<(Transport, Credentials), io::Error> 32 | { 33 | use protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; 34 | 35 | let packet = protobuf_init!(ClientResponseEncrypted::new(), { 36 | login_credentials => { 37 | username: credentials.username, 38 | typ: credentials.auth_type, 39 | auth_data: credentials.auth_data, 40 | }, 41 | system_info => { 42 | cpu_family: CpuFamily::CPU_UNKNOWN, 43 | os: Os::OS_UNKNOWN, 44 | system_information_string: format!("librespot_{}_{}", version::short_sha(), version::build_id()), 45 | device_id: device_id, 46 | }, 47 | version_string: version::version_string(), 48 | }); 49 | 50 | let cmd = 0xab; 51 | let data = packet.write_to_bytes().unwrap(); 52 | 53 | transport.send((cmd, data)).and_then(|transport| { 54 | transport.into_future().map_err(|(err, _stream)| err) 55 | }).and_then(|(packet, transport)| { 56 | match packet { 57 | Some((0xac, data)) => { 58 | let welcome_data: APWelcome = 59 | protobuf::parse_from_bytes(data.as_ref()).unwrap(); 60 | 61 | let reusable_credentials = Credentials { 62 | username: welcome_data.get_canonical_username().to_owned(), 63 | auth_type: welcome_data.get_reusable_auth_credentials_type(), 64 | auth_data: welcome_data.get_reusable_auth_credentials().to_owned(), 65 | }; 66 | 67 | Ok((transport, reusable_credentials)) 68 | } 69 | 70 | Some((0xad, _)) => panic!("Authentication failed"), 71 | Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd), 72 | None => panic!("EOF"), 73 | } 74 | }).boxed() 75 | } 76 | -------------------------------------------------------------------------------- /core/src/diffie_hellman.rs: -------------------------------------------------------------------------------- 1 | use num_bigint::BigUint; 2 | use num_traits::FromPrimitive; 3 | use rand::Rng; 4 | 5 | use util; 6 | 7 | lazy_static! { 8 | pub static ref DH_GENERATOR: BigUint = BigUint::from_u64(0x2).unwrap(); 9 | pub static ref DH_PRIME: BigUint = BigUint::from_bytes_be(&[ 10 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 11 | 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 12 | 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 13 | 0x08, 0x8a, 0x67, 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 14 | 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 15 | 0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 16 | 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 17 | 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 18 | 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 19 | 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, 20 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff ]); 21 | } 22 | 23 | pub struct DHLocalKeys { 24 | private_key: BigUint, 25 | public_key: BigUint, 26 | } 27 | 28 | impl DHLocalKeys { 29 | pub fn random(rng: &mut R) -> DHLocalKeys { 30 | let key_data = util::rand_vec(rng, 95); 31 | 32 | let private_key = BigUint::from_bytes_be(&key_data); 33 | let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); 34 | 35 | DHLocalKeys { 36 | private_key: private_key, 37 | public_key: public_key, 38 | } 39 | } 40 | 41 | pub fn public_key(&self) -> Vec { 42 | self.public_key.to_bytes_be() 43 | } 44 | 45 | pub fn shared_secret(&self, remote_key: &[u8]) -> Vec { 46 | let shared_key = util::powm(&BigUint::from_bytes_be(remote_key), 47 | &self.private_key, 48 | &DH_PRIME); 49 | shared_key.to_bytes_be() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /core/src/lib.in.rs: -------------------------------------------------------------------------------- 1 | pub mod connection; 2 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))] 2 | 3 | // TODO: many items from tokio-core::io have been deprecated in favour of tokio-io 4 | #![allow(deprecated)] 5 | 6 | #[macro_use] extern crate error_chain; 7 | #[macro_use] extern crate futures; 8 | #[macro_use] extern crate lazy_static; 9 | #[macro_use] extern crate log; 10 | #[macro_use] extern crate serde_derive; 11 | 12 | extern crate base64; 13 | extern crate byteorder; 14 | extern crate crypto; 15 | extern crate hyper; 16 | extern crate num_bigint; 17 | extern crate num_integer; 18 | extern crate num_traits; 19 | extern crate protobuf; 20 | extern crate rand; 21 | extern crate rpassword; 22 | extern crate serde; 23 | extern crate serde_json; 24 | extern crate shannon; 25 | extern crate tokio_core; 26 | extern crate uuid; 27 | 28 | extern crate librespot_protocol as protocol; 29 | 30 | #[macro_use] mod component; 31 | pub mod apresolve; 32 | pub mod audio_key; 33 | pub mod authentication; 34 | pub mod cache; 35 | pub mod channel; 36 | pub mod config; 37 | pub mod diffie_hellman; 38 | pub mod mercury; 39 | pub mod session; 40 | pub mod util; 41 | pub mod version; 42 | 43 | include!(concat!(env!("OUT_DIR"), "/lib.rs")); 44 | -------------------------------------------------------------------------------- /core/src/mercury/mod.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder}; 2 | use futures::sync::{oneshot, mpsc}; 3 | use futures::{Async, Poll, BoxFuture, Future}; 4 | use protobuf; 5 | use protocol; 6 | use std::collections::HashMap; 7 | use std::mem; 8 | use tokio_core::io::EasyBuf; 9 | 10 | use util::SeqGenerator; 11 | 12 | mod types; 13 | pub use self::types::*; 14 | 15 | mod sender; 16 | pub use self::sender::MercurySender; 17 | 18 | component! { 19 | MercuryManager : MercuryManagerInner { 20 | sequence: SeqGenerator = SeqGenerator::new(0), 21 | pending: HashMap, MercuryPending> = HashMap::new(), 22 | subscriptions: Vec<(String, mpsc::UnboundedSender)> = Vec::new(), 23 | } 24 | } 25 | 26 | pub struct MercuryPending { 27 | parts: Vec>, 28 | partial: Option>, 29 | callback: Option>>, 30 | } 31 | 32 | pub struct MercuryFuture(oneshot::Receiver>); 33 | impl Future for MercuryFuture { 34 | type Item = T; 35 | type Error = MercuryError; 36 | 37 | fn poll(&mut self) -> Poll { 38 | match self.0.poll() { 39 | Ok(Async::Ready(Ok(value))) => Ok(Async::Ready(value)), 40 | Ok(Async::Ready(Err(err))) => Err(err), 41 | Ok(Async::NotReady) => Ok(Async::NotReady), 42 | Err(oneshot::Canceled) => Err(MercuryError), 43 | } 44 | } 45 | } 46 | 47 | impl MercuryManager { 48 | fn next_seq(&self) -> Vec { 49 | let mut seq = vec![0u8; 8]; 50 | BigEndian::write_u64(&mut seq, self.lock(|inner| inner.sequence.get())); 51 | seq 52 | } 53 | 54 | pub fn request(&self, req: MercuryRequest) 55 | -> MercuryFuture 56 | { 57 | let (tx, rx) = oneshot::channel(); 58 | 59 | let pending = MercuryPending { 60 | parts: Vec::new(), 61 | partial: None, 62 | callback: Some(tx), 63 | }; 64 | 65 | let seq = self.next_seq(); 66 | self.lock(|inner| inner.pending.insert(seq.clone(), pending)); 67 | 68 | let cmd = req.method.command(); 69 | let data = req.encode(&seq); 70 | 71 | self.session().send_packet(cmd, data); 72 | MercuryFuture(rx) 73 | } 74 | 75 | pub fn get>(&self, uri: T) 76 | -> MercuryFuture 77 | { 78 | self.request(MercuryRequest { 79 | method: MercuryMethod::GET, 80 | uri: uri.into(), 81 | content_type: None, 82 | payload: Vec::new(), 83 | }) 84 | } 85 | 86 | pub fn send>(&self, uri: T, data: Vec) 87 | -> MercuryFuture 88 | { 89 | self.request(MercuryRequest { 90 | method: MercuryMethod::SEND, 91 | uri: uri.into(), 92 | content_type: None, 93 | payload: vec![data], 94 | }) 95 | } 96 | 97 | pub fn sender>(&self, uri: T) -> MercurySender { 98 | MercurySender::new(self.clone(), uri.into()) 99 | } 100 | 101 | pub fn subscribe>(&self, uri: T) 102 | -> BoxFuture, MercuryError> 103 | { 104 | let uri = uri.into(); 105 | let request = self.request(MercuryRequest { 106 | method: MercuryMethod::SUB, 107 | uri: uri.clone(), 108 | content_type: None, 109 | payload: Vec::new(), 110 | }); 111 | 112 | let manager = self.clone(); 113 | request.map(move |response| { 114 | let (tx, rx) = mpsc::unbounded(); 115 | 116 | manager.lock(move |inner| { 117 | debug!("subscribed uri={} count={}", uri, response.payload.len()); 118 | if response.payload.len() > 0 { 119 | // Old subscription protocol, watch the provided list of URIs 120 | for sub in response.payload { 121 | let mut sub : protocol::pubsub::Subscription 122 | = protobuf::parse_from_bytes(&sub).unwrap(); 123 | let sub_uri = sub.take_uri(); 124 | 125 | debug!("subscribed sub_uri={}", sub_uri); 126 | 127 | inner.subscriptions.push((sub_uri, tx.clone())); 128 | } 129 | } else { 130 | // New subscription protocol, watch the requested URI 131 | inner.subscriptions.push((uri, tx)); 132 | } 133 | }); 134 | 135 | rx 136 | }).boxed() 137 | } 138 | 139 | pub fn dispatch(&self, cmd: u8, mut data: EasyBuf) { 140 | let seq_len = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize; 141 | let seq = data.drain_to(seq_len).as_ref().to_owned(); 142 | 143 | let flags = data.drain_to(1).as_ref()[0]; 144 | let count = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize; 145 | 146 | let pending = self.lock(|inner| inner.pending.remove(&seq)); 147 | 148 | let mut pending = match pending { 149 | Some(pending) => pending, 150 | None if cmd == 0xb5 => { 151 | MercuryPending { 152 | parts: Vec::new(), 153 | partial: None, 154 | callback: None, 155 | } 156 | } 157 | None => { 158 | warn!("Ignore seq {:?} cmd {:x}", seq, cmd); 159 | return; 160 | } 161 | }; 162 | 163 | for i in 0..count { 164 | let mut part = Self::parse_part(&mut data); 165 | if let Some(mut partial) = mem::replace(&mut pending.partial, None) { 166 | partial.extend_from_slice(&part); 167 | part = partial; 168 | } 169 | 170 | if i == count - 1 && (flags == 2) { 171 | pending.partial = Some(part) 172 | } else { 173 | pending.parts.push(part); 174 | } 175 | } 176 | 177 | if flags == 0x1 { 178 | self.complete_request(cmd, pending); 179 | } else { 180 | self.lock(move |inner| inner.pending.insert(seq, pending)); 181 | } 182 | } 183 | 184 | fn parse_part(data: &mut EasyBuf) -> Vec { 185 | let size = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize; 186 | data.drain_to(size).as_ref().to_owned() 187 | } 188 | 189 | fn complete_request(&self, cmd: u8, mut pending: MercuryPending) { 190 | let header_data = pending.parts.remove(0); 191 | let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap(); 192 | 193 | let response = MercuryResponse { 194 | uri: header.get_uri().to_owned(), 195 | status_code: header.get_status_code(), 196 | payload: pending.parts, 197 | }; 198 | 199 | if response.status_code >= 400 { 200 | warn!("error {} for uri {}", response.status_code, &response.uri); 201 | if let Some(cb) = pending.callback { 202 | cb.complete(Err(MercuryError)); 203 | } 204 | } else { 205 | if cmd == 0xb5 { 206 | self.lock(|inner| { 207 | let mut found = false; 208 | inner.subscriptions.retain(|&(ref prefix, ref sub)| { 209 | if response.uri.starts_with(prefix) { 210 | found = true; 211 | 212 | // if send fails, remove from list of subs 213 | // TODO: send unsub message 214 | sub.send(response.clone()).is_ok() 215 | } else { 216 | // URI doesn't match 217 | true 218 | } 219 | }); 220 | 221 | if !found { 222 | debug!("unknown subscription uri={}", response.uri); 223 | } 224 | }) 225 | } else if let Some(cb) = pending.callback { 226 | cb.complete(Ok(response)); 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /core/src/mercury/sender.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use futures::{Async, Poll, Future, Sink, StartSend, AsyncSink}; 3 | 4 | use super::*; 5 | 6 | pub struct MercurySender { 7 | mercury: MercuryManager, 8 | uri: String, 9 | pending: VecDeque>, 10 | } 11 | 12 | impl MercurySender { 13 | // TODO: pub(super) when stable 14 | pub fn new(mercury: MercuryManager, uri: String) -> MercurySender { 15 | MercurySender { 16 | mercury: mercury, 17 | uri: uri, 18 | pending: VecDeque::new(), 19 | } 20 | } 21 | } 22 | 23 | impl Clone for MercurySender { 24 | fn clone(&self) -> MercurySender { 25 | MercurySender { 26 | mercury: self.mercury.clone(), 27 | uri: self.uri.clone(), 28 | pending: VecDeque::new(), 29 | } 30 | } 31 | } 32 | 33 | impl Sink for MercurySender { 34 | type SinkItem = Vec; 35 | type SinkError = MercuryError; 36 | 37 | fn start_send(&mut self, item: Self::SinkItem) -> StartSend { 38 | let task = self.mercury.send(self.uri.clone(), item); 39 | self.pending.push_back(task); 40 | Ok(AsyncSink::Ready) 41 | } 42 | 43 | fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { 44 | loop { 45 | match self.pending.front_mut() { 46 | Some(task) => { 47 | try_ready!(task.poll()); 48 | } 49 | None => { 50 | return Ok(Async::Ready(())); 51 | } 52 | } 53 | self.pending.pop_front(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/mercury/types.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, WriteBytesExt}; 2 | use protobuf::Message; 3 | use std::io::Write; 4 | 5 | use protocol; 6 | 7 | #[derive(Debug, PartialEq, Eq)] 8 | pub enum MercuryMethod { 9 | GET, 10 | SUB, 11 | UNSUB, 12 | SEND, 13 | } 14 | 15 | #[derive(Debug)] 16 | pub struct MercuryRequest { 17 | pub method: MercuryMethod, 18 | pub uri: String, 19 | pub content_type: Option, 20 | pub payload: Vec>, 21 | } 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct MercuryResponse { 25 | pub uri: String, 26 | pub status_code: i32, 27 | pub payload: Vec>, 28 | } 29 | 30 | #[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] 31 | pub struct MercuryError; 32 | 33 | impl ToString for MercuryMethod { 34 | fn to_string(&self) -> String { 35 | match *self { 36 | MercuryMethod::GET => "GET", 37 | MercuryMethod::SUB => "SUB", 38 | MercuryMethod::UNSUB => "UNSUB", 39 | MercuryMethod::SEND => "SEND", 40 | } 41 | .to_owned() 42 | } 43 | } 44 | 45 | impl MercuryMethod { 46 | pub fn command(&self) -> u8 { 47 | match *self { 48 | MercuryMethod::GET | MercuryMethod::SEND => 0xb2, 49 | MercuryMethod::SUB => 0xb3, 50 | MercuryMethod::UNSUB => 0xb4, 51 | } 52 | } 53 | } 54 | 55 | impl MercuryRequest { 56 | pub fn encode(&self, seq: &[u8]) -> Vec { 57 | let mut packet = Vec::new(); 58 | packet.write_u16::(seq.len() as u16).unwrap(); 59 | packet.write_all(seq).unwrap(); 60 | packet.write_u8(1).unwrap(); // Flags: FINAL 61 | packet.write_u16::(1 + self.payload.len() as u16).unwrap(); // Part count 62 | 63 | let mut header = protocol::mercury::Header::new(); 64 | header.set_uri(self.uri.clone()); 65 | header.set_method(self.method.to_string()); 66 | 67 | if let Some(ref content_type) = self.content_type { 68 | header.set_content_type(content_type.clone()); 69 | } 70 | 71 | packet.write_u16::(header.compute_size() as u16).unwrap(); 72 | header.write_to_writer(&mut packet).unwrap(); 73 | 74 | for p in &self.payload { 75 | packet.write_u16::(p.len() as u16).unwrap(); 76 | packet.write(p).unwrap(); 77 | } 78 | 79 | packet 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/src/session.rs: -------------------------------------------------------------------------------- 1 | use crypto::digest::Digest; 2 | use crypto::sha1::Sha1; 3 | use futures::sync::mpsc; 4 | use futures::{Future, Stream, BoxFuture, IntoFuture, Poll, Async}; 5 | use std::io; 6 | use std::sync::{RwLock, Arc, Weak}; 7 | use tokio_core::io::EasyBuf; 8 | use tokio_core::reactor::{Handle, Remote}; 9 | use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering}; 10 | 11 | use apresolve::apresolve_or_fallback; 12 | use authentication::Credentials; 13 | use cache::Cache; 14 | use component::Lazy; 15 | use connection; 16 | use config::SessionConfig; 17 | 18 | use audio_key::AudioKeyManager; 19 | use channel::ChannelManager; 20 | use mercury::MercuryManager; 21 | 22 | pub struct SessionData { 23 | country: String, 24 | canonical_username: String, 25 | } 26 | 27 | pub struct SessionInternal { 28 | config: SessionConfig, 29 | data: RwLock, 30 | 31 | tx_connection: mpsc::UnboundedSender<(u8, Vec)>, 32 | 33 | audio_key: Lazy, 34 | channel: Lazy, 35 | mercury: Lazy, 36 | cache: Option>, 37 | 38 | handle: Remote, 39 | 40 | session_id: usize, 41 | } 42 | 43 | static SESSION_COUNTER : AtomicUsize = ATOMIC_USIZE_INIT; 44 | 45 | #[derive(Clone)] 46 | pub struct Session(pub Arc); 47 | 48 | pub fn device_id(name: &str) -> String { 49 | let mut h = Sha1::new(); 50 | h.input_str(name); 51 | h.result_str() 52 | } 53 | 54 | impl Session { 55 | pub fn connect(config: SessionConfig, credentials: Credentials, 56 | cache: Option, handle: Handle) 57 | -> Box> 58 | { 59 | let access_point = apresolve_or_fallback::(&handle); 60 | 61 | 62 | let handle_ = handle.clone(); 63 | let connection = access_point.and_then(move |addr| { 64 | info!("Connecting to AP \"{}\"", addr); 65 | connection::connect::<&str>(&addr, &handle_) 66 | }); 67 | 68 | let device_id = config.device_id.clone(); 69 | let authentication = connection.and_then(move |connection| { 70 | connection::authenticate(connection, credentials, device_id) 71 | }); 72 | 73 | let result = authentication.map(move |(transport, reusable_credentials)| { 74 | info!("Authenticated as \"{}\" !", reusable_credentials.username); 75 | if let Some(ref cache) = cache { 76 | cache.save_credentials(&reusable_credentials); 77 | } 78 | 79 | let (session, task) = Session::create( 80 | &handle, transport, config, cache, reusable_credentials.username.clone() 81 | ); 82 | 83 | handle.spawn(task.map_err(|e| panic!(e))); 84 | 85 | session 86 | }); 87 | 88 | Box::new(result) 89 | } 90 | 91 | fn create(handle: &Handle, transport: connection::Transport, 92 | config: SessionConfig, cache: Option, username: String) 93 | -> (Session, BoxFuture<(), io::Error>) 94 | { 95 | let (sink, stream) = transport.split(); 96 | 97 | let (sender_tx, sender_rx) = mpsc::unbounded(); 98 | let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); 99 | 100 | debug!("new Session[{}]", session_id); 101 | 102 | let session = Session(Arc::new(SessionInternal { 103 | config: config, 104 | data: RwLock::new(SessionData { 105 | country: String::new(), 106 | canonical_username: username, 107 | }), 108 | 109 | tx_connection: sender_tx, 110 | 111 | cache: cache.map(Arc::new), 112 | 113 | audio_key: Lazy::new(), 114 | channel: Lazy::new(), 115 | mercury: Lazy::new(), 116 | 117 | handle: handle.remote().clone(), 118 | 119 | session_id: session_id, 120 | })); 121 | 122 | let sender_task = sender_rx 123 | .map_err(|e| -> io::Error { panic!(e) }) 124 | .forward(sink).map(|_| ()); 125 | let receiver_task = DispatchTask(stream, session.weak()); 126 | 127 | let task = (receiver_task, sender_task).into_future() 128 | .map(|((), ())| ()).boxed(); 129 | 130 | (session, task) 131 | } 132 | 133 | pub fn audio_key(&self) -> &AudioKeyManager { 134 | self.0.audio_key.get(|| AudioKeyManager::new(self.weak())) 135 | } 136 | 137 | pub fn channel(&self) -> &ChannelManager { 138 | self.0.channel.get(|| ChannelManager::new(self.weak())) 139 | } 140 | 141 | pub fn mercury(&self) -> &MercuryManager { 142 | self.0.mercury.get(|| MercuryManager::new(self.weak())) 143 | } 144 | 145 | pub fn spawn(&self, f: F) 146 | where F: FnOnce(&Handle) -> R + Send + 'static, 147 | R: IntoFuture, 148 | R::Future: 'static 149 | { 150 | self.0.handle.spawn(f) 151 | } 152 | 153 | fn debug_info(&self) { 154 | debug!("Session[{}] strong={} weak={}", 155 | self.0.session_id, Arc::strong_count(&self.0), Arc::weak_count(&self.0)); 156 | } 157 | 158 | #[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))] 159 | fn dispatch(&self, cmd: u8, data: EasyBuf) { 160 | match cmd { 161 | 0x4 => { 162 | self.debug_info(); 163 | self.send_packet(0x49, data.as_ref().to_owned()); 164 | }, 165 | 0x4a => (), 166 | 0x1b => { 167 | let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); 168 | info!("Country: {:?}", country); 169 | self.0.data.write().unwrap().country = country; 170 | } 171 | 172 | 0x9 | 0xa => self.channel().dispatch(cmd, data), 173 | 0xd | 0xe => self.audio_key().dispatch(cmd, data), 174 | 0xb2...0xb6 => self.mercury().dispatch(cmd, data), 175 | _ => (), 176 | } 177 | } 178 | 179 | pub fn send_packet(&self, cmd: u8, data: Vec) { 180 | self.0.tx_connection.send((cmd, data)).unwrap(); 181 | } 182 | 183 | pub fn cache(&self) -> Option<&Arc> { 184 | self.0.cache.as_ref() 185 | } 186 | 187 | pub fn config(&self) -> &SessionConfig { 188 | &self.0.config 189 | } 190 | 191 | pub fn username(&self) -> String { 192 | self.0.data.read().unwrap().canonical_username.clone() 193 | } 194 | 195 | pub fn country(&self) -> String { 196 | self.0.data.read().unwrap().country.clone() 197 | } 198 | 199 | pub fn device_id(&self) -> &str { 200 | &self.config().device_id 201 | } 202 | 203 | pub fn weak(&self) -> SessionWeak { 204 | SessionWeak(Arc::downgrade(&self.0)) 205 | } 206 | 207 | pub fn session_id(&self) -> usize { 208 | self.0.session_id 209 | } 210 | } 211 | 212 | #[derive(Clone)] 213 | pub struct SessionWeak(pub Weak); 214 | 215 | impl SessionWeak { 216 | pub fn try_upgrade(&self) -> Option { 217 | self.0.upgrade().map(Session) 218 | } 219 | 220 | pub fn upgrade(&self) -> Session { 221 | self.try_upgrade().expect("Session died") 222 | } 223 | } 224 | 225 | impl Drop for SessionInternal { 226 | fn drop(&mut self) { 227 | debug!("drop Session[{}]", self.session_id); 228 | } 229 | } 230 | 231 | struct DispatchTask(S, SessionWeak) 232 | where S: Stream; 233 | 234 | impl Future for DispatchTask 235 | where S: Stream 236 | { 237 | type Item = (); 238 | type Error = S::Error; 239 | 240 | fn poll(&mut self) -> Poll { 241 | let session = match self.1.try_upgrade() { 242 | Some(session) => session, 243 | None => { 244 | return Ok(Async::Ready(())) 245 | }, 246 | }; 247 | 248 | loop { 249 | let (cmd, data) = try_ready!(self.0.poll()).expect("connection closed"); 250 | session.dispatch(cmd, data); 251 | } 252 | } 253 | } 254 | 255 | impl Drop for DispatchTask 256 | where S: Stream 257 | { 258 | fn drop(&mut self) { 259 | debug!("drop Dispatch"); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /core/src/util/int128.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | 3 | #[derive(Debug,Copy,Clone,PartialEq,Eq,Hash)] 4 | #[allow(non_camel_case_types)] 5 | pub struct u128 { 6 | high: u64, 7 | low: u64, 8 | } 9 | 10 | impl u128 { 11 | pub fn zero() -> u128 { 12 | u128::from_parts(0, 0) 13 | } 14 | 15 | pub fn from_parts(high: u64, low: u64) -> u128 { 16 | u128 { 17 | high: high, 18 | low: low, 19 | } 20 | } 21 | 22 | pub fn parts(&self) -> (u64, u64) { 23 | (self.high, self.low) 24 | } 25 | } 26 | 27 | impl std::ops::Add for u128 { 28 | type Output = u128; 29 | fn add(self, rhs: u128) -> u128 { 30 | let low = self.low + rhs.low; 31 | let high = self.high + rhs.high + 32 | if low < self.low { 33 | 1 34 | } else { 35 | 0 36 | }; 37 | 38 | u128::from_parts(high, low) 39 | } 40 | } 41 | 42 | impl<'a> std::ops::Add<&'a u128> for u128 { 43 | type Output = u128; 44 | fn add(self, rhs: &'a u128) -> u128 { 45 | let low = self.low + rhs.low; 46 | let high = self.high + rhs.high + 47 | if low < self.low { 48 | 1 49 | } else { 50 | 0 51 | }; 52 | 53 | u128::from_parts(high, low) 54 | } 55 | } 56 | 57 | impl std::convert::From for u128 { 58 | fn from(n: u8) -> u128 { 59 | u128::from_parts(0, n as u64) 60 | } 61 | } 62 | 63 | 64 | impl std::ops::Mul for u128 { 65 | type Output = u128; 66 | 67 | fn mul(self, rhs: u128) -> u128 { 68 | let top: [u64; 4] = [self.high >> 32, 69 | self.high & 0xFFFFFFFF, 70 | self.low >> 32, 71 | self.low & 0xFFFFFFFF]; 72 | 73 | let bottom: [u64; 4] = [rhs.high >> 32, 74 | rhs.high & 0xFFFFFFFF, 75 | rhs.low >> 32, 76 | rhs.low & 0xFFFFFFFF]; 77 | 78 | let mut rows = [u128::zero(); 16]; 79 | for i in 0..4 { 80 | for j in 0..4 { 81 | let shift = i + j; 82 | let product = top[3 - i] * bottom[3 - j]; 83 | let (high, low) = match shift { 84 | 0 => (0, product), 85 | 1 => (product >> 32, product << 32), 86 | 2 => (product, 0), 87 | 3 => (product << 32, 0), 88 | _ => { 89 | if product == 0 { 90 | (0, 0) 91 | } else { 92 | panic!("Overflow on mul {:?} {:?} ({} {})", self, rhs, i, j) 93 | } 94 | } 95 | }; 96 | rows[j * 4 + i] = u128::from_parts(high, low); 97 | } 98 | } 99 | 100 | rows.iter().fold(u128::zero(), std::ops::Add::add) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | use num_bigint::BigUint; 2 | use num_traits::{Zero, One}; 3 | use num_integer::Integer; 4 | use rand::{Rng, Rand}; 5 | use std::io; 6 | use std::mem; 7 | use std::ops::{Mul, Rem, Shr}; 8 | use std::fs; 9 | use std::path::Path; 10 | use std::process::Command; 11 | use std::time::{UNIX_EPOCH, SystemTime}; 12 | 13 | mod int128; 14 | mod spotify_id; 15 | mod subfile; 16 | 17 | pub use util::int128::u128; 18 | pub use util::spotify_id::{SpotifyId, FileId}; 19 | pub use util::subfile::Subfile; 20 | 21 | pub fn rand_vec(rng: &mut G, size: usize) -> Vec { 22 | rng.gen_iter().take(size).collect() 23 | } 24 | 25 | pub fn now_ms() -> i64 { 26 | let dur = match SystemTime::now().duration_since(UNIX_EPOCH) { 27 | Ok(dur) => dur, 28 | Err(err) => err.duration(), 29 | }; 30 | (dur.as_secs() * 1000 + (dur.subsec_nanos() / 1000_000) as u64) as i64 31 | } 32 | 33 | pub fn mkdir_existing(path: &Path) -> io::Result<()> { 34 | fs::create_dir(path).or_else(|err| { 35 | if err.kind() == io::ErrorKind::AlreadyExists { 36 | Ok(()) 37 | } else { 38 | Err(err) 39 | } 40 | }) 41 | } 42 | 43 | pub fn run_program(program: &str) { 44 | info!("Running {}", program); 45 | let mut v: Vec<&str> = program.split_whitespace().collect(); 46 | let status = Command::new(&v.remove(0)) 47 | .args(&v) 48 | .status() 49 | .expect("program failed to start"); 50 | info!("Exit status: {}", status); 51 | } 52 | 53 | pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { 54 | let mut base = base.clone(); 55 | let mut exp = exp.clone(); 56 | let mut result: BigUint = One::one(); 57 | 58 | while !exp.is_zero() { 59 | if exp.is_odd() { 60 | result = result.mul(&base).rem(modulus); 61 | } 62 | exp = exp.shr(1); 63 | base = (&base).mul(&base).rem(modulus); 64 | } 65 | 66 | result 67 | } 68 | 69 | pub struct StrChunks<'s>(&'s str, usize); 70 | 71 | pub trait StrChunksExt { 72 | fn chunks(&self, size: usize) -> StrChunks; 73 | } 74 | 75 | impl StrChunksExt for str { 76 | fn chunks(&self, size: usize) -> StrChunks { 77 | StrChunks(self, size) 78 | } 79 | } 80 | 81 | impl<'s> Iterator for StrChunks<'s> { 82 | type Item = &'s str; 83 | fn next(&mut self) -> Option<&'s str> { 84 | let &mut StrChunks(data, size) = self; 85 | if data.is_empty() { 86 | None 87 | } else { 88 | let ret = Some(&data[..size]); 89 | self.0 = &data[size..]; 90 | ret 91 | } 92 | } 93 | } 94 | 95 | pub trait ReadSeek : ::std::io::Read + ::std::io::Seek { } 96 | impl ReadSeek for T { } 97 | 98 | pub trait Seq { 99 | fn next(&self) -> Self; 100 | } 101 | 102 | macro_rules! impl_seq { 103 | ($($ty:ty)*) => { $( 104 | impl Seq for $ty { 105 | fn next(&self) -> Self { *self + 1 } 106 | } 107 | )* } 108 | } 109 | 110 | impl_seq!(u8 u16 u32 u64 usize); 111 | 112 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] 113 | pub struct SeqGenerator(T); 114 | 115 | impl SeqGenerator { 116 | pub fn new(value: T) -> Self { 117 | SeqGenerator(value) 118 | } 119 | 120 | pub fn get(&mut self) -> T { 121 | let value = self.0.next(); 122 | mem::replace(&mut self.0, value) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /core/src/util/spotify_id.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::fmt; 3 | use util::u128; 4 | use byteorder::{BigEndian, ByteOrder}; 5 | use std::ascii::AsciiExt; 6 | 7 | #[derive(Debug,Copy,Clone,PartialEq,Eq,Hash)] 8 | pub struct SpotifyId(u128); 9 | 10 | const BASE62_DIGITS: &'static [u8] = 11 | b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 12 | const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef"; 13 | 14 | impl SpotifyId { 15 | pub fn from_base16(id: &str) -> SpotifyId { 16 | assert!(id.is_ascii()); 17 | let data = id.as_bytes(); 18 | 19 | let mut n: u128 = u128::zero(); 20 | for c in data { 21 | let d = BASE16_DIGITS.iter().position(|e| e == c).unwrap() as u8; 22 | n = n * u128::from(16); 23 | n = n + u128::from(d); 24 | } 25 | 26 | SpotifyId(n) 27 | } 28 | 29 | pub fn from_base62(id: &str) -> SpotifyId { 30 | assert!(id.is_ascii()); 31 | let data = id.as_bytes(); 32 | 33 | let mut n: u128 = u128::zero(); 34 | for c in data { 35 | let d = BASE62_DIGITS.iter().position(|e| e == c).unwrap() as u8; 36 | n = n * u128::from(62); 37 | n = n + u128::from(d); 38 | } 39 | 40 | SpotifyId(n) 41 | } 42 | 43 | pub fn from_raw(data: &[u8]) -> SpotifyId { 44 | assert_eq!(data.len(), 16); 45 | 46 | let high = BigEndian::read_u64(&data[0..8]); 47 | let low = BigEndian::read_u64(&data[8..16]); 48 | 49 | SpotifyId(u128::from_parts(high, low)) 50 | } 51 | 52 | pub fn to_base16(&self) -> String { 53 | let &SpotifyId(ref n) = self; 54 | let (high, low) = n.parts(); 55 | 56 | let mut data = [0u8; 32]; 57 | for i in 0..16 { 58 | data[31 - i] = BASE16_DIGITS[(low.wrapping_shr(4 * i as u32) & 0xF) as usize]; 59 | } 60 | for i in 0..16 { 61 | data[15 - i] = BASE16_DIGITS[(high.wrapping_shr(4 * i as u32) & 0xF) as usize]; 62 | } 63 | 64 | std::str::from_utf8(&data).unwrap().to_owned() 65 | } 66 | 67 | pub fn to_raw(&self) -> [u8; 16] { 68 | let &SpotifyId(ref n) = self; 69 | let (high, low) = n.parts(); 70 | 71 | let mut data = [0u8; 16]; 72 | 73 | BigEndian::write_u64(&mut data[0..8], high); 74 | BigEndian::write_u64(&mut data[8..16], low); 75 | 76 | data 77 | } 78 | } 79 | 80 | #[derive(Copy,Clone,PartialEq,Eq,PartialOrd,Ord,Hash)] 81 | pub struct FileId(pub [u8; 20]); 82 | 83 | impl FileId { 84 | pub fn to_base16(&self) -> String { 85 | self.0 86 | .iter() 87 | .map(|b| format!("{:02x}", b)) 88 | .collect::>() 89 | .concat() 90 | } 91 | } 92 | 93 | impl fmt::Debug for FileId { 94 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 95 | f.debug_tuple("FileId").field(&self.to_base16()).finish() 96 | } 97 | } 98 | 99 | impl fmt::Display for FileId { 100 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 101 | f.write_str(&self.to_base16()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /core/src/util/subfile.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Seek, SeekFrom, Result}; 2 | 3 | pub struct Subfile { 4 | stream: T, 5 | offset: u64, 6 | } 7 | 8 | impl Subfile { 9 | pub fn new(mut stream: T, offset: u64) -> Subfile { 10 | stream.seek(SeekFrom::Start(offset)).unwrap(); 11 | Subfile { 12 | stream: stream, 13 | offset: offset, 14 | } 15 | } 16 | } 17 | 18 | impl Read for Subfile { 19 | fn read(&mut self, buf: &mut [u8]) -> Result { 20 | self.stream.read(buf) 21 | } 22 | } 23 | 24 | impl Seek for Subfile { 25 | fn seek(&mut self, mut pos: SeekFrom) -> Result { 26 | pos = match pos { 27 | SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset), 28 | x => x, 29 | }; 30 | 31 | let newpos = try!(self.stream.seek(pos)); 32 | if newpos > self.offset { 33 | Ok(newpos - self.offset) 34 | } else { 35 | Ok(0) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/version.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/version.rs")); 2 | 3 | pub fn version_string() -> String { 4 | format!("librespot-{}", short_sha()) 5 | } 6 | -------------------------------------------------------------------------------- /docs/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | Once the connection is setup, the client can authenticate with the AP. For this, it sends an 3 | `ClientResponseEncrypted` message, using packet type `0xab`. 4 | 5 | A few different authentication methods are available. They are described below. 6 | 7 | The AP will then reply with either a `APWelcome` message using packet type `0xac` if authentication 8 | is successful, or an `APLoginFailed` with packet type `0xad` otherwise. 9 | 10 | ## Password based Authentication 11 | Password authentication is trivial. 12 | The `ClientResponseEncrypted` message's `LoginCredentials` is simply filled with the username 13 | and setting the password as the `auth_data`, and type `AUTHENTICATION_USER_PASS`. 14 | 15 | ## Zeroconf based Authentication 16 | Rather than relying on the user entering a username and password, devices can use zeroconf based 17 | authentication. This is especially useful for headless Spotify Connect devices. 18 | 19 | In this case, an already authenticated device, a phone or computer for example, discovers Spotify 20 | Connect receivers on the local network using Zeroconf. The receiver exposes an HTTP server with 21 | service type `_spotify-connect._tcp`, 22 | 23 | Two actions on the HTTP server are exposed, `getInfo` and `addUser`. 24 | The former returns information about the receiver, including its DH public key, in JSON format. 25 | The latter is used to send the username, the controller's DH public key, as well as the encrypted 26 | blob used to authenticate with Spotify's servers. 27 | 28 | The blob is decrypted using the following algorithm. 29 | 30 | ``` 31 | # encrypted_blob is the blob sent by the controller, decoded using base64 32 | # shared_secret is the result of the DH key exchange 33 | 34 | IV = encrypted_blob[:0x10] 35 | expected_mac = encrypted_blob[-0x14:] 36 | encrypted = encrypted_blob[0x10:-0x14] 37 | 38 | base_key = SHA1(shared_secret) 39 | checksum_key = HMAC-SHA1(base_key, "checksum") 40 | encryption_key = HMAC-SHA1(base_key, "encryption")[:0x10] 41 | 42 | mac = HMAC-SHA1(checksum_key, encrypted) 43 | assert mac == expected_mac 44 | 45 | blob = AES128-CTR-DECRYPT(encryption_key, IV, encrypted) 46 | ``` 47 | 48 | The blob is then used as described in the next section. 49 | 50 | ## Blob based Authentication 51 | 52 | ``` 53 | data = b64_decode(blob) 54 | base_key = PBKDF2(SHA1(deviceID), username, 0x100, 1) 55 | key = SHA1(base_key) || htonl(len(base_key)) 56 | login_data = AES192-DECRYPT(key, data) 57 | ``` 58 | 59 | ## Facebook based Authentication 60 | The client starts an HTTPS server, and makes the user visit 61 | `https://login.spotify.com/login-facebook-sso/?csrf=CSRF&port=PORT` 62 | in their browser, where CSRF is a random token, and PORT is the HTTPS server's port. 63 | 64 | This will redirect to Facebook, where the user must login and authorize Spotify, and 65 | finally make a GET request to 66 | `https://login.spotilocal.com:PORT/login/facebook_login_sso.json?csrf=CSRF&access_token=TOKEN`, 67 | where PORT and CSRF are the same as sent earlier, and TOKEN is the facebook authentication token. 68 | 69 | Since `login.spotilocal.com` resolves the 127.0.0.1, the request is received by the client. 70 | 71 | The client must then contact Facebook's API at 72 | `https://graph.facebook.com/me?fields=id&access_token=TOKEN` 73 | in order to retrieve the user's Facebook ID. 74 | 75 | The Facebook ID is the `username`, the TOKEN the `auth_data`, and `auth_type` is set to `AUTHENTICATION_FACEBOOK_TOKEN`. 76 | 77 | -------------------------------------------------------------------------------- /docs/connection.md: -------------------------------------------------------------------------------- 1 | # Connection Setup 2 | ## Access point Connection 3 | The first step to connecting to Spotify's servers is finding an Access Point (AP) to do so. 4 | Clients make an HTTP GET request to `http://apresolve.spotify.com` to retrieve a list of hostname an port combination in JSON format. 5 | An AP is randomly picked from that list to connect to. 6 | 7 | The connection is done using a bare TCP socket. Despite many APs using ports 80 and 443, neither HTTP nor TLS are used to connect. 8 | 9 | If `http://apresolve.spotify.com` is unresponsive, `ap.spotify.com:80` is used as a fallback. 10 | 11 | ## Connection Hello 12 | The first 3 packets exchanged are unencrypted, and have the following format : 13 | 14 | header | length | payload 15 | ---------|--------|--------- 16 | variable | 32 | variable 17 | 18 | Length is a 32 bit, big endian encoded, integer. 19 | It is the length of the entire packet, ie `len(header) + 4 + len(payload)`. 20 | 21 | The header is only present in the very first packet sent by the client, and is two bytes long, `[0, 4]`. 22 | It probably corresponds to the protocol version used. 23 | 24 | The payload is a protobuf encoded message. 25 | 26 | The client starts by sending a `ClientHello` message, describing the client info, a random nonce and client's Diffie Hellman public key. 27 | 28 | The AP replies by a `APResponseMessage` message, containing a random nonce and the server's DH key. 29 | 30 | The client solves a challenge based on these two packets, and sends it back using a `ClientResponsePlaintext`. 31 | It also computes the shared keys used to encrypt the rest of the communication. 32 | 33 | ## Login challenge and cipher key computation. 34 | The client starts by computing the DH shared secret using it's private key and the server's public key. 35 | HMAC-SHA1 is then used to compute the send and receive keys, as well as the login challenge. 36 | 37 | ``` 38 | data = [] 39 | for i in 1..6 { 40 | data += HMAC(client_hello || ap_response || [ i ], shared) 41 | } 42 | 43 | challenge = HMAC(client_hello || ap_response, data[:20]) 44 | send_key = data[20:52] 45 | recv_key = data[52:84] 46 | ``` 47 | 48 | `client_hello` and `ap_response` are the first packets sent respectively by the client and the AP. 49 | These include the header and length fields. 50 | 51 | ## Encrypted packets 52 | Every packet after ClientResponsePlaintext is encrypted using a Shannon cipher. 53 | 54 | The cipher is setup with 4 bytes big endian nonce, incremented after each packet, starting at zero. 55 | Two independent ciphers and accompanying nonces are used, one for transmission and one for reception, 56 | using respectively `send_key` and `recv_key` as keys. 57 | 58 | The packet format is as followed : 59 | 60 | cmd | length | payload | mac 61 | ----|--------|----------|---- 62 | 8 | 16 | variable | 32 63 | 64 | Each packet has a type identified by the 8 bit `cmd` field. 65 | The 16 bit big endian length only includes the length of the payload. 66 | 67 | -------------------------------------------------------------------------------- /examples/play.rs: -------------------------------------------------------------------------------- 1 | extern crate librespot; 2 | extern crate tokio_core; 3 | 4 | use std::env; 5 | use tokio_core::reactor::Core; 6 | 7 | use librespot::core::authentication::Credentials; 8 | use librespot::core::config::{PlayerConfig, SessionConfig}; 9 | use librespot::core::session::Session; 10 | use librespot::core::util::SpotifyId; 11 | 12 | use librespot::audio_backend; 13 | use librespot::player::Player; 14 | 15 | fn main() { 16 | let mut core = Core::new().unwrap(); 17 | let handle = core.handle(); 18 | 19 | let session_config = SessionConfig::default(); 20 | let player_config = PlayerConfig::default(); 21 | 22 | let args : Vec<_> = env::args().collect(); 23 | if args.len() != 4 { 24 | println!("Usage: {} USERNAME PASSWORD TRACK", args[0]); 25 | } 26 | let username = args[1].to_owned(); 27 | let password = args[2].to_owned(); 28 | let credentials = Credentials::with_password(username, password); 29 | 30 | let track = SpotifyId::from_base62(&args[3]); 31 | 32 | let backend = audio_backend::find(None).unwrap(); 33 | 34 | println!("Connecting .."); 35 | let session = core.run(Session::connect(session_config, credentials, None, handle)).unwrap(); 36 | 37 | let player = Player::new(player_config, session.clone(), None, move || (backend)(None)); 38 | 39 | println!("Playing..."); 40 | core.run(player.load(track, true, 0)).unwrap(); 41 | 42 | println!("Done"); 43 | } 44 | -------------------------------------------------------------------------------- /metadata/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-metadata" 3 | version = "0.1.0" 4 | authors = ["Paul Lietar "] 5 | 6 | [dependencies] 7 | byteorder = "1.0" 8 | futures = "0.1.8" 9 | linear-map = "1.0" 10 | protobuf = "1.1" 11 | 12 | [dependencies.librespot-core] 13 | path = "../core" 14 | [dependencies.librespot-protocol] 15 | path = "../protocol" 16 | -------------------------------------------------------------------------------- /metadata/src/cover.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, WriteBytesExt}; 2 | use std::io::Write; 3 | 4 | use core::channel::ChannelData; 5 | use core::session::Session; 6 | use core::util::FileId; 7 | 8 | pub fn get(session: &Session, file: FileId) -> ChannelData { 9 | let (channel_id, channel) = session.channel().allocate(); 10 | let (_headers, data) = channel.split(); 11 | 12 | let mut packet: Vec = Vec::new(); 13 | packet.write_u16::(channel_id).unwrap(); 14 | packet.write_u16::(0).unwrap(); 15 | packet.write(&file.0).unwrap(); 16 | session.send_packet(0x19, packet); 17 | 18 | data 19 | } 20 | -------------------------------------------------------------------------------- /metadata/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate byteorder; 2 | extern crate futures; 3 | extern crate linear_map; 4 | extern crate protobuf; 5 | 6 | extern crate librespot_core as core; 7 | extern crate librespot_protocol as protocol; 8 | 9 | pub mod cover; 10 | 11 | use futures::{Future, BoxFuture}; 12 | use linear_map::LinearMap; 13 | 14 | use core::mercury::MercuryError; 15 | use core::session::Session; 16 | use core::util::{SpotifyId, FileId, StrChunksExt}; 17 | 18 | pub use protocol::metadata::AudioFile_Format as FileFormat; 19 | 20 | fn countrylist_contains(list: &str, country: &str) -> bool { 21 | list.chunks(2).any(|cc| cc == country) 22 | } 23 | 24 | fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) -> bool 25 | where I: IntoIterator 26 | { 27 | let mut forbidden = "".to_string(); 28 | let mut has_forbidden = false; 29 | 30 | let mut allowed = "".to_string(); 31 | let mut has_allowed = false; 32 | 33 | let rs = restrictions.into_iter().filter(|r| 34 | r.get_catalogue_str().contains(&catalogue.to_owned()) 35 | ); 36 | 37 | for r in rs { 38 | if r.has_countries_forbidden() { 39 | forbidden.push_str(r.get_countries_forbidden()); 40 | has_forbidden = true; 41 | } 42 | 43 | if r.has_countries_allowed() { 44 | allowed.push_str(r.get_countries_allowed()); 45 | has_allowed = true; 46 | } 47 | } 48 | 49 | (has_forbidden || has_allowed) && 50 | (!has_forbidden || !countrylist_contains(forbidden.as_str(), country)) && 51 | (!has_allowed || countrylist_contains(allowed.as_str(), country)) 52 | } 53 | 54 | pub trait Metadata : Send + Sized + 'static { 55 | type Message: protobuf::MessageStatic; 56 | 57 | fn base_url() -> &'static str; 58 | fn parse(msg: &Self::Message, session: &Session) -> Self; 59 | 60 | fn get(session: &Session, id: SpotifyId) -> BoxFuture { 61 | let uri = format!("{}/{}", Self::base_url(), id.to_base16()); 62 | let request = session.mercury().get(uri); 63 | 64 | let session = session.clone(); 65 | request.and_then(move |response| { 66 | let data = response.payload.first().expect("Empty payload"); 67 | let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap(); 68 | 69 | Ok(Self::parse(&msg, &session)) 70 | }).boxed() 71 | } 72 | } 73 | 74 | #[derive(Debug, Clone)] 75 | pub struct Track { 76 | pub id: SpotifyId, 77 | pub name: String, 78 | pub album: SpotifyId, 79 | pub artists: Vec, 80 | pub files: LinearMap, 81 | pub alternatives: Vec, 82 | pub available: bool, 83 | } 84 | 85 | #[derive(Debug, Clone)] 86 | pub struct Album { 87 | pub id: SpotifyId, 88 | pub name: String, 89 | pub artists: Vec, 90 | pub tracks: Vec, 91 | pub covers: Vec, 92 | } 93 | 94 | #[derive(Debug, Clone)] 95 | pub struct Artist { 96 | pub id: SpotifyId, 97 | pub name: String, 98 | pub top_tracks: Vec, 99 | } 100 | 101 | impl Metadata for Track { 102 | type Message = protocol::metadata::Track; 103 | 104 | fn base_url() -> &'static str { 105 | "hm://metadata/3/track" 106 | } 107 | 108 | fn parse(msg: &Self::Message, session: &Session) -> Self { 109 | let country = session.country(); 110 | 111 | let artists = msg.get_artist() 112 | .iter() 113 | .filter(|artist| artist.has_gid()) 114 | .map(|artist| SpotifyId::from_raw(artist.get_gid())) 115 | .collect::>(); 116 | 117 | let files = msg.get_file() 118 | .iter() 119 | .filter(|file| file.has_file_id()) 120 | .map(|file| { 121 | let mut dst = [0u8; 20]; 122 | dst.clone_from_slice(file.get_file_id()); 123 | (file.get_format(), FileId(dst)) 124 | }) 125 | .collect(); 126 | 127 | Track { 128 | id: SpotifyId::from_raw(msg.get_gid()), 129 | name: msg.get_name().to_owned(), 130 | album: SpotifyId::from_raw(msg.get_album().get_gid()), 131 | artists: artists, 132 | files: files, 133 | alternatives: msg.get_alternative() 134 | .iter() 135 | .map(|alt| SpotifyId::from_raw(alt.get_gid())) 136 | .collect(), 137 | available: parse_restrictions(msg.get_restriction(), 138 | &country, 139 | "premium"), 140 | } 141 | } 142 | } 143 | 144 | impl Metadata for Album { 145 | type Message = protocol::metadata::Album; 146 | 147 | fn base_url() -> &'static str { 148 | "hm://metadata/3/album" 149 | } 150 | 151 | fn parse(msg: &Self::Message, _: &Session) -> Self { 152 | let artists = msg.get_artist() 153 | .iter() 154 | .filter(|artist| artist.has_gid()) 155 | .map(|artist| SpotifyId::from_raw(artist.get_gid())) 156 | .collect::>(); 157 | 158 | let tracks = msg.get_disc() 159 | .iter() 160 | .flat_map(|disc| disc.get_track()) 161 | .filter(|track| track.has_gid()) 162 | .map(|track| SpotifyId::from_raw(track.get_gid())) 163 | .collect::>(); 164 | 165 | let covers = msg.get_cover_group() 166 | .get_image() 167 | .iter() 168 | .filter(|image| image.has_file_id()) 169 | .map(|image| { 170 | let mut dst = [0u8; 20]; 171 | dst.clone_from_slice(image.get_file_id()); 172 | FileId(dst) 173 | }) 174 | .collect::>(); 175 | 176 | Album { 177 | id: SpotifyId::from_raw(msg.get_gid()), 178 | name: msg.get_name().to_owned(), 179 | artists: artists, 180 | tracks: tracks, 181 | covers: covers, 182 | } 183 | } 184 | } 185 | 186 | 187 | impl Metadata for Artist { 188 | type Message = protocol::metadata::Artist; 189 | 190 | fn base_url() -> &'static str { 191 | "hm://metadata/3/artist" 192 | } 193 | 194 | fn parse(msg: &Self::Message, session: &Session) -> Self { 195 | let country = session.country(); 196 | 197 | let top_tracks: Vec = match msg.get_top_track() 198 | .iter() 199 | .find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country)) { 200 | Some(tracks) => { 201 | tracks.get_track() 202 | .iter() 203 | .filter(|track| track.has_gid()) 204 | .map(|track| SpotifyId::from_raw(track.get_gid())) 205 | .collect::>() 206 | }, 207 | None => Vec::new() 208 | }; 209 | 210 | 211 | Artist { 212 | id: SpotifyId::from_raw(msg.get_gid()), 213 | name: msg.get_name().to_owned(), 214 | top_tracks: top_tracks 215 | } 216 | } 217 | } 218 | 219 | -------------------------------------------------------------------------------- /metadata/src/metadata.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plietar/librespot/8971d3aa6864469a79c2640bb2afaf41c3d3728d/metadata/src/metadata.rs -------------------------------------------------------------------------------- /protocol/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-protocol" 3 | version = "0.1.0" 4 | authors = ["Paul Liétar "] 5 | build = "build.rs" 6 | 7 | [dependencies] 8 | protobuf = "1.0.10" 9 | -------------------------------------------------------------------------------- /protocol/build.rs: -------------------------------------------------------------------------------- 1 | use std::io::prelude::*; 2 | use std::fs::File; 3 | 4 | mod files; 5 | 6 | fn main() { 7 | for &(path, expected_checksum) in files::FILES { 8 | let actual = cksum_file(path).unwrap(); 9 | if expected_checksum != actual { 10 | panic!("Checksum for {:?} does not match. Try running build.sh", path); 11 | } 12 | } 13 | } 14 | 15 | fn cksum_file>(path: T) -> std::io::Result { 16 | let mut file = File::open(path)?; 17 | let mut contents = Vec::new(); 18 | file.read_to_end(&mut contents)?; 19 | 20 | Ok(cksum(&contents)) 21 | } 22 | 23 | fn cksum>(data: T) -> u32 { 24 | let data = data.as_ref(); 25 | 26 | let mut value = 0u32; 27 | for x in data { 28 | value = (value << 8) ^ CRC_LOOKUP_ARRAY[(*x as u32 ^ (value >> 24)) as usize]; 29 | } 30 | 31 | let mut n = data.len(); 32 | while n != 0 { 33 | value = (value << 8) ^ CRC_LOOKUP_ARRAY[((n & 0xFF) as u32 ^ (value >> 24)) as usize]; 34 | n >>= 8; 35 | } 36 | 37 | !value 38 | } 39 | 40 | static CRC_LOOKUP_ARRAY : &'static[u32] = &[ 41 | 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 42 | 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, 43 | 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, 44 | 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 45 | 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 46 | 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, 47 | 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 48 | 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, 49 | 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, 50 | 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 51 | 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, 52 | 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, 53 | 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 54 | 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, 55 | 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 56 | 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 57 | 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, 58 | 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, 59 | 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 60 | 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, 61 | 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, 62 | 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 63 | 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, 64 | 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, 65 | 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 66 | 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, 67 | 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, 68 | 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 69 | 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, 70 | 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, 71 | 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 72 | 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, 73 | 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, 74 | 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 75 | 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, 76 | 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, 77 | 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 78 | 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, 79 | 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, 80 | 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 81 | 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, 82 | 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, 83 | 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 84 | 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, 85 | 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, 86 | 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 87 | 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, 88 | 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, 89 | 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 90 | 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, 91 | 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, 92 | 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 93 | 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, 94 | 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, 95 | 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 96 | 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, 97 | 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, 98 | 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 99 | 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, 100 | 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, 101 | 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 102 | 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, 103 | 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, 104 | 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 105 | ]; 106 | -------------------------------------------------------------------------------- /protocol/build.sh: -------------------------------------------------------------------------------- 1 | set -eu 2 | 3 | SRC="authentication keyexchange mercury 4 | metadata pubsub spirc" 5 | 6 | cat > src/lib.rs < files.rs <> src/lib.rs 26 | echo " (\"$src\", $checksum)," >> files.rs 27 | done 28 | 29 | echo "];" >> files.rs 30 | -------------------------------------------------------------------------------- /protocol/files.rs: -------------------------------------------------------------------------------- 1 | // Autogenerated by build.sh 2 | 3 | pub const FILES : &'static [(&'static str, u32)] = &[ 4 | ("proto/authentication.proto", 2098196376), 5 | ("proto/keyexchange.proto", 451735664), 6 | ("proto/mercury.proto", 709993906), 7 | ("proto/metadata.proto", 2474472423), 8 | ("proto/pubsub.proto", 2686584829), 9 | ("proto/spirc.proto", 3618770573), 10 | ]; 11 | -------------------------------------------------------------------------------- /protocol/proto/ad-hermes-proxy.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Rule { 4 | optional string type = 0x1; 5 | optional uint32 times = 0x2; 6 | optional uint64 interval = 0x3; 7 | } 8 | 9 | message AdRequest { 10 | optional string client_language = 0x1; 11 | optional string product = 0x2; 12 | optional uint32 version = 0x3; 13 | optional string type = 0x4; 14 | repeated string avoidAds = 0x5; 15 | } 16 | 17 | message AdQueueResponse { 18 | repeated AdQueueEntry adQueueEntry = 0x1; 19 | } 20 | 21 | message AdFile { 22 | optional string id = 0x1; 23 | optional string format = 0x2; 24 | } 25 | 26 | message AdQueueEntry { 27 | optional uint64 start_time = 0x1; 28 | optional uint64 end_time = 0x2; 29 | optional double priority = 0x3; 30 | optional string token = 0x4; 31 | optional uint32 ad_version = 0x5; 32 | optional string id = 0x6; 33 | optional string type = 0x7; 34 | optional string campaign = 0x8; 35 | optional string advertiser = 0x9; 36 | optional string url = 0xa; 37 | optional uint64 duration = 0xb; 38 | optional uint64 expiry = 0xc; 39 | optional string tracking_url = 0xd; 40 | optional string banner_type = 0xe; 41 | optional string html = 0xf; 42 | optional string image = 0x10; 43 | optional string background_image = 0x11; 44 | optional string background_url = 0x12; 45 | optional string background_color = 0x13; 46 | optional string title = 0x14; 47 | optional string caption = 0x15; 48 | repeated AdFile file = 0x16; 49 | repeated Rule rule = 0x17; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /protocol/proto/appstore.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message AppInfo { 4 | optional string identifier = 0x1; 5 | optional int32 version_int = 0x2; 6 | } 7 | 8 | message AppInfoList { 9 | repeated AppInfo items = 0x1; 10 | } 11 | 12 | message SemanticVersion { 13 | optional int32 major = 0x1; 14 | optional int32 minor = 0x2; 15 | optional int32 patch = 0x3; 16 | } 17 | 18 | message RequestHeader { 19 | optional string market = 0x1; 20 | optional Platform platform = 0x2; 21 | enum Platform { 22 | WIN32_X86 = 0x0; 23 | OSX_X86 = 0x1; 24 | LINUX_X86 = 0x2; 25 | IPHONE_ARM = 0x3; 26 | SYMBIANS60_ARM = 0x4; 27 | OSX_POWERPC = 0x5; 28 | ANDROID_ARM = 0x6; 29 | WINCE_ARM = 0x7; 30 | LINUX_X86_64 = 0x8; 31 | OSX_X86_64 = 0x9; 32 | PALM_ARM = 0xa; 33 | LINUX_SH = 0xb; 34 | FREEBSD_X86 = 0xc; 35 | FREEBSD_X86_64 = 0xd; 36 | BLACKBERRY_ARM = 0xe; 37 | SONOS_UNKNOWN = 0xf; 38 | LINUX_MIPS = 0x10; 39 | LINUX_ARM = 0x11; 40 | LOGITECH_ARM = 0x12; 41 | LINUX_BLACKFIN = 0x13; 42 | ONKYO_ARM = 0x15; 43 | QNXNTO_ARM = 0x16; 44 | BADPLATFORM = 0xff; 45 | } 46 | optional AppInfoList app_infos = 0x6; 47 | optional string bridge_identifier = 0x7; 48 | optional SemanticVersion bridge_version = 0x8; 49 | optional DeviceClass device_class = 0x9; 50 | enum DeviceClass { 51 | DESKTOP = 0x1; 52 | TABLET = 0x2; 53 | MOBILE = 0x3; 54 | WEB = 0x4; 55 | TV = 0x5; 56 | } 57 | } 58 | 59 | message AppItem { 60 | optional string identifier = 0x1; 61 | optional Requirement requirement = 0x2; 62 | enum Requirement { 63 | REQUIRED_INSTALL = 0x1; 64 | LAZYLOAD = 0x2; 65 | OPTIONAL_INSTALL = 0x3; 66 | } 67 | optional string manifest = 0x4; 68 | optional string checksum = 0x5; 69 | optional string bundle_uri = 0x6; 70 | optional string small_icon_uri = 0x7; 71 | optional string large_icon_uri = 0x8; 72 | optional string medium_icon_uri = 0x9; 73 | optional Type bundle_type = 0xa; 74 | enum Type { 75 | APPLICATION = 0x0; 76 | FRAMEWORK = 0x1; 77 | BRIDGE = 0x2; 78 | } 79 | optional SemanticVersion version = 0xb; 80 | optional uint32 ttl_in_seconds = 0xc; 81 | optional IdentifierList categories = 0xd; 82 | } 83 | 84 | message AppList { 85 | repeated AppItem items = 0x1; 86 | } 87 | 88 | message IdentifierList { 89 | repeated string identifiers = 0x1; 90 | } 91 | 92 | message BannerConfig { 93 | optional string json = 0x1; 94 | } 95 | 96 | -------------------------------------------------------------------------------- /protocol/proto/authentication.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ClientResponseEncrypted { 4 | required LoginCredentials login_credentials = 0xa; 5 | optional AccountCreation account_creation = 0x14; 6 | optional FingerprintResponseUnion fingerprint_response = 0x1e; 7 | optional PeerTicketUnion peer_ticket = 0x28; 8 | required SystemInfo system_info = 0x32; 9 | optional string platform_model = 0x3c; 10 | optional string version_string = 0x46; 11 | optional LibspotifyAppKey appkey = 0x50; 12 | optional ClientInfo client_info = 0x5a; 13 | } 14 | 15 | message LoginCredentials { 16 | optional string username = 0xa; 17 | required AuthenticationType typ = 0x14; 18 | optional bytes auth_data = 0x1e; 19 | } 20 | 21 | enum AuthenticationType { 22 | AUTHENTICATION_USER_PASS = 0x0; 23 | AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS = 0x1; 24 | AUTHENTICATION_STORED_FACEBOOK_CREDENTIALS = 0x2; 25 | AUTHENTICATION_SPOTIFY_TOKEN = 0x3; 26 | AUTHENTICATION_FACEBOOK_TOKEN = 0x4; 27 | } 28 | 29 | enum AccountCreation { 30 | ACCOUNT_CREATION_ALWAYS_PROMPT = 0x1; 31 | ACCOUNT_CREATION_ALWAYS_CREATE = 0x3; 32 | } 33 | 34 | message FingerprintResponseUnion { 35 | optional FingerprintGrainResponse grain = 0xa; 36 | optional FingerprintHmacRipemdResponse hmac_ripemd = 0x14; 37 | } 38 | 39 | message FingerprintGrainResponse { 40 | required bytes encrypted_key = 0xa; 41 | } 42 | 43 | message FingerprintHmacRipemdResponse { 44 | required bytes hmac = 0xa; 45 | } 46 | 47 | message PeerTicketUnion { 48 | optional PeerTicketPublicKey public_key = 0xa; 49 | optional PeerTicketOld old_ticket = 0x14; 50 | } 51 | 52 | message PeerTicketPublicKey { 53 | required bytes public_key = 0xa; 54 | } 55 | 56 | message PeerTicketOld { 57 | required bytes peer_ticket = 0xa; 58 | required bytes peer_ticket_signature = 0x14; 59 | } 60 | 61 | message SystemInfo { 62 | required CpuFamily cpu_family = 0xa; 63 | optional uint32 cpu_subtype = 0x14; 64 | optional uint32 cpu_ext = 0x1e; 65 | optional Brand brand = 0x28; 66 | optional uint32 brand_flags = 0x32; 67 | required Os os = 0x3c; 68 | optional uint32 os_version = 0x46; 69 | optional uint32 os_ext = 0x50; 70 | optional string system_information_string = 0x5a; 71 | optional string device_id = 0x64; 72 | } 73 | 74 | enum CpuFamily { 75 | CPU_UNKNOWN = 0x0; 76 | CPU_X86 = 0x1; 77 | CPU_X86_64 = 0x2; 78 | CPU_PPC = 0x3; 79 | CPU_PPC_64 = 0x4; 80 | CPU_ARM = 0x5; 81 | CPU_IA64 = 0x6; 82 | CPU_SH = 0x7; 83 | CPU_MIPS = 0x8; 84 | CPU_BLACKFIN = 0x9; 85 | } 86 | 87 | enum Brand { 88 | BRAND_UNBRANDED = 0x0; 89 | BRAND_INQ = 0x1; 90 | BRAND_HTC = 0x2; 91 | BRAND_NOKIA = 0x3; 92 | } 93 | 94 | enum Os { 95 | OS_UNKNOWN = 0x0; 96 | OS_WINDOWS = 0x1; 97 | OS_OSX = 0x2; 98 | OS_IPHONE = 0x3; 99 | OS_S60 = 0x4; 100 | OS_LINUX = 0x5; 101 | OS_WINDOWS_CE = 0x6; 102 | OS_ANDROID = 0x7; 103 | OS_PALM = 0x8; 104 | OS_FREEBSD = 0x9; 105 | OS_BLACKBERRY = 0xa; 106 | OS_SONOS = 0xb; 107 | OS_LOGITECH = 0xc; 108 | OS_WP7 = 0xd; 109 | OS_ONKYO = 0xe; 110 | OS_PHILIPS = 0xf; 111 | OS_WD = 0x10; 112 | OS_VOLVO = 0x11; 113 | OS_TIVO = 0x12; 114 | OS_AWOX = 0x13; 115 | OS_MEEGO = 0x14; 116 | OS_QNXNTO = 0x15; 117 | OS_BCO = 0x16; 118 | } 119 | 120 | message LibspotifyAppKey { 121 | required uint32 version = 0x1; 122 | required bytes devkey = 0x2; 123 | required bytes signature = 0x3; 124 | required string useragent = 0x4; 125 | required bytes callback_hash = 0x5; 126 | } 127 | 128 | message ClientInfo { 129 | optional bool limited = 0x1; 130 | optional ClientInfoFacebook fb = 0x2; 131 | optional string language = 0x3; 132 | } 133 | 134 | message ClientInfoFacebook { 135 | optional string machine_id = 0x1; 136 | } 137 | 138 | message APWelcome { 139 | required string canonical_username = 0xa; 140 | required AccountType account_type_logged_in = 0x14; 141 | required AccountType credentials_type_logged_in = 0x19; 142 | required AuthenticationType reusable_auth_credentials_type = 0x1e; 143 | required bytes reusable_auth_credentials = 0x28; 144 | optional bytes lfs_secret = 0x32; 145 | optional AccountInfo account_info = 0x3c; 146 | optional AccountInfoFacebook fb = 0x46; 147 | } 148 | 149 | enum AccountType { 150 | Spotify = 0x0; 151 | Facebook = 0x1; 152 | } 153 | 154 | message AccountInfo { 155 | optional AccountInfoSpotify spotify = 0x1; 156 | optional AccountInfoFacebook facebook = 0x2; 157 | } 158 | 159 | message AccountInfoSpotify { 160 | } 161 | 162 | message AccountInfoFacebook { 163 | optional string access_token = 0x1; 164 | optional string machine_id = 0x2; 165 | } 166 | -------------------------------------------------------------------------------- /protocol/proto/facebook-publish.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message EventReply { 4 | optional int32 queued = 0x1; 5 | optional RetryInfo retry = 0x2; 6 | } 7 | 8 | message RetryInfo { 9 | optional int32 retry_delay = 0x1; 10 | optional int32 max_retry = 0x2; 11 | } 12 | 13 | message Id { 14 | optional string uri = 0x1; 15 | optional int64 start_time = 0x2; 16 | } 17 | 18 | message Start { 19 | optional int32 length = 0x1; 20 | optional string context_uri = 0x2; 21 | optional int64 end_time = 0x3; 22 | } 23 | 24 | message Seek { 25 | optional int64 end_time = 0x1; 26 | } 27 | 28 | message Pause { 29 | optional int32 seconds_played = 0x1; 30 | optional int64 end_time = 0x2; 31 | } 32 | 33 | message Resume { 34 | optional int32 seconds_played = 0x1; 35 | optional int64 end_time = 0x2; 36 | } 37 | 38 | message End { 39 | optional int32 seconds_played = 0x1; 40 | optional int64 end_time = 0x2; 41 | } 42 | 43 | message Event { 44 | optional Id id = 0x1; 45 | optional Start start = 0x2; 46 | optional Seek seek = 0x3; 47 | optional Pause pause = 0x4; 48 | optional Resume resume = 0x5; 49 | optional End end = 0x6; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /protocol/proto/facebook.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Credential { 4 | optional string facebook_uid = 0x1; 5 | optional string access_token = 0x2; 6 | } 7 | 8 | message EnableRequest { 9 | optional Credential credential = 0x1; 10 | } 11 | 12 | message EnableReply { 13 | optional Credential credential = 0x1; 14 | } 15 | 16 | message DisableRequest { 17 | optional Credential credential = 0x1; 18 | } 19 | 20 | message RevokeRequest { 21 | optional Credential credential = 0x1; 22 | } 23 | 24 | message InspectCredentialRequest { 25 | optional Credential credential = 0x1; 26 | } 27 | 28 | message InspectCredentialReply { 29 | optional Credential alternative_credential = 0x1; 30 | optional bool app_user = 0x2; 31 | optional bool permanent_error = 0x3; 32 | optional bool transient_error = 0x4; 33 | } 34 | 35 | message UserState { 36 | optional Credential credential = 0x1; 37 | } 38 | 39 | message UpdateUserStateRequest { 40 | optional Credential credential = 0x1; 41 | } 42 | 43 | message OpenGraphError { 44 | repeated string permanent = 0x1; 45 | repeated string invalid_token = 0x2; 46 | repeated string retries = 0x3; 47 | } 48 | 49 | message OpenGraphScrobble { 50 | optional int32 create_delay = 0x1; 51 | } 52 | 53 | message OpenGraphConfig { 54 | optional OpenGraphError error = 0x1; 55 | optional OpenGraphScrobble scrobble = 0x2; 56 | } 57 | 58 | message AuthConfig { 59 | optional string url = 0x1; 60 | repeated string permissions = 0x2; 61 | repeated string blacklist = 0x3; 62 | repeated string whitelist = 0x4; 63 | repeated string cancel = 0x5; 64 | } 65 | 66 | message ConfigReply { 67 | optional string domain = 0x1; 68 | optional string app_id = 0x2; 69 | optional string app_namespace = 0x3; 70 | optional AuthConfig auth = 0x4; 71 | optional OpenGraphConfig og = 0x5; 72 | } 73 | 74 | message UserFields { 75 | optional bool app_user = 0x1; 76 | optional bool display_name = 0x2; 77 | optional bool first_name = 0x3; 78 | optional bool middle_name = 0x4; 79 | optional bool last_name = 0x5; 80 | optional bool picture_large = 0x6; 81 | optional bool picture_square = 0x7; 82 | optional bool gender = 0x8; 83 | optional bool email = 0x9; 84 | } 85 | 86 | message UserOptions { 87 | optional bool cache_is_king = 0x1; 88 | } 89 | 90 | message UserRequest { 91 | optional UserOptions options = 0x1; 92 | optional UserFields fields = 0x2; 93 | } 94 | 95 | message User { 96 | optional string spotify_username = 0x1; 97 | optional string facebook_uid = 0x2; 98 | optional bool app_user = 0x3; 99 | optional string display_name = 0x4; 100 | optional string first_name = 0x5; 101 | optional string middle_name = 0x6; 102 | optional string last_name = 0x7; 103 | optional string picture_large = 0x8; 104 | optional string picture_square = 0x9; 105 | optional string gender = 0xa; 106 | optional string email = 0xb; 107 | } 108 | 109 | message FriendsFields { 110 | optional bool app_user = 0x1; 111 | optional bool display_name = 0x2; 112 | optional bool picture_large = 0x6; 113 | } 114 | 115 | message FriendsOptions { 116 | optional int32 limit = 0x1; 117 | optional int32 offset = 0x2; 118 | optional bool cache_is_king = 0x3; 119 | optional bool app_friends = 0x4; 120 | optional bool non_app_friends = 0x5; 121 | } 122 | 123 | message FriendsRequest { 124 | optional FriendsOptions options = 0x1; 125 | optional FriendsFields fields = 0x2; 126 | } 127 | 128 | message FriendsReply { 129 | repeated User friends = 0x1; 130 | optional bool more = 0x2; 131 | } 132 | 133 | message ShareRequest { 134 | optional Credential credential = 0x1; 135 | optional string uri = 0x2; 136 | optional string message_text = 0x3; 137 | } 138 | 139 | message ShareReply { 140 | optional string post_id = 0x1; 141 | } 142 | 143 | message InboxRequest { 144 | optional Credential credential = 0x1; 145 | repeated string facebook_uids = 0x3; 146 | optional string message_text = 0x4; 147 | optional string message_link = 0x5; 148 | } 149 | 150 | message InboxReply { 151 | optional string message_id = 0x1; 152 | optional string thread_id = 0x2; 153 | } 154 | 155 | message PermissionsOptions { 156 | optional bool cache_is_king = 0x1; 157 | } 158 | 159 | message PermissionsRequest { 160 | optional Credential credential = 0x1; 161 | optional PermissionsOptions options = 0x2; 162 | } 163 | 164 | message PermissionsReply { 165 | repeated string permissions = 0x1; 166 | } 167 | 168 | message GrantPermissionsRequest { 169 | optional Credential credential = 0x1; 170 | repeated string permissions = 0x2; 171 | } 172 | 173 | message GrantPermissionsReply { 174 | repeated string granted = 0x1; 175 | repeated string failed = 0x2; 176 | } 177 | 178 | message TransferRequest { 179 | optional Credential credential = 0x1; 180 | optional string source_username = 0x2; 181 | optional string target_username = 0x3; 182 | } 183 | 184 | -------------------------------------------------------------------------------- /protocol/proto/keyexchange.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ClientHello { 4 | required BuildInfo build_info = 0xa; 5 | repeated Fingerprint fingerprints_supported = 0x14; 6 | repeated Cryptosuite cryptosuites_supported = 0x1e; 7 | repeated Powscheme powschemes_supported = 0x28; 8 | required LoginCryptoHelloUnion login_crypto_hello = 0x32; 9 | required bytes client_nonce = 0x3c; 10 | optional bytes padding = 0x46; 11 | optional FeatureSet feature_set = 0x50; 12 | } 13 | 14 | 15 | message BuildInfo { 16 | required Product product = 0xa; 17 | repeated ProductFlags product_flags = 0x14; 18 | required Platform platform = 0x1e; 19 | required uint64 version = 0x28; 20 | } 21 | 22 | enum Product { 23 | PRODUCT_CLIENT = 0x0; 24 | PRODUCT_LIBSPOTIFY= 0x1; 25 | PRODUCT_MOBILE = 0x2; 26 | PRODUCT_PARTNER = 0x3; 27 | PRODUCT_LIBSPOTIFY_EMBEDDED = 0x5; 28 | } 29 | 30 | enum ProductFlags { 31 | PRODUCT_FLAG_NONE = 0x0; 32 | PRODUCT_FLAG_DEV_BUILD = 0x1; 33 | } 34 | 35 | enum Platform { 36 | PLATFORM_WIN32_X86 = 0x0; 37 | PLATFORM_OSX_X86 = 0x1; 38 | PLATFORM_LINUX_X86 = 0x2; 39 | PLATFORM_IPHONE_ARM = 0x3; 40 | PLATFORM_S60_ARM = 0x4; 41 | PLATFORM_OSX_PPC = 0x5; 42 | PLATFORM_ANDROID_ARM = 0x6; 43 | PLATFORM_WINDOWS_CE_ARM = 0x7; 44 | PLATFORM_LINUX_X86_64 = 0x8; 45 | PLATFORM_OSX_X86_64 = 0x9; 46 | PLATFORM_PALM_ARM = 0xa; 47 | PLATFORM_LINUX_SH = 0xb; 48 | PLATFORM_FREEBSD_X86 = 0xc; 49 | PLATFORM_FREEBSD_X86_64 = 0xd; 50 | PLATFORM_BLACKBERRY_ARM = 0xe; 51 | PLATFORM_SONOS = 0xf; 52 | PLATFORM_LINUX_MIPS = 0x10; 53 | PLATFORM_LINUX_ARM = 0x11; 54 | PLATFORM_LOGITECH_ARM = 0x12; 55 | PLATFORM_LINUX_BLACKFIN = 0x13; 56 | PLATFORM_WP7_ARM = 0x14; 57 | PLATFORM_ONKYO_ARM = 0x15; 58 | PLATFORM_QNXNTO_ARM = 0x16; 59 | PLATFORM_BCO_ARM = 0x17; 60 | } 61 | 62 | enum Fingerprint { 63 | FINGERPRINT_GRAIN = 0x0; 64 | FINGERPRINT_HMAC_RIPEMD = 0x1; 65 | } 66 | 67 | enum Cryptosuite { 68 | CRYPTO_SUITE_SHANNON = 0x0; 69 | CRYPTO_SUITE_RC4_SHA1_HMAC = 0x1; 70 | } 71 | 72 | enum Powscheme { 73 | POW_HASH_CASH = 0x0; 74 | } 75 | 76 | 77 | message LoginCryptoHelloUnion { 78 | optional LoginCryptoDiffieHellmanHello diffie_hellman = 0xa; 79 | } 80 | 81 | 82 | message LoginCryptoDiffieHellmanHello { 83 | required bytes gc = 0xa; 84 | required uint32 server_keys_known = 0x14; 85 | } 86 | 87 | 88 | message FeatureSet { 89 | optional bool autoupdate2 = 0x1; 90 | optional bool current_location = 0x2; 91 | } 92 | 93 | 94 | message APResponseMessage { 95 | optional APChallenge challenge = 0xa; 96 | optional UpgradeRequiredMessage upgrade = 0x14; 97 | optional APLoginFailed login_failed = 0x1e; 98 | } 99 | 100 | message APChallenge { 101 | required LoginCryptoChallengeUnion login_crypto_challenge = 0xa; 102 | required FingerprintChallengeUnion fingerprint_challenge = 0x14; 103 | required PoWChallengeUnion pow_challenge = 0x1e; 104 | required CryptoChallengeUnion crypto_challenge = 0x28; 105 | required bytes server_nonce = 0x32; 106 | optional bytes padding = 0x3c; 107 | } 108 | 109 | message LoginCryptoChallengeUnion { 110 | optional LoginCryptoDiffieHellmanChallenge diffie_hellman = 0xa; 111 | } 112 | 113 | message LoginCryptoDiffieHellmanChallenge { 114 | required bytes gs = 0xa; 115 | required int32 server_signature_key = 0x14; 116 | required bytes gs_signature = 0x1e; 117 | } 118 | 119 | message FingerprintChallengeUnion { 120 | optional FingerprintGrainChallenge grain = 0xa; 121 | optional FingerprintHmacRipemdChallenge hmac_ripemd = 0x14; 122 | } 123 | 124 | 125 | message FingerprintGrainChallenge { 126 | required bytes kek = 0xa; 127 | } 128 | 129 | 130 | message FingerprintHmacRipemdChallenge { 131 | required bytes challenge = 0xa; 132 | } 133 | 134 | 135 | message PoWChallengeUnion { 136 | optional PoWHashCashChallenge hash_cash = 0xa; 137 | } 138 | 139 | message PoWHashCashChallenge { 140 | optional bytes prefix = 0xa; 141 | optional int32 length = 0x14; 142 | optional int32 target = 0x1e; 143 | } 144 | 145 | 146 | message CryptoChallengeUnion { 147 | optional CryptoShannonChallenge shannon = 0xa; 148 | optional CryptoRc4Sha1HmacChallenge rc4_sha1_hmac = 0x14; 149 | } 150 | 151 | 152 | message CryptoShannonChallenge { 153 | } 154 | 155 | 156 | message CryptoRc4Sha1HmacChallenge { 157 | } 158 | 159 | 160 | message UpgradeRequiredMessage { 161 | required bytes upgrade_signed_part = 0xa; 162 | required bytes signature = 0x14; 163 | optional string http_suffix = 0x1e; 164 | } 165 | 166 | message APLoginFailed { 167 | required ErrorCode error_code = 0xa; 168 | optional int32 retry_delay = 0x14; 169 | optional int32 expiry = 0x1e; 170 | optional string error_description = 0x28; 171 | } 172 | 173 | enum ErrorCode { 174 | ProtocolError = 0x0; 175 | TryAnotherAP = 0x2; 176 | BadConnectionId = 0x5; 177 | TravelRestriction = 0x9; 178 | PremiumAccountRequired = 0xb; 179 | BadCredentials = 0xc; 180 | CouldNotValidateCredentials = 0xd; 181 | AccountExists = 0xe; 182 | ExtraVerificationRequired = 0xf; 183 | InvalidAppKey = 0x10; 184 | ApplicationBanned = 0x11; 185 | } 186 | 187 | message ClientResponsePlaintext { 188 | required LoginCryptoResponseUnion login_crypto_response = 0xa; 189 | required PoWResponseUnion pow_response = 0x14; 190 | required CryptoResponseUnion crypto_response = 0x1e; 191 | } 192 | 193 | 194 | message LoginCryptoResponseUnion { 195 | optional LoginCryptoDiffieHellmanResponse diffie_hellman = 0xa; 196 | } 197 | 198 | 199 | message LoginCryptoDiffieHellmanResponse { 200 | required bytes hmac = 0xa; 201 | } 202 | 203 | 204 | message PoWResponseUnion { 205 | optional PoWHashCashResponse hash_cash = 0xa; 206 | } 207 | 208 | 209 | message PoWHashCashResponse { 210 | required bytes hash_suffix = 0xa; 211 | } 212 | 213 | 214 | message CryptoResponseUnion { 215 | optional CryptoShannonResponse shannon = 0xa; 216 | optional CryptoRc4Sha1HmacResponse rc4_sha1_hmac = 0x14; 217 | } 218 | 219 | 220 | message CryptoShannonResponse { 221 | optional int32 dummy = 0x1; 222 | } 223 | 224 | 225 | message CryptoRc4Sha1HmacResponse { 226 | optional int32 dummy = 0x1; 227 | } 228 | 229 | -------------------------------------------------------------------------------- /protocol/proto/mercury.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message MercuryMultiGetRequest { 4 | repeated MercuryRequest request = 0x1; 5 | } 6 | 7 | message MercuryMultiGetReply { 8 | repeated MercuryReply reply = 0x1; 9 | } 10 | 11 | message MercuryRequest { 12 | optional string uri = 0x1; 13 | optional string content_type = 0x2; 14 | optional bytes body = 0x3; 15 | optional bytes etag = 0x4; 16 | } 17 | 18 | message MercuryReply { 19 | optional sint32 status_code = 0x1; 20 | optional string status_message = 0x2; 21 | optional CachePolicy cache_policy = 0x3; 22 | enum CachePolicy { 23 | CACHE_NO = 0x1; 24 | CACHE_PRIVATE = 0x2; 25 | CACHE_PUBLIC = 0x3; 26 | } 27 | optional sint32 ttl = 0x4; 28 | optional bytes etag = 0x5; 29 | optional string content_type = 0x6; 30 | optional bytes body = 0x7; 31 | } 32 | 33 | 34 | message Header { 35 | optional string uri = 0x01; 36 | optional string content_type = 0x02; 37 | optional string method = 0x03; 38 | optional sint32 status_code = 0x04; 39 | repeated UserField user_fields = 0x06; 40 | } 41 | 42 | message UserField { 43 | optional string key = 0x01; 44 | optional bytes value = 0x02; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /protocol/proto/mergedprofile.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message MergedProfileRequest { 4 | } 5 | 6 | message MergedProfileReply { 7 | optional string username = 0x1; 8 | optional string artistid = 0x2; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /protocol/proto/metadata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message TopTracks { 4 | optional string country = 0x1; 5 | repeated Track track = 0x2; 6 | } 7 | 8 | message ActivityPeriod { 9 | optional sint32 start_year = 0x1; 10 | optional sint32 end_year = 0x2; 11 | optional sint32 decade = 0x3; 12 | } 13 | 14 | message Artist { 15 | optional bytes gid = 0x1; 16 | optional string name = 0x2; 17 | optional sint32 popularity = 0x3; 18 | repeated TopTracks top_track = 0x4; 19 | repeated AlbumGroup album_group = 0x5; 20 | repeated AlbumGroup single_group = 0x6; 21 | repeated AlbumGroup compilation_group = 0x7; 22 | repeated AlbumGroup appears_on_group = 0x8; 23 | repeated string genre = 0x9; 24 | repeated ExternalId external_id = 0xa; 25 | repeated Image portrait = 0xb; 26 | repeated Biography biography = 0xc; 27 | repeated ActivityPeriod activity_period = 0xd; 28 | repeated Restriction restriction = 0xe; 29 | repeated Artist related = 0xf; 30 | optional bool is_portrait_album_cover = 0x10; 31 | optional ImageGroup portrait_group = 0x11; 32 | } 33 | 34 | message AlbumGroup { 35 | repeated Album album = 0x1; 36 | } 37 | 38 | message Date { 39 | optional sint32 year = 0x1; 40 | optional sint32 month = 0x2; 41 | optional sint32 day = 0x3; 42 | } 43 | 44 | message Album { 45 | optional bytes gid = 0x1; 46 | optional string name = 0x2; 47 | repeated Artist artist = 0x3; 48 | optional Type typ = 0x4; 49 | enum Type { 50 | ALBUM = 0x1; 51 | SINGLE = 0x2; 52 | COMPILATION = 0x3; 53 | EP = 0x4; 54 | } 55 | optional string label = 0x5; 56 | optional Date date = 0x6; 57 | optional sint32 popularity = 0x7; 58 | repeated string genre = 0x8; 59 | repeated Image cover = 0x9; 60 | repeated ExternalId external_id = 0xa; 61 | repeated Disc disc = 0xb; 62 | repeated string review = 0xc; 63 | repeated Copyright copyright = 0xd; 64 | repeated Restriction restriction = 0xe; 65 | repeated Album related = 0xf; 66 | repeated SalePeriod sale_period = 0x10; 67 | optional ImageGroup cover_group = 0x11; 68 | } 69 | 70 | message Track { 71 | optional bytes gid = 0x1; 72 | optional string name = 0x2; 73 | optional Album album = 0x3; 74 | repeated Artist artist = 0x4; 75 | optional sint32 number = 0x5; 76 | optional sint32 disc_number = 0x6; 77 | optional sint32 duration = 0x7; 78 | optional sint32 popularity = 0x8; 79 | optional bool explicit = 0x9; 80 | repeated ExternalId external_id = 0xa; 81 | repeated Restriction restriction = 0xb; 82 | repeated AudioFile file = 0xc; 83 | repeated Track alternative = 0xd; 84 | repeated SalePeriod sale_period = 0xe; 85 | repeated AudioFile preview = 0xf; 86 | } 87 | 88 | message Image { 89 | optional bytes file_id = 0x1; 90 | optional Size size = 0x2; 91 | enum Size { 92 | DEFAULT = 0x0; 93 | SMALL = 0x1; 94 | LARGE = 0x2; 95 | XLARGE = 0x3; 96 | } 97 | optional sint32 width = 0x3; 98 | optional sint32 height = 0x4; 99 | } 100 | 101 | message ImageGroup { 102 | repeated Image image = 0x1; 103 | } 104 | 105 | message Biography { 106 | optional string text = 0x1; 107 | repeated Image portrait = 0x2; 108 | repeated ImageGroup portrait_group = 0x3; 109 | } 110 | 111 | message Disc { 112 | optional sint32 number = 0x1; 113 | optional string name = 0x2; 114 | repeated Track track = 0x3; 115 | } 116 | 117 | message Copyright { 118 | optional Type typ = 0x1; 119 | enum Type { 120 | P = 0x0; 121 | C = 0x1; 122 | } 123 | optional string text = 0x2; 124 | } 125 | 126 | message Restriction { 127 | optional string countries_allowed = 0x2; 128 | optional string countries_forbidden = 0x3; 129 | optional Type typ = 0x4; 130 | enum Type { 131 | STREAMING = 0x0; 132 | } 133 | repeated string catalogue_str = 0x5; 134 | } 135 | 136 | message SalePeriod { 137 | repeated Restriction restriction = 0x1; 138 | optional Date start = 0x2; 139 | optional Date end = 0x3; 140 | } 141 | 142 | message ExternalId { 143 | optional string typ = 0x1; 144 | optional string id = 0x2; 145 | } 146 | 147 | message AudioFile { 148 | optional bytes file_id = 0x1; 149 | optional Format format = 0x2; 150 | enum Format { 151 | OGG_VORBIS_96 = 0x0; 152 | OGG_VORBIS_160 = 0x1; 153 | OGG_VORBIS_320 = 0x2; 154 | MP3_256 = 0x3; 155 | MP3_320 = 0x4; 156 | MP3_160 = 0x5; 157 | MP3_96 = 0x6; 158 | MP3_160_ENC = 0x7; 159 | OTHER2 = 0x8; 160 | OTHER3 = 0x9; 161 | AAC_160 = 0xa; 162 | AAC_320 = 0xb; 163 | OTHER4 = 0xc; 164 | OTHER5 = 0xd; 165 | } 166 | } 167 | 168 | -------------------------------------------------------------------------------- /protocol/proto/playlist4changes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "playlist4ops.proto"; 4 | import "playlist4meta.proto"; 5 | import "playlist4content.proto"; 6 | import "playlist4issues.proto"; 7 | 8 | message ChangeInfo { 9 | optional string user = 0x1; 10 | optional int32 timestamp = 0x2; 11 | optional bool admin = 0x3; 12 | optional bool undo = 0x4; 13 | optional bool redo = 0x5; 14 | optional bool merge = 0x6; 15 | optional bool compressed = 0x7; 16 | optional bool migration = 0x8; 17 | } 18 | 19 | message Delta { 20 | optional bytes base_version = 0x1; 21 | repeated Op ops = 0x2; 22 | optional ChangeInfo info = 0x4; 23 | } 24 | 25 | message Merge { 26 | optional bytes base_version = 0x1; 27 | optional bytes merge_version = 0x2; 28 | optional ChangeInfo info = 0x4; 29 | } 30 | 31 | message ChangeSet { 32 | optional Kind kind = 0x1; 33 | enum Kind { 34 | KIND_UNKNOWN = 0x0; 35 | DELTA = 0x2; 36 | MERGE = 0x3; 37 | } 38 | optional Delta delta = 0x2; 39 | optional Merge merge = 0x3; 40 | } 41 | 42 | message RevisionTaggedChangeSet { 43 | optional bytes revision = 0x1; 44 | optional ChangeSet change_set = 0x2; 45 | } 46 | 47 | message Diff { 48 | optional bytes from_revision = 0x1; 49 | repeated Op ops = 0x2; 50 | optional bytes to_revision = 0x3; 51 | } 52 | 53 | message ListDump { 54 | optional bytes latestRevision = 0x1; 55 | optional int32 length = 0x2; 56 | optional ListAttributes attributes = 0x3; 57 | optional ListChecksum checksum = 0x4; 58 | optional ListItems contents = 0x5; 59 | repeated Delta pendingDeltas = 0x7; 60 | } 61 | 62 | message ListChanges { 63 | optional bytes baseRevision = 0x1; 64 | repeated Delta deltas = 0x2; 65 | optional bool wantResultingRevisions = 0x3; 66 | optional bool wantSyncResult = 0x4; 67 | optional ListDump dump = 0x5; 68 | repeated int32 nonces = 0x6; 69 | } 70 | 71 | message SelectedListContent { 72 | optional bytes revision = 0x1; 73 | optional int32 length = 0x2; 74 | optional ListAttributes attributes = 0x3; 75 | optional ListChecksum checksum = 0x4; 76 | optional ListItems contents = 0x5; 77 | optional Diff diff = 0x6; 78 | optional Diff syncResult = 0x7; 79 | repeated bytes resultingRevisions = 0x8; 80 | optional bool multipleHeads = 0x9; 81 | optional bool upToDate = 0xa; 82 | repeated ClientResolveAction resolveAction = 0xc; 83 | repeated ClientIssue issues = 0xd; 84 | repeated int32 nonces = 0xe; 85 | } 86 | 87 | -------------------------------------------------------------------------------- /protocol/proto/playlist4content.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "playlist4meta.proto"; 4 | import "playlist4issues.proto"; 5 | 6 | message Item { 7 | optional string uri = 0x1; 8 | optional ItemAttributes attributes = 0x2; 9 | } 10 | 11 | message ListItems { 12 | optional int32 pos = 0x1; 13 | optional bool truncated = 0x2; 14 | repeated Item items = 0x3; 15 | } 16 | 17 | message ContentRange { 18 | optional int32 pos = 0x1; 19 | optional int32 length = 0x2; 20 | } 21 | 22 | message ListContentSelection { 23 | optional bool wantRevision = 0x1; 24 | optional bool wantLength = 0x2; 25 | optional bool wantAttributes = 0x3; 26 | optional bool wantChecksum = 0x4; 27 | optional bool wantContent = 0x5; 28 | optional ContentRange contentRange = 0x6; 29 | optional bool wantDiff = 0x7; 30 | optional bytes baseRevision = 0x8; 31 | optional bytes hintRevision = 0x9; 32 | optional bool wantNothingIfUpToDate = 0xa; 33 | optional bool wantResolveAction = 0xc; 34 | repeated ClientIssue issues = 0xd; 35 | repeated ClientResolveAction resolveAction = 0xe; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /protocol/proto/playlist4issues.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ClientIssue { 4 | optional Level level = 0x1; 5 | enum Level { 6 | LEVEL_UNKNOWN = 0x0; 7 | LEVEL_DEBUG = 0x1; 8 | LEVEL_INFO = 0x2; 9 | LEVEL_NOTICE = 0x3; 10 | LEVEL_WARNING = 0x4; 11 | LEVEL_ERROR = 0x5; 12 | } 13 | optional Code code = 0x2; 14 | enum Code { 15 | CODE_UNKNOWN = 0x0; 16 | CODE_INDEX_OUT_OF_BOUNDS = 0x1; 17 | CODE_VERSION_MISMATCH = 0x2; 18 | CODE_CACHED_CHANGE = 0x3; 19 | CODE_OFFLINE_CHANGE = 0x4; 20 | CODE_CONCURRENT_CHANGE = 0x5; 21 | } 22 | optional int32 repeatCount = 0x3; 23 | } 24 | 25 | message ClientResolveAction { 26 | optional Code code = 0x1; 27 | enum Code { 28 | CODE_UNKNOWN = 0x0; 29 | CODE_NO_ACTION = 0x1; 30 | CODE_RETRY = 0x2; 31 | CODE_RELOAD = 0x3; 32 | CODE_DISCARD_LOCAL_CHANGES = 0x4; 33 | CODE_SEND_DUMP = 0x5; 34 | CODE_DISPLAY_ERROR_MESSAGE = 0x6; 35 | } 36 | optional Initiator initiator = 0x2; 37 | enum Initiator { 38 | INITIATOR_UNKNOWN = 0x0; 39 | INITIATOR_SERVER = 0x1; 40 | INITIATOR_CLIENT = 0x2; 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /protocol/proto/playlist4meta.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ListChecksum { 4 | optional int32 version = 0x1; 5 | optional bytes sha1 = 0x4; 6 | } 7 | 8 | message DownloadFormat { 9 | optional Codec codec = 0x1; 10 | enum Codec { 11 | CODEC_UNKNOWN = 0x0; 12 | OGG_VORBIS = 0x1; 13 | FLAC = 0x2; 14 | MPEG_1_LAYER_3 = 0x3; 15 | } 16 | } 17 | 18 | message ListAttributes { 19 | optional string name = 0x1; 20 | optional string description = 0x2; 21 | optional bytes picture = 0x3; 22 | optional bool collaborative = 0x4; 23 | optional string pl3_version = 0x5; 24 | optional bool deleted_by_owner = 0x6; 25 | optional bool restricted_collaborative = 0x7; 26 | optional int64 deprecated_client_id = 0x8; 27 | optional bool public_starred = 0x9; 28 | optional string client_id = 0xa; 29 | } 30 | 31 | message ItemAttributes { 32 | optional string added_by = 0x1; 33 | optional int64 timestamp = 0x2; 34 | optional string message = 0x3; 35 | optional bool seen = 0x4; 36 | optional int64 download_count = 0x5; 37 | optional DownloadFormat download_format = 0x6; 38 | optional string sevendigital_id = 0x7; 39 | optional int64 sevendigital_left = 0x8; 40 | optional int64 seen_at = 0x9; 41 | optional bool public = 0xa; 42 | } 43 | 44 | message StringAttribute { 45 | optional string key = 0x1; 46 | optional string value = 0x2; 47 | } 48 | 49 | message StringAttributes { 50 | repeated StringAttribute attribute = 0x1; 51 | } 52 | 53 | -------------------------------------------------------------------------------- /protocol/proto/playlist4ops.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "playlist4meta.proto"; 4 | import "playlist4content.proto"; 5 | 6 | message Add { 7 | optional int32 fromIndex = 0x1; 8 | repeated Item items = 0x2; 9 | optional ListChecksum list_checksum = 0x3; 10 | optional bool addLast = 0x4; 11 | optional bool addFirst = 0x5; 12 | } 13 | 14 | message Rem { 15 | optional int32 fromIndex = 0x1; 16 | optional int32 length = 0x2; 17 | repeated Item items = 0x3; 18 | optional ListChecksum list_checksum = 0x4; 19 | optional ListChecksum items_checksum = 0x5; 20 | optional ListChecksum uris_checksum = 0x6; 21 | optional bool itemsAsKey = 0x7; 22 | } 23 | 24 | message Mov { 25 | optional int32 fromIndex = 0x1; 26 | optional int32 length = 0x2; 27 | optional int32 toIndex = 0x3; 28 | optional ListChecksum list_checksum = 0x4; 29 | optional ListChecksum items_checksum = 0x5; 30 | optional ListChecksum uris_checksum = 0x6; 31 | } 32 | 33 | message ItemAttributesPartialState { 34 | optional ItemAttributes values = 0x1; 35 | repeated ItemAttributeKind no_value = 0x2; 36 | 37 | enum ItemAttributeKind { 38 | ITEM_UNKNOWN = 0x0; 39 | ITEM_ADDED_BY = 0x1; 40 | ITEM_TIMESTAMP = 0x2; 41 | ITEM_MESSAGE = 0x3; 42 | ITEM_SEEN = 0x4; 43 | ITEM_DOWNLOAD_COUNT = 0x5; 44 | ITEM_DOWNLOAD_FORMAT = 0x6; 45 | ITEM_SEVENDIGITAL_ID = 0x7; 46 | ITEM_SEVENDIGITAL_LEFT = 0x8; 47 | ITEM_SEEN_AT = 0x9; 48 | ITEM_PUBLIC = 0xa; 49 | } 50 | } 51 | 52 | message ListAttributesPartialState { 53 | optional ListAttributes values = 0x1; 54 | repeated ListAttributeKind no_value = 0x2; 55 | 56 | enum ListAttributeKind { 57 | LIST_UNKNOWN = 0x0; 58 | LIST_NAME = 0x1; 59 | LIST_DESCRIPTION = 0x2; 60 | LIST_PICTURE = 0x3; 61 | LIST_COLLABORATIVE = 0x4; 62 | LIST_PL3_VERSION = 0x5; 63 | LIST_DELETED_BY_OWNER = 0x6; 64 | LIST_RESTRICTED_COLLABORATIVE = 0x7; 65 | } 66 | } 67 | 68 | message UpdateItemAttributes { 69 | optional int32 index = 0x1; 70 | optional ItemAttributesPartialState new_attributes = 0x2; 71 | optional ItemAttributesPartialState old_attributes = 0x3; 72 | optional ListChecksum list_checksum = 0x4; 73 | optional ListChecksum old_attributes_checksum = 0x5; 74 | } 75 | 76 | message UpdateListAttributes { 77 | optional ListAttributesPartialState new_attributes = 0x1; 78 | optional ListAttributesPartialState old_attributes = 0x2; 79 | optional ListChecksum list_checksum = 0x3; 80 | optional ListChecksum old_attributes_checksum = 0x4; 81 | } 82 | 83 | message Op { 84 | optional Kind kind = 0x1; 85 | enum Kind { 86 | KIND_UNKNOWN = 0x0; 87 | ADD = 0x2; 88 | REM = 0x3; 89 | MOV = 0x4; 90 | UPDATE_ITEM_ATTRIBUTES = 0x5; 91 | UPDATE_LIST_ATTRIBUTES = 0x6; 92 | } 93 | optional Add add = 0x2; 94 | optional Rem rem = 0x3; 95 | optional Mov mov = 0x4; 96 | optional UpdateItemAttributes update_item_attributes = 0x5; 97 | optional UpdateListAttributes update_list_attributes = 0x6; 98 | } 99 | 100 | message OpList { 101 | repeated Op ops = 0x1; 102 | } 103 | 104 | -------------------------------------------------------------------------------- /protocol/proto/popcount.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message PopcountRequest { 4 | } 5 | 6 | message PopcountResult { 7 | optional sint64 count = 0x1; 8 | optional bool truncated = 0x2; 9 | repeated string user = 0x3; 10 | repeated sint64 subscriptionTimestamps = 0x4; 11 | repeated sint64 insertionTimestamps = 0x5; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /protocol/proto/presence.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message PlaylistPublishedState { 4 | optional string uri = 0x1; 5 | optional int64 timestamp = 0x2; 6 | } 7 | 8 | message PlaylistTrackAddedState { 9 | optional string playlist_uri = 0x1; 10 | optional string track_uri = 0x2; 11 | optional int64 timestamp = 0x3; 12 | } 13 | 14 | message TrackFinishedPlayingState { 15 | optional string uri = 0x1; 16 | optional string context_uri = 0x2; 17 | optional int64 timestamp = 0x3; 18 | optional string referrer_uri = 0x4; 19 | } 20 | 21 | message FavoriteAppAddedState { 22 | optional string app_uri = 0x1; 23 | optional int64 timestamp = 0x2; 24 | } 25 | 26 | message TrackStartedPlayingState { 27 | optional string uri = 0x1; 28 | optional string context_uri = 0x2; 29 | optional int64 timestamp = 0x3; 30 | optional string referrer_uri = 0x4; 31 | } 32 | 33 | message UriSharedState { 34 | optional string uri = 0x1; 35 | optional string message = 0x2; 36 | optional int64 timestamp = 0x3; 37 | } 38 | 39 | message ArtistFollowedState { 40 | optional string uri = 0x1; 41 | optional string artist_name = 0x2; 42 | optional string artist_cover_uri = 0x3; 43 | optional int64 timestamp = 0x4; 44 | } 45 | 46 | message DeviceInformation { 47 | optional string os = 0x1; 48 | optional string type = 0x2; 49 | } 50 | 51 | message GenericPresenceState { 52 | optional int32 type = 0x1; 53 | optional int64 timestamp = 0x2; 54 | optional string item_uri = 0x3; 55 | optional string item_name = 0x4; 56 | optional string item_image = 0x5; 57 | optional string context_uri = 0x6; 58 | optional string context_name = 0x7; 59 | optional string context_image = 0x8; 60 | optional string referrer_uri = 0x9; 61 | optional string referrer_name = 0xa; 62 | optional string referrer_image = 0xb; 63 | optional string message = 0xc; 64 | optional DeviceInformation device_information = 0xd; 65 | } 66 | 67 | message State { 68 | optional int64 timestamp = 0x1; 69 | optional Type type = 0x2; 70 | enum Type { 71 | PLAYLIST_PUBLISHED = 0x1; 72 | PLAYLIST_TRACK_ADDED = 0x2; 73 | TRACK_FINISHED_PLAYING = 0x3; 74 | FAVORITE_APP_ADDED = 0x4; 75 | TRACK_STARTED_PLAYING = 0x5; 76 | URI_SHARED = 0x6; 77 | ARTIST_FOLLOWED = 0x7; 78 | GENERIC = 0xb; 79 | } 80 | optional string uri = 0x3; 81 | optional PlaylistPublishedState playlist_published = 0x4; 82 | optional PlaylistTrackAddedState playlist_track_added = 0x5; 83 | optional TrackFinishedPlayingState track_finished_playing = 0x6; 84 | optional FavoriteAppAddedState favorite_app_added = 0x7; 85 | optional TrackStartedPlayingState track_started_playing = 0x8; 86 | optional UriSharedState uri_shared = 0x9; 87 | optional ArtistFollowedState artist_followed = 0xa; 88 | optional GenericPresenceState generic = 0xb; 89 | } 90 | 91 | message StateList { 92 | repeated State states = 0x1; 93 | } 94 | 95 | -------------------------------------------------------------------------------- /protocol/proto/pubsub.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Subscription { 4 | optional string uri = 0x1; 5 | optional int32 expiry = 0x2; 6 | optional int32 status_code = 0x3; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /protocol/proto/radio.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message RadioRequest { 4 | repeated string uris = 0x1; 5 | optional int32 salt = 0x2; 6 | optional int32 length = 0x4; 7 | optional string stationId = 0x5; 8 | repeated string lastTracks = 0x6; 9 | } 10 | 11 | message MultiSeedRequest { 12 | repeated string uris = 0x1; 13 | } 14 | 15 | message Feedback { 16 | optional string uri = 0x1; 17 | optional string type = 0x2; 18 | optional double timestamp = 0x3; 19 | } 20 | 21 | message Tracks { 22 | repeated string gids = 0x1; 23 | optional string source = 0x2; 24 | optional string identity = 0x3; 25 | repeated string tokens = 0x4; 26 | repeated Feedback feedback = 0x5; 27 | } 28 | 29 | message Station { 30 | optional string id = 0x1; 31 | optional string title = 0x2; 32 | optional string titleUri = 0x3; 33 | optional string subtitle = 0x4; 34 | optional string subtitleUri = 0x5; 35 | optional string imageUri = 0x6; 36 | optional double lastListen = 0x7; 37 | repeated string seeds = 0x8; 38 | optional int32 thumbsUp = 0x9; 39 | optional int32 thumbsDown = 0xa; 40 | } 41 | 42 | message Rules { 43 | optional string js = 0x1; 44 | } 45 | 46 | message StationResponse { 47 | optional Station station = 0x1; 48 | repeated Feedback feedback = 0x2; 49 | } 50 | 51 | message StationList { 52 | repeated Station stations = 0x1; 53 | } 54 | 55 | message LikedPlaylist { 56 | optional string uri = 0x1; 57 | } 58 | 59 | -------------------------------------------------------------------------------- /protocol/proto/search.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message SearchRequest { 4 | optional string query = 0x1; 5 | optional Type type = 0x2; 6 | enum Type { 7 | TRACK = 0x0; 8 | ALBUM = 0x1; 9 | ARTIST = 0x2; 10 | PLAYLIST = 0x3; 11 | USER = 0x4; 12 | } 13 | optional int32 limit = 0x3; 14 | optional int32 offset = 0x4; 15 | optional bool did_you_mean = 0x5; 16 | optional string spotify_uri = 0x2; 17 | repeated bytes file_id = 0x3; 18 | optional string url = 0x4; 19 | optional string slask_id = 0x5; 20 | } 21 | 22 | message Playlist { 23 | optional string uri = 0x1; 24 | optional string name = 0x2; 25 | repeated Image image = 0x3; 26 | } 27 | 28 | message User { 29 | optional string username = 0x1; 30 | optional string full_name = 0x2; 31 | repeated Image image = 0x3; 32 | optional sint32 followers = 0x4; 33 | } 34 | 35 | message SearchReply { 36 | optional sint32 hits = 0x1; 37 | repeated Track track = 0x2; 38 | repeated Album album = 0x3; 39 | repeated Artist artist = 0x4; 40 | repeated Playlist playlist = 0x5; 41 | optional string did_you_mean = 0x6; 42 | repeated User user = 0x7; 43 | } 44 | 45 | -------------------------------------------------------------------------------- /protocol/proto/social.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message DecorationData { 4 | optional string username = 0x1; 5 | optional string full_name = 0x2; 6 | optional string image_url = 0x3; 7 | optional string large_image_url = 0x5; 8 | optional string first_name = 0x6; 9 | optional string last_name = 0x7; 10 | optional string facebook_uid = 0x8; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /protocol/proto/socialgraph.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message CountReply { 4 | repeated int32 counts = 0x1; 5 | } 6 | 7 | message UserListRequest { 8 | optional string last_result = 0x1; 9 | optional int32 count = 0x2; 10 | optional bool include_length = 0x3; 11 | } 12 | 13 | message UserListReply { 14 | repeated User users = 0x1; 15 | optional int32 length = 0x2; 16 | } 17 | 18 | message User { 19 | optional string username = 0x1; 20 | optional int32 subscriber_count = 0x2; 21 | optional int32 subscription_count = 0x3; 22 | } 23 | 24 | message ArtistListReply { 25 | repeated Artist artists = 0x1; 26 | } 27 | 28 | message Artist { 29 | optional string artistid = 0x1; 30 | optional int32 subscriber_count = 0x2; 31 | } 32 | 33 | message StringListRequest { 34 | repeated string args = 0x1; 35 | } 36 | 37 | message StringListReply { 38 | repeated string reply = 0x1; 39 | } 40 | 41 | message TopPlaylistsRequest { 42 | optional string username = 0x1; 43 | optional int32 count = 0x2; 44 | } 45 | 46 | message TopPlaylistsReply { 47 | repeated string uris = 0x1; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /protocol/proto/spirc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Frame { 4 | optional uint32 version = 0x1; 5 | optional string ident = 0x2; 6 | optional string protocol_version = 0x3; 7 | optional uint32 seq_nr = 0x4; 8 | optional MessageType typ = 0x5; 9 | optional DeviceState device_state = 0x7; 10 | optional Goodbye goodbye = 0xb; 11 | optional State state = 0xc; 12 | optional uint32 position = 0xd; 13 | optional uint32 volume = 0xe; 14 | optional int64 state_update_id = 0x11; 15 | repeated string recipient = 0x12; 16 | optional bytes context_player_state = 0x13; 17 | optional string new_name = 0x14; 18 | optional Metadata metadata = 0x19; 19 | } 20 | 21 | enum MessageType { 22 | kMessageTypeHello = 0x1; 23 | kMessageTypeGoodbye = 0x2; 24 | kMessageTypeProbe = 0x3; 25 | kMessageTypeNotify = 0xa; 26 | kMessageTypeLoad = 0x14; 27 | kMessageTypePlay = 0x15; 28 | kMessageTypePause = 0x16; 29 | kMessageTypePlayPause = 0x17; 30 | kMessageTypeSeek = 0x18; 31 | kMessageTypePrev = 0x19; 32 | kMessageTypeNext = 0x1a; 33 | kMessageTypeVolume = 0x1b; 34 | kMessageTypeShuffle = 0x1c; 35 | kMessageTypeRepeat = 0x1d; 36 | kMessageTypeVolumeDown = 0x1f; 37 | kMessageTypeVolumeUp = 0x20; 38 | kMessageTypeReplace = 0x21; 39 | kMessageTypeLogout = 0x22; 40 | kMessageTypeAction = 0x23; 41 | kMessageTypeRename = 0x24; 42 | kMessageTypeUpdateMetadata = 0x80; 43 | } 44 | 45 | message DeviceState { 46 | optional string sw_version = 0x1; 47 | optional bool is_active = 0xa; 48 | optional bool can_play = 0xb; 49 | optional uint32 volume = 0xc; 50 | optional string name = 0xd; 51 | optional uint32 error_code = 0xe; 52 | optional int64 became_active_at = 0xf; 53 | optional string error_message = 0x10; 54 | repeated Capability capabilities = 0x11; 55 | optional string context_player_error = 0x14; 56 | repeated Metadata metadata = 0x19; 57 | } 58 | 59 | message Capability { 60 | optional CapabilityType typ = 0x1; 61 | repeated int64 intValue = 0x2; 62 | repeated string stringValue = 0x3; 63 | } 64 | 65 | enum CapabilityType { 66 | kSupportedContexts = 0x1; 67 | kCanBePlayer = 0x2; 68 | kRestrictToLocal = 0x3; 69 | kDeviceType = 0x4; 70 | kGaiaEqConnectId = 0x5; 71 | kSupportsLogout = 0x6; 72 | kIsObservable = 0x7; 73 | kVolumeSteps = 0x8; 74 | kSupportedTypes = 0x9; 75 | kCommandAcks = 0xa; 76 | kSupportsRename = 0xb; 77 | kHidden = 0xc; 78 | } 79 | 80 | message Goodbye { 81 | optional string reason = 0x1; 82 | } 83 | 84 | message State { 85 | optional string context_uri = 0x2; 86 | optional uint32 index = 0x3; 87 | optional uint32 position_ms = 0x4; 88 | optional PlayStatus status = 0x5; 89 | optional uint64 position_measured_at = 0x7; 90 | optional string context_description = 0x8; 91 | optional bool shuffle = 0xd; 92 | optional bool repeat = 0xe; 93 | optional string last_command_ident = 0x14; 94 | optional uint32 last_command_msgid = 0x15; 95 | optional bool playing_from_fallback = 0x18; 96 | optional uint32 row = 0x19; 97 | optional uint32 playing_track_index = 0x1a; 98 | repeated TrackRef track = 0x1b; 99 | optional Ad ad = 0x1c; 100 | } 101 | 102 | enum PlayStatus { 103 | kPlayStatusStop = 0x0; 104 | kPlayStatusPlay = 0x1; 105 | kPlayStatusPause = 0x2; 106 | kPlayStatusLoading = 0x3; 107 | } 108 | 109 | message TrackRef { 110 | optional bytes gid = 0x1; 111 | optional string uri = 0x2; 112 | optional bool queued = 0x3; 113 | optional string context = 0x4; 114 | } 115 | 116 | message Ad { 117 | optional int32 next = 0x1; 118 | optional bytes ogg_fid = 0x2; 119 | optional bytes image_fid = 0x3; 120 | optional int32 duration = 0x4; 121 | optional string click_url = 0x5; 122 | optional string impression_url = 0x6; 123 | optional string product = 0x7; 124 | optional string advertiser = 0x8; 125 | optional bytes gid = 0x9; 126 | } 127 | 128 | message Metadata { 129 | optional string type = 0x1; 130 | optional string metadata = 0x2; 131 | } 132 | -------------------------------------------------------------------------------- /protocol/proto/suggest.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Track { 4 | optional bytes gid = 0x1; 5 | optional string name = 0x2; 6 | optional bytes image = 0x3; 7 | repeated string artist_name = 0x4; 8 | repeated bytes artist_gid = 0x5; 9 | optional uint32 rank = 0x6; 10 | } 11 | 12 | message Artist { 13 | optional bytes gid = 0x1; 14 | optional string name = 0x2; 15 | optional bytes image = 0x3; 16 | optional uint32 rank = 0x6; 17 | } 18 | 19 | message Album { 20 | optional bytes gid = 0x1; 21 | optional string name = 0x2; 22 | optional bytes image = 0x3; 23 | repeated string artist_name = 0x4; 24 | repeated bytes artist_gid = 0x5; 25 | optional uint32 rank = 0x6; 26 | } 27 | 28 | message Playlist { 29 | optional string uri = 0x1; 30 | optional string name = 0x2; 31 | optional string image_uri = 0x3; 32 | optional string owner_name = 0x4; 33 | optional string owner_uri = 0x5; 34 | optional uint32 rank = 0x6; 35 | } 36 | 37 | message Suggestions { 38 | repeated Track track = 0x1; 39 | repeated Album album = 0x2; 40 | repeated Artist artist = 0x3; 41 | repeated Playlist playlist = 0x4; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /protocol/proto/toplist.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Toplist { 4 | repeated string items = 0x1; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /protocol/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Autogenerated by build.sh 2 | 3 | extern crate protobuf; 4 | pub mod authentication; 5 | pub mod keyexchange; 6 | pub mod mercury; 7 | pub mod metadata; 8 | pub mod pubsub; 9 | pub mod spirc; 10 | -------------------------------------------------------------------------------- /src/audio_backend/alsa.rs: -------------------------------------------------------------------------------- 1 | use super::{Open, Sink}; 2 | use std::io; 3 | use alsa::{PCM, Stream, Mode, Format, Access}; 4 | 5 | pub struct AlsaSink(Option, String); 6 | 7 | impl Open for AlsaSink { 8 | fn open(device: Option) -> AlsaSink { 9 | info!("Using alsa sink"); 10 | 11 | let name = device.unwrap_or("default".to_string()); 12 | 13 | AlsaSink(None, name) 14 | } 15 | } 16 | 17 | impl Sink for AlsaSink { 18 | fn start(&mut self) -> io::Result<()> { 19 | if self.0.is_some() { 20 | } else { 21 | self.0 = Some(PCM::open(&*self.1, 22 | Stream::Playback, Mode::Blocking, 23 | Format::Signed16, Access::Interleaved, 24 | 2, 44100).ok().unwrap()); 25 | } 26 | Ok(()) 27 | } 28 | 29 | fn stop(&mut self) -> io::Result<()> { 30 | self.0 = None; 31 | Ok(()) 32 | } 33 | 34 | fn write(&mut self, data: &[i16]) -> io::Result<()> { 35 | self.0.as_mut().unwrap().write_interleaved(&data).unwrap(); 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/audio_backend/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub trait Open { 4 | fn open(Option) -> Self; 5 | } 6 | 7 | pub trait Sink { 8 | fn start(&mut self) -> io::Result<()>; 9 | fn stop(&mut self) -> io::Result<()>; 10 | fn write(&mut self, data: &[i16]) -> io::Result<()>; 11 | } 12 | 13 | fn mk_sink(device: Option) -> Box { 14 | Box::new(S::open(device)) 15 | } 16 | 17 | #[cfg(feature = "alsa-backend")] 18 | mod alsa; 19 | #[cfg(feature = "alsa-backend")] 20 | use self::alsa::AlsaSink; 21 | 22 | #[cfg(feature = "portaudio-backend")] 23 | mod portaudio; 24 | #[cfg(feature = "portaudio-backend")] 25 | use self::portaudio::PortAudioSink; 26 | 27 | #[cfg(feature = "pulseaudio-backend")] 28 | mod pulseaudio; 29 | #[cfg(feature = "pulseaudio-backend")] 30 | use self::pulseaudio::PulseAudioSink; 31 | 32 | mod pipe; 33 | use self::pipe::StdoutSink; 34 | 35 | pub const BACKENDS : &'static [ 36 | (&'static str, fn(Option) -> Box) 37 | ] = &[ 38 | #[cfg(feature = "alsa-backend")] 39 | ("alsa", mk_sink::), 40 | #[cfg(feature = "portaudio-backend")] 41 | ("portaudio", mk_sink::), 42 | #[cfg(feature = "pulseaudio-backend")] 43 | ("pulseaudio", mk_sink::), 44 | ("pipe", mk_sink::), 45 | ]; 46 | 47 | pub fn find(name: Option) -> Option) -> Box> { 48 | if let Some(name) = name { 49 | BACKENDS.iter().find(|backend| name == backend.0).map(|backend| backend.1) 50 | } else { 51 | Some(BACKENDS.first().expect("No backends were enabled at build time").1) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/audio_backend/pipe.rs: -------------------------------------------------------------------------------- 1 | use super::{Open, Sink}; 2 | use std::fs::OpenOptions; 3 | use std::io::{self, Write}; 4 | use std::mem; 5 | use std::slice; 6 | 7 | pub struct StdoutSink(Box); 8 | 9 | impl Open for StdoutSink { 10 | fn open(path: Option) -> StdoutSink { 11 | if let Some(path) = path { 12 | let file = OpenOptions::new().write(true).open(path).unwrap(); 13 | StdoutSink(Box::new(file)) 14 | } else { 15 | StdoutSink(Box::new(io::stdout())) 16 | } 17 | } 18 | } 19 | 20 | impl Sink for StdoutSink { 21 | fn start(&mut self) -> io::Result<()> { 22 | Ok(()) 23 | } 24 | 25 | fn stop(&mut self) -> io::Result<()> { 26 | Ok(()) 27 | } 28 | 29 | fn write(&mut self, data: &[i16]) -> io::Result<()> { 30 | let data: &[u8] = unsafe { 31 | slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * mem::size_of::()) 32 | }; 33 | 34 | self.0.write_all(data)?; 35 | self.0.flush()?; 36 | 37 | Ok(()) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/audio_backend/portaudio.rs: -------------------------------------------------------------------------------- 1 | use super::{Open, Sink}; 2 | use std::io; 3 | use std::process::exit; 4 | use std::time::Duration; 5 | use portaudio_rs; 6 | use portaudio_rs::stream::*; 7 | use portaudio_rs::device::{DeviceIndex, DeviceInfo, get_default_output_index}; 8 | 9 | pub struct PortAudioSink<'a>(Option>, StreamParameters); 10 | 11 | fn output_devices() -> Box> { 12 | let count = portaudio_rs::device::get_count().unwrap(); 13 | let devices = (0..count) 14 | .filter_map(|idx| { 15 | portaudio_rs::device::get_info(idx).map(|info| (idx, info)) 16 | }).filter(|&(_, ref info)| { 17 | info.max_output_channels > 0 18 | }); 19 | 20 | Box::new(devices) 21 | } 22 | 23 | fn list_outputs() { 24 | let default = get_default_output_index(); 25 | 26 | for (idx, info) in output_devices() { 27 | if Some(idx) == default { 28 | println!("- {} (default)", info.name); 29 | } else { 30 | println!("- {}", info.name) 31 | } 32 | } 33 | } 34 | 35 | fn find_output(device: &str) -> Option { 36 | output_devices() 37 | .find(|&(_, ref info)| info.name == device) 38 | .map(|(idx, _)| idx) 39 | } 40 | 41 | impl <'a> Open for PortAudioSink<'a> { 42 | fn open(device: Option) -> PortAudioSink<'a> { 43 | 44 | debug!("Using PortAudio sink"); 45 | 46 | portaudio_rs::initialize().unwrap(); 47 | 48 | let device_idx = match device.as_ref().map(AsRef::as_ref) { 49 | Some("?") => { 50 | list_outputs(); 51 | exit(0) 52 | } 53 | Some(device) => find_output(device), 54 | None => get_default_output_index(), 55 | }.expect("Could not find device"); 56 | 57 | let info = portaudio_rs::device::get_info(device_idx); 58 | let latency = match info { 59 | Some(info) => info.default_high_output_latency, 60 | None => Duration::new(0, 0), 61 | }; 62 | 63 | let params = StreamParameters { 64 | device: device_idx, 65 | channel_count: 2, 66 | suggested_latency: latency, 67 | data: 0i16, 68 | }; 69 | 70 | PortAudioSink(None, params) 71 | } 72 | } 73 | 74 | impl <'a> Sink for PortAudioSink<'a> { 75 | fn start(&mut self) -> io::Result<()> { 76 | if self.0.is_none() { 77 | self.0 = Some(Stream::open( 78 | None, Some(self.1), 79 | 44100.0, 80 | FRAMES_PER_BUFFER_UNSPECIFIED, 81 | StreamFlags::empty(), 82 | None 83 | ).unwrap());; 84 | } 85 | 86 | self.0.as_mut().unwrap().start().unwrap(); 87 | Ok(()) 88 | } 89 | fn stop(&mut self) -> io::Result<()> { 90 | self.0.as_mut().unwrap().stop().unwrap(); 91 | self.0 = None; 92 | Ok(()) 93 | } 94 | fn write(&mut self, data: &[i16]) -> io::Result<()> { 95 | match self.0.as_mut().unwrap().write(data) { 96 | Ok(_) => (), 97 | Err(portaudio_rs::PaError::OutputUnderflowed) => 98 | error!("PortAudio write underflow"), 99 | Err(e) => panic!("PA Error {}", e), 100 | }; 101 | 102 | Ok(()) 103 | } 104 | } 105 | 106 | impl <'a> Drop for PortAudioSink<'a> { 107 | fn drop(&mut self) { 108 | portaudio_rs::terminate().unwrap(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/audio_backend/pulseaudio.rs: -------------------------------------------------------------------------------- 1 | use super::{Open, Sink}; 2 | use std::io; 3 | use libpulse_sys::*; 4 | use std::ptr::{null, null_mut}; 5 | use std::mem::{transmute}; 6 | use std::ffi::CString; 7 | 8 | pub struct PulseAudioSink(*mut pa_simple); 9 | 10 | impl Open for PulseAudioSink { 11 | fn open(device: Option) -> PulseAudioSink { 12 | debug!("Using PulseAudio sink"); 13 | 14 | if device.is_some() { 15 | panic!("pulseaudio sink does not support specifying a device name"); 16 | } 17 | 18 | let ss = pa_sample_spec { 19 | format: PA_SAMPLE_S16LE, 20 | channels: 2, // stereo 21 | rate: 44100 22 | }; 23 | 24 | let name = CString::new("librespot").unwrap(); 25 | let description = CString::new("A spoty client library").unwrap(); 26 | 27 | let s = unsafe { 28 | pa_simple_new(null(), // Use the default server. 29 | name.as_ptr(), // Our application's name. 30 | PA_STREAM_PLAYBACK, 31 | null(), // Use the default device. 32 | description.as_ptr(), // Description of our stream. 33 | &ss, // Our sample format. 34 | null(), // Use default channel map 35 | null(), // Use default buffering attributes. 36 | null_mut(), // Ignore error code. 37 | ) 38 | }; 39 | assert!(s != null_mut()); 40 | 41 | PulseAudioSink(s) 42 | } 43 | } 44 | 45 | impl Sink for PulseAudioSink { 46 | fn start(&mut self) -> io::Result<()> { 47 | Ok(()) 48 | } 49 | 50 | fn stop(&mut self) -> io::Result<()> { 51 | Ok(()) 52 | } 53 | 54 | fn write(&mut self, data: &[i16]) -> io::Result<()> { 55 | unsafe { 56 | let ptr = transmute(data.as_ptr()); 57 | let bytes = data.len() as usize * 2; 58 | pa_simple_write(self.0, ptr, bytes, null_mut()); 59 | }; 60 | 61 | Ok(()) 62 | } 63 | } 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/discovery.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use crypto::digest::Digest; 3 | use crypto::mac::Mac; 4 | use crypto; 5 | use futures::sync::mpsc; 6 | use futures::{Future, Stream, BoxFuture, Poll, Async}; 7 | use hyper::server::{Service, NewService, Request, Response, Http}; 8 | use hyper::{self, Get, Post, StatusCode}; 9 | use mdns; 10 | use num_bigint::BigUint; 11 | use rand; 12 | use std::collections::BTreeMap; 13 | use std::io; 14 | use std::sync::Arc; 15 | use tokio_core::net::TcpListener; 16 | use tokio_core::reactor::Handle; 17 | use url; 18 | 19 | use core::diffie_hellman::{DH_GENERATOR, DH_PRIME}; 20 | use core::authentication::Credentials; 21 | use core::util; 22 | use core::config::ConnectConfig; 23 | 24 | #[derive(Clone)] 25 | struct Discovery(Arc); 26 | struct DiscoveryInner { 27 | config: ConnectConfig, 28 | device_id: String, 29 | private_key: BigUint, 30 | public_key: BigUint, 31 | tx: mpsc::UnboundedSender, 32 | } 33 | 34 | impl Discovery { 35 | pub fn new(config: ConnectConfig, device_id: String) 36 | -> (Discovery, mpsc::UnboundedReceiver) 37 | { 38 | let (tx, rx) = mpsc::unbounded(); 39 | 40 | let key_data = util::rand_vec(&mut rand::thread_rng(), 95); 41 | let private_key = BigUint::from_bytes_be(&key_data); 42 | let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); 43 | 44 | let discovery = Discovery(Arc::new(DiscoveryInner { 45 | config: config, 46 | device_id: device_id, 47 | private_key: private_key, 48 | public_key: public_key, 49 | tx: tx, 50 | })); 51 | 52 | (discovery, rx) 53 | } 54 | } 55 | 56 | impl Discovery { 57 | fn handle_get_info(&self, _params: &BTreeMap) 58 | -> ::futures::Finished 59 | { 60 | let public_key = self.0.public_key.to_bytes_be(); 61 | let public_key = base64::encode(&public_key); 62 | 63 | let result = json!({ 64 | "status": 101, 65 | "statusString": "ERROR-OK", 66 | "spotifyError": 0, 67 | "version": "2.1.0", 68 | "deviceID": (self.0.device_id), 69 | "remoteName": (self.0.config.name), 70 | "activeUser": "", 71 | "publicKey": (public_key), 72 | "deviceType": (self.0.config.device_type.to_string().to_uppercase()), 73 | "libraryVersion": "0.1.0", 74 | "accountReq": "PREMIUM", 75 | "brandDisplayName": "librespot", 76 | "modelDisplayName": "librespot", 77 | }); 78 | 79 | let body = result.to_string(); 80 | ::futures::finished(Response::new().with_body(body)) 81 | } 82 | 83 | fn handle_add_user(&self, params: &BTreeMap) 84 | -> ::futures::Finished 85 | { 86 | let username = params.get("userName").unwrap(); 87 | let encrypted_blob = params.get("blob").unwrap(); 88 | let client_key = params.get("clientKey").unwrap(); 89 | 90 | let encrypted_blob = base64::decode(encrypted_blob).unwrap(); 91 | 92 | let client_key = base64::decode(client_key).unwrap(); 93 | let client_key = BigUint::from_bytes_be(&client_key); 94 | 95 | let shared_key = util::powm(&client_key, &self.0.private_key, &DH_PRIME); 96 | 97 | let iv = &encrypted_blob[0..16]; 98 | let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; 99 | let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; 100 | 101 | let base_key = { 102 | let mut data = [0u8; 20]; 103 | let mut h = crypto::sha1::Sha1::new(); 104 | h.input(&shared_key.to_bytes_be()); 105 | h.result(&mut data); 106 | data[..16].to_owned() 107 | }; 108 | 109 | let checksum_key = { 110 | let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key); 111 | h.input(b"checksum"); 112 | h.result().code().to_owned() 113 | }; 114 | 115 | let encryption_key = { 116 | let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key); 117 | h.input(b"encryption"); 118 | h.result().code().to_owned() 119 | }; 120 | 121 | let mac = { 122 | let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &checksum_key); 123 | h.input(encrypted); 124 | h.result().code().to_owned() 125 | }; 126 | 127 | assert_eq!(&mac[..], cksum); 128 | 129 | let decrypted = { 130 | let mut data = vec![0u8; encrypted.len()]; 131 | let mut cipher = crypto::aes::ctr(crypto::aes::KeySize::KeySize128, 132 | &encryption_key[0..16], iv); 133 | cipher.process(encrypted, &mut data); 134 | String::from_utf8(data).unwrap() 135 | }; 136 | 137 | let credentials = Credentials::with_blob(username.to_owned(), &decrypted, &self.0.device_id); 138 | 139 | self.0.tx.send(credentials).unwrap(); 140 | 141 | let result = json!({ 142 | "status": 101, 143 | "spotifyError": 0, 144 | "statusString": "ERROR-OK" 145 | }); 146 | 147 | let body = result.to_string(); 148 | ::futures::finished(Response::new().with_body(body)) 149 | } 150 | 151 | fn not_found(&self) 152 | -> ::futures::Finished 153 | { 154 | ::futures::finished(Response::new().with_status(StatusCode::NotFound)) 155 | } 156 | } 157 | 158 | impl Service for Discovery { 159 | type Request = Request; 160 | type Response = Response; 161 | type Error = hyper::Error; 162 | type Future = BoxFuture; 163 | 164 | fn call(&self, request: Request) -> Self::Future { 165 | let mut params = BTreeMap::new(); 166 | 167 | let (method, uri, _, _, body) = request.deconstruct(); 168 | if let Some(query) = uri.query() { 169 | params.extend(url::form_urlencoded::parse(query.as_bytes()).into_owned()); 170 | } 171 | 172 | if method != Get { 173 | debug!("{:?} {:?} {:?}", method, uri.path(), params); 174 | } 175 | 176 | let this = self.clone(); 177 | body.fold(Vec::new(), |mut acc, chunk| { 178 | acc.extend_from_slice(chunk.as_ref()); 179 | Ok::<_, hyper::Error>(acc) 180 | }).map(move |body| { 181 | params.extend(url::form_urlencoded::parse(&body).into_owned()); 182 | params 183 | }).and_then(move |params| { 184 | match (method, params.get("action").map(AsRef::as_ref)) { 185 | (Get, Some("getInfo")) => this.handle_get_info(¶ms), 186 | (Post, Some("addUser")) => this.handle_add_user(¶ms), 187 | _ => this.not_found(), 188 | } 189 | }).boxed() 190 | } 191 | } 192 | 193 | impl NewService for Discovery { 194 | type Request = Request; 195 | type Response = Response; 196 | type Error = hyper::Error; 197 | type Instance = Self; 198 | 199 | fn new_service(&self) -> io::Result { 200 | Ok(self.clone()) 201 | } 202 | } 203 | 204 | pub struct DiscoveryStream { 205 | credentials: mpsc::UnboundedReceiver, 206 | _svc: mdns::Service, 207 | task: Box>, 208 | } 209 | 210 | pub fn discovery(handle: &Handle, config: ConnectConfig, device_id: String) 211 | -> io::Result 212 | { 213 | let (discovery, creds_rx) = Discovery::new(config.clone(), device_id); 214 | 215 | let listener = TcpListener::bind(&"0.0.0.0:0".parse().unwrap(), handle)?; 216 | let addr = listener.local_addr()?; 217 | 218 | let http = Http::new(); 219 | let handle_ = handle.clone(); 220 | let task = Box::new(listener.incoming().for_each(move |(socket, addr)| { 221 | http.bind_connection(&handle_, socket, addr, discovery.clone()); 222 | Ok(()) 223 | })); 224 | 225 | let responder = mdns::Responder::spawn(&handle)?; 226 | let svc = responder.register( 227 | "_spotify-connect._tcp".to_owned(), 228 | config.name, 229 | addr.port(), 230 | &["VERSION=1.0", "CPath=/"]); 231 | 232 | Ok(DiscoveryStream { 233 | credentials: creds_rx, 234 | _svc: svc, 235 | task: task, 236 | }) 237 | } 238 | 239 | impl Stream for DiscoveryStream { 240 | type Item = Credentials; 241 | type Error = io::Error; 242 | 243 | fn poll(&mut self) -> Poll, Self::Error> { 244 | match self.task.poll()? { 245 | Async::Ready(()) => unreachable!(), 246 | Async::NotReady => (), 247 | } 248 | 249 | Ok(self.credentials.poll().unwrap()) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/keymaster.rs: -------------------------------------------------------------------------------- 1 | use futures::{Future, BoxFuture}; 2 | use serde_json; 3 | 4 | use core::mercury::MercuryError; 5 | use core::session::Session; 6 | 7 | #[derive(Deserialize, Debug, Clone)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct Token { 10 | pub access_token: String, 11 | pub expires_in: u32, 12 | pub token_type: String, 13 | pub scope: Vec, 14 | } 15 | 16 | pub fn get_token(session: &Session, client_id: &str, scopes: &str) -> BoxFuture { 17 | let url = format!("hm://keymaster/token/authenticated?client_id={}&scope={}", 18 | client_id, scopes); 19 | session.mercury().get(url).map(move |response| { 20 | let data = response.payload.first().expect("Empty payload"); 21 | let data = String::from_utf8(data.clone()).unwrap(); 22 | let token : Token = serde_json::from_str(&data).unwrap(); 23 | 24 | token 25 | }).boxed() 26 | } 27 | -------------------------------------------------------------------------------- /src/lib.in.rs: -------------------------------------------------------------------------------- 1 | pub mod spirc; 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![crate_name = "librespot"] 2 | 3 | #![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))] 4 | 5 | // TODO: many items from tokio-core::io have been deprecated in favour of tokio-io 6 | #![allow(deprecated)] 7 | 8 | #[macro_use] extern crate log; 9 | #[macro_use] extern crate serde_json; 10 | #[macro_use] extern crate serde_derive; 11 | 12 | extern crate base64; 13 | extern crate crypto; 14 | extern crate futures; 15 | extern crate hyper; 16 | extern crate mdns; 17 | extern crate num_bigint; 18 | extern crate protobuf; 19 | extern crate rand; 20 | extern crate tokio_core; 21 | extern crate url; 22 | 23 | pub extern crate librespot_audio as audio; 24 | pub extern crate librespot_core as core; 25 | pub extern crate librespot_protocol as protocol; 26 | pub extern crate librespot_metadata as metadata; 27 | 28 | #[cfg(feature = "alsa-backend")] 29 | extern crate alsa; 30 | 31 | #[cfg(feature = "portaudio-rs")] 32 | extern crate portaudio_rs; 33 | 34 | #[cfg(feature = "libpulse-sys")] 35 | extern crate libpulse_sys; 36 | 37 | pub mod audio_backend; 38 | pub mod discovery; 39 | pub mod keymaster; 40 | pub mod mixer; 41 | pub mod player; 42 | 43 | include!(concat!(env!("OUT_DIR"), "/lib.rs")); 44 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // TODO: many items from tokio-core::io have been deprecated in favour of tokio-io 2 | #![allow(deprecated)] 3 | 4 | #[macro_use] extern crate log; 5 | extern crate env_logger; 6 | extern crate futures; 7 | extern crate getopts; 8 | extern crate librespot; 9 | extern crate tokio_core; 10 | extern crate tokio_signal; 11 | 12 | use env_logger::LogBuilder; 13 | use futures::{Future, Async, Poll, Stream}; 14 | use std::env; 15 | use std::io::{self, stderr, Write}; 16 | use std::path::PathBuf; 17 | use std::process::exit; 18 | use std::str::FromStr; 19 | use tokio_core::reactor::{Handle, Core}; 20 | use tokio_core::io::IoStream; 21 | use std::mem; 22 | 23 | use librespot::core::authentication::{get_credentials, Credentials}; 24 | use librespot::core::cache::Cache; 25 | use librespot::core::config::{Bitrate, DeviceType, PlayerConfig, SessionConfig, ConnectConfig}; 26 | use librespot::core::session::Session; 27 | use librespot::core::version; 28 | 29 | use librespot::audio_backend::{self, Sink, BACKENDS}; 30 | use librespot::discovery::{discovery, DiscoveryStream}; 31 | use librespot::mixer::{self, Mixer}; 32 | use librespot::player::Player; 33 | use librespot::spirc::{Spirc, SpircTask}; 34 | 35 | fn usage(program: &str, opts: &getopts::Options) -> String { 36 | let brief = format!("Usage: {} [options]", program); 37 | opts.usage(&brief) 38 | } 39 | 40 | fn setup_logging(verbose: bool) { 41 | let mut builder = LogBuilder::new(); 42 | match env::var("RUST_LOG") { 43 | Ok(config) => { 44 | builder.parse(&config); 45 | builder.init().unwrap(); 46 | 47 | if verbose { 48 | warn!("`--verbose` flag overidden by `RUST_LOG` environment variable"); 49 | } 50 | } 51 | Err(_) => { 52 | if verbose { 53 | builder.parse("mdns=info,librespot=trace"); 54 | } else { 55 | builder.parse("mdns=info,librespot=info"); 56 | } 57 | builder.init().unwrap(); 58 | } 59 | } 60 | } 61 | 62 | fn list_backends() { 63 | println!("Available Backends : "); 64 | for (&(name, _), idx) in BACKENDS.iter().zip(0..) { 65 | if idx == 0 { 66 | println!("- {} (default)", name); 67 | } else { 68 | println!("- {}", name); 69 | } 70 | } 71 | } 72 | 73 | #[derive(Clone)] 74 | struct Setup { 75 | backend: fn(Option) -> Box, 76 | device: Option, 77 | 78 | mixer: fn() -> Box, 79 | 80 | cache: Option, 81 | player_config: PlayerConfig, 82 | session_config: SessionConfig, 83 | connect_config: ConnectConfig, 84 | credentials: Option, 85 | enable_discovery: bool, 86 | } 87 | 88 | fn setup(args: &[String]) -> Setup { 89 | let mut opts = getopts::Options::new(); 90 | opts.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE") 91 | .optflag("", "disable-audio-cache", "Disable caching of the audio data.") 92 | .reqopt("n", "name", "Device name", "NAME") 93 | .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE") 94 | .optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE") 95 | .optopt("", "onstart", "Run PROGRAM when playback is about to begin.", "PROGRAM") 96 | .optopt("", "onstop", "Run PROGRAM when playback has ended.", "PROGRAM") 97 | .optflag("v", "verbose", "Enable verbose output") 98 | .optopt("u", "username", "Username to sign in with", "USERNAME") 99 | .optopt("p", "password", "Password", "PASSWORD") 100 | .optflag("", "disable-discovery", "Disable discovery mode") 101 | .optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND") 102 | .optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE") 103 | .optopt("", "mixer", "Mixer to use", "MIXER"); 104 | 105 | let matches = match opts.parse(&args[1..]) { 106 | Ok(m) => m, 107 | Err(f) => { 108 | writeln!(stderr(), "error: {}\n{}", f.to_string(), usage(&args[0], &opts)).unwrap(); 109 | exit(1); 110 | } 111 | }; 112 | 113 | let verbose = matches.opt_present("verbose"); 114 | setup_logging(verbose); 115 | 116 | info!("librespot {} ({}). Built on {}. Build ID: {}", 117 | version::short_sha(), 118 | version::commit_date(), 119 | version::short_now(), 120 | version::build_id()); 121 | 122 | let backend_name = matches.opt_str("backend"); 123 | if backend_name == Some("?".into()) { 124 | list_backends(); 125 | exit(0); 126 | } 127 | 128 | let backend = audio_backend::find(backend_name) 129 | .expect("Invalid backend"); 130 | 131 | let device = matches.opt_str("device"); 132 | 133 | let mixer_name = matches.opt_str("mixer"); 134 | let mixer = mixer::find(mixer_name.as_ref()) 135 | .expect("Invalid mixer"); 136 | 137 | let name = matches.opt_str("name").unwrap(); 138 | let use_audio_cache = !matches.opt_present("disable-audio-cache"); 139 | 140 | let cache = matches.opt_str("c").map(|cache_location| { 141 | Cache::new(PathBuf::from(cache_location), use_audio_cache) 142 | }); 143 | 144 | let credentials = { 145 | let cached_credentials = cache.as_ref().and_then(Cache::credentials); 146 | 147 | get_credentials( 148 | matches.opt_str("username"), 149 | matches.opt_str("password"), 150 | cached_credentials 151 | ) 152 | }; 153 | 154 | let session_config = { 155 | let device_id = librespot::core::session::device_id(&name); 156 | 157 | SessionConfig { 158 | user_agent: version::version_string(), 159 | device_id: device_id, 160 | } 161 | }; 162 | 163 | let player_config = { 164 | let bitrate = matches.opt_str("b").as_ref() 165 | .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) 166 | .unwrap_or(Bitrate::default()); 167 | 168 | PlayerConfig { 169 | bitrate: bitrate, 170 | onstart: matches.opt_str("onstart"), 171 | onstop: matches.opt_str("onstop"), 172 | } 173 | }; 174 | 175 | let connect_config = { 176 | let device_type = matches.opt_str("device-type").as_ref() 177 | .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) 178 | .unwrap_or(DeviceType::default()); 179 | 180 | ConnectConfig { 181 | name: name, 182 | device_type: device_type, 183 | } 184 | }; 185 | 186 | let enable_discovery = !matches.opt_present("disable-discovery"); 187 | 188 | Setup { 189 | backend: backend, 190 | cache: cache, 191 | session_config: session_config, 192 | player_config: player_config, 193 | connect_config: connect_config, 194 | credentials: credentials, 195 | device: device, 196 | enable_discovery: enable_discovery, 197 | mixer: mixer, 198 | } 199 | } 200 | 201 | struct Main { 202 | cache: Option, 203 | player_config: PlayerConfig, 204 | session_config: SessionConfig, 205 | connect_config: ConnectConfig, 206 | backend: fn(Option) -> Box, 207 | device: Option, 208 | mixer: fn() -> Box, 209 | handle: Handle, 210 | 211 | discovery: Option, 212 | signal: IoStream<()>, 213 | 214 | spirc: Option, 215 | spirc_task: Option, 216 | connect: Box>, 217 | 218 | shutdown: bool, 219 | } 220 | 221 | impl Main { 222 | fn new(handle: Handle, setup: Setup) -> Main { 223 | let mut task = Main { 224 | handle: handle.clone(), 225 | cache: setup.cache, 226 | session_config: setup.session_config, 227 | player_config: setup.player_config, 228 | connect_config: setup.connect_config, 229 | backend: setup.backend, 230 | device: setup.device, 231 | mixer: setup.mixer, 232 | 233 | connect: Box::new(futures::future::empty()), 234 | discovery: None, 235 | spirc: None, 236 | spirc_task: None, 237 | shutdown: false, 238 | signal: tokio_signal::ctrl_c(&handle).flatten_stream().boxed(), 239 | }; 240 | 241 | if setup.enable_discovery { 242 | let config = task.connect_config.clone(); 243 | let device_id = task.session_config.device_id.clone(); 244 | 245 | task.discovery = Some(discovery(&handle, config, device_id).unwrap()); 246 | } 247 | 248 | if let Some(credentials) = setup.credentials { 249 | task.credentials(credentials); 250 | } 251 | 252 | task 253 | } 254 | 255 | fn credentials(&mut self, credentials: Credentials) { 256 | let config = self.session_config.clone(); 257 | let handle = self.handle.clone(); 258 | 259 | let connection = Session::connect(config, credentials, self.cache.clone(), handle); 260 | 261 | self.connect = connection; 262 | self.spirc = None; 263 | let task = mem::replace(&mut self.spirc_task, None); 264 | if let Some(task) = task { 265 | self.handle.spawn(task); 266 | } 267 | } 268 | } 269 | 270 | impl Future for Main { 271 | type Item = (); 272 | type Error = (); 273 | 274 | fn poll(&mut self) -> Poll<(), ()> { 275 | loop { 276 | let mut progress = false; 277 | 278 | if let Some(Async::Ready(Some(creds))) = self.discovery.as_mut().map(|d| d.poll().unwrap()) { 279 | if let Some(ref spirc) = self.spirc { 280 | spirc.shutdown(); 281 | } 282 | self.credentials(creds); 283 | 284 | progress = true; 285 | } 286 | 287 | if let Async::Ready(session) = self.connect.poll().unwrap() { 288 | self.connect = Box::new(futures::future::empty()); 289 | let device = self.device.clone(); 290 | let mixer = (self.mixer)(); 291 | let player_config = self.player_config.clone(); 292 | let connect_config = self.connect_config.clone(); 293 | 294 | let audio_filter = mixer.get_audio_filter(); 295 | let backend = self.backend; 296 | let player = Player::new(player_config, session.clone(), audio_filter, move || { 297 | (backend)(device) 298 | }); 299 | 300 | let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer); 301 | self.spirc = Some(spirc); 302 | self.spirc_task = Some(spirc_task); 303 | 304 | progress = true; 305 | } 306 | 307 | if let Async::Ready(Some(())) = self.signal.poll().unwrap() { 308 | if !self.shutdown { 309 | if let Some(ref spirc) = self.spirc { 310 | spirc.shutdown(); 311 | } 312 | self.shutdown = true; 313 | } else { 314 | return Ok(Async::Ready(())); 315 | } 316 | 317 | progress = true; 318 | } 319 | 320 | if let Some(ref mut spirc_task) = self.spirc_task { 321 | if let Async::Ready(()) = spirc_task.poll().unwrap() { 322 | if self.shutdown { 323 | return Ok(Async::Ready(())); 324 | } else { 325 | panic!("Spirc shut down unexpectedly"); 326 | } 327 | } 328 | } 329 | 330 | if !progress { 331 | return Ok(Async::NotReady); 332 | } 333 | } 334 | } 335 | } 336 | 337 | fn main() { 338 | let mut core = Core::new().unwrap(); 339 | let handle = core.handle(); 340 | 341 | let args: Vec = std::env::args().collect(); 342 | 343 | core.run(Main::new(handle, setup(&args))).unwrap() 344 | } 345 | 346 | -------------------------------------------------------------------------------- /src/mixer/mod.rs: -------------------------------------------------------------------------------- 1 | pub trait Mixer : Send { 2 | fn open() -> Self where Self: Sized; 3 | fn start(&self); 4 | fn stop(&self); 5 | fn set_volume(&self, volume: u16); 6 | fn volume(&self) -> u16; 7 | fn get_audio_filter(&self) -> Option> { 8 | None 9 | } 10 | } 11 | 12 | pub trait AudioFilter { 13 | fn modify_stream(&self, data: &mut [i16]); 14 | } 15 | 16 | pub mod softmixer; 17 | use self::softmixer::SoftMixer; 18 | 19 | fn mk_sink() -> Box { 20 | Box::new(M::open()) 21 | } 22 | 23 | pub fn find>(name: Option) -> Option Box> { 24 | match name.as_ref().map(AsRef::as_ref) { 25 | None | Some("softvol") => Some(mk_sink::), 26 | _ => None, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/mixer/softmixer.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::sync::atomic::{AtomicUsize, Ordering}; 3 | 4 | use super::Mixer; 5 | use super::AudioFilter; 6 | 7 | #[derive(Clone)] 8 | pub struct SoftMixer { 9 | volume: Arc 10 | } 11 | 12 | impl Mixer for SoftMixer { 13 | fn open() -> SoftMixer { 14 | SoftMixer { 15 | volume: Arc::new(AtomicUsize::new(0xFFFF)) 16 | } 17 | } 18 | fn start(&self) { 19 | } 20 | fn stop(&self) { 21 | } 22 | fn volume(&self) -> u16 { 23 | self.volume.load(Ordering::Relaxed) as u16 24 | } 25 | fn set_volume(&self, volume: u16) { 26 | self.volume.store(volume as usize, Ordering::Relaxed); 27 | } 28 | fn get_audio_filter(&self) -> Option> { 29 | Some(Box::new(SoftVolumeApplier { volume: self.volume.clone() })) 30 | } 31 | } 32 | 33 | struct SoftVolumeApplier { 34 | volume: Arc 35 | } 36 | 37 | impl AudioFilter for SoftVolumeApplier { 38 | fn modify_stream(&self, data: &mut [i16]) { 39 | let volume = self.volume.load(Ordering::Relaxed) as u16; 40 | if volume != 0xFFFF { 41 | for x in data.iter_mut() { 42 | *x = (*x as i32 * volume as i32 / 0xFFFF) as i16; 43 | } 44 | } 45 | } 46 | } 47 | --------------------------------------------------------------------------------