mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
A couple useful things for servers
This commit is contained in:
parent
c6b5664972
commit
798cfc233e
4 changed files with 251 additions and 1 deletions
|
@ -19,7 +19,7 @@ serde = {version = "1.0.145", features = ["derive"]}
|
||||||
serde_json = "1.0.86"
|
serde_json = "1.0.86"
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
tokio = {version = "1.23.1", features = ["fs"]}
|
tokio = {version = "1.23.1", features = ["fs"]}
|
||||||
uuid = "^1.1.2"
|
uuid = {version = "^1.1.2", features = ["serde"]}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.9.1"
|
env_logger = "0.9.1"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use azalea_buf::McBuf;
|
use azalea_buf::McBuf;
|
||||||
|
use serde::{de::Visitor, ser::SerializeStruct, Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -24,3 +25,146 @@ pub struct ProfilePropertyValue {
|
||||||
pub value: String,
|
pub value: String,
|
||||||
pub signature: Option<String>,
|
pub signature: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serialize for GameProfile {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let mut serializer = serializer.serialize_struct("GameProfile", 3)?;
|
||||||
|
serializer.serialize_field("id", &self.uuid)?;
|
||||||
|
serializer.serialize_field("name", &self.name)?;
|
||||||
|
serializer.serialize_field(
|
||||||
|
"properties",
|
||||||
|
&SerializedProfilePropertyValue::from_map(self.properties.clone()),
|
||||||
|
)?;
|
||||||
|
serializer.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for GameProfile {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(field_identifier, rename_all = "lowercase")]
|
||||||
|
enum Field {
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
Properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GameProfileVisitor;
|
||||||
|
impl<'de> Visitor<'de> for GameProfileVisitor {
|
||||||
|
type Value = GameProfile;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("struct GameProfile")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: serde::de::SeqAccess<'de>,
|
||||||
|
{
|
||||||
|
let id = seq
|
||||||
|
.next_element()?
|
||||||
|
.ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
|
||||||
|
let name = seq
|
||||||
|
.next_element()?
|
||||||
|
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
|
||||||
|
let properties = seq
|
||||||
|
.next_element()?
|
||||||
|
.ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
|
||||||
|
Ok(GameProfile {
|
||||||
|
uuid: id,
|
||||||
|
name,
|
||||||
|
properties: SerializedProfilePropertyValue::into_map(properties),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: serde::de::MapAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut id = None;
|
||||||
|
let mut name = None;
|
||||||
|
let mut properties = None;
|
||||||
|
while let Some(key) = map.next_key::<Field>()? {
|
||||||
|
match key {
|
||||||
|
Field::Id => {
|
||||||
|
if id.is_some() {
|
||||||
|
return Err(serde::de::Error::duplicate_field("id"));
|
||||||
|
}
|
||||||
|
id = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::Name => {
|
||||||
|
if name.is_some() {
|
||||||
|
return Err(serde::de::Error::duplicate_field("name"));
|
||||||
|
}
|
||||||
|
name = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::Properties => {
|
||||||
|
if properties.is_some() {
|
||||||
|
return Err(serde::de::Error::duplicate_field("properties"));
|
||||||
|
}
|
||||||
|
properties = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let id = id.ok_or_else(|| serde::de::Error::missing_field("id"))?;
|
||||||
|
let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?;
|
||||||
|
let properties = SerializedProfilePropertyValue::into_map(
|
||||||
|
properties.ok_or_else(|| serde::de::Error::missing_field("properties"))?,
|
||||||
|
);
|
||||||
|
Ok(GameProfile {
|
||||||
|
uuid: id,
|
||||||
|
name,
|
||||||
|
properties,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIELDS: &'static [&'static str] = &["id", "name", "properties"];
|
||||||
|
deserializer.deserialize_struct("GameProfile", FIELDS, GameProfileVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct SerializedProfilePropertyValue {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
|
pub signature: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializedProfilePropertyValue {
|
||||||
|
pub fn from_map(
|
||||||
|
map: HashMap<String, ProfilePropertyValue>,
|
||||||
|
) -> Vec<SerializedProfilePropertyValue> {
|
||||||
|
let mut list: Vec<SerializedProfilePropertyValue> = Vec::new();
|
||||||
|
for (key, value) in map {
|
||||||
|
list.push(SerializedProfilePropertyValue {
|
||||||
|
name: key,
|
||||||
|
value: value.value,
|
||||||
|
signature: value.signature,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_map(
|
||||||
|
list: Vec<SerializedProfilePropertyValue>,
|
||||||
|
) -> HashMap<String, ProfilePropertyValue> {
|
||||||
|
let mut map: HashMap<String, ProfilePropertyValue> = HashMap::new();
|
||||||
|
for value in list {
|
||||||
|
map.insert(
|
||||||
|
value.name,
|
||||||
|
ProfilePropertyValue {
|
||||||
|
value: value.value,
|
||||||
|
signature: value.signature,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ use serde_json::json;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::game_profile::GameProfile;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum SessionServerError {
|
pub enum SessionServerError {
|
||||||
#[error("Error sending HTTP request to sessionserver: {0}")]
|
#[error("Error sending HTTP request to sessionserver: {0}")]
|
||||||
|
@ -88,3 +90,49 @@ pub async fn join(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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: &String,
|
||||||
|
public_key: &[u8],
|
||||||
|
private_key: &[u8; 16],
|
||||||
|
ip: Option<&String>,
|
||||||
|
) -> Result<GameProfile, SessionServerError> {
|
||||||
|
let hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
|
||||||
|
"".as_bytes(),
|
||||||
|
public_key,
|
||||||
|
private_key,
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut url = format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={hash}");
|
||||||
|
if let Some(ip) = ip {
|
||||||
|
url = format!("{url}&ip={ip}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = reqwest::get(url).await?;
|
||||||
|
match res.status() {
|
||||||
|
reqwest::StatusCode::OK => {}
|
||||||
|
reqwest::StatusCode::NO_CONTENT => {
|
||||||
|
return Err(SessionServerError::InvalidSession);
|
||||||
|
}
|
||||||
|
reqwest::StatusCode::FORBIDDEN => {
|
||||||
|
return Err(SessionServerError::Unknown(
|
||||||
|
res.json::<ForbiddenError>().await?.error,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
status_code => {
|
||||||
|
// log the headers
|
||||||
|
log::debug!("Error headers: {:#?}", res.headers());
|
||||||
|
let body = res.text().await?;
|
||||||
|
return Err(SessionServerError::UnexpectedResponse {
|
||||||
|
status_code: status_code.as_u16(),
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(res.json::<GameProfile>().await?)
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket};
|
||||||
use crate::packets::ProtocolPacket;
|
use crate::packets::ProtocolPacket;
|
||||||
use crate::read::{read_packet, ReadPacketError};
|
use crate::read::{read_packet, ReadPacketError};
|
||||||
use crate::write::write_packet;
|
use crate::write::write_packet;
|
||||||
|
use azalea_auth::game_profile::GameProfile;
|
||||||
use azalea_auth::sessionserver::SessionServerError;
|
use azalea_auth::sessionserver::SessionServerError;
|
||||||
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
|
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
|
@ -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: &String,
|
||||||
|
public_key: &[u8],
|
||||||
|
private_key: &[u8; 16],
|
||||||
|
ip: Option<&String>,
|
||||||
|
) -> Result<GameProfile, SessionServerError> {
|
||||||
|
azalea_auth::sessionserver::serverside_auth(username, public_key, private_key, ip).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// rust doesn't let us implement From because allegedly it conflicts with
|
// 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
|
// `core`'s "impl<T> From<T> for T" so we do this instead
|
||||||
impl<R1, W1> Connection<R1, W1>
|
impl<R1, W1> Connection<R1, W1>
|
||||||
|
|
Loading…
Add table
Reference in a new issue