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