mojang_api/
lib.rs

1//! A simple, easy-to-use library for interfacing with the Mojang API.
2//!
3//! All functions involving IO in this crate are asynchronous and utilize
4//! async/await. As a result, you will have to use nightly Rust until async/await
5//! is stabilized.
6//!
7//! This crate provides a number of functions:
8//!
9//! * Server-side authentication with the Mojang API, used to verify
10//! that clients have logged in correctly. This is available using the
11//! [`server_auth`](fn.server_auth.html) function.
12//! * Obtaining the "server hash" required for authentication, available using
13//! [`server_hash`](fn.server_hash.html). Since Mojang uses abnormal hash digests
14//! for obtaining the value, this crate provides a simple way to obtain it.
15//!
16//! # Examples
17//! Authenticating a client on a server:
18//! ```no_run
19//! # #[tokio::main]
20//! # async fn main() -> Result<(), mojang_api::Error> {
21//! # let shared_secret = [0; 16];
22//! # let username = "test";
23//! # let public_key = &[0];
24//!
25//! // Obtain the "server hash"
26//! let server_hash = mojang_api::server_hash(
27//!     "", // Note that the "server ID" is always an empty string
28//!     shared_secret,
29//!     public_key,
30//! );
31//!
32//! // Make the API request
33//! let response = mojang_api::server_auth(&server_hash, username).await?;
34//!
35//! // Now do something with it...
36//! # Ok(())
37//! # }
38//! ```
39
40#![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
59/// Result type used by this crate. This is equivalent
60/// to `std::result::Result<T, mojang_api::Error>`.
61pub type Result<T> = StdResult<T, Error>;
62
63lazy_static! {
64    static ref CLIENT_TOKEN: Uuid = Uuid::new_v4();
65}
66
67/// Error type for this crate.
68#[derive(Debug)]
69pub enum Error {
70    /// Indicates that an IO error occurred.
71    Io(io::Error),
72    /// Indicates that an HTTP error occurred.
73    Http(reqwest::Error),
74    /// Indicates that the UTF8 bytes failed to parse.
75    Utf8(FromUtf8Error),
76    /// Indicates that the response included malformed JSON.
77    /// This could also indicate that, for example, authentication
78    /// failed, because the response would have unexpected fields.
79    Json(serde_json::Error),
80    /// Client authentication returned an incorrect status code.
81    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/// Represents the response received when performing
116/// server-side authentication with the Mojang API.
117///
118/// The response includes the player's UUID, username,
119/// and optionally some `ProfileProperty`s, which may
120/// represent, for example, the player's skin.
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122pub struct ServerAuthResponse {
123    /// The UUID of the player.
124    pub id: Uuid,
125    /// The current username of the player.
126    pub name: String,
127    /// The player's profile properties.
128    #[serde(default)] // If none returned, use empty vector
129    pub properties: Vec<ProfileProperty>,
130}
131
132/// Represents a profile property returned in the server
133/// authentication request.
134///
135/// The most common profile property is called "textures"
136/// and contains the skin of the player.
137///
138/// Note that both `value` and `signature` are base64-encoded
139/// strings.
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141pub struct ProfileProperty {
142    /// The name of this profile property.
143    pub name: String,
144    /// The base64-encoded value of this profile property.
145    pub value: String,
146    /// The signature of this profile property, signed with Yggdrasil's private key.
147    pub signature: String,
148}
149
150/// Performs server-side authentication using the given server hash
151/// and username.
152///
153/// The server hash can be retrieved using [`server_hash`](fn.server_hash.html).
154/// Obtaining it requires the server's public RSA key and the secret key
155/// being used for encryption with the client.
156///
157/// Performing this request also requires the client's username.
158/// Servers should use the value sent in the Login Start packet.
159///
160/// The request is performed asynchronously, and this function is `async`.
161///
162/// See [wiki.vg](https://wiki.vg/Protocol_Encryption#Server) for more
163/// information.
164///
165/// # Examples
166/// ```no_run
167/// # #[tokio::main]
168/// # async fn main() -> Result<(), mojang_api::Error> {
169/// # fn server_hash() -> String { "".to_string() }
170/// # fn username() -> String { "".to_string() }
171/// // Obtain the server hash and username...
172/// let hash = server_hash();
173/// let username = username();
174///
175/// // Make the API request
176/// let response = mojang_api::server_auth(&hash, &username).await?;
177/// # Ok(())
178/// # }
179/// ```
180pub 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
205/// Computes the "server hash" required for authentication
206/// based on the server ID, the shared secret used for
207/// communication with the client, and the server's
208/// public RSA key.
209///
210/// On modern Minecraft versions, the server ID
211/// is always an empty string.
212///
213/// # Examples
214/// ```
215/// # fn shared_secret() -> [u8; 16] { [0; 16] }
216/// # fn pub_key() -> &'static [u8] { &[1] }
217/// let hash = mojang_api::server_hash("", shared_secret(), pub_key());
218/// ```
219pub 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
228/// Generates a digest for the given hasher using
229/// Minecraft's unorthodox hex digestion method.
230///
231/// # Examples
232/// ```
233/// use sha1::Sha1;
234/// let mut hasher = Sha1::new();
235/// hasher.update(b"Notch");
236/// assert_eq!(
237///    mojang_api::hexdigest(&hasher),
238///    "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
239/// );
240/// ```
241pub 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/// Represents the response received from the client authentication endpoint.
249///
250/// The response includes an access token, used for client-side authentication,
251/// as well as information about the user which was authenticated.
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
253#[serde(rename_all = "camelCase")]
254pub struct ClientLoginResponse {
255    /// The access token which can later be used for client-side authentication
256    /// when logging into a server.
257    pub access_token: String,
258    /// Contains information about the user which authenticated.
259    pub user: User,
260    /// Contains information about the user profile.
261    #[serde(rename = "selectedProfile")]
262    pub profile: SelectedProfile,
263}
264
265/// Information about a user's profile, including their name, UUID, etc. Similar to `User`.
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
267pub struct SelectedProfile {
268    /// UUID.
269    #[serde(rename = "id")]
270    pub uuid: Uuid,
271    /// Username.
272    pub name: String,
273}
274
275/// Information about a user, including UUID, email, username, etc.
276#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
277#[serde(rename_all = "camelCase")]
278pub struct User {
279    /// User ID.
280    pub id: String,
281    /// User's email.
282    pub email: Option<String>,
283    /// Username.
284    pub username: String,
285    // TODO: properties
286}
287
288/// Authenticates a user, returning a client access token and metadata for the user.
289///
290/// The returned access token can later be used with `client_auth` to log in to a server.
291///
292/// # Examples
293/// ```no_run
294/// # #[tokio::main]
295/// # async fn main() -> mojang_api::Result<()> {
296/// let response: mojang_api::ClientLoginResponse = mojang_api::client_login("username", "password").await?;
297/// println!("Access token: {}", response.access_token);
298/// println!("User email: {}", response.user.email);
299/// # Ok(())
300/// # }
301/// ```
302pub 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
337/// Performs client-side authentication with the given access
338/// token and server hash.
339///
340/// The access token can be obtained using `client_login`;
341/// the server hash can be computed with `server_hash`.
342///
343/// This API endpoint returns no response. If all goes well,
344/// then no error will be returned, and the client can proceed
345/// with the login process.
346///
347/// # Examples
348/// ```no_run
349/// # #[tokio::main] async fn main() -> mojang_api::Result<()> {
350/// let login = mojang_api::client_login("username", "password").await?;
351/// let server_hash = mojang_api::server_hash("", [0u8; 16], &[1]);
352///
353/// mojang_api::client_auth(&login.access_token, login.profile.uuid, &server_hash);
354/// # Ok(())
355/// # }
356/// ```
357pub 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        // Examples from wiki.vg
412        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}