This commit is contained in:
Mrrp 2025-03-17 18:52:48 -07:00
parent 33a2e3156e
commit 7bb564f91e
Signed by: SevenOfAces
GPG key ID: 5270C09105320F6F
16 changed files with 1877 additions and 89 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

3
.gitignore vendored
View file

@ -1 +1,2 @@
target target
.direnv

1288
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,17 +8,22 @@ license = "MIT License"
# Used by library # Used by library
macaddr = "1.0.1" macaddr = "1.0.1"
log = "0.4" log = "0.4"
actix-web = "4"
# Used by server runtime # Used by server runtime
fern = { optional = true, version = "0.7" } fern = { optional = true, version = "0.7" }
[dev-dependencies] [dev-dependencies]
stopwatch = "0.0.7" stopwatch = "0.0.7"
rand = "0.9" rand = "0.9"
[features] [features]
default = ["client", "server"]
bin-deps = ["fern"] bin-deps = ["fern"]
client = []
server = []
[[bin]] [[bin]]
name = "matchmaker_server" name = "matchmaker_server"
path = "src/main.rs" path = "src/main.rs"
required-features = [] required-features = ["server"]

49
docs/WEBAPI_V1.md Normal file
View file

@ -0,0 +1,49 @@
# Web API Documentation - Draft
## /api/info
### GET
```json
{
"supportedVersions": [
"<VERSION IDENTIFIER>"
],
"partitions": [
"us",
"uk"
// ...
],
"heartbeatTimeoutDuration": 7
}
```
## /api/query
### POST
```json
{
// if this is unspecified/null, infer a default partition
"partition": "us",
""
// Validated against a server-side maximum and minimum
"count": 7,
// Validated to prevent out-of-bounds issues.
// This allows reading a slice of the instances,
// allowing pagination.
"offset": 0
}
```
## /api/connect
## /api/heartbeat
```json
{
"token":
}
```

4
rust-toolchain.toml Normal file
View file

@ -0,0 +1,4 @@
[toolchain]
channel = "1.81"
components = ["rustfmt"]
profile = "minimal"

View file

@ -1,4 +1,40 @@
{ pkgs ? import <nixpkgs> {} }: { pkgs ? import <nixpkgs> {} }:
pkgs.mkShell { let
buildInputs = [ pkgs.cargo pkgs.rustc ]; overrides = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml));
} libPath = with pkgs; lib.makeLibraryPath [
# load external libraries that you need in your rust project here
];
in
pkgs.mkShell rec {
buildInputs = with pkgs; [
clang
# Replace llvmPackages with llvmPackages_X, where X is the latest LLVM version (at the time of writing, 16)
llvmPackages.bintools
rustup
];
RUSTC_VERSION = overrides.toolchain.channel;
# https://github.com/rust-lang/rust-bindgen#environment-variables
LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ];
shellHook = ''
export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin
export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/
'';
# Add precompiled library to rustc search path
RUSTFLAGS = (builtins.map (a: ''-L ${a}/lib'') [
# add libraries here (e.g. pkgs.libvmi)
]);
LD_LIBRARY_PATH = libPath;
# Add glibc, clang, glib, and other headers to bindgen search path
BINDGEN_EXTRA_CLANG_ARGS =
# Includes normal include path
(builtins.map (a: ''-I"${a}/include"'') [
# add dev libraries here (e.g. pkgs.libvmi.dev)
pkgs.glibc.dev
])
# Includes with special directory paths
++ [
''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"''
''-I"${pkgs.glib.dev}/include/glib-2.0"''
''-I${pkgs.glib.out}/lib/glib-2.0/include/''
];
}

View file

@ -1,28 +0,0 @@
use auth::AuthGuard;
use util::Address;
pub mod util;
pub mod auth;
pub enum DataStorageError {
AlreadyExists,
AntiSpam(String)
}
pub trait DataStorageItem {
type Id;
type Address: Address;
fn id(&self) -> Self::Id;
fn address_info(&self) -> Self::Address;
}
pub trait DataStorage<Auth: AuthGuard, Item: DataStorageItem>: IntoIterator<Item = Item> {
fn update_instance(&mut self, address: &impl Address, guard: Option<Auth>)
-> Result<Item::Id, DataStorageError>;
fn send_heartbeat(&mut self, address: &impl Address)
-> Result<(),DataStorageError>;
fn peek_instance(&self, id: Item::Id);
}

View file

