mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-09-22 22:28:22 +03:00
finish implementing features, add logging, windows-specific features
This commit is contained in:
Generated
+19
-3
@@ -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"
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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(());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ pub struct ServerState {
|
||||
pub time_since_last_poll: Option<u64>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CardSubmissionRequest {
|
||||
#[serde(rename = "cardId")]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user