diff --git a/Cargo.toml b/Cargo.toml index cf633a0..13beb15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = "0.7.4" +axum = { version = "0.7.4", features = ["query"] } +serde = { version = "1.0.215", features = ["derive"] } shuttle-axum = "0.49.0" shuttle-runtime = "0.49.0" tokio = "1.28.2" diff --git a/src/lib.rs b/src/lib.rs index 3a68250..d21b250 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ mod routes; pub use routes::hello_world; -use routes::{hello_bird, minus_one}; +use routes::{hello_bird, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, minus_one}; pub fn router() -> axum::Router { use axum::routing::get; @@ -10,6 +10,10 @@ pub fn router() -> axum::Router { Router::new() .route("/hello_world", get(hello_world)) .route("/-1/seek", get(minus_one)) + .route("/2/dest", get(ipv4_dest)) + .route("/2/key", get(ipv4_key)) + .route("/2/v6/dest", get(ipv6_dest)) + .route("/2/v6/key", get(ipv6_key)) .route("/", get(hello_bird)) } @@ -49,4 +53,82 @@ mod test { response.assert_header("location", "https://www.youtube.com/watch?v=9Gc4QTqslN4"); response.assert_status(StatusCode::FOUND); } + + #[tokio::test] + async fn test_ipv4_dest() { + let server = test_server(); + + let response = server.get("/2/dest?from=10.0.0.0&key=1.2.3.255").await; + response.assert_status_ok(); + response.assert_text("11.2.3.255"); + + let response = server.get("/2/dest?from=invalid-from&key=1.2.3.255").await; + response.assert_status_bad_request(); + + let response = server.get("/2/dest?from=10.0.0.0&key=invalid-key").await; + response.assert_status_bad_request(); + + let response = server + .get("/2/dest?from=128.128.33.0&key=255.0.255.33") + .await; + response.assert_status_ok(); + response.assert_text("127.128.32.33"); + } + + #[tokio::test] + async fn test_ipv4_key() { + let server = test_server(); + + let response = server.get("/2/key?from=10.0.0.0&to=11.2.3.255").await; + response.assert_status_ok(); + response.assert_text("1.2.3.255"); + + let response = server.get("/2/key?from=invalid-from&to=1.2.3.255").await; + response.assert_status_bad_request(); + + let response = server.get("/2/key?from=10.0.0.0&to=invalid-to").await; + response.assert_status_bad_request(); + + let response = server + .get("/2/key?from=128.128.33.0&to=127.128.32.33") + .await; + response.assert_status_ok(); + response.assert_text("255.0.255.33"); + } + + #[tokio::test] + async fn test_ipv6_dest() { + let server = test_server(); + + let response = server.get("/2/v6/dest?from=fe80::1&key=5:6:7::3333").await; + response.assert_status_ok(); + response.assert_text("fe85:6:7::3332"); + + let response = server + .get("/2/v6/dest?from=invalid-from&key=5:6:7::3333") + .await; + response.assert_status_bad_request(); + + let response = server.get("/2/v6/dest?from=fe80::1&key=invalid-key").await; + response.assert_status_bad_request(); + } + + #[tokio::test] + async fn test_ipv6_key() { + let server = test_server(); + + let response = server + .get("/2/v6/key?from=aaaa::aaaa&to=5555:ffff:c:0:0:c:1234:5555") + .await; + response.assert_status_ok(); + response.assert_text("ffff:ffff:c::c:1234:ffff"); + + let response = server + .get("/2/v6/dest?from=invalid-from&to=5:6:7::3333") + .await; + response.assert_status_bad_request(); + + let response = server.get("/2/v6/dest?from=fe80::1&to=invalid-to").await; + response.assert_status_bad_request(); + } } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 86a21fc..0d095b7 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,7 +1,12 @@ mod hello_bird; mod hello_world; mod minus_one; +mod task_two; pub use hello_bird::hello_bird; pub use hello_world::hello_world; pub use minus_one::minus_one; +pub use task_two::ipv4_dest; +pub use task_two::ipv4_key; +pub use task_two::ipv6_dest; +pub use task_two::ipv6_key; diff --git a/src/routes/task_two/ipv4_dest.rs b/src/routes/task_two/ipv4_dest.rs new file mode 100644 index 0000000..9ef40b4 --- /dev/null +++ b/src/routes/task_two/ipv4_dest.rs @@ -0,0 +1,37 @@ +#![allow(dead_code, clippy::unused_async)] + +use std::{net::Ipv4Addr, str::FromStr}; + +use axum::{ + extract::Query, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use super::DestParams; + +pub async fn ipv4_dest(params: Query) -> Result { + let params: DestParams = params.0; + let Ok(from) = Ipv4Addr::from_str(¶ms.from) else { + return Err((StatusCode::BAD_REQUEST, "Invalid from IP address").into_response()); + }; + let Ok(key) = Ipv4Addr::from_str(¶ms.key) else { + return Err((StatusCode::BAD_REQUEST, "Invalid key IP address").into_response()); + }; + + Ok(calculate_ipv4_dest(from, key)) +} + +pub fn calculate_ipv4_dest(from: Ipv4Addr, key: Ipv4Addr) -> String { + let result: Vec = from + .octets() + .iter() + .zip(key.octets().iter()) + .map(|(a, b)| a.overflowing_add(*b).0) + .collect(); + result + .iter() + .map(std::string::ToString::to_string) + .collect::>() + .join(".") +} diff --git a/src/routes/task_two/ipv4_key.rs b/src/routes/task_two/ipv4_key.rs new file mode 100644 index 0000000..1bdda19 --- /dev/null +++ b/src/routes/task_two/ipv4_key.rs @@ -0,0 +1,36 @@ +#![allow(dead_code, clippy::unused_async)] + +use std::{net::Ipv4Addr, str::FromStr}; + +use axum::{ + extract::Query, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use super::KeyParams; + +pub async fn ipv4_key(params: Query) -> Result { + let params: KeyParams = params.0; + let Ok(from) = Ipv4Addr::from_str(¶ms.from) else { + return Err((StatusCode::BAD_REQUEST, "Invalid from IP address").into_response()); + }; + let Ok(to) = Ipv4Addr::from_str(¶ms.to) else { + return Err((StatusCode::BAD_REQUEST, "Invalid key IP address").into_response()); + }; + Ok(calculate_ipv4_key(from, to)) +} + +pub fn calculate_ipv4_key(from: Ipv4Addr, to: Ipv4Addr) -> String { + let result: Vec = to + .octets() + .iter() + .zip(from.octets().iter()) + .map(|(a, b)| a.overflowing_sub(*b).0) + .collect(); + result + .iter() + .map(std::string::ToString::to_string) + .collect::>() + .join(".") +} diff --git a/src/routes/task_two/ipv6_dest.rs b/src/routes/task_two/ipv6_dest.rs new file mode 100644 index 0000000..8db3290 --- /dev/null +++ b/src/routes/task_two/ipv6_dest.rs @@ -0,0 +1,37 @@ +#![allow(dead_code, clippy::unused_async)] + +use std::{net::Ipv6Addr, str::FromStr}; + +use axum::{ + extract::Query, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use super::DestParams; + +pub async fn ipv6_dest(params: Query) -> Result { + let params: DestParams = params.0; + let Ok(from) = Ipv6Addr::from_str(¶ms.from) else { + return Err((StatusCode::BAD_REQUEST, "Invalid from IP address").into_response()); + }; + let Ok(key) = Ipv6Addr::from_str(¶ms.key) else { + return Err((StatusCode::BAD_REQUEST, "Invalid key IP address").into_response()); + }; + + Ok(calculate_ipv6_dest(from, key)) +} + +pub fn calculate_ipv6_dest(from: Ipv6Addr, key: Ipv6Addr) -> String { + let result: Vec = from + .segments() + .iter() + .zip(key.segments().iter()) + .map(|(a, b)| a ^ b) + .map(|s| format!("{s:x}")) + .collect(); + + let ip: Ipv6Addr = result.join(":").parse().unwrap(); + + ip.to_string() +} diff --git a/src/routes/task_two/ipv6_key.rs b/src/routes/task_two/ipv6_key.rs new file mode 100644 index 0000000..e92f7f9 --- /dev/null +++ b/src/routes/task_two/ipv6_key.rs @@ -0,0 +1,37 @@ +#![allow(dead_code, clippy::unused_async)] + +use std::{net::Ipv6Addr, str::FromStr}; + +use axum::{ + extract::Query, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use super::KeyParams; + +pub async fn ipv6_key(params: Query) -> Result { + let params: KeyParams = params.0; + let Ok(from) = Ipv6Addr::from_str(¶ms.from) else { + return Err((StatusCode::BAD_REQUEST, "Invalid from IP address").into_response()); + }; + let Ok(to) = Ipv6Addr::from_str(¶ms.to) else { + return Err((StatusCode::BAD_REQUEST, "Invalid key IP address").into_response()); + }; + + Ok(calculate_ipv6_key(from, to)) +} + +pub fn calculate_ipv6_key(from: Ipv6Addr, to: Ipv6Addr) -> String { + let result: Vec = to + .segments() + .iter() + .zip(from.segments().iter()) + .map(|(a, b)| a ^ b) + .map(|s| format!("{s:x}")) + .collect(); + + let ip: Ipv6Addr = result.join(":").parse().unwrap(); + + ip.to_string() +} diff --git a/src/routes/task_two/mod.rs b/src/routes/task_two/mod.rs new file mode 100644 index 0000000..797eff8 --- /dev/null +++ b/src/routes/task_two/mod.rs @@ -0,0 +1,72 @@ +#![allow(dead_code, clippy::unused_async)] + +mod ipv4_dest; +mod ipv4_key; +mod ipv6_dest; +mod ipv6_key; + +pub use ipv4_dest::ipv4_dest; +pub use ipv4_key::ipv4_key; +pub use ipv6_dest::ipv6_dest; +pub use ipv6_key::ipv6_key; + +#[derive(serde::Deserialize)] +pub struct DestParams { + from: String, + key: String, +} + +#[derive(serde::Deserialize)] +pub struct KeyParams { + from: String, + to: String, +} + +#[cfg(test)] +mod test { + use std::net::{Ipv4Addr, Ipv6Addr}; + use std::str::FromStr; + + use ipv4_dest::calculate_ipv4_dest; + use ipv4_key::calculate_ipv4_key; + use ipv6_dest::calculate_ipv6_dest; + use ipv6_key::calculate_ipv6_key; + + use super::*; + + #[test] + fn test_calculate_ipv4_dest() { + let from = Ipv4Addr::from_str("10.0.0.0").unwrap(); + let key = Ipv4Addr::from_str("1.2.3.255").unwrap(); + assert_eq!(calculate_ipv4_dest(from, key), "11.2.3.255"); + + let from = Ipv4Addr::from_str("128.128.33.0").unwrap(); + let key = Ipv4Addr::from_str("255.0.255.33").unwrap(); + assert_eq!(calculate_ipv4_dest(from, key), "127.128.32.33"); + } + + #[test] + fn test_calculate_ipv4_key() { + let from = Ipv4Addr::from_str("10.0.0.0").unwrap(); + let to = Ipv4Addr::from_str("11.2.3.255").unwrap(); + assert_eq!(calculate_ipv4_key(from, to), "1.2.3.255"); + + let from = Ipv4Addr::from_str("128.128.33.0").unwrap(); + let to = Ipv4Addr::from_str("127.128.32.33").unwrap(); + assert_eq!(calculate_ipv4_key(from, to), "255.0.255.33"); + } + + #[test] + fn test_calculate_ipv6_dest() { + let from = Ipv6Addr::from_str("fe80::1").unwrap(); + let key = Ipv6Addr::from_str("5:6:7::3333").unwrap(); + assert_eq!(calculate_ipv6_dest(from, key), "fe85:6:7::3332"); + } + + #[test] + fn test_calculate_ipv6_key() { + let from = Ipv6Addr::from_str("aaaa::aaaa").unwrap(); + let to = Ipv6Addr::from_str("5555:ffff:c:0:0:c:1234:5555").unwrap(); + assert_eq!(calculate_ipv6_key(from, to), "ffff:ffff:c::c:1234:ffff"); + } +}