From ca37a6825517bb362fb9b2f80421eb95446efd3e Mon Sep 17 00:00:00 2001 From: jknapp Date: Thu, 21 Aug 2025 18:32:47 -0700 Subject: [PATCH] Add IP blocking functionality to HAProxy Manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add blocked_ips database table to store blocked IP addresses - Implement API endpoints for IP blocking management: - GET /api/blocked-ips: List all blocked IPs - POST /api/blocked-ips: Block an IP address - DELETE /api/blocked-ips: Unblock an IP address - Update HAProxy configuration generation to include blocked IP ACLs - Create blocked IP page template for denied access - Add comprehensive API documentation for WHP integration - Include test script for IP blocking functionality - Update .gitignore with Python patterns - Add CLAUDE.md for codebase documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 39 +++ CLAUDE.md | 81 +++++++ IP_BLOCKING_API.md | 422 +++++++++++++++++++++++++++++++++ haproxy_manager.py | 104 +++++++- scripts/test-ip-blocking.sh | 184 ++++++++++++++ templates/blocked_ip_page.html | 116 +++++++++ templates/hap_listener.tpl | 9 + 7 files changed, 954 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 IP_BLOCKING_API.md create mode 100755 scripts/test-ip-blocking.sh create mode 100644 templates/blocked_ip_page.html 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

+ + +
+ + + +

Block an IP Address

+
+ +
+
+ +
+ + +

Currently Blocked IPs

+ + + + + + + + + + + + + + + + + + + + + + + +
IP AddressReasonBlocked ByBlocked AtAction
+
+ + + +
+
+ + +``` + +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 %}