librespot_core/connection/
mod.rs

1mod codec;
2mod handshake;
3
4pub use self::{codec::ApCodec, handshake::handshake};
5
6use std::{io, time::Duration};
7
8use futures_util::{SinkExt, StreamExt};
9use num_traits::FromPrimitive;
10use protobuf::Message;
11use thiserror::Error;
12use tokio::net::TcpStream;
13use tokio_util::codec::Framed;
14use url::Url;
15
16use crate::{Error, authentication::Credentials, packet::PacketType, version};
17
18use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
19
20pub type Transport = Framed<TcpStream, ApCodec>;
21
22fn login_error_message(code: &ErrorCode) -> &'static str {
23    pub use ErrorCode::*;
24    match code {
25        ProtocolError => "Protocol error",
26        TryAnotherAP => "Try another access point",
27        BadConnectionId => "Bad connection ID",
28        TravelRestriction => "Travel restriction",
29        PremiumAccountRequired => "Premium account required",
30        BadCredentials => "Bad credentials",
31        CouldNotValidateCredentials => "Could not validate credentials",
32        AccountExists => "Account exists",
33        ExtraVerificationRequired => "Extra verification required",
34        InvalidAppKey => "Invalid app key",
35        ApplicationBanned => "Application banned",
36    }
37}
38
39#[derive(Debug, Error)]
40pub enum AuthenticationError {
41    #[error("Login failed with reason: {}", login_error_message(.0))]
42    LoginFailed(ErrorCode),
43    #[error("invalid packet {0}")]
44    Packet(u8),
45    #[error("transport returned no data")]
46    Transport,
47}
48
49impl From<AuthenticationError> for Error {
50    fn from(err: AuthenticationError) -> Self {
51        match err {
52            AuthenticationError::LoginFailed(_) => Error::permission_denied(err),
53            AuthenticationError::Packet(_) => Error::unimplemented(err),
54            AuthenticationError::Transport => Error::unavailable(err),
55        }
56    }
57}
58
59impl From<APLoginFailed> for AuthenticationError {
60    fn from(login_failure: APLoginFailed) -> Self {
61        Self::LoginFailed(login_failure.error_code())
62    }
63}
64
65pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::Result<Transport> {
66    const TIMEOUT: Duration = Duration::from_secs(5);
67    tokio::time::timeout(TIMEOUT, {
68        let socket = crate::socket::connect(host, port, proxy).await?;
69        debug!("Connection to AP established.");
70        handshake(socket)
71    })
72    .await?
73}
74
75pub async fn connect_with_retry(
76    host: &str,
77    port: u16,
78    proxy: Option<&Url>,
79    max_retries: u8,
80) -> io::Result<Transport> {
81    let mut num_retries = 0;
82    loop {
83        match connect(host, port, proxy).await {
84            Ok(f) => return Ok(f),
85            Err(e) => {
86                debug!("Connection to \"{host}:{port}\" failed: {e}");
87                if num_retries < max_retries {
88                    num_retries += 1;
89                    debug!("Retry access point...");
90                    continue;
91                }
92                return Err(e);
93            }
94        }
95    }
96}
97
98pub async fn authenticate(
99    transport: &mut Transport,
100    credentials: Credentials,
101    device_id: &str,
102) -> Result<Credentials, Error> {
103    use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
104
105    let cpu_family = match std::env::consts::ARCH {
106        "blackfin" => CpuFamily::CPU_BLACKFIN,
107        "arm" | "aarch64" => CpuFamily::CPU_ARM,
108        "ia64" => CpuFamily::CPU_IA64,
109        "mips" => CpuFamily::CPU_MIPS,
110        "ppc" => CpuFamily::CPU_PPC,
111        "ppc64" => CpuFamily::CPU_PPC_64,
112        "sh" => CpuFamily::CPU_SH,
113        "x86" => CpuFamily::CPU_X86,
114        "x86_64" => CpuFamily::CPU_X86_64,
115        _ => CpuFamily::CPU_UNKNOWN,
116    };
117
118    let os = match crate::config::OS {
119        "android" => Os::OS_ANDROID,
120        "freebsd" | "netbsd" | "openbsd" => Os::OS_FREEBSD,
121        "ios" => Os::OS_IPHONE,
122        "linux" => Os::OS_LINUX,
123        "macos" => Os::OS_OSX,
124        "windows" => Os::OS_WINDOWS,
125        _ => Os::OS_UNKNOWN,
126    };
127
128    let mut packet = ClientResponseEncrypted::new();
129    if let Some(username) = credentials.username {
130        packet
131            .login_credentials
132            .mut_or_insert_default()
133            .set_username(username);
134    }
135    packet
136        .login_credentials
137        .mut_or_insert_default()
138        .set_typ(credentials.auth_type);
139    packet
140        .login_credentials
141        .mut_or_insert_default()
142        .set_auth_data(credentials.auth_data);
143    packet
144        .system_info
145        .mut_or_insert_default()
146        .set_cpu_family(cpu_family);
147    packet.system_info.mut_or_insert_default().set_os(os);
148    packet
149        .system_info
150        .mut_or_insert_default()
151        .set_system_information_string(format!(
152            "librespot-{}-{}",
153            version::SHA_SHORT,
154            version::BUILD_ID
155        ));
156    packet
157        .system_info
158        .mut_or_insert_default()
159        .set_device_id(device_id.to_string());
160    packet.set_version_string(format!("librespot {}", version::SEMVER));
161
162    let cmd = PacketType::Login;
163    let data = packet.write_to_bytes()?;
164
165    debug!("Authenticating with AP using {:?}", credentials.auth_type);
166    transport.send((cmd as u8, data)).await?;
167    let (cmd, data) = transport
168        .next()
169        .await
170        .ok_or(AuthenticationError::Transport)??;
171    let packet_type = FromPrimitive::from_u8(cmd);
172    let result = match packet_type {
173        Some(PacketType::APWelcome) => {
174            let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;
175
176            let reusable_credentials = Credentials {
177                username: Some(welcome_data.canonical_username().to_owned()),
178                auth_type: welcome_data.reusable_auth_credentials_type(),
179                auth_data: welcome_data.reusable_auth_credentials().to_owned(),
180            };
181
182            Ok(reusable_credentials)
183        }
184        Some(PacketType::AuthFailure) => {
185            let error_data = APLoginFailed::parse_from_bytes(data.as_ref())?;
186            Err(error_data.into())
187        }
188        _ => {
189            trace!("Did not expect {cmd:?} AES key packet with data {data:#?}");
190            Err(AuthenticationError::Packet(cmd))
191        }
192    };
193    Ok(result?)
194}