feat(server): handle compressed requests and cors

This commit is contained in:
Anna 2023-10-05 22:12:24 -04:00
parent fe151e6905
commit 28746242d3
Signed by: anna
GPG Key ID: D0943384CD9F87D1
3 changed files with 134 additions and 13 deletions

4
server/Cargo.lock generated
View File

@ -236,7 +236,10 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets",
]
@ -1040,6 +1043,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"chrono",
"serde",
"sqlx",
"tokio",

View File

@ -8,8 +8,9 @@ edition = "2021"
[dependencies]
anyhow = "1"
axum = "0.6"
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "chrono"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tower = "0.4"
tower-http = { version = "0.4", features = ["decompression-gzip"] }
tower-http = { version = "0.4", features = ["compression-gzip", "decompression-gzip", "cors"] }

View File

@ -3,13 +3,15 @@ use std::sync::Arc;
use anyhow::Result;
use axum::{BoxError, Json, Router, Server};
use axum::error_handling::HandleErrorLayer;
use axum::extract::State;
use axum::http::StatusCode;
use axum::extract::{Path, State};
use axum::http::{Method, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use tower::ServiceBuilder;
use tower_http::compression::CompressionLayer;
use tower_http::cors::CorsLayer;
use tower_http::decompression::RequestDecompressionLayer;
static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!();
@ -19,10 +21,20 @@ async fn main() -> Result<()> {
let pool = SqlitePool::connect("./database.sqlite").await?;
MIGRATOR.run(&pool).await?;
let cors = CorsLayer::new()
.allow_origin([
"https://map.anna.lgbt".parse()?,
])
.allow_methods([Method::GET]);
let app = Router::new()
.route("/", get(index))
.route("/:territory", get(territory))
.route("/:territory/:world", get(territory_world))
.route("/upload", post(upload))
.with_state(Arc::new(pool))
.layer(cors)
.layer(CompressionLayer::new())
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async move {
@ -41,6 +53,72 @@ async fn index() -> &'static str {
"hi"
}
async fn territory(
State(pool): State<Arc<SqlitePool>>,
Path(territory): Path<u32>,
) -> Result<Json<Vec<AnonymousPlayerInfo>>, AppError>
{
let info = sqlx::query_as!(
AnonymousPlayerInfo,
// language=sqlite
r#"
select world,
x,
y,
z,
w,
customize,
level,
job,
free_company,
current_hp,
max_hp
from players
where territory = ?
and unixepoch('now') - unixepoch(timestamp) < 30
"#,
territory,
)
.fetch_all(&*pool)
.await?;
Ok(Json(info))
}
async fn territory_world(
State(pool): State<Arc<SqlitePool>>,
Path((territory, world)): Path<(u32, u32)>,
) -> Result<Json<Vec<AnonymousPlayerInfo>>, AppError>
{
let info = sqlx::query_as!(
AnonymousPlayerInfo,
// language=sqlite
r#"
select world,
x,
y,
z,
w,
customize,
level,
job,
free_company,
current_hp,
max_hp
from players
where territory = ?
and current_world = ?
and unixepoch('now') - unixepoch(timestamp) < 30
"#,
territory,
world,
)
.fetch_all(&*pool)
.await?;
Ok(Json(info))
}
async fn upload(
pool: State<Arc<SqlitePool>>,
data: Json<Update>,
@ -48,9 +126,16 @@ async fn upload(
let mut t = pool.begin().await?;
for player in &data.players {
let fc = match player.free_company.trim() {
"" => None,
x => Some(x),
if !player.is_sane() {
continue;
}
let name = player.name.trim();
let fc = match &player.free_company {
Some(x) if x.trim() == "" => None,
Some(x) => Some(x),
None => None,
};
sqlx::query!(
@ -73,8 +158,9 @@ async fn upload(
current_hp = ?,
max_hp = ?
",
player.name,
name,
player.world,
data.territory,
data.world,
player.x,
@ -120,18 +206,48 @@ struct Update {
struct PlayerInfo {
name: String,
world: u32,
x: f32,
y: f32,
z: f32,
w: f32,
x: f64,
y: f64,
z: f64,
w: f64,
customize: Vec<u8>,
level: u8,
job: u32,
free_company: String,
free_company: Option<String>,
current_hp: u32,
max_hp: u32,
}
impl PlayerInfo {
fn is_sane(&self) -> bool {
if self.name.trim().is_empty() {
return false;
}
if self.world == 65535 || self.level == 0 {
return false;
}
true
}
}
#[derive(Serialize)]
struct AnonymousPlayerInfo {
world: i64,
// timestamp: DateTime<Utc>,
x: f64,
y: f64,
z: f64,
w: f64,
customize: Vec<u8>,
level: i64,
job: i64,
free_company: Option<String>,
current_hp: i64,
max_hp: i64,
}
// Make our own error that wraps `anyhow::Error`.
struct AppError(anyhow::Error);