Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: streaming on Android devices #1403

Merged
merged 5 commits into from
Dec 5, 2024

Conversation

SilverMira
Copy link
Contributor

@SilverMira SilverMira commented Nov 24, 2024

Fixes: #1399

I have mostly figured out the nuances and rules to get streaming on Android to work again.

  1. If we send client_hello that has platform == Platform::PLATFORM_ANDROID_ARM, it does not matter what credentials we send to the AP, all APs will error with TryAnotherAP, tested true for credentials obtained via
    • oauth flow access_token with Keymaster client ID, requesting streaming scope (Credentials::with_access_token)
    • oauth flow access_token with Android client ID, requesting streaming scope (Credentials::with_access_token)
    • login5 flow with Android client ID
      let credentials = Credentials {
          username: Some(id),
          auth_type: AuthenticationType::AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS,
          auth_data, // stored_credentials from Login5Manager::login
      };
    Therefore the idea is to present ourselves as Linux in client_hello, additional spoofing isn't required, authenticate can still present ourselves as Android OS after that. With just this, playback from credentials obtained from Login5Manager::login is already working.
  2. In the case where on Android and user has an access_token that is obtained via Keymaster client ID, Login5Manager::login5_request must use the same Keymaster client ID to get an api access token for Track::get. Prior to this PR, on platforms such as Android/IOS where we have specific client IDs, this scenario always fails with InvalidCredentials.
  3. Fix one occurrence of arm64 when matching against std::env::consts::ARCH

If this is merged, to get playback on Android working, there's 2 ways.

  1. oauth flow using Keymaster client ID, when initializing Session, manually override SessionConfig::client_id to be Keymaster's client ID, this is the same flow as using librespot on desktop platforms. (example)
  2. login5 flow and use the stored_credentials returned from Login5Manager::login as credentials for Session::connect (example)

Additionally, from my testing, it appears that although we can initiate an oauth flow using Android client ID with scope streaming, I have never gotten it to work with Session::connect, PremiumAccountRequired error always appears during Session::connect, though in the first place, I couldn't find any references that mentioned this is possible anyways.

@photovoltex
Copy link
Member

Hey @SilverMira,
I think when fully mocking android (adjusting OS and os_version in config.rs) i achieved to get a token with the login5 login stored credentials. Did you test it on bare metal and it didn't work? Could you maybe check what the OS and os_version on bare metal return? Maybe the user-agent is still not quite correct or the os function reports something that is not expected or incorrect, which we could fix or adjust instead of mocking linux on android^^;

Even tho mocking linux on android wouldn't be so far off from the reality :D, thanks in advance

@SilverMira
Copy link
Contributor Author

SilverMira commented Nov 26, 2024

Hello @photovoltex, getting the token from login5 is not an issue, I can confirm Login5Manager::login can work on desktop platform if changed OS to "android" and os_version to "30" as per documented in the file itself without any other modifications (on bare metal without any modifications as well), we get back a stored_credentials in the tuple.

The problem happens when we try to connect to a Session using the obtained credentials, all 6 APs will error with TryAnotherAP. Here's how I've been using the login5 credentials, maybe I'm not using the credentials correctly (not sure either since there isn't much example of how to use the obtained credentials).

let session_config = SessionConfig::default();

let session = Session::new(session_config, None);
log::trace!("Authing with login5");
let (token, auth_data) = match session.login5().login(id.clone(), password).await {
    Ok(result) => result,
    Err(err) => {
        log::error!("Error while login5 auth: {err:?}");
        Err(err)?
    }
};

log::trace!("Login5 success: token={token:?} auth_data={auth_data:?}");
let credentials = Credentials {
    username: Some(id),
    auth_type: AuthenticationType::AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS,
    auth_data,
};
// This session.connect always fails with TryAnotherAP
if let Err(err) = session.connect(credentials, false).await {
    log::error!("Error connecting to librespot: {err:?}");
    Err(err)?;
};

What I'd found is that during Session::connect, in the initial handshake, if client_hello sends Platform::PLATFORM_ANDROID_ARM, all APs will reject with TryAnotherAP, and if the platform sent during that time is of Linux's, everything works as it should.

