From ff4210d0329568f6046bc8a0dc3fda3041731cf8 Mon Sep 17 00:00:00 2001 From: itsscb Date: Tue, 17 Dec 2024 00:35:40 +0100 Subject: [PATCH] challenge: 16 --- Cargo.toml | 8 +++- src/lib.rs | 5 ++- src/routes/mod.rs | 4 ++ src/routes/task_sixteen/claims.rs | 41 ++++++++++++++++++++ src/routes/task_sixteen/decode.rs | 57 ++++++++++++++++++++++++++++ src/routes/task_sixteen/mod.rs | 7 ++++ src/routes/task_sixteen/unwrap.rs | 35 +++++++++++++++++ src/routes/task_sixteen/wrap.rs | 53 ++++++++++++++++++++++++++ src/routes/task_twelve/game/board.rs | 12 ------ src/routes/task_twelve/game/mod.rs | 17 --------- src/routes/task_twelve/mod.rs | 1 - tests/task_twelve/main.rs | 5 +-- 12 files changed, 209 insertions(+), 36 deletions(-) create mode 100644 src/routes/task_sixteen/claims.rs create mode 100644 src/routes/task_sixteen/decode.rs create mode 100644 src/routes/task_sixteen/mod.rs create mode 100644 src/routes/task_sixteen/unwrap.rs create mode 100644 src/routes/task_sixteen/wrap.rs diff --git a/Cargo.toml b/Cargo.toml index e287100..7d48d9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,18 +10,22 @@ serde_json = "1.0.133" shuttle-axum = "0.49.0" shuttle-runtime = "0.49.0" tokio = "1.28.2" +jsonwebtoken = { version = "9.3.0", optional = true } cargo-manifest = { version = "0.17.0", optional = true } serde_yml = { version = "0.0.12", optional = true } toml = { version = "0.8.19", optional = true } rand = { version = "0.8.5", optional = true } +axum-extra = { version = "0.9.6", features = ["cookie"] } +chrono = "0.4.39" +tracing = "0.1.41" [dev-dependencies] axum-test = "16.4.0" [features] -default = [] +default = ["jsonwebtoken"] task1-9 = ["cargo-manifest", "serde_yml", "toml"] task12 = ["rand"] -task16 = [] +task16 = ["jsonwebtoken"] task19 = [] task23 = [] diff --git a/src/lib.rs b/src/lib.rs index 1343882..6582fe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ mod routes; -// use axum::routing::{get, post}; #[cfg(feature = "task12")] use routes::{board, place, random_board, reset, Board}; +use routes::{decode, unwrap, wrap}; #[cfg(feature = "task1-9")] use routes::{ hello_bird, hello_world, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, milk, minus_one, @@ -42,4 +42,7 @@ pub fn router() -> axum::Router { .with_state(Board::new()); Router::new() + .route("/16/wrap", post(wrap)) + .route("/16/unwrap", get(unwrap)) + .route("/16/decode", post(decode)) } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 10727d8..884e58c 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -3,6 +3,10 @@ mod task_twelve; #[cfg(feature = "task12")] pub use task_twelve::{board, game::Board, place, random_board, reset}; +// #[cfg(feature = "task16")] +mod task_sixteen; +pub use task_sixteen::{decode, unwrap, wrap}; + #[cfg(feature = "task1-9")] mod hello_bird; diff --git a/src/routes/task_sixteen/claims.rs b/src/routes/task_sixteen/claims.rs new file mode 100644 index 0000000..d260a72 --- /dev/null +++ b/src/routes/task_sixteen/claims.rs @@ -0,0 +1,41 @@ +use std::fmt::{self, Display, Formatter}; + +use chrono::{Duration, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + exp: Option, + #[serde(rename = "sub")] + sub: Option, +} + +impl Claims { + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + pub fn new(sub: Option) -> Self { + let exp = Some((Utc::now() + Duration::minutes(15)).timestamp()); + Self { exp, sub } + } + + pub fn sub(&self) -> Option { + self.sub.clone() + } +} + +impl Display for Claims { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self.exp { + Some(exp) => write!( + f, + "expires: {:?}\nsub: {}", + exp, + self.sub.as_deref().unwrap_or("None") + ), + None => write!( + f, + "expires: None\nsub: {}", + self.sub.as_deref().unwrap_or("None") + ), + } + } +} diff --git a/src/routes/task_sixteen/decode.rs b/src/routes/task_sixteen/decode.rs new file mode 100644 index 0000000..e5a5868 --- /dev/null +++ b/src/routes/task_sixteen/decode.rs @@ -0,0 +1,57 @@ +use std::collections::HashSet; + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation}; +use serde_json::Value; +use tracing::{error, info, instrument}; + +#[instrument(skip(payload))] +pub async fn decode(payload: String) -> Result { + info!("payload" = payload); + + let decoding_key = DecodingKey::from_rsa_pem( + "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs5BlLjDtKuEY2NV3+xhH +WWlKrZDWkIOV+HoLURIBEpAHa11xU+wL9sySR17j4bL9MJawlCJAGArW8vnDiAv8 +366PfOhCqZsD9N2iG28y7vf5q1PhoXl/Vfuelykw0k+r4054h0uCg9Olal0Nm/V8 +vsdPEC3wjNLBi86oYESkW43/7lbBWPBti1POCVJDuBEASZFhIR2+mfz6AFWQwmqO +zzhP1Yli/7EtNMELWezQJXnVLQ3JvjT2btWWwKYT468YX/NtQgMC7SLvIRBuWb/Z +ayfoi/9rGndqW0YPE1xwJEQA415w5HbfTneyAIxDy7TC8/+dFaKRcoPiEQA1T5bk +OQIDAQAB +-----END PUBLIC KEY-----" + .as_bytes(), + ) + .unwrap(); + + let mut validation = Validation::new(Algorithm::RS256); + validation.validate_exp = true; + validation.required_spec_claims = HashSet::new(); + validation.algorithms.push(Algorithm::RS512); + let token = jsonwebtoken::decode::(&payload, &decoding_key, &validation); + + match token { + Ok(token) => { + info!("token" = format!("{:?}", token)); + Ok(token.claims.to_string()) + } + Err(e) => { + if e.to_string().contains("InvalidSignature") { + error!( + "error" = format!("{}", e.to_string()), + "status" = StatusCode::UNAUTHORIZED.to_string() + ); + + return Err((StatusCode::UNAUTHORIZED, "").into_response()); + } + error!( + "error" = format!("{}", e.to_string()), + "status" = StatusCode::BAD_REQUEST.to_string() + ); + + return Err((StatusCode::BAD_REQUEST, "Token decoding error").into_response()); + } + } +} diff --git a/src/routes/task_sixteen/mod.rs b/src/routes/task_sixteen/mod.rs new file mode 100644 index 0000000..799c316 --- /dev/null +++ b/src/routes/task_sixteen/mod.rs @@ -0,0 +1,7 @@ +mod claims; +mod decode; +mod unwrap; +mod wrap; +pub use decode::decode; +pub use unwrap::unwrap; +pub use wrap::wrap; diff --git a/src/routes/task_sixteen/unwrap.rs b/src/routes/task_sixteen/unwrap.rs new file mode 100644 index 0000000..1004545 --- /dev/null +++ b/src/routes/task_sixteen/unwrap.rs @@ -0,0 +1,35 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use axum_extra::extract::CookieJar; +use jsonwebtoken::{DecodingKey, Validation}; +use tracing::{info, instrument}; + +use super::claims::Claims; + +#[instrument(skip(jar))] +pub async fn unwrap(jar: CookieJar) -> Result { + let payload = jar + .get("gift") + .map(|cookie| cookie.value().to_string()) + .map(|token| { + let token = jsonwebtoken::decode::( + &token, + &DecodingKey::from_secret("secret".as_ref()), + &Validation::default(), + ); + token + }); + + match payload { + Some(Ok(claims)) => { + info!("claims" = claims.claims.sub()); + claims + .claims + .sub() + .map_or_else(|| Err((StatusCode::BAD_REQUEST, "").into_response()), Ok) + } + _ => Err((StatusCode::BAD_REQUEST, "").into_response()), + } +} diff --git a/src/routes/task_sixteen/wrap.rs b/src/routes/task_sixteen/wrap.rs new file mode 100644 index 0000000..5ccd870 --- /dev/null +++ b/src/routes/task_sixteen/wrap.rs @@ -0,0 +1,53 @@ +use axum::{ + http::{header::CONTENT_TYPE, HeaderMap, StatusCode}, + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::extract::{cookie::Cookie, CookieJar}; +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde_json::{json, Value}; +use tracing::{info, instrument, warn}; + +use super::claims::Claims; + +#[instrument(skip(headers, jar))] +pub async fn wrap( + headers: HeaderMap, + jar: CookieJar, + Json(payload): Json, +) -> Result { + match headers.get(CONTENT_TYPE) { + Some(content_type) => match content_type.to_str().unwrap() { + "application/json" => (), + _ => { + return Err((StatusCode::UNSUPPORTED_MEDIA_TYPE, "").into_response()); + } + }, + None => { + return Err(( + StatusCode::UNSUPPORTED_MEDIA_TYPE, + "Content type not provided", + ) + .into_response()); + } + } + + let payload_str = payload.to_string(); + warn!("{}", json!(&payload).to_string()); + info!("payload" = payload_str); + let token = encode( + &Header::default(), + &Claims::new(Some(payload_str)), + &EncodingKey::from_secret("secret".as_ref()), + ) + .map_err(|e| { + eprintln!("Error: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() + })?; + + info!("token" = token); + + let jar = jar.add(Cookie::new("gift", token)); + + Ok(jar) +} diff --git a/src/routes/task_twelve/game/board.rs b/src/routes/task_twelve/game/board.rs index 24cafff..93c44fa 100644 --- a/src/routes/task_twelve/game/board.rs +++ b/src/routes/task_twelve/game/board.rs @@ -42,7 +42,6 @@ impl Board { pub fn random(&self, random: &mut StdRng) { let mut columns: [[Option; 4]; 4] = [[None; 4]; 4]; - // let mut seed = self.seed.lock().unwrap(); for i in (0..4).rev() { (0..4).for_each(|j| { let random = random.gen::(); @@ -54,17 +53,6 @@ impl Board { columns[j][i] = slot; }); } - // for column in &mut columns.iter_mut() { - // for slot in column.iter_mut() { - // let random = random.gen::(); - // if random { - // *slot = Some(Slot::Cookie); - // } else { - // *slot = Some(Slot::Milk); - // } - // } - // } - // drop(seed); { let mut cols = self.columns.lock().unwrap(); diff --git a/src/routes/task_twelve/game/mod.rs b/src/routes/task_twelve/game/mod.rs index da8a858..076a63f 100644 --- a/src/routes/task_twelve/game/mod.rs +++ b/src/routes/task_twelve/game/mod.rs @@ -9,20 +9,3 @@ const EMPTY: &str = "⬛"; const COOKIE: &str = "🍪"; const MILK: &str = "🥛"; const WALL: &str = "⬜"; - -// #[cfg(test)] -// mod tests { -// use super::*; - -// #[test] -// fn test_board() { -// let board = Board::new(); -// println!("{board}"); - -// for _ in 0..4 { -// assert!(board.insert(0, Slot::Milk).is_ok()); -// } - -// println!("{board}"); -// } -// } diff --git a/src/routes/task_twelve/mod.rs b/src/routes/task_twelve/mod.rs index 974d2c9..dcf62a5 100644 --- a/src/routes/task_twelve/mod.rs +++ b/src/routes/task_twelve/mod.rs @@ -36,7 +36,6 @@ pub async fn place( #[allow(clippy::unused_async)] pub async fn random_board(State(board): State) -> impl IntoResponse { - // let mut random = rand::rngs::StdRng::seed_from_u64(2024); let seed = board.get_seed(); let mut random = seed.lock().unwrap(); board.random(&mut random); diff --git a/tests/task_twelve/main.rs b/tests/task_twelve/main.rs index e81b451..12c4c0f 100644 --- a/tests/task_twelve/main.rs +++ b/tests/task_twelve/main.rs @@ -128,10 +128,9 @@ No winner. let response = server.post("/12/place/cookie/3").await; response.assert_status_ok(); - // for i in 4..5 { let response = server.post("/12/place/milk/4").await; response.assert_status_ok(); - // } + let response = server.post("/12/place/cookie/4").await; response.assert_status_ok(); @@ -154,7 +153,7 @@ No winner. ⬜⬜⬜⬜⬜⬜ "; let response = server.get("/12/random-board").await; - // dbg!(response.text()); + response.assert_status_ok(); response.assert_text(want);