Add IP blocking functionality to HAProxy Manager
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 1m1s
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:
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal 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
81
CLAUDE.md
Normal 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
422
IP_BLOCKING_API.md
Normal 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
|
@@ -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
184
scripts/test-ip-blocking.sh
Executable 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"
|
116
templates/blocked_ip_page.html
Normal file
116
templates/blocked_ip_page.html
Normal 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>
|
@@ -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 %}
|
||||
|
Reference in New Issue
Block a user