Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for players authentication and Postgres database #9

Merged
merged 13 commits into from
Jul 14, 2024
13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,25 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-governor = "0.5"
actix-web = "4.4"
cached = { version = "0.49", features = ["async"] }
base64 = "0.22"
cached = { version = "0.52", features = ["async"] }
chacha20poly1305 = "0.10"
confy = "0.6"
deadpool-postgres = "0.14"
deku = "0.17"
env_logger = "0.11"
futures = "0.3"
octocrab = "0.38"
rand_core = "0.6"
reqwest = { version = "0.12", features = ["charset", "http2", "macos-system-configuration", "rustls-tls"], default-features = false }
secure-string = { version = "0.3", features = ["serde"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_with = { version = "3.8", features = ["base64", "time_0_3"] }
strum = { version = "0.26", features = ["derive"] }
tokio = "1.37"
tokio-postgres = { version = "0.7", features = ["with-uuid-1"] }
url = "2.5"
uuid = { version = "1.8", features = ["v4", "macro-diagnostics"] }
12 changes: 12 additions & 0 deletions src/app_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use cached::TimedCache;
use std::sync::Mutex;

use crate::config::ApiConfig;
use crate::fetcher::Fetcher;
use crate::version::CachedReleased;

pub struct AppData {
pub cache: Mutex<TimedCache<&'static str, CachedReleased>>,
pub config: ApiConfig,
pub fetcher: Fetcher,
}
40 changes: 38 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use std::time::Duration;

use secure_string::SecureString;
use serde::{Deserialize, Serialize};
use serde_with::base64::Base64;
use serde_with::serde_as;
use serde_with::DurationSeconds;

#[serde_as]
#[derive(Serialize, Deserialize)]
pub struct ApiConfig {
pub listen_address: String,
Expand All @@ -9,8 +15,23 @@ pub struct ApiConfig {
pub game_repository: String,
pub updater_repository: String,
pub updater_filename: String,
pub cache_lifespan: u64,
#[serde_as(as = "DurationSeconds<u64>")]
pub cache_lifespan: Duration,
pub github_pat: Option<SecureString>,
pub db_host: String,
pub db_user: String,
pub db_password: SecureString,
pub db_database: String,
pub player_nickname_maxlength: usize,
pub player_allow_non_ascii: bool,
pub game_api_token: String,
pub game_api_url: String,
pub game_server_address: String,
pub game_server_port: u16,
#[serde_as(as = "DurationSeconds<u64>")]
pub game_api_token_duration: Duration,
#[serde_as(as = "Base64")]
pub connection_token_key: [u8; 32],
}

impl Default for ApiConfig {
Expand All @@ -22,8 +43,23 @@ impl Default for ApiConfig {
game_repository: "ThisSpaceOfMine".to_string(),
updater_filename: "this_updater_of_mine".to_string(),
updater_repository: "ThisUpdaterOfMine".to_string(),
cache_lifespan: 5 * 60,
cache_lifespan: Duration::from_secs(5 * 60),
github_pat: None,
db_host: "localhost".to_string(),
db_user: "api".to_string(),
db_password: "password".into(),
db_database: "tsom_db".to_string(),
player_nickname_maxlength: 16,
player_allow_non_ascii: false,
game_api_token: "".into(),
game_api_url: "http://localhost".to_string(),
game_server_address: "localhost".to_string(),
game_server_port: 29536,
game_api_token_duration: Duration::from_secs(5 * 60),
connection_token_key: [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30, 31,
],
}
}
}
96 changes: 96 additions & 0 deletions src/errors/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use actix_web::body::BoxBody;
use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError};
use serde::{Serialize, Serializer};
use std::fmt;
use strum::AsRefStr;

use crate::error_from;

#[derive(Debug)]
pub enum ErrorCause {
Database,
Internal,
}

#[derive(Debug, AsRefStr)]
#[strum(serialize_all = "snake_case")]
pub enum ErrorCode {
AuthenticationInvalidToken,
NicknameEmpty,
NicknameToolong,
NicknameForbiddenCharacters,

#[strum(to_string = "{0}")]
External(String),

Check warning on line 25 in src/errors/api.rs

View workflow job for this annotation

GitHub Actions / clippy

field `0` is never read

warning: field `0` is never read --> src/errors/api.rs:25:14 | 25 | External(String), | -------- ^^^^^^ | | | field in this variant | = note: `#[warn(dead_code)]` on by default help: consider changing the field to be of unit type to suppress this warning while preserving the field numbering, or remove the field | 25 | External(()), | ~~
}

#[derive(Debug, Serialize)]
pub struct RequestError {
err_code: ErrorCode,
err_desc: String,
}

#[derive(Debug)]
pub enum RouteError {
ServerError(ErrorCause, ErrorCode),
InvalidRequest(RequestError),
}

impl Serialize for ErrorCode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.as_ref().serialize(serializer)
}
}

