aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEightFactorial <murphkev000@gmail.com>2023-01-21 20:14:23 -0800
committerGitHub <noreply@github.com>2023-01-21 22:14:23 -0600
commit9ee5e71bb13e596248fde000d8717c86276b0ce1 (patch)
treebd6363af53bca9bbd3dede1c7ee59615b94eb107
parent1059afa6fcf8b2776fd25dac07ed2e76ab48bed3 (diff)
downloadazalea-drasl-9ee5e71bb13e596248fde000d8717c86276b0ce1.tar.xz
Server functions and proxy example (#59)
* A couple useful things for servers * Add proxy example * Use Uuid's serde feature * Add const options to proxy example * Example crates go in dev-dependencies * Warn instead of error * Log address on login * Requested changes * add a test for deserializing game profile + random small changes Co-authored-by: mat <github@matdoes.dev>
-rw-r--r--Cargo.lock172
-rw-r--r--azalea-auth/Cargo.toml8
-rwxr-xr-xazalea-auth/src/auth.rs4
-rwxr-xr-xazalea-auth/src/game_profile.rs98
-rwxr-xr-xazalea-auth/src/sessionserver.rs94
-rwxr-xr-xazalea-client/src/account.rs4
-rwxr-xr-xazalea-client/src/chat.rs4
-rw-r--r--azalea-client/src/client.rs8
-rw-r--r--azalea-protocol/Cargo.toml6
-rw-r--r--azalea-protocol/examples/handshake_proxy.rs225
-rwxr-xr-xazalea-protocol/src/connect.rs69
-rw-r--r--azalea/Cargo.toml4
12 files changed, 655 insertions, 41 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9b655ab0..c47a3e25 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -97,7 +97,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
"libc",
"winapi",
]
@@ -121,7 +121,7 @@ dependencies = [
"azalea-physics",
"azalea-protocol",
"azalea-world",
- "env_logger",
+ "env_logger 0.10.0",
"futures",
"log",
"nohash-hasher",
@@ -140,7 +140,7 @@ dependencies = [
"azalea-buf",
"azalea-crypto",
"chrono",
- "env_logger",
+ "env_logger 0.9.3",
"log",
"num-bigint",
"reqwest",
@@ -300,6 +300,7 @@ dependencies = [
name = "azalea-protocol"
version = "0.5.0"
dependencies = [
+ "anyhow",
"async-compression",
"async-recursion",
"azalea-auth",
@@ -319,11 +320,14 @@ dependencies = [
"futures",
"futures-util",
"log",
+ "once_cell",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-util",
+ "tracing",
+ "tracing-subscriber",
"trust-dns-resolver",
"uuid",
]
@@ -415,7 +419,7 @@ dependencies = [
"anyhow",
"azalea",
"azalea-protocol",
- "env_logger",
+ "env_logger 0.9.3",
"parking_lot",
"rand",
"tokio",
@@ -712,6 +716,40 @@ dependencies = [
]
[[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
name = "fastrand"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -929,6 +967,15 @@ dependencies = [
]
[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "http"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1055,12 +1102,34 @@ dependencies = [
]
[[package]]
+name = "io-lifetimes"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
+dependencies = [
+ "libc",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
name = "ipnet"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745"
[[package]]
+name = "is-terminal"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
+dependencies = [
+ "hermit-abi 0.2.6",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1109,6 +1178,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+
+[[package]]
name = "lock_api"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1218,6 +1293,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
name = "num"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1310,7 +1395,7 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
"libc",
]
@@ -1325,9 +1410,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.16.0"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "oorandom"
@@ -1381,6 +1466,12 @@ dependencies = [
]
[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1651,6 +1742,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
+name = "rustix"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1769,6 +1874,15 @@ dependencies = [
]
[[package]]
+name = "sharded-slab"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1889,6 +2003,15 @@ dependencies = [
]
[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2004,6 +2127,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
]
[[package]]
@@ -2110,6 +2259,15 @@ name = "uuid"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
diff --git a/azalea-auth/Cargo.toml b/azalea-auth/Cargo.toml
index deffe62d..5de305d5 100644
--- a/azalea-auth/Cargo.toml
+++ b/azalea-auth/Cargo.toml
@@ -9,8 +9,8 @@ version = "0.5.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-azalea-buf = {path = "../azalea-buf", version = "^0.5.0" }
-azalea-crypto = {path = "../azalea-crypto", version = "^0.5.0" }
+azalea-buf = {path = "../azalea-buf", version = "^0.5.0"}
+azalea-crypto = {path = "../azalea-crypto", version = "^0.5.0"}
chrono = {version = "0.4.22", default-features = false}
log = "0.4.17"
num-bigint = "0.4.3"
@@ -19,8 +19,8 @@ serde = {version = "1.0.145", features = ["derive"]}
serde_json = "1.0.86"
thiserror = "1.0.37"
tokio = {version = "1.23.1", features = ["fs"]}
-uuid = "^1.1.2"
+uuid = {version = "^1.1.2", features = ["serde"]}
[dev-dependencies]
-env_logger = "0.9.1"
+env_logger = "0.9.3"
tokio = {version = "1.23.1", features = ["full"]}
diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/auth.rs
index bed15d74..e668a947 100755
--- a/azalea-auth/src/auth.rs
+++ b/azalea-auth/src/auth.rs
@@ -10,6 +10,7 @@ use std::{
time::{Instant, SystemTime, UNIX_EPOCH},
};
use thiserror::Error;
+use uuid::Uuid;
#[derive(Default)]
pub struct AuthOpts {
@@ -209,8 +210,7 @@ pub struct GameOwnershipItem {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProfileResponse {
- // todo: make the id a uuid
- pub id: String,
+ pub id: Uuid,
pub name: String,
pub skins: Vec<serde_json::Value>,
pub capes: Vec<serde_json::Value>,
diff --git a/azalea-auth/src/game_profile.rs b/azalea-auth/src/game_profile.rs
index 39cd29e7..bdd6cda5 100755
--- a/azalea-auth/src/game_profile.rs
+++ b/azalea-auth/src/game_profile.rs
@@ -1,8 +1,9 @@
use azalea_buf::McBuf;
+use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
-#[derive(McBuf, Debug, Clone, Default)]
+#[derive(McBuf, Debug, Clone, Default, Eq, PartialEq)]
pub struct GameProfile {
pub uuid: Uuid,
pub name: String,
@@ -19,8 +20,101 @@ impl GameProfile {
}
}
-#[derive(McBuf, Debug, Clone)]
+impl From<SerializableGameProfile> for GameProfile {
+ fn from(value: SerializableGameProfile) -> Self {
+ let mut properties = HashMap::new();
+ for value in value.properties {
+ properties.insert(
+ value.name,
+ ProfilePropertyValue {
+ value: value.value,
+ signature: value.signature,
+ },
+ );
+ }
+ Self {
+ uuid: value.id,
+ name: value.name,
+ properties,
+ }
+ }
+}
+
+#[derive(McBuf, Debug, Clone, Eq, PartialEq)]
pub struct ProfilePropertyValue {
pub value: String,
pub signature: Option<String>,
}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SerializableGameProfile {
+ pub id: Uuid,
+ pub name: String,
+ pub properties: Vec<SerializableProfilePropertyValue>,
+}
+
+impl From<GameProfile> for SerializableGameProfile {
+ fn from(value: GameProfile) -> Self {
+ let mut properties = Vec::new();
+ for (key, value) in value.properties {
+ properties.push(SerializableProfilePropertyValue {
+ name: key,
+ value: value.value,
+ signature: value.signature,
+ });
+ }
+ Self {
+ id: value.uuid,
+ name: value.name,
+ properties,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SerializableProfilePropertyValue {
+ pub name: String,
+ pub value: String,
+ pub signature: Option<String>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_deserialize_game_profile() {
+ let json = r#"{
+ "id": "f1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6",
+ "name": "Notch",
+ "properties": [
+ {
+ "name": "qwer",
+ "value": "asdf",
+ "signature": "zxcv"
+ }
+ ]
+ }"#;
+ let profile = GameProfile::from(
+ serde_json::from_str::<SerializableProfilePropertyValue>(json).unwrap(),
+ );
+ assert_eq!(
+ profile,
+ GameProfile {
+ uuid: Uuid::parse_str("f1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6").unwrap(),
+ name: "Notch".to_string(),
+ properties: {
+ let mut map = HashMap::new();
+ map.insert(
+ "asdf".to_string(),
+ ProfilePropertyValue {
+ value: "qwer".to_string(),
+ signature: Some("zxcv".to_string()),
+ },
+ );
+ map
+ },
+ }
+ );
+ }
+}
diff --git a/azalea-auth/src/sessionserver.rs b/azalea-auth/src/sessionserver.rs
index 685cfa25..502ae098 100755
--- a/azalea-auth/src/sessionserver.rs
+++ b/azalea-auth/src/sessionserver.rs
@@ -1,11 +1,15 @@
//! Tell Mojang you're joining a multiplayer server.
+use log::debug;
+use reqwest::StatusCode;
use serde::Deserialize;
use serde_json::json;
use thiserror::Error;
use uuid::Uuid;
+use crate::game_profile::{GameProfile, SerializableGameProfile};
+
#[derive(Debug, Error)]
-pub enum SessionServerError {
+pub enum ClientSessionServerError {
#[error("Error sending HTTP request to sessionserver: {0}")]
HttpError(#[from] reqwest::Error),
#[error("Multiplayer is not enabled for this account")]
@@ -24,6 +28,18 @@ pub enum SessionServerError {
UnexpectedResponse { status_code: u16, body: String },
}
+#[derive(Debug, Error)]
+pub enum ServerSessionServerError {
+ #[error("Error sending HTTP request to sessionserver: {0}")]
+ HttpError(#[from] reqwest::Error),
+ #[error("Invalid or expired session")]
+ InvalidSession,
+ #[error("Unexpected response from sessionserver (status code {status_code}): {body}")]
+ UnexpectedResponse { status_code: u16, body: String },
+ #[error("Unknown sessionserver error: {0}")]
+ Unknown(String),
+}
+
#[derive(Deserialize)]
pub struct ForbiddenError {
pub error: String,
@@ -39,7 +55,7 @@ pub async fn join(
private_key: &[u8],
uuid: &Uuid,
server_id: &str,
-) -> Result<(), SessionServerError> {
+) -> Result<(), ClientSessionServerError> {
let client = reqwest::Client::new();
let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
@@ -63,28 +79,82 @@ pub async fn join(
.await?;
match res.status() {
- reqwest::StatusCode::NO_CONTENT => Ok(()),
- reqwest::StatusCode::FORBIDDEN => {
+ StatusCode::NO_CONTENT => Ok(()),
+ StatusCode::FORBIDDEN => {
let forbidden = res.json::<ForbiddenError>().await?;
match forbidden.error.as_str() {
- "InsufficientPrivilegesException" => Err(SessionServerError::MultiplayerDisabled),
- "UserBannedException" => Err(SessionServerError::Banned),
+ "InsufficientPrivilegesException" => {
+ Err(ClientSessionServerError::MultiplayerDisabled)
+ }
+ "UserBannedException" => Err(ClientSessionServerError::Banned),
"AuthenticationUnavailableException" => {
- Err(SessionServerError::AuthServersUnreachable)
+ Err(ClientSessionServerError::AuthServersUnreachable)
}
- "InvalidCredentialsException" => Err(SessionServerError::InvalidSession),
- "ForbiddenOperationException" => Err(SessionServerError::ForbiddenOperation),
- _ => Err(SessionServerError::Unknown(forbidden.error)),
+ "InvalidCredentialsException" => Err(ClientSessionServerError::InvalidSession),
+ "ForbiddenOperationException" => Err(ClientSessionServerError::ForbiddenOperation),
+ _ => Err(ClientSessionServerError::Unknown(forbidden.error)),
}
}
status_code => {
// log the headers
- log::debug!("Error headers: {:#?}", res.headers());
+ debug!("Error headers: {:#?}", res.headers());
let body = res.text().await?;
- Err(SessionServerError::UnexpectedResponse {
+ Err(ClientSessionServerError::UnexpectedResponse {
status_code: status_code.as_u16(),
body,
})
}
}
}
+
+/// Ask Mojang's servers if the player joining is authenticated.
+/// Included in the reply is the player's skin and cape.
+/// The IP field is optional and equivalent to enabling
+/// 'prevent-proxy-connections' in server.properties
+pub async fn serverside_auth(
+ username: &str,
+ public_key: &[u8],
+ private_key: &[u8; 16],
+ ip: Option<&str>,
+) -> Result<GameProfile, ServerSessionServerError> {
+ let hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
+ "".as_bytes(),
+ public_key,
+ private_key,
+ ));
+
+ let url = reqwest::Url::parse_with_params(
+ "https://sessionserver.mojang.com/session/minecraft/hasJoined",
+ if let Some(ip) = ip {
+ vec![("username", username), ("serverId", &hash), ("ip", ip)]
+ } else {
+ vec![("username", username), ("serverId", &hash)]
+ },
+ )
+ .expect("URL should always be valid");
+
+ let res = reqwest::get(url).await?;
+
+ match res.status() {
+ StatusCode::OK => {}
+ StatusCode::NO_CONTENT => {
+ return Err(ServerSessionServerError::InvalidSession);
+ }
+ StatusCode::FORBIDDEN => {
+ return Err(ServerSessionServerError::Unknown(
+ res.json::<ForbiddenError>().await?.error,
+ ))
+ }
+ status_code => {
+ // log the headers
+ debug!("Error headers: {:#?}", res.headers());
+ let body = res.text().await?;
+ return Err(ServerSessionServerError::UnexpectedResponse {
+ status_code: status_code.as_u16(),
+ body,
+ });
+ }
+ };
+
+ Ok(res.json::<SerializableGameProfile>().await?.into())
+}
diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs
index d1c20cc8..79feb1a7 100755
--- a/azalea-client/src/account.rs
+++ b/azalea-client/src/account.rs
@@ -31,7 +31,7 @@ pub struct Account {
/// This is an Arc<Mutex> so it can be modified by [`Self::refresh`].
pub access_token: Option<Arc<Mutex<String>>>,
/// Only required for online-mode accounts.
- pub uuid: Option<uuid::Uuid>,
+ pub uuid: Option<Uuid>,
/// The parameters (i.e. email) that were passed for creating this
/// [`Account`]. This is used to for automatic reauthentication when we get
@@ -85,7 +85,7 @@ impl Account {
Ok(Self {
username: auth_result.profile.name,
access_token: Some(Arc::new(Mutex::new(auth_result.access_token))),
- uuid: Some(Uuid::parse_str(&auth_result.profile.id).expect("Invalid UUID")),
+ uuid: Some(auth_result.profile.id),
auth_opts: AuthOpts::Microsoft {
email: email.to_string(),
},
diff --git a/azalea-client/src/chat.rs b/azalea-client/src/chat.rs
index 44ad8a71..de71f586 100755
--- a/azalea-client/src/chat.rs
+++ b/azalea-client/src/chat.rs
@@ -77,8 +77,8 @@ impl ChatPacket {
/// player-sent chat message, this will be None.
pub fn uuid(&self) -> Option<Uuid> {
match self {
- ChatPacket::System(_) => return None,
- ChatPacket::Player(m) => return Some(m.sender),
+ ChatPacket::System(_) => None,
+ ChatPacket::Player(m) => Some(m.sender),
}
}
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index dbb1b71e..125facda 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -1,6 +1,6 @@
pub use crate::chat::ChatPacket;
use crate::{movement::WalkDirection, plugins::PluginStates, Account, PlayerInfo};
-use azalea_auth::{game_profile::GameProfile, sessionserver::SessionServerError};
+use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
use azalea_chat::Component;
use azalea_core::{ChunkPos, ResourceLocation, Vec3};
use azalea_protocol::{
@@ -141,7 +141,7 @@ pub enum JoinError {
#[error("{0}")]
Io(#[from] io::Error),
#[error("{0}")]
- SessionServer(#[from] azalea_auth::sessionserver::SessionServerError),
+ SessionServer(#[from] azalea_auth::sessionserver::ClientSessionServerError),
#[error("The given address could not be parsed into a ServerAddress")]
InvalidAddress,
#[error("Couldn't refresh access token: {0}")]
@@ -315,8 +315,8 @@ impl Client {
}
if matches!(
e,
- SessionServerError::InvalidSession
- | SessionServerError::ForbiddenOperation
+ ClientSessionServerError::InvalidSession
+ | ClientSessionServerError::ForbiddenOperation
) {
// uh oh, we got an invalid session and have
// to reauthenticate now
diff --git a/azalea-protocol/Cargo.toml b/azalea-protocol/Cargo.toml
index 6da763bd..9066cdc3 100644
--- a/azalea-protocol/Cargo.toml
+++ b/azalea-protocol/Cargo.toml
@@ -40,3 +40,9 @@ uuid = "1.1.2"
connecting = []
default = ["packets"]
packets = ["connecting", "dep:async-compression", "dep:azalea-core"]
+
+[dev-dependencies]
+anyhow = "^1.0.65"
+tracing = "^0.1.36"
+tracing-subscriber = "^0.3.15"
+once_cell = "1.17.0" \ No newline at end of file
diff --git a/azalea-protocol/examples/handshake_proxy.rs b/azalea-protocol/examples/handshake_proxy.rs
new file mode 100644
index 00000000..5a4b4b6a
--- /dev/null
+++ b/azalea-protocol/examples/handshake_proxy.rs
@@ -0,0 +1,225 @@
+//! A "simple" server that gets login information and proxies connections.
+//! After login all connections are encrypted and Azalea cannot read them.
+
+use azalea_protocol::{
+ connect::Connection,
+ packets::{
+ handshake::{
+ client_intention_packet::ClientIntentionPacket, ClientboundHandshakePacket,
+ ServerboundHandshakePacket,
+ },
+ login::{serverbound_hello_packet::ServerboundHelloPacket, ServerboundLoginPacket},
+ status::{
+ clientbound_pong_response_packet::ClientboundPongResponsePacket,
+ clientbound_status_response_packet::{
+ ClientboundStatusResponsePacket, Players, Version,
+ },
+ ServerboundStatusPacket,
+ },
+ ConnectionProtocol, PROTOCOL_VERSION,
+ },
+ read::ReadPacketError,
+};
+use futures::FutureExt;
+use log::{error, info, warn};
+use once_cell::sync::Lazy;
+use std::error::Error;
+use tokio::{
+ io::{self, AsyncWriteExt},
+ net::{TcpListener, TcpStream},
+};
+use tracing::Level;
+
+const LISTEN_ADDR: &str = "127.0.0.1:25566";
+const PROXY_ADDR: &str = "173.205.80.60:25565";
+
+const PROXY_DESC: &str = "An Azalea Minecraft Proxy";
+
+// String must be formatted like "data:image/png;base64,<data>"
+static PROXY_FAVICON: Lazy<Option<String>> = Lazy::new(|| None);
+
+static PROXY_VERSION: Lazy<Version> = Lazy::new(|| Version {
+ name: "1.19.3".to_string(),
+ protocol: PROTOCOL_VERSION as i32,
+});
+
+const PROXY_PLAYERS: Players = Players {
+ max: 1,
+ online: 0,
+ sample: Vec::new(),
+};
+
+const PROXY_PREVIEWS_CHAT: Option<bool> = Some(false);
+const PROXY_SECURE_CHAT: Option<bool> = Some(false);
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt().with_max_level(Level::INFO).init();
+
+ // Bind to an address and port
+ let listener = TcpListener::bind(LISTEN_ADDR).await?;
+ loop {
+ // When a connection is made, pass it off to another thread
+ let (stream, _) = listener.accept().await?;
+ tokio::spawn(handle_connection(stream));
+ }
+}
+
+async fn handle_connection(stream: TcpStream) -> anyhow::Result<()> {
+ stream.set_nodelay(true)?;
+ let ip = stream.peer_addr()?;
+ let mut conn: Connection<ServerboundHandshakePacket, ClientboundHandshakePacket> =
+ Connection::wrap(stream);
+
+ // The first packet sent from a client is the intent packet.
+ // This specifies whether the client is pinging
+ // the server or is going to join the game.
+ let intent = match conn.read().await {
+ Ok(packet) => match packet {
+ ServerboundHandshakePacket::ClientIntention(packet) => {
+ info!(
+ "New connection: {0}, Version {1}, {2:?}",
+ ip.ip(),
+ packet.protocol_version,
+ packet.intention
+ );
+ packet
+ }
+ },
+ Err(e) => {
+ let e = e.into();
+ warn!("Error during intent: {e}");
+ return Err(e);
+ }
+ };
+
+ match intent.intention {
+ // If the client is pinging the proxy,
+ // reply with the information below.
+ ConnectionProtocol::Status => {
+ let mut conn = conn.status();
+ loop {
+ match conn.read().await {
+ Ok(p) => match p {
+ ServerboundStatusPacket::StatusRequest(_) => {
+ conn.write(
+ ClientboundStatusResponsePacket {
+ description: PROXY_DESC.into(),
+ favicon: PROXY_FAVICON.clone(),
+ players: PROXY_PLAYERS.clone(),
+ version: PROXY_VERSION.clone(),
+ previews_chat: PROXY_PREVIEWS_CHAT,
+ enforces_secure_chat: PROXY_SECURE_CHAT,
+ }
+ .get(),
+ )
+ .await?;
+ }
+ ServerboundStatusPacket::PingRequest(p) => {
+ conn.write(ClientboundPongResponsePacket { time: p.time }.get())
+ .await?;
+ break;
+ }
+ },
+ Err(e) => match *e {
+ ReadPacketError::ConnectionClosed => {
+ break;
+ }
+ e => {
+ warn!("Error during status: {e}");
+ return Err(e.into());
+ }
+ },
+ }
+ }
+ }
+ // If the client intends to join the proxy,
+ // wait for them to send the `Hello` packet to
+ // log their username and uuid, then forward the
+ // connection along to the proxy target.
+ ConnectionProtocol::Login => {
+ let mut conn = conn.login();
+ loop {
+ match conn.read().await {
+ Ok(p) => {
+ if let ServerboundLoginPacket::Hello(hello) = p {
+ info!(
+ "Player \'{0}\' from {1} logging in with uuid: {2}",
+ hello.name,
+ ip.ip(),
+ if let Some(id) = hello.profile_id {
+ id.to_string()
+ } else {
+ "".to_string()
+ }
+ );
+
+ tokio::spawn(transfer(conn.unwrap()?, intent, hello).map(|r| {
+ if let Err(e) = r {
+ error!("Failed to proxy: {e}");
+ }
+ }));
+
+ break;
+ }
+ }
+ Err(e) => match *e {
+ ReadPacketError::ConnectionClosed => {
+ break;
+ }
+ e => {
+ warn!("Error during login: {e}");
+ return Err(e.into());
+ }
+ },
+ }
+ }
+ }
+ _ => {
+ warn!("Client provided weird intent: {:?}", intent.intention);
+ }
+ }
+
+ Ok(())
+}
+
+async fn transfer(
+ mut inbound: TcpStream,
+ intent: ClientIntentionPacket,
+ hello: ServerboundHelloPacket,
+) -> Result<(), Box<dyn Error>> {
+ let outbound = TcpStream::connect(PROXY_ADDR).await?;
+ let name = hello.name.clone();
+ outbound.set_nodelay(true)?;
+
+ // Repeat the intent and hello packet
+ // recieved earlier to the proxy target
+ let mut outbound_conn: Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> =
+ Connection::wrap(outbound);
+ outbound_conn.write(intent.get()).await?;
+
+ let mut outbound_conn = outbound_conn.login();
+ outbound_conn.write(hello.get()).await?;
+
+ let mut outbound = outbound_conn.unwrap()?;
+
+ // Split the incoming and outgoing connections in
+ // halves and handle each pair on separate threads.
+ let (mut ri, mut wi) = inbound.split();
+ let (mut ro, mut wo) = outbound.split();
+
+ let client_to_server = async {
+ io::copy(&mut ri, &mut wo).await?;
+ wo.shutdown().await
+ };
+
+ let server_to_client = async {
+ io::copy(&mut ro, &mut wi).await?;
+ wi.shutdown().await
+ };
+
+ tokio::try_join!(client_to_server, server_to_client)?;
+ info!("Player \'{name}\' left the game");
+
+ Ok(())
+}
diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs
index 48401d03..149ea95d 100755
--- a/azalea-protocol/src/connect.rs
+++ b/azalea-protocol/src/connect.rs
@@ -8,7 +8,8 @@ use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket};
use crate::packets::ProtocolPacket;
use crate::read::{read_packet, ReadPacketError};
use crate::write::write_packet;
-use azalea_auth::sessionserver::SessionServerError;
+use azalea_auth::game_profile::GameProfile;
+use azalea_auth::sessionserver::{ClientSessionServerError, ServerSessionServerError};
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
use bytes::BytesMut;
use log::{error, info};
@@ -17,7 +18,7 @@ use std::marker::PhantomData;
use std::net::SocketAddr;
use thiserror::Error;
use tokio::io::AsyncWriteExt;
-use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
+use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReuniteError};
use tokio::net::TcpStream;
use uuid::Uuid;
@@ -327,7 +328,7 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
uuid: &Uuid,
private_key: [u8; 16],
packet: &ClientboundHelloPacket,
- ) -> Result<(), SessionServerError> {
+ ) -> Result<(), ClientSessionServerError> {
azalea_auth::sessionserver::join(
access_token,
&packet.public_key,
@@ -339,6 +340,63 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
}
}
+impl Connection<ServerboundHandshakePacket, ClientboundHandshakePacket> {
+ /// Change our state from handshake to login. This is the state that is used
+ /// for logging in.
+ pub fn login(self) -> Connection<ServerboundLoginPacket, ClientboundLoginPacket> {
+ Connection::from(self)
+ }
+
+ /// Change our state from handshake to status. This is the state that is
+ /// used for pinging the server.
+ pub fn status(self) -> Connection<ServerboundStatusPacket, ClientboundStatusPacket> {
+ Connection::from(self)
+ }
+}
+
+impl Connection<ServerboundLoginPacket, ClientboundLoginPacket> {
+ /// Set our compression threshold, i.e. the maximum size that a packet is
+ /// allowed to be without getting compressed. If you set it to less than 0
+ /// then compression gets disabled.
+ pub fn set_compression_threshold(&mut self, threshold: i32) {
+ // if you pass a threshold of less than 0, compression is disabled
+ if threshold >= 0 {
+ self.reader.compression_threshold = Some(threshold as u32);
+ self.writer.compression_threshold = Some(threshold as u32);
+ } else {
+ self.reader.compression_threshold = None;
+ self.writer.compression_threshold = None;
+ }
+ }
+
+ /// Set the encryption key that is used to encrypt and decrypt packets. It's
+ /// the same for both reading and writing.
+ pub fn set_encryption_key(&mut self, key: [u8; 16]) {
+ let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key);
+ self.reader.dec_cipher = Some(dec_cipher);
+ self.writer.enc_cipher = Some(enc_cipher);
+ }
+
+ /// Change our state from login to game. This is the state that's used when
+ /// the client is actually in the game.
+ pub fn game(self) -> Connection<ServerboundGamePacket, ClientboundGamePacket> {
+ Connection::from(self)
+ }
+
+ /// Verify connecting clients have authenticated with Minecraft's servers.
+ /// This must happen after the client sends a `ServerboundLoginPacket::Key`
+ /// packet.
+ pub async fn authenticate(
+ &self,
+ username: &str,
+ public_key: &[u8],
+ private_key: &[u8; 16],
+ ip: Option<&str>,
+ ) -> Result<GameProfile, ServerSessionServerError> {
+ azalea_auth::sessionserver::serverside_auth(username, public_key, private_key, ip).await
+ }
+}
+
// rust doesn't let us implement From because allegedly it conflicts with
// `core`'s "impl<T> From<T> for T" so we do this instead
impl<R1, W1> Connection<R1, W1>
@@ -390,4 +448,9 @@ where
},
}
}
+
+ /// Convert from a `Connection` into a `TcpStream`. Useful for servers.
+ pub fn unwrap(self) -> Result<TcpStream, ReuniteError> {
+ self.reader.read_stream.reunite(self.writer.write_stream)
+ }
}
diff --git a/azalea/Cargo.toml b/azalea/Cargo.toml
index 7c82a00c..d3fba18f 100644
--- a/azalea/Cargo.toml
+++ b/azalea/Cargo.toml
@@ -32,6 +32,4 @@ tokio = "^1.23.1"
uuid = "1.2.2"
[dev-dependencies]
-anyhow = "^1.0.65"
-env_logger = "^0.9.1"
-tokio = "^1.23.1"
+env_logger = "^0.10.0"