mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Add zeroconf based authentication.
This commit is contained in:
parent
4806f3e85a
commit
bead9daf78
14 changed files with 664 additions and 249 deletions
|
@ -7,6 +7,7 @@ addons:
|
||||||
packages:
|
packages:
|
||||||
- protobuf-compiler
|
- protobuf-compiler
|
||||||
- portaudio19-dev
|
- portaudio19-dev
|
||||||
|
- libavahi-compat-libdnssd-dev
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- export PATH=$HOME/.cargo/bin:$PATH
|
- export PATH=$HOME/.cargo/bin:$PATH
|
||||||
|
|
147
Cargo.lock
generated
147
Cargo.lock
generated
|
@ -4,20 +4,25 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"byteorder 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"byteorder 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"dns-sd 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"eventual 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"eventual 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
"getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"json_macros 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lazy_static 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
"lazy_static 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"librespot-protocol 0.1.0",
|
"librespot-protocol 0.1.0",
|
||||||
"num 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
"num 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"portaudio 0.1.2 (git+https://github.com/mvdnes/portaudio-rs)",
|
"portaudio 0.1.2 (git+https://github.com/mvdnes/portaudio-rs)",
|
||||||
"protobuf 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"protobuf 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"protobuf_macros 0.1.0 (git+https://github.com/plietar/rust-protobuf-macros.git)",
|
"protobuf_macros 0.1.0 (git+https://github.com/plietar/rust-protobuf-macros.git)",
|
||||||
"rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rpassword 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rpassword 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rust-crypto 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rust-crypto 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"shannon 0.1.1 (git+https://github.com/plietar/rust-shannon.git)",
|
"shannon 0.1.1 (git+https://github.com/plietar/rust-shannon.git)",
|
||||||
"tempfile 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tempfile 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
"time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"tiny_http 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"url 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"vergen 0.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
"vergen 0.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"vorbis 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
"vorbis 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
@ -31,6 +36,11 @@ dependencies = [
|
||||||
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ascii"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -54,6 +64,82 @@ name = "byteorder"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"num 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chunked_transfer"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dns-sd"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding-index-japanese"
|
||||||
|
version = "1.20141219.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding-index-korean"
|
||||||
|
version = "1.20141219.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding-index-simpchinese"
|
||||||
|
version = "1.20141219.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding-index-singlebyte"
|
||||||
|
version = "1.20141219.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding-index-tradchinese"
|
||||||
|
version = "1.20141219.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_index_tests"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eventual"
|
name = "eventual"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -78,6 +164,14 @@ name = "getopts"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "json_macros"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kernel32-sys"
|
name = "kernel32-sys"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
@ -116,7 +210,7 @@ name = "librespot-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mod_path 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"mod_path 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"protobuf 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"protobuf 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -127,6 +221,11 @@ dependencies = [
|
||||||
"libc 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matches"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mod_path"
|
name = "mod_path"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -177,7 +276,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "1.0.9"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -278,6 +377,48 @@ dependencies = [
|
||||||
"winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny_http"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"ascii 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"chrono 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"encoding 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "0.2.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vergen"
|
name = "vergen"
|
||||||
version = "0.0.16"
|
version = "0.0.16"
|
||||||
|
|
|
@ -18,16 +18,21 @@ path = "protocol"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bit-set = "~0.2.0"
|
bit-set = "~0.2.0"
|
||||||
byteorder = "~0.4.2"
|
byteorder = "~0.4.2"
|
||||||
|
dns-sd = "~0.1.0"
|
||||||
eventual = "~0.1.5"
|
eventual = "~0.1.5"
|
||||||
getopts = "~0.2.14"
|
getopts = "~0.2.14"
|
||||||
|
json_macros = "~0.2.6"
|
||||||
lazy_static = "~0.1.15"
|
lazy_static = "~0.1.15"
|
||||||
num = "~0.1.29"
|
num = "~0.1.29"
|
||||||
protobuf = "~1.0.9"
|
protobuf = "~1.0.10"
|
||||||
rand = "~0.3.12"
|
rand = "~0.3.12"
|
||||||
rpassword = "~0.1.0"
|
rpassword = "~0.1.0"
|
||||||
rust-crypto = "~0.2.34"
|
rust-crypto = "~0.2.34"
|
||||||
|
rustc-serialize = "~0.3.16"
|
||||||
time = "~0.1.34"
|
time = "~0.1.34"
|
||||||
|
tiny_http = "~0.5.1"
|
||||||
tempfile = "~1.1.3"
|
tempfile = "~1.1.3"
|
||||||
|
url = "~0.5.2"
|
||||||
vorbis = "~0.0.13"
|
vorbis = "~0.0.13"
|
||||||
|
|
||||||
[dependencies.protobuf_macros]
|
[dependencies.protobuf_macros]
|
||||||
|
|
|
@ -6,5 +6,5 @@ build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mod_path = "~0.1.6"
|
mod_path = "~0.1.6"
|
||||||
protobuf = "~1.0.4"
|
protobuf = "~1.0.10"
|
||||||
|
|
||||||
|
|
149
src/authentication.rs
Normal file
149
src/authentication.rs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
use crypto;
|
||||||
|
use crypto::aes;
|
||||||
|
use crypto::pbkdf2::pbkdf2;
|
||||||
|
use crypto::sha1::Sha1;
|
||||||
|
use crypto::hmac::Hmac;
|
||||||
|
use crypto::digest::Digest;
|
||||||
|
use protobuf::{self, Message, ProtobufEnum};
|
||||||
|
use std::io::{self, Read};
|
||||||
|
use std::result::Result;
|
||||||
|
use rustc_serialize::base64::FromBase64;
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
|
use librespot_protocol::authentication::AuthenticationType;
|
||||||
|
use session::Session;
|
||||||
|
use util;
|
||||||
|
|
||||||
|
fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> {
|
||||||
|
let mut data = [0u8];
|
||||||
|
try!(stream.read_exact(&mut data));
|
||||||
|
Ok(data[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_int<R: Read>(stream: &mut R) -> io::Result<u32> {
|
||||||
|
let lo = try!(read_u8(stream)) as u32;
|
||||||
|
if lo & 0x80 == 0 {
|
||||||
|
return Ok(lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hi = try!(read_u8(stream)) as u32;
|
||||||
|
Ok(lo & 0x7f | hi << 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bytes<R: Read>(stream: &mut R) -> io::Result<Vec<u8>> {
|
||||||
|
let length = try!(read_int(stream));
|
||||||
|
let mut data = vec![0u8; length as usize];
|
||||||
|
try!(stream.read_exact(&mut data));
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
fn login(&self, username: String, auth_data: Vec<u8>, typ: AuthenticationType) -> Result<(), ()> {
|
||||||
|
let packet = protobuf_init!(protocol::authentication::ClientResponseEncrypted::new(), {
|
||||||
|
login_credentials => {
|
||||||
|
username: username,
|
||||||
|
typ: typ,
|
||||||
|
auth_data: auth_data,
|
||||||
|
},
|
||||||
|
system_info => {
|
||||||
|
cpu_family: protocol::authentication::CpuFamily::CPU_UNKNOWN,
|
||||||
|
os: protocol::authentication::Os::OS_UNKNOWN,
|
||||||
|
system_information_string: "librespot".to_owned(),
|
||||||
|
device_id: self.0.data.read().unwrap().device_id.clone()
|
||||||
|
},
|
||||||
|
version_string: util::version::version_string(),
|
||||||
|
appkey => {
|
||||||
|
version: self.0.config.application_key[0] as u32,
|
||||||
|
devkey: self.0.config.application_key[0x1..0x81].to_vec(),
|
||||||
|
signature: self.0.config.application_key[0x81..0x141].to_vec(),
|
||||||
|
useragent: self.0.config.user_agent.clone(),
|
||||||
|
callback_hash: vec![0; 20],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.connect();
|
||||||
|
self.send_packet(0xab, &packet.write_to_bytes().unwrap()).unwrap();
|
||||||
|
let (cmd, data) = self.recv();
|
||||||
|
match cmd {
|
||||||
|
0xac => {
|
||||||
|
let welcome_data : protocol::authentication::APWelcome =
|
||||||
|
protobuf::parse_from_bytes(&data).unwrap();
|
||||||
|
self.0.data.write().unwrap().canonical_username =
|
||||||
|
welcome_data.get_canonical_username().to_string();
|
||||||
|
|
||||||
|
eprintln!("Authenticated !");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
0xad => {
|
||||||
|
let msg : protocol::keyexchange::APLoginFailed =
|
||||||
|
protobuf::parse_from_bytes(&data).unwrap();
|
||||||
|
eprintln!("Authentication failed, {:?}", msg);
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("Unexpected message {:x}", cmd);
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login_password(&self, username: String, password: String) -> Result<(), ()> {
|
||||||
|
self.login(username, password.into_bytes(),
|
||||||
|
AuthenticationType::AUTHENTICATION_USER_PASS)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login_blob(&self, username: String, blob: &str) -> Result<(), ()> {
|
||||||
|
let blob = blob.from_base64().unwrap();
|
||||||
|
|
||||||
|
let secret = {
|
||||||
|
let mut data = [0u8; 20];
|
||||||
|
let mut h = crypto::sha1::Sha1::new();
|
||||||
|
h.input(&self.0.data.read().unwrap().device_id.as_bytes());
|
||||||
|
h.result(&mut data);
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = {
|
||||||
|
let mut data = [0u8; 24];
|
||||||
|
let mut mac = Hmac::new(Sha1::new(), &secret);
|
||||||
|
pbkdf2(&mut mac, username.as_bytes(), 0x100, &mut data[0..20]);
|
||||||
|
|
||||||
|
let mut hash = Sha1::new();
|
||||||
|
hash.input(&data[0..20]);
|
||||||
|
hash.result(&mut data[0..20]);
|
||||||
|
BigEndian::write_u32(&mut data[20..], 20);
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
let blob = {
|
||||||
|
// Anyone know what this block mode is ?
|
||||||
|
let mut data = vec![0u8; blob.len()];
|
||||||
|
let mut cipher = aes::ecb_decryptor(
|
||||||
|
aes::KeySize::KeySize192, &key, crypto::blockmodes::NoPadding);
|
||||||
|
cipher.decrypt(&mut crypto::buffer::RefReadBuffer::new(&blob),
|
||||||
|
&mut crypto::buffer::RefWriteBuffer::new(&mut data),
|
||||||
|
true).unwrap();
|
||||||
|
|
||||||
|
let l = blob.len();
|
||||||
|
for i in 0..l-0x10 {
|
||||||
|
data[l - i - 1] ^= data[l - i - 0x11];
|
||||||
|
}
|
||||||
|
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cursor = io::Cursor::new(&blob);
|
||||||
|
read_u8(&mut cursor).unwrap();
|
||||||
|
read_bytes(&mut cursor).unwrap();
|
||||||
|
read_u8(&mut cursor).unwrap();
|
||||||
|
let auth_type = read_int(&mut cursor).unwrap();
|
||||||
|
let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap();
|
||||||
|
read_u8(&mut cursor).unwrap();
|
||||||
|
let auth_data = read_bytes(&mut cursor).unwrap();;
|
||||||
|
|
||||||
|
self.login(username, auth_data, auth_type)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,8 +6,6 @@ use std::io::{Read, Write};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::result;
|
use std::result;
|
||||||
|
|
||||||
use keys::SharedKeys;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
IoError(io::Error),
|
IoError(io::Error),
|
||||||
|
@ -74,14 +72,18 @@ impl PlainConnection {
|
||||||
Ok(buffer)
|
Ok(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_cipher(self, keys: SharedKeys) -> CipherConnection {
|
pub fn into_stream(self) -> TcpStream {
|
||||||
CipherConnection{
|
self.stream
|
||||||
stream: ShannonStream::new(self.stream, &keys.send_key(), &keys.recv_key())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CipherConnection {
|
impl CipherConnection {
|
||||||
|
pub fn new(stream: TcpStream, recv_key: &[u8], send_key: &[u8]) -> CipherConnection {
|
||||||
|
CipherConnection {
|
||||||
|
stream: ShannonStream::new(stream, recv_key, send_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_packet(&mut self, cmd: u8, data: &[u8]) -> Result<()> {
|
pub fn send_packet(&mut self, cmd: u8, data: &[u8]) -> Result<()> {
|
||||||
try!(self.stream.write_u8(cmd)); try!(self.stream.write_u16::<BigEndian>(data.len() as u16));
|
try!(self.stream.write_u8(cmd)); try!(self.stream.write_u16::<BigEndian>(data.len() as u16));
|
||||||
try!(self.stream.write(data));
|
try!(self.stream.write(data));
|
||||||
|
@ -109,11 +111,3 @@ pub trait PacketHandler {
|
||||||
fn handle(&mut self, cmd: u8, data: Vec<u8>);
|
fn handle(&mut self, cmd: u8, data: Vec<u8>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
match packet.cmd {
|
|
||||||
0x09 => &self.dispatch.stream,
|
|
||||||
0xd | 0xe => &self.dispatch.audio_key,
|
|
||||||
0xb2...0xb6 => &self.dispatch.mercury,
|
|
||||||
_ => &self.dispatch.main,
|
|
||||||
}.send(packet).unwrap();
|
|
||||||
*/
|
|
||||||
|
|
51
src/diffie_hellman.rs
Normal file
51
src/diffie_hellman.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use num::{BigUint, FromPrimitive};
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use util;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref DH_GENERATOR: BigUint = BigUint::from_u64(0x2).unwrap();
|
||||||
|
pub static ref DH_PRIME: BigUint = BigUint::from_bytes_be(&[
|
||||||
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9,
|
||||||
|
0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6,
|
||||||
|
0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e,
|
||||||
|
0x08, 0x8a, 0x67, 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6,
|
||||||
|
0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
|
||||||
|
0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a,
|
||||||
|
0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14,
|
||||||
|
0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45,
|
||||||
|
0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4,
|
||||||
|
0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DHLocalKeys {
|
||||||
|
private_key: BigUint,
|
||||||
|
public_key: BigUint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DHLocalKeys {
|
||||||
|
pub fn random<R: Rng>(rng: &mut R) -> DHLocalKeys {
|
||||||
|
let key_data = util::rand_vec(rng, 95);
|
||||||
|
|
||||||
|
let private_key = BigUint::from_bytes_be(&key_data);
|
||||||
|
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
|
||||||
|
|
||||||
|
DHLocalKeys {
|
||||||
|
private_key: private_key,
|
||||||
|
public_key: public_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn public_key(&self) -> Vec<u8> {
|
||||||
|
return self.public_key.to_bytes_be();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
|
||||||
|
let shared_key = util::powm(&BigUint::from_bytes_be(remote_key),
|
||||||
|
&self.private_key,
|
||||||
|
&DH_PRIME);
|
||||||
|
shared_key.to_bytes_be()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
154
src/discovery.rs
Normal file
154
src/discovery.rs
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
use crypto;
|
||||||
|
use crypto::mac::Mac;
|
||||||
|
use crypto::digest::Digest;
|
||||||
|
use dns_sd::DNSService;
|
||||||
|
use tiny_http::{Method, Response, ResponseBox, Server};
|
||||||
|
use num::BigUint;
|
||||||
|
use url;
|
||||||
|
use rand;
|
||||||
|
use rustc_serialize::base64::{self, ToBase64, FromBase64};
|
||||||
|
|
||||||
|
use session::Session;
|
||||||
|
use diffie_hellman::{DH_GENERATOR, DH_PRIME};
|
||||||
|
use util;
|
||||||
|
|
||||||
|
pub struct DiscoveryManager {
|
||||||
|
session: Session,
|
||||||
|
private_key: BigUint,
|
||||||
|
public_key: BigUint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_found() -> ResponseBox {
|
||||||
|
Response::from_string("Not found").with_status_code(404).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscoveryManager {
|
||||||
|
pub fn new(session: Session) -> DiscoveryManager {
|
||||||
|
let key_data = util::rand_vec(&mut rand::thread_rng(), 95);
|
||||||
|
let private_key = BigUint::from_bytes_be(&key_data);
|
||||||
|
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
|
||||||
|
|
||||||
|
DiscoveryManager {
|
||||||
|
session: session,
|
||||||
|
private_key: private_key,
|
||||||
|
public_key: public_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> ResponseBox {
|
||||||
|
let public_key = self.public_key.to_bytes_be().to_base64(base64::STANDARD);
|
||||||
|
Response::from_string(json!({
|
||||||
|
"status": 101,
|
||||||
|
"statusString": "ERROR-OK",
|
||||||
|
"spotifyError": 0,
|
||||||
|
"version": "2.1.0",
|
||||||
|
"deviceID": (self.session.0.data.read().unwrap().device_id),
|
||||||
|
"remoteName": (self.session.0.config.device_name),
|
||||||
|
"activeUser": "",
|
||||||
|
"publicKey": (public_key),
|
||||||
|
"deviceType": "UNKNOWN",
|
||||||
|
"libraryVersion": "0.1.0",
|
||||||
|
"accountReq": "PREMIUM",
|
||||||
|
"brandDisplayName": "librespot",
|
||||||
|
"modelDisplayName": "librespot",
|
||||||
|
}).to_string()).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_user(&self, params: &[(String, String)]) -> ResponseBox {
|
||||||
|
let &(_, ref username) = params.iter().find(|& &(ref key, _)| key == "userName").unwrap();
|
||||||
|
let &(_, ref encrypted_blob) = params.iter().find(|& &(ref key, _)| key == "blob").unwrap();
|
||||||
|
let &(_, ref client_key) = params.iter().find(|& &(ref key, _)| key == "clientKey").unwrap();
|
||||||
|
|
||||||
|
let encrypted_blob = encrypted_blob.from_base64().unwrap();
|
||||||
|
|
||||||
|
let client_key = client_key.from_base64().unwrap();
|
||||||
|
let client_key = BigUint::from_bytes_be(&client_key);
|
||||||
|
|
||||||
|
let shared_key = util::powm(&client_key, &self.private_key, &DH_PRIME);
|
||||||
|
|
||||||
|
let iv = &encrypted_blob[0..16];
|
||||||
|
let encrypted = &encrypted_blob[16..encrypted_blob.len()-20];
|
||||||
|
let cksum = &encrypted_blob[encrypted_blob.len()-20..encrypted_blob.len()];
|
||||||
|
|
||||||
|
let base_key = {
|
||||||
|
let mut data = [0u8; 20];
|
||||||
|
let mut h = crypto::sha1::Sha1::new();
|
||||||
|
h.input(&shared_key.to_bytes_be());
|
||||||
|
h.result(&mut data);
|
||||||
|
data[..16].to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let checksum_key = {
|
||||||
|
let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key);
|
||||||
|
h.input("checksum".as_bytes());
|
||||||
|
h.result().code().to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let encryption_key = {
|
||||||
|
let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key);
|
||||||
|
h.input("encryption".as_bytes());
|
||||||
|
h.result().code().to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mac = {
|
||||||
|
let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &checksum_key);
|
||||||
|
h.input(encrypted);
|
||||||
|
h.result().code().to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(mac.as_slice(), cksum);
|
||||||
|
|
||||||
|
let decrypted = {
|
||||||
|
let mut data = vec![0u8; encrypted.len()];
|
||||||
|
let mut cipher = crypto::aes::ctr(crypto::aes::KeySize::KeySize128, &encryption_key, &iv);
|
||||||
|
cipher.process(&encrypted, &mut data);
|
||||||
|
String::from_utf8(data).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.session.login_blob(username.to_owned(), &decrypted).unwrap();
|
||||||
|
|
||||||
|
Response::from_string(json!({
|
||||||
|
"status": 101,
|
||||||
|
"spotifyError": 0,
|
||||||
|
"statusString": "ERROR-OK"
|
||||||
|
}).to_string()).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&mut self) {
|
||||||
|
let server = Server::http("0.0.0.0:8000").unwrap();
|
||||||
|
let svc = DNSService::register(Some(&self.session.0.config.device_name),
|
||||||
|
"_spotify-connect._tcp",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
8000,
|
||||||
|
&["VERSION=1.0", "CPath=/"]
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
for mut request in server.incoming_requests() {
|
||||||
|
let (_, query, _) = url::parse_path(request.url()).unwrap();
|
||||||
|
let mut params = query.map(|q| url::form_urlencoded::parse(q.as_bytes())).unwrap_or(Vec::new());
|
||||||
|
|
||||||
|
if *request.method() == Method::Post {
|
||||||
|
let mut body = Vec::new();
|
||||||
|
request.as_reader().read_to_end(&mut body).unwrap();
|
||||||
|
let form = url::form_urlencoded::parse(&body);
|
||||||
|
params.extend(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{:?}", params);
|
||||||
|
|
||||||
|
let &(_, ref action) = params.iter().find(|& &(ref key, _)| key == "action").unwrap();
|
||||||
|
match action.as_ref() {
|
||||||
|
"getInfo" => request.respond(self.get_info()).unwrap(),
|
||||||
|
"addUser" => {
|
||||||
|
request.respond(self.add_user(¶ms)).unwrap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => request.respond(not_found()).unwrap(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(svc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
103
src/keys.rs
103
src/keys.rs
|
@ -1,103 +0,0 @@
|
||||||
use crypto;
|
|
||||||
use crypto::mac::Mac;
|
|
||||||
use num::{BigUint, FromPrimitive};
|
|
||||||
use rand;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use util;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref DH_GENERATOR: BigUint = BigUint::from_u64(0x2).unwrap();
|
|
||||||
static ref DH_PRIME: BigUint = BigUint::from_bytes_be(&[
|
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9,
|
|
||||||
0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6,
|
|
||||||
0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e,
|
|
||||||
0x08, 0x8a, 0x67, 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6,
|
|
||||||
0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
|
|
||||||
0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a,
|
|
||||||
0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14,
|
|
||||||
0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45,
|
|
||||||
0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4,
|
|
||||||
0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff,
|
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff ]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PrivateKeys {
|
|
||||||
private_key: BigUint,
|
|
||||||
public_key: BigUint,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SharedKeys {
|
|
||||||
//private: PrivateKeys,
|
|
||||||
challenge: Vec<u8>,
|
|
||||||
send_key: Vec<u8>,
|
|
||||||
recv_key: Vec<u8>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrivateKeys {
|
|
||||||
pub fn new() -> PrivateKeys {
|
|
||||||
let key_data = util::rand_vec(&mut rand::thread_rng(), 95);
|
|
||||||
Self::new_with_key(&key_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_with_key(key_data: &[u8]) -> PrivateKeys {
|
|
||||||
let private_key = BigUint::from_bytes_be(key_data);
|
|
||||||
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
|
|
||||||
|
|
||||||
PrivateKeys {
|
|
||||||
private_key: private_key,
|
|
||||||
public_key: public_key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
pub fn private_key(&self) -> Vec<u8> {
|
|
||||||
return self.private_key.to_bytes_be();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn public_key(&self) -> Vec<u8> {
|
|
||||||
return self.public_key.to_bytes_be();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_remote_key(self, remote_key: &[u8], client_packet: &[u8], server_packet: &[u8]) -> SharedKeys {
|
|
||||||
let shared_key = util::powm(&BigUint::from_bytes_be(remote_key), &self.private_key, &DH_PRIME);
|
|
||||||
|
|
||||||
let mut data = Vec::with_capacity(0x64);
|
|
||||||
let mut mac = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &shared_key.to_bytes_be());
|
|
||||||
|
|
||||||
for i in 1..6 {
|
|
||||||
mac.input(client_packet);
|
|
||||||
mac.input(server_packet);
|
|
||||||
mac.input(&[i]);
|
|
||||||
data.write(&mac.result().code()).unwrap();
|
|
||||||
mac.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
mac = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &data[..0x14]);
|
|
||||||
mac.input(client_packet);
|
|
||||||
mac.input(server_packet);
|
|
||||||
|
|
||||||
SharedKeys {
|
|
||||||
//private: self,
|
|
||||||
challenge: mac.result().code().to_vec(),
|
|
||||||
send_key: data[0x14..0x34].to_vec(),
|
|
||||||
recv_key: data[0x34..0x54].to_vec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SharedKeys {
|
|
||||||
pub fn challenge(&self) -> &[u8] {
|
|
||||||
&self.challenge
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_key(&self) -> &[u8] {
|
|
||||||
&self.send_key
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recv_key(&self) -> &[u8] {
|
|
||||||
&self.recv_key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
21
src/lib.rs
21
src/lib.rs
|
@ -1,35 +1,42 @@
|
||||||
#![crate_name = "librespot"]
|
#![crate_name = "librespot"]
|
||||||
|
|
||||||
#![feature(plugin,zero_one,iter_arith,mpsc_select,clone_from_slice)]
|
#![feature(plugin,zero_one,iter_arith,mpsc_select,clone_from_slice,convert)]
|
||||||
|
|
||||||
#![plugin(protobuf_macros)]
|
#![plugin(protobuf_macros)]
|
||||||
|
#![plugin(json_macros)]
|
||||||
#[macro_use] extern crate lazy_static;
|
#[macro_use] extern crate lazy_static;
|
||||||
|
|
||||||
extern crate bit_set;
|
extern crate bit_set;
|
||||||
extern crate byteorder;
|
extern crate byteorder;
|
||||||
extern crate crypto;
|
extern crate crypto;
|
||||||
|
extern crate dns_sd;
|
||||||
extern crate eventual;
|
extern crate eventual;
|
||||||
extern crate num;
|
extern crate num;
|
||||||
extern crate portaudio;
|
extern crate portaudio;
|
||||||
extern crate protobuf;
|
extern crate protobuf;
|
||||||
extern crate shannon;
|
extern crate shannon;
|
||||||
extern crate rand;
|
extern crate rand;
|
||||||
|
extern crate rustc_serialize;
|
||||||
extern crate time;
|
extern crate time;
|
||||||
|
extern crate tiny_http;
|
||||||
extern crate tempfile;
|
extern crate tempfile;
|
||||||
|
extern crate url;
|
||||||
extern crate vorbis;
|
extern crate vorbis;
|
||||||
|
|
||||||
extern crate librespot_protocol;
|
extern crate librespot_protocol;
|
||||||
|
|
||||||
#[macro_use] pub mod util;
|
#[macro_use] pub mod util;
|
||||||
pub mod audio_decrypt;
|
mod audio_decrypt;
|
||||||
pub mod audio_file;
|
mod audio_file;
|
||||||
pub mod audio_key;
|
mod audio_key;
|
||||||
pub mod connection;
|
mod authentication;
|
||||||
pub mod keys;
|
mod connection;
|
||||||
|
mod diffie_hellman;
|
||||||
|
pub mod discovery;
|
||||||
pub mod mercury;
|
pub mod mercury;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod spirc;
|
pub mod spirc;
|
||||||
pub mod stream;
|
mod stream;
|
||||||
|
|
||||||
|
|
50
src/main.rs
50
src/main.rs
|
@ -2,20 +2,19 @@ extern crate getopts;
|
||||||
extern crate librespot;
|
extern crate librespot;
|
||||||
extern crate rpassword;
|
extern crate rpassword;
|
||||||
|
|
||||||
|
use getopts::Options;
|
||||||
|
use rpassword::read_password;
|
||||||
use std::clone::Clone;
|
use std::clone::Clone;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{stdout, Read, Write};
|
use std::io::{stdout, Read, Write};
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use getopts::Options;
|
use librespot::discovery::DiscoveryManager;
|
||||||
use rpassword::read_password;
|
|
||||||
|
|
||||||
use librespot::session::{Config, Session};
|
|
||||||
use librespot::util::version::version_string;
|
|
||||||
use librespot::player::Player;
|
use librespot::player::Player;
|
||||||
|
use librespot::session::{Config, Session};
|
||||||
use librespot::spirc::SpircManager;
|
use librespot::spirc::SpircManager;
|
||||||
|
use librespot::util::version::version_string;
|
||||||
|
|
||||||
fn usage(program: &str, opts: &Options) -> String {
|
fn usage(program: &str, opts: &Options) -> String {
|
||||||
let brief = format!("Usage: {} [options]", program);
|
let brief = format!("Usage: {} [options]", program);
|
||||||
|
@ -40,9 +39,16 @@ fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut appkey_file = File::open(
|
let appkey = {
|
||||||
Path::new(&*matches.opt_str("a").unwrap())
|
let mut file = File::open(
|
||||||
).expect("Could not open app key.");
|
Path::new(&*matches.opt_str("a").unwrap())
|
||||||
|
).expect("Could not open app key.");
|
||||||
|
|
||||||
|
let mut data = Vec::new();
|
||||||
|
file.read_to_end(&mut data).unwrap();
|
||||||
|
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
let username = matches.opt_str("u").unwrap();
|
let username = matches.opt_str("u").unwrap();
|
||||||
let cache_location = matches.opt_str("c").unwrap();
|
let cache_location = matches.opt_str("c").unwrap();
|
||||||
|
@ -54,30 +60,26 @@ fn main() {
|
||||||
read_password().unwrap()
|
read_password().unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut appkey = Vec::new();
|
|
||||||
appkey_file.read_to_end(&mut appkey).unwrap();
|
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
application_key: appkey,
|
application_key: appkey,
|
||||||
user_agent: version_string(),
|
user_agent: version_string(),
|
||||||
device_id: name.clone(),
|
device_name: name,
|
||||||
cache_location: PathBuf::from(cache_location)
|
cache_location: PathBuf::from(cache_location)
|
||||||
};
|
};
|
||||||
|
|
||||||
let session = Session::new(config);
|
let session = Session::new(config);
|
||||||
session.login(username.clone(), password);
|
//session.login_password(username, password).unwrap();
|
||||||
session.poll();
|
let mut discovery = DiscoveryManager::new(session.clone());
|
||||||
|
discovery.run();
|
||||||
|
|
||||||
let _session = session.clone();
|
let player = Player::new(session.clone());
|
||||||
|
let mut spirc = SpircManager::new(session.clone(), player);
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
loop {
|
spirc.run()
|
||||||
_session.poll();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let player = Player::new(&session);
|
loop {
|
||||||
|
session.poll();
|
||||||
let mut spirc_manager = SpircManager::new(&session, player, name);
|
}
|
||||||
spirc_manager.run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ enum PlayerCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn new(session: &Session) -> Player {
|
pub fn new(session: Session) -> Player {
|
||||||
let (cmd_tx, cmd_rx) = mpsc::channel();
|
let (cmd_tx, cmd_rx) = mpsc::channel();
|
||||||
|
|
||||||
let state = Arc::new((Mutex::new(PlayerState {
|
let state = Arc::new((Mutex::new(PlayerState {
|
||||||
|
@ -52,7 +52,7 @@ impl Player {
|
||||||
}), Condvar::new()));
|
}), Condvar::new()));
|
||||||
|
|
||||||
let internal = PlayerInternal {
|
let internal = PlayerInternal {
|
||||||
session: session.clone(),
|
session: session,
|
||||||
commands: cmd_rx,
|
commands: cmd_rx,
|
||||||
state: state.clone()
|
state: state.clone()
|
||||||
};
|
};
|
||||||
|
@ -85,7 +85,8 @@ impl PlayerInternal {
|
||||||
let mut decoder = None;
|
let mut decoder = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let cmd = if self.state.0.lock().unwrap().status == PlayStatus::kPlayStatusPlay {
|
let playing = self.state.0.lock().unwrap().status == PlayStatus::kPlayStatusPlay;
|
||||||
|
let cmd = if playing {
|
||||||
self.commands.try_recv().ok()
|
self.commands.try_recv().ok()
|
||||||
} else {
|
} else {
|
||||||
Some(self.commands.recv().unwrap())
|
Some(self.commands.recv().unwrap())
|
||||||
|
@ -98,7 +99,11 @@ impl PlayerInternal {
|
||||||
stream.stop().unwrap();
|
stream.stop().unwrap();
|
||||||
}
|
}
|
||||||
state.end_of_track = false;
|
state.end_of_track = false;
|
||||||
state.status = PlayStatus::kPlayStatusLoading;
|
state.status = if play {
|
||||||
|
PlayStatus::kPlayStatusPlay
|
||||||
|
} else {
|
||||||
|
PlayStatus::kPlayStatusPause
|
||||||
|
};
|
||||||
state.position_ms = position;
|
state.position_ms = position;
|
||||||
state.position_measured_at = util::now_ms();
|
state.position_measured_at = util::now_ms();
|
||||||
return true;
|
return true;
|
||||||
|
|
160
src/session.rs
160
src/session.rs
|
@ -1,35 +1,38 @@
|
||||||
use crypto::digest::Digest;
|
use crypto::digest::Digest;
|
||||||
|
use crypto::mac::Mac;
|
||||||
use crypto::sha1::Sha1;
|
use crypto::sha1::Sha1;
|
||||||
|
use crypto::hmac::Hmac;
|
||||||
use eventual::Future;
|
use eventual::Future;
|
||||||
use protobuf::{self, Message};
|
use protobuf::{self, Message};
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
use std::sync::{Mutex, RwLock, Arc, mpsc};
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Mutex, RwLock, Arc, mpsc};
|
||||||
|
|
||||||
|
use audio_key::{AudioKeyManager, AudioKey, AudioKeyError};
|
||||||
|
use audio_file::{AudioFileManager, AudioFile};
|
||||||
use connection::{self, PlainConnection, CipherConnection};
|
use connection::{self, PlainConnection, CipherConnection};
|
||||||
use keys::PrivateKeys;
|
use connection::PacketHandler;
|
||||||
|
use diffie_hellman::DHLocalKeys;
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
use util::{SpotifyId, FileId, mkdir_existing};
|
|
||||||
|
|
||||||
use mercury::{MercuryManager, MercuryRequest, MercuryResponse};
|
use mercury::{MercuryManager, MercuryRequest, MercuryResponse};
|
||||||
use metadata::{MetadataManager, MetadataRef, MetadataTrait};
|
use metadata::{MetadataManager, MetadataRef, MetadataTrait};
|
||||||
use stream::{StreamManager, StreamEvent};
|
use stream::{StreamManager, StreamEvent};
|
||||||
use audio_key::{AudioKeyManager, AudioKey, AudioKeyError};
|
use util::{SpotifyId, FileId, mkdir_existing};
|
||||||
use audio_file::{AudioFileManager, AudioFile};
|
|
||||||
use connection::PacketHandler;
|
|
||||||
|
|
||||||
use util;
|
use util;
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub application_key: Vec<u8>,
|
pub application_key: Vec<u8>,
|
||||||
pub user_agent: String,
|
pub user_agent: String,
|
||||||
pub device_id: String,
|
pub device_name: String,
|
||||||
pub cache_location: PathBuf,
|
pub cache_location: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SessionData {
|
pub struct SessionData {
|
||||||
pub country: String,
|
pub country: String,
|
||||||
pub canonical_username: String,
|
pub canonical_username: String,
|
||||||
|
pub device_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SessionInternal {
|
pub struct SessionInternal {
|
||||||
|
@ -41,24 +44,45 @@ pub struct SessionInternal {
|
||||||
stream: Mutex<StreamManager>,
|
stream: Mutex<StreamManager>,
|
||||||
audio_key: Mutex<AudioKeyManager>,
|
audio_key: Mutex<AudioKeyManager>,
|
||||||
audio_file: Mutex<AudioFileManager>,
|
audio_file: Mutex<AudioFileManager>,
|
||||||
rx_connection: Mutex<CipherConnection>,
|
rx_connection: Mutex<Option<CipherConnection>>,
|
||||||
tx_connection: Mutex<CipherConnection>,
|
tx_connection: Mutex<Option<CipherConnection>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Session(pub Arc<SessionInternal>);
|
pub struct Session(pub Arc<SessionInternal>);
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn new(mut config: Config) -> Session {
|
pub fn new(config: Config) -> Session {
|
||||||
config.device_id = {
|
mkdir_existing(&config.cache_location).unwrap();
|
||||||
|
|
||||||
|
let device_id = {
|
||||||
let mut h = Sha1::new();
|
let mut h = Sha1::new();
|
||||||
h.input_str(&config.device_id);
|
h.input_str(&config.device_name);
|
||||||
h.result_str()
|
h.result_str()
|
||||||
};
|
};
|
||||||
|
|
||||||
mkdir_existing(&config.cache_location).unwrap();
|
Session(Arc::new(SessionInternal {
|
||||||
|
config: config,
|
||||||
|
data: RwLock::new(SessionData {
|
||||||
|
country: String::new(),
|
||||||
|
canonical_username: String::new(),
|
||||||
|
device_id: device_id,
|
||||||
|
}),
|
||||||
|
|
||||||
|
rx_connection: Mutex::new(None),
|
||||||
|
tx_connection: Mutex::new(None),
|
||||||
|
|
||||||
|
mercury: Mutex::new(MercuryManager::new()),
|
||||||
|
metadata: Mutex::new(MetadataManager::new()),
|
||||||
|
stream: Mutex::new(StreamManager::new()),
|
||||||
|
audio_key: Mutex::new(AudioKeyManager::new()),
|
||||||
|
audio_file: Mutex::new(AudioFileManager::new()),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect(&self) {
|
||||||
|
let local_keys = DHLocalKeys::random(&mut thread_rng());
|
||||||
|
|
||||||
let keys = PrivateKeys::new();
|
|
||||||
let mut connection = PlainConnection::connect().unwrap();
|
let mut connection = PlainConnection::connect().unwrap();
|
||||||
|
|
||||||
let request = protobuf_init!(protocol::keyexchange::ClientHello::new(), {
|
let request = protobuf_init!(protocol::keyexchange::ClientHello::new(), {
|
||||||
|
@ -82,7 +106,7 @@ impl Session {
|
||||||
],
|
],
|
||||||
*/
|
*/
|
||||||
login_crypto_hello.diffie_hellman => {
|
login_crypto_hello.diffie_hellman => {
|
||||||
gc: keys.public_key(),
|
gc: local_keys.public_key(),
|
||||||
server_keys_known: 1,
|
server_keys_known: 1,
|
||||||
},
|
},
|
||||||
client_nonce: util::rand_vec(&mut thread_rng(), 0x10),
|
client_nonce: util::rand_vec(&mut thread_rng(), 0x10),
|
||||||
|
@ -100,75 +124,55 @@ impl Session {
|
||||||
let response : protocol::keyexchange::APResponseMessage =
|
let response : protocol::keyexchange::APResponseMessage =
|
||||||
protobuf::parse_from_bytes(&init_server_packet[4..]).unwrap();
|
protobuf::parse_from_bytes(&init_server_packet[4..]).unwrap();
|
||||||
|
|
||||||
protobuf_bind!(response, {
|
let remote_key = response.get_challenge()
|
||||||
challenge => {
|
.get_login_crypto_challenge()
|
||||||
login_crypto_challenge.diffie_hellman => {
|
.get_diffie_hellman()
|
||||||
gs: remote_key,
|
.get_gs();
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let shared_keys = keys.add_remote_key(remote_key, &init_client_packet, &init_server_packet);
|
let shared_secret = local_keys.shared_secret(remote_key);
|
||||||
|
let (challenge, send_key, recv_key) = {
|
||||||
|
let mut data = Vec::with_capacity(0x64);
|
||||||
|
let mut mac = Hmac::new(Sha1::new(), &shared_secret);
|
||||||
|
|
||||||
|
for i in 1..6 {
|
||||||
|
mac.input(&init_client_packet);
|
||||||
|
mac.input(&init_server_packet);
|
||||||
|
mac.input(&[i]);
|
||||||
|
data.write(&mac.result().code()).unwrap();
|
||||||
|
mac.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
mac = Hmac::new(Sha1::new(), &data[..0x14]);
|
||||||
|
mac.input(&init_client_packet);
|
||||||
|
mac.input(&init_server_packet);
|
||||||
|
|
||||||
|
(mac.result().code().to_vec(),
|
||||||
|
data[0x14..0x34].to_vec(),
|
||||||
|
data[0x34..0x54].to_vec())
|
||||||
|
};
|
||||||
|
|
||||||
let packet = protobuf_init!(protocol::keyexchange::ClientResponsePlaintext::new(), {
|
let packet = protobuf_init!(protocol::keyexchange::ClientResponsePlaintext::new(), {
|
||||||
login_crypto_response.diffie_hellman => {
|
login_crypto_response.diffie_hellman => {
|
||||||
hmac: shared_keys.challenge().to_vec()
|
hmac: challenge
|
||||||
},
|
},
|
||||||
pow_response => {},
|
pow_response => {},
|
||||||
crypto_response => {},
|
crypto_response => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
connection.send_packet(&packet.write_to_bytes().unwrap()).unwrap();
|
connection.send_packet(&packet.write_to_bytes().unwrap()).unwrap();
|
||||||
|
|
||||||
let cipher_connection = connection.setup_cipher(shared_keys);
|
let cipher_connection = CipherConnection::new(
|
||||||
|
connection.into_stream(),
|
||||||
|
&send_key,
|
||||||
|
&recv_key);
|
||||||
|
|
||||||
Session(Arc::new(SessionInternal {
|
*self.0.rx_connection.lock().unwrap() = Some(cipher_connection.clone());
|
||||||
config: config,
|
*self.0.tx_connection.lock().unwrap() = Some(cipher_connection);
|
||||||
data: RwLock::new(SessionData {
|
|
||||||
country: String::new(),
|
|
||||||
canonical_username: String::new(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
rx_connection: Mutex::new(cipher_connection.clone()),
|
|
||||||
tx_connection: Mutex::new(cipher_connection),
|
|
||||||
|
|
||||||
mercury: Mutex::new(MercuryManager::new()),
|
|
||||||
metadata: Mutex::new(MetadataManager::new()),
|
|
||||||
stream: Mutex::new(StreamManager::new()),
|
|
||||||
audio_key: Mutex::new(AudioKeyManager::new()),
|
|
||||||
audio_file: Mutex::new(AudioFileManager::new()),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login(&self, username: String, password: String) {
|
|
||||||
let packet = protobuf_init!(protocol::authentication::ClientResponseEncrypted::new(), {
|
|
||||||
login_credentials => {
|
|
||||||
username: username,
|
|
||||||
typ: protocol::authentication::AuthenticationType::AUTHENTICATION_USER_PASS,
|
|
||||||
auth_data: password.into_bytes(),
|
|
||||||
},
|
|
||||||
system_info => {
|
|
||||||
cpu_family: protocol::authentication::CpuFamily::CPU_UNKNOWN,
|
|
||||||
os: protocol::authentication::Os::OS_UNKNOWN,
|
|
||||||
system_information_string: "librespot".to_owned(),
|
|
||||||
device_id: self.0.config.device_id.clone()
|
|
||||||
},
|
|
||||||
version_string: util::version::version_string(),
|
|
||||||
appkey => {
|
|
||||||
version: self.0.config.application_key[0] as u32,
|
|
||||||
devkey: self.0.config.application_key[0x1..0x81].to_vec(),
|
|
||||||
signature: self.0.config.application_key[0x81..0x141].to_vec(),
|
|
||||||
useragent: self.0.config.user_agent.clone(),
|
|
||||||
callback_hash: vec![0; 20],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.send_packet(0xab, &packet.write_to_bytes().unwrap()).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn poll(&self) {
|
pub fn poll(&self) {
|
||||||
let (cmd, data) =
|
let (cmd, data) = self.recv();
|
||||||
self.0.rx_connection.lock().unwrap().recv_packet().unwrap();
|
|
||||||
|
|
||||||
match cmd {
|
match cmd {
|
||||||
0x4 => self.send_packet(0x49, &data).unwrap(),
|
0x4 => self.send_packet(0x49, &data).unwrap(),
|
||||||
|
@ -179,23 +183,17 @@ impl Session {
|
||||||
self.0.data.write().unwrap().country =
|
self.0.data.write().unwrap().country =
|
||||||
String::from_utf8(data).unwrap();
|
String::from_utf8(data).unwrap();
|
||||||
},
|
},
|
||||||
|
|
||||||
0xb2...0xb6 => self.0.mercury.lock().unwrap().handle(cmd, data),
|
0xb2...0xb6 => self.0.mercury.lock().unwrap().handle(cmd, data),
|
||||||
0xac => {
|
|
||||||
let welcome_data : protocol::authentication::APWelcome =
|
|
||||||
protobuf::parse_from_bytes(&data).unwrap();
|
|
||||||
self.0.data.write().unwrap().canonical_username =
|
|
||||||
welcome_data.get_canonical_username().to_string();
|
|
||||||
eprintln!("Authentication succeeded")
|
|
||||||
},
|
|
||||||
|
|
||||||
0xad => eprintln!("Authentication failed"),
|
|
||||||
_ => ()
|
_ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn recv(&self) -> (u8, Vec<u8>) {
|
||||||
|
self.0.rx_connection.lock().unwrap().as_mut().unwrap().recv_packet().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_packet(&self, cmd: u8, data: &[u8]) -> connection::Result<()> {
|
pub fn send_packet(&self, cmd: u8, data: &[u8]) -> connection::Result<()> {
|
||||||
self.0.tx_connection.lock().unwrap().send_packet(cmd, data)
|
self.0.tx_connection.lock().unwrap().as_mut().unwrap().send_packet(cmd, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn audio_key(&self, track: SpotifyId, file: FileId) -> Future<AudioKey, AudioKeyError> {
|
pub fn audio_key(&self, track: SpotifyId, file: FileId) -> Future<AudioKey, AudioKeyError> {
|
||||||
|
|
33
src/spirc.rs
33
src/spirc.rs
|
@ -11,9 +11,9 @@ use mercury::{MercuryRequest, MercuryMethod};
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
pub use librespot_protocol::spirc::PlayStatus;
|
pub use librespot_protocol::spirc::PlayStatus;
|
||||||
|
|
||||||
pub struct SpircManager<'s, D: SpircDelegate> {
|
pub struct SpircManager<D: SpircDelegate> {
|
||||||
delegate: D,
|
delegate: D,
|
||||||
session: &'s Session,
|
session: Session,
|
||||||
|
|
||||||
state_update_id: i64,
|
state_update_id: i64,
|
||||||
seq_nr: u32,
|
seq_nr: u32,
|
||||||
|
@ -58,18 +58,22 @@ pub trait SpircState {
|
||||||
fn end_of_track(&self) -> bool;
|
fn end_of_track(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'s, D: SpircDelegate> SpircManager<'s, D> {
|
impl <D: SpircDelegate> SpircManager<D> {
|
||||||
pub fn new(session: &'s Session, delegate: D,
|
pub fn new(session: Session, delegate: D)
|
||||||
name: String) -> SpircManager<'s, D> {
|
-> SpircManager<D> {
|
||||||
|
|
||||||
|
let ident = session.0.data.read().unwrap().device_id.clone();
|
||||||
|
let name = session.0.config.device_name.clone();
|
||||||
|
|
||||||
SpircManager {
|
SpircManager {
|
||||||
delegate: delegate,
|
delegate: delegate,
|
||||||
session: &session,
|
session: session,
|
||||||
|
|
||||||
state_update_id: 0,
|
state_update_id: 0,
|
||||||
seq_nr: 0,
|
seq_nr: 0,
|
||||||
|
|
||||||
name: name,
|
name: name,
|
||||||
ident: session.0.config.device_id.clone(),
|
ident: ident,
|
||||||
device_type: 5,
|
device_type: 5,
|
||||||
can_play: true,
|
can_play: true,
|
||||||
|
|
||||||
|
@ -93,6 +97,8 @@ impl <'s, D: SpircDelegate> SpircManager<'s, D> {
|
||||||
self.session.0.data.read().unwrap().canonical_username.clone()));
|
self.session.0.data.read().unwrap().canonical_username.clone()));
|
||||||
let updates = self.delegate.updates();
|
let updates = self.delegate.updates();
|
||||||
|
|
||||||
|
self.notify(true, None);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
select! {
|
select! {
|
||||||
pkt = rx.recv() => {
|
pkt = rx.recv() => {
|
||||||
|
@ -119,7 +125,7 @@ impl <'s, D: SpircDelegate> SpircManager<'s, D> {
|
||||||
self.delegate.load(track, true, 0);
|
self.delegate.load(track, true, 0);
|
||||||
} else {
|
} else {
|
||||||
self.state_update_id = update_time.unwrap();
|
self.state_update_id = update_time.unwrap();
|
||||||
self.notify(None);
|
self.notify(false, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +139,7 @@ impl <'s, D: SpircDelegate> SpircManager<'s, D> {
|
||||||
}
|
}
|
||||||
match frame.get_typ() {
|
match frame.get_typ() {
|
||||||
protocol::spirc::MessageType::kMessageTypeHello => {
|
protocol::spirc::MessageType::kMessageTypeHello => {
|
||||||
self.notify(Some(frame.get_ident()));
|
self.notify(false, Some(frame.get_ident()));
|
||||||
}
|
}
|
||||||
protocol::spirc::MessageType::kMessageTypeLoad => {
|
protocol::spirc::MessageType::kMessageTypeLoad => {
|
||||||
if !self.is_active {
|
if !self.is_active {
|
||||||
|
@ -171,13 +177,18 @@ impl <'s, D: SpircDelegate> SpircManager<'s, D> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify(&mut self, recipient: Option<&str>) {
|
fn notify(&mut self, hello: bool, recipient: Option<&str>) {
|
||||||
let mut pkt = protobuf_init!(protocol::spirc::Frame::new(), {
|
let mut pkt = protobuf_init!(protocol::spirc::Frame::new(), {
|
||||||
version: 1,
|
version: 1,
|
||||||
ident: self.ident.clone(),
|
ident: self.ident.clone(),
|
||||||
protocol_version: "2.0.0".to_owned(),
|
protocol_version: "2.0.0".to_owned(),
|
||||||
seq_nr: { self.seq_nr += 1; self.seq_nr },
|
seq_nr: { self.seq_nr += 1; self.seq_nr },
|
||||||
typ: protocol::spirc::MessageType::kMessageTypeNotify,
|
typ: if hello {
|
||||||
|
protocol::spirc::MessageType::kMessageTypeHello
|
||||||
|
} else {
|
||||||
|
protocol::spirc::MessageType::kMessageTypeNotify
|
||||||
|
},
|
||||||
|
|
||||||
device_state: self.device_state(),
|
device_state: self.device_state(),
|
||||||
recipient: protobuf::RepeatedField::from_vec(
|
recipient: protobuf::RepeatedField::from_vec(
|
||||||
recipient.map(|r| vec![r.to_owned()] ).unwrap_or(vec![])
|
recipient.map(|r| vec![r.to_owned()] ).unwrap_or(vec![])
|
||||||
|
|
Loading…
Reference in a new issue