diff --git a/NGINX_SETUP_QUICK.md b/NGINX_SETUP_QUICK.md
new file mode 100644
index 0000000..22e626f
--- /dev/null
+++ b/NGINX_SETUP_QUICK.md
@@ -0,0 +1,84 @@
+# Quick Start: Nginx Setup for DigiServer v2
+
+## Pre-requisites
+- SSL certificates in `./data/nginx-ssl/cert.pem` and `./data/nginx-ssl/key.pem`
+- Docker and Docker Compose installed
+- Port 80 and 443 available
+
+## Quick Setup (3 steps)
+
+### 1. Generate Self-Signed Certificates
+```bash
+./generate_nginx_certs.sh localhost 365
+```
+
+### 2. Update Nginx Configuration
+- Edit `nginx.conf` to set your domain:
+ ```nginx
+ server_name localhost; # Change to your domain
+ ```
+
+### 3. Start Docker Compose
+```bash
+docker-compose up -d
+```
+
+## Verification
+
+### Check if Nginx is running
+```bash
+docker ps | grep nginx
+```
+
+### Test HTTP â HTTPS redirect
+```bash
+curl -L http://localhost
+```
+
+### Test HTTPS (with self-signed cert)
+```bash
+curl -k https://localhost
+```
+
+### View logs
+```bash
+docker logs digiserver-nginx
+docker exec digiserver-nginx tail -f /var/log/nginx/access.log
+```
+
+## Using Production Certificates
+
+### Option A: Let's Encrypt (Free)
+1. Install certbot: `apt-get install certbot`
+2. Generate cert: `certbot certonly --standalone -d your-domain.com`
+3. Copy cert: `cp /etc/letsencrypt/live/your-domain.com/fullchain.pem ./data/nginx-ssl/cert.pem`
+4. Copy key: `cp /etc/letsencrypt/live/your-domain.com/privkey.pem ./data/nginx-ssl/key.pem`
+5. Fix permissions: `sudo chown 101:101 ./data/nginx-ssl/*`
+6. Reload: `docker exec digiserver-nginx nginx -s reload`
+
+### Option B: Commercial Certificate
+1. Place your certificate files in `./data/nginx-ssl/cert.pem` and `./data/nginx-ssl/key.pem`
+2. Fix permissions: `sudo chown 101:101 ./data/nginx-ssl/*`
+3. Reload: `docker exec digiserver-nginx nginx -s reload`
+
+## Troubleshooting
+
+| Issue | Solution |
+|-------|----------|
+| Port 80/443 in use | `sudo netstat -tlnp \| grep :80` or `:443` |
+| Certificate permission denied | `sudo chown 101:101 ./data/nginx-ssl/*` |
+| Nginx won't start | `docker logs digiserver-nginx` |
+| Connection refused | Check firewall: `sudo ufw allow 80/tcp && sudo ufw allow 443/tcp` |
+
+## File Locations
+- Main config: `./nginx.conf`
+- SSL certs: `./data/nginx-ssl/`
+- Logs: `./data/nginx-logs/`
+- Custom domains: `./nginx-custom-domains.conf` (auto-generated)
+
+## Next: Production Setup
+1. Update `.env` with your DOMAIN and EMAIL
+2. Configure HTTPS settings in admin panel
+3. Run: `python nginx_manager.py generate`
+4. Test: `docker exec digiserver-nginx nginx -t`
+5. Reload: `docker exec digiserver-nginx nginx -s reload`
diff --git a/PROXY_FIX_SETUP.md b/PROXY_FIX_SETUP.md
new file mode 100644
index 0000000..659f200
--- /dev/null
+++ b/PROXY_FIX_SETUP.md
@@ -0,0 +1,56 @@
+# ProxyFix Middleware Setup - DigiServer v2
+
+## Overview
+ProxyFix middleware is now properly configured in the Flask app to handle reverse proxy headers from Nginx (or Caddy). This ensures correct handling of:
+- **X-Real-IP**: Client's real IP address
+- **X-Forwarded-For**: List of IPs in the proxy chain
+- **X-Forwarded-Proto**: Original protocol (http/https)
+- **X-Forwarded-Host**: Original hostname
+
+## Configuration Details
+
+### Flask App (app/app.py)
+```python
+from werkzeug.middleware.proxy_fix import ProxyFix
+
+app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
+```
+
+**Parameters:**
+- `x_for=1`: Trust one proxy for X-Forwarded-For header
+- `x_proto=1`: Trust proxy for X-Forwarded-Proto header
+- `x_host=1`: Trust proxy for X-Forwarded-Host header
+- `x_port=1`: Trust proxy for X-Forwarded-Port header
+
+### Config Settings (app/config.py)
+
+```python
+# Reverse proxy trust (for Nginx/Caddy with ProxyFix middleware)
+TRUSTED_PROXIES = os.getenv('TRUSTED_PROXIES', '127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16')
+PREFERRED_URL_SCHEME = os.getenv('PREFERRED_URL_SCHEME', 'https')
+```
+
+## Testing ProxyFix
+
+### 1. Test Real Client IP
+```bash
+docker exec digiserver-app flask shell
+>>> from flask import request
+>>> request.remote_addr # Should show client IP
+```
+
+### 2. Test URL Scheme
+```bash
+docker exec digiserver-app flask shell
+>>> from flask import url_for
+>>> url_for('auth.login', _external=True) # Should use https://
+```
+
+## Verification Checklist
+
+- [x] ProxyFix imported in app.py
+- [x] app.wsgi_app wrapped with ProxyFix
+- [x] TRUSTED_PROXIES configured
+- [x] PREFERRED_URL_SCHEME set to 'https'
+- [x] SESSION_COOKIE_SECURE=True in ProductionConfig
+- [x] Nginx headers configured correctly
diff --git a/app/app.py b/app/app.py
index 42e3e51..6c6120c 100755
--- a/app/app.py
+++ b/app/app.py
@@ -4,6 +4,7 @@ Modern Flask application with blueprint architecture
"""
import os
from flask import Flask, render_template
+from werkzeug.middleware.proxy_fix import ProxyFix
from dotenv import load_dotenv
from app.config import DevelopmentConfig, ProductionConfig, TestingConfig
@@ -37,6 +38,10 @@ def create_app(config_name=None):
app.config.from_object(config)
+ # Apply ProxyFix middleware for reverse proxy (Nginx/Caddy)
+ # This ensures proper handling of X-Forwarded-* headers
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
+
# Initialize extensions
db.init_app(app)
bcrypt.init_app(app)
diff --git a/app/blueprints/admin.py b/app/blueprints/admin.py
index 62fe38a..3898fab 100644
--- a/app/blueprints/admin.py
+++ b/app/blueprints/admin.py
@@ -11,6 +11,7 @@ from app.extensions import db, bcrypt
from app.models import User, Player, Group, Content, ServerLog, Playlist, HTTPSConfig
from app.utils.logger import log_action
from app.utils.caddy_manager import CaddyConfigGenerator
+from app.utils.nginx_config_reader import get_nginx_status
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@@ -870,10 +871,14 @@ def https_config():
db.session.commit()
log_action('info', f'HTTPS status auto-corrected to enabled (detected from request)')
+ # Get Nginx configuration status
+ nginx_status = get_nginx_status()
+
return render_template('admin/https_config.html',
config=config,
is_https_active=is_https_active,
- current_host=current_host)
+ current_host=current_host,
+ nginx_status=nginx_status)
except Exception as e:
log_action('error', f'Error loading HTTPS config page: {str(e)}')
flash('Error loading HTTPS configuration page.', 'danger')
diff --git a/app/config.py b/app/config.py
index bcda914..16c162d 100755
--- a/app/config.py
+++ b/app/config.py
@@ -29,6 +29,11 @@ class Config:
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
+ # Reverse proxy trust (for Nginx/Caddy with ProxyFix middleware)
+ # These are set by werkzeug.middleware.proxy_fix
+ TRUSTED_PROXIES = os.getenv('TRUSTED_PROXIES', '127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16')
+ PREFERRED_URL_SCHEME = os.getenv('PREFERRED_URL_SCHEME', 'https')
+
# Cache
SEND_FILE_MAX_AGE_DEFAULT = 300 # 5 minutes for static files
diff --git a/app/templates/admin/https_config.html b/app/templates/admin/https_config.html
index a3012f1..7eb2b7a 100644
--- a/app/templates/admin/https_config.html
+++ b/app/templates/admin/https_config.html
@@ -160,6 +160,95 @@
+
+
+
đ§ Nginx Reverse Proxy Status
+ {% if nginx_status.available %}
+
+
+ Status:
+ â
Nginx Configured
+
+
+
+ Configuration Path:
+ {{ nginx_status.path }}
+
+
+ {% if nginx_status.ssl_enabled %}
+
+ SSL/TLS:
+ đ Enabled
+
+ {% else %}
+
+ SSL/TLS:
+ â ī¸ Not Configured
+
+ {% endif %}
+
+ {% if nginx_status.http_ports %}
+
+ HTTP Ports:
+ {{ nginx_status.http_ports|join(', ') }}
+
+ {% endif %}
+
+ {% if nginx_status.https_ports %}
+
+ HTTPS Ports:
+ {{ nginx_status.https_ports|join(', ') }}
+
+ {% endif %}
+
+ {% if nginx_status.server_names %}
+
+ Server Names:
+ {% for name in nginx_status.server_names %}
+ {{ name }}{% if not loop.last %}
{% endif %}
+ {% endfor %}
+
+ {% endif %}
+
+ {% if nginx_status.upstream_servers %}
+
+ Upstream Servers:
+ {% for server in nginx_status.upstream_servers %}
+ {{ server }}{% if not loop.last %}
{% endif %}
+ {% endfor %}
+
+ {% endif %}
+
+ {% if nginx_status.ssl_protocols %}
+
+ SSL Protocols:
+ {{ nginx_status.ssl_protocols|join(', ') }}
+
+ {% endif %}
+
+ {% if nginx_status.client_max_body_size %}
+
+ Max Body Size:
+ {{ nginx_status.client_max_body_size }}
+
+ {% endif %}
+
+ {% if nginx_status.gzip_enabled %}
+
+ Gzip Compression:
+ â
Enabled
+
+ {% endif %}
+
+ {% else %}
+
+
â ī¸ Nginx configuration not accessible
+
Error: {{ nginx_status.error|default('Unknown error') }}
+
Path checked: {{ nginx_status.path }}
+
+ {% endif %}
+
+
âšī¸ Important Information
@@ -466,6 +555,45 @@
color: #0066cc;
}
+.nginx-status-card {
+ background: linear-gradient(135deg, #f0f7ff 0%, #e7f3ff 100%);
+ border-left: 5px solid #0066cc;
+ margin-bottom: 30px;
+}
+
+.nginx-status-card h2 {
+ color: #0066cc;
+ margin-top: 0;
+}
+
+.nginx-status-content {
+ padding: 10px 0;
+}
+
+.status-item {
+ padding: 12px;
+ background: white;
+ border-radius: 4px;
+ margin-bottom: 10px;
+ border-left: 3px solid #0066cc;
+ font-size: 14px;
+}
+
+.status-item strong {
+ display: inline-block;
+ min-width: 150px;
+ color: #333;
+}
+
+.status-item code {
+ background: #f0f7ff;
+ padding: 4px 8px;
+ border-radius: 3px;
+ font-family: 'Courier New', monospace;
+ color: #0066cc;
+ word-break: break-all;
+}
+
.info-sections {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
diff --git a/app/utils/nginx_config_reader.py b/app/utils/nginx_config_reader.py
new file mode 100644
index 0000000..b8f4d6c
--- /dev/null
+++ b/app/utils/nginx_config_reader.py
@@ -0,0 +1,120 @@
+"""Nginx configuration reader utility."""
+import os
+import re
+from typing import Dict, List, Optional, Any
+
+
+class NginxConfigReader:
+ """Read and parse Nginx configuration files."""
+
+ def __init__(self, config_path: str = '/etc/nginx/nginx.conf'):
+ """Initialize Nginx config reader."""
+ self.config_path = config_path
+ self.config_content = None
+ self.is_available = os.path.exists(config_path)
+
+ if self.is_available:
+ try:
+ with open(config_path, 'r') as f:
+ self.config_content = f.read()
+ except Exception as e:
+ self.is_available = False
+ self.error = str(e)
+
+ def get_status(self) -> Dict[str, Any]:
+ """Get Nginx configuration status."""
+ if not self.is_available:
+ return {
+ 'available': False,
+ 'error': 'Nginx configuration not found',
+ 'path': self.config_path
+ }
+
+ return {
+ 'available': True,
+ 'path': self.config_path,
+ 'file_exists': os.path.exists(self.config_path),
+ 'ssl_enabled': self._check_ssl_enabled(),
+ 'http_ports': self._extract_http_ports(),
+ 'https_ports': self._extract_https_ports(),
+ 'upstream_servers': self._extract_upstream_servers(),
+ 'server_names': self._extract_server_names(),
+ 'ssl_protocols': self._extract_ssl_protocols(),
+ 'client_max_body_size': self._extract_client_max_body_size(),
+ 'gzip_enabled': self._check_gzip_enabled(),
+ }
+
+ def _check_ssl_enabled(self) -> bool:
+ """Check if SSL is enabled."""
+ if not self.config_content:
+ return False
+ return 'ssl_certificate' in self.config_content
+
+ def _extract_http_ports(self) -> List[int]:
+ """Extract HTTP listening ports."""
+ if not self.config_content:
+ return []
+ pattern = r'listen\s+(\d+)'
+ matches = re.findall(pattern, self.config_content)
+ return sorted(list(set(int(p) for p in matches if int(p) < 1000)))
+
+ def _extract_https_ports(self) -> List[int]:
+ """Extract HTTPS listening ports."""
+ if not self.config_content:
+ return []
+ pattern = r'listen\s+(\d+).*ssl'
+ matches = re.findall(pattern, self.config_content)
+ return sorted(list(set(int(p) for p in matches)))
+
+ def _extract_upstream_servers(self) -> List[str]:
+ """Extract upstream servers."""
+ if not self.config_content:
+ return []
+ upstream_match = re.search(r'upstream\s+\w+\s*{([^}]+)}', self.config_content)
+ if upstream_match:
+ upstream_content = upstream_match.group(1)
+ servers = re.findall(r'server\s+([^\s;]+)', upstream_content)
+ return servers
+ return []
+
+ def _extract_server_names(self) -> List[str]:
+ """Extract server names."""
+ if not self.config_content:
+ return []
+ pattern = r'server_name\s+([^;]+);'
+ matches = re.findall(pattern, self.config_content)
+ result = []
+ for match in matches:
+ names = match.strip().split()
+ result.extend(names)
+ return result
+
+ def _extract_ssl_protocols(self) -> List[str]:
+ """Extract SSL protocols."""
+ if not self.config_content:
+ return []
+ pattern = r'ssl_protocols\s+([^;]+);'
+ match = re.search(pattern, self.config_content)
+ if match:
+ return match.group(1).strip().split()
+ return []
+
+ def _extract_client_max_body_size(self) -> Optional[str]:
+ """Extract client max body size."""
+ if not self.config_content:
+ return None
+ pattern = r'client_max_body_size\s+([^;]+);'
+ match = re.search(pattern, self.config_content)
+ return match.group(1).strip() if match else None
+
+ def _check_gzip_enabled(self) -> bool:
+ """Check if gzip is enabled."""
+ if not self.config_content:
+ return False
+ return bool(re.search(r'gzip\s+on\s*;', self.config_content))
+
+
+def get_nginx_status() -> Dict[str, Any]:
+ """Get Nginx configuration status."""
+ reader = NginxConfigReader()
+ return reader.get_status()
diff --git a/docker-compose.yml b/docker-compose.yml
index c87082a..73d1e4f 100755
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -26,19 +26,19 @@ services:
networks:
- digiserver-network
- # Caddy reverse proxy with automatic HTTPS
- caddy:
- image: caddy:2-alpine
- container_name: digiserver-caddy
+ # Nginx reverse proxy with HTTPS support
+ nginx:
+ image: nginx:alpine
+ container_name: digiserver-nginx
ports:
- "80:80"
- "443:443"
- - "443:443/udp" # HTTP/3 support
- - "2019:2019" # Caddy admin API
volumes:
- - ./data/Caddyfile:/etc/caddy/Caddyfile:ro
- - ./data/caddy-data:/data
- - ./data/caddy-config:/config
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ - ./nginx-custom-domains.conf:/etc/nginx/conf.d/custom-domains.conf:rw
+ - ./data/nginx-ssl:/etc/nginx/ssl:ro
+ - ./data/nginx-logs:/var/log/nginx
+ - ./data/certbot:/var/www/certbot:ro # For Let's Encrypt ACME challenges
environment:
- DOMAIN=${DOMAIN:-localhost}
- EMAIL=${EMAIL:-admin@localhost}
@@ -46,6 +46,12 @@ services:
digiserver-app:
condition: service_started
restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 10s
networks:
- digiserver-network
diff --git a/generate_nginx_certs.sh b/generate_nginx_certs.sh
new file mode 100755
index 0000000..d081f1e
--- /dev/null
+++ b/generate_nginx_certs.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# Generate self-signed SSL certificates for Nginx
+# Usage: ./generate_nginx_certs.sh [domain] [days]
+
+DOMAIN=${1:-localhost}
+DAYS=${2:-365}
+CERT_DIR="./data/nginx-ssl"
+
+echo "đ Generating self-signed SSL certificate for Nginx"
+echo "Domain: $DOMAIN"
+echo "Valid for: $DAYS days"
+echo "Certificate directory: $CERT_DIR"
+
+# Create directory if it doesnt exist
+mkdir -p "$CERT_DIR"
+
+# Generate private key and certificate
+openssl req -x509 -nodes -days "$DAYS" \
+ -newkey rsa:2048 \
+ -keyout "$CERT_DIR/key.pem" \
+ -out "$CERT_DIR/cert.pem" \
+ -subj "/CN=$DOMAIN/O=DigiServer/C=US"
+
+# Set proper permissions
+chmod 644 "$CERT_DIR/cert.pem"
+chmod 600 "$CERT_DIR/key.pem"
+
+echo "â
Certificates generated successfully!"
+echo "Certificate: $CERT_DIR/cert.pem"
+echo "Key: $CERT_DIR/key.pem"
diff --git a/nginx-custom-domains.conf b/nginx-custom-domains.conf
new file mode 100644
index 0000000..32fc0da
--- /dev/null
+++ b/nginx-custom-domains.conf
@@ -0,0 +1,21 @@
+# Nginx configuration for custom HTTPS domains
+# This file will be dynamically generated based on HTTPSConfig database entries
+# Include this in your nginx.conf with: include /etc/nginx/conf.d/custom-domains.conf;
+
+# Example entry for custom domain:
+# server {
+# listen 443 ssl http2;
+# listen [::]:443 ssl http2;
+# server_name digiserver.example.com;
+#
+# ssl_certificate /etc/nginx/ssl/custom/cert.pem;
+# ssl_certificate_key /etc/nginx/ssl/custom/key.pem;
+#
+# location / {
+# proxy_pass http://digiserver_app;
+# proxy_set_header Host $host;
+# proxy_set_header X-Real-IP $remote_addr;
+# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+# proxy_set_header X-Forwarded-Proto $scheme;
+# }
+# }
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..c2323bc
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,117 @@
+user nginx;
+worker_processes auto;
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 1024;
+ use epoll;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 65;
+ types_hash_max_size 2048;
+ client_max_body_size 2048M;
+
+ # Gzip compression
+ gzip on;
+ gzip_vary on;
+ gzip_proxied any;
+ gzip_comp_level 6;
+ gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml;
+
+ # Upstream to Flask application
+ upstream digiserver_app {
+ server digiserver-app:5000;
+ keepalive 32;
+ }
+
+ # HTTP Server - redirect to HTTPS
+ server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+ server_name _;
+
+ # Allow ACME challenges for Let's Encrypt
+ location /.well-known/acme-challenge/ {
+ root /var/www/certbot;
+ }
+
+ # Redirect HTTP to HTTPS for non-ACME requests
+ location / {
+ return 301 https://$host$request_uri;
+ }
+ }
+
+ # HTTPS Server (with self-signed cert by default)
+ server {
+ listen 443 ssl http2 default_server;
+ listen [::]:443 ssl http2 default_server;
+ server_name localhost;
+
+ # SSL certificate paths (will be volume-mounted)
+ ssl_certificate /etc/nginx/ssl/cert.pem;
+ ssl_certificate_key /etc/nginx/ssl/key.pem;
+
+ # SSL Configuration
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 10m;
+
+ # Security Headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "no-referrer-when-downgrade" always;
+ add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
+
+ # Proxy settings
+ location / {
+ proxy_pass http://digiserver_app;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $server_name;
+
+ # Timeouts for large uploads
+ proxy_connect_timeout 300s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+
+ # Buffering
+ proxy_buffering on;
+ proxy_buffer_size 128k;
+ proxy_buffers 4 256k;
+ proxy_busy_buffers_size 256k;
+ }
+
+ # Static files caching
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ proxy_pass http://digiserver_app;
+ proxy_cache_valid 200 60d;
+ expires 60d;
+ add_header Cache-Control "public, immutable";
+ }
+ }
+
+ # Additional server blocks for custom domains can be included here
+ include /etc/nginx/conf.d/*.conf;
+}