Начало положено...

This commit is contained in:
Shiroyasha 2024-05-13 11:48:46 +03:00
commit 3fd49300db
Signed by: shiroyashik
GPG key ID: E4953D3940D7860A
12 changed files with 2361 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
/Assets-main
output.log

1801
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

36
Cargo.toml Normal file
View file

@ -0,0 +1,36 @@
[package]
name = "sculptor"
authors = ["Shiroyashik <shiroyashik@shsr.ru>"]
version = "0.1.0"
edition = "2021"
publish = false
[workspace]
members = ["elyby-api"]
[dependencies]
# Logging
log = "0.4.21"
fern = { version = "0.6.2", features = ["colored"] }
# Errors
anyhow = "1.0.83"
# Serialization
chrono = { version = "0.4.38", features = ["now"] }
serde = { version = "1.0.201", features = ["derive"] }
# Other
elyby-api = { path = "./elyby-api" }
dashmap = "5.5.3"
# Crypto
ring = "0.17.8"
rand = "0.8.5"
# Web framework
axum = { version = "0.7.5", features = ["ws", "macros"] }
tower-http = { version = "0.5.2", features = ["trace"] }
tokio = { version = "1.37.0", features = ["full"] }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

14
elyby-api/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "elyby-api"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.83"
log = "0.4.21"
reqwest = "0.12.4"
serde = { version = "1.0.201", features = ["derive"] }
serde_json = "1.0.117"
tokio = { version = "1.37.0", features = ["full"] }

31
elyby-api/src/lib.rs Normal file
View file

@ -0,0 +1,31 @@
use anyhow::{anyhow, Result};
use log::debug;
pub async fn has_joined(server_id: &str, username: &str) -> Result<bool> {
let client = reqwest::Client::new();
let res = client.get(
format!("http://minecraft.ely.by/session/hasJoined?serverId={server_id}&username={username}")).send().await?;
debug!("{res:?}");
match res.status().as_u16() {
200 => Ok(true),
401 => Ok(false),
_ => Err(anyhow!("Unknown code: {}", res.status().as_u16()))
}
}
#[tokio::test]
async fn test_has_joined() {
let result = has_joined("0f8fef917f1f62b963804d822b67fe6f59aad7d", "test").await.unwrap();
assert_eq!(result, false)
}
// #[cfg(test)]
// mod tests {
// use super::*;
// #[test]
// fn it_works() {
// let result = add(2, 2);
// assert_eq!(result, 4);
// }
// }

63
src/auth.rs Normal file
View file

@ -0,0 +1,63 @@
use axum::{extract::{Query, State}, routing::get, Router, debug_handler};
use serde::Deserialize;
use ring::digest::{self, digest};
use crate::AppState;
pub fn router() -> Router<AppState> {
Router::new()
.route("/id", get(id))
.route("/verify", get(verify))
}
#[derive(Deserialize)]
struct Id {username: String}
#[debug_handler]
async fn id(
Query(query): Query<Id>,
State(state): State<AppState>,
) -> String {
let server_id = bytes_into_string(&digest(&digest::SHA1_FOR_LEGACY_USE_ONLY, &rand()).as_ref()[0 .. 20]);
let state = state.pending.lock().expect("Mutex poisoned!");
state.insert(server_id.clone(), query.username);
server_id
}
#[derive(Deserialize)]
struct Verify {id: String}
#[debug_handler]
async fn verify(
Query(query): Query<Verify>,
State(state): State<AppState>,
) -> String {
let server_id = query.id.clone();
let username = state.pending.lock().expect("Mutex poisoned!").remove(&server_id).unwrap().1;
if !elyby_api::has_joined(&server_id, &username).await.unwrap() {
return String::from("failed to verify")
}
let authenticated = state.authenticated.lock().expect("Mutex poisoned!");
authenticated.insert(server_id.clone(), username);
format!("{server_id}")
}
fn rand() -> [u8; 50] {
use rand::{Rng, thread_rng};
let mut rng = thread_rng();
let distr = rand::distributions::Uniform::new_inclusive(0, 255);
let mut nums: [u8; 50] = [0u8; 50];
for x in &mut nums {
*x = rng.sample(distr);
}
nums
}
pub fn bytes_into_string(code: &[u8]) -> String {
use std::fmt::Write;
let mut result = String::new();
for byte in code {
write!(result, "{:02x}", byte).unwrap();
}
result
}

