mirror of
https://github.com/shiroyashik/sculptor.git
synced 2025-12-06 13:01:12 +03:00
The system design has changed, but the refactor is not over.
In future commits, I will strive to maintain a consistent style.
This commit is contained in:
parent
7594e3d615
commit
a1f9eba502
25 changed files with 410 additions and 367 deletions
61
src/api/figura/auth.rs
Normal file
61
src/api/figura/auth.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
use axum::{debug_handler, extract::{Query, State}, response::{IntoResponse, Response}, routing::get, Router};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use ring::digest::{self, digest};
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
use crate::{auth::{has_joined, Userinfo}, utils::rand, AppState};
|
||||||
|
use super::types::auth::*;
|
||||||
|
|
||||||
|
pub fn router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/id", get(id))
|
||||||
|
.route("/verify", get(verify))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
async fn id(
|
||||||
|
// First stage of authentication
|
||||||
|
Query(query): Query<Id>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> String {
|
||||||
|
let server_id =
|
||||||
|
hex::encode(&digest(&digest::SHA1_FOR_LEGACY_USE_ONLY, &rand()).as_ref()[0..20]);
|
||||||
|
let state = state.user_manager;
|
||||||
|
state.pending_insert(server_id.clone(), query.username);
|
||||||
|
server_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
async fn verify(
|
||||||
|
// Second stage of authentication
|
||||||
|
Query(query): Query<Verify>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Response {
|
||||||
|
let server_id = query.id.clone();
|
||||||
|
let username = state.user_manager.pending_remove(&server_id).unwrap().1; // TODO: Add error check
|
||||||
|
let userinfo = match has_joined(&server_id, &username).await {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
error!("[Authentication] {e}");
|
||||||
|
return (StatusCode::INTERNAL_SERVER_ERROR, "internal verify error".to_string()).into_response();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let Some((uuid, auth_system)) = userinfo {
|
||||||
|
info!("[Authentication] {username} logged in using {auth_system:?}");
|
||||||
|
let authenticated = state.user_manager;
|
||||||
|
authenticated.insert(
|
||||||
|
uuid,
|
||||||
|
server_id.clone(),
|
||||||
|
Userinfo {
|
||||||
|
username,
|
||||||
|
uuid,
|
||||||
|
auth_system,
|
||||||
|
token: Some(server_id.clone()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(StatusCode::OK, server_id.to_string()).into_response()
|
||||||
|
} else {
|
||||||
|
info!("[Authentication] failed to verify {username}");
|
||||||
|
(StatusCode::BAD_REQUEST, "failed to verify".to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/api/figura/mod.rs
Normal file
7
src/api/figura/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
mod types;
|
||||||
|
mod websocket;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod profile;
|
||||||
|
pub mod info;
|
||||||
|
|
||||||
|
pub use websocket::handler as ws;
|
||||||
|
|
@ -15,9 +15,9 @@ use uuid::Uuid;
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::Token,
|
auth::Token,
|
||||||
utils::{calculate_file_sha256, format_uuid, get_correct_array},
|
utils::{calculate_file_sha256, format_uuid, get_correct_array},
|
||||||
ws::S2CMessage,
|
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
use super::types::S2CMessage;
|
||||||
|
|
||||||
pub async fn user_info(
|
pub async fn user_info(
|
||||||
Path(uuid): Path<Uuid>,
|
Path(uuid): Path<Uuid>,
|
||||||
11
src/api/figura/types/auth.rs
Normal file
11
src/api/figura/types/auth.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Id {
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Verify {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
mod c2s;
|
mod c2s;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod s2c;
|
mod s2c;
|
||||||
|
pub mod auth;
|
||||||
|
|
||||||
pub use c2s::C2SMessage;
|
pub use c2s::C2SMessage;
|
||||||
pub use errors::MessageLoadError;
|
pub use errors::MessageLoadError;
|
||||||
|
|
@ -15,10 +15,8 @@ use tokio::sync::{
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::AppState;
|
||||||
ws::{C2SMessage, S2CMessage},
|
use super::types::{C2SMessage, S2CMessage};
|
||||||
AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response {
|
pub async fn handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response {
|
||||||
ws.on_upgrade(|socket| handle_socket(socket, state))
|
ws.on_upgrade(|socket| handle_socket(socket, state))
|
||||||
2
src/api/mod.rs
Normal file
2
src/api/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod figura;
|
||||||
|
pub mod v1;
|
||||||
22
src/api/v1/auth.rs
Normal file
22
src/api/v1/auth.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
use axum::{extract::State, http::StatusCode, response::{IntoResponse, Response}};
|
||||||
|
|
||||||
|
use crate::{auth::Token, AppState};
|
||||||
|
|
||||||
|
pub async fn status(
|
||||||
|
Token(token): Token,
|
||||||
|
State(state): State<AppState>
|
||||||
|
) -> Response {
|
||||||
|
match token {
|
||||||
|
Some(token) => {
|
||||||
|
if state.user_manager.is_authenticated(&token) {
|
||||||
|
(StatusCode::OK, "ok".to_string()).into_response()
|
||||||
|
} else {
|
||||||
|
|
||||||
|
(StatusCode::UNAUTHORIZED, "unauthorized".to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
(StatusCode::BAD_REQUEST, "bad request".to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,29 +1,14 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Query, State},
|
extract::{Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response}
|
||||||
routing::{get, post},
|
|
||||||
Router
|
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{auth::Token, AppState};
|
use crate::{auth::Token, AppState};
|
||||||
|
use super::types::UserUuid;
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub(super) async fn verify(
|
||||||
Router::new()
|
|
||||||
.route("/verify", get(verify))
|
|
||||||
.route("/raw", post(raw))
|
|
||||||
.route("/sub/raw", post(sub_raw))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct UserUuid {
|
|
||||||
uuid: Option<Uuid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn verify(
|
|
||||||
Token(token): Token,
|
Token(token): Token,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
|
@ -32,7 +17,7 @@ async fn verify(
|
||||||
.unwrap_or_else(|x| x)
|
.unwrap_or_else(|x| x)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn raw(
|
pub(super) async fn raw(
|
||||||
Token(token): Token,
|
Token(token): Token,
|
||||||
Query(query): Query<UserUuid>,
|
Query(query): Query<UserUuid>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -68,7 +53,7 @@ async fn raw(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sub_raw(
|
pub(super) async fn sub_raw(
|
||||||
Token(token): Token,
|
Token(token): Token,
|
||||||
Query(query): Query<UserUuid>,
|
Query(query): Query<UserUuid>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
16
src/api/v1/mod.rs
Normal file
16
src/api/v1/mod.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
use axum::{routing::{get, post}, Router};
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
mod http2ws;
|
||||||
|
mod users;
|
||||||
|
mod auth;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
pub fn router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/verify", get(http2ws::verify))
|
||||||
|
.route("/raw", post(http2ws::raw))
|
||||||
|
.route("/sub/raw", post(http2ws::sub_raw))
|
||||||
|
.route("/auth/", get(auth::status))
|
||||||
|
.route("/users/create", post(users::create_user))
|
||||||
|
}
|
||||||
7
src/api/v1/types.rs
Normal file
7
src/api/v1/types.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(super) struct UserUuid {
|
||||||
|
pub uuid: Option<Uuid>,
|
||||||
|
}
|
||||||
24
src/api/v1/users.rs
Normal file
24
src/api/v1/users.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json
|
||||||
|
};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::{auth::{Token, Userinfo}, AppState};
|
||||||
|
|
||||||
|
pub(super) async fn create_user(
|
||||||
|
Token(token): Token,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(json): Json<Userinfo>
|
||||||
|
) -> Response {
|
||||||
|
debug!("Json: {json:?}");
|
||||||
|
match state.config.lock().await.clone().verify_token(&token) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => return e,
|
||||||
|
}
|
||||||
|
|
||||||
|
state.user_manager.insert_user(json.uuid, json);
|
||||||
|
(StatusCode::OK, "ok".to_string()).into_response()
|
||||||
|
}
|
||||||
309
src/auth.rs
309
src/auth.rs
|
|
@ -1,309 +0,0 @@
|
||||||
use std::{str::FromStr, sync::Arc};
|
|
||||||
|
|
||||||
use crate::utils::*;
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use axum::{
|
|
||||||
async_trait, debug_handler, extract::{FromRequestParts, Query, State}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, routing::{get, post}, Json, Router
|
|
||||||
};
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use ring::digest::{self, digest};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use tracing::{debug, error, info, trace};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::AppState;
|
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
|
||||||
Router::new()
|
|
||||||
.route("/id", get(id))
|
|
||||||
.route("/verify", get(verify))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn router_v1() -> Router<AppState> {
|
|
||||||
Router::new()
|
|
||||||
.route("/create", post(create_user))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Web
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct Id {
|
|
||||||
username: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
async fn id(
|
|
||||||
// First stage of authentication
|
|
||||||
Query(query): Query<Id>,
|
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> String {
|
|
||||||
let server_id =
|
|
||||||
hex::encode(&digest(&digest::SHA1_FOR_LEGACY_USE_ONLY, &rand()).as_ref()[0..20]);
|
|
||||||
let state = state.user_manager;
|
|
||||||
state.pending_insert(server_id.clone(), query.username);
|
|
||||||
server_id
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct Verify {
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
async fn verify(
|
|
||||||
// Second stage of authentication
|
|
||||||
Query(query): Query<Verify>,
|
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> Response {
|
|
||||||
let server_id = query.id.clone();
|
|
||||||
let username = state.user_manager.pending_remove(&server_id).unwrap().1; // TODO: Add error check
|
|
||||||
let userinfo = match has_joined(&server_id, &username).await {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(e) => {
|
|
||||||
error!("[Authentication] {e}");
|
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR, "internal verify error".to_string()).into_response();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if let Some((uuid, auth_system)) = userinfo {
|
|
||||||
info!("[Authentication] {username} logged in using {auth_system:?}");
|
|
||||||
let authenticated = state.user_manager;
|
|
||||||
authenticated.insert(
|
|
||||||
uuid,
|
|
||||||
server_id.clone(),
|
|
||||||
Userinfo {
|
|
||||||
username,
|
|
||||||
uuid,
|
|
||||||
auth_system,
|
|
||||||
token: Some(server_id.clone()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
(StatusCode::OK, server_id.to_string()).into_response()
|
|
||||||
} else {
|
|
||||||
info!("[Authentication] failed to verify {username}");
|
|
||||||
(StatusCode::BAD_REQUEST, "failed to verify".to_string()).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn status(
|
|
||||||
Token(token): Token,
|
|
||||||
State(state): State<AppState>
|
|
||||||
) -> Response {
|
|
||||||
match token {
|
|
||||||
Some(token) => {
|
|
||||||
if state.user_manager.is_authenticated(&token) {
|
|
||||||
(StatusCode::OK, "ok".to_string()).into_response()
|
|
||||||
} else {
|
|
||||||
|
|
||||||
(StatusCode::UNAUTHORIZED, "unauthorized".to_string()).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
(StatusCode::BAD_REQUEST, "bad request".to_string()).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_user(
|
|
||||||
Token(token): Token,
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Json(json): Json<Userinfo>
|
|
||||||
) -> Response {
|
|
||||||
debug!("Json: {json:?}");
|
|
||||||
match state.config.lock().await.clone().verify_token(&token) {
|
|
||||||
Ok(_) => {},
|
|
||||||
Err(e) => return e,
|
|
||||||
}
|
|
||||||
|
|
||||||
state.user_manager.insert_user(json.uuid, json);
|
|
||||||
(StatusCode::OK, "ok".to_string()).into_response()
|
|
||||||
}
|
|
||||||
// Web End
|
|
||||||
|
|
||||||
// It's an extractor that pulls a token from the Header.
|
|
||||||
#[derive(PartialEq, Debug)]
|
|
||||||
pub struct Token(pub Option<String>);
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<S> FromRequestParts<S> for Token
|
|
||||||
where
|
|
||||||
S: Send + Sync,
|
|
||||||
{
|
|
||||||
type Rejection = StatusCode;
|
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
|
|
||||||
let token = parts
|
|
||||||
.headers
|
|
||||||
.get("token")
|
|
||||||
.and_then(|value| value.to_str().ok());
|
|
||||||
trace!(token = ?token);
|
|
||||||
match token {
|
|
||||||
Some(token) => Ok(Self(Some(token.to_string()))),
|
|
||||||
None => Ok(Self(None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// End Extractor
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum AuthSystem {
|
|
||||||
Internal,
|
|
||||||
ElyBy,
|
|
||||||
Mojang,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for AuthSystem {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
match self {
|
|
||||||
AuthSystem::Internal => String::from("internal"),
|
|
||||||
AuthSystem::ElyBy => String::from("elyby"),
|
|
||||||
AuthSystem::Mojang => String::from("mojang"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for AuthSystem {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"internal" => Ok(Self::Internal),
|
|
||||||
"elyby" => Ok(Self::ElyBy),
|
|
||||||
"mojang" => Ok(Self::Mojang),
|
|
||||||
_ => Err(anyhow!("No auth system called: {s}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthSystem {
|
|
||||||
fn get_url(&self) -> String {
|
|
||||||
match self {
|
|
||||||
AuthSystem::Internal => panic!("Can't get internal URL!"),
|
|
||||||
AuthSystem::ElyBy => String::from("http://minecraft.ely.by/session/hasJoined"),
|
|
||||||
AuthSystem::Mojang => String::from("https://sessionserver.mojang.com/session/minecraft/hasJoined"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Work with external APIs
|
|
||||||
/// Get UUID from JSON response
|
|
||||||
#[inline]
|
|
||||||
fn get_id_json(json: &serde_json::Value) -> anyhow::Result<Uuid> {
|
|
||||||
trace!("json: {json:#?}"); // For debugging, we'll get to this later!
|
|
||||||
let uuid = Uuid::parse_str(json.get("id").unwrap().as_str().unwrap())?;
|
|
||||||
Ok(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn fetch_json(
|
|
||||||
auth_system: AuthSystem,
|
|
||||||
server_id: &str,
|
|
||||||
username: &str,
|
|
||||||
) -> anyhow::Result<Option<(Uuid, AuthSystem)>> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let url = auth_system.get_url();
|
|
||||||
|
|
||||||
let res = client
|
|
||||||
.get(url)
|
|
||||||
.query(&[("serverId", server_id), ("username", username)])
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
debug!("{res:?}");
|
|
||||||
match res.status().as_u16() {
|
|
||||||
200 => {
|
|
||||||
let json = serde_json::from_str::<serde_json::Value>(&res.text().await?)?;
|
|
||||||
let uuid = get_id_json(&json)?;
|
|
||||||
Ok(Some((uuid, auth_system)))
|
|
||||||
}
|
|
||||||
401 => Ok(None), // Ely.By None
|
|
||||||
204 => Ok(None), // Mojang None
|
|
||||||
_ => Err(anyhow!("Unknown code: {}", res.status().as_u16())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn has_joined(
|
|
||||||
server_id: &str,
|
|
||||||
username: &str,
|
|
||||||
) -> anyhow::Result<Option<(Uuid, AuthSystem)>> {
|
|
||||||
let (elyby, mojang) = (
|
|
||||||
fetch_json(AuthSystem::ElyBy,server_id, username).await?,
|
|
||||||
fetch_json(AuthSystem::Mojang, server_id, username).await?
|
|
||||||
);
|
|
||||||
|
|
||||||
if elyby.is_none() && mojang.is_none() {
|
|
||||||
Ok(None)
|
|
||||||
} else if mojang.is_some() {
|
|
||||||
Ok(mojang)
|
|
||||||
} else if elyby.is_some() {
|
|
||||||
Ok(elyby)
|
|
||||||
} else {
|
|
||||||
panic!("Impossible error!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// End of work with external APIs
|
|
||||||
|
|
||||||
// User manager
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UManager {
|
|
||||||
/// Users with incomplete authentication
|
|
||||||
pending: Arc<DashMap<String, String>>, // <SHA1 serverId, USERNAME> TODO: Add automatic purge
|
|
||||||
/// Authenticated users
|
|
||||||
authenticated: Arc<DashMap<String, Uuid>>, // <SHA1 serverId, Userinfo> NOTE: In the future, try it in a separate LockRw branch
|
|
||||||
/// Registered users
|
|
||||||
registered: Arc<DashMap<Uuid, Userinfo>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Userinfo {
|
|
||||||
pub username: String,
|
|
||||||
pub uuid: Uuid,
|
|
||||||
pub auth_system: AuthSystem,
|
|
||||||
pub token: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UManager {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
pending: Arc::new(DashMap::new()),
|
|
||||||
registered: Arc::new(DashMap::new()),
|
|
||||||
authenticated: Arc::new(DashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn pending_insert(&self, server_id: String, username: String) {
|
|
||||||
self.pending.insert(server_id, username);
|
|
||||||
}
|
|
||||||
pub fn pending_remove(&self, server_id: &str) -> std::option::Option<(std::string::String, std::string::String)> {
|
|
||||||
self.pending.remove(server_id)
|
|
||||||
}
|
|
||||||
pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) -> Option<Userinfo> {
|
|
||||||
self.authenticated.insert(token, uuid);
|
|
||||||
self.registered.insert(uuid, userinfo)
|
|
||||||
}
|
|
||||||
pub fn insert_user(&self, uuid: Uuid, userinfo: Userinfo) -> Option<Userinfo> {
|
|
||||||
self.registered.insert(uuid, userinfo)
|
|
||||||
}
|
|
||||||
pub fn get(
|
|
||||||
&self,
|
|
||||||
token: &String,
|
|
||||||
) -> Option<dashmap::mapref::one::Ref<'_, Uuid, Userinfo>> {
|
|
||||||
let uuid = self.authenticated.get(token)?;
|
|
||||||
self.registered.get(uuid.value())
|
|
||||||
}
|
|
||||||
pub fn get_by_uuid(
|
|
||||||
&self,
|
|
||||||
uuid: &Uuid,
|
|
||||||
) -> Option<dashmap::mapref::one::Ref<'_, Uuid, Userinfo>> {
|
|
||||||
self.registered.get(uuid)
|
|
||||||
}
|
|
||||||
pub fn is_authenticated(&self, token: &String) -> bool {
|
|
||||||
self.authenticated.contains_key(token)
|
|
||||||
}
|
|
||||||
pub fn _is_registered(&self, uuid: &Uuid) -> bool {
|
|
||||||
self.registered.contains_key(uuid)
|
|
||||||
}
|
|
||||||
pub fn remove(&self, uuid: &Uuid) {
|
|
||||||
let token = self.registered.remove(uuid).unwrap().1.token.unwrap();
|
|
||||||
self.authenticated.remove(&token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// End of User manager
|
|
||||||
151
src/auth/auth.rs
Normal file
151
src/auth/auth.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use axum::{
|
||||||
|
async_trait, extract::FromRequestParts, http::{request::Parts, StatusCode}
|
||||||
|
};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use tracing::{debug, trace};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
// It's an extractor that pulls a token from the Header.
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct Token(pub Option<String>);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for Token
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = StatusCode;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let token = parts
|
||||||
|
.headers
|
||||||
|
.get("token")
|
||||||
|
.and_then(|value| value.to_str().ok());
|
||||||
|
trace!(token = ?token);
|
||||||
|
match token {
|
||||||
|
Some(token) => Ok(Self(Some(token.to_string()))),
|
||||||
|
None => Ok(Self(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// End Extractor
|
||||||
|
|
||||||
|
// Work with external APIs
|
||||||
|
/// Get UUID from JSON response
|
||||||
|
#[inline]
|
||||||
|
fn get_id_json(json: &serde_json::Value) -> anyhow::Result<Uuid> {
|
||||||
|
trace!("json: {json:#?}"); // For debugging, we'll get to this later!
|
||||||
|
let uuid = Uuid::parse_str(json.get("id").unwrap().as_str().unwrap())?;
|
||||||
|
Ok(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
async fn fetch_json(
|
||||||
|
auth_system: AuthSystem,
|
||||||
|
server_id: &str,
|
||||||
|
username: &str,
|
||||||
|
) -> anyhow::Result<Option<(Uuid, AuthSystem)>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let url = auth_system.get_url();
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.get(url)
|
||||||
|
.query(&[("serverId", server_id), ("username", username)])
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
debug!("{res:?}");
|
||||||
|
match res.status().as_u16() {
|
||||||
|
200 => {
|
||||||
|
let json = serde_json::from_str::<serde_json::Value>(&res.text().await?)?;
|
||||||
|
let uuid = get_id_json(&json)?;
|
||||||
|
Ok(Some((uuid, auth_system)))
|
||||||
|
}
|
||||||
|
401 => Ok(None), // Ely.By None
|
||||||
|
204 => Ok(None), // Mojang None
|
||||||
|
_ => Err(anyhow!("Unknown code: {}", res.status().as_u16())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn has_joined(
|
||||||
|
server_id: &str,
|
||||||
|
username: &str,
|
||||||
|
) -> anyhow::Result<Option<(Uuid, AuthSystem)>> {
|
||||||
|
let (elyby, mojang) = (
|
||||||
|
fetch_json(AuthSystem::ElyBy,server_id, username).await?,
|
||||||
|
fetch_json(AuthSystem::Mojang, server_id, username).await?
|
||||||
|
);
|
||||||
|
|
||||||
|
if elyby.is_none() && mojang.is_none() {
|
||||||
|
Ok(None)
|
||||||
|
} else if mojang.is_some() {
|
||||||
|
Ok(mojang)
|
||||||
|
} else if elyby.is_some() {
|
||||||
|
Ok(elyby)
|
||||||
|
} else {
|
||||||
|
panic!("Impossible error!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// End of work with external APIs
|
||||||
|
|
||||||
|
// User manager
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UManager {
|
||||||
|
/// Users with incomplete authentication
|
||||||
|
pending: Arc<DashMap<String, String>>, // <SHA1 serverId, USERNAME> TODO: Add automatic purge
|
||||||
|
/// Authenticated users TODO: Change name to sessions
|
||||||
|
authenticated: Arc<DashMap<String, Uuid>>, // <SHA1 serverId, Userinfo> NOTE: In the future, try it in a separate LockRw branch
|
||||||
|
/// Registered users
|
||||||
|
registered: Arc<DashMap<Uuid, Userinfo>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
pending: Arc::new(DashMap::new()),
|
||||||
|
registered: Arc::new(DashMap::new()),
|
||||||
|
authenticated: Arc::new(DashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn pending_insert(&self, server_id: String, username: String) {
|
||||||
|
self.pending.insert(server_id, username);
|
||||||
|
}
|
||||||
|
pub fn pending_remove(&self, server_id: &str) -> std::option::Option<(std::string::String, std::string::String)> {
|
||||||
|
self.pending.remove(server_id)
|
||||||
|
}
|
||||||
|
pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) -> Option<Userinfo> {
|
||||||
|
self.authenticated.insert(token, uuid);
|
||||||
|
self.registered.insert(uuid, userinfo)
|
||||||
|
}
|
||||||
|
pub fn insert_user(&self, uuid: Uuid, userinfo: Userinfo) -> Option<Userinfo> {
|
||||||
|
self.registered.insert(uuid, userinfo)
|
||||||
|
}
|
||||||
|
pub fn get(
|
||||||
|
&self,
|
||||||
|
token: &String,
|
||||||
|
) -> Option<dashmap::mapref::one::Ref<'_, Uuid, Userinfo>> {
|
||||||
|
let uuid = self.authenticated.get(token)?;
|
||||||
|
self.registered.get(uuid.value())
|
||||||
|
}
|
||||||
|
pub fn get_by_uuid(
|
||||||
|
&self,
|
||||||
|
uuid: &Uuid,
|
||||||
|
) -> Option<dashmap::mapref::one::Ref<'_, Uuid, Userinfo>> {
|
||||||
|
self.registered.get(uuid)
|
||||||
|
}
|
||||||
|
pub fn is_authenticated(&self, token: &String) -> bool {
|
||||||
|
self.authenticated.contains_key(token)
|
||||||
|
}
|
||||||
|
pub fn _is_registered(&self, uuid: &Uuid) -> bool {
|
||||||
|
self.registered.contains_key(uuid)
|
||||||
|
}
|
||||||
|
pub fn remove(&self, uuid: &Uuid) {
|
||||||
|
let token = self.registered.remove(uuid).unwrap().1.token.unwrap();
|
||||||
|
self.authenticated.remove(&token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// End of User manager
|
||||||
5
src/auth/mod.rs
Normal file
5
src/auth/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
mod auth;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
pub use auth::*;
|
||||||
|
pub use types::*;
|
||||||
56
src/auth/types.rs
Normal file
56
src/auth/types.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Userinfo {
|
||||||
|
pub username: String,
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub auth_system: AuthSystem,
|
||||||
|
pub token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum AuthSystem {
|
||||||
|
Internal,
|
||||||
|
ElyBy,
|
||||||
|
Mojang,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for AuthSystem {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
AuthSystem::Internal => String::from("internal"),
|
||||||
|
AuthSystem::ElyBy => String::from("elyby"),
|
||||||
|
AuthSystem::Mojang => String::from("mojang"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for AuthSystem {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"internal" => Ok(Self::Internal),
|
||||||
|
"elyby" => Ok(Self::ElyBy),
|
||||||
|
"mojang" => Ok(Self::Mojang),
|
||||||
|
_ => Err(anyhow!("No auth system called: {s}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthSystem {
|
||||||
|
pub(super) fn get_url(&self) -> String {
|
||||||
|
match self {
|
||||||
|
AuthSystem::Internal => panic!("Can't get internal URL!"),
|
||||||
|
AuthSystem::ElyBy => String::from("http://minecraft.ely.by/session/hasJoined"),
|
||||||
|
AuthSystem::Mojang => String::from("https://sessionserver.mojang.com/session/minecraft/hasJoined"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
58
src/main.rs
58
src/main.rs
|
|
@ -9,29 +9,44 @@ use tower_http::trace::TraceLayer;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// WebSocket worker
|
// // WebSocket worker
|
||||||
mod ws;
|
// mod ws;
|
||||||
use ws::handler;
|
// use ws::handler;
|
||||||
|
|
||||||
// API: Auth
|
// // API: Auth
|
||||||
|
// mod auth;
|
||||||
|
// use auth::{self as api_auth, UManager};
|
||||||
|
|
||||||
|
// // API: Server info
|
||||||
|
// mod info;
|
||||||
|
// use info as api_info;
|
||||||
|
|
||||||
|
// // API: Profile
|
||||||
|
// mod profile;
|
||||||
|
// use profile as api_profile;
|
||||||
|
|
||||||
|
// API
|
||||||
|
mod api;
|
||||||
|
use api::{
|
||||||
|
figura::{ws, info as api_info, profile as api_profile, auth as api_auth},
|
||||||
|
// v1::{},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auth
|
||||||
mod auth;
|
mod auth;
|
||||||
use auth::{self as api_auth, UManager};
|
use auth::UManager;
|
||||||
|
|
||||||
// API: Server info
|
// Config
|
||||||
mod info;
|
mod state;
|
||||||
use info as api_info;
|
use state::Config;
|
||||||
|
|
||||||
// API: Profile
|
|
||||||
mod profile;
|
|
||||||
use profile as api_profile;
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
mod utils;
|
mod utils;
|
||||||
use utils::update_advanced_users;
|
use utils::update_advanced_users;
|
||||||
|
|
||||||
// Config
|
// // Config
|
||||||
mod config;
|
// mod config;
|
||||||
use config::Config;
|
// use config::Config;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
|
|
@ -42,7 +57,7 @@ pub struct AppState {
|
||||||
/// Ping broadcasts for WebSocket connections
|
/// Ping broadcasts for WebSocket connections
|
||||||
broadcasts: Arc<DashMap<Uuid, broadcast::Sender<Vec<u8>>>>,
|
broadcasts: Arc<DashMap<Uuid, broadcast::Sender<Vec<u8>>>>,
|
||||||
/// Current configuration
|
/// Current configuration
|
||||||
config: Arc<Mutex<config::Config>>,
|
config: Arc<Mutex<state::Config>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOGGER_ENV: &'static str = "RUST_LOG";
|
const LOGGER_ENV: &'static str = "RUST_LOG";
|
||||||
|
|
@ -94,13 +109,9 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let v1 = Router::new()
|
|
||||||
.nest("/", ws::http2ws_router())
|
|
||||||
.nest("/user", api_auth::router_v1());
|
|
||||||
|
|
||||||
let api = Router::new()
|
let api = Router::new()
|
||||||
.nest("//auth", api_auth::router())
|
.nest("//auth", api_auth::router())
|
||||||
.nest("/v1", v1)
|
.nest("/v1", api::v1::router())
|
||||||
.route("/limits", get(api_info::limits))
|
.route("/limits", get(api_info::limits))
|
||||||
.route("/version", get(api_info::version))
|
.route("/version", get(api_info::version))
|
||||||
.route("/motd", get(api_info::motd))
|
.route("/motd", get(api_info::motd))
|
||||||
|
|
@ -112,10 +123,9 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/api", api)
|
.nest("/api", api)
|
||||||
.route("/api/", get(api_auth::status))
|
.route("/ws", get(ws))
|
||||||
.route("/ws", get(handler))
|
|
||||||
.route("/health", get(|| async { "ok" }))
|
.route("/health", get(|| async { "ok" }))
|
||||||
.route_layer(from_extractor::<api_auth::Token>())
|
.route_layer(from_extractor::<auth::Token>())
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
.layer(TraceLayer::new_for_http().on_request(()));
|
.layer(TraceLayer::new_for_http().on_request(()));
|
||||||
|
|
||||||
|
|
|
||||||
4
src/state/mod.rs
Normal file
4
src/state/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
mod config;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
pub use config::*;
|
||||||
0
src/state/state.rs
Normal file
0
src/state/state.rs
Normal file
|
|
@ -1,8 +0,0 @@
|
||||||
mod types;
|
|
||||||
mod websocket;
|
|
||||||
mod http;
|
|
||||||
|
|
||||||
pub use types::C2SMessage;
|
|
||||||
pub use types::S2CMessage;
|
|
||||||
pub use websocket::handler;
|
|
||||||
pub use http::router as http2ws_router;
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue