9 Commits

Author SHA1 Message Date
danielvici123
9ced8d11c6 docker: Add OCI source label to Dockerfile
All checks were successful
Release Docker Image / Push Docker image to Registry (push) Successful in 1m40s
Build and Release / Build All Platforms (push) Successful in 1m57s
2026-06-09 21:23:12 +02:00
danielvici123
c2456a7ac0 Update .gitea/workflows/build-image.yml
All checks were successful
Release Docker Image / Push Docker image to Registry (push) Successful in 1m50s
Build and Release / Build All Platforms (push) Successful in 2m2s
2026-06-09 19:16:41 +00:00
danielvici123
c68be2a9f5 ci: Remove Docker build caching 2026-06-09 21:11:26 +02:00
danielvici123
13afb2292e chore: Bump version to 0.6.0
Some checks failed
Build and Release / Build All Platforms (push) Successful in 2m1s
Release Docker Image / Push Docker image to Registry (push) Failing after 8s
2026-06-09 20:17:56 +02:00
danielvici123
a4182e9cfe docs: Add status.json example response 2026-06-09 20:13:47 +02:00
danielvici123
4063c770e0 ci: Add Gitea workflow for Docker image release 2026-06-09 20:13:47 +02:00
danielvici123
0103d2939d feat: Add Dockerization support 2026-06-09 20:13:47 +02:00
danielvici123
520fd4f1e6 feat: Add colorized request logging 2026-06-09 20:13:47 +02:00
danielvici123
36a3b96c12 ci: use static linking for windows binary 2026-06-08 22:01:36 +02:00
12 changed files with 269 additions and 29 deletions

31
.dockerignore Normal file
View File

