finish implementing features, add logging, windows-specific features

This commit is contained in:
ppc
2026-01-05 21:32:38 +00:00
parent 5c1360ecdc
commit d043f57ad0
8 changed files with 225 additions and 69 deletions
+19 -3
View File
@@ -8,11 +8,13 @@ version = "2.0.0"
dependencies = [
"axum",
"hostname",
"log",
"rust-ini",
"serde",
"tokio",
"tower",
"tower-http",
"tracing",
"windows-sys 0.61.2",
]
[[package]]
@@ -551,9 +553,11 @@ dependencies = [
"bitflags",
"bytes",
"http",
"http-body",
"pin-project-lite",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@@ -576,9 +580,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
@@ -691,6 +707,6 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "zmij"
version = "1.0.10"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e0d8dffbae3d840f64bda38e28391faef673a7b5a6017840f2a106c8145868"
checksum = "dcb2c125bd7365735bebeb420ccb880265ed2d2bddcbcd49f597fdfe6bd5e577"
+4 -3
View File
@@ -1,6 +1,5 @@
[package]
name = "amnet-server"
version = "2.0.0"
edition = "2024"
version.workspace = true
@@ -13,8 +12,10 @@ crate-type = ["cdylib"]
[dependencies]
axum = "0.8.8"
hostname = "0.4"
log = "0.4"
tracing = "0.1"
rust-ini = "0.21"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] }
tower-http = { version = "0.6.8", features = ["cors"] }
tower = "0.5.2"
tower-http = { version = "0.6.8", features = ["cors", "trace"] }
windows-sys = { version = "0.61.2", features = ["Win32_System_Console", "Win32_UI_Input_KeyboardAndMouse"] }
+5 -5
View File
@@ -1,14 +1,15 @@
use ini::Ini;
use log::error;
use std::env;
use std::path::PathBuf;
use std::sync::OnceLock;
use ini::Ini;
use tracing::{error, warn};
mod defaults {
pub const CONFIG_FILE: &str = "./segatools.ini";
pub const CONFIG_FILE_ENV: &str = "SEGATOOLS_CONFIG_PATH";
pub const SERVER_NAME: &str = "AMNET-PC";
pub const SERVER_NAME: &str = "AMNET-SERVER";
pub const SERVER_ADDRESS: &str = "0.0.0.0:6070";
pub const AIME_FILE: &str = r"DEVICE\aime.txt";
@@ -21,7 +22,6 @@ mod limits {
pub const SERVER_NAME_MAX_LEN: usize = 16;
}
static CONFIG: OnceLock<Config> = OnceLock::new();
pub (crate) struct Config {
@@ -50,7 +50,7 @@ impl Config {
pub fn instance() -> &'static Config {
CONFIG.get_or_init(|| {
Config::load().unwrap_or_else(|e| {
log::warn!("Failed to load config: {}. Using defaults.", e);
warn!("Failed to load config: {}. Using defaults.", e);
Config::default()
})
})
+115 -18
View File
@@ -1,5 +1,9 @@
use std::sync::OnceLock;
use tokio::net::TcpListener;
use tokio::runtime::Runtime;
use tracing::{info, warn};
use crate::card::CardPresenter;
use crate::config::Config;
use crate::metrics::SystemMetrics;
@@ -11,41 +15,93 @@ mod metrics;
mod utils;
mod webserver;
static WEBSERVER_INIT: OnceLock<()> = OnceLock::<()>::new();
#[unsafe(no_mangle)]
pub extern "C" fn aime_io_get_api_version() -> u16 {
0x0100
}
#[unsafe(no_mangle)]
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");
WEBSERVER_INIT.get_or_init(|| {
std::thread::spawn(|| {
let rt = Runtime::new().expect("Failed to create tokio runtime");
rt.block_on(start_server_async());
});
});
return 0;
}
#[unsafe(no_mangle)]
pub extern "C" fn aime_io_nfc_poll(unit_no: u8) -> i32 {
let _ = unit_no; // not used
if unit_no != 0 {
return 1;
}
if let Ok(mut last_poll) = SystemMetrics::instance().last_poll_time.lock() {
*last_poll = Some(std::time::Instant::now());
if let Err(_) = SystemMetrics::instance().set_last_poll() {
warn!("Failed to update last poll time");
return 1;
}
let enter_pressed: bool;
#[cfg(target_os = "windows")]
{
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
enter_pressed = unsafe { (GetAsyncKeyState(0x0D) & (0x8000u16 as i16)) != 0 };
}
#[cfg(not(target_os = "windows"))]
{
enter_pressed = false; // Default to false on non-Windows platforms
}
if !enter_pressed {
return 1;
}
let aime_file_path = &Config::instance().aime_txt_path;
let aime_code = match std::fs::read_to_string(aime_file_path) {
Ok(content) => content.trim().to_string(),
Err(e) => {
warn!("Failed to read text file at {}: {}", aime_file_path.display(), e);
return 1;
}
};
if aime_code.is_empty() || !utils::is_valid_access_code(&aime_code) {
warn!("AIME code in {} is invalid", aime_file_path.display());
return 1;
}
info!("AIME code read from {}: {}", aime_file_path.display(), aime_code);
let presenter_result = CardPresenter::instance().present_card(
&aime_code,
None,
std::time::Duration::from_secs(5),
);
match presenter_result {
crate::card::CardPresentResult::Accepted => (),
crate::card::CardPresentResult::Rejected(reason) => {
warn!("AIME code from {} was rejected: {}", aime_file_path.display(), reason);
return 1;
}
crate::card::CardPresentResult::SlotUnavailable(release_time) => {
warn!(
"AIME code from {} could not be presented: slot unavailable until {:?}",
aime_file_path.display(),
release_time,
);
return 1;
}
}
return 0;
}
#[unsafe(no_mangle)]
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;
@@ -58,18 +114,20 @@ pub extern "C" fn aime_io_nfc_get_aime_id(unit_no: u8, luid: *mut u8, luid_size:
};
let Some(card) = CardPresenter::instance().request_card(require_idm) else {
// todo debug logging
return 1;
};
let copy_size = std::cmp::min(card.access_code.len(), luid_size);
unsafe {
let copy_size = std::cmp::min(card.access_code.len(), luid_size);
std::ptr::copy_nonoverlapping(card.access_code.as_ptr(), luid, copy_size);
}
info!("Provided AIME access code: {}", utils::format_hex_bytes(&card.access_code));
return 0;
}
#[unsafe(no_mangle)]
pub extern "C" fn aime_io_nfc_get_felica_id(unit_no: u8, idm: *mut u64) -> i32 {
if unit_no != 0 {
return 1;
@@ -92,10 +150,49 @@ pub extern "C" fn aime_io_nfc_get_felica_id(unit_no: u8, idm: *mut u64) -> i32 {
*idm = idm_value;
}
info!("Provided AIME IDm: {}", utils::format_hex_bytes(&card_idm));
return 0;
}
#[unsafe(no_mangle)]
pub extern "C" fn aime_io_led_set_color(_unit_no: u8, _r: u8, _g: u8, _b: u8) {
// do nothing
return;
}
#[unsafe(no_mangle)]
#[allow(non_snake_case)]
#[cfg(target_os = "windows")]
pub extern "system" fn DllMain(_hinst_dll: *mut std::ffi::c_void, fdw_reason: u32, _lpv_reserved: *mut std::ffi::c_void) -> i32 {
if fdw_reason != 1 /* DLL_PROCESS_ATTACH */ {
return 1;
}
unsafe {
windows_sys::Win32::System::Console::AllocConsole();
}
return 1;
}
async fn start_server_async() {
let app = webserver::build_server();
let config = Config::instance();
let listen_addr = config.server_listen_address.as_str();
let listener = TcpListener::bind(listen_addr)
.await
.unwrap_or_else(|e| panic!("Failed to bind to {}: {}", listen_addr, e));
SystemMetrics::instance().set_start_time();
let game_id = config.game_id.clone().unwrap_or_else(|| "SXXX".to_string());
info!("AMNet Server - Version {} ({}) loaded", env!("CARGO_PKG_VERSION"), game_id);
info!("Listening on {}", listen_addr);
info!("Visit http://card.ppc.moe from a mobile device or use the AMNet App to get started.");
axum::serve(listener, app)
.await
.expect("Failed to start server");
}
+40 -4
View File
@@ -1,16 +1,17 @@
use std::sync::{OnceLock, Mutex};
use std::{sync::{Mutex, OnceLock}, time::{Duration, Instant}};
static METRICS: OnceLock<SystemMetrics> = OnceLock::new();
#[derive(Debug)]
pub (crate) struct SystemMetrics {
pub start_time: Mutex<Option<std::time::Instant>>,
pub last_poll_time: Mutex<Option<std::time::Instant>>,
start_time: OnceLock<Instant>,
last_poll_time: Mutex<Option<Instant>>,
}
impl Default for SystemMetrics {
fn default() -> Self {
Self {
start_time: Mutex::new(None),
start_time: OnceLock::new(),
last_poll_time: Mutex::new(None),
}
}
@@ -20,4 +21,39 @@ impl SystemMetrics {
pub fn instance() -> &'static SystemMetrics {
METRICS.get_or_init(|| SystemMetrics::default())
}
/// get the start_time of the server.
pub fn get_server_uptime(&self) -> Option<Duration> {
let Some(time) = self.start_time.get() else {
return None;
};
Some(time.elapsed())
}
/// sets the start time of the server.
/// this does nothing if the time was already set.
pub fn set_start_time(&self) {
self.start_time.get_or_init(|| Instant::now());
}
/// get the last time the card was polled by the game.
/// if the data is not available, an empty err will be returned.
pub fn get_last_poll(&self) -> Option<Duration> {
let Ok(time_guard) = self.last_poll_time.lock() else {
return None;
};
Some(time_guard.as_ref()?.elapsed())
}
/// Set the last poll time to now, returning the previously stored value
pub fn set_last_poll(&self) -> Result<(), ()> {
let Ok(mut time_guard) = self.last_poll_time.lock() else {
return Err(());
};
time_guard.replace(Instant::now());
return Ok(());
}
}
+23
View File
@@ -36,3 +36,26 @@ pub (crate) fn parse_idm_hex(idm_hex: &String) -> Option<[u8; 8]> {
Some(idm_bytes)
}
pub (crate) fn is_valid_access_code(code: &String) -> bool {
if code.len() > 20 {
return false;
}
let cleaned_code: String = code.chars().filter(|c| !c.is_whitespace()).collect();
if cleaned_code.len() != 20 {
return false;
}
let mut seen_non_zero = false;
let is_all_digits = cleaned_code.chars().all(|c| {
if c != '0' {
seen_non_zero = true;
}
c.is_ascii_digit()
});
is_all_digits && seen_non_zero
}
-1
View File
@@ -25,7 +25,6 @@ pub struct ServerState {
pub time_since_last_poll: Option<u64>,
}
#[derive(Deserialize)]
pub struct CardSubmissionRequest {
#[serde(rename = "cardId")]
+19 -35
View File
@@ -1,41 +1,35 @@
use crate::card::{CardPresentResult, CardPresenter};
use crate::config::Config;
use crate::metrics::SystemMetrics;
use crate::webapi::API_VERSION;
use std::time::{Duration, Instant};
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};
use tower::ServiceBuilder;
use tower_http::{cors::{Any, CorsLayer}, trace::TraceLayer};
use crate::card::{CardPresentResult, CardPresenter};
use crate::config::Config;
use crate::metrics::SystemMetrics;
use crate::utils;
use crate::webapi::API_VERSION;
pub fn build_server() -> Router {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any);
let services = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(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)
.layer(services)
}
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 session_uptime = metrics.get_server_uptime().map(|d| d.as_secs()).unwrap_or(0);
let time_since_last_poll = metrics.get_last_poll().map(|d| d.as_secs());
let state = crate::webapi::ServerState {
api_version: API_VERSION,
@@ -55,18 +49,8 @@ async fn card_submission_handler(
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 !utils::is_valid_access_code(&payload.access_code) {
return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid access code provided");
}
if payload.card_idm_hex.as_ref().is_some_and(|idm| idm.chars().any(|c| !c.is_ascii_hexdigit())) {
@@ -76,7 +60,7 @@ async fn card_submission_handler(
let presenter = CardPresenter::instance().present_card(
&payload.access_code,
payload.card_idm_hex.as_ref(),
std::time::Duration::from_secs(5),
Duration::from_secs(5),
);
match presenter {