impl RequestError {
pub fn new(err_code: ErrorCode, err_desc: String) -> Self {
Self { err_code, err_desc }
}
}

impl fmt::Display for RouteError {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
unimplemented!()
}
}

error_from! { transform_io rand_core::Error, RouteError }
error_from! { transform std::io::Error, RouteError, |value| {
RouteError::ServerError(
ErrorCause::Internal,
ErrorCode::External(value.to_string())
)
} }
error_from! { transform tokio_postgres::Error, RouteError, |value| {
RouteError::ServerError(
ErrorCause::Database,
ErrorCode::External(value.to_string())
)
} }

error_from! { transform deadpool_postgres::PoolError, RouteError, |value| {
RouteError::ServerError(
ErrorCause::Database,
ErrorCode::External(value.to_string())
)
} }

impl ResponseError for RouteError {
fn status_code(&self) -> StatusCode {
match self {
RouteError::ServerError(..) => StatusCode::INTERNAL_SERVER_ERROR,
RouteError::InvalidRequest(_) => StatusCode::BAD_REQUEST,
}
}

fn error_response(&self) -> HttpResponse<BoxBody> {
match self {
RouteError::ServerError(cause, err_code) => {
eprintln!("{cause:?} error: {}", err_code.as_ref());
HttpResponse::InternalServerError().finish()
}
RouteError::InvalidRequest(err) => HttpResponse::BadRequest().json(err),
}
}
}
17 changes: 17 additions & 0 deletions src/errors/fetcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use crate::error_from;

pub type FetchResult<T> = std::result::Result<T, FetcherError>;

#[derive(Debug)]
pub enum FetcherError {
OctoError(octocrab::Error),

Check warning on line 7 in src/errors/fetcher.rs

View workflow job for this annotation

GitHub Actions / clippy

field `0` is never read

warning: field `0` is never read --> src/errors/fetcher.rs:7:15 | 7 | OctoError(octocrab::Error), | --------- ^^^^^^^^^^^^^^^ | | | field in this variant | help: consider changing the field to be of unit type to suppress this warning while preserving the field numbering, or remove the field | 7 | OctoError(()), | ~~
ReqwestError(reqwest::Error),

Check warning on line 8 in src/errors/fetcher.rs

View workflow job for this annotation

GitHub Actions / clippy

field `0` is never read

warning: field `0` is never read --> src/errors/fetcher.rs:8:18 | 8 | ReqwestError(reqwest::Error), | ------------ ^^^^^^^^^^^^^^ | | | field in this variant | help: consider changing the field to be of unit type to suppress this warning while preserving the field numbering, or remove the field | 8 | ReqwestError(()), | ~~
InvalidSha256(usize),

Check warning on line 9 in src/errors/fetcher.rs

View workflow job for this annotation

GitHub Actions / clippy

field `0` is never read

warning: field `0` is never read --> src/errors/fetcher.rs:9:19 | 9 | InvalidSha256(usize), | ------------- ^^^^^ | | | field in this variant | help: consider changing the field to be of unit type to suppress this warning while preserving the field numbering, or remove the field | 9 | InvalidSha256(()), | ~~
WrongChecksum,
NoReleaseFound,
InvalidVersion,
}

error_from! { move octocrab::Error, FetcherError, FetcherError::OctoError }
error_from! { move reqwest::Error, FetcherError, FetcherError::ReqwestError }
error_from! { replace semver::Error, FetcherError, FetcherError::InvalidVersion }
35 changes: 35 additions & 0 deletions src/errors/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
pub mod api;
pub mod fetcher;

// to delete '$into_type:path' you need to use proc macros and further manipulation of the AST
#[macro_export]
macro_rules! error_from {
(move $from:path, $into_type:path, $into:path) => {
impl From<$from> for $into_type {
fn from(err: $from) -> Self {
$into(err)
}
}
};
(replace $from:path, $into_type:path, $into:path) => {
impl From<$from> for $into_type {
fn from(_: $from) -> Self {
$into
}
}
};
(transform $from:path, $into_type:path, |$err_name:ident| $blk:block) => {
impl From<$from> for $into_type {
fn from($err_name: $from) -> Self {
$blk
}
}
};
(transform_io $from:path, $into_type:path) => {
impl From<$from> for $into_type {
fn from(err: $from) -> Self {
std::io::Error::from(err).into()
}
}
};
}
47 changes: 9 additions & 38 deletions src/fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ use octocrab::{Octocrab, OctocrabBuilder};
use semver::Version;

