Skip to content

Commit

Permalink
i wonder how many changes will github show
Browse files Browse the repository at this point in the history
  • Loading branch information
dankmeme01 committed Dec 14, 2023
1 parent 5b01d8a commit fa8c672
Show file tree
Hide file tree
Showing 71 changed files with 1,035 additions and 616 deletions.
2 changes: 1 addition & 1 deletion server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["central", "game", "game-derive", "shared"]
members = ["central", "esp", "game", "derive", "shared"]
resolver = "2"

# TODO bring back in release
Expand Down
3 changes: 0 additions & 3 deletions server/central/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@ globed-shared = { path = "../shared" }
anyhow = "1.0.75"
async-rate-limit = "0.0.3"
async-watcher = "0.2.0"
base64 = "0.21.5"
blake2 = "0.10.6"
digest = "0.10.7"
hmac = "0.12.1"
ipnet = "2.9.0"
iprange = "0.6.7"
rand = "0.8.5"
reqwest = "0.11.22"
roa = { version = "0.6.1", features = ["router"] }
serde = { version = "1.0.193", features = ["serde_derive"] }
serde_json = "1.0.108"
sha2 = "0.10.8"
tokio = { version = "1.35.0", features = ["full"] }
totp-rs = "5.4.0"
2 changes: 2 additions & 0 deletions server/central/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ pub struct ServerConfig {
#[serde(default = "default_secret_key")]
pub secret_key: String,
#[serde(default = "default_secret_key")]
pub secret_key2: String,
#[serde(default = "default_secret_key")]
pub game_server_password: String,
#[serde(default = "default_cloudflare_protection")]
pub cloudflare_protection: bool,
Expand Down
45 changes: 39 additions & 6 deletions server/central/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
clippy::must_use_candidate,
clippy::module_name_repetitions,
clippy::missing_errors_doc,
clippy::missing_panics_doc
clippy::missing_panics_doc,
clippy::wildcard_imports
)]

use std::{error::Error, path::PathBuf, time::Duration};
use std::{
error::Error,
net::{IpAddr, SocketAddr},
path::PathBuf,
time::Duration,
};

use async_watcher::{notify::RecursiveMode, AsyncDebouncer};
use config::ServerConfig;
Expand Down Expand Up @@ -69,8 +75,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
let mnt_point = config.web_mountpoint.clone();

let state_skey = config.secret_key.clone();
let state_skey2 = config.secret_key2.clone();

let state = ServerState::new(ServerStateData::new(config_path.clone(), config, &state_skey));
let state = ServerState::new(ServerStateData::new(config_path.clone(), config, &state_skey, &state_skey2));

// config file watcher

Expand Down Expand Up @@ -103,10 +110,36 @@ async fn main() -> Result<(), Box<dyn Error>> {
let route_list = router.routes(Box::leak(Box::new(mnt_point)))?;
let app = App::state(state).end(route_list);

app.listen(mnt_addr, |addr| {
let listen_addr = match mnt_addr.parse::<SocketAddr>() {
Ok(x) => x,
Err(err) => {
// try parse it as an IP address and use port 41000
if let Ok(x) = mnt_addr.parse::<IpAddr>() {
SocketAddr::new(x, 41000)
} else {
error!("failed to parse the HTTP bind address: {err}");
warn!("hint: the address must be a valid IPv4/IPv6 address with an optional port number");
warn!("hint: examples include \"127.0.0.1\", \"0.0.0.0:41000\"");
abort_misconfig();
}
}
};

let server = match app.listen(listen_addr, |addr| {
info!("Globed central server launched on {addr}");
})?
.await?;
}) {
Ok(x) => x,
Err(e) => {
error!("failed to setup the HTTP server with address {listen_addr}: {e}");
if listen_addr.port() < 1024 {
warn!("hint: ports below 1024 are commonly privileged and you can't use them as a regular user");
warn!("hint: pick a higher port number or leave it out completely to use the default port number (41000)");
}
abort_misconfig();
}
};

server.await?;

Ok(())
}
68 changes: 10 additions & 58 deletions server/central/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ use std::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
time::{Duration, Instant},
};

use anyhow::anyhow;
use async_rate_limit::sliding_window::SlidingWindowRateLimiter;
use base64::{engine::general_purpose as b64e, Engine};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use globed_shared::{
hmac::{Hmac, Mac},
sha2::Sha256,
TokenIssuer,
};
use tokio::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};

use crate::config::{ServerConfig, UserlistMode};
Expand All @@ -32,26 +34,29 @@ pub struct ServerStateData {
pub config_path: PathBuf,
pub config: ServerConfig,
pub hmac: Hmac<Sha256>,
pub token_issuer: TokenIssuer,
pub active_challenges: HashMap<IpAddr, ActiveChallenge>,
pub http_client: reqwest::Client,
pub login_attempts: HashMap<IpAddr, Instant>,
pub ratelimiter: Arc<Mutex<SlidingWindowRateLimiter>>,
}