@@ -0,0 +1,31 @@
# Rust
target/
**/*.rs.bk
# Git
.git
.gitignore
# Docker
Dockerfile
.dockerignore
# Config and local data
*.json
!config.json.example
!response.json.example
status.json
# IDEs and Editors
.vscode/
.idea/
*.swp
*.swo
# CI/CD
.gitea/
.github/
# OS files
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,52 @@
name: Release Docker Image
on:
push:
tags:
- 'v*.*.*'
jobs:
push_to_registries:
name: Push Docker image to Registry
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Registry
uses: docker/login-action@v3
with:
registry: git.danielvici.com
username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Extract Docker tags
id: meta
run: |
version_tag="${{github.ref_name}}"
CLEAN_TAG=$(echo $version_tag | sed 's/^v//')
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
REPO=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]')
TAGS="git.danielvici.com/${OWNER}/${REPO}:${CLEAN_TAG}"
if [[ "$version_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TAGS="${TAGS},git.danielvici.com/${OWNER}/${REPO}:latest"
TAGS="${TAGS},git.danielvici.com/${OWNER}/${REPO}:$(echo $CLEAN_TAG | cut -d. -f1)"
fi
echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
- name: Build and push Docker images
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}

View File

@@ -34,6 +34,7 @@ jobs:
run: cargo build --release --target x86_64-pc-windows-gnu run: cargo build --release --target x86_64-pc-windows-gnu
env: env:
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
RUSTFLAGS: -C target-feature=+crt-static -C link-args=-static
- name: Package Release Assets - name: Package Release Assets
run: | run: |

17
Cargo.lock generated
View File

@@ -200,6 +200,16 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "colored"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@@ -213,6 +223,7 @@ dependencies = [
"axum", "axum",
"axum-server", "axum-server",
"chrono", "chrono",
"colored",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -508,6 +519,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.186"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dhl" name = "dhl"
version = "0.5.0" version = "0.6.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@@ -11,4 +11,5 @@ serde_json = "1.0"
axum-server = { version = "0.7", features = ["tls-rustls"] } axum-server = { version = "0.7", features = ["tls-rustls"] }
tower-http = { version = "0.5", features = ["trace"] } tower-http = { version = "0.5", features = ["trace"] }
chrono = "0.4" chrono = "0.4"
colored = "2.1"

46
Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
# Build stage
FROM rust:alpine AS builder
# Install build dependencies
RUN apk add --no-cache musl-dev
WORKDIR /usr/src/dhl
# 1. Copy only the dependency manifests
COPY Cargo.toml Cargo.lock ./
# 2. Create a dummy source file to build dependencies
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
cargo build --release && \
rm -rf src
# 3. Now copy the real source code
COPY src ./src
# 4. Build the actual application
# We touch the main file to ensure cargo rebuilds it
RUN touch src/main.rs && cargo build --release
# Final stage
FROM alpine:latest
LABEL org.opencontainers.image.source="https://git.danielvici.com/danielvici123/dhl"
# Set environment variable to signal the app it's running in Docker
ENV DOCKER_CONTAINER=true
# Install runtime dependencies
RUN apk add --no-cache ca-certificates
WORKDIR /app
# Copy the binary from the builder stage
COPY --from=builder /usr/src/dhl/target/release/dhl /usr/local/bin/dhl
# Volume for configuration and response files
VOLUME ["/app"]
# Default port
EXPOSE 3000
CMD ["dhl"]

View File

@@ -7,7 +7,10 @@ DHL is a lightweight, configurable HTTP/HTTPS mock server built with Rust and Ax
- **Live Configuration Reloading**: Changes to `config.json` (routes, status codes, etc.) and response files are applied immediately without restarting the server. - **Live Configuration Reloading**: Changes to `config.json` (routes, status codes, etc.) and response files are applied immediately without restarting the server.
- **Multi-Route Support**: Define as many paths as you need. - **Multi-Route Support**: Define as many paths as you need.
- **JSON Response Files**: Responses are loaded from external JSON files. - **JSON Response Files**: Responses are loaded from external JSON files.
- **Request Logging**: Detailed logging of incoming requests, including methods, paths, and headers. - **Colorized Request Logging**: Detailed logging of incoming requests with terminal colors. Status codes are color-coded for quick identification:
- <span style="color:green">**2xx Success**</span>: Green
- <span style="color:yellow">**4xx Client Error**</span>: Yellow
- <span style="color:red">**5xx Server Error**</span>: Red
- **Header Masking**: Sensitive headers like `Authorization` can be masked in logs. - **Header Masking**: Sensitive headers like `Authorization` can be masked in logs.
- **HTTPS/TLS Support**: Optional TLS support for secure connections. - **HTTPS/TLS Support**: Optional TLS support for secure connections.
- **Automatic Default Generation**: Creates default configuration and response files if they are missing. - **Automatic Default Generation**: Creates default configuration and response files if they are missing.
@@ -96,6 +99,52 @@ cargo run
cargo build --release cargo build --release
``` ```
### Using with Docker
You can run DHL as a Docker container. The application automatically generates a default `config.json` and `response.json` if they are missing in the work directory.
#### Build the Image
```bash
docker build -t dhl .
```
#### Run with Docker CLI
To persist your configuration and response files, mount a local directory to `/app` in the container. The application will create default files in that directory if it is empty.
```bash
# Create a folder for your mock data
mkdir mock-data
# Run the container and mount the folder
docker run -p 3000:3000 -v $(pwd)/mock-data:/app dhl
```
#### Run with Docker Compose
An example `docker-compose.yml` is provided in the root directory:
```yaml
version: '3.8'
services:
dhl:
build: .
ports:
- "3000:3000"
volumes:
- ./mock-data:/app
restart: unless-stopped
```
To start:
```bash
docker-compose up -d
```
**Note:** The default host is set to `0.0.0.0` for Docker compatibility. If you are using a custom `config.json`, ensure the `host` is set to `0.0.0.0`.
### Release Assets ### Release Assets
When downloading a release, you will find: When downloading a release, you will find:

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
services:
dhl:
build: .
image: dhl:latest
container_name: dhl
ports:
- "3000:3000"
volumes:
# Mount a local folder to /app to manage config.json and response files
- ./mock-data:/app
restart: unless-stopped

View File

@@ -1,28 +1,28 @@
use serde::Deserialize; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use std::fs; use std::fs;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct TlsConfig { pub struct TlsConfig {
pub enabled: bool, pub enabled: bool,
pub cert_path: PathBuf, pub cert_path: PathBuf,
pub key_path: PathBuf, pub key_path: PathBuf,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct MaskingConfig { pub struct MaskingConfig {
pub enabled: bool, pub enabled: bool,
pub headers: Vec<String>, pub headers: Vec<String>,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RouteConfig { pub struct RouteConfig {
pub path: String, pub path: String,
pub response_file: PathBuf, pub response_file: PathBuf,
pub status_code: u16, pub status_code: u16,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config { pub struct Config {
pub host: String, pub host: String,
pub port: u16, pub port: u16,
@@ -41,7 +41,11 @@ impl Config {
}) })
} }
Err(_) => { Err(_) => {
Self::default() let default_config = Self::default();
if let Ok(json) = serde_json::to_string_pretty(&default_config) {
let _ = fs::write("config.json", json);
}
default_config
} }
} }
} }
@@ -50,7 +54,7 @@ impl Config {
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
host: "127.0.0.1".to_string(), host: "0.0.0.0".to_string(),
port: 3000, port: 3000,
routes: vec![RouteConfig { routes: vec![RouteConfig {
path: "/".to_string(), path: "/".to_string(),

View File

@@ -17,6 +17,7 @@ use crate::config::Config;
use crate::state::AppState; use crate::state::AppState;
use crate::handlers::{handler, fallback_handler}; use crate::handlers::{handler, fallback_handler};
use crate::middleware::logging_middleware; use crate::middleware::logging_middleware;
use colored::*;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -46,7 +47,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.layer(axum_middleware::from_fn_with_state(state.clone(), logging_middleware)) .layer(axum_middleware::from_fn_with_state(state.clone(), logging_middleware))
.with_state(state); .with_state(state);
let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?; let mut host = config.host.clone();
// In Docker, we usually want to bind to 0.0.0.0 to be accessible
if std::env::var("DOCKER_CONTAINER").is_ok() {
host = "0.0.0.0".to_string();
}
let addr: SocketAddr = format!("{}:{}", host, config.port).parse()?;
// 4. Start server // 4. Start server
if config.tls.enabled { if config.tls.enabled {
@@ -71,17 +79,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
fn print_banner(config: &Config) { fn print_banner(config: &Config) {
println!("------------------------------------------"); println!("{}", "------------------------------------------".bright_black());
println!(" ____ ___ _ ______________ "); println!("{}", " ____ ___ _ ______________ ".blue().bold());
println!(" / __ |/ | / | / / _/ ____/ / "); println!("{}", " / __ |/ | / | / / _/ ____/ / ".blue().bold());
println!(" / / / / /| | / |/ // // __/ / / "); println!("{}", " / / / / /| | / |/ // // __/ / / ".blue().bold());
println!(" / /_/ / ___ |/ /| // // /___/ /___ "); println!("{}", " / /_/ / ___ |/ /| // // /___/ /___ ".blue().bold());
println!(" /_____/_/ |_/_/ |_/___/_____/_____/ "); println!("{}", " /_____/_/ |_/_/ |_/___/_____/_____/ ".blue().bold());
println!("------------------------------------------");
println!(" "); println!(" ");
println!("------ HTTP LISTENER ------"); println!("{}", "Config: ".bold().underline());
println!("host : {}", config.host); println!(" ");
println!("port : {}", config.port); println!("{:<14}: {}", "host".bright_black(), config.host.yellow());
println!("Live Config Reloading: ENABLED"); println!("{:<14}: {}", "port".bright_black(), config.port.to_string().yellow());
println!("------------------------------------------"); println!("{:<14}: {}", "Live Reload".bright_black(), "ENABLED".green().bold());
println!("{}", "------------------------------------------".bright_black());
} }

View File

@@ -7,6 +7,7 @@ use axum::{
use chrono::Local; use chrono::Local;
use std::time::Instant; use std::time::Instant;
use crate::state::SharedState; use crate::state::SharedState;
use colored::*;
// Note: Axum body might need to be imported differently depending on version // Note: Axum body might need to be imported differently depending on version
// but since the original used axum::body::Body, I'll stick to that or similar. // but since the original used axum::body::Body, I'll stick to that or similar.
@@ -23,10 +24,13 @@ pub async fn logging_middleware(
// Load config live for masking settings // Load config live for masking settings
let config = crate::config::Config::load(); let config = crate::config::Config::load();
println!("---- REQUEST START {} ----", start_time.format("%Y-%m-%d %H:%M:%S")); let method = req.method().clone();
println!("Method: {}", req.method()); let path = req.uri().path().to_string();
println!("Path: {}", req.uri().path());
println!("Headers:"); println!("{}", format!("---- REQUEST START {} ----", start_time.format("%Y-%m-%d %H:%M:%S")).blue().bold());
println!("{}: {}", "Method".bright_black(), method);
println!("{}: {}", "Path".bright_black(), path);
println!("{}:", "Headers".bright_black());
for (name, value) in req.headers() { for (name, value) in req.headers() {
let name_str = name.as_str(); let name_str = name.as_str();
@@ -35,9 +39,9 @@ pub async fn logging_middleware(
config.masking.headers.iter().any(|h| h.eq_ignore_ascii_case(name_str)); config.masking.headers.iter().any(|h| h.eq_ignore_ascii_case(name_str));
if should_mask { if should_mask {
println!(" {}: ***", name_str); println!(" {}: {}", name_str.bright_black(), "***".yellow());
} else { } else {
println!(" {}: {:?}", name_str, value); println!(" {}: {:?}", name_str.bright_black(), value);
} }
} }
@@ -45,8 +49,21 @@ pub async fn logging_middleware(
let end_time = Local::now(); let end_time = Local::now();
let duration = start_instant.elapsed(); let duration = start_instant.elapsed();
let status = response.status();
println!("--- REQUEST END {} - {:?} ---------", end_time.format("%H:%M:%S"), duration); let status_colored = if status.is_success() {
status.to_string().green()
} else if status.is_server_error() {
status.to_string().red()
} else if status.is_client_error() {
status.to_string().yellow()
} else {
status.to_string().cyan()
};
println!("{}: {}", "Status".bright_black(), status_colored.bold());
println!("{}", format!("--- REQUEST END {} - {:?} ---------", end_time.format("%H:%M:%S"), duration).blue().bold());
println!();
response response
} }

3
status.json Normal file
View File

@@ -0,0 +1,3 @@
{
"message": "edit me for path /api/status"
}