azalea_auth/
sessionserver.rs

1//! Tell Mojang you're joining a multiplayer server.
2use std::sync::LazyLock;
3
4use reqwest::StatusCode;
5use serde::Deserialize;
6use serde_json::json;
7use thiserror::Error;
8use tracing::debug;
9use uuid::Uuid;
10
11use crate::game_profile::{GameProfile, SerializableGameProfile};
12
13#[derive(Debug, Error)]
14pub enum ClientSessionServerError {
15    #[error("Error sending HTTP request to sessionserver: {0}")]
16    HttpError(#[from] reqwest::Error),
17    #[error("Multiplayer is not enabled for this account")]
18    MultiplayerDisabled,
19    #[error("This account has been banned from multiplayer")]
20    Banned,
21    #[error("The authentication servers are currently not reachable")]
22    AuthServersUnreachable,
23    #[error("Invalid or expired session")]
24    InvalidSession,
25    #[error("Unknown sessionserver error: {0}")]
26    Unknown(String),
27    #[error("Forbidden operation (expired session?)")]
28    ForbiddenOperation,
29    #[error("RateLimiter disallowed request")]
30    RateLimited,
31    #[error("Unexpected response from sessionserver (status code {status_code}): {body}")]
32    UnexpectedResponse { status_code: u16, body: String },
33}
34
35#[derive(Debug, Error)]
36pub enum ServerSessionServerError {
37    #[error("Error sending HTTP request to sessionserver: {0}")]
38    HttpError(#[from] reqwest::Error),
39    #[error("Invalid or expired session")]
40    InvalidSession,
41    #[error("Unexpected response from sessionserver (status code {status_code}): {body}")]
42    UnexpectedResponse { status_code: u16, body: String },
43    #[error("Unknown sessionserver error: {0}")]
44    Unknown(String),
45}
46
47#[derive(Deserialize)]
48pub struct ForbiddenError {
49    pub error: String,
50    pub path: String,
51}
52
53static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(reqwest::Client::new);
54
55/// Tell Mojang's servers that you are going to join a multiplayer server,
56/// which is required to join online-mode servers. The server ID is an empty
57/// string.
58pub async fn join(
59    access_token: &str,
60    public_key: &[u8],
61    private_key: &[u8],
62    uuid: &Uuid,
63    server_id: &str,
64) -> Result<(), ClientSessionServerError> {
65    let client = REQWEST_CLIENT.clone();
66
67    let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
68        server_id.as_bytes(),
69        public_key,
70        private_key,
71    ));
72
73    join_with_server_id_hash(&client, access_token, uuid, &server_hash).await
74}
75
76pub async fn join_with_server_id_hash(
77    client: &reqwest::Client,
78    access_token: &str,
79    uuid: &Uuid,
80    server_hash: &str,
81) -> Result<(), ClientSessionServerError> {
82    let mut encode_buffer = Uuid::encode_buffer();
83    let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer);
84
85    let data = json!({
86        "accessToken": access_token,
87        "selectedProfile": undashed_uuid,
88        "serverId": server_hash
89    });
90    let res = client
91        .post("https://sessionserver.mojang.com/session/minecraft/join")
92        .json(&data)
93        .send()
94        .await?;
95
96    match res.status() {
97        StatusCode::NO_CONTENT => Ok(()),
98        StatusCode::FORBIDDEN => {
99            let forbidden = res.json::<ForbiddenError>().await?;
100            match forbidden.error.as_str() {
101                "InsufficientPrivilegesException" => {
102                    Err(ClientSessionServerError::MultiplayerDisabled)
103                }
104                "UserBannedException" => Err(ClientSessionServerError::Banned),
105                "AuthenticationUnavailableException" => {
106                    Err(ClientSessionServerError::AuthServersUnreachable)
107                }
108                "InvalidCredentialsException" => Err(ClientSessionServerError::InvalidSession),
109                "ForbiddenOperationException" => Err(ClientSessionServerError::ForbiddenOperation),
110                _ => Err(ClientSessionServerError::Unknown(forbidden.error)),
111            }
112        }
113        StatusCode::TOO_MANY_REQUESTS => Err(ClientSessionServerError::RateLimited),
114        status_code => {
115            // log the headers
116            debug!("Error headers: {:#?}", res.headers());
117            let body = res.text().await?;
118            Err(ClientSessionServerError::UnexpectedResponse {
119                status_code: status_code.as_u16(),
120                body,
121            })
122        }
123    }
124}
125
126/// Ask Mojang's servers if the player joining is authenticated.
127/// Included in the reply is the player's skin and cape.
128/// The IP field is optional and equivalent to enabling
129/// 'prevent-proxy-connections' in server.properties
130pub async fn serverside_auth(
131    username: &str,
132    public_key: &[u8],
133    private_key: &[u8; 16],
134    ip: Option<&str>,
135) -> Result<GameProfile, ServerSessionServerError> {
136    let hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
137        "".as_bytes(),
138        public_key,
139        private_key,
140    ));
141
142    let url = reqwest::Url::parse_with_params(
143        "https://sessionserver.mojang.com/session/minecraft/hasJoined",
144        if let Some(ip) = ip {
145            vec![("username", username), ("serverId", &hash), ("ip", ip)]
146        } else {
147            vec![("username", username), ("serverId", &hash)]
148        },
149    )
150    .expect("URL should always be valid");
151
152    let res = reqwest::get(url).await?;
153
154    match res.status() {
155        StatusCode::OK => {}
156        StatusCode::NO_CONTENT => {
157            return Err(ServerSessionServerError::InvalidSession);
158        }
159        StatusCode::FORBIDDEN => {
160            return Err(ServerSessionServerError::Unknown(
161                res.json::<ForbiddenError>().await?.error,
162            ))
163        }
164        status_code => {
165            // log the headers
166            debug!("Error headers: {:#?}", res.headers());
167            let body = res.text().await?;
168            return Err(ServerSessionServerError::UnexpectedResponse {
169                status_code: status_code.as_u16(),
170                body,
171            });
172        }
173    };
174
175    Ok(res.json::<SerializableGameProfile>().await?.into())
176}