Panel: Yggdrasil auth server (authlib-injector), skins and capes
- Yggdrasil API per the authlib-injector spec: metadata with signing key and skin domains, authenticate/refresh/validate/invalidate/signout, join/hasJoined, profile lookup, texture upload, and the minecraftservices endpoints (chat certificates, publickeys, attributes, blocklist) - 4096-bit signing key generated once into the data volume; textures and chat certificates signed SHA1withRSA (verified with Java's own crypto) - Skins/capes stored content-addressed after validation and re-encoding; cape library with public/group/private visibility; head avatars API - Launcher login returns a game session; launcher sessions recorded for launcher-only servers; authlib-injector download mirror - Schema v2: player UUIDs (offline UUID backfilled), skins, capes, tokens, sessions, chat keys, game server tables - Remove Microsoft sign-in from the engine and panel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
29 files changed
+2436
-400
No files matched your search
@@ -0,0 +1,68 @@
|
||||
//! Request helpers: the panel's public URL and the client's IP address.
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::state::AppState;
|
||||
use crate::store;
|
||||
use axum::extract::{ConnectInfo, FromRequestParts};
|
||||
use axum::http::request::Parts;
|
||||
use axum::http::HeaderMap;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
fn header<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
|
||||
headers.get(name).and_then(|v| v.to_str().ok()).map(str::trim).filter(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
/// The URL players reach the panel at, without a trailing slash.
|
||||
///
|
||||
/// Order: the admin's setting → `PUBLIC_URL` → what the request says
|
||||
/// (honouring `X-Forwarded-*` from a reverse proxy).
|
||||
pub async fn public_base(state: &AppState, headers: &HeaderMap) -> String {
|
||||
if let Ok(s) = store::settings(state).await {
|
||||
if let Some(u) = s.public_url.filter(|u| !u.is_empty()) {
|
||||
return u.trim_end_matches('/').to_string();
|
||||
}
|
||||
}
|
||||
if let Some(u) = &state.cfg.public_url {
|
||||
return u.trim_end_matches('/').to_string();
|
||||
}
|
||||
let host = header(headers, "x-forwarded-host").or_else(|| header(headers, "host")).unwrap_or("localhost:8080");
|
||||
let proto = header(headers, "x-forwarded-proto").map(|p| p.split(',').next().unwrap_or(p).trim()).unwrap_or("http");
|
||||
format!("{proto}://{}", host.split(',').next().unwrap_or(host).trim())
|
||||
}
|
||||
|
||||
/// Host part of a URL (`https://a.b:8443/x` → `a.b`), used for authlib's
|
||||
/// skin-domain allow-list.
|
||||
pub fn host_of(url: &str) -> String {
|
||||
let rest = url.split("://").nth(1).unwrap_or(url);
|
||||
let authority = rest.split('/').next().unwrap_or(rest);
|
||||
let host = authority.rsplit('@').next().unwrap_or(authority);
|
||||
if host.starts_with('[') {
|
||||
return host.split(']').next().unwrap_or(host).trim_start_matches('[').to_string();
|
||||
}
|
||||
host.split(':').next().unwrap_or(host).to_string()
|
||||
}
|
||||
|
||||
/// Client IP: the first `X-Forwarded-For` hop, `X-Real-IP`, or the socket.
|
||||
pub struct ClientIp(pub Option<String>);
|
||||
|
||||
impl<S: Send + Sync> FromRequestParts<S> for ClientIp {
|
||||
type Rejection = AppError;
|
||||
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
|
||||
let forwarded = header(&parts.headers, "x-forwarded-for").and_then(|v| v.split(',').next()).map(|v| v.trim().to_string());
|
||||
let real = header(&parts.headers, "x-real-ip").map(String::from);
|
||||
let socket = parts.extensions.get::<ConnectInfo<SocketAddr>>().map(|c| c.0.ip().to_string());
|
||||
Ok(ClientIp(forwarded.or(real).or(socket)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hosts() {
|
||||
assert_eq!(host_of("https://panel.example.com/api"), "panel.example.com");
|
||||
assert_eq!(host_of("http://localhost:8080"), "localhost");
|
||||
assert_eq!(host_of("https://[::1]:8443/"), "::1");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user