impl ServerStateData {
pub fn new(config_path: PathBuf, config: ServerConfig, secret_key: &str) -> Self {
pub fn new(config_path: PathBuf, config: ServerConfig, secret_key: &str, token_secret_key: &str) -> Self {
let skey_bytes = secret_key.as_bytes();
let hmac = Hmac::<Sha256>::new_from_slice(skey_bytes).unwrap();

let http_client = reqwest::ClientBuilder::new().user_agent("").build().unwrap();

let api_rl = config.gd_api_ratelimit;
let api_rl_period = config.gd_api_period;
let token_expiry = Duration::from_secs(config.token_expiry);

Self {
config_path,
config,
hmac,
token_issuer: TokenIssuer::new(token_secret_key, token_expiry),
active_challenges: HashMap::new(),
http_client,
login_attempts: HashMap::new(),
Expand Down Expand Up @@ -88,59 +93,6 @@ impl ServerStateData {
self.verify_totp(orig_value.as_bytes(), answer)
}

// generate a token, similar to jwt but more efficient and lightweight
pub fn generate_token(&self, account_id: i32, account_name: &str) -> String {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("whoops our clock went backwards")
.as_secs();

let data = format!("{account_id}.{account_name}.{timestamp}");
let mut hmac = self.hmac.clone();
hmac.update(data.as_bytes());
let res = hmac.finalize();

format!(
"{}.{}",
b64e::URL_SAFE_NO_PAD.encode(data),
b64e::URL_SAFE_NO_PAD.encode(res.into_bytes())
)
}

// verify the token and return the username if it's valid
pub fn verify_token(&self, account_id: i32, token: &str) -> anyhow::Result<String> {
let timestamp = SystemTime::now();

let (claims, signature) = token.split_once('.').ok_or(anyhow!("malformed token"))?;

let data_str = String::from_utf8(b64e::URL_SAFE_NO_PAD.decode(claims)?)?;
let mut claims = data_str.split('.');

let orig_id = claims.next().ok_or(anyhow!("malformed token"))?.parse::<i32>()?;
let orig_name = claims.next().ok_or(anyhow!("malformed token"))?;
let orig_ts = claims.next().ok_or(anyhow!("malformed token"))?;

if orig_id != account_id {
return Err(anyhow!("token validation failed"));
}

let elapsed = timestamp.duration_since(UNIX_EPOCH + Duration::from_secs(orig_ts.parse::<u64>()?))?;

if elapsed > Duration::from_secs(self.config.token_expiry) {
return Err(anyhow!("expired token, please reauthenticate"));
}

// verify the signature
let mut hmac = self.hmac.clone();
hmac.update(data_str.as_bytes());

let signature = b64e::URL_SAFE_NO_PAD.decode(signature)?;

hmac.verify_slice(&signature)?;

Ok(orig_name.to_string())
}

pub fn is_ratelimited(&self, addr: &IpAddr) -> bool {
match self.login_attempts.get(addr) {
None => false,
Expand Down
1 change: 0 additions & 1 deletion server/central/src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ pub mod routes {
.on("/challenge/verify", post(auth::challenge_finish))
/* game-server api, not for the end user */
.on("/gs/boot", post(game_server::boot))
.on("/gs/verify", post(game_server::verify_token))
}

macro_rules! check_maintenance {
Expand Down
8 changes: 5 additions & 3 deletions server/central/src/web/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ use std::{

use anyhow::anyhow;
use async_rate_limit::limiters::VariableCostRateLimiter;
use base64::{engine::general_purpose as b64e, Engine as _};
use globed_shared::logger::{debug, info, log, warn};
use globed_shared::{
base64::{engine::general_purpose as b64e, Engine as _},
logger::*,
};
use rand::{distributions::Alphanumeric, Rng};
use roa::{http::StatusCode, preload::PowerBody, query::Query, throw, Context};

Expand Down Expand Up @@ -95,7 +97,7 @@ pub async fn totp_login(context: &mut Context<ServerState>) -> roa::Result {
throw!(StatusCode::UNAUTHORIZED, "login failed");
}

let token = state.generate_token(account_id, account_name);
let token = state.token_issuer.generate(account_id, account_name);
drop(state);

debug!("totp login from {} ({}) successful", account_name, account_id);
Expand Down
45 changes: 14 additions & 31 deletions server/central/src/web/routes/game_server.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use globed_shared::{logger::debug, GameServerBootData, PROTOCOL_VERSION};
use globed_shared::{
esp::{ByteBuffer, ByteBufferExtWrite},
logger::debug,
GameServerBootData, PROTOCOL_VERSION,
};
use reqwest::StatusCode;
use roa::{preload::PowerBody, query::Query, throw, Context};

Expand Down Expand Up @@ -53,6 +57,8 @@ pub async fn boot(context: &mut Context<ServerState>) -> roa::Result {
special_users: config.special_users.clone(),
tps: config.tps,
maintenance: config.maintenance,
secret_key2: config.secret_key2.clone(),
token_expiry: config.token_expiry,
};

debug!(
Expand All @@ -61,38 +67,15 @@ pub async fn boot(context: &mut Context<ServerState>) -> roa::Result {
context.remote_addr.ip()
);

let bdata = serde_json::to_string(&bdata)?;
let mut bb = ByteBuffer::new();
bb.write_value(&bdata);

drop(state);
context.write(bdata);

Ok(())
}

pub async fn verify_token(context: &mut Context<ServerState>) -> roa::Result {
check_user_agent!(context, _ua);

let password = &*context.must_query("pw")?;
let correct = context.state_read().await.config.game_server_password.clone();

if !verify_password(password, &correct) {
throw!(StatusCode::UNAUTHORIZED, "invalid gameserver credentials");
}

let account_id = &*context.must_query("account_id")?;
let account_id = account_id.parse::<i32>()?;
let token = &*context.must_query("token")?;

let result = context.state_read().await.verify_token(account_id, token);

match result {
Ok(name) => {
context.write(format!("status_ok:{name}"));
}
Err(e) => {
context.write(e.to_string());
}
}
context.write(bb.into_vec());
context
.resp
.headers
.insert(roa::http::header::CONTENT_TYPE, "application/octet-stream".parse().unwrap());

Ok(())
}
File renamed without changes.
Loading

0 comments on commit fa8c672

Please sign in to comment.