The trace when running the above snippet with no other modifications than OS = "android" and os_version = "30" on Windows host is as below, redacted some of the rustls logs and access tokens

[2024-11-26T01:52:35Z DEBUG librespot_core::login5] Received 1 challenges, solving...
[2024-11-26T01:52:35Z DEBUG librespot_core::login5] Solving hashcash took 0s 1759000ns
[2024-11-26T01:52:35Z DEBUG librespot_core::http_client] Requesting https://login5.spotify.com/v3/login
[2024-11-26T01:52:35Z TRACE rust_lib_frb_base::api::simple] Login5 success: token=Token { access_token: "---", expires_in: 3600s, token_type: "Bearer", scopes: [], timestamp: Instant { t: 1622.7673751s } } auth_data=[---]
[2024-11-26T01:52:35Z DEBUG librespot::component] new ApResolver
[2024-11-26T01:52:35Z DEBUG librespot_core::http_client] Requesting https://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient
[2024-11-26T01:52:35Z DEBUG rustls::client::hs] No cached session for DnsName("apresolve.spotify.com")
[2024-11-26T01:52:35Z DEBUG rustls::client::hs] Not resuming any session
[2024-11-26T01:52:35Z TRACE rustls::client::hs] Sending ClientHello Message {...}
[2024-11-26T01:52:36Z TRACE rustls::client::hs] We got ServerHello ServerHelloPayload {...}
[2024-11-26T01:52:36Z DEBUG rustls::client::hs] Using ciphersuite TLS13_AES_256_GCM_SHA384
[2024-11-26T01:52:36Z DEBUG rustls::client::tls13] Not resuming
[2024-11-26T01:52:36Z TRACE rustls::client::client_conn] EarlyData rejected
[2024-11-26T01:52:36Z TRACE rustls::conn] Dropping CCS
[2024-11-26T01:52:36Z DEBUG rustls::client::tls13] TLS1.3 encrypted extensions: [Protocols([ProtocolName(6832)])]
[2024-11-26T01:52:36Z DEBUG rustls::client::hs] ALPN protocol is Some(b"h2")
[2024-11-26T01:52:36Z TRACE rustls::client::tls13] Server cert is CertificateChain(...)
[2024-11-26T01:52:36Z INFO  librespot_core::session] Connecting to AP "ap-gae2.spotify.com:4070"
[2024-11-26T01:52:36Z DEBUG librespot_core::connection] Authenticating with AP using AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS
[2024-11-26T01:52:36Z WARN  librespot_core::session] Instructed to try another access point...
[2024-11-26T01:52:36Z INFO  librespot_core::session] Connecting to AP "ap-gae2.spotify.com:443"
[2024-11-26T01:52:36Z DEBUG librespot_core::connection] Authenticating with AP using AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS
[2024-11-26T01:52:36Z WARN  librespot_core::session] Instructed to try another access point...
[2024-11-26T01:52:36Z INFO  librespot_core::session] Connecting to AP "ap-gae2.spotify.com:80"
[2024-11-26T01:52:36Z DEBUG librespot_core::connection] Authenticating with AP using AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS
[2024-11-26T01:52:37Z WARN  librespot_core::session] Instructed to try another access point...
[2024-11-26T01:52:37Z INFO  librespot_core::session] Connecting to AP "ap-guc3.spotify.com:4070"
[2024-11-26T01:52:37Z DEBUG librespot_core::connection] Authenticating with AP using AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS
[2024-11-26T01:52:38Z WARN  librespot_core::session] Instructed to try another access point...
[2024-11-26T01:52:38Z INFO  librespot_core::session] Connecting to AP "ap-gue1.spotify.com:443"
[2024-11-26T01:52:38Z DEBUG librespot_core::connection] Authenticating with AP using AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS
[2024-11-26T01:52:38Z WARN  librespot_core::session] Instructed to try another access point...
[2024-11-26T01:52:38Z INFO  librespot_core::session] Connecting to AP "ap-gew4.spotify.com:80"
[2024-11-26T01:52:39Z DEBUG librespot_core::connection] Authenticating with AP using AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS
[2024-11-26T01:52:39Z ERROR librespot_core::session] Tried too many access points
[2024-11-26T01:52:39Z ERROR rust_lib_frb_base::api::simple] Error connecting to librespot: Error { kind: PermissionDenied, error: LoginFailed(TryAnotherAP) }