@ -1,33 +0,0 @@
use std::net::IpAddr;
use macaddr::MacAddr;
pub trait Address: PartialEq {
fn mac(&self) -> MacAddr;
fn ip(&self) -> IpAddr;
}
#[derive(PartialEq, Debug)]
struct SimpleAddress {
mac: MacAddr,
ip: IpAddr
}
impl SimpleAddress {
fn new(ip: IpAddr, mac: MacAddr) -> SimpleAddress {
return SimpleAddress {
ip,
mac
}
}
}
impl Address for SimpleAddress {
fn mac(&self) -> MacAddr {
self.mac
}
fn ip(&self) -> IpAddr {
self.ip
}
}

View file

@ -1,9 +1,15 @@
pub mod data; pub mod server;
pub enum MatchmakerError {
AlreadyExists,
InternalError(String),
LimitReached(String),
Unauthorized,
AntiSpam(String),
}
pub trait Matchmaker { pub trait Matchmaker {
fn run(&mut self); fn run(&mut self);
} }
struct DefaultMatchmaker { struct DefaultMatchmaker {}
}

View file

@ -1,3 +1 @@
fn main() { fn main() {}
}

33
src/server/access.rs Normal file
View file

@ -0,0 +1,33 @@
/* -------------------------------------------------------------------------- */
/* Interfaces */
/* -------------------------------------------------------------------------- */
use std::collections::HashMap;
use super::util::Address;
pub struct AccessInfo {
address: Address,
}
pub trait AccessGuard {
fn check(&self, info: &AccessInfo);
}
pub enum AccessAction {
Allow,
Block,
}
/* -------------------------------------------------------------------------- */
/* Rate Limiting */
/* -------------------------------------------------------------------------- */
struct RateLimitAccessor {}
pub struct RateLimitAccessGuard {
max_read: i32,
max_write: i32,
max_accessors: i32,
accessors: HashMap<Address, RateLimitAccessor>,
}

View file

