azalea_auth/
sessionserver.rs1use 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
55pub 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 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
126pub 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 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}