2024-02-06 15:51:30 -05:00
|
|
|
use axum::{
|
2024-02-13 16:25:03 -05:00
|
|
|
extract::{Host, Json, OriginalUri},
|
2024-02-07 10:12:40 -05:00
|
|
|
http::{header::HeaderMap, Method},
|
2024-02-06 15:51:30 -05:00
|
|
|
Router,
|
|
|
|
};
|
2023-01-13 09:56:06 -05:00
|
|
|
use clap::Parser;
|
2024-02-13 16:25:03 -05:00
|
|
|
use serde::{Deserialize, Serialize};
|
2024-02-07 15:01:15 -05:00
|
|
|
use serde_json::Value;
|
2023-01-13 09:56:06 -05:00
|
|
|
use std::collections::BTreeMap;
|
2024-02-07 15:01:15 -05:00
|
|
|
use tower_http::trace;
|
2024-02-13 16:25:03 -05:00
|
|
|
use tracing::{info, warn, Level};
|
2023-01-13 09:56:06 -05:00
|
|
|
|
|
|
|
#[derive(Parser)]
|
|
|
|
#[command(author, version, about, long_about = None)]
|
|
|
|
struct CliArgs {
|
|
|
|
/// Port to run on.
|
|
|
|
#[arg(short, long, default_value_t = 8080)]
|
|
|
|
port: u16,
|
|
|
|
|
|
|
|
/// Listen to IP mask
|
|
|
|
#[arg(short, long, default_value = "0.0.0.0")]
|
|
|
|
ips: String,
|
|
|
|
}
|
2023-01-12 15:31:03 -05:00
|
|
|
|
2024-02-13 16:25:03 -05:00
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
2023-01-12 15:31:03 -05:00
|
|
|
struct EchoResponse {
|
|
|
|
method: String,
|
|
|
|
path: String,
|
|
|
|
host: String,
|
2023-01-13 09:56:06 -05:00
|
|
|
headers: BTreeMap<String, String>,
|
2024-02-07 15:01:15 -05:00
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
body: Option<Value>,
|
2023-01-12 15:31:03 -05:00
|
|
|
}
|
|
|
|
|
2024-02-07 10:12:40 -05:00
|
|
|
async fn echo_request(
|
|
|
|
method: Method,
|
|
|
|
original_uri: OriginalUri,
|
|
|
|
host: Host,
|
|
|
|
header_map: HeaderMap,
|
2024-02-13 16:25:03 -05:00
|
|
|
body: Option<Json<Value>>,
|
2024-02-07 10:12:40 -05:00
|
|
|
) -> Json<EchoResponse> {
|
|
|
|
let method = method.to_string();
|
|
|
|
let host = host.0;
|
|
|
|
let path = original_uri.path().to_string();
|
|
|
|
let headers = header_map
|
|
|
|
.iter()
|
|
|
|
.map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("").to_string()))
|
|
|
|
.collect();
|
2023-01-12 15:31:03 -05:00
|
|
|
|
2024-02-13 16:25:03 -05:00
|
|
|
let body = match body {
|
2024-02-07 10:12:40 -05:00
|
|
|
None => {
|
2024-02-07 15:01:15 -05:00
|
|
|
warn!("Received a non-JSON body.");
|
|
|
|
None
|
2024-02-13 16:25:03 -05:00
|
|
|
}
|
2024-02-08 14:55:09 -05:00
|
|
|
Some(Json(value)) => {
|
|
|
|
info!("JSON request: {}", value.to_string());
|
2024-02-13 16:25:03 -05:00
|
|
|
Some(value)
|
|
|
|
}
|
2024-02-07 10:12:40 -05:00
|
|
|
};
|
|
|
|
let response = EchoResponse {
|
|
|
|
method,
|
|
|
|
host,
|
|
|
|
path,
|
|
|
|
headers,
|
2024-02-13 16:25:03 -05:00
|
|
|
body,
|
2024-02-07 10:12:40 -05:00
|
|
|
};
|
|
|
|
Json(response)
|
2023-01-12 15:31:03 -05:00
|
|
|
}
|
|
|
|
|
2024-02-13 16:25:03 -05:00
|
|
|
fn app() -> Router {
|
|
|
|
Router::new().fallback(echo_request).layer(
|
|
|
|
trace::TraceLayer::new_for_http()
|
|
|
|
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
|
|
|
|
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-02-06 15:51:30 -05:00
|
|
|
#[tokio::main]
|
|
|
|
async fn main() {
|
2023-01-13 09:56:06 -05:00
|
|
|
let cli_args = CliArgs::parse();
|
|
|
|
let listen_on = (cli_args.ips, cli_args.port);
|
2024-02-06 15:51:30 -05:00
|
|
|
let listen_on = format!("{ip}:{port}", ip = listen_on.0, port = listen_on.1);
|
|
|
|
|
2024-02-07 15:01:15 -05:00
|
|
|
// From https://stackoverflow.com/questions/75009289/how-to-enable-logging-tracing-with-axum
|
2024-02-13 16:25:03 -05:00
|
|
|
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
|
2024-02-07 15:01:15 -05:00
|
|
|
|
|
|
|
info!("Starting the mirror-server to listen to {}", listen_on);
|
2024-02-06 15:51:30 -05:00
|
|
|
let listener = tokio::net::TcpListener::bind(&listen_on)
|
|
|
|
.await
|
2024-02-07 15:01:15 -05:00
|
|
|
.unwrap_or_else(|_| panic!("Failed to binding to {}", listen_on));
|
2024-02-13 16:25:03 -05:00
|
|
|
axum::serve(listener, app())
|
2023-01-12 15:31:03 -05:00
|
|
|
.await
|
2024-02-06 15:51:30 -05:00
|
|
|
.expect("Server should start");
|
2023-01-12 09:41:47 -05:00
|
|
|
}
|
2024-02-13 16:25:03 -05:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use axum::http::{HeaderName, HeaderValue, Method, StatusCode};
|
|
|
|
use axum_test::TestServer;
|
|
|
|
use rstest::rstest;
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn handles_simple_get_request() {
|
|
|
|
let expected_headers: BTreeMap<String, String> = BTreeMap::new();
|
|
|
|
let expected_response = EchoResponse {
|
|
|
|
method: "GET".to_string(),
|
|
|
|
path: "/".to_string(),
|
|
|
|
host: "localhost".to_string(),
|
|
|
|
body: None,
|
|
|
|
headers: expected_headers,
|
|
|
|
};
|
|
|
|
let server = TestServer::new(app()).unwrap();
|
|
|
|
let response = server.get("/").await;
|
|
|
|
response.assert_status(StatusCode::OK);
|
|
|
|
response.assert_json::<EchoResponse>(&expected_response);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[rstest]
|
|
|
|
#[case::single_level("/test")]
|
|
|
|
#[case::multiple_level("/test/multiple/levels/")]
|
|
|
|
#[case::slash_ending("/test/")]
|
|
|
|
#[tokio::test]
|
|
|
|
async fn handles_different_urls(#[case] url: String) {
|
|
|
|
let expected_headers: BTreeMap<String, String> = BTreeMap::new();
|
|
|
|
let expected_response = EchoResponse {
|
|
|
|
method: "GET".to_string(),
|
|
|
|
path: url.clone(),
|
|
|
|
host: "localhost".to_string(),
|
|
|
|
body: None,
|
|
|
|
headers: expected_headers,
|
|
|
|
};
|
|
|
|
let server = TestServer::new(app()).unwrap();
|
|
|
|
let response = server.get(&url).await;
|
|
|
|
response.assert_status(StatusCode::OK);
|
|
|
|
response.assert_json::<EchoResponse>(&expected_response);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[rstest]
|
|
|
|
#[case::post(Method::POST)]
|
|
|
|
#[case::put(Method::PUT)]
|
|
|
|
#[case::patch(Method::PATCH)]
|
|
|
|
#[case::delete(Method::DELETE)]
|
|
|
|
#[tokio::test]
|
|
|
|
async fn handles_different_http_methods(#[case] http_method: Method) {
|
|
|
|
let expected_headers: BTreeMap<String, String> = BTreeMap::new();
|
|
|
|
let expected_response = EchoResponse {
|
|
|
|
method: http_method.to_string(),
|
|
|
|
path: "/testing".to_string(),
|
|
|
|
host: "localhost".to_string(),
|
|
|
|
body: None,
|
|
|
|
headers: expected_headers,
|
|
|
|
};
|
|
|
|
let server = TestServer::new(app()).unwrap();
|
|
|
|
let response = server.method(http_method, "/testing").await;
|
|
|
|
response.assert_status(StatusCode::OK);
|
|
|
|
response.assert_json::<EchoResponse>(&expected_response);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn handle_non_json_request() {
|
|
|
|
let mut expected_headers: BTreeMap<String, String> = BTreeMap::new();
|
|
|
|
expected_headers.insert("content-type".to_string(), "text/plain".to_string());
|
|
|
|
let expected_response = EchoResponse {
|
|
|
|
method: "POST".to_string(),
|
|
|
|
path: "/".to_string(),
|
|
|
|
host: "localhost".to_string(),
|
|
|
|
body: None,
|
|
|
|
headers: expected_headers,
|
|
|
|
};
|
|
|
|
let server = TestServer::new(app()).unwrap();
|
|
|
|
let response = server.post("/").text("hello world").await;
|
|
|
|
response.assert_status(StatusCode::OK);
|
|
|
|
response.assert_json::<EchoResponse>(&expected_response);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn handle_json_request() {
|
|
|
|
let test_json = serde_json::json!({"hello": "world"});
|
|
|
|
let mut expected_headers: BTreeMap<String, String> = BTreeMap::new();
|
|
|
|
expected_headers.insert("content-type".to_string(), "application/json".to_string());
|
|
|
|
let expected_response = EchoResponse {
|
|
|
|
method: "POST".to_string(),
|
|
|
|
path: "/".to_string(),
|
|
|
|
host: "localhost".to_string(),
|
|
|
|
body: Some(test_json.clone()),
|
|
|
|
headers: expected_headers,
|
|
|
|
};
|
|
|
|
let server = TestServer::new(app()).unwrap();
|
|
|
|
let response = server.post("/").json(&test_json).await;
|
|
|
|
response.assert_status(StatusCode::OK);
|
|
|
|
response.assert_json::<EchoResponse>(&expected_response);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn handle_extra_headers() {
|
|
|
|
let mut expected_headers: BTreeMap<String, String> = BTreeMap::new();
|
|
|
|
expected_headers.insert("x-test-message".to_string(), "Howdy!".to_string());
|
|
|
|
let expected_response = EchoResponse {
|
|
|
|
method: "GET".to_string(),
|
|
|
|
path: "/".to_string(),
|
|
|
|
host: "localhost".to_string(),
|
|
|
|
body: None,
|
|
|
|
headers: expected_headers,
|
|
|
|
};
|
|
|
|
let server = TestServer::new(app()).unwrap();
|
|
|
|
let response = server
|
|
|
|
.get("/")
|
|
|
|
.add_header(
|
|
|
|
HeaderName::from_static("x-test-message"),
|
|
|
|
HeaderValue::from_static("Howdy!"),
|
|
|
|
)
|
|
|
|
.await;
|
|
|
|
response.assert_status(StatusCode::OK);
|
|
|
|
response.assert_json::<EchoResponse>(&expected_response);
|
|
|
|
}
|
|
|
|
}
|