Not fully working, but saving progress
This commit is contained in:
parent
f222b6e79a
commit
9c52edd53a
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM python:3.12-slim
|
||||
RUN apt update -y && apt dist-upgrade -y && apt install socat haproxy -y && apt clean && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /haproxy
|
||||
COPY ./templates /haproxy/templates
|
||||
COPY requirements.txt /haproxy/
|
||||
COPY haproxy_manager.py /haproxy/
|
||||
RUN pip install -r requirements.txt
|
||||
EXPOSE 80 443 8000
|
||||
#CMD ["python", "app.py"]
|
@ -1,3 +1,2 @@
|
||||
# haproxy-manager-base
|
||||
# HAProxy Manager Base
|
||||
|
||||
Base code for HAProxy Web Manager
|
@ -1,59 +0,0 @@
|
||||
# Multi-stage build
|
||||
FROM node:14-alpine as frontend-builder
|
||||
WORKDIR /app/frontend
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm install
|
||||
COPY frontend/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM python:3.8-slim
|
||||
|
||||
# Install HAProxy and Certbot
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
haproxy \
|
||||
certbot \
|
||||
python3-certbot \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /etc/haproxy/certs \
|
||||
&& mkdir -p /var/lib/haproxy \
|
||||
&& mkdir -p /run/haproxy
|
||||
|
||||
# Set up Python environment
|
||||
WORKDIR /app
|
||||
COPY backend/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy backend code
|
||||
COPY backend/ ./backend/
|
||||
|
||||
# Copy frontend build
|
||||
COPY --from=frontend-builder /app/frontend/build ./frontend/build
|
||||
|
||||
# Copy HAProxy configuration
|
||||
COPY backend/templates/haproxy.cfg.j2 /etc/haproxy/haproxy.cfg.template
|
||||
|
||||
# Install curl for healthcheck
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /app/backend/data
|
||||
|
||||
# Set permissions
|
||||
RUN chown -R nobody:nogroup /app/backend/data
|
||||
|
||||
# Add healthcheck
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:5000/health || exit 1
|
||||
|
||||
# Create run script
|
||||
RUN echo '#!/bin/sh\n\
|
||||
python backend/app.py &\n\
|
||||
haproxy -f /etc/haproxy/haproxy.cfg -db\n' > /start.sh && \
|
||||
chmod +x /start.sh
|
||||
|
||||
EXPOSE 80 443 5000
|
||||
|
||||
CMD ["/start.sh"]
|
@ -1,32 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
haproxy-manager:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- haproxy-certs:/etc/haproxy/certs
|
||||
- letsencrypt:/etc/letsencrypt
|
||||
- sqlite-data:/app/backend/data
|
||||
environment:
|
||||
- FLASK_ENV=production
|
||||
- SECRET_KEY=changeme
|
||||
- DATABASE_URL=sqlite:///data/haproxy-manager.db
|
||||
- JWT_SECRET_KEY=change-this-in-production
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
haproxy-certs:
|
||||
letsencrypt:
|
||||
sqlite-data:
|
280
haproxy_manager.py
Normal file
280
haproxy_manager.py
Normal file
@ -0,0 +1,280 @@
|
||||
import sqlite3
|
||||
import os
|
||||
from flask import Flask, request, jsonify
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import jinja2
|
||||
import socket
|
||||
import shutil
|
||||
import psutil
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
DB_FILE = 'haproxy_config.db'
|
||||
TEMPLATE_DIR = Path('templates')
|
||||
HAPROXY_CONFIG_PATH = '/etc/haproxy/haproxy.cfg'
|
||||
SSL_CERTS_DIR = '/etc/haproxy/certs'
|
||||
|
||||
def init_db():
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create domains table
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS domains (
|
||||
id INTEGER PRIMARY KEY,
|
||||
domain TEXT UNIQUE NOT NULL,
|
||||
ssl_enabled BOOLEAN DEFAULT 0,
|
||||
ssl_cert_path TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
# Create backends table
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS backends (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
domain_id INTEGER,
|
||||
settings TEXT,
|
||||
FOREIGN KEY (domain_id) REFERENCES domains (id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Create backend_servers table
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS backend_servers (
|
||||
id INTEGER PRIMARY KEY,
|
||||
backend_id INTEGER,
|
||||
server_name TEXT NOT NULL,
|
||||
server_address TEXT NOT NULL,
|
||||
server_port INTEGER NOT NULL,
|
||||
server_options TEXT,
|
||||
FOREIGN KEY (backend_id) REFERENCES backends (id)
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
|
||||
def generate_self_signed_cert(ssl_certs_dir):
|
||||
"""Generate a self-signed certificate for a domain."""
|
||||
self_sign_cert = os.path.join(ssl_certs_dir, "default_self_signed_cert.pem")
|
||||
print(self_sign_cert)
|
||||
if os.path.exists(self_sign_cert):
|
||||
print("Self Signed Cert Found")
|
||||
return True
|
||||
try:
|
||||
os.mkdir(ssl_certs_dir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
DOMAIN = socket.gethostname()
|
||||
# Generate private key and certificate
|
||||
subprocess.run([
|
||||
'openssl', 'req', '-x509', '-newkey', 'rsa:4096',
|
||||
'-keyout', '/tmp/key.pem',
|
||||
'-out', '/tmp/cert.pem',
|
||||
'-days', '3650',
|
||||
'-nodes', # No passphrase
|
||||
'-subj', f'/CN={DOMAIN}'
|
||||
], check=True)
|
||||
|
||||
# Combine cert and key for HAProxy
|
||||
with open(self_sign_cert, 'wb') as combined:
|
||||
for file in ['/tmp/cert.pem', '/tmp/key.pem']:
|
||||
with open(file, 'rb') as f:
|
||||
combined.write(f.read())
|
||||
os.remove(file) # Clean up temporary files
|
||||
generate_config()
|
||||
return True
|
||||
|
||||
def is_process_running(process_name):
|
||||
for process in psutil.process_iter(['name']):
|
||||
if process.info['name'] == process_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Initialize template engine
|
||||
template_loader = jinja2.FileSystemLoader(TEMPLATE_DIR)
|
||||
template_env = jinja2.Environment(loader=template_loader)
|
||||
|
||||
@app.route('/api/domain', methods=['POST'])
|
||||
def add_domain():
|
||||
data = request.get_json()
|
||||
domain = data.get('domain')
|
||||
backend_name = data.get('backend_name')
|
||||
servers = data.get('servers', [])
|
||||
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Add domain
|
||||
cursor.execute('INSERT INTO domains (domain) VALUES (?)', (domain,))
|
||||
domain_id = cursor.lastrowid
|
||||
|
||||
# Add backend
|
||||
cursor.execute('INSERT INTO backends (name, domain_id) VALUES (?, ?)',
|
||||
(backend_name, domain_id))
|
||||
backend_id = cursor.lastrowid
|
||||
|
||||
# Add servers
|
||||
for server in servers:
|
||||
cursor.execute('''
|
||||
INSERT INTO backend_servers
|
||||
(backend_id, server_name, server_address, server_port, server_options)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (backend_id, server['name'], server['address'],
|
||||
server['port'], server.get('options')))
|
||||
# Close cursor and connection
|
||||
cursor.close()
|
||||
conn.close()
|
||||
generate_config()
|
||||
return jsonify({'status': 'success', 'domain_id': domain_id})
|
||||
|
||||
@app.route('/api/ssl', methods=['POST'])
|
||||
def request_ssl():
|
||||
data = request.get_json()
|
||||
domain = data.get('domain')
|
||||
|
||||
# Request Let's Encrypt certificate
|
||||
result = subprocess.run([
|
||||
'certbot', 'certonly', '--standalone',
|
||||
'--preferred-challenges', 'http',
|
||||
'-d', domain, '--non-interactive --http-01-port=8688'
|
||||
])
|
||||
|
||||
if result.returncode == 0:
|
||||
# Combine cert files and copy to HAProxy certs directory
|
||||
cert_path = f'/etc/letsencrypt/live/{domain}/fullchain.pem'
|
||||
key_path = f'/etc/letsencrypt/live/{domain}/privkey.pem'
|
||||
combined_path = f'{SSL_CERTS_DIR}/{domain}.pem'
|
||||
|
||||
with open(combined_path, 'w') as combined:
|
||||
subprocess.run(['cat', cert_path, key_path], stdout=combined)
|
||||
|
||||
# Update database
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
UPDATE domains
|
||||
SET ssl_enabled = 1, ssl_cert_path = ?
|
||||
WHERE domain = ?
|
||||
''', (combined_path, domain))
|
||||
# Close cursor and connection
|
||||
cursor.close()
|
||||
conn.close()
|
||||
generate_config()
|
||||
return jsonify({'status': 'success'})
|
||||
return jsonify({'status': 'error', 'message': 'Failed to obtain SSL certificate'})
|
||||
|
||||
def generate_config():
|
||||
try:
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
# Enable dictionary-like access to rows
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = '''
|
||||
SELECT
|
||||
d.id as domain_id,
|
||||
d.domain,
|
||||
d.ssl_enabled,
|
||||
d.ssl_cert_path,
|
||||
b.id as backend_id,
|
||||
b.name as backend_name
|
||||
FROM domains d
|
||||
LEFT JOIN backends b ON d.id = b.domain_id
|
||||
'''
|
||||
cursor.execute(query)
|
||||
|
||||
# Fetch and immediately convert to list of dicts to avoid any cursor issues
|
||||
domains = [dict(domain) for domain in cursor.fetchall()]
|
||||
config_parts = []
|
||||
|
||||
# Add Haproxy Default Headers
|
||||
default_headers = template_env.get_template('hap_header.tpl').render()
|
||||
config_parts.append(default_headers)
|
||||
|
||||
# Add Listener Block
|
||||
listener_block = template_env.get_template('hap_listener.tpl').render(
|
||||
crt_path = SSL_CERTS_DIR
|
||||
)
|
||||
config_parts.append(listener_block)
|
||||
|
||||
# Add Let's Encrypt
|
||||
letsencrypt_acl = template_env.get_template('hap_letsencrypt.tpl').render()
|
||||
config_parts.append(letsencrypt_acl)
|
||||
|
||||
# Add domain configurations
|
||||
for domain in domains:
|
||||
if not domain['backend_name']:
|
||||
print(f"Skipping domain {domain['domain']} - no backend name") # Debug log
|
||||
continue
|
||||
|
||||
# Add domain ACL
|
||||
try:
|
||||
domain_acl = template_env.get_template('hap_subdomain_acl.tpl').render(
|
||||
domain=domain['domain'],
|
||||
name=domain['backend_name']
|
||||
)
|
||||
config_parts.append(domain_acl)
|
||||
print(f"Added ACL for domain: {domain['domain']}") # Debug log
|
||||
except Exception as e:
|
||||
print(f"Error generating domain ACL for {domain['domain']}: {e}")
|
||||
continue
|
||||
|
||||
# Add backend configuration
|
||||
try:
|
||||
cursor.execute('''
|
||||
SELECT * FROM backend_servers WHERE backend_id = ?
|
||||
''', (domain['backend_id'],))
|
||||
servers = [dict(server) for server in cursor.fetchall()]
|
||||
|
||||
if not servers:
|
||||
print(f"No servers found for backend {domain['backend_name']}") # Debug log
|
||||
continue
|
||||
|
||||
backend_block = template_env.get_template('hap_backend.tpl').render(
|
||||
name=domain['backend_name'],
|
||||
ssl_enabled=domain['ssl_enabled'],
|
||||
servers=servers
|
||||
)
|
||||
config_parts.append(backend_block)
|
||||
print(f"Added backend block for: {domain['backend_name']}") # Debug log
|
||||
except Exception as e:
|
||||
print(f"Error generating backend block for {domain['backend_name']}: {e}")
|
||||
continue
|
||||
|
||||
# Write complete configuration to tmp
|
||||
config_content = '\n'.join(config_parts)
|
||||
print("Final config content:", config_content) # Debug log
|
||||
|
||||
# Write complete configuration to tmp
|
||||
# Check HAProxy Configuration, and reload if it works
|
||||
with open("/tmp/haproxy_temp.cfg", 'w') as f:
|
||||
f.write('\n'.join(config_parts))
|
||||
result = subprocess.run(['haproxy', '-c', '-f', "/tmp/haproxy_temp.cfg"], capture_output=True)
|
||||
if result.returncode == 0:
|
||||
shutil.copyfile("/tmp/haproxy_temp.cfg", HAPROXY_CONFIG_PATH)
|
||||
os.remove("/tmp/haproxy_temp.cfg")
|
||||
if is_process_running('haproxy'):
|
||||
subprocess.run(['echo', '"reload"', '|', 'socat', 'stdio', '/tmp/haproxy-cli'])
|
||||
else:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['haproxy', '-W', '-S', '/tmp/haproxy-cli,level,admin', '-f', HAPROXY_CONFIG_PATH],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
print("HAProxy started successfully")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to start HAProxy: {e.stdout}\n{e.stderr}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Error generating config: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
generate_self_signed_cert(SSL_CERTS_DIR)
|
||||
app.run(host='0.0.0.0', port=8000)
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
Flask==2.3.3
|
||||
Jinja2==3.1.2
|
||||
psutil
|
5
scripts/start-up.sh
Normal file
5
scripts/start-up.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Exit on error
|
||||
set -eo pipefail
|
||||
|
9
templates/hap_backend.tpl
Normal file
9
templates/hap_backend.tpl
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
backend {{ name }}-backend
|
||||
|
||||
option forwardfor
|
||||
http-request add-header X-CLIENT-IP %[src]
|
||||
{% if ssl_enabled %} ttp-request set-header X-Forwarded-Proto https if \{ ssl_fc \} {% endif %}
|
||||
{% for server in servers %}
|
||||
server {{ server.name }} {{ server.address }}:{{ server.port }} {{ server.options }}
|
||||
{% endfor %}
|
48
templates/hap_header.tpl
Normal file
48
templates/hap_header.tpl
Normal file
@ -0,0 +1,48 @@
|
||||
#---------------------------------------------------------------------
|
||||
# Global settings
|
||||
#---------------------------------------------------------------------
|
||||
global
|
||||
# to have these messages end up in /var/log/haproxy.log you will
|
||||
# need to:
|
||||
#
|
||||
# 1) configure syslog to accept network log events. This is done
|
||||
# by adding the '-r' option to the SYSLOGD_OPTIONS in
|
||||
# /etc/sysconfig/syslog
|
||||
#
|
||||
# 2) configure local2 events to go to the /var/log/haproxy.log
|
||||
# file. A line like the following can be added to
|
||||
# /etc/sysconfig/syslog
|
||||
#
|
||||
# local2.* /var/log/haproxy.log
|
||||
#
|
||||
log 127.0.0.1 local2
|
||||
|
||||
chroot /var/lib/haproxy
|
||||
pidfile /var/run/haproxy.pid
|
||||
maxconn 4000
|
||||
user haproxy
|
||||
group haproxy
|
||||
daemon
|
||||
|
||||
tune.ssl.default-dh-param 2048
|
||||
#---------------------------------------------------------------------
|
||||
# common defaults that all the 'listen' and 'backend' sections will
|
||||
# use if not designated in their block
|
||||
#---------------------------------------------------------------------
|
||||
defaults
|
||||
mode http
|
||||
log global
|
||||
option httplog
|
||||
option dontlognull
|
||||
option http-server-close
|
||||
option forwardfor #except 127.0.0.0/8
|
||||
option redispatch
|
||||
retries 3
|
||||
timeout http-request 300s
|
||||
timeout queue 2m
|
||||
timeout connect 120s
|
||||
timeout client 10m
|
||||
timeout server 10m
|
||||
timeout http-keep-alive 120s
|
||||
timeout check 10s
|
||||
maxconn 3000
|
9
templates/hap_letsencrypt.tpl
Normal file
9
templates/hap_letsencrypt.tpl
Normal file
@ -0,0 +1,9 @@
|
||||
#Let's Encrypt SSL
|
||||
acl letsencrypt-acl path_beg /.well-known/acme-challenge/
|
||||
use_backend letsencrypt-backend if letsencrypt-acl
|
||||
|
||||
|
||||
#Pass SSL Requests to Lets Encrypt
|
||||
backend letsencrypt-backend
|
||||
server letsencrypt 127.0.0.1:8688
|
||||
|
5
templates/hap_listener.tpl
Normal file
5
templates/hap_listener.tpl
Normal file
@ -0,0 +1,5 @@
|
||||
#web
|
||||
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
|
3
templates/hap_path_acl.tpl
Normal file
3
templates/hap_path_acl.tpl
Normal file
@ -0,0 +1,3 @@
|
||||
#Path Method {{ path }}
|
||||
acl {{ path }}-acl path_beg {{ path }}
|
||||
use_backend {{ name }}-backend if {{ path }}-acl
|
4
templates/hap_subdomain_acl.tpl
Normal file
4
templates/hap_subdomain_acl.tpl
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
#Subdomain method {{ domain }}
|
||||
acl {{ domain }}-acl hdr(host) -i {{ domain }}
|
||||
use_backend {{ name }}-backend if {{ domain }}-acl
|
Loading…
x
Reference in New Issue
Block a user