Also, I'm not sure why the config.rs suggests android os version 30 to be valid, because on bare metal, os_version returns "15" rather than the API level which is in the range of 30.

@photovoltex
Copy link
Member

Oh thanks for the clarification. Looking back i never tried to use the stored credentials from the login5 login to connect. What a bummer that it doesn't work.

Could you maybe document in code why we match both os android and linux for the linux platform, so it doesn't feel like a random decision?

Later down the line with the dealer and a different key retrieval for the tracks, we might even be able to completely avoid the handshake in the future on mobile. But for now we might just have to use a workaround.

@kingosticks
Copy link
Contributor

Can you connect to the access point if you do it the normal way, with an access token as per the docs, for example. I recall login5 works very differently on Android and I'm not sure how much of that got implemented, I didn't follow it

@SilverMira
Copy link
Contributor Author

SilverMira commented Nov 26, 2024

@photovoltex sure, will add a few lines there.

@kingosticks Yes, as per findings in this PR's description, I've tried with access token obtained via oauth with Keymaster's client ID (ie: the desktop oauth way), that too will error with TryAnotherAP during Session::connect. And access token obtained via oauth with Android's client ID, we'd get the same TryAnotherAP as well.

After getting past TryAnotherAP with 080aa3f, we'd still need b488b8f to get Keymaster's access token to work for playback on Android

It appears that a combination of `Platform::PLATFORM_ANDROID_ARM` and
connecting with `Credentials::with_access_token` causes all AP to error
TryAnotherAP
If we are trying to get an access token from login5 using stored
credentials, ie: from oauth flow, we should use the oauth's client ID,
this matches with the semantics as described in
`Login5Manager::auth_token`
@kingosticks
Copy link
Contributor

kingosticks commented Nov 26, 2024

What about with a token from https://open.spotify.com/get_access_token or https://developer.spotify.com/documentation/web-playback-sdk/tutorials/getting-starte

Does mobile actually still use Mercury (an access point connection)? That go example I provided is specifically for mobile, check that. oh, wait, that only gets an access token. Sorry, I've forgotten all this already.

@SilverMira
Copy link
Contributor Author

What about with a token from https://open.spotify.com/get_access_token or https://developer.spotify.com/documentation/web-playback-sdk/tutorials/getting-starte

I have just checked for access_tokens from both of them, TryAnotherAP as well.

Does mobile actually still use Mercury (an access point connection)? That second go example I provided is specifically for mobile, check that.

The go example is essentially Login5Manager::login isn't it? where you solve a hashcash challenge and login with just ID and password, and only works with Mobile's client ID, if it is then yes, I've tried that too, TryAnotherAP as well.

@photovoltex
Copy link
Member

Does mobile actually still use Mercury (an access point connection)?

From what i saw, I think it doesn't. But without the dealer and a different way to get the encryption keys for the tracks we still rely on it. At least thats my current understanding.

@kingosticks
Copy link
Contributor

Does mobile actually still use Mercury (an access point connection)?

From what i saw, I think it doesn't. But without the dealer and a different way to get the encryption keys for the tracks we still rely on it. At least thats my current understanding.

That certainly would explain things. In which case we either pretend not to be Android, or leave it broken. And then re-evaluate once all the non-Mercury pieces are in.

@photovoltex
Copy link
Member

It would probably be in favor of people using librespot on android to have a working version to stream audio. So I would say bringing these changes in is a good thing, as long as we document the decisions for it^^

@roderickvd
Copy link
Member

I agree. Would you write a CHANGELOG message before we merge this?

@roderickvd
Copy link
Member

Has a merge conflict now that I've merged your other change before 😅

@roderickvd roderickvd merged commit f646ef2 into librespot-org:dev Dec 5, 2024
1 of 2 checks passed
@SilverMira
Copy link
Contributor Author

No issues, I've just resolved it, kinda expected the changelog to have a conflict when I was writing for both the PRs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Certificate issues when connecting session in Android app
4 participants