Add IP blocking functionality to HAProxy Manager
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 1m1s

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-08-21 18:32:47 -07:00
parent a7ce40f600
commit ca37a68255
7 changed files with 954 additions and 1 deletions

39
.gitignore vendored Normal file
View File

@@ -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

81
CLAUDE.md Normal file
View File

@@ -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/`

422
IP_BLOCKING_API.md Normal file
View File

@@ -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
<?php
class HAProxyIPBlocker {
private $apiUrl;
private $apiKey;
public function __construct($apiUrl, $apiKey) {
$this->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
<?php
// Initialize the HAProxy IP Blocker
$haproxyBlocker = new HAProxyIPBlocker(
getenv('HAPROXY_MANAGER_URL') ?: 'http://haproxy-manager:8000',
getenv('HAPROXY_API_KEY')
);
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action'])) {
switch ($_POST['action']) {
case 'block':
$ip = filter_var($_POST['ip_address'], FILTER_VALIDATE_IP);
if ($ip) {
$result = $haproxyBlocker->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();
?>
<!DOCTYPE html>
<html>
<head>
<title>IP Blocking Management - WHP</title>
</head>
<body>
<h1>IP Blocking Management</h1>
<?php if (isset($message)): ?>
<div class="alert"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<!-- Block IP Form -->
<h2>Block an IP Address</h2>
<form method="POST">
<input type="hidden" name="action" value="block">
<label>IP Address: <input type="text" name="ip_address" required pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"></label><br>
<label>Reason: <input type="text" name="reason" size="50"></label><br>
<button type="submit">Block IP</button>
</form>
<!-- Currently Blocked IPs -->
<h2>Currently Blocked IPs</h2>
<table border="1">
<thead>
<tr>
<th>IP Address</th>
<th>Reason</th>
<th>Blocked By</th>
<th>Blocked At</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if ($blockedIPs['success']): ?>
<?php foreach ($blockedIPs['data'] as $ip): ?>
<tr>
<td><?= htmlspecialchars($ip['ip_address']) ?></td>
<td><?= htmlspecialchars($ip['reason']) ?></td>
<td><?= htmlspecialchars($ip['blocked_by']) ?></td>
<td><?= htmlspecialchars($ip['blocked_at']) ?></td>
<td>
<form method="POST" style="display:inline">
<input type="hidden" name="action" value="unblock">
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
<button type="submit">Unblock</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</body>
</html>
```
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

View File

@@ -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

184
scripts/test-ip-blocking.sh Executable file
View File

@@ -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"

View File

@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Access Denied</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
text-align: center;
padding: 50px 20px;
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
max-width: 600px;
width: 100%;
}
.icon {
font-size: 64px;
margin-bottom: 20px;
display: block;
}
h1 {
color: #e74c3c;
margin-bottom: 20px;
font-size: 2.2em;
font-weight: 600;
}
p {
color: #555;
line-height: 1.7;
margin-bottom: 15px;
font-size: 1.1em;
}
.contact {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
padding: 12px 24px;
border-radius: 6px;
text-decoration: none;
display: inline-block;
margin-top: 25px;
font-weight: 500;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.contact:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
}
.ip-info {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 15px;
margin: 20px 0;
font-family: 'Courier New', monospace;
color: #495057;
}
.error-code {
background: #ffebee;
border: 1px solid #ffcdd2;
border-radius: 6px;
padding: 10px;
margin: 20px 0;
color: #c62828;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<span class="icon">🚫</span>
<h1>Access Denied</h1>
<p>Your IP address has been blocked from accessing this website.</p>
<p>If you believe this block has been made in error, please contact our support team for assistance.</p>
<div class="error-code">
Error Code: 403 - Forbidden
</div>
<div class="ip-info">
<strong>Your IP:</strong> <span id="client-ip"></span><br>
<strong>Time:</strong> <span id="timestamp"></span><br>
<strong>Domain:</strong> <span id="domain"></span>
</div>
<a href="mailto:support@example.com" class="contact">Contact Support</a>
</div>
<script>
// Display the current domain and timestamp
document.getElementById('domain').textContent = window.location.hostname;
document.getElementById('timestamp').textContent = new Date().toLocaleString();
// Attempt to get client IP (this will show the proxy IP in most cases)
// For actual client IP, this would need to be injected by the server
document.getElementById('client-ip').textContent = 'Hidden for privacy';
// You could also make an AJAX call to get the real client IP if needed
// fetch('/api/my-ip').then(r => r.json()).then(data => {
// document.getElementById('client-ip').textContent = data.ip;
// }).catch(() => {
// document.getElementById('client-ip').textContent = 'Unable to determine';
// });
</script>
</body>
</html>

View File

@@ -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 %}