From fb32ad5db3289f5757f2c17960cf64eb2196c4ed Mon Sep 17 00:00:00 2001 From: core Date: Mon, 8 Apr 2024 14:14:22 -0400 Subject: [PATCH] functioning api --- .gitignore | 3 +- Cargo.lock | 26 +++ Cargo.toml | 4 +- postgres.docker-compose.yml | 15 ++ starkingdoms-api/.env | 1 + starkingdoms-api/Cargo.toml | 31 +++ starkingdoms-api/Dockerfile | 5 + starkingdoms-api/build.rs | 18 ++ starkingdoms-api/diesel.toml | 9 + starkingdoms-api/migrations/.keep | 0 .../down.sql | 6 + .../up.sql | 36 ++++ .../down.sql | 1 + .../up.sql | 8 + starkingdoms-api/src/auth.rs | 38 ++++ starkingdoms-api/src/config.rs | 43 ++++ starkingdoms-api/src/error.rs | 133 +++++++++++++ starkingdoms-api/src/main.rs | 185 ++++++++++++++++++ starkingdoms-api/src/models.rs | 26 +++ starkingdoms-api/src/response.rs | 122 ++++++++++++ starkingdoms-api/src/routes/mod.rs | 16 ++ starkingdoms-api/src/routes/sign_save.rs | 100 ++++++++++ starkingdoms-api/src/schema.rs | 9 + starkingdoms-api/src/tokens.rs | 26 +++ .../src/pages/ShipEditor.svelte | 26 ++- starkingdoms-client/src/save.ts | 16 +- starkingdoms-common/src/lib.rs | 15 ++ 27 files changed, 911 insertions(+), 7 deletions(-) create mode 100644 postgres.docker-compose.yml create mode 100644 starkingdoms-api/.env create mode 100644 starkingdoms-api/Cargo.toml create mode 100644 starkingdoms-api/Dockerfile create mode 100644 starkingdoms-api/build.rs create mode 100644 starkingdoms-api/diesel.toml create mode 100644 starkingdoms-api/migrations/.keep create mode 100644 starkingdoms-api/migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 starkingdoms-api/migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 starkingdoms-api/migrations/2024-04-08-152705_create_table_saves/down.sql create mode 100644 starkingdoms-api/migrations/2024-04-08-152705_create_table_saves/up.sql create mode 100644 starkingdoms-api/src/auth.rs create mode 100644 starkingdoms-api/src/config.rs create mode 100644 starkingdoms-api/src/error.rs create mode 100644 starkingdoms-api/src/main.rs create mode 100644 starkingdoms-api/src/models.rs create mode 100644 starkingdoms-api/src/response.rs create mode 100644 starkingdoms-api/src/routes/mod.rs create mode 100644 starkingdoms-api/src/routes/sign_save.rs create mode 100644 starkingdoms-api/src/schema.rs create mode 100644 starkingdoms-api/src/tokens.rs diff --git a/.gitignore b/.gitignore index c60dce425c14341f0801a0ecd6c28b71e5c2f03b..6c73a128751703abb29136ceeb7db42d54e2b7a3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ assets/final starkingdoms-client/node_modules starkingdoms-client/dist client/node_modules -starkingdoms-backplane/config.toml \ No newline at end of file +starkingdoms-backplane/config.toml +starkingdoms-api/config.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ed64d387b7306d646895cc1d85884ec2ae8a9384..6dfabe36da0e788ffe7d2b342625a340b548c246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3664,6 +3664,32 @@ dependencies = [ "bitflags 2.5.0", ] +[[package]] +name = "starkingdoms-api" +version = "0.1.0-alpha1" +dependencies = [ + "actix-cors", + "actix-web", + "argon2", + "bb8", + "diesel", + "diesel-async", + "diesel_migrations", + "env_logger", + "hex", + "hmac", + "jwt", + "log", + "password-hash", + "rand", + "rs-snowflake", + "serde", + "serde_json", + "sha2", + "starkingdoms-common", + "toml 0.8.12", +] + [[package]] name = "starkingdoms-backplane" version = "0.1.0-alpha1" diff --git a/Cargo.toml b/Cargo.toml index f69ca87dae8489e89c4fd3d7cec4da5f3a76e658..f0502b585fe548779e6b55d9611275c27c4035f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [ "starkingdoms-backplane", "starkingdoms-common", "savefile_decoder" -] +, "starkingdoms-api"] resolver = "2" [profile.dev.package."*"] @@ -13,4 +13,4 @@ opt-level = 3 [profile.release-ci] inherits = "release" codegen-units = 1 -lto = "fat" \ No newline at end of file +lto = "fat" diff --git a/postgres.docker-compose.yml b/postgres.docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..26f2ce45e4a12f98724b1cb7cb77bf82ac28c457 --- /dev/null +++ b/postgres.docker-compose.yml @@ -0,0 +1,15 @@ +version: "3" +services: + hayyadb: + image: postgres + environment: + - POSTGRES_PASSWORD=postgres + - POSTGRES_USER=postgres + - POSTGRES_DB=stkapi + ports: + - 5432:5432 + volumes: + - db:/var/lib/postgresql/data + +volumes: + db: diff --git a/starkingdoms-api/.env b/starkingdoms-api/.env new file mode 100644 index 0000000000000000000000000000000000000000..2cd864a26f4c9315f565eb39332070452cda99b9 --- /dev/null +++ b/starkingdoms-api/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://postgres:postgres@localhost/stkapi \ No newline at end of file diff --git a/starkingdoms-api/Cargo.toml b/starkingdoms-api/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..18f9a70e69df953db4bbc79d5f2cbbc128ff55b3 --- /dev/null +++ b/starkingdoms-api/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "starkingdoms-api" +version = "0.1.0-alpha1" +authors = ["core "] +edition = "2021" +description = "The save storage API keeping your ship designs safe" +license = "AGPL-3" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "4" +actix-cors = "0.6" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" +log = "0.4" +env_logger = "0.10" +diesel = { version = "2", features = ["serde_json"] } +diesel-async = { version = "0.4", features = ["postgres", "bb8", "async-connection-wrapper"] } +diesel_migrations = "2" +bb8 = "0.8" +rand = "0.8" +argon2 = "0.5" +password-hash = "0.5" +rs-snowflake = "0.6" +jwt = "0.16" +sha2 = "0.10" +hmac = "0.12" +hex = "0.4" +starkingdoms-common = { version = "0.1", path = "../starkingdoms-common" } \ No newline at end of file diff --git a/starkingdoms-api/Dockerfile b/starkingdoms-api/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b1e87156584cccb588dc0cb8d1ecf7590c0e650e --- /dev/null +++ b/starkingdoms-api/Dockerfile @@ -0,0 +1,5 @@ +FROM debian:bookworm-slim + +COPY target/release-ci/starkingdoms-backplane /bin/starkingdoms-backplane + +ENTRYPOINT ["starkingdoms-backplane", "/etc/starkingdoms/backplane.toml"] \ No newline at end of file diff --git a/starkingdoms-api/build.rs b/starkingdoms-api/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..30716a76a118d584943b28cbe11b498d577c212b --- /dev/null +++ b/starkingdoms-api/build.rs @@ -0,0 +1,18 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +fn main() { + println!("cargo:rerun-if-changed=migrations") +} diff --git a/starkingdoms-api/diesel.toml b/starkingdoms-api/diesel.toml new file mode 100644 index 0000000000000000000000000000000000000000..c028f4a6aa1a8b47c0f42624942f4d2ef3274f9a --- /dev/null +++ b/starkingdoms-api/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId"] + +[migrations_directory] +dir = "migrations" diff --git a/starkingdoms-api/migrations/.keep b/starkingdoms-api/migrations/.keep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/starkingdoms-api/migrations/00000000000000_diesel_initial_setup/down.sql b/starkingdoms-api/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..a9f526091194b70e312bd1b30084ea34e0df9670 --- /dev/null +++ b/starkingdoms-api/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/starkingdoms-api/migrations/00000000000000_diesel_initial_setup/up.sql b/starkingdoms-api/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..d68895b1a7b7dbbd699eb627b23b01746eb80222 --- /dev/null +++ b/starkingdoms-api/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/starkingdoms-api/migrations/2024-04-08-152705_create_table_saves/down.sql b/starkingdoms-api/migrations/2024-04-08-152705_create_table_saves/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..d9a93fe9a1a1027023719e1f63402aa51adb8b30 --- /dev/null +++ b/starkingdoms-api/migrations/2024-04-08-152705_create_table_saves/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/starkingdoms-api/migrations/2024-04-08-152705_create_table_saves/up.sql b/starkingdoms-api/migrations/2024-04-08-152705_create_table_saves/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..99802c9589702ce0328201f690fcb0f8c33ccab8 --- /dev/null +++ b/starkingdoms-api/migrations/2024-04-08-152705_create_table_saves/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE saves +( + id BIGINT NOT NULL PRIMARY KEY, + user_id BIGINT NOT NULL, + pack VARCHAR NOT NULL +); + +CREATE INDEX idx_saves_user_id ON saves (user_id); \ No newline at end of file diff --git a/starkingdoms-api/src/auth.rs b/starkingdoms-api/src/auth.rs new file mode 100644 index 0000000000000000000000000000000000000000..e4aaee0795991c3cf922789fcb7b830d190ed06c --- /dev/null +++ b/starkingdoms-api/src/auth.rs @@ -0,0 +1,38 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#[macro_export] +macro_rules! auth { + ($i:expr,$c:expr) => {{ + let authorization_hdr_value = match $i.headers().get("Authorization") { + Some(hdr) => hdr, + None => $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + ), + }; + let hdr_value_split = $crate::handle_error!(authorization_hdr_value.to_str()) + .split(' ') + .collect::>(); + if hdr_value_split.len() < 2 { + $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + ) + } + let tokens = hdr_value_split[1..].to_vec(); + }}; +} diff --git a/starkingdoms-api/src/config.rs b/starkingdoms-api/src/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..543f01b1c35246dd51cb5771a62e3f37b600764f --- /dev/null +++ b/starkingdoms-api/src/config.rs @@ -0,0 +1,43 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use serde::Deserialize; +use std::net::IpAddr; + +#[derive(Deserialize, Clone)] +pub struct Config { + pub server: ConfigServer, + pub database: ConfigDatabase, +} + +#[derive(Deserialize, Clone)] +pub struct ConfigServer { + pub bind: ConfigServerBind, + pub workers: Option, + pub machine_id: i32, + pub node_id: i32, + pub application_key: String, +} + +#[derive(Deserialize, Clone)] +pub struct ConfigServerBind { + pub ip: IpAddr, + pub port: u16, +} + +#[derive(Deserialize, Clone)] +pub struct ConfigDatabase { + pub url: String, +} diff --git a/starkingdoms-api/src/error.rs b/starkingdoms-api/src/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..f577fb47bd85c1f006c3e3e7f5d32a5271f92550 --- /dev/null +++ b/starkingdoms-api/src/error.rs @@ -0,0 +1,133 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use actix_web::error::{JsonPayloadError, PayloadError}; +use serde::Serialize; +use std::fmt::{Display, Formatter}; + +#[derive(Serialize, Debug)] +pub struct APIErrorsResponse { + pub errors: Vec, +} + +#[derive(Serialize, Debug)] +pub struct APIErrorResponse { + pub code: String, + pub message: String, + pub path: Option, +} + +impl Display for APIErrorResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From<&JsonPayloadError> for APIErrorResponse { + fn from(value: &JsonPayloadError) -> Self { + match value { + JsonPayloadError::OverflowKnownLength { length, limit } => { + APIErrorResponse { + code: "ERR_PAYLOAD_OVERFLOW_KNOWN_LENGTH".to_string(), + message: format!("Payload size is bigger than allowed & content length header set. (length: {}, limit: {})", length, limit), + path: None, + } + }, + JsonPayloadError::Overflow { limit } => { + APIErrorResponse { + code: "ERR_PAYLOAD_OVERFLOW".to_string(), + message: format!("Payload size is bigger than allowed but no content-length header is set. (limit: {})", limit), + path: None, + } + }, + JsonPayloadError::ContentType => { + APIErrorResponse { + code: "ERR_NOT_JSON".to_string(), + message: "Content-Type header not set to expected application/json".to_string(), + path: None, + } + }, + JsonPayloadError::Deserialize(e) => { + APIErrorResponse { + code: "ERR_JSON_DESERIALIZE".to_string(), + message: format!("Error deserializing JSON: {}", e), + path: None, + } + }, + JsonPayloadError::Serialize(e) => { + APIErrorResponse { + code: "ERR_JSON_SERIALIZE".to_string(), + message: format!("Error serializing JSON: {}", e), + path: None, + } + }, + JsonPayloadError::Payload(e) => { + e.into() + }, + _ => { + APIErrorResponse { + code: "ERR_UNKNOWN_ERROR".to_string(), + message: "An unknown error has occured".to_string(), + path: None, + } + } + } + } +} + +impl From<&PayloadError> for APIErrorResponse { + fn from(value: &PayloadError) -> Self { + match value { + PayloadError::Incomplete(e) => APIErrorResponse { + code: "ERR_UNEXPECTED_EOF".to_string(), + message: match e { + None => "Payload reached EOF but was incomplete".to_string(), + Some(e) => format!("Payload reached EOF but was incomplete: {}", e), + }, + path: None, + }, + PayloadError::EncodingCorrupted => APIErrorResponse { + code: "ERR_CORRUPTED_PAYLOAD".to_string(), + message: "Payload content encoding corrupted".to_string(), + path: None, + }, + PayloadError::Overflow => APIErrorResponse { + code: "ERR_PAYLOAD_OVERFLOW".to_string(), + message: "Payload reached size limit".to_string(), + path: None, + }, + PayloadError::UnknownLength => APIErrorResponse { + code: "ERR_PAYLOAD_UNKNOWN_LENGTH".to_string(), + message: "Unable to determine payload length".to_string(), + path: None, + }, + PayloadError::Http2Payload(e) => APIErrorResponse { + code: "ERR_HTTP2_ERROR".to_string(), + message: format!("HTTP/2 error: {}", e), + path: None, + }, + PayloadError::Io(e) => APIErrorResponse { + code: "ERR_IO_ERROR".to_string(), + message: format!("I/O error: {}", e), + path: None, + }, + _ => APIErrorResponse { + code: "ERR_UNKNOWN_ERROR".to_string(), + message: "An unknown error has occured".to_string(), + path: None, + }, + } + } +} diff --git a/starkingdoms-api/src/main.rs b/starkingdoms-api/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..0ec5a1d5ee39f3b28db904e833b89c5215d00627 --- /dev/null +++ b/starkingdoms-api/src/main.rs @@ -0,0 +1,185 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use crate::config::Config; +use crate::error::APIErrorResponse; +use actix_web::middleware::Logger; +use actix_web::web::{Data, JsonConfig}; +use actix_web::{App, Error, HttpResponse, HttpServer}; +use diesel::Connection; +use diesel_async::async_connection_wrapper::AsyncConnectionWrapper; +use diesel_async::pooled_connection::bb8::Pool; +use diesel_async::pooled_connection::AsyncDieselConnectionManager; +use diesel_async::AsyncPgConnection; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use hmac::digest::KeyInit; +use hmac::Hmac; +use log::{error, info}; +use sha2::Sha256; +use snowflake::SnowflakeIdGenerator; +use std::fs; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, UNIX_EPOCH}; + +pub mod error; +#[macro_use] +pub mod response; +pub mod config; +pub mod models; +pub mod routes; +pub mod schema; + +pub mod auth; +pub mod tokens; + +#[derive(Clone)] +pub struct AppState { + pub config: Config, + pub pool: bb8::Pool>, + pub idgen: Arc>, + pub key: Hmac, + pub key_raw: Vec +} + +pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); + +#[actix_web::main] +async fn main() { + env_logger::init(); + info!( + "StarKingdoms API v{} starting up", + env!("CARGO_PKG_VERSION") + ); + + let mut args = std::env::args(); + let config_path = match args.nth(1) { + Some(path) => path, + None => { + eprintln!("usage: starkingdoms-api "); + std::process::exit(1); + } + }; + + let config_pathbuf = PathBuf::from(config_path); + info!("Loading config from {}", config_pathbuf.display()); + + let config_str = match fs::read_to_string(&config_pathbuf) { + Ok(c_str) => c_str, + Err(e) => { + error!( + "Error loading configuration from {}: {}", + config_pathbuf.display(), + e + ); + std::process::exit(1); + } + }; + + let config: Config = match toml::from_str(&config_str) { + Ok(config) => config, + Err(e) => { + error!( + "Error parsing configuration in {}: {}", + config_pathbuf.display(), + e + ); + std::process::exit(1); + } + }; + + info!("Connecting to the database..."); + + let pool_config = AsyncDieselConnectionManager::::new(&config.database.url); + let pool = match Pool::builder().build(pool_config).await { + Ok(pool) => pool, + Err(e) => { + error!("Error while creating database pool: {}", e); + std::process::exit(1); + } + }; + + info!("Running pending migrations..."); + + let local_config = config.clone(); + let db_url = config.database.url.clone(); + + match actix_web::rt::task::spawn_blocking(move || { + // Lock block + let mut conn = match AsyncConnectionWrapper::::establish(&db_url) { + Ok(conn) => conn, + Err(e) => { + error!("Error acquiring connection from pool: {}", e); + std::process::exit(1); + } + }; + + match conn.run_pending_migrations(MIGRATIONS) { + Ok(_) => (), + Err(e) => { + error!("Failed to run pending migrations: {}", e); + std::process::exit(1); + } + } + }) + .await + { + Ok(_) => (), + Err(e) => { + error!("Error waiting for migrations: {}", e); + std::process::exit(1); + } + } + + let key = Hmac::new_from_slice(config.server.application_key.clone().as_bytes()).unwrap(); + + let stk_epoch = UNIX_EPOCH + Duration::from_secs(1616260136); + let id_generator = SnowflakeIdGenerator::with_epoch( + config.server.machine_id, + config.server.node_id, + stk_epoch, + ); + + let app_state = Data::new(AppState { + pool, + idgen: Arc::new(Mutex::new(id_generator)), + key, + key_raw: config.server.application_key.as_bytes().to_vec(), + config: config.clone(), + }); + + let server = HttpServer::new(move || { + App::new() + .app_data(JsonConfig::default().error_handler(|err, _rq| { + Error::from({ + let err2: APIErrorResponse = (&err).into(); + actix_web::error::InternalError::from_response( + err, + HttpResponse::BadRequest().json(err2), + ) + }) + })) + .service(routes::sign_save::sign_save_req) + .wrap(Logger::default()) + .wrap(actix_cors::Cors::permissive()) + .app_data(app_state.clone()) + }) + .bind((local_config.server.bind.ip, local_config.server.bind.port)) + .unwrap(); + + server.run().await.unwrap(); + + info!("Goodbye!"); +} diff --git a/starkingdoms-api/src/models.rs b/starkingdoms-api/src/models.rs new file mode 100644 index 0000000000000000000000000000000000000000..67831d912f59e3658a9c0e6063ad6d8b3068ff00 --- /dev/null +++ b/starkingdoms-api/src/models.rs @@ -0,0 +1,26 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use diesel::{Identifiable, Insertable, Queryable, Selectable}; + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, PartialEq, Clone)] +#[diesel(table_name = crate::schema::saves)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Save { + pub id: i64, + pub user_id: i64, + pub pack: String +} diff --git a/starkingdoms-api/src/response.rs b/starkingdoms-api/src/response.rs new file mode 100644 index 0000000000000000000000000000000000000000..fbd404ecf00523c01d79c6f9790d895f49c18967 --- /dev/null +++ b/starkingdoms-api/src/response.rs @@ -0,0 +1,122 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use crate::error::APIErrorsResponse; +use actix_web::body::BoxBody; +use actix_web::error::JsonPayloadError; +use actix_web::http::StatusCode; +use actix_web::{HttpRequest, HttpResponse, Responder}; +use serde::Serialize; +use std::fmt::{Debug, Display, Formatter}; + +#[derive(Debug)] +pub enum JsonAPIResponse { + Error(StatusCode, APIErrorsResponse), + Success(StatusCode, T), +} + +impl Display for JsonAPIResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl Responder for JsonAPIResponse { + type Body = BoxBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + match self { + JsonAPIResponse::Error(c, r) => match serde_json::to_string(&r) { + Ok(body) => HttpResponse::build(c).body(body), + Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), + }, + JsonAPIResponse::Success(c, b) => match serde_json::to_string(&b) { + Ok(body) => HttpResponse::build(c).body(body), + Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), + }, + } + } +} + +#[macro_export] +macro_rules! err { + ($c:expr,$e:expr) => { + return $crate::response::JsonAPIResponse::Error($c, $e) + }; +} + +#[macro_export] +macro_rules! ok { + ($c:expr,$e:expr) => { + return $crate::response::JsonAPIResponse::Success($c, $e) + }; + ($e:expr) => { + return $crate::response::JsonAPIResponse::Success(actix_web::http::StatusCode::OK, $e) + }; +} + +#[macro_export] +macro_rules! internal_error { + ($e:expr) => {{ + log::error!("internal error: {}", $e); + $crate::err!( + actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, + $crate::make_err!("ERR_INTERNAL_ERROR", $e) + ); + }}; +} + +#[macro_export] +macro_rules! handle_error { + ($e:expr,$c:expr,$r:expr) => { + match $e { + Ok(r) => r, + Err(e) => { + log::error!("error: {}", e); + $crate::err!($c, $r) + } + } + }; + ($e:expr) => { + match $e { + Ok(r) => r, + Err(e) => { + $crate::internal_error!(e) + } + } + }; +} + +#[macro_export] +macro_rules! make_err { + ($c:expr,$m:expr,$p:expr) => { + $crate::error::APIErrorsResponse { + errors: vec![$crate::error::APIErrorResponse { + code: $c.to_string(), + message: $m.to_string(), + path: Some($p.to_string()), + }], + } + }; + ($c:expr,$m:expr) => { + $crate::error::APIErrorsResponse { + errors: vec![$crate::error::APIErrorResponse { + code: $c.to_string(), + message: $m.to_string(), + path: None, + }], + } + }; +} diff --git a/starkingdoms-api/src/routes/mod.rs b/starkingdoms-api/src/routes/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..e8190c6f0671ec371430cb9ed5f4662b4b117048 --- /dev/null +++ b/starkingdoms-api/src/routes/mod.rs @@ -0,0 +1,16 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +pub mod sign_save; \ No newline at end of file diff --git a/starkingdoms-api/src/routes/sign_save.rs b/starkingdoms-api/src/routes/sign_save.rs new file mode 100644 index 0000000000000000000000000000000000000000..ef88dca5645629514a6c653d1fffe9c11ea1dc92 --- /dev/null +++ b/starkingdoms-api/src/routes/sign_save.rs @@ -0,0 +1,100 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use std::collections::HashMap; +use std::hash::Hash; +use crate::response::JsonAPIResponse; +use crate::AppState; +use actix_web::{ + http::StatusCode, + post, + web::{Data, Json}, +}; +use log::error; +use serde::{Deserialize, Serialize}; +use starkingdoms_common::{__noverify__unpack_savefile, pack_savefile, PartType, SaveModule, unpack_savefile}; + +#[derive(Deserialize)] +pub struct SignSaveRequest { + pub old_save: String, + pub new_partial: String +} + +#[derive(Serialize, Debug)] +pub struct SignSaveResponse { + pub signed_save: String +} + +fn inc(a: &mut HashMap, t: &T, by: u64) { + if let Some(r) = a.get_mut(t) { + *r += by; + } else { + a.insert(t.clone(), by); + } +} + +fn recursive_inc(a: &mut HashMap, m: &Option) { + if let Some(m) = m { + inc(a, &m.part_type, 1); + for child in &m.children { + recursive_inc(a, child); + } + } +} + +#[post("/sign_save")] +pub async fn sign_save_req( + req: Json, + state: Data, +) -> JsonAPIResponse { + let old_save = handle_error!(unpack_savefile(&state.key_raw, req.old_save.clone())); + let new_save = handle_error!(__noverify__unpack_savefile(req.new_partial.clone())); + + // ensure part counts are the same + let mut part_counts_old: HashMap = HashMap::new(); + + for unused_modules in &old_save.unused_modules { + inc(&mut part_counts_old, &unused_modules.0, unused_modules.1 as u64); + } + for child in &old_save.children { + recursive_inc(&mut part_counts_old, child); + } + + let mut part_counts_new: HashMap = HashMap::new(); + + for unused_modules in &new_save.unused_modules { + inc(&mut part_counts_new, &unused_modules.0, unused_modules.1 as u64); + } + for child in &new_save.children { + recursive_inc(&mut part_counts_new, child); + } + + if part_counts_old != part_counts_new { + error!("save validation failed: {:?} -> {:?}", part_counts_old, part_counts_new); + error!("old file: {:?}", old_save); + error!("new file: {:?}", new_save); + err!( + StatusCode::BAD_REQUEST, + make_err!("ERR_SAVE_VALIDATION_FAILED", "save validation failed: part counts do not match") + ) + } + + // resign the savefile and return it to the user + + ok!(SignSaveResponse { + signed_save: pack_savefile(&state.key_raw, new_save) + }) +} diff --git a/starkingdoms-api/src/schema.rs b/starkingdoms-api/src/schema.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ba43f51c9b5704b04a027a97a829e7281826d36 --- /dev/null +++ b/starkingdoms-api/src/schema.rs @@ -0,0 +1,9 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + saves (id) { + id -> Int8, + user_id -> Int8, + pack -> Varchar, + } +} diff --git a/starkingdoms-api/src/tokens.rs b/starkingdoms-api/src/tokens.rs new file mode 100644 index 0000000000000000000000000000000000000000..739f18bbcd5231d7505f9b775305fe4a21eac1b7 --- /dev/null +++ b/starkingdoms-api/src/tokens.rs @@ -0,0 +1,26 @@ +// StarKingdoms.IO, a browser game about drifting through space +// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use serde::{Deserialize, Serialize}; +use std::time::SystemTime; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UserToken { + pub id: i64, + pub username: String, + pub permission_level: i32, + pub expires: SystemTime, +} diff --git a/starkingdoms-client/src/pages/ShipEditor.svelte b/starkingdoms-client/src/pages/ShipEditor.svelte index f59a42311b38a7c749ebb99a7fbdf3be485018fd..4a4c9ae1f94289ebfa243039cc9480cadaa2aaa5 100644 --- a/starkingdoms-client/src/pages/ShipEditor.svelte +++ b/starkingdoms-client/src/pages/ShipEditor.svelte @@ -5,7 +5,7 @@ import "../css/style.scss"; import HeartIcon from "../icons/HeartIcon.svelte"; import Popup from "../components/ui/Popup.svelte"; - import { unpack_save } from "../save.ts"; + import {__pack_save_for_api, unpack_save} from "../save.ts"; import { PartType } from "../protocol.ts"; import { onMount } from "svelte"; import { part_texture_url } from "../textures.ts"; @@ -347,7 +347,7 @@ return [part_type, children]; } - function save_btn() { + async function save_btn() { if (confirm_save) { // todo: @ghostly you need to turn this back into a savefile and then call pack_partial on it let children = save_recursive(0, 0, 0).children; @@ -360,6 +360,28 @@ } let save_data = [children, unused_modules]; console.log(save_data); + + let packed_savefile = __pack_save_for_api(save_data); + + let r = await fetch(config.environments[3].apiBaseUrl + '/sign_save', { + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + old_save: window.localStorage.getItem("save")!, + new_partial: packed_savefile + }) + }); + + if (r.ok) { + let body = await r.json(); + + // @ts-ignore + window.localStorage.setItem("save", body.signed_save); + window.location.href = "/"; + } + //window.location.href = "/"; } confirm_save = !confirm_save; diff --git a/starkingdoms-client/src/save.ts b/starkingdoms-client/src/save.ts index 124dec6bdb70714f53a40825889a51985b7770a0..a74c69a20768e92ae911764280c5cfbedaa72460 100644 --- a/starkingdoms-client/src/save.ts +++ b/starkingdoms-client/src/save.ts @@ -12,10 +12,22 @@ function bytesToBase64(bytes: any) { } export function unpack_save(data: string): any { + console.log(decode(base64ToBytes(data))); // @ts-ignore return decode(decode(base64ToBytes(data))[0]); } -export function pack_partial(data: any): string { - return bytesToBase64(encode(data)); +export function __pack_save_for_api(data: any): string { + console.log([ + encode(data), + [] + ]); + return bytesToBase64( + encode( + [ + Array.from(encode(data)), + [] + ], + ) + ); } diff --git a/starkingdoms-common/src/lib.rs b/starkingdoms-common/src/lib.rs index b754fd835732943e96db46134a92b445e25d1d4f..88d1e6ddad72fcaf434cfa018232a824f0e222b8 100644 --- a/starkingdoms-common/src/lib.rs +++ b/starkingdoms-common/src/lib.rs @@ -92,3 +92,18 @@ pub fn unpack_savefile(key: &[u8], file: String) -> Result Result> { + // << reverse! << + let savefile_bytes = base64::engine::general_purpose::STANDARD + .decode(file) + .map_err(|e| format!("error decoding b64: {e}"))?; + + let save_file: Savefile = rmp_serde::from_slice(&savefile_bytes) + .map_err(|e| format!("error decoding savefile wrapper: {e}"))?; + + + let save_data = rmp_serde::from_slice(&save_file.data_msgpack) + .map_err(|e| format!("error decoding inner signature: {e}"))?; + + Ok(save_data) +}