Initial commit
This commit is contained in:
commit
7abd08dbff
6 changed files with 1393 additions and 0 deletions
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.github/
|
||||||
|
credentials/
|
||||||
|
target/
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
1214
Cargo.lock
generated
Normal file
1214
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal 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
9
Dockerfile
Normal 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
148
src/main.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue