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