complete base rewrite

This commit is contained in:
ppc
2026-01-05 21:32:38 +00:00
parent 96a9e2a498
commit 2125753e37
10 changed files with 315 additions and 283 deletions
+15 -129
View File
@@ -31,8 +31,8 @@ dependencies = [
"bytes",
"form_urlencoded",
"futures-util",
"http 1.4.0",
"http-body 1.0.1",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
@@ -62,8 +62,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http 1.4.0",
"http-body 1.0.1",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
@@ -73,12 +73,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.10.0"
@@ -132,22 +126,6 @@ dependencies = [
"const-random",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -218,17 +196,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http"
version = "1.4.0"
@@ -239,17 +206,6 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
@@ -257,7 +213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.4.0",
"http",
]
[[package]]
@@ -268,17 +224,11 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http 1.4.0",
"http-body 1.0.1",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
[[package]]
name = "httparse"
version = "1.10.1"
@@ -301,8 +251,8 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"http 1.4.0",
"http-body 1.0.1",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
@@ -320,8 +270,8 @@ checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [
"bytes",
"futures-core",
"http 1.4.0",
"http-body 1.0.1",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
@@ -340,15 +290,6 @@ version = "0.2.179"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
@@ -400,29 +341,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -459,15 +377,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
@@ -484,12 +393,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
@@ -556,16 +459,6 @@ dependencies = [
"serde",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -614,12 +507,9 @@ version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
@@ -654,17 +544,13 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.3.5"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags 1.3.2",
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http 0.2.12",
"http-body 0.4.6",
"http-range-header",
"http",
"pin-project-lite",
"tower-layer",
"tower-service",
+2 -2
View File
@@ -12,5 +12,5 @@ hostname = "0.4"
log = "0.4"
rust-ini = "0.21"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.3", features = ["cors"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] }
tower-http = { version = "0.6.8", features = ["cors"] }
-108
View File
@@ -1,108 +0,0 @@
use std::time::Instant;
use axum::{Json, http::{StatusCode, header}, response::{IntoResponse, Response}};
use serde::{Deserialize, Serialize};
use crate::{amcard::{CardPresentResult, CardPresenter}, config::Config, metrics::SystemMetrics};
pub const API_VERSION: u8 = 1;
#[derive(Serialize)]
struct ServerError {
error: String,
}
#[derive(Serialize)]
struct ServerState {
#[serde(rename = "apiVersion")]
api_version: u8,
#[serde(rename = "gameId")]
game_id: Option<String>,
#[serde(rename = "serverName")]
server_name: String,
#[serde(rename = "sessionUptime")]
session_uptime: u64,
#[serde(rename = "timeSinceLastPoll")]
time_since_last_poll: u64,
}
pub fn server_state_handler() -> Json<ServerState> {
let config = Config::instance();
let metrics = SystemMetrics::instance();
let state = ServerState {
api_version: API_VERSION,
game_id: config.game_id.clone(),
server_name: config.server_name.clone(),
session_uptime: metrics.start_time.map_or(0, |start| start.elapsed().as_secs()),
time_since_last_poll: metrics.last_poll_time.map_or(0, |last_poll| last_poll.elapsed().as_secs()),
};
Json(state)
}
#[derive(Deserialize)]
struct CardSubmissionRequest {
#[serde(rename = "cardId")]
access_code: String,
#[serde(rename = "physicalCardIDm")]
card_idm_hex: Option<String>,
}
pub fn card_submission_handler(Json(payload): Json<CardSubmissionRequest>) -> impl IntoResponse {
if payload.access_code.len() > 20 || payload.access_code.len() == 0 {
return build_error_response(StatusCode::BAD_REQUEST, "Access code is required");
}
if payload.access_code.chars().any(|c| !c.is_ascii_digit()) {
return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid access code provided");
}
if payload.access_code.chars().all(|c| c == '0') {
return build_error_response(StatusCode::FORBIDDEN, "All-zero access codes are forbidden.");
}
if payload.card_idm_hex.as_ref().is_some_and(|idm| idm.chars().any(|c| !c.is_ascii_hexdigit())) {
return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid IDm provided");
}
let presenter = CardPresenter::instance().present_card(
&payload.access_code,
payload.card_idm_hex.as_ref(),
std::time::Duration::from_secs(5)
);
match presenter {
CardPresentResult::Accepted => StatusCode::ACCEPTED.into_response(),
CardPresentResult::Rejected(_) => StatusCode::FORBIDDEN.into_response(),
CardPresentResult::SlotUnavailable(release_time) => {
let now = Instant::now();
let wait_secs = if release_time > now {
(release_time - now).as_secs()
} else {
0
};
let mut response = build_error_response(StatusCode::TOO_MANY_REQUESTS, "A card has already been submitted. Please wait before submitting another.");
response.headers_mut().insert(
header::RETRY_AFTER,
wait_secs.to_string().parse().unwrap(),
);
response
}
}
}
fn build_error_response(status: StatusCode, message: &str) -> Response {
let error = ServerError {
error: message.to_string(),
};
(status, Json(error)).into_response()
}
@@ -3,7 +3,7 @@ use std::{sync::{Mutex, OnceLock}, time::{Duration, Instant}};
use crate::utils::{parse_access_code, parse_idm_hex};
#[derive(Debug, Clone)]
pub (in crate) struct StoredCard {
pub (crate) struct StoredCard {
pub access_code: [u8; 10],
pub card_idm: Option<[u8; 8]>,
@@ -16,7 +16,7 @@ impl StoredCard {
}
}
pub enum CardPresentResult {
pub (crate) enum CardPresentResult {
Accepted,
Rejected(String /* reason */),
SlotUnavailable(Instant /* release time */),
@@ -24,7 +24,8 @@ pub enum CardPresentResult {
static PRESENTER: OnceLock<CardPresenter> = OnceLock::new();
pub (in crate) struct CardPresenter {
#[derive(Debug)]
pub (crate) struct CardPresenter {
current_card: Mutex<Option<StoredCard>>,
}
@@ -41,7 +42,7 @@ impl CardPresenter {
PRESENTER.get_or_init(|| CardPresenter::default())
}
pub fn request_card(&mut self, require_idm: Option<bool>) -> Option<StoredCard> {
pub fn request_card(&self, require_idm: Option<bool>) -> Option<StoredCard> {
let mut guard = self.current_card.lock().unwrap();
let card = guard.as_ref()?;
+41 -27
View File
@@ -4,22 +4,27 @@ use std::env;
use std::path::PathBuf;
use std::sync::OnceLock;
const DEFAULT_CONFIG_FILE: &str = "./segatools.ini";
const CONFIG_FILE_OVERRIDE_ENV: &str = "SEGATOOLS_CONFIG_PATH";
mod defaults {
pub const CONFIG_FILE: &str = "./segatools.ini";
pub const CONFIG_FILE_ENV: &str = "SEGATOOLS_CONFIG_PATH";
const DEFAULT_SERVER_NAME: &str = "AMNET-PC";
pub const SERVER_NAME: &str = "AMNET-PC";
pub const SERVER_ADDRESS: &str = "0.0.0.0:6070";
const DEFAULT_AIME_FILE: &str = r"DEVICE\aime.txt";
const DEFAULT_AIME_KEYBOARD_ENABLED: bool = true;
pub const AIME_FILE: &str = r"DEVICE\aime.txt";
pub const AIME_KEYBOARD_ENABLED: bool = true;
pub const INCLUDE_IDM_FOR_PHYSICAL_CARDS: bool = false;
}
const DEFAULT_INCLUDE_IDM_FOR_PHYSICAL_CARDS: bool = false;
mod limits {
pub const GAME_ID_LEN: usize = 4;
pub const SERVER_NAME_MAX_LEN: usize = 16;
}
const GAME_ID_EXACT_LEN: usize = 4;
const SERVER_NAME_MAX_LEN: usize = 16;
static CONFIG: OnceLock<Config> = OnceLock::new();
pub (in crate) struct Config {
pub (crate) struct Config {
/// Game ID (4 characters or none)
pub game_id: Option<String>,
@@ -36,6 +41,9 @@ pub (in crate) struct Config {
/// Path to the aime.txt file
pub aime_txt_path: PathBuf,
/// Server address that should be listened on
pub server_listen_address: String,
}
impl Config {
@@ -49,7 +57,7 @@ impl Config {
}
fn load() -> Result<Self, Box<dyn std::error::Error>> {
let config_path = env::var(CONFIG_FILE_OVERRIDE_ENV).unwrap_or_else(|_| DEFAULT_CONFIG_FILE.to_string());
let config_path = env::var(defaults::CONFIG_FILE_ENV).unwrap_or_else(|_| defaults::CONFIG_FILE.to_string());
let ini = Ini::load_from_file(&config_path).map_err(|e| format!("Failed to load config file '{}': {}", config_path, e))?;
let io_section = ini.section(Some("aimeio")).ok_or("Missing [aimeio] section")?;
@@ -59,46 +67,51 @@ impl Config {
let game_id = io_section.get("gameId").unwrap_or("");
let game_id = if game_id.is_empty() {
None
} else if game_id.chars().count() == GAME_ID_EXACT_LEN && game_id.chars().all(|c| c.is_ascii_alphabetic()) {
} else if game_id.chars().count() == limits::GAME_ID_LEN && game_id.chars().all(|c| c.is_ascii_alphabetic()) {
Some(game_id.to_ascii_uppercase().to_string())
} else {
error!("Invalid gameId '{game_id}': must be exactly {GAME_ID_EXACT_LEN} characters or empty");
error!("Invalid gameId '{game_id}': must be exactly {} characters or empty", limits::GAME_ID_LEN);
None
};
let default_hostname = hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| DEFAULT_SERVER_NAME.to_string());
.unwrap_or_else(|| defaults::SERVER_NAME.to_string());
let server_name = io_section.get("serverName")
.unwrap_or(&default_hostname)
.chars()
.take(SERVER_NAME_MAX_LEN)
.take(limits::SERVER_NAME_MAX_LEN)
.collect();
// Read boolean flags
let physical_card_use_idm = io_section.get("useAimeDBForPhysicalCards")
.and_then(|v| v.parse::<i32>().ok())
.map(|v| v > 0)
.unwrap_or(false);
.unwrap_or(defaults::INCLUDE_IDM_FOR_PHYSICAL_CARDS);
let enable_aime_txt = io_section.get("enableKeyboardMode")
.and_then(|v| v.parse::<i32>().ok())
.map(|v| v > 0)
.unwrap_or(DEFAULT_AIME_KEYBOARD_ENABLED);
.unwrap_or(defaults::AIME_KEYBOARD_ENABLED);
let aime_txt_path = aime_section
.and_then(|s| s.get("aimePath"))
.unwrap_or(DEFAULT_AIME_FILE)
.unwrap_or(defaults::AIME_FILE)
.into();
let server_listen_address = io_section.get("listenAddress")
.unwrap_or(defaults::SERVER_ADDRESS)
.to_string();
Ok(Config {
game_id,
server_name,
physical_card_use_idm,
enable_aime_txt,
aime_txt_path,
server_listen_address,
})
}
}
@@ -108,14 +121,15 @@ impl Default for Config {
let default_hostname = hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| DEFAULT_SERVER_NAME.to_string());
Config {
.unwrap_or_else(|| defaults::SERVER_NAME.to_string());
Self {
game_id: None,
server_name: default_hostname.chars().take(SERVER_NAME_MAX_LEN).collect(),
physical_card_use_idm: DEFAULT_INCLUDE_IDM_FOR_PHYSICAL_CARDS,
enable_aime_txt: DEFAULT_AIME_KEYBOARD_ENABLED,
aime_txt_path: PathBuf::from(DEFAULT_AIME_FILE),
server_name: default_hostname.chars().take(limits::SERVER_NAME_MAX_LEN).collect(),
physical_card_use_idm: defaults::INCLUDE_IDM_FOR_PHYSICAL_CARDS,
enable_aime_txt: defaults::AIME_KEYBOARD_ENABLED,
aime_txt_path: PathBuf::from(defaults::AIME_FILE),
server_listen_address: defaults::SERVER_ADDRESS.to_string(),
}
}
}
+95 -4
View File
@@ -1,10 +1,101 @@
use tokio::net::TcpListener;
use tokio::runtime::Runtime;
use crate::card::CardPresenter;
use crate::config::Config;
use crate::metrics::SystemMetrics;
mod config;
mod amcard;
mod amserver;
mod card;
mod webapi;
mod metrics;
mod utils;
mod webserver;
#[unsafe(no_mangle)]
pub extern "C" fn say_hello() {
println!("Hello, world!");
pub extern "C" fn aime_io_get_api_version() -> u16 {
0x0100
}
pub extern "C" fn aime_io_init() -> i32 {
std::thread::spawn(|| {
let runtime = Runtime::new().expect("Failed to create tokio runtime");
runtime.block_on(async {
let app = webserver::build_server();
let listen_addr = Config::instance().server_listen_address.as_str();
let listener = TcpListener::bind(listen_addr)
.await
.unwrap_or_else(|e| panic!("Failed to bind to {}: {}", listen_addr, e));
axum::serve(listener, app)
.await
.expect("Failed to start server");
});
});
return 0;
}
pub extern "C" fn aime_io_nfc_poll(unit_no: u8) -> i32 {
let _ = unit_no; // not used
if let Ok(mut last_poll) = SystemMetrics::instance().last_poll_time.lock() {
*last_poll = Some(std::time::Instant::now());
}
return 0;
}
pub extern "C" fn aime_io_nfc_get_aime_id(unit_no: u8, luid: *mut u8, luid_size: usize) -> i32 {
if unit_no != 0 {
return 1;
}
let require_idm = if Config::instance().physical_card_use_idm {
Some(false)
} else {
None
};
let Some(card) = CardPresenter::instance().request_card(require_idm) else {
return 1;
};
let copy_size = std::cmp::min(card.access_code.len(), luid_size);
unsafe {
std::ptr::copy_nonoverlapping(card.access_code.as_ptr(), luid, copy_size);
}
return 0;
}
pub extern "C" fn aime_io_nfc_get_felica_id(unit_no: u8, idm: *mut u64) -> i32 {
if unit_no != 0 {
return 1;
}
let Some(card) = CardPresenter::instance().request_card(Some(true)) else {
return 1;
};
let Some(card_idm) = card.card_idm else {
return 1;
};
let mut idm_value: u64 = 0;
for byte in card_idm {
idm_value = (idm_value << 8) | (byte as u64);
}
unsafe {
*idm = idm_value;
}
return 0;
}
pub extern "C" fn aime_io_led_set_color(_unit_no: u8, _r: u8, _g: u8, _b: u8) {
// do nothing
return;
}
+6 -6
View File
@@ -1,17 +1,17 @@
use std::sync::OnceLock;
use std::sync::{OnceLock, Mutex};
static METRICS: OnceLock<SystemMetrics> = OnceLock::new();
pub (in crate) struct SystemMetrics {
pub start_time: Option<std::time::Instant>,
pub last_poll_time: Option<std::time::Instant>,
pub (crate) struct SystemMetrics {
pub start_time: Mutex<Option<std::time::Instant>>,
pub last_poll_time: Mutex<Option<std::time::Instant>>,
}
impl Default for SystemMetrics {
fn default() -> Self {
Self {
start_time: None,
last_poll_time: None,
start_time: Mutex::new(None),
last_poll_time: Mutex::new(None),
}
}
}
+3 -3
View File
@@ -1,11 +1,11 @@
pub (in crate) fn format_hex_bytes(bytes: &[u8]) -> String {
pub (crate) fn format_hex_bytes(bytes: &[u8]) -> String {
bytes.chunks(2)
.map(|chunk| chunk.iter().map(|b| format!("{:02X}", b)).collect::<String>())
.collect::<Vec<_>>()
.join(" ")
}
pub (in crate) fn parse_access_code(code: &String) -> Option<[u8; 10]> {
pub (crate) fn parse_access_code(code: &String) -> Option<[u8; 10]> {
let matrix_code = format!("{:0>20}", code.replace(" ", ""));
let mut bytes: [u8; 10] = [0; 10];
@@ -19,7 +19,7 @@ pub (in crate) fn parse_access_code(code: &String) -> Option<[u8; 10]> {
Some(bytes)
}
pub (in crate) fn parse_idm_hex(idm_hex: &String) -> Option<[u8; 8]> {
pub (crate) fn parse_idm_hex(idm_hex: &String) -> Option<[u8; 8]> {
if idm_hex.len() > 16 || !idm_hex.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
+36
View File
@@ -0,0 +1,36 @@
use serde::{Deserialize, Serialize};
pub const API_VERSION: u8 = 1;
#[derive(Serialize)]
pub struct ServerError {
pub error: String,
}
#[derive(Serialize)]
pub struct ServerState {
#[serde(rename = "apiVersion")]
pub api_version: u8,
#[serde(rename = "gameId")]
pub game_id: Option<String>,
#[serde(rename = "serverName")]
pub server_name: String,
#[serde(rename = "sessionUptime")]
pub session_uptime: u64,
#[serde(rename = "timeSinceLastPoll")]
pub time_since_last_poll: Option<u64>,
}
#[derive(Deserialize)]
pub struct CardSubmissionRequest {
#[serde(rename = "cardId")]
pub access_code: String,
#[serde(rename = "physicalCardIDm")]
pub card_idm_hex: Option<String>,
}
+112
View File
@@ -0,0 +1,112 @@
use crate::card::{CardPresentResult, CardPresenter};
use crate::config::Config;
use crate::metrics::SystemMetrics;
use crate::webapi::API_VERSION;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use std::time::Instant;
use tower_http::cors::{Any, CorsLayer};
pub fn build_server() -> Router {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any);
Router::<()>::new()
.route("/amnet/info", get(server_state_handler))
.route("/amnet/signin", post(card_submission_handler))
.layer(cors)
}
async fn server_state_handler() -> Json<crate::webapi::ServerState> {
let config = Config::instance();
let metrics = SystemMetrics::instance();
let session_uptime = metrics
.start_time
.lock()
.ok()
.and_then(|start| start.as_ref().map(|s| s.elapsed().as_secs()))
.unwrap_or(0);
let time_since_last_poll = metrics
.last_poll_time
.lock()
.ok()
.and_then(|last_poll| last_poll.as_ref().map(|lp| lp.elapsed().as_secs()));
let state = crate::webapi::ServerState {
api_version: API_VERSION,
game_id: config.game_id.clone(),
server_name: config.server_name.clone(),
session_uptime,
time_since_last_poll,
};
Json(state)
}
async fn card_submission_handler(
Json(payload): Json<crate::webapi::CardSubmissionRequest>,
) -> impl IntoResponse {
if payload.access_code.len() > 20 || payload.access_code.len() == 0 {
return build_error_response(StatusCode::BAD_REQUEST, "Access code is required");
}
if payload.access_code.chars().any(|c| !c.is_ascii_digit()) {
return build_error_response(
StatusCode::UNPROCESSABLE_ENTITY,
"Invalid access code provided",
);
}
if payload.access_code.chars().all(|c| c == '0') {
return build_error_response(
StatusCode::FORBIDDEN,
"All-zero access codes are forbidden.",
);
}
if payload.card_idm_hex.as_ref().is_some_and(|idm| idm.chars().any(|c| !c.is_ascii_hexdigit())) {
return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid IDm provided");
}
let presenter = CardPresenter::instance().present_card(
&payload.access_code,
payload.card_idm_hex.as_ref(),
std::time::Duration::from_secs(5),
);
match presenter {
CardPresentResult::Accepted => StatusCode::ACCEPTED.into_response(),
CardPresentResult::Rejected(message) => build_error_response(StatusCode::FORBIDDEN, format!("Card rejected: {}", message).as_str()),
CardPresentResult::SlotUnavailable(release_time) => {
let now = Instant::now();
let wait_secs = if release_time > now {
(release_time - now).as_secs()
} else {
0
};
let mut response = build_error_response(
StatusCode::TOO_MANY_REQUESTS,
"A card has already been submitted. Please wait before submitting another.",
);
let headers = response.headers_mut();
headers.insert(header::RETRY_AFTER, wait_secs.to_string().parse().unwrap());
response
}
}
}
fn build_error_response(status: StatusCode, message: &str) -> Response {
let error = crate::webapi::ServerError {
error: message.to_string(),
};
(status, Json(error)).into_response()
}