Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified examples/scripts/_lib.sh
100644 → 100755
Empty file.
23 changes: 14 additions & 9 deletions examples/scripts/proxy_single.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,33 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/_lib.sh"

BIN=${BIN:-"cargo run --"}
CFG=examples/configs/proxy_single_rewrite.toml
CFG=examples/configs/proxy_single.toml

cleanup_ports 8081 9001

# Create temp dir for backend
TMP_DIR=$(mktemp -d)
mkdir -p "$TMP_DIR/api"
echo "hello from backend" > "$TMP_DIR/api/hello"

# Start backend
python3 -m http.server 9001 --bind 127.0.0.1 >/dev/null 2>&1 &
(cd "$TMP_DIR" && python3 -m http.server 9001 --bind 127.0.0.1) >/dev/null 2>&1 &
backend_pid=$!
trap 'kill $backend_pid 2>/dev/null || true' EXIT
trap 'kill $backend_pid 2>/dev/null || true; rm -rf "$TMP_DIR"' EXIT
wait_port_listen 9001

# Start gateway
$BIN serve --config "$CFG" &
gateway_pid=$!
timeout_guard 30 "$gateway_pid"
trap 'kill $gateway_pid $backend_pid 2>/dev/null || true' EXIT
trap 'kill $gateway_pid $backend_pid 2>/dev/null || true; rm -rf "$TMP_DIR"' EXIT
wait_port_listen 8081
wait_http_ok http://127.0.0.1:8081/api 50 0.1 200 || { echo "[proxy_single] FAIL: gateway not ready"; exit 1; }

code1=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8081/api)
code2=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8081/api/)
if [[ "$code1" == "200" && "$code2" == "200" ]]; then
# Test
resp=$(curl -s http://127.0.0.1:8081/api/hello)
if [[ "$resp" == "hello from backend"* ]]; then
echo "[proxy_single] OK"
else
echo "[proxy_single] FAIL: code1=$code1 code2=$code2"; exit 1
echo "[proxy_single] FAIL: expected 'hello from backend', got '$resp'"
exit 1
fi
32 changes: 32 additions & 0 deletions examples/scripts/proxy_single_rewrite.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/_lib.sh"

BIN=${BIN:-"cargo run --"}
CFG=examples/configs/proxy_single_rewrite.toml

cleanup_ports 8081 9001

# Start backend
python3 -m http.server 9001 --bind 127.0.0.1 >/dev/null 2>&1 &
backend_pid=$!
trap 'kill $backend_pid 2>/dev/null || true' EXIT
wait_port_listen 9001

# Start gateway
$BIN serve --config "$CFG" &
gateway_pid=$!
timeout_guard 30 "$gateway_pid"
trap 'kill $gateway_pid $backend_pid 2>/dev/null || true' EXIT
wait_port_listen 8081
wait_http_ok http://127.0.0.1:8081/api 50 0.1 200 || { echo "[proxy_single] FAIL: gateway not ready"; exit 1; }

code1=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8081/api)
code2=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8081/api/)
if [[ "$code1" == "200" && "$code2" == "200" ]]; then
echo "[proxy_single_rewrite] OK"
else
echo "[proxy_single_rewrite] FAIL: code1=$code1 code2=$code2"; exit 1
fi
Comment thread
Akagi201 marked this conversation as resolved.
Empty file modified examples/scripts/ws_binary.sh
100644 → 100755
Empty file.
Empty file modified examples/scripts/ws_close.sh
100644 → 100755
Empty file.
Empty file modified examples/scripts/ws_echo.sh
100644 → 100755
Empty file.
Empty file modified examples/scripts/ws_large_payload.sh
100644 → 100755
Empty file.
Empty file modified examples/scripts/ws_ping_pong.sh
100644 → 100755
Empty file.
14 changes: 0 additions & 14 deletions src/adapters/http_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -903,20 +903,6 @@ impl HttpHandler {

None
}

/// Placeholder for future websocket proxying support.
pub async fn handle_websocket_upgrade(
&self,
_req: Request<AxumBody>,
) -> Result<Response<AxumBody>, eyre::Error> {
// TODO: Implement WebSocket proxying
tracing::warn!("websocket not implemented");

Response::builder()
.status(StatusCode::NOT_IMPLEMENTED)
.body(AxumBody::from("WebSocket proxying not yet implemented"))
.wrap_err("Failed to build not implemented response")
}
}

