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)
|
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()
|
conn.commit()
|
||||||
|
|
||||||
def certbot_register():
|
def certbot_register():
|
||||||
@@ -699,6 +710,86 @@ def remove_domain():
|
|||||||
log_operation('remove_domain', False, str(e))
|
log_operation('remove_domain', False, str(e))
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
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():
|
def generate_config():
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(DB_FILE)
|
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
|
# Fetch and immediately convert to list of dicts to avoid any cursor issues
|
||||||
domains = [dict(domain) for domain in cursor.fetchall()]
|
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 = []
|
config_parts = []
|
||||||
|
|
||||||
# Add Haproxy Default Headers
|
# Add Haproxy Default Headers
|
||||||
@@ -730,7 +826,8 @@ def generate_config():
|
|||||||
|
|
||||||
# Add Listener Block
|
# Add Listener Block
|
||||||
listener_block = template_env.get_template('hap_listener.tpl').render(
|
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)
|
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.')
|
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)
|
default_app.run(host='0.0.0.0', port=8080)
|
||||||
|
|
||||||
# Start the default page server in a separate thread
|
# 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
|
bind 0.0.0.0:80
|
||||||
# crt can now be a path, so it will load all .pem files in the path
|
# 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
|
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