137
src/main.rs Normal file
View file

@ -0,0 +1,137 @@
use anyhow::Result;
use axum::{
extract::Path,
routing::{delete, get, post, put},
Router,
};
use chrono::prelude::*;
use dashmap::DashMap;
use fern::colors::{Color, ColoredLevelConfig};
use log::info;
use std::sync::{Arc, Mutex};
use tower_http::trace::TraceLayer;
// WebSocket worker
mod ws;
use ws::handler;
// API
mod auth;
use auth as api_auth;
#[derive(Debug, Clone)]
pub struct Userinfo {
id: usize
}
#[derive(Debug, Clone)]
pub struct AppState {
authenticated: Arc<Mutex<DashMap<String, String>>>, // <SHA1, USERNAME>
pending: Arc<Mutex<DashMap<String, String>>>
}
#[tokio::main]
async fn main() -> Result<()> {
println!("The Sculptor");
let colors = ColoredLevelConfig::new()
.info(Color::Green)
.debug(Color::Magenta)
.trace(Color::Cyan)
.warn(Color::Yellow);
fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
Local::now().to_rfc3339_opts(SecondsFormat::Millis, true),
colors.color(record.level()),
record.target(),
message
))
})
.level(log::LevelFilter::Debug)
// .level_for("hyper", log::LevelFilter::Info)
.chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?;
// Config init here
let listen = "0.0.0.0:6665";
// State init here
let state = AppState {
authenticated: Arc::new(Mutex::new(DashMap::new())),
pending: Arc::new(Mutex::new(DashMap::new()))
};
let api = Router::new()
.nest(
"//auth",
api_auth::router()
) // check Auth; return 200 OK if token valid
.route(
"/limits",
get(|| async { "@toomanylimits" })
) // Need more info :( TODO:
.route(
"/version",
get(|| async { "{\"release\":\"2.7.1\",\"prerelease\":\"2.7.1\"}" }),
)
.route(
"/motd",
get(|| async { "\"written by an black cat :3 mew\"" }),
)
.route(
"/equip",
post(|| async { "Do it! NOW!" })
) // set Equipped; TODO:
.route(
"/:owner/:id",
get(|Path((owner, id)): Path<(String, String)>| async move {
format!("getting user {id}, owner {owner}")
}),
) // get Avatar
.route(
"/:avatar",
put(|Path(avatar): Path<String>| async move { format!("put {avatar}") }),
) // put Avatar
.route(
"/:avatar",
delete(|Path(avatar): Path<String>| async move { format!("delete {avatar}") }),
); // delete Avatar
let app = Router::new()
.nest("/api", api)
.route("/ws", get(handler))
.layer(TraceLayer::new_for_http().on_request(()))
.with_state(state);
let listener = tokio::net::TcpListener::bind(listen).await?;
info!("Listening on {}", listener.local_addr()?);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
info!("Serve stopped. Closing...");
Ok(())
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => {},
() = terminate => {},
}
info!("Terminate signal received");
}

89
src/ws/c2s.rs Normal file
View file