impl Clone for HttpHandler {
Expand Down
55 changes: 3 additions & 52 deletions src/config/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ use std::collections::HashMap;

use serde::{Deserialize, Serialize};

/// Default function for HTTP/1 enabled flag
fn default_http1_enabled() -> bool {
true
}

/// Configuration for static file serving
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
Expand Down Expand Up @@ -168,8 +163,6 @@ pub struct ServerConfig {
pub protocols: ProtocolConfig,
#[serde(default)]
pub static_files: Option<StaticFilesConfig>,
#[serde(default = "default_http1_enabled")]
pub http1_enabled: bool,
}

impl ServerConfig {
Expand All @@ -191,7 +184,6 @@ impl Default for ServerConfig {
backend_health_paths: HashMap::new(),
protocols: ProtocolConfig::default(),
static_files: None,
http1_enabled: true,
}
}
}
Expand All @@ -207,7 +199,6 @@ pub struct ServerConfigBuilder {
backend_health_paths: HashMap<String, String>,
protocols: Option<ProtocolConfig>,
static_files: Option<StaticFilesConfig>,
http1_enabled: bool,
}

impl Default for ServerConfigBuilder {
Expand All @@ -222,7 +213,6 @@ impl Default for ServerConfigBuilder {
backend_health_paths: HashMap::new(),
protocols: None,
static_files: None,
http1_enabled: true,
}
}
}
Expand Down Expand Up @@ -252,12 +242,6 @@ impl ServerConfigBuilder {
self
}

/// Enable or disable HTTP/1
pub fn http1_enabled(mut self, enabled: bool) -> Self {
self.http1_enabled = enabled;
self
}

/// Add a route with the given path prefix and configuration
pub fn route(mut self, path_prefix: impl Into<String>, config: RouteConfig) -> Self {
self.routes.insert(path_prefix.into(), config);
Expand All @@ -269,17 +253,6 @@ impl ServerConfigBuilder {
self.tls = Some(TlsConfig {
cert_path: Some(cert_path.into()),
key_path: Some(key_path.into()),
acme: None,
});
self
}

/// Set ACME configuration for automatic certificate management
pub fn acme(mut self, acme_config: AcmeConfig) -> Self {
self.tls = Some(TlsConfig {
cert_path: None,
key_path: None,
acme: Some(acme_config),
});
self
}
Expand Down Expand Up @@ -327,39 +300,17 @@ impl ServerConfigBuilder {
backend_health_paths: self.backend_health_paths,
protocols: self.protocols.unwrap_or_default(),
static_files: self.static_files,
http1_enabled: self.http1_enabled,
})
}
}

/// TLS configuration either via manual certificate/key pair or ACME automation.
/// TLS configuration via manual certificate/key pair.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TlsConfig {
/// Path to PEM encoded certificate (if using manual mode)
/// Path to PEM encoded certificate
pub cert_path: Option<String>,
/// Path to PEM encoded private key (if using manual mode)
/// Path to PEM encoded private key
pub key_path: Option<String>,
/// Automatic certificate management configuration
pub acme: Option<AcmeConfig>,
}

/// ACME (e.g. Let's Encrypt) certificate management configuration.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AcmeConfig {
/// Enable ACME flow
pub enabled: bool,
/// Domain list to request certificates for
pub domains: Vec<String>,
/// Contact email for ACME account
pub email: String,
/// Optional custom CA directory URL (defaults to production endpoint)
pub ca_url: Option<String>,
/// Use CA staging environment (rate‑limit friendly)
pub staging: Option<bool>,
/// Where to store issued certs / keys
pub storage_path: Option<String>,
/// Days before expiry to attempt renewal
pub renewal_days_before_expiry: Option<u64>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down
66 changes: 5 additions & 61 deletions src/config/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use eyre::Result;
use regex::Regex;

use crate::config::models::{
AcmeConfig, LoadBalanceStrategy, RateLimitConfig, RouteConfig, ServerConfig, TlsConfig,
LoadBalanceStrategy, RateLimitConfig, RouteConfig, ServerConfig, TlsConfig,
};