@ -185,7 +185,7 @@ mod tests {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CodeAuthKey { pub struct CodeAuthKey {
key: u64 key: u64,
} }
impl CodeAuthKey { impl CodeAuthKey {
@ -197,7 +197,7 @@ impl CodeAuthKey {
pub fn new<T: Hash + ?Sized>(code: &T) -> CodeAuthKey { pub fn new<T: Hash + ?Sized>(code: &T) -> CodeAuthKey {
CodeAuthKey { CodeAuthKey {
key: Self::_generate_key(code) key: Self::_generate_key(code),
} }
} }
} }
@ -211,20 +211,18 @@ impl PartialEq for CodeAuthKey {
} }
pub struct CodeAuthGuard { pub struct CodeAuthGuard {
reference_key: CodeAuthKey reference_key: CodeAuthKey,
} }
impl CodeAuthGuard { impl CodeAuthGuard {
fn new(key: CodeAuthKey) -> Self { fn new(key: CodeAuthKey) -> Self {
Self { Self { reference_key: key }
reference_key: key
}
} }
} }
impl AuthGuard for CodeAuthGuard { impl AuthGuard for CodeAuthGuard {
type Key = CodeAuthKey; type Key = CodeAuthKey;
fn verify(&self, key: &Self::Key) -> Result<bool, ()> { fn verify(&self, key: &Self::Key) -> Result<bool, ()> {
Ok(self.reference_key == *key) Ok(self.reference_key == *key)
} }
@ -233,15 +231,14 @@ impl AuthGuard for CodeAuthGuard {
/* ------------------------------- Unit Tests ------------------------------- */ /* ------------------------------- Unit Tests ------------------------------- */
#[cfg(test)] #[cfg(test)]
mod code_authguard_tests { mod code_authguard_tests {
use super::*;
use rand::{distr::Alphanumeric, prelude::*}; use rand::{distr::Alphanumeric, prelude::*};
use stopwatch::Stopwatch; use stopwatch::Stopwatch;
use super::*;
fn _create_rng_string() -> String { fn _create_rng_string() -> String {
let mut rng = rand::rng(); let mut rng = rand::rng();
let count = rng.random_range(5..100); let count = rng.random_range(5..100);
rng rng.sample_iter(Alphanumeric)
.sample_iter(Alphanumeric)
.take(count) .take(count)
.map(char::from) .map(char::from)
.collect() .collect()
@ -251,7 +248,7 @@ mod code_authguard_tests {
fn basic_use() { fn basic_use() {
let code_a = "reference_a"; let code_a = "reference_a";
let code_b = "reference_b"; let code_b = "reference_b";
// Using the keys directly // Using the keys directly
let key_a = CodeAuthKey::new(&code_a); let key_a = CodeAuthKey::new(&code_a);
let key_b = CodeAuthKey::new(&code_b); let key_b = CodeAuthKey::new(&code_b);
@ -259,7 +256,7 @@ mod code_authguard_tests {
assert_eq!(key_a, key_a); assert_eq!(key_a, key_a);
assert_eq!(key_b, key_b); assert_eq!(key_b, key_b);
assert_ne!(key_a, key_b); assert_ne!(key_a, key_b);
// Using the CodeAuthGuard // Using the CodeAuthGuard
let guard = CodeAuthGuard::new(key_a.clone()); let guard = CodeAuthGuard::new(key_a.clone());
let mut result_verify = guard.verify(&key_a); let mut result_verify = guard.verify(&key_a);
@ -274,7 +271,7 @@ mod code_authguard_tests {
#[test] #[test]
fn basic_benchmark_compare_speed() { fn basic_benchmark_compare_speed() {
// TODO: Look into better ways of performing benchmarks. // TODO: Look into better ways of performing benchmarks.
// Some setup // Some setup
let key_global = CodeAuthKey::new("reference"); let key_global = CodeAuthKey::new("reference");
let count = 100000; let count = 100000;
@ -300,9 +297,9 @@ mod code_authguard_tests {
let key_local = CodeAuthKey::new(&i); let key_local = CodeAuthKey::new(&i);
assert_ne!(key_local, key_global); assert_ne!(key_local, key_global);
} }
sw.stop(); sw.stop();
println!("Incorrect key compare perf: {}ms", sw.elapsed_ms()); println!("Incorrect key compare perf: {}ms", sw.elapsed_ms());
} }
} }

25
src/server/mod.rs Normal file
View file

@ -0,0 +1,25 @@
use auth::AuthGuard;
use util::Address;
use crate::MatchmakerError;
pub mod access;
pub mod auth;
pub mod storage;
pub mod util;
pub trait DataStorageItem {
type Id;
fn id(&self) -> Self::Id;
fn address_info(&self) -> Address;
}
pub trait DataStorage<Auth: AuthGuard, Item: DataStorageItem>: IntoIterator<Item = Item> {
fn reserve_instance(
&mut self,
address: &Address,
guard: Option<Auth>,
) -> Result<Item::Id, MatchmakerError>;
fn remove_instance(&mut self, id: Item::Id);
}

375
src/server/storage.rs Normal file
View file

@ -0,0 +1,375 @@
use std::{
collections::{BTreeMap, HashMap}, iter::Map, net::IpAddr, sync::RwLock
};
use super::util::Address;
/* -------------------------------------------------------------------------- */
/* Instance Storage */
/* -------------------------------------------------------------------------- */
#[derive(Debug)]
pub enum InstanceStorageError {
NotFound,
Full
}
pub trait Instance {
type Id;
fn id(&self) -> Self::Id;
fn ip(&self) -> IpAddr;
}
pub trait InstanceStorage<Item: Instance>: IntoIterator<Item = Item> {
// Create
// Request
fn get_by_address(&self, address: &Address) -> Result<&Item, InstanceStorageError>;
fn get_by_address_mut(
&mut self,
address: &Address,
) -> Result<&mut Item, InstanceStorageError>;
fn get_by_id(&self, id: Item::Id) -> Result<&Item, InstanceStorageError>;
fn get_by_id_mut(&mut self, id: Item::Id) -> Result<&mut Item, InstanceStorageError>;
// Delete
fn remove_by_address(&mut self, address: &Address) -> Result<(), InstanceStorageError>;
fn remove_by_id(&mut self, id: Item::Id) -> Result<(), InstanceStorageError>;
}
/* ------------------------- Default Implementation ------------------------- */
pub struct BasicInstance {
id: u32,
ip: IpAddr
}
impl Instance for BasicInstance {
type Id = u32;
fn id(&self) -> Self::Id {
self.id
}
fn ip(&self) -> IpAddr {
self.ip
}
}
pub struct BasicInstanceStorage {
max_instances: u32,
instances: BTreeMap<Address, BasicInstance>,
}
impl BasicInstanceStorage {
fn new(max_instances: u32) -> Self {
Self {
instances: BTreeMap::new(),
max_instances
}
}
}
impl InstanceStorage<BasicInstance> for BasicInstanceStorage {
fn get_by_address(&self, address: &Address) -> Result<&BasicInstance, InstanceStorageError> {
match self.instances.get(address) {
Some(v) => {
Ok(v)
},
None => Err(InstanceStorageError::NotFound)
}
}
fn get_by_address_mut(
&mut self,
address: &Address,
) -> Result<&mut BasicInstance, InstanceStorageError> {
match self.instances.get_mut(address) {
Some(v) => {
Ok(v)
},
None => Err(InstanceStorageError::NotFound)
}
}
fn get_by_id(&self, id: <BasicInstance as Instance>::Id) -> Result<&BasicInstance, InstanceStorageError> {
let search_result = self.instances
.iter()
.find(|v| v.1.id == id);
match search_result {
Some(v) => {
Ok(v.1)
},
None => {
Err(InstanceStorageError::NotFound)
}
}
}
fn get_by_id_mut(&mut self, id: <BasicInstance as Instance>::Id) -> Result<&mut BasicInstance, InstanceStorageError> {
let search_result = self.instances
.iter_mut()
.find(|v| v.1.id == id);
match search_result {
Some(v) => {
Ok(v.1)
},
None => {
Err(InstanceStorageError::NotFound)
}
}
}
fn remove_by_address(&mut self, address: &Address) -> Result<(), InstanceStorageError> {
match self.instances.remove(address) {
Some(_) => {
Ok(())
},
None => Err(InstanceStorageError::NotFound)
}
}
fn remove_by_id(&mut self, id: <BasicInstance as Instance>::Id) -> Result<(), InstanceStorageError> {
let original_len = self.instances.len();
self.instances.retain(|_, v| v.id != id);
if self.instances.len() == original_len {
return Err(InstanceStorageError::NotFound);
}
Ok(())
}
}
impl IntoIterator for BasicInstanceStorage {
type Item = BasicInstance;
type IntoIter = std::vec::IntoIter<BasicInstance>;
fn into_iter(self) -> Self::IntoIter {
self.instances
.into_iter()
.map(|(_address, instance)| instance)
.collect::<Vec<BasicInstance>>()
.into_iter()
}
}
#[cfg(test)]
mod basic_instance_test {
use std::net::{IpAddr, Ipv4Addr};
use macaddr::{MacAddr, MacAddr6};
use super::*;
fn _create_sample_address(id: u8) -> Address {
let ip: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, id));
let mac: MacAddr = MacAddr::V6(MacAddr6::new(id, id, id, id, id, id));
Address::new(ip, mac)
}
#[test]
fn test_get_by_address_found() {
let mut storage = BasicInstanceStorage::new(10);
let instance = BasicInstance { id: 42, ip: IpAddr::V4(Ipv4Addr::LOCALHOST) };
let addr = _create_sample_address(1);
storage.instances.insert(addr.clone(), instance);
let fetched = storage.get_by_address(&addr).unwrap();
assert_eq!(fetched.id, 42);
}
#[test]
fn test_get_by_address_not_found() {
let storage = BasicInstanceStorage::new(10);
let addr = _create_sample_address(1);
let result = storage.get_by_address(&addr);
assert!(matches!(result, Err(InstanceStorageError::NotFound)));
}
#[test]
fn test_get_by_address_mut() {
let mut storage = BasicInstanceStorage::new(10);
let addr = _create_sample_address(1);
let instance = BasicInstance { id: 42, ip: IpAddr::V4(Ipv4Addr::LOCALHOST) };
storage.instances.insert(addr.clone(), instance);
{
let fetched_mut = storage.get_by_address_mut(&addr).unwrap();
fetched_mut.id = 100;
}
let fetched = storage.get_by_address(&addr).unwrap();
assert_eq!(fetched.id, 100);
}
#[test]
fn test_get_by_id_found() {
let mut storage = BasicInstanceStorage::new(10);
let addr = _create_sample_address(1);
let instance = BasicInstance { id: 42, ip: IpAddr::V4(Ipv4Addr::LOCALHOST) };
storage.instances.insert(addr, instance);
let fetched = storage.get_by_id(42).unwrap();
assert_eq!(fetched.id, 42);
}
#[test]
fn test_get_by_id_not_found() {
let storage = BasicInstanceStorage::new(10);
let result = storage.get_by_id(42);
assert!(matches!(result, Err(InstanceStorageError::NotFound)));
}
#[test]
fn test_get_by_id_mut() {
let mut storage = BasicInstanceStorage::new(10);
let addr = _create_sample_address(1);
let instance = BasicInstance { id: 42, ip: IpAddr::V4(Ipv4Addr::LOCALHOST) };
storage.instances.insert(addr, instance);
{
let fetched_mut = storage.get_by_id_mut(42);
assert!(fetched_mut.is_ok());
let fetched_mut = fetched_mut.unwrap();
fetched_mut.id = 99;
}
let fetched = storage.get_by_id(42);
assert!(fetched.is_err());
}
#[test]
fn test_remove_by_address() {
let mut storage = BasicInstanceStorage::new(10);
let addr = _create_sample_address(1);
storage.instances.insert(addr.clone(), BasicInstance { id: 42, ip: IpAddr::V4(Ipv4Addr::LOCALHOST) });
assert!(storage.remove_by_address(&addr).is_ok());
assert!(storage.instances.is_empty());
}
#[test]
fn test_remove_by_address_not_found() {
let mut storage = BasicInstanceStorage::new(10);
let addr = _create_sample_address(1);
let result = storage.remove_by_address(&addr);
assert!(matches!(result, Err(InstanceStorageError::NotFound)));
}
#[test]
fn test_remove_by_id() {
let mut storage = BasicInstanceStorage::new(10);
let addr = _create_sample_address(1);
storage.instances.insert(addr, BasicInstance { id: 42, ip: IpAddr::V4(Ipv4Addr::LOCALHOST) });
assert!(storage.remove_by_id(42).is_ok());
assert!(storage.instances.is_empty());
}
#[test]
fn test_remove_by_id_not_found() {
let mut storage = BasicInstanceStorage::new(10);
let result = storage.remove_by_id(42);
assert!(matches!(result, Err(InstanceStorageError::NotFound)));
}
#[test]
fn test_into_iterator() {
let mut storage = BasicInstanceStorage::new(10);
storage.instances.insert(_create_sample_address(1), BasicInstance { id: 42, ip: IpAddr::V4(Ipv4Addr::LOCALHOST) });
storage.instances.insert(_create_sample_address(2), BasicInstance { id: 43, ip: IpAddr::V4(Ipv4Addr::LOCALHOST) });
let instances: Vec<_> = storage.into_iter().collect();
assert_eq!(instances.len(), 2);
let ids: Vec<u32> = instances.into_iter().map(|inst| inst.id).collect();
assert!(ids.contains(&42));
assert!(ids.contains(&43));
}
}
/* -------------------------------------------------------------------------- */
/* Attribute System */
/* -------------------------------------------------------------------------- */
// TODO: Optimize the heck out of this - This is effectively a placeholder
// It will likely be *very* space inefficient.
/* -------------------------------- Metadata -------------------------------- */
#[repr(u8)]
#[derive(PartialEq)]
enum AttributeType {
Float,
Integer,
Boolean,
String,
}
enum AttributeValue {
Float(f32),
Integer(i32),
Boolean(bool),
String(String),
}
impl AttributeValue {
fn get_type(&self) -> AttributeType {
match self {
Float => AttributeType::Float,
Integer => AttributeType::Integer,
Boolean => AttributeType::Boolean,
String => AttributeType::String,
}
}
}
enum AttributeError {
InvalidType(AttributeType),
InvalidKey(String)
}
/* --------------------------------- Schema --------------------------------- */
// TODO: Builder pattern to create this
pub struct AttributeSchema {
attribute_schema: Map<String, AttributeSchemaElement>,
}
pub struct AttributeSchemaElement {
default_type_and_value: AttributeValue,
}
/* ---------------------------------- List ---------------------------------- */
struct AttributeListElement {
value: AttributeValue,
}
pub struct AttributeList<'a> {
schema: &'a AttributeSchema,
data: RwLock<HashMap<String, AttributeListElement>>,
}
impl<'a> AttributeList<'a> {
fn new(schema: &'a AttributeSchema) -> Self {
Self {
schema,
// TODO: Populate with initial values
data: RwLock::new(HashMap::new()),
}
}
fn set_attribute(&mut self, key: &str, value: AttributeValue) -> Result<(), AttributeError> {
// ! Naive unwrap is present
let mut write = self.data.write().unwrap();
{
let element_read = &write[key];
if value.get_type() != element_read.value.get_type() {
return Err(AttributeError::InvalidType(value.get_type()));
}
}
{
// ! Double query
let element = write.get_mut(key);
match element {
Some(v) => {
v.value = value;
},
None => {
// TODO: Verify that this is the best thing to do
return Err(AttributeError::InvalidKey(key.to_string()))
}
}
}
Ok(())
}
}

33
src/server/util.rs Normal file
View file

@ -0,0 +1,33 @@
use std::net::IpAddr;
use macaddr::MacAddr;
#[derive(Debug, Hash, PartialOrd, Ord, Clone)]
pub struct Address {
mac: MacAddr,
ip: IpAddr,
}
impl Address {
pub fn new(ip: IpAddr, mac: MacAddr) -> Self {
return Self { ip, mac };
}
pub fn mac(&self) -> MacAddr {
self.mac
}
pub fn ip(&self) -> IpAddr {
self.ip
}
}
impl PartialEq for Address {
fn eq(&self, other: &Self) -> bool {
self.ip == other.ip
}
}
impl Eq for Address {
}