mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-09-22 22:28:22 +03:00
complete base rewrite
This commit is contained in:
Generated
+15
-129
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user