challenge: 16

This commit is contained in:
itsscb 2024-12-17 00:35:40 +01:00
parent 81263f7803
commit ff4210d032
12 changed files with 209 additions and 36 deletions

View File

@ -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 = []

View File

@ -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))
}

View File

@ -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;

View File

@ -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<i64>,
#[serde(rename = "sub")]
sub: Option<String>,
}
impl Claims {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
pub fn new(sub: Option<String>) -> Self {
let exp = Some((Utc::now() + Duration::minutes(15)).timestamp());
Self { exp, sub }
}
pub fn sub(&self) -> Option<String> {
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")
),
}
}
}

View File

@ -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<String, Response> {
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::<Value>(&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());
}
}
}

View File

@ -0,0 +1,7 @@
mod claims;
mod decode;
mod unwrap;
mod wrap;
pub use decode::decode;
pub use unwrap::unwrap;
pub use wrap::wrap;

View File

@ -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<String, Response> {
let payload = jar
.get("gift")
.map(|cookie| cookie.value().to_string())
.map(|token| {
let token = jsonwebtoken::decode::<Claims>(
&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()),
}
}

View File

@ -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<Value>,
) -> Result<CookieJar, Response> {
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)
}

View File

@ -42,7 +42,6 @@ impl Board {
pub fn random(&self, random: &mut StdRng) {
let mut columns: [[Option<Slot>; 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::<bool>();
@ -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::<bool>();
// if random {
// *slot = Some(Slot::Cookie);
// } else {
// *slot = Some(Slot::Milk);
// }
// }
// }
// drop(seed);
{
let mut cols = self.columns.lock().unwrap();

View File

@ -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}");
// }
// }

View File

@ -36,7 +36,6 @@ pub async fn place(
#[allow(clippy::unused_async)]
pub async fn random_board(State(board): State<Board>) -> 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);

View File

@ -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);