mc_headless_auth 0.1.0

Headless Minecraft Server Authentication
Documentation
use std::{io::Cursor, net::TcpStream};

use bytes::Buf;
use constcat::concat;
use image::{ImageFormat, RgbaImage};
use serde::Serialize;
use serde_json::Value;

use crate::{
    ServerError,
    config::{MIN_SUPPORTED_VERSION, StatusConfig},
    message::MessageGenerator,
    minecraft::{
        handshake::Handshake, packet::Packet, packets, protocol_version, server::ConnectionState,
    },
    token::TokenGenerator,
};

pub fn advance<T: TokenGenerator, M: MessageGenerator>(
    stream: &mut TcpStream,
    _state: ConnectionState<T, M>,
    handshake: Handshake,
    config: StatusConfig,
) -> Result<(), ServerError> {
    let _ = Packet::from_stream(stream, 0x00)?;

    let status = get_status(handshake.protocol_version.0, config)?;
    packets::status_response(status).write_stream(stream)?;

    let mut ping = Packet::from_stream(stream, 0x01)?;
    let timestamp = ping.data.get_i64();

    packets::pong_response(timestamp).write_stream(stream)?;

    Ok(())
}

fn get_status(protocol: i32, config: StatusConfig) -> Result<String, ServerError> {
    let status = StatusResponse {
        version: Version {
            name: concat!(MIN_SUPPORTED_VERSION, "+").to_string(),
            protocol,
        },
        players: Players {
            max: 0,
            online: 0,
            sample: Vec::default(),
        },
        description: if protocol < protocol_version::MIN_SUPPORTED_PROTOCOL {
            Some(Value::String(config.legacy_decription.unwrap_or_default()))
        } else {
            config.description
        },
        favicon: encode_favicon(config.favicon)?,
        enforces_secure_chat: false,
    };

    Ok(serde_json::to_string_pretty(&status)?)
}

fn encode_favicon(favicon: Option<RgbaImage>) -> Result<Option<String>, ServerError> {
    use base64::prelude::*;

    let favicon = match favicon {
        Some(f) => f,
        None => return Ok(None),
    };

    if favicon.width() > 64 || favicon.height() > 64 {
        return Ok(None);
    }

    let mut img = Cursor::new(Vec::new());
    favicon.write_to(&mut img, ImageFormat::Png)?;

    Ok(Some(format!(
        "data:image/png;base64,{}",
        BASE64_STANDARD.encode(img.get_ref())
    )))
}

#[derive(Debug, Clone, Serialize)]
struct StatusResponse {
    version: Version,
    players: Players,
    #[serde(skip_serializing_if = "Option::is_none")]
    description: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    favicon: Option<String>,
    #[serde(rename = "enforcesSecureChat")]
    enforces_secure_chat: bool,
}

#[derive(Debug, Clone, Serialize)]
struct Version {
    name: String,
    protocol: i32,
}

#[derive(Debug, Clone, Serialize)]
struct Players {
    max: i32,
    online: i32,
    sample: Vec<Player>,
}

#[derive(Debug, Clone, Serialize)]
struct Player {
    name: String,
    id: String,
}