use crate::config::ApiConfig;
use crate::errors::fetcher::{FetchResult, FetcherError};
use crate::game_data::{Asset, Assets, GameRelease, Repo};

type Result<T> = std::result::Result<T, FetcherError>;

pub struct Fetcher {
octocrab: Octocrab,
game_repo: Repo,
Expand All @@ -19,18 +18,8 @@ pub struct Fetcher {

struct ChecksumFetcher(reqwest::Client);

#[derive(Debug)]
pub enum FetcherError {
OctoError(octocrab::Error),
ReqwestError(reqwest::Error),
InvalidSha256(usize),
WrongChecksum,
NoReleaseFound,
InvalidVersion,
}

impl Fetcher {
pub fn from_config(config: &ApiConfig) -> Result<Self> {
pub fn from_config(config: &ApiConfig) -> FetchResult<Self> {
let mut octocrab = OctocrabBuilder::default();
if let Some(github_pat) = &config.github_pat {
octocrab = octocrab.personal_token(github_pat.unsecure().to_string());
Expand All @@ -49,7 +38,7 @@ impl Fetcher {
self.octocrab.repos(repo.owner(), repo.repository())
}

pub async fn get_latest_game_release(&self) -> Result<GameRelease> {
pub async fn get_latest_game_release(&self) -> FetchResult<GameRelease> {
let releases = self
.on_repo(&self.game_repo)
.releases()
Expand Down Expand Up @@ -78,7 +67,7 @@ impl Fetcher {

Ok((platform.to_string(), asset))
})
.collect::<Result<Assets>>()?;
.collect::<FetchResult<Assets>>()?;

for (version, release) in versions_released {
for ((platform, mut asset), sha256) in self
Expand Down Expand Up @@ -108,7 +97,7 @@ impl Fetcher {
}
}

pub async fn get_latest_updater_release(&self) -> Result<Assets> {
pub async fn get_latest_updater_release(&self) -> FetchResult<Assets> {
let last_release = self
.on_repo(&self.updater_repo)
.releases()
Expand All @@ -128,15 +117,15 @@ impl Fetcher {

Ok((platform.to_string(), asset))
})
.collect::<Result<Assets>>()
.collect::<FetchResult<Assets>>()
}

async fn get_assets_and_checksums<'a: 'b, 'b, A>(
&self,
assets: A,
version: &Version,
binaries: Option<&Assets>,
) -> impl Iterator<Item = ((&'b str, Asset), Result<String>)>
) -> impl Iterator<Item = ((&'b str, Asset), FetchResult<String>)>
where
A: IntoIterator<Item = &'a repos::Asset>,
{
Expand Down Expand Up @@ -169,7 +158,7 @@ impl ChecksumFetcher {
Self(reqwest::Client::new())
}

async fn resolve(&self, asset: &Asset) -> Result<String> {
async fn resolve(&self, asset: &Asset) -> FetchResult<String> {
let response = self
.0
.get(format!("{}.sha256", asset.download_url))
Expand All @@ -180,7 +169,7 @@ impl ChecksumFetcher {
self.parse_response(asset.name.as_str(), response.as_str())
}

fn parse_response(&self, asset_name: &str, response: &str) -> Result<String> {
fn parse_response(&self, asset_name: &str, response: &str) -> FetchResult<String> {
let parts: Vec<_> = response.split_whitespace().collect();
if parts.len() != 2 {
return Err(FetcherError::InvalidSha256(parts.len()));
Expand All @@ -194,24 +183,6 @@ impl ChecksumFetcher {
}
}

impl From<octocrab::Error> for FetcherError {
fn from(err: octocrab::Error) -> Self {
FetcherError::OctoError(err)
}
}

impl From<reqwest::Error> for FetcherError {
fn from(err: reqwest::Error) -> Self {
FetcherError::ReqwestError(err)
}
}

impl From<semver::Error> for FetcherError {
fn from(_: semver::Error) -> Self {
FetcherError::InvalidVersion
}
}

fn remove_game_suffix(asset_name: &str) -> &str {
let platform = asset_name
.find('.')
Expand Down
Loading
Loading