diff --git a/.gitignore b/.gitignore index 1610fa1..7cfbb8e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,11 @@ __pycache__ .pytest_cache *.pyc .env +/examples/static/conf/config.yml +/examples/docker/certs/haproxy/.place_holder_cert.pem +/examples/static/host1.local.pem +/examples/swarm/certs/host1.local.pem +/examples/docker/host2.local.pem +/examples/swarm/certs/host2.local.pem +/examples/docker/jwt_private.pem +/examples/docker/jwt_pubkey.pem diff --git a/examples/docker/README.md b/examples/docker/README.md index 3c43984..8ebbb67 100644 --- a/examples/docker/README.md +++ b/examples/docker/README.md @@ -145,6 +145,25 @@ labels: myproxy.http.port: 80 ``` + +**Usage:** +```bash +docker compose -f docker-compose-changed-label.yml up -d +``` + +**Test:** +```bash +# Test load balancing (hostname changes between containers) +curl -H "Host: www.helloworld.com" localhost:19901 +# Response: f6d8d45b7411 +curl -H "Host: www.helloworld.com" localhost:19901 +# Response: 59b213cb8592 + +# Test redirect +curl -I -H "Host: google.helloworld.com" localhost:19901 +# Should redirect to: www.google.com/ +``` + --- ### 5. Portainer Integration (`docker-compose-portainer.yml`) diff --git a/examples/docker/docker-compose-changed-label.yml b/examples/docker/docker-compose-changed-label.yml index 76ca38c..b5f7259 100644 --- a/examples/docker/docker-compose-changed-label.yml +++ b/examples/docker/docker-compose-changed-label.yml @@ -27,7 +27,7 @@ services: container: image: byjg/static-httpserver labels: - haproxy.http.redirect: host1.local--https://host1.local + haproxy.http.redirect: '{"host1.local": "https://host1.local"}' haproxy.http.host: host1.local haproxy.http.port: 80 diff --git a/examples/static/README.md b/examples/static/README.md index 8f5bac6..d6fefbc 100644 --- a/examples/static/README.md +++ b/examples/static/README.md @@ -1,49 +1,312 @@ # Static Configuration Example -This directory demonstrates EasyHAProxy using **static configuration** mode instead of dynamic service discovery. +This directory demonstrates EasyHAProxy using **static YAML configuration** instead of dynamic service discovery. -## What is Static Mode? - -Static mode uses a YAML configuration file (`config.yml`) to define HAProxy routing rules instead of discovering services automatically from Docker/Kubernetes/Swarm labels. - -**Use cases:** -- Non-containerized backends -- Mixed environments (containers + VMs + bare metal) -- Fixed infrastructure where services don't change frequently -- Testing HAProxy configurations +Static mode is useful for: +- Non-containerized backends (VMs, bare metal) +- Fixed infrastructure +- Explicit routing control --- -## Files in This Example +## Prerequisites -- `conf/config.yml` - Static configuration defining hosts and routing -- `docker-compose.yml` - EasyHAProxy container mounting the config file -- `host1.local.pem` - Example SSL certificate +### 1. Generate SSL Certificates ---- - -## Configuration Structure - -### docker-compose.yml - -```yaml -services: - haproxy: - image: byjg/easy-haproxy:4.6.0 - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./conf:/etc/haproxy/static # Mount static config - - ./host1.local.pem:/certs/haproxy/host1.local.pem - environment: - EASYHAPROXY_DISCOVER: static # Use static mode - EASYHAPROXY_SSL_MODE: "loose" - ports: - - "80:80" - - "443:443" - - "1936:1936" +```bash +# From repository root +./examples/generate-keys.sh ``` -### conf/config.yml +### 2. Add Host Entry + +```bash +echo "127.0.0.1 host1.local www.host1.local" | sudo tee -a /etc/hosts +echo "127.0.0.1 host2.local www.host2.local" | sudo tee -a /etc/hosts +``` + +--- + +## Scenario 1: Basic (HTTP → HTTPS Redirect) + +**What it does:** Simple HTTP to HTTPS redirect with SSL termination. + +### Getting Started + +```bash +cd examples/static + +# 1. Copy the basic config +cp conf/config-basic.yml conf/config.yml + +# 2. Start backend container +docker run -d --name container -p 8080:8080 byjg/static-httpserver + +# 3. Start EasyHAProxy +docker compose up -d +``` + +### Test + +```bash +# Test HTTP redirect +curl -I http://host1.local +# Expected: HTTP/1.1 301 Moved Permanently +# Expected: Location: https://host1.local + +# Test HTTPS +curl -k https://host1.local +# Expected: Hello from Static HTTP Server! + +# Test www redirect +curl -I http://www.host1.local +# Expected: HTTP/1.1 301 Moved Permanently +# Expected: Location: https://host1.local +``` + +### Stats Interface + +Open: http://localhost:1936 +- Username: `admin` +- Password: `password` + +### Clean Up + +```bash +docker compose down +docker stop container && docker rm container +``` + +--- + +## Scenario 2: Certbot (Let's Encrypt SSL) + +**What it does:** Automatic SSL certificates from Let's Encrypt using ACME HTTP-01 challenge. + +### Requirements + +- Public IP address +- Domain pointing to your IP +- Ports 80/443 publicly accessible + +### Getting Started + +```bash +cd examples/static + +# 1. Copy the certbot config +cp conf/config-certbot.yml conf/config.yml + +# 2. Edit config.yml and change: +# - Replace "example.com" with your real domain +# - Update EASYHAPROXY_CERTBOT_EMAIL in docker-compose.yml + +# 3. Start backend container +docker run -d --name container -p 8080:8080 byjg/static-httpserver + +# 4. Start EasyHAProxy +docker compose up -d + +# 5. Check logs for certificate generation +docker compose logs -f +``` + +### What to Expect + +``` +# Certbot will: +# 1. Request certificate from Let's Encrypt +# 2. Complete HTTP-01 challenge +# 3. Save certificate in /certs/certbot/ +# 4. Reload HAProxy with new certificate +``` + +### Test + +```bash +# Test HTTPS with real certificate +curl https://your-domain.com +# Expected: No certificate warnings (valid SSL) + +# Test HTTP redirect +curl -I http://your-domain.com +# Expected: HTTP/1.1 301 Moved Permanently +``` + +### Clean Up + +```bash +docker compose down +docker stop container && docker rm container +``` + +**Note:** Certificates are stored in Docker volume `certs_certbot` and persist across restarts. + +--- + +## Scenario 3: Deny Pages (Block Specific Paths) + +**What it does:** Blocks access to sensitive paths like `/admin`, `/wp-login.php`, etc. + +### Getting Started + +```bash +cd examples/static + +# 1. Copy the deny-pages config +cp conf/config-deny-pages.yml conf/config.yml + +# 2. Start backend container +docker run -d --name container -p 8080:8080 byjg/static-httpserver + +# 3. Start EasyHAProxy +docker compose up -d +``` + +### Test + +```bash +# Test normal page (should work) +curl -k https://host1.local/ +# Expected: Hello from Static HTTP Server! + +# Test blocked path (should fail) +curl -I -k https://host1.local/admin +# Expected: HTTP/1.1 404 Not Found + +curl -I -k https://host1.local/wp-login.php +# Expected: HTTP/1.1 404 Not Found + +curl -I -k https://host1.local/.env +# Expected: HTTP/1.1 404 Not Found +``` + +### What's Blocked + +The example blocks these paths: +- `/admin` +- `/wp-admin` +- `/wp-login.php` +- `/.env` +- `/config` + +### Customize Blocked Paths + +Edit `conf/config.yml`: + +```yaml +plugin_config: + deny_pages: + paths: /admin,/private,/internal + status_code: 403 # or 404 +``` + +### Clean Up + +```bash +docker compose down +docker stop container && docker rm container +``` + +--- + +## Scenario 4: JWT Validator (API Authentication) + +**What it does:** Validates JWT tokens in Authorization header before allowing access. + +### Getting Started + +```bash +cd examples/static + +# 1. Copy the JWT validator config +cp conf/config-jwt-validator.yml conf/config.yml + +# 2. JWT keys were already generated by generate-keys.sh +# Location: examples/docker/jwt_pubkey.pem and jwt_private.pem + +# 3. Start backend container +docker run -d --name container -p 8080:8080 byjg/static-httpserver + +# 4. Start EasyHAProxy +docker compose up -d +``` + +### Test Without Token (Should Fail) + +```bash +curl -k https://host1.local/ +# Expected: Missing Authorization HTTP header +``` + +### Test With Valid Token + +```bash +# 1. Generate a test JWT token using jwt_private.pem +# You can use https://jwt.io or a JWT library + +# 2. Example with valid token: +TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." +curl -k -H "Authorization: Bearer $TOKEN" https://host1.local/ +# Expected: Hello from Static HTTP Server! (if token is valid) +``` + +### Generate Test Token + +```python +# Python example using PyJWT +import jwt +from datetime import datetime, timedelta + +with open('examples/docker/jwt_private.pem', 'r') as f: + private_key = f.read() + +payload = { + 'iss': 'https://auth.example.com/', + 'aud': 'https://api.example.com', + 'exp': datetime.utcnow() + timedelta(hours=1) +} + +token = jwt.encode(payload, private_key, algorithm='RS256') +print(token) +``` + +### What's Validated + +- Authorization header must be present +- Token must be valid JWT format +- Signature must match public key (`jwt_pubkey.pem`) +- Issuer must match: `https://auth.example.com/` +- Audience must match: `https://api.example.com` +- Token must not be expired + +### Customize JWT Settings + +Edit `conf/config.yml`: + +```yaml +plugin_config: + jwt_validator: + algorithm: RS256 + issuer: https://your-auth-server.com/ + audience: https://your-api.com + pubkey_path: /certs/haproxy/jwt_pubkey.pem +``` + +### Clean Up + +```bash +docker compose down +docker stop container && docker rm container +``` + +--- + +## Configuration File Reference + +All scenarios use `/etc/haproxy/static/config.yml` mounted from `./conf/config.yml`. + +### Basic Structure ```yaml stats: @@ -54,416 +317,102 @@ stats: customerrors: true easymapping: - # HTTP Port 80 - Redirects to HTTPS - port: 80 redirect: host1.local: https://host1.local - www.host1.local: https://host1.local - # HTTPS Port 443 - port: 443 ssl: true hosts: host1.local: containers: - - container:8080 # Backend container + - container:8080 ``` ---- - -## How It Works - -### 1. Port Definitions - -Each item in `easymapping` defines a listening port: +### With Plugins ```yaml easymapping: - - port: 80 # Listen on port 80 - redirect: {...} # Optional redirects - - - port: 443 # Listen on port 443 - ssl: true # Enable SSL - hosts: {...} # Virtual hosts -``` - -### 2. Redirect Configuration - -Redirect specific domains to different URLs: - -```yaml -- port: 80 - redirect: - host1.local: https://host1.local # HTTP → HTTPS - www.host1.local: https://host1.local # www → non-www + HTTPS - old.domain.com: https://new.domain.com # Domain change -``` - -### 3. Virtual Hosts - -Define hosts and their backend containers: - -```yaml -- port: 443 - ssl: true - hosts: - host1.local: # Virtual host domain - containers: - - container:8080 # Backend: container_name:port - - another_container:3000 # Multiple backends = load balancing - - host2.local: - containers: - - webserver:80 -``` - -**Backend formats:** -- `container_name:port` - Docker container by name -- `ip_address:port` - Direct IP address -- `hostname:port` - Hostname resolution - -### 4. SSL Configuration - -```yaml -- port: 443 - ssl: true # Enable SSL on this port - hosts: - secure.example.com: - containers: - - app:8080 -``` - -SSL certificates must be placed in: -- `/certs/haproxy/.pem` inside container -- `./certs/.pem` on host (if volume mounted) - -Certificate format: PEM file containing both certificate and private key. - -### 5. Stats Interface - -```yaml -stats: - username: admin - password: password - port: 1936 -``` - -Access at: `http://localhost:1936` - ---- - -## Running the Example - -### Prerequisites: Generate SSL Certificates - -Before running the examples, you need to generate the required SSL certificates: - -```bash -# From the repository root -./examples/generate-keys.sh -``` - -This script automatically generates: -- SSL certificates for host1.local and host2.local -- JWT keys for authentication examples -- All other .pem files needed for testing - -### 1. Start the Example - -```bash -cd examples/static -docker compose up -d -``` - -### 2. Create Backend Container - -The static config references `container:8080`. Create a container with this name: - -```bash -docker run -d --name container \ - -p 8080:8080 \ - byjg/static-httpserver -``` - -Or add to `docker-compose.yml`: - -```yaml -services: - # ... haproxy service ... - - container: - image: byjg/static-httpserver - ports: - - "8080:8080" -``` - -### 3. Test - -```bash -# Add to /etc/hosts: -# 127.0.0.1 host1.local www.host1.local - -# Test HTTP redirect -curl -I http://host1.local -# Should return: HTTP/1.1 301 Moved Permanently -# Location: https://host1.local - -# Test HTTPS -curl -k https://host1.local - -# Access stats -open http://localhost:1936 -# Username: admin -# Password: password + - port: 443 + ssl: true + hosts: + host1.local: + containers: + - container:8080 + plugins: + - deny_pages + plugin_config: + deny_pages: + paths: /admin,/private + status_code: 404 ``` --- -## Advanced Configuration +## Advanced: Multiple Backends -### Load Balancing Multiple Backends +Load balance across multiple containers: ```yaml hosts: api.example.com: containers: - - api_server_1:8080 - - api_server_2:8080 - - api_server_3:8080 + - api1:8080 + - api2:8080 + - api3:8080 ``` -Default algorithm: round-robin +--- -### Custom Balance Algorithm +## Advanced: External Backends -```yaml -hosts: - api.example.com: - balance: leastconn # Use least connections instead of round-robin - containers: - - api_1:8080 - - api_2:8080 -``` - -**Available algorithms:** -- `roundrobin` - Distribute evenly (default) -- `leastconn` - Send to server with fewest connections -- `source` - Same client IP always goes to same server - -### External Backends (Non-Docker) +Route to non-Docker backends: ```yaml hosts: legacy.example.com: containers: - - 192.168.1.100:8080 # VM - - 192.168.1.101:8080 # Another VM - - database.local:5432 # Database server + - 192.168.1.100:8080 + - 192.168.1.101:8080 ``` -### Health Checks - -```yaml -hosts: - webapp.example.com: - containers: - - server1:8080 - - server2:8080 - healthcheck: - path: /health - interval: 5s -``` - -### Multiple Domains, Same Backend - -```yaml -hosts: - example.com: - containers: - - webapp:8080 - www.example.com: - containers: - - webapp:8080 # Same backend - app.example.com: - containers: - - webapp:8080 # Same backend -``` - -### Path-Based Routing - -While static mode focuses on host-based routing, you can achieve path-based routing using redirects: - -```yaml -- port: 80 - redirect: - api.example.com/v1: https://api-v1.internal:8080 - api.example.com/v2: https://api-v2.internal:8080 -``` - -Or use HAProxy ACLs via custom templates (advanced). - ---- - -## Plugins with Static Configuration - -Enable plugins globally or per-host in static mode: - -### Global Plugin Configuration - -```yaml -plugins: - enabled: [cleanup] - config: - cleanup: - max_idle_time: 600 -``` - -### Per-Host Plugin Configuration (via env vars) - -Since static mode doesn't support per-host plugin configuration directly, use environment variables for domain-specific plugins: - -```yaml -environment: - EASYHAPROXY_PLUGINS_ENABLED: cloudflare,deny_pages - EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH: /etc/haproxy/cloudflare_ips.lst - EASYHAPROXY_PLUGIN_DENY_PAGES_PATHS: /admin,/private -``` - -See [Using Plugins](../../docs/plugins.md) for more details. - ---- - -## Comparison: Static vs. Dynamic Discovery - -| Feature | Static Mode | Docker/Swarm/K8s Mode | -|---------------|-----------------------------|----------------------------------------------| -| Configuration | YAML file | Container labels / Ingress annotations | -| Backend types | Any (containers, VMs, IPs) | Containers only | -| Updates | Manual config edit + reload | Automatic discovery | -| Use case | Fixed infrastructure | Dynamic container environments | -| Plugin config | Global via YAML/env | Per-container/ingress via labels/annotations | - ---- - -## Tips - -1. **Reloading Configuration:** - ```bash - # EasyHAProxy watches config.yml for changes - # Edit conf/config.yml, changes auto-reload - - # Or manually restart: - docker compose restart haproxy - ``` - -2. **Validate Configuration:** - ```bash - # Check HAProxy config is valid - docker compose exec haproxy haproxy -c -f /etc/haproxy/haproxy.cfg - ``` - -3. **View Generated Config:** - ```bash - docker compose exec haproxy cat /etc/haproxy/haproxy.cfg - ``` - -4. **Debugging:** - ```bash - # Enable debug mode - docker compose up - # Watch logs in real-time - ``` - -5. **SSL Certificate Management:** - ```bash - # Generate self-signed cert - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout host.key -out host.crt \ - -subj "/CN=host1.local" - - # Combine into PEM - cat host.crt host.key > host1.local.pem - ``` - --- ## Troubleshooting -### Backend Unreachable +### FileNotFoundError: config.yml -**Error:** `503 Service Unavailable` - -**Causes:** -- Backend container not running -- Wrong container name in config -- Wrong port number -- Network connectivity issues - -**Debug:** ```bash -# Check backend container is running -docker ps | grep container_name +# Make sure config.yml exists +ls conf/config.yml -# Test backend directly -curl http://container_name:port - -# Check HAProxy logs -docker compose logs haproxy +# If missing, copy from an example: +cp conf/config-basic.yml conf/config.yml ``` -### Configuration Not Reloading +### 503 Service Unavailable -**Solution:** ```bash -# Restart HAProxy -docker compose restart haproxy - -# Check file is mounted correctly -docker compose exec haproxy cat /etc/haproxy/static/config.yml +# Check backend is running +docker ps | grep container +curl http://localhost:8080 ``` ### SSL Certificate Not Found -**Error:** Certificate errors in logs - -**Solution:** ```bash -# Verify certificate is mounted -docker compose exec haproxy ls -la /certs/haproxy/ +# Verify certificate exists +ls -la host1.local.pem -# Check certificate format (must be PEM with cert + key) -openssl x509 -in host.pem -text -noout -openssl rsa -in host.pem -check +# Regenerate if needed +cd ../.. && ./examples/generate-keys.sh ``` ---- +### Changes Not Applied -## Migration from Dynamic to Static - -If you have Docker labels and want to convert to static config: - -**Docker label:** -```yaml -labels: - easyhaproxy.http.host: api.example.com - easyhaproxy.http.port: 80 - easyhaproxy.http.localport: 8080 - easyhaproxy.http.redirect_ssl: true -``` - -**Static config equivalent:** -```yaml -easymapping: - - port: 80 - redirect: - api.example.com: https://api.example.com - - - port: 443 - ssl: true - hosts: - api.example.com: - containers: - - container_name:8080 +```bash +# Restart to reload config +docker compose restart ``` --- @@ -471,6 +420,5 @@ easymapping: ## Further Reading - [Static Configuration Guide](../../docs/static.md) -- [Environment Variables](../../docs/environment-variable.md) - [Using Plugins](../../docs/plugins.md) -- [SSL Configuration](../../docs/ssl.md) +- [Environment Variables](../../docs/environment-variable.md) diff --git a/src/easymapping/__init__.py b/src/easymapping/__init__.py index ccf5812..77930a4 100644 --- a/src/easymapping/__init__.py +++ b/src/easymapping/__init__.py @@ -4,6 +4,7 @@ import os import re from jinja2 import Environment, FileSystemLoader +from functions import loggerEasyHaproxy class DockerLabelHandler: @@ -32,7 +33,16 @@ class DockerLabelHandler: def get_json(self, label, default_value={}): if self.has_label(label): - return json.loads(self.__data[label]) + value = self.__data[label] + if not value: # Handle empty strings + return default_value + try: + return json.loads(value) + except json.JSONDecodeError as e: + loggerEasyHaproxy.error( + f"Invalid JSON in label '{label}': {value}. Error: {e}. Using default value." + ) + return default_value return default_value def set_data(self, data): @@ -67,8 +77,7 @@ class HaproxyConfigGenerator: self.global_plugin_configs = [] except Exception as e: # If plugin system fails to initialize, log but continue - import logging - logging.warning(f"Failed to initialize plugin system: {e}") + loggerEasyHaproxy.warning(f"Failed to initialize plugin system: {e}") self.plugin_manager = None self.global_plugin_configs = [] @@ -102,8 +111,7 @@ class HaproxyConfigGenerator: global_configs = [r.haproxy_config for r in global_results if r.haproxy_config] self.global_plugin_configs.extend(global_configs) except Exception as e: - import logging - logging.warning(f"Failed to execute global plugins: {e}") + loggerEasyHaproxy.warning(f"Failed to execute global plugins: {e}") templates_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'templates') file_loader = FileSystemLoader(templates_dir) @@ -274,8 +282,7 @@ class HaproxyConfigGenerator: if result.metadata["fcgi_app_definition"] not in self.global_plugin_configs: self.global_plugin_configs.append(result.metadata["fcgi_app_definition"]) except Exception as e: - import logging - logging.warning(f"Failed to execute domain plugins for {hostname}: {e}") + loggerEasyHaproxy.warning(f"Failed to execute domain plugins for {hostname}: {e}") easymapping[port]["hosts"][hostname]["plugin_configs"] = [] else: easymapping[port]["hosts"][hostname]["plugin_configs"] = [] diff --git a/src/main.py b/src/main.py index 30412a3..7af2bbf 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,4 @@ import os -import logging from deepdiff import DeepDiff diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py index ac7d1c4..e822d92 100644 --- a/src/plugins/__init__.py +++ b/src/plugins/__init__.py @@ -145,7 +145,7 @@ class PluginManager: elif plugin.plugin_type == PluginType.DOMAIN: self.domain_plugins.append(plugin) - self.logger.info(f"Loaded {source} plugin: {plugin.name} ({plugin.plugin_type.value})") + self.logger.debug(f"Loaded {source} plugin: {plugin.name} ({plugin.plugin_type.value})") except Exception as e: self._handle_error(f"Failed to load plugin from {filepath}: {str(e)}")