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"
|
||||
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"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use azalea_buf::McBuf;
|
||||
use serde::{de::Visitor, ser::SerializeStruct, Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -24,3 +25,146 @@ pub struct ProfilePropertyValue {
|
|||
pub value: 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 uuid::Uuid;
|
||||
|
||||
use crate::game_profile::GameProfile;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SessionServerError {
|
||||
#[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::read::{read_packet, ReadPacketError};
|
||||
use crate::write::write_packet;
|
||||
use azalea_auth::game_profile::GameProfile;
|
||||
use azalea_auth::sessionserver::SessionServerError;
|
||||
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
|
||||
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
|
||||
// `core`'s "impl<T> From<T> for T" so we do this instead
|
||||
impl<R1, W1> Connection<R1, W1>
|
||||
|
|
Loading…
Add table
Reference in a new issue