diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b3b9fb3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Virtual environments
+venv/
+env/
+ENV/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Logs
+*.log
+
+# OS
+.DS_Store
+Thumbs.db
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..f1a59e8
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,81 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Development Commands
+
+### Testing
+- **API Testing**: `./scripts/test-api.sh` - Tests all API endpoints with optional authentication
+- **Certificate Request Testing**: `./scripts/test-certificate-request.sh` - Tests certificate generation endpoints
+- **Manual Testing**: Run `curl` commands against `http://localhost:8000` endpoints as shown in README.md
+
+### Running the Application
+- **Docker Build**: `docker build -t haproxy-manager .`
+- **Local Development**: `python haproxy_manager.py` (requires HAProxy, certbot, and dependencies installed)
+- **Container Run**: See README.md for various docker run configurations
+
+### Monitoring and Debugging
+- **Error Monitoring**: `./scripts/monitor-errors.sh` - Monitor application error logs
+- **External Monitoring**: `./scripts/monitor-errors-external.sh` - External monitoring script
+- **Health Check**: `curl http://localhost:8000/health`
+- **Log Files**:
+ - `/var/log/haproxy-manager.log` - General application logs
+ - `/var/log/haproxy-manager-errors.log` - Error logs for alerting
+
+## Architecture Overview
+
+### Core Components
+
+1. **haproxy_manager.py** - Main Flask application providing:
+ - RESTful API for HAProxy configuration management
+ - SQLite database integration for domain/backend storage
+ - Let's Encrypt certificate automation
+ - HAProxy configuration generation from Jinja2 templates
+ - Optional API key authentication via `HAPROXY_API_KEY` environment variable
+
+2. **Database Schema** - SQLite database with three main tables:
+ - `domains` - Domain configurations with SSL settings
+ - `backends` - Backend service definitions linked to domains
+ - `backend_servers` - Individual servers within backend groups
+
+3. **Template System** - Jinja2 templates for HAProxy configuration generation:
+ - `hap_header.tpl` - Global HAProxy settings and defaults
+ - `hap_backend.tpl` - Backend server definitions
+ - `hap_listener.tpl` - Frontend listener configurations
+ - `hap_letsencrypt.tpl` - SSL certificate configurations
+ - Template override support for custom backend configurations
+
+4. **Certificate Management** - Automated SSL certificate handling:
+ - Let's Encrypt integration with certbot
+ - Self-signed certificate fallback for development
+ - Certificate renewal automation via cron
+ - Certificate download endpoints for external services
+
+### Configuration Flow
+
+1. Domain added via `/api/domain` endpoint → Database updated
+2. `generate_config()` function → Reads database, renders Jinja2 templates → Writes `/etc/haproxy/haproxy.cfg`
+3. HAProxy reload via socket API (`/tmp/haproxy-cli`) or process restart
+4. SSL certificate generation via Let's Encrypt or self-signed fallback
+
+### Key Design Patterns
+
+- **Template-driven configuration**: HAProxy config generated from modular Jinja2 templates
+- **Database-backed state**: All configuration persisted in SQLite for reliability
+- **API-first design**: All operations exposed via REST endpoints
+- **Process monitoring**: Health checks and automatic HAProxy restart capabilities
+- **Comprehensive logging**: Operation logging with error alerting support
+
+### Authentication & Security
+
+- Optional API key authentication controlled by `HAPROXY_API_KEY` environment variable
+- All API endpoints (except `/health` and `/`) require Bearer token when API key is set
+- Certificate private keys combined with certificates in HAProxy-compatible format
+- Default backend page for unmatched domains instead of exposing HAProxy errors
+
+### Deployment Context
+
+- Designed to run as Docker container with persistent volumes for certificates and configurations
+- Exposes ports 80 (HTTP), 443 (HTTPS), and 8000 (management API/UI)
+- Management interface on port 8000 should be firewall-protected in production
+- Supports deployment on servers with git directory at `/root/whp` and web file sync via rsync to `/docker/whp/web/`
\ No newline at end of file
diff --git a/IP_BLOCKING_API.md b/IP_BLOCKING_API.md
new file mode 100644
index 0000000..030d783
--- /dev/null
+++ b/IP_BLOCKING_API.md
@@ -0,0 +1,422 @@
+# IP Blocking API Documentation
+
+This document describes the IP blocking functionality added to HAProxy Manager, which allows WHP (Web Hosting Platform) to manage blocked IP addresses through the API.
+
+## Overview
+
+The IP blocking feature allows administrators to:
+- Block specific IP addresses from accessing any sites managed by HAProxy
+- Unblock previously blocked IP addresses
+- View all currently blocked IP addresses
+- Track who blocked an IP and when
+
+When an IP is blocked, visitors from that IP address will see a custom "Access Denied" page instead of the requested website.
+
+## API Endpoints
+
+### Authentication
+
+All IP blocking endpoints require API key authentication when `HAPROXY_API_KEY` is set:
+
+```bash
+Authorization: Bearer your-api-key
+```
+
+### 1. Get All Blocked IPs
+
+Retrieve a list of all currently blocked IP addresses.
+
+**Endpoint:** `GET /api/blocked-ips`
+
+**Response:**
+```json
+[
+ {
+ "id": 1,
+ "ip_address": "192.168.1.100",
+ "reason": "Suspicious activity detected",
+ "blocked_at": "2024-01-15 10:30:00",
+ "blocked_by": "WHP Admin Panel"
+ },
+ {
+ "id": 2,
+ "ip_address": "10.0.0.50",
+ "reason": "Brute force attempts",
+ "blocked_at": "2024-01-15 11:45:00",
+ "blocked_by": "Security System"
+ }
+]
+```
+
+**Example Request:**
+```bash
+curl -X GET http://localhost:8000/api/blocked-ips \
+ -H "Authorization: Bearer your-api-key"
+```
+
+### 2. Block an IP Address
+
+Add an IP address to the blocked list.
+
+**Endpoint:** `POST /api/blocked-ips`
+
+**Request Body:**
+```json
+{
+ "ip_address": "192.168.1.100",
+ "reason": "Suspicious activity detected",
+ "blocked_by": "WHP Admin Panel"
+}
+```
+
+**Parameters:**
+- `ip_address` (required): The IP address to block (e.g., "192.168.1.100")
+- `reason` (optional): Reason for blocking (default: "No reason provided")
+- `blocked_by` (optional): Who/what initiated the block (default: "API")
+
+**Response:**
+```json
+{
+ "status": "success",
+ "blocked_ip_id": 1,
+ "message": "IP 192.168.1.100 has been blocked"
+}
+```
+
+**Error Responses:**
+- `400 Bad Request`: IP address is missing
+- `409 Conflict`: IP address is already blocked
+- `500 Internal Server Error`: Configuration generation failed
+
+**Example Request:**
+```bash
+curl -X POST http://localhost:8000/api/blocked-ips \
+ -H "Authorization: Bearer your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "ip_address": "192.168.1.100",
+ "reason": "Multiple failed login attempts",
+ "blocked_by": "WHP Security Module"
+ }'
+```
+
+### 3. Unblock an IP Address
+
+Remove an IP address from the blocked list.
+
+**Endpoint:** `DELETE /api/blocked-ips`
+
+**Request Body:**
+```json
+{
+ "ip_address": "192.168.1.100"
+}
+```
+
+**Parameters:**
+- `ip_address` (required): The IP address to unblock
+
+**Response:**
+```json
+{
+ "status": "success",
+ "message": "IP 192.168.1.100 has been unblocked"
+}
+```
+
+**Error Responses:**
+- `400 Bad Request`: IP address is missing
+- `404 Not Found`: IP address not found in blocked list
+- `500 Internal Server Error`: Configuration generation failed
+
+**Example Request:**
+```bash
+curl -X DELETE http://localhost:8000/api/blocked-ips \
+ -H "Authorization: Bearer your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"ip_address": "192.168.1.100"}'
+```
+
+## Integration with WHP
+
+### PHP Integration Example
+
+Here's how to integrate the IP blocking API into WHP using PHP:
+
+```php
+apiUrl = rtrim($apiUrl, '/');
+ $this->apiKey = $apiKey;
+ }
+
+ /**
+ * Get all blocked IPs
+ */
+ public function getBlockedIPs() {
+ return $this->makeRequest('GET', '/api/blocked-ips');
+ }
+
+ /**
+ * Block an IP address
+ */
+ public function blockIP($ipAddress, $reason = null, $blockedBy = 'WHP Control Panel') {
+ $data = [
+ 'ip_address' => $ipAddress,
+ 'reason' => $reason ?: 'Blocked via WHP Control Panel',
+ 'blocked_by' => $blockedBy
+ ];
+
+ return $this->makeRequest('POST', '/api/blocked-ips', $data);
+ }
+
+ /**
+ * Unblock an IP address
+ */
+ public function unblockIP($ipAddress) {
+ $data = ['ip_address' => $ipAddress];
+ return $this->makeRequest('DELETE', '/api/blocked-ips', $data);
+ }
+
+ /**
+ * Make API request
+ */
+ private function makeRequest($method, $endpoint, $data = null) {
+ $url = $this->apiUrl . $endpoint;
+
+ $headers = [
+ 'Authorization: Bearer ' . $this->apiKey,
+ 'Content-Type: application/json'
+ ];
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
+
+ if ($data) {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+ }
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ $result = json_decode($response, true);
+
+ if ($httpCode >= 200 && $httpCode < 300) {
+ return ['success' => true, 'data' => $result];
+ } else {
+ return ['success' => false, 'error' => $result['message'] ?? 'Unknown error', 'code' => $httpCode];
+ }
+ }
+}
+
+// Usage example:
+$haproxyBlocker = new HAProxyIPBlocker('http://haproxy-manager:8000', 'your-api-key-here');
+
+// Block an IP
+$result = $haproxyBlocker->blockIP('192.168.1.100', 'Spam detection', 'WHP Anti-Spam Module');
+if ($result['success']) {
+ echo "IP blocked successfully: " . $result['data']['message'];
+} else {
+ echo "Error: " . $result['error'];
+}
+
+// Get all blocked IPs
+$blockedIPs = $haproxyBlocker->getBlockedIPs();
+if ($blockedIPs['success']) {
+ foreach ($blockedIPs['data'] as $ip) {
+ echo "Blocked IP: {$ip['ip_address']} - Reason: {$ip['reason']}\n";
+ }
+}
+
+// Unblock an IP
+$result = $haproxyBlocker->unblockIP('192.168.1.100');
+if ($result['success']) {
+ echo "IP unblocked successfully";
+}
+?>
+```
+
+### WHP Control Panel Integration
+
+To add IP blocking management to the WHP control panel:
+
+1. **Create a management interface page** (`/admin/ip-blocking.php`):
+
+```php
+blockIP(
+ $ip,
+ $_POST['reason'] ?? '',
+ $_SESSION['admin_username'] ?? 'WHP Admin'
+ );
+ $message = $result['success']
+ ? "IP {$ip} has been blocked"
+ : "Error: " . $result['error'];
+ }
+ break;
+
+ case 'unblock':
+ $ip = filter_var($_POST['ip_address'], FILTER_VALIDATE_IP);
+ if ($ip) {
+ $result = $haproxyBlocker->unblockIP($ip);
+ $message = $result['success']
+ ? "IP {$ip} has been unblocked"
+ : "Error: " . $result['error'];
+ }
+ break;
+ }
+ }
+}
+
+// Get current blocked IPs
+$blockedIPs = $haproxyBlocker->getBlockedIPs();
+?>
+
+
+
+
+ IP Blocking Management - WHP
+
+
+ IP Blocking Management
+
+
+ = htmlspecialchars($message) ?>
+
+
+
+ Block an IP Address
+
+
+
+ Currently Blocked IPs
+
+
+
+ IP Address |
+ Reason |
+ Blocked By |
+ Blocked At |
+ Action |
+
+
+
+
+
+
+ = htmlspecialchars($ip['ip_address']) ?> |
+ = htmlspecialchars($ip['reason']) ?> |
+ = htmlspecialchars($ip['blocked_by']) ?> |
+ = htmlspecialchars($ip['blocked_at']) ?> |
+
+
+ |
+
+
+
+
+
+
+
+```
+
+2. **Environment Configuration**
+
+Add these environment variables to your WHP configuration:
+
+```bash
+# HAProxy Manager API Configuration
+HAPROXY_MANAGER_URL=http://haproxy-manager:8000
+HAPROXY_API_KEY=your-secure-api-key-here
+```
+
+3. **Automatic Blocking Integration**
+
+You can automatically block IPs based on certain criteria:
+
+```php
+// Example: Auto-block after multiple failed login attempts
+function handleFailedLogin($username, $ipAddress) {
+ global $haproxyBlocker;
+
+ // Track failed attempts (implement your own logic)
+ $failedAttempts = getFailedAttempts($ipAddress);
+
+ if ($failedAttempts >= 5) {
+ $haproxyBlocker->blockIP(
+ $ipAddress,
+ "5+ failed login attempts for user: {$username}",
+ "WHP Security System"
+ );
+
+ // Log the blocking action
+ error_log("Auto-blocked IP {$ipAddress} due to multiple failed login attempts");
+ }
+}
+```
+
+## How It Works
+
+1. **Database Storage**: Blocked IPs are stored in the SQLite database table `blocked_ips`
+2. **HAProxy Configuration**: When an IP is blocked/unblocked, the HAProxy configuration is regenerated
+3. **ACL Rules**: HAProxy uses ACL rules to check if a source IP is in the blocked list
+4. **Blocked Page**: Blocked IPs are served a custom "Access Denied" page via the default backend
+
+## Testing
+
+To test the IP blocking functionality:
+
+```bash
+# Block your test IP
+curl -X POST http://localhost:8000/api/blocked-ips \
+ -H "Authorization: Bearer your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"ip_address": "YOUR_TEST_IP", "reason": "Testing"}'
+
+# Try to access a site (you should see the blocked page)
+curl -H "X-Forwarded-For: YOUR_TEST_IP" http://localhost
+
+# Unblock the IP
+curl -X DELETE http://localhost:8000/api/blocked-ips \
+ -H "Authorization: Bearer your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"ip_address": "YOUR_TEST_IP"}'
+```
+
+## Notes
+
+- IP blocks are applied globally to all domains managed by HAProxy
+- The blocked IP page is served with HTTP 403 Forbidden status
+- Blocked IPs are persistent across HAProxy restarts (stored in database)
+- HAProxy configuration is automatically regenerated when IPs are blocked/unblocked
+- Consider implementing rate limiting on the API endpoints to prevent abuse
\ No newline at end of file
diff --git a/haproxy_manager.py b/haproxy_manager.py
index 21680df..1e2b65a 100644
--- a/haproxy_manager.py
+++ b/haproxy_manager.py
@@ -100,6 +100,17 @@ def init_db():
FOREIGN KEY (backend_id) REFERENCES backends (id)
)
''')
+
+ # Create blocked_ips table
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS blocked_ips (
+ id INTEGER PRIMARY KEY,
+ ip_address TEXT UNIQUE NOT NULL,
+ reason TEXT,
+ blocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ blocked_by TEXT
+ )
+ ''')
conn.commit()
def certbot_register():
@@ -699,6 +710,86 @@ def remove_domain():
log_operation('remove_domain', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500
+@app.route('/api/blocked-ips', methods=['GET'])
+@require_api_key
+def get_blocked_ips():
+ """Get all blocked IP addresses"""
+ try:
+ with sqlite3.connect(DB_FILE) as conn:
+ conn.row_factory = sqlite3.Row
+ cursor = conn.cursor()
+ cursor.execute('SELECT * FROM blocked_ips ORDER BY blocked_at DESC')
+ blocked_ips = [dict(row) for row in cursor.fetchall()]
+ log_operation('get_blocked_ips', True)
+ return jsonify(blocked_ips)
+ except Exception as e:
+ log_operation('get_blocked_ips', False, str(e))
+ return jsonify({'status': 'error', 'message': str(e)}), 500
+
+@app.route('/api/blocked-ips', methods=['POST'])
+@require_api_key
+def add_blocked_ip():
+ """Add an IP address to the blocked list"""
+ data = request.get_json()
+ ip_address = data.get('ip_address')
+ reason = data.get('reason', 'No reason provided')
+ blocked_by = data.get('blocked_by', 'API')
+
+ if not ip_address:
+ log_operation('add_blocked_ip', False, 'IP address is required')
+ return jsonify({'status': 'error', 'message': 'IP address is required'}), 400
+
+ try:
+ with sqlite3.connect(DB_FILE) as conn:
+ cursor = conn.cursor()
+ cursor.execute('INSERT INTO blocked_ips (ip_address, reason, blocked_by) VALUES (?, ?, ?)',
+ (ip_address, reason, blocked_by))
+ blocked_ip_id = cursor.lastrowid
+
+ # Regenerate HAProxy config to apply the block
+ generate_config()
+
+ log_operation('add_blocked_ip', True, f'IP {ip_address} blocked successfully')
+ return jsonify({'status': 'success', 'blocked_ip_id': blocked_ip_id, 'message': f'IP {ip_address} has been blocked'})
+ except sqlite3.IntegrityError:
+ log_operation('add_blocked_ip', False, f'IP {ip_address} is already blocked')
+ return jsonify({'status': 'error', 'message': 'IP address is already blocked'}), 409
+ except Exception as e:
+ log_operation('add_blocked_ip', False, str(e))
+ return jsonify({'status': 'error', 'message': str(e)}), 500
+
+@app.route('/api/blocked-ips', methods=['DELETE'])
+@require_api_key
+def remove_blocked_ip():
+ """Remove an IP address from the blocked list"""
+ data = request.get_json()
+ ip_address = data.get('ip_address')
+
+ if not ip_address:
+ log_operation('remove_blocked_ip', False, 'IP address is required')
+ return jsonify({'status': 'error', 'message': 'IP address is required'}), 400
+
+ try:
+ with sqlite3.connect(DB_FILE) as conn:
+ cursor = conn.cursor()
+ cursor.execute('SELECT id FROM blocked_ips WHERE ip_address = ?', (ip_address,))
+ ip_result = cursor.fetchone()
+
+ if not ip_result:
+ log_operation('remove_blocked_ip', False, f'IP {ip_address} not found in blocked list')
+ return jsonify({'status': 'error', 'message': 'IP address not found in blocked list'}), 404
+
+ cursor.execute('DELETE FROM blocked_ips WHERE ip_address = ?', (ip_address,))
+
+ # Regenerate HAProxy config to remove the block
+ generate_config()
+
+ log_operation('remove_blocked_ip', True, f'IP {ip_address} unblocked successfully')
+ return jsonify({'status': 'success', 'message': f'IP {ip_address} has been unblocked'})
+ except Exception as e:
+ log_operation('remove_blocked_ip', False, str(e))
+ return jsonify({'status': 'error', 'message': str(e)}), 500
+
def generate_config():
try:
conn = sqlite3.connect(DB_FILE)
@@ -722,6 +813,11 @@ def generate_config():
# Fetch and immediately convert to list of dicts to avoid any cursor issues
domains = [dict(domain) for domain in cursor.fetchall()]
+
+ # Get blocked IPs
+ cursor.execute('SELECT ip_address FROM blocked_ips')
+ blocked_ips = [row[0] for row in cursor.fetchall()]
+
config_parts = []
# Add Haproxy Default Headers
@@ -730,7 +826,8 @@ def generate_config():
# Add Listener Block
listener_block = template_env.get_template('hap_listener.tpl').render(
- crt_path = SSL_CERTS_DIR
+ crt_path = SSL_CERTS_DIR,
+ blocked_ips = blocked_ips
)
config_parts.append(listener_block)
@@ -973,6 +1070,11 @@ if __name__ == '__main__':
secondary_message=os.environ.get('HAPROXY_DEFAULT_SECONDARY_MESSAGE', 'If you believe this is an error, please check the domain name and try again.')
)
+ @default_app.route('/blocked-ip')
+ def blocked_ip_page():
+ """Serve the blocked IP page for blocked clients"""
+ return render_template('blocked_ip_page.html')
+
default_app.run(host='0.0.0.0', port=8080)
# Start the default page server in a separate thread
diff --git a/scripts/test-ip-blocking.sh b/scripts/test-ip-blocking.sh
new file mode 100755
index 0000000..0dfc7da
--- /dev/null
+++ b/scripts/test-ip-blocking.sh
@@ -0,0 +1,184 @@
+#!/bin/bash
+
+# HAProxy Manager IP Blocking Test Script
+# This script tests the IP blocking functionality
+
+BASE_URL="http://localhost:8000"
+API_KEY="${HAPROXY_API_KEY:-}"
+TEST_IP="192.168.100.50"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Function to print colored output
+print_status() {
+ local status=$1
+ local message=$2
+
+ if [ "$status" = "PASS" ]; then
+ echo -e "${GREEN}✓ PASS${NC}: $message"
+ elif [ "$status" = "FAIL" ]; then
+ echo -e "${RED}✗ FAIL${NC}: $message"
+ else
+ echo -e "${YELLOW}? INFO${NC}: $message"
+ fi
+}
+
+# Function to make API request
+api_request() {
+ local method=$1
+ local endpoint=$2
+ local data=$3
+
+ local headers=""
+ if [ -n "$API_KEY" ]; then
+ headers="-H \"Authorization: Bearer $API_KEY\""
+ fi
+
+ if [ -n "$data" ]; then
+ headers="$headers -H \"Content-Type: application/json\" -d '$data'"
+ fi
+
+ eval "curl -s -w '\n%{http_code}' $headers -X $method $BASE_URL$endpoint"
+}
+
+echo "HAProxy Manager IP Blocking Test Suite"
+echo "======================================"
+echo "Base URL: $BASE_URL"
+echo "API Key: ${API_KEY:-"Not configured"}"
+echo "Test IP: $TEST_IP"
+echo ""
+
+# Test 1: Get current blocked IPs
+print_status "INFO" "Testing GET /api/blocked-ips endpoint..."
+response=$(api_request "GET" "/api/blocked-ips")
+http_code=$(echo "$response" | tail -n 1)
+body=$(echo "$response" | head -n -1)
+
+if [ "$http_code" = "200" ] || [ "$http_code" = "401" ]; then
+ print_status "PASS" "Get blocked IPs endpoint working (status: $http_code)"
+ echo "Current blocked IPs: $body"
+else
+ print_status "FAIL" "Get blocked IPs failed with status $http_code"
+fi
+
+echo ""
+
+# Test 2: Block an IP
+print_status "INFO" "Testing POST /api/blocked-ips endpoint..."
+block_data='{
+ "ip_address": "'$TEST_IP'",
+ "reason": "Test blocking from script",
+ "blocked_by": "Test Script"
+}'
+
+response=$(api_request "POST" "/api/blocked-ips" "$block_data")
+http_code=$(echo "$response" | tail -n 1)
+body=$(echo "$response" | head -n -1)
+
+if [ "$http_code" = "200" ] || [ "$http_code" = "201" ]; then
+ print_status "PASS" "Block IP endpoint working - IP $TEST_IP blocked"
+ echo "Response: $body"
+elif [ "$http_code" = "409" ]; then
+ print_status "INFO" "IP $TEST_IP is already blocked"
+elif [ "$http_code" = "401" ]; then
+ print_status "FAIL" "Authentication required (check API key)"
+else
+ print_status "FAIL" "Block IP failed with status $http_code"
+ echo "Response: $body"
+fi
+
+echo ""
+
+# Test 3: Try to block same IP again (should get 409)
+print_status "INFO" "Testing duplicate block (should fail)..."
+response=$(api_request "POST" "/api/blocked-ips" "$block_data")
+http_code=$(echo "$response" | tail -n 1)
+
+if [ "$http_code" = "409" ]; then
+ print_status "PASS" "Duplicate block correctly rejected with 409"
+else
+ print_status "FAIL" "Unexpected status $http_code for duplicate block"
+fi
+
+echo ""
+
+# Test 4: Get blocked IPs to verify our IP is there
+print_status "INFO" "Verifying IP is in blocked list..."
+response=$(api_request "GET" "/api/blocked-ips")
+body=$(echo "$response" | head -n -1)
+
+if echo "$body" | grep -q "$TEST_IP"; then
+ print_status "PASS" "IP $TEST_IP found in blocked list"
+else
+ print_status "FAIL" "IP $TEST_IP not found in blocked list"
+fi
+
+echo ""
+
+# Test 5: Unblock the IP
+print_status "INFO" "Testing DELETE /api/blocked-ips endpoint..."
+unblock_data='{"ip_address": "'$TEST_IP'"}'
+
+response=$(api_request "DELETE" "/api/blocked-ips" "$unblock_data")
+http_code=$(echo "$response" | tail -n 1)
+body=$(echo "$response" | head -n -1)
+
+if [ "$http_code" = "200" ]; then
+ print_status "PASS" "Unblock IP endpoint working - IP $TEST_IP unblocked"
+ echo "Response: $body"
+elif [ "$http_code" = "404" ]; then
+ print_status "INFO" "IP $TEST_IP was not in blocked list"
+elif [ "$http_code" = "401" ]; then
+ print_status "FAIL" "Authentication required (check API key)"
+else
+ print_status "FAIL" "Unblock IP failed with status $http_code"
+fi
+
+echo ""
+
+# Test 6: Try to unblock non-existent IP (should get 404)
+print_status "INFO" "Testing unblock of non-existent IP..."
+fake_data='{"ip_address": "1.2.3.4"}'
+response=$(api_request "DELETE" "/api/blocked-ips" "$fake_data")
+http_code=$(echo "$response" | tail -n 1)
+
+if [ "$http_code" = "404" ]; then
+ print_status "PASS" "Non-existent IP correctly returned 404"
+else
+ print_status "FAIL" "Unexpected status $http_code for non-existent IP"
+fi
+
+echo ""
+
+# Test 7: Test missing IP address in request
+print_status "INFO" "Testing requests with missing IP address..."
+invalid_data='{}'
+
+response=$(api_request "POST" "/api/blocked-ips" "$invalid_data")
+http_code=$(echo "$response" | tail -n 1)
+if [ "$http_code" = "400" ]; then
+ print_status "PASS" "Block request with missing IP correctly returned 400"
+else
+ print_status "FAIL" "Unexpected status $http_code for missing IP in block request"
+fi
+
+response=$(api_request "DELETE" "/api/blocked-ips" "$invalid_data")
+http_code=$(echo "$response" | tail -n 1)
+if [ "$http_code" = "400" ]; then
+ print_status "PASS" "Unblock request with missing IP correctly returned 400"
+else
+ print_status "FAIL" "Unexpected status $http_code for missing IP in unblock request"
+fi
+
+echo ""
+echo "======================================"
+echo "IP Blocking tests completed"
+echo ""
+echo "To manually test the blocked page:"
+echo "1. Block an IP: curl -X POST $BASE_URL/api/blocked-ips -H 'Authorization: Bearer YOUR_KEY' -H 'Content-Type: application/json' -d '{\"ip_address\": \"YOUR_IP\"}'"
+echo "2. Access any domain through HAProxy from that IP"
+echo "3. You should see the 'Access Denied' page"
\ No newline at end of file
diff --git a/templates/blocked_ip_page.html b/templates/blocked_ip_page.html
new file mode 100644
index 0000000..4dcae79
--- /dev/null
+++ b/templates/blocked_ip_page.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+ Access Denied
+
+
+
+
+
🚫
+
Access Denied
+
Your IP address has been blocked from accessing this website.
+
If you believe this block has been made in error, please contact our support team for assistance.
+
+
+ Error Code: 403 - Forbidden
+
+
+
+ Your IP:
+ Time:
+ Domain:
+
+
+
Contact Support
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hap_listener.tpl b/templates/hap_listener.tpl
index b9b842e..6860cf2 100644
--- a/templates/hap_listener.tpl
+++ b/templates/hap_listener.tpl
@@ -3,3 +3,12 @@ frontend web
bind 0.0.0.0:80
# crt can now be a path, so it will load all .pem files in the path
bind 0.0.0.0:443 ssl crt {{ crt_path }} alpn h2,http/1.1
+
+ {% if blocked_ips %}
+ # IP blocking - single ACL with all blocked IPs
+ acl is_blocked src{% for blocked_ip in blocked_ips %} {{ blocked_ip }}{% endfor %}
+
+ # If IP is blocked, set path to blocked page and use default backend
+ http-request set-path /blocked-ip if is_blocked
+ use_backend default-backend if is_blocked
+ {% endif %}