/// Validation result type alias
Expand All @@ -27,9 +27,6 @@ pub enum ValidationError {
#[error("Invalid TLS configuration: {message}")]
InvalidTls { message: String },

#[error("Invalid ACME configuration: {message}")]
InvalidAcme { message: String },

#[error("Route conflict detected: {message}")]
RouteConflict { message: String },

Expand Down Expand Up @@ -345,8 +342,8 @@ impl ServerConfigValidator {

/// Validate TLS configuration
fn validate_tls_config(config: &TlsConfig) -> ValidationResult<()> {
match (&config.cert_path, &config.key_path, &config.acme) {
(Some(cert), Some(key), None) => {
match (&config.cert_path, &config.key_path) {
(Some(cert), Some(key)) => {
// Manual certificate configuration
if !std::path::Path::new(cert).exists() {
return Err(ValidationError::InvalidTls {
Expand All @@ -362,66 +359,13 @@ impl ServerConfigValidator {

Ok(())
}
(None, None, Some(acme_config)) => {
// ACME configuration
Self::validate_acme_config(acme_config)
}
(None, None, None) => Err(ValidationError::InvalidTls {
message:
"TLS configuration must specify either certificate paths or ACME configuration"
.to_string(),
}),
_ => Err(ValidationError::InvalidTls {
message:
"TLS configuration cannot specify both certificate paths and ACME configuration"
.to_string(),
message: "TLS configuration must specify both certificate and private key paths"
.to_string(),
}),
}
}

/// Validate ACME configuration
fn validate_acme_config(config: &AcmeConfig) -> ValidationResult<()> {
if !config.enabled {
return Ok(());
}

if config.domains.is_empty() {
return Err(ValidationError::InvalidAcme {
message: "ACME configuration must specify at least one domain".to_string(),
});
}

// Validate email format
let email_regex = Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").expect("Invalid email regex");
if !email_regex.is_match(&config.email) {
return Err(ValidationError::InvalidAcme {
message: format!("Invalid email address: {}", config.email),
});
}

// Validate domains
for domain in &config.domains {
if domain.is_empty() || domain.contains(' ') {
return Err(ValidationError::InvalidAcme {
message: format!("Invalid domain: {domain}"),
});
}
}

// Validate renewal period
if let Some(days) = config.renewal_days_before_expiry {
if days == 0 || days > 89 {
return Err(ValidationError::InvalidAcme {
message: format!(
"Renewal days before expiry must be between 1 and 89, got {days}"
),
});
}
}

Ok(())
}

/// Check for conflicting route paths
fn check_route_conflicts(
routes: &std::collections::HashMap<String, RouteConfig>,
Expand Down
57 changes: 57 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ enum Commands {
#[clap(short, long, default_value = "config.toml")]
config: String,
},
/// Initialize a new configuration file
Init {
/// Output path for the new config file
#[clap(short, long, default_value = "config.toml")]
config: String,
},
/// Start the gateway server (default)
Serve {
/// Configuration file to use
Expand All @@ -57,6 +63,7 @@ async fn main() -> Result<()> {
// Determine the command to run
let (command, config_path) = match args.command {
Some(Commands::Validate { config }) => ("validate", config),
Some(Commands::Init { config }) => ("init", config),
Some(Commands::Serve { config }) => ("serve", config),
None => ("serve", args.config), // Default to serve with config from args
};
Expand All @@ -65,6 +72,9 @@ async fn main() -> Result<()> {
"validate" => {
return validate_config_command(&config_path).await;
}
"init" => {
return init_config_command(&config_path).await;
}
"serve" => {
// Continue with normal server startup
}
Expand Down Expand Up @@ -642,3 +652,50 @@ async fn validate_config_command(config_path: &str) -> Result<()> {
}
}
}

/// Initialize a new configuration file
async fn init_config_command(config_path: &str) -> Result<()> {
let path = Path::new(config_path);
if path.exists() {
eprintln!("❌ Error: Configuration file '{config_path}' already exists");
std::process::exit(1);
}

let default_config = r#"# Axon API Gateway Configuration

# The address to listen on
listen_addr = "127.0.0.1:8080"

# Health check configuration
[health_check]
enabled = true
interval_secs = 10
path = "/health"

# Protocol configuration
[protocols]
http2_enabled = true
websocket_enabled = true

# Example Route: Proxy to a backend
[routes."/api"]
type = "proxy"
target = "http://localhost:3000"

# Example Route: Static files
[routes."/static"]
type = "static"
root = "./static"

# Example Route: Load Balancer
# [routes."/service"]
# type = "load_balance"
# targets = ["http://localhost:3001", "http://localhost:3002"]
# strategy = "round_robin"
"#;

tokio::fs::write(path, default_config).await.context("Failed to write config file")?;
println!("✅ Created default configuration at: {config_path}");
println!(" Run 'axon serve --config {config_path}' to start the server");
Ok(())
}