Compare commits
No commits in common. "6775667f807aeb0b1247f5f406d4e7aec163f7ca" and "e627581f6673fb8ceefc7c3318c1164e86d1efc8" have entirely different histories.
6775667f80
...
e627581f66
|
@ -1,4 +0,0 @@
|
||||||
target/
|
|
||||||
Cargo.lock
|
|
||||||
**/*.rs.bk
|
|
||||||
.idea
|
|
59
.drone.yml
59
.drone.yml
|
@ -1,59 +0,0 @@
|
||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: mirror-server
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: create-build-image
|
|
||||||
image: plugins/docker
|
|
||||||
settings:
|
|
||||||
username:
|
|
||||||
from_secret: docker-username
|
|
||||||
password:
|
|
||||||
from_secret: docker-password
|
|
||||||
registry: code.birch-tree.net
|
|
||||||
repo: code.birch-tree.net/dorian/mirror-server
|
|
||||||
target: BUILD
|
|
||||||
tags:
|
|
||||||
- build
|
|
||||||
cache_from:
|
|
||||||
- code.birch-tree.net/dorian/mirror-server:build
|
|
||||||
|
|
||||||
- name: test
|
|
||||||
image: code.birch-tree.net/dorian/mirror-server:build
|
|
||||||
commands:
|
|
||||||
- cargo test
|
|
||||||
depends_on:
|
|
||||||
- create-build-image
|
|
||||||
|
|
||||||
- name: create-release-image
|
|
||||||
image: plugins/docker
|
|
||||||
settings:
|
|
||||||
username:
|
|
||||||
from_secret: docker-username
|
|
||||||
password:
|
|
||||||
from_secret: docker-password
|
|
||||||
registry: code.birch-tree.net
|
|
||||||
repo: code.birch-tree.net/dorian/mirror-server
|
|
||||||
tags:
|
|
||||||
- 0.3.0
|
|
||||||
- latest
|
|
||||||
cache_from:
|
|
||||||
- code.birch-tree.net/dorian/mirror-server:latest
|
|
||||||
- code.birch-tree.net/dorian/mirror-server:build
|
|
||||||
depends_on:
|
|
||||||
- test
|
|
||||||
|
|
||||||
- name: create-debian-package
|
|
||||||
image: code.birch-tree.net/dorian/mirror-server:build
|
|
||||||
commands:
|
|
||||||
- ./publish-deb.sh
|
|
||||||
environment:
|
|
||||||
USERNAME: dorian
|
|
||||||
PASSWORD:
|
|
||||||
from_secret: gitea-release-password
|
|
||||||
depends_on:
|
|
||||||
- test
|
|
||||||
|
|
||||||
image_pull_secrets:
|
|
||||||
- docker-config
|
|
|
@ -25,7 +25,3 @@ tower = "0.4"
|
||||||
tower-http = { version = "0.5", features = ["trace"] }
|
tower-http = { version = "0.5", features = ["trace"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
axum-test = "14.0"
|
|
||||||
rstest = "0.18"
|
|
||||||
|
|
38
Dockerfile
38
Dockerfile
|
@ -1,38 +0,0 @@
|
||||||
FROM rust:1.76 AS BUILD
|
|
||||||
|
|
||||||
ENV APP_NAME=mirror-server
|
|
||||||
ENV APP_HOME=/srv/${APP_NAME}
|
|
||||||
RUN apt update \
|
|
||||||
&& apt install -y curl \
|
|
||||||
&& cargo install cargo-deb
|
|
||||||
|
|
||||||
# Setup working env.
|
|
||||||
RUN mkdir -p ${APP_HOME}
|
|
||||||
WORKDIR ${APP_HOME}
|
|
||||||
|
|
||||||
# Create build target cache.
|
|
||||||
RUN USER=root cargo init --bin .
|
|
||||||
COPY ["Cargo.toml", "./"]
|
|
||||||
RUN cargo build \
|
|
||||||
&& cargo build --tests \
|
|
||||||
&& cargo build --release \
|
|
||||||
&& rm src/*.rs
|
|
||||||
|
|
||||||
# Bring in the source
|
|
||||||
COPY ["src", "./src/"]
|
|
||||||
|
|
||||||
# Build the example API.
|
|
||||||
RUN cargo build --release
|
|
||||||
|
|
||||||
# Create the minimal server image.
|
|
||||||
FROM debian:buster-slim AS SERVER
|
|
||||||
|
|
||||||
ENV APP_NAME=mirror-server
|
|
||||||
ENV APP_HOME=/srv/${APP_NAME}
|
|
||||||
RUN apt update
|
|
||||||
|
|
||||||
RUN mkdir -p ${APP_HOME}
|
|
||||||
WORKDIR ${APP_HOME}
|
|
||||||
|
|
||||||
COPY --from=BUILD ${APP_HOME}/target/release/${APP_NAME} /usr/local/bin/
|
|
||||||
CMD ${APP_NAME}
|
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
A simple server for mirroring HTTP requests for testing.
|
A simple server for mirroring HTTP requests for testing.
|
||||||
|
|
||||||
[![Build Status](https://ci.birch-tree.net/api/badges/dorian/mirror-server/status.svg)](https://ci.birch-tree.net/dorian/mirror-server)
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
* Use the latest stable version of Rust using rustup.
|
* Use the latest stable version of Rust using rustup.
|
||||||
|
@ -31,7 +29,5 @@ sudo dpkg -i "mirror-server-${VERSION}_amd64.deb"
|
||||||
* [x] Migrate actix to axum for easier maintainability.
|
* [x] Migrate actix to axum for easier maintainability.
|
||||||
* [x] Add mirroring of JSON request.
|
* [x] Add mirroring of JSON request.
|
||||||
* [x] Add logging to server.
|
* [x] Add logging to server.
|
||||||
* [x] Create Docker image for mirror-server for easier distribution.
|
* [ ] Add convenience path to access logging for a certain date / records.
|
||||||
* [x] Add publishing of DEB and Docker image to DroneCI.
|
* [ ] Dockerize mirror-server for easier distribution.
|
||||||
* [ ] Add documentation to the API.
|
|
||||||
* [ ] Publish crate on <https://crates.io/>.
|
|
||||||
|
|
|
@ -1,28 +1,22 @@
|
||||||
#!/usr/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
if [[ -z "${USERNAME}" ]]; then
|
read -r -p "Username: " USERNAME
|
||||||
echo "Set the USERNAME for Gitea"
|
read -r -s -p "Password: " PASSWORD
|
||||||
|
|
||||||
|
_pkg_name=mirror-server
|
||||||
|
_pkg_version=0.2.0
|
||||||
|
_deb_file="${_pkg_name}_${_pkg_version}_amd64.deb"
|
||||||
|
_deb_path="target/debian"
|
||||||
|
_gitea_server="code.birch-tree.net"
|
||||||
|
|
||||||
|
if [[ ! -f "${_deb_path}/${_deb_file}" ]];
|
||||||
|
then
|
||||||
|
echo "Run cargo deb first!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "${PASSWORD}" ]]; then
|
|
||||||
echo "Set the PASSWORD for Gitea"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo 'Create the Debian package...'
|
|
||||||
|
|
||||||
pkg_full_path=$(cargo deb)
|
|
||||||
pkg_filename=$(basename "${pkg_full_path}")
|
|
||||||
pkg_name=$(echo "${pkg_filename}" | awk -F _ '{print $1}')
|
|
||||||
pkg_version=$(echo "${pkg_filename}" | awk -F _ '{print $2}')
|
|
||||||
|
|
||||||
gitea_server="code.birch-tree.net"
|
|
||||||
|
|
||||||
curl --user "${USERNAME}:${PASSWORD}" \
|
curl --user "${USERNAME}:${PASSWORD}" \
|
||||||
--upload-file "${pkg_full_path}" \
|
--upload-file "${_deb_path}/${_deb_file}" \
|
||||||
--silent \
|
|
||||||
-X PUT \
|
-X PUT \
|
||||||
"https://${gitea_server}/api/packages/${USERNAME}/generic/${pkg_name}/${pkg_version}/${pkg_filename}"
|
"https://${_gitea_server}/api/packages/${USERNAME}/generic/${_pkg_name}/${_pkg_version}/${_deb_file}"
|
||||||
|
|
||||||
echo "Published ${pkg_name} v${pkg_version} ===> ${pkg_filename}"
|
|
||||||
|
|
167
src/main.rs
167
src/main.rs
|
@ -1,14 +1,14 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Host, Json, OriginalUri},
|
extract::{Host, OriginalUri, Json},
|
||||||
http::{header::HeaderMap, Method},
|
http::{header::HeaderMap, Method},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use tower_http::trace;
|
use tower_http::trace;
|
||||||
use tracing::{info, warn, Level};
|
use tracing::{Level, info, warn};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
|
@ -22,7 +22,7 @@ struct CliArgs {
|
||||||
ips: String,
|
ips: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Serialize)]
|
||||||
struct EchoResponse {
|
struct EchoResponse {
|
||||||
method: String,
|
method: String,
|
||||||
path: String,
|
path: String,
|
||||||
|
@ -35,46 +35,35 @@ struct EchoResponse {
|
||||||
async fn echo_request(
|
async fn echo_request(
|
||||||
method: Method,
|
method: Method,
|
||||||
original_uri: OriginalUri,
|
original_uri: OriginalUri,
|
||||||
Host(host): Host,
|
host: Host,
|
||||||
header_map: HeaderMap,
|
header_map: HeaderMap,
|
||||||
body: Option<Json<Value>>,
|
body: Option<Json<Value>>
|
||||||
) -> Json<EchoResponse> {
|
) -> Json<EchoResponse> {
|
||||||
let method = method.to_string();
|
let method = method.to_string();
|
||||||
let host = host;
|
let host = host.0;
|
||||||
let path = original_uri.path().to_string();
|
let path = original_uri.path().to_string();
|
||||||
let headers = header_map
|
let headers = header_map
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("").to_string()))
|
.map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("").to_string()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let body = match body {
|
let json_body = match body {
|
||||||
None => {
|
None => {
|
||||||
warn!("Received a non-JSON body.");
|
warn!("Received a non-JSON body.");
|
||||||
None
|
None
|
||||||
}
|
},
|
||||||
Some(Json(value)) => {
|
Some(Json(value)) => Some(value),
|
||||||
info!("JSON request: {}", value.to_string());
|
|
||||||
Some(value)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let response = EchoResponse {
|
let response = EchoResponse {
|
||||||
method,
|
method,
|
||||||
host,
|
host,
|
||||||
path,
|
path,
|
||||||
headers,
|
headers,
|
||||||
body,
|
body: json_body,
|
||||||
};
|
};
|
||||||
Json(response)
|
Json(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let cli_args = CliArgs::parse();
|
let cli_args = CliArgs::parse();
|
||||||
|
@ -82,136 +71,20 @@ async fn main() {
|
||||||
let listen_on = format!("{ip}:{port}", ip = listen_on.0, port = listen_on.1);
|
let listen_on = format!("{ip}:{port}", ip = listen_on.0, port = listen_on.1);
|
||||||
|
|
||||||
// From https://stackoverflow.com/questions/75009289/how-to-enable-logging-tracing-with-axum
|
// From https://stackoverflow.com/questions/75009289/how-to-enable-logging-tracing-with-axum
|
||||||
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(Level::INFO)
|
||||||
|
.init();
|
||||||
|
let app = 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)),
|
||||||
|
);
|
||||||
|
|
||||||
info!("Starting the mirror-server to listen to {}", listen_on);
|
info!("Starting the mirror-server to listen to {}", listen_on);
|
||||||
let listener = tokio::net::TcpListener::bind(&listen_on)
|
let listener = tokio::net::TcpListener::bind(&listen_on)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|_| panic!("Failed to binding to {}", listen_on));
|
.unwrap_or_else(|_| panic!("Failed to binding to {}", listen_on));
|
||||||
axum::serve(listener, app())
|
axum::serve(listener, app)
|
||||||
.await
|
.await
|
||||||
.expect("Server should start");
|
.expect("Server should start");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue