Initial commit

This commit is contained in:
Bad Manners 2024-10-01 20:05:08 -03:00 committed by Eric Rodrigues Pires
commit 7abd08dbff
6 changed files with 1393 additions and 0 deletions

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
.github/
credentials/
target/

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1214
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "sish_games"
description = "Server for sish games"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.89"
axum = "0.7.7"
base64 = "0.22.1"
clap = { version = "4", features = ["derive"] }
hmac = "0.12.1"
jsonwebtoken = "9"
ring = "0.17.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10.8"
tokio = { version = "1", features = ["full"] }

9
Dockerfile Normal file
View file

@ -0,0 +1,9 @@
FROM rust:1.81.0-alpine3.20 AS builder
RUN apk add --no-cache musl-dev libressl-dev
WORKDIR /usr/src/app
COPY . .
RUN cargo build --release
FROM alpine:3.20
COPY --from=builder /usr/src/app/target/release/sish_games /usr/local/bin/sish_games
ENTRYPOINT [ "sish_games" ]

148
src/main.rs Normal file
View file

@ -0,0 +1,148 @@
use std::{collections::HashSet, path::PathBuf, sync::Arc};
use anyhow::{Context, Result};
use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::post, Json, Router};
use base64::{prelude::BASE64_STANDARD, Engine};
use clap::Parser;
use hmac::{Hmac, Mac};
use jsonwebtoken::{
decode, encode, get_current_timestamp, Algorithm, DecodingKey, EncodingKey, Validation,
};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tokio::{fs, net::TcpListener};
#[derive(Debug, Serialize, Deserialize)]
struct AuthenticationClaims {
sub: String,
aud: String,
exp: u64,
}
#[derive(Deserialize)]
struct GenerationRequest {
audience: String,
user: String,
password: String,
}
#[derive(Clone)]
struct GenerationState {
encoding_key: Arc<EncodingKey>,
audiences: Arc<HashSet<String>>,
}
#[allow(dead_code)]
#[derive(Deserialize)]
struct ValidationPasswordRequest {
password: String,
user: String,
remote_addr: String,
}
#[derive(Clone)]
struct ValidationState {
decoding_key: Arc<DecodingKey>,
validation: Arc<Validation>,
}
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(short = 'i', long, value_name = "FILE")]
private_key: PathBuf,
#[arg(short, long, value_name = "FILE")]
public_key: PathBuf,
#[arg(short, long, num_args(1..))]
audiences: Vec<String>,
#[arg(short, long)]
validation_token: String,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let audiences = args.audiences;
let private_key = fs::read(args.private_key)
.await
.with_context(|| "Private key was not found")?;
let encoding_key =
Arc::new(EncodingKey::from_ec_pem(&private_key).with_context(|| "Invalid private key")?);
let public_key = fs::read(args.public_key)
.await
.with_context(|| "Private key was not found")?;
let decoding_key =
Arc::new(DecodingKey::from_ec_pem(&public_key).with_context(|| "Invalid public key")?);
let mut validation = Validation::new(Algorithm::ES256);
validation.set_audience(&audiences);
validation.set_required_spec_claims(&["exp", "aud", "sub"]);
let router = Router::new()
.route("/", post(generation_handler))
.with_state(GenerationState {
encoding_key,
audiences: Arc::new(audiences.iter().cloned().collect()),
})
.route(
&format!("/validate/{}", args.validation_token),
post(validation_handler),
)
.with_state(ValidationState {
decoding_key,
validation: Arc::new(validation),
});
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, router).await.unwrap();
Ok(())
}
async fn generation_handler(
State(state): State<GenerationState>,
Json(payload): Json<GenerationRequest>,
) -> impl IntoResponse {
if !state.audiences.contains(&payload.audience) {
return StatusCode::FORBIDDEN.into_response();
}
let password = match BASE64_STANDARD.decode(payload.password) {
Ok(password) => password,
Err(_) => return StatusCode::FORBIDDEN.into_response(),
};
let mut hmac = Hmac::<Sha256>::new_from_slice(payload.audience.as_bytes()).unwrap();
hmac.update(payload.user.as_bytes());
if hmac.verify_slice(&password).is_err() {
return StatusCode::FORBIDDEN.into_response();
}
let claims = AuthenticationClaims {
aud: payload.audience,
exp: get_current_timestamp() + 5 * 60,
sub: payload.user,
};
let jwt = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&claims,
&state.encoding_key,
)
.unwrap();
jwt.into_response()
}
async fn validation_handler(
State(state): State<ValidationState>,
Json(payload): Json<ValidationPasswordRequest>,
) -> impl IntoResponse {
let claims = match decode::<AuthenticationClaims>(
&payload.password,
&state.decoding_key,
&state.validation,
) {
Ok(token) => token.claims,
Err(_) => return StatusCode::FORBIDDEN,
};
if claims.sub == payload.user {
StatusCode::OK
} else {
StatusCode::FORBIDDEN
}
}