@ -0,0 +1,89 @@
use super::MessageLoadError;
use std::convert::{TryFrom, TryInto};
#[repr(u8)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum C2SMessage<'a> {
Token(&'a [u8]) = 0,
Ping(u32, bool, &'a [u8]) = 1,
Sub(u128) = 2, // owo
Unsub(u128) = 3,
}
// 6 - 6
impl<'a> TryFrom<&'a [u8]> for C2SMessage<'a> {
type Error = MessageLoadError;
fn try_from(buf: &'a [u8]) -> Result<Self, <Self as TryFrom<&'a [u8]>>::Error> {
if buf.len() == 0 {
Err(MessageLoadError::BadLength("C2SMessage", 1, false, 0))
} else {
match buf[0] {
0 => Ok(C2SMessage::Token(&buf[1..])),
1 => {
if buf.len() >= 6 {
Ok(C2SMessage::Ping(
u32::from_be_bytes((&buf[1..5]).try_into().unwrap()),
buf[5] != 0,
&buf[6..],
))
} else {
Err(MessageLoadError::BadLength(
"C2SMessage::Ping",
6,
false,
buf.len(),
))
}
}
2 => {
if buf.len() == 17 {
Ok(C2SMessage::Sub(u128::from_be_bytes(
(&buf[1..]).try_into().unwrap(),
)))
} else {
Err(MessageLoadError::BadLength(
"C2SMessage::Sub",
17,
true,
buf.len(),
))
}
}
3 => {
if buf.len() == 17 {
Ok(C2SMessage::Unsub(u128::from_be_bytes(
(&buf[1..]).try_into().unwrap(),
)))
} else {
Err(MessageLoadError::BadLength(
"C2SMessage::Unsub",
17,
true,
buf.len(),
))
}
}
a => Err(MessageLoadError::BadEnum(
"C2SMessage.type",
0..=3,
a.into(),
)),
}
}
}
}
impl<'a> Into<Box<[u8]>> for C2SMessage<'a> {
fn into(self) -> Box<[u8]> {
use std::iter;
let a: Box<[u8]> = match self {
C2SMessage::Token(t) => iter::once(0).chain(t.into_iter().copied()).collect(),
C2SMessage::Ping(p, s, d) => iter::once(1)
.chain(p.to_be_bytes())
.chain(iter::once(s.into()))
.chain(d.into_iter().copied())
.collect(),
C2SMessage::Sub(s) => iter::once(2).chain(s.to_be_bytes()).collect(),
C2SMessage::Unsub(s) => iter::once(3).chain(s.to_be_bytes()).collect(),
};
a
}
}

42
src/ws/errors.rs Normal file
View file

@ -0,0 +1,42 @@
use std::fmt::*;
use std::ops::RangeInclusive;
#[derive(Debug)]
pub enum MessageLoadError {
BadEnum(&'static str, RangeInclusive<usize>, usize),
BadLength(&'static str, usize, bool, usize),
}
impl Display for MessageLoadError {
fn fmt(&self, fmt: &mut Formatter) -> Result {
match self {
Self::BadEnum(f, r, c) => write!(
fmt,
"invalid value of {f}: must be {} to {} inclusive, got {c}",
r.start(),
r.end()
),
Self::BadLength(f, n, e, c) => write!(
fmt,
"buffer wrong size for {f}: must be {} {n} bytes, got {c}",
if *e { "exactly" } else { "at least" }
),
}
}
}
#[cfg(test)]
#[test]
fn message_load_error_display() {
use MessageLoadError::*;
assert_eq!(
BadEnum("foo", 3..=5, 7).to_string(),
"invalid value of foo: must be 3 to 5 inclusive, got 7"
);
assert_eq!(
BadLength("bar", 17, false, 12).to_string(),
"buffer wrong size for bar: must be at least 17 bytes, got 12"
);
assert_eq!(
BadLength("bar", 17, true, 19).to_string(),
"buffer wrong size for bar: must be exactly 17 bytes, got 19"
);
}

51
src/ws/handler.rs Normal file
View file

@ -0,0 +1,51 @@
use axum::{extract::{ws::{Message, WebSocket}, WebSocketUpgrade}, response::Response};
use log::{error, info, warn};
use crate::ws::C2SMessage;
pub async fn handler(ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(handle_socket)
}
async fn handle_socket(mut socket: WebSocket) {
while let Some(msg) = socket.recv().await {
info!("{msg:?}");
let mut msg = if let Ok(msg) = msg {
msg
} else {
// if reached here - client disconnected
warn!("ws disconnected!");
return;
};
// Work with code here
let msg_array = msg.clone().into_data();
let msg_array = msg_array.as_slice();
let newmsg = match C2SMessage::try_from(msg_array) {
Ok(data) => data,
Err(e) => {
error!("MessageLoadError: {e:?}");
return;
},
};
match newmsg {
C2SMessage::Token(token) => {
// TODO: Authenticated check
msg = Message::Binary(vec![0])
},
// C2SMessage::Ping(_, _, _) => todo!(),
// C2SMessage::Sub(_) => todo!(),
// C2SMessage::Unsub(_) => todo!(),
_ => ()
}
info!("{newmsg:?}");
if socket.send(msg).await.is_err() {
// if reached here - client disconnected
warn!("ws disconnected!");
return;
}
}
}

9
src/ws/mod.rs Normal file
View file

@ -0,0 +1,9 @@
mod c2s;
mod s2c;
mod handler;
mod errors;
pub use c2s::C2SMessage;
pub use s2c::S2CMessage;
pub use handler::handler;
pub use errors::MessageLoadError;

85
src/ws/s2c.rs Normal file
View file

@ -0,0 +1,85 @@
use super::MessageLoadError;
use std::convert::{TryFrom, TryInto};
#[repr(u8)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum S2CMessage<'a> {
Auth = 0,
Ping(u128, u32, bool, &'a [u8]) = 1,
Event(u128) = 2,
Toast(u8, &'a str, Option<&'a str>) = 3,
Chat(&'a str) = 4,
Notice(u8) = 5,
}
impl<'a> TryFrom<&'a [u8]> for S2CMessage<'a> {
type Error = MessageLoadError;
fn try_from(buf: &'a [u8]) -> Result<Self, <Self as TryFrom<&'a [u8]>>::Error> {
if buf.len() == 0 {
Err(MessageLoadError::BadLength("S2CMessage", 1, false, 0))
} else {
use MessageLoadError::*;
use S2CMessage::*;
match buf[0] {
0 => {
if buf.len() == 1 {
Ok(Auth)
} else {
Err(BadLength("S2CMessage::Auth", 1, true, buf.len()))
}
}
1 => {
if buf.len() >= 22 {
Ok(Ping(
u128::from_be_bytes((&buf[1..17]).try_into().unwrap()),
u32::from_be_bytes((&buf[17..21]).try_into().unwrap()),
buf[21] != 0,
&buf[22..],
))
} else {
Err(BadLength("S2CMessage::Ping", 22, false, buf.len()))
}
}
2 => {
if buf.len() == 17 {
Ok(Event(u128::from_be_bytes(
(&buf[1..17]).try_into().unwrap(),
)))
} else {
Err(BadLength("S2CMessage::Event", 17, true, buf.len()))
}
}
3 => todo!(),
4 => todo!(),
5 => todo!(),
a => Err(BadEnum("S2CMessage.type", 0..=5, a.into())),
}
}
}
}
impl<'a> Into<Box<[u8]>> for S2CMessage<'a> {
fn into(self) -> Box<[u8]> {
use std::iter::once;
use S2CMessage::*;
match self {
Auth => Box::new([0]),
Ping(u, i, s, d) => once(1)
.chain(u.to_be_bytes().iter().copied())
.chain(i.to_be_bytes().iter().copied())
.chain(once(if s { 1 } else { 0 }))
.chain(d.into_iter().copied())
.collect(),
Event(u) => once(2).chain(u.to_be_bytes().iter().copied()).collect(),
Toast(t, h, d) => once(3)
.chain(once(t))
.chain(h.as_bytes().into_iter().copied())
.chain(
d.into_iter()
.map(|s| once(0).chain(s.as_bytes().into_iter().copied()))
.flatten(),
)
.collect(),
Chat(c) => once(4).chain(c.as_bytes().iter().copied()).collect(),
Notice(t) => Box::new([5, t]),
}
}
}