#![forbid(unsafe_code, missing_docs, missing_debug_implementations, warnings)]
#![doc(html_root_url = "https://docs.rs/mojang-api/0.6.1")]
use lazy_static::lazy_static;
use log::trace;
use num_bigint::BigInt;
use reqwest::header::CONTENT_TYPE;
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha1::Sha1;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
use std::string::FromUtf8Error;
use uuid::Uuid;
type StdResult<T, E> = std::result::Result<T, E>;
pub type Result<T> = StdResult<T, Error>;
lazy_static! {
static ref CLIENT_TOKEN: Uuid = Uuid::new_v4();
}
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Http(reqwest::Error),
Utf8(FromUtf8Error),
Json(serde_json::Error),
ClientAuthFailure(String, u32),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> StdResult<(), fmt::Error> {
match self {
Error::Io(e) => write!(f, "{}", e)?,
Error::Http(e) => write!(f, "{}", e)?,
Error::Utf8(e) => write!(f, "{}", e)?,
Error::Json(e) => write!(f, "{}", e)?,
Error::ClientAuthFailure(body, code) => write!(
f,
"client authentication did not return OK: body {}, response code {}",
body, code
)?,
}
Ok(())
}
}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Error::Io(e1), Error::Io(e2)) => e1.to_string() == e2.to_string(),
(Error::Http(e1), Error::Http(e2)) => e1.to_string() == e2.to_string(),
(Error::Utf8(e1), Error::Utf8(e2)) => e1.to_string() == e2.to_string(),
(Error::Json(e1), Error::Json(e2)) => e1.to_string() == e2.to_string(),
_ => false,
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ServerAuthResponse {
pub id: Uuid,
pub name: String,
#[serde(default)] pub properties: Vec<ProfileProperty>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProfileProperty {
pub name: String,
pub value: String,
pub signature: String,
}
pub async fn server_auth(server_hash: &str, username: &str) -> Result<ServerAuthResponse> {
#[cfg(not(test))]
let url = format!(
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username={}&serverId={}&unsigned=false",
username, server_hash
);
#[cfg(test)]
let url = format!("{}/{}/{}", mockito::server_url(), username, server_hash,);
let string = Client::new()
.get(&url)
.send()
.await
.map_err(Error::Http)?
.text()
.await
.map_err(Error::Http)?;
trace!("Authentication response: {}", string);
let response = serde_json::from_str(&string).map_err(Error::Json)?;
Ok(response)
}
pub fn server_hash(server_id: &str, shared_secret: [u8; 16], pub_key: &[u8]) -> String {
let mut hasher = Sha1::new();
hasher.update(server_id.as_bytes());
hasher.update(&shared_secret);
hasher.update(pub_key);
hexdigest(&hasher)
}
pub fn hexdigest(hasher: &Sha1) -> String {
let output = hasher.digest().bytes();
let bigint = BigInt::from_signed_bytes_be(&output);
format!("{:x}", bigint)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ClientLoginResponse {
pub access_token: String,
pub user: User,
#[serde(rename = "selectedProfile")]
pub profile: SelectedProfile,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SelectedProfile {
#[serde(rename = "id")]
pub uuid: Uuid,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub id: String,
pub email: Option<String>,
pub username: String,
}
pub async fn client_login(username: &str, password: &str) -> Result<ClientLoginResponse> {
#[cfg(test)]
let url = format!("{}/authenticate", mockito::server_url());
#[cfg(not(test))]
let url = String::from("https://authserver.mojang.com/authenticate");
let client_token = *CLIENT_TOKEN;
let payload = json!({
"agent": {
"name": "Minecraft",
"version": 1
},
"username": username,
"password": password,
"clientToken": client_token,
"requestUser": true
})
.to_string();
let client = Client::new();
let response = client
.post(&url)
.header(CONTENT_TYPE, "application/json")
.body(payload)
.send()
.await
.map_err(Error::Http)?
.text()
.await
.map_err(Error::Http)?;
serde_json::from_str(&response).map_err(Error::Json)
}
pub async fn client_auth(access_token: &str, uuid: Uuid, server_hash: &str) -> Result<()> {
#[cfg(not(test))]
let url = String::from("https://sessionserver.mojang.com/session/minecraft/join");
#[cfg(test)]
let url = mockito::server_url();
let selected_profile = uuid.to_simple().to_string();
let payload = json!({
"accessToken": access_token,
"selectedProfile": selected_profile,
"serverId": server_hash
});
let client = Client::new();
let response = client
.post(&url)
.header(CONTENT_TYPE, "application/json")
.body(payload.to_string())
.send()
.await
.map_err(Error::Http)?;
let status = response.status();
if status != StatusCode::NO_CONTENT {
return Err(Error::ClientAuthFailure(
response.text().await.map_err(Error::Http)?,
status.as_u16() as u32,
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::ErrorKind;
use uuid::Uuid;
#[test]
fn test_error_equality() {
assert_eq!(
Error::Io(io::Error::new(ErrorKind::NotFound, "Test error")),
Error::Io(io::Error::new(ErrorKind::NotFound, "Test error"))
);
assert_ne!(
Error::Io(io::Error::new(ErrorKind::NotFound, "Test error")),
Error::Io(io::Error::new(ErrorKind::NotFound, "Different test error"))
);
}
#[test]
fn test_hexdigest() {
let mut hasher = Sha1::new();
hasher.update(b"Notch");
assert_eq!(
hexdigest(&hasher),
"4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
);
let mut hasher = Sha1::new();
hasher.update(b"jeb_");
assert_eq!(
hexdigest(&hasher),
"-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"
);
let mut hasher = Sha1::new();
hasher.update(b"simon");
assert_eq!(
hexdigest(&hasher),
"88e16a1019277b15d58faf0541e11910eb756f6"
);
}
#[tokio::test]
async fn test_server_auth() -> Result<()> {
let uuid = Uuid::new_v4();
let username = "test_";
let prop_name = "test_prop";
let prop_val = "test_val";
let prop_signature = "jioiodqwqiowoiqf";
let prop = ProfileProperty {
name: prop_name.to_string(),
value: prop_val.to_string(),
signature: prop_signature.to_string(),
};
let response = ServerAuthResponse {
name: username.to_string(),
id: uuid,
properties: vec![prop],
};
println!("{}", serde_json::to_string(&response).unwrap());
let hash = server_hash("", [0; 16], &[0]);
let _m = mockito::mock("GET", format!("/{}/{}", username, hash).as_str())
.with_body(serde_json::to_string(&response).unwrap())
.create();
let result = server_auth(&hash, username).await?;
assert_eq!(result.id, uuid);
assert_eq!(result.name, username);
assert_eq!(result.properties.len(), 1);
let prop = result.properties.first().unwrap();
assert_eq!(prop.name, prop_name);
assert_eq!(prop.value, prop_val);
assert_eq!(prop.signature, prop_signature);
Ok(())
}
#[tokio::test]
async fn test_client_login() {
let expected_response = ClientLoginResponse {
access_token: String::from("test_29408"),
user: User {
id: Uuid::new_v4().to_string(),
email: Some("test@example.com".to_string()),
username: "test".to_string(),
},
profile: SelectedProfile {
uuid: Default::default(),
name: "".to_string(),
},
};
let _m = mockito::mock("POST", "/authenticate")
.with_body(serde_json::to_string(&expected_response).unwrap())
.create();
let response = client_login("test", "password").await.unwrap();
assert_eq!(response, expected_response);
}
}