1#![forbid(unsafe_code, missing_docs, missing_debug_implementations, warnings)]
41#![doc(html_root_url = "https://docs.rs/mojang-api/0.6.1")]
42
43use lazy_static::lazy_static;
44use log::trace;
45use num_bigint::BigInt;
46use reqwest::header::CONTENT_TYPE;
47use reqwest::{Client, StatusCode};
48use serde::{Deserialize, Serialize};
49use serde_json::json;
50use sha1::Sha1;
51use std::fmt;
52use std::fmt::{Display, Formatter};
53use std::io;
54use std::string::FromUtf8Error;
55use uuid::Uuid;
56
57type StdResult<T, E> = std::result::Result<T, E>;
58
59pub type Result<T> = StdResult<T, Error>;
62
63lazy_static! {
64 static ref CLIENT_TOKEN: Uuid = Uuid::new_v4();
65}
66
67#[derive(Debug)]
69pub enum Error {
70 Io(io::Error),
72 Http(reqwest::Error),
74 Utf8(FromUtf8Error),
76 Json(serde_json::Error),
80 ClientAuthFailure(String, u32),
82}
83
84impl Display for Error {
85 fn fmt(&self, f: &mut Formatter) -> StdResult<(), fmt::Error> {
86 match self {
87 Error::Io(e) => write!(f, "{}", e)?,
88 Error::Http(e) => write!(f, "{}", e)?,
89 Error::Utf8(e) => write!(f, "{}", e)?,
90 Error::Json(e) => write!(f, "{}", e)?,
91 Error::ClientAuthFailure(body, code) => write!(
92 f,
93 "client authentication did not return OK: body {}, response code {}",
94 body, code
95 )?,
96 }
97 Ok(())
98 }
99}
100
101impl PartialEq for Error {
102 fn eq(&self, other: &Self) -> bool {
103 match (self, other) {
104 (Error::Io(e1), Error::Io(e2)) => e1.to_string() == e2.to_string(),
105 (Error::Http(e1), Error::Http(e2)) => e1.to_string() == e2.to_string(),
106 (Error::Utf8(e1), Error::Utf8(e2)) => e1.to_string() == e2.to_string(),
107 (Error::Json(e1), Error::Json(e2)) => e1.to_string() == e2.to_string(),
108 _ => false,
109 }
110 }
111}
112
113impl std::error::Error for Error {}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122pub struct ServerAuthResponse {
123 pub id: Uuid,
125 pub name: String,
127 #[serde(default)] pub properties: Vec<ProfileProperty>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141pub struct ProfileProperty {
142 pub name: String,
144 pub value: String,
146 pub signature: String,
148}
149
150pub async fn server_auth(server_hash: &str, username: &str) -> Result<ServerAuthResponse> {
181 #[cfg(not(test))]
182 let url = format!(
183 "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={}&serverId={}&unsigned=false",
184 username, server_hash
185 );
186 #[cfg(test)]
187 let url = format!("{}/{}/{}", mockito::server_url(), username, server_hash,);
188
189 let string = Client::new()
190 .get(&url)
191 .send()
192 .await
193 .map_err(Error::Http)?
194 .text()
195 .await
196 .map_err(Error::Http)?;
197
198 trace!("Authentication response: {}", string);
199
200 let response = serde_json::from_str(&string).map_err(Error::Json)?;
201
202 Ok(response)
203}
204
205pub fn server_hash(server_id: &str, shared_secret: [u8; 16], pub_key: &[u8]) -> String {
220 let mut hasher = Sha1::new();
221 hasher.update(server_id.as_bytes());
222 hasher.update(&shared_secret);
223 hasher.update(pub_key);
224
225 hexdigest(&hasher)
226}
227
228pub fn hexdigest(hasher: &Sha1) -> String {
242 let output = hasher.digest().bytes();
243
244 let bigint = BigInt::from_signed_bytes_be(&output);
245 format!("{:x}", bigint)
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
253#[serde(rename_all = "camelCase")]
254pub struct ClientLoginResponse {
255 pub access_token: String,
258 pub user: User,
260 #[serde(rename = "selectedProfile")]
262 pub profile: SelectedProfile,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
267pub struct SelectedProfile {
268 #[serde(rename = "id")]
270 pub uuid: Uuid,
271 pub name: String,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
277#[serde(rename_all = "camelCase")]
278pub struct User {
279 pub id: String,
281 pub email: Option<String>,
283 pub username: String,
285 }
287
288pub async fn client_login(username: &str, password: &str) -> Result<ClientLoginResponse> {
303 #[cfg(test)]
304 let url = format!("{}/authenticate", mockito::server_url());
305 #[cfg(not(test))]
306 let url = String::from("https://authserver.mojang.com/authenticate");
307
308 let client_token = *CLIENT_TOKEN;
309
310 let payload = json!({
311 "agent": {
312 "name": "Minecraft",
313 "version": 1
314 },
315 "username": username,
316 "password": password,
317 "clientToken": client_token,
318 "requestUser": true
319 })
320 .to_string();
321
322 let client = Client::new();
323 let response = client
324 .post(&url)
325 .header(CONTENT_TYPE, "application/json")
326 .body(payload)
327 .send()
328 .await
329 .map_err(Error::Http)?
330 .text()
331 .await
332 .map_err(Error::Http)?;
333
334 serde_json::from_str(&response).map_err(Error::Json)
335}
336
337pub async fn client_auth(access_token: &str, uuid: Uuid, server_hash: &str) -> Result<()> {
358 #[cfg(not(test))]
359 let url = String::from("https://sessionserver.mojang.com/session/minecraft/join");
360 #[cfg(test)]
361 let url = mockito::server_url();
362
363 let selected_profile = uuid.to_simple().to_string();
364
365 let payload = json!({
366 "accessToken": access_token,
367 "selectedProfile": selected_profile,
368 "serverId": server_hash
369 });
370
371 let client = Client::new();
372 let response = client
373 .post(&url)
374 .header(CONTENT_TYPE, "application/json")
375 .body(payload.to_string())
376 .send()
377 .await
378 .map_err(Error::Http)?;
379
380 let status = response.status();
381 if status != StatusCode::NO_CONTENT {
382 return Err(Error::ClientAuthFailure(
383 response.text().await.map_err(Error::Http)?,
384 status.as_u16() as u32,
385 ));
386 }
387
388 Ok(())
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use std::io::ErrorKind;
395 use uuid::Uuid;
396
397 #[test]
398 fn test_error_equality() {
399 assert_eq!(
400 Error::Io(io::Error::new(ErrorKind::NotFound, "Test error")),
401 Error::Io(io::Error::new(ErrorKind::NotFound, "Test error"))
402 );
403 assert_ne!(
404 Error::Io(io::Error::new(ErrorKind::NotFound, "Test error")),
405 Error::Io(io::Error::new(ErrorKind::NotFound, "Different test error"))
406 );
407 }
408
409 #[test]
410 fn test_hexdigest() {
411 let mut hasher = Sha1::new();
413 hasher.update(b"Notch");
414 assert_eq!(
415 hexdigest(&hasher),
416 "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
417 );
418
419 let mut hasher = Sha1::new();
420 hasher.update(b"jeb_");
421 assert_eq!(
422 hexdigest(&hasher),
423 "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"
424 );
425
426 let mut hasher = Sha1::new();
427 hasher.update(b"simon");
428 assert_eq!(
429 hexdigest(&hasher),
430 "88e16a1019277b15d58faf0541e11910eb756f6"
431 );
432 }
433
434 #[tokio::test]
435 async fn test_server_auth() -> Result<()> {
436 let uuid = Uuid::new_v4();
437 let username = "test_";
438 let prop_name = "test_prop";
439 let prop_val = "test_val";
440 let prop_signature = "jioiodqwqiowoiqf";
441
442 let prop = ProfileProperty {
443 name: prop_name.to_string(),
444 value: prop_val.to_string(),
445 signature: prop_signature.to_string(),
446 };
447
448 let response = ServerAuthResponse {
449 name: username.to_string(),
450 id: uuid,
451 properties: vec![prop],
452 };
453
454 println!("{}", serde_json::to_string(&response).unwrap());
455
456 let hash = server_hash("", [0; 16], &[0]);
457 let _m = mockito::mock("GET", format!("/{}/{}", username, hash).as_str())
458 .with_body(serde_json::to_string(&response).unwrap())
459 .create();
460
461 let result = server_auth(&hash, username).await?;
462
463 assert_eq!(result.id, uuid);
464 assert_eq!(result.name, username);
465 assert_eq!(result.properties.len(), 1);
466
467 let prop = result.properties.first().unwrap();
468
469 assert_eq!(prop.name, prop_name);
470 assert_eq!(prop.value, prop_val);
471 assert_eq!(prop.signature, prop_signature);
472
473 Ok(())
474 }
475
476 #[tokio::test]
477 async fn test_client_login() {
478 let expected_response = ClientLoginResponse {
479 access_token: String::from("test_29408"),
480 user: User {
481 id: Uuid::new_v4().to_string(),
482 email: Some("test@example.com".to_string()),
483 username: "test".to_string(),
484 },
485 profile: SelectedProfile {
486 uuid: Default::default(),
487 name: "".to_string(),
488 },
489 };
490
491 let _m = mockito::mock("POST", "/authenticate")
492 .with_body(serde_json::to_string(&expected_response).unwrap())
493 .create();
494
495 let response = client_login("test", "password").await.unwrap();
496
497 assert_eq!(response, expected_response);
498 }
499}