Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4386f53f5 | |||
| 8f443379cd | |||
| a47b57f26a | |||
| cd401e108f | |||
| 3d11470f81 | |||
| 877ecf32b4 | |||
| 3b78959901 | |||
| 6e110289cd | |||
| afab617c5c | |||
| b737826d11 | |||
| f9704f6dd3 | |||
| 75a3ec9d7e | |||
| 21f27e0d27 | |||
| 7ed6100f05 | |||
| c3efa127d9 | |||
| e4fed12143 | |||
| 7fc81a0f65 | |||
| 611031ec6c | |||
| 686bce2df9 | |||
| efbb5725d3 | |||
| acd37a9c15 | |||
| 203d5e0575 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,3 +14,5 @@ BUI*
|
||||
build.sh
|
||||
docker-compose.dist*
|
||||
GITEA.md
|
||||
PROXY_AUTHELIA_SETUP.md
|
||||
alias.rune.pm.config
|
||||
@@ -134,3 +134,7 @@ To access from other devices on your network:
|
||||
1. Find your server's IP address
|
||||
2. Access via `http://YOUR_IP:5172`
|
||||
3. Make sure your firewall allows connections on port 5172
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
@@ -11,15 +11,16 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app.py .
|
||||
COPY malias_wrapper.py .
|
||||
COPY malias.py .
|
||||
COPY reset_password.py .
|
||||
COPY templates/ templates/
|
||||
COPY static/ static/
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Copy entrypoint script
|
||||
# Copy entrypoint script and make scripts executable
|
||||
COPY docker-entrypoint.sh .
|
||||
RUN chmod +x docker-entrypoint.sh
|
||||
RUN chmod +x docker-entrypoint.sh reset_password.py
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5172
|
||||
@@ -28,6 +29,7 @@ EXPOSE 5172
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV FLASK_DEBUG=False
|
||||
ENV FLASK_ENV=production
|
||||
ENV ENABLE_PROXY=false
|
||||
|
||||
# Run the application via entrypoint
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
|
||||
432
PROXY_SETUP.md
Normal file
432
PROXY_SETUP.md
Normal file
@@ -0,0 +1,432 @@
|
||||
# Reverse Proxy Setup Guide
|
||||
|
||||
This guide helps you configure reverse proxies (Nginx, Traefik, Zoraxy, Authelia, Caddy, etc.) to work with Mailcow Alias Manager.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 ENABLE_PROXY Configuration
|
||||
|
||||
The application supports **two access modes** controlled by the `ENABLE_PROXY` environment variable in `docker-compose.yml`:
|
||||
|
||||
### **Mode 1: Direct Access (ENABLE_PROXY=false)** - DEFAULT
|
||||
|
||||
Use this when accessing the application directly via IP:port **without a reverse proxy**.
|
||||
|
||||
**docker-compose.yml:**
|
||||
```yaml
|
||||
environment:
|
||||
- ENABLE_PROXY=false # Default setting
|
||||
```
|
||||
|
||||
**Access:** `http://192.168.1.100:5172` (replace with your server IP)
|
||||
|
||||
**Features:**
|
||||
- ✅ Works over HTTP (no HTTPS required)
|
||||
- ✅ Standard cookie behavior (SameSite=Lax)
|
||||
- ✅ No proxy configuration needed
|
||||
- ✅ Simple login flow
|
||||
- ✅ Perfect for internal/LAN access
|
||||
|
||||
**When to use:**
|
||||
- Accessing from internal network only
|
||||
- No reverse proxy in place
|
||||
- Testing or development
|
||||
- Simple single-server setup
|
||||
|
||||
---
|
||||
|
||||
### **Mode 2: Proxy Access (ENABLE_PROXY=true)**
|
||||
|
||||
Use this when running **behind a reverse proxy** (Authelia, Zoraxy, Nginx, Traefik, Caddy).
|
||||
|
||||
**docker-compose.yml:**
|
||||
```yaml
|
||||
environment:
|
||||
- ENABLE_PROXY=true
|
||||
```
|
||||
|
||||
**Access:** `https://alias.yourdomain.com` (through your reverse proxy)
|
||||
|
||||
**Features:**
|
||||
- ✅ ProxyFix middleware handles X-Forwarded-* headers
|
||||
- ✅ HTTPS redirect support
|
||||
- ✅ Secure cookies (HTTPS only)
|
||||
- ✅ Works with authentication proxies (Authelia)
|
||||
- ✅ Multi-proxy chain support
|
||||
|
||||
**When to use:**
|
||||
- Accessing from internet via domain name
|
||||
- Behind Nginx, Traefik, Caddy, HAProxy
|
||||
- Behind authentication proxy (Authelia, Authentik)
|
||||
- SSL/TLS termination at proxy
|
||||
- Production deployments with HTTPS
|
||||
|
||||
**Special Feature - Authelia Auto-Login:**
|
||||
When `ENABLE_PROXY=true` and you're using Authelia, the app automatically logs you in using Authelia's authentication headers. **No app password needed!** Simply authenticate through Authelia, and you'll be logged into the Mailcow Alias Manager automatically.
|
||||
|
||||
---
|
||||
|
||||
### **Switching Between Modes**
|
||||
|
||||
To switch from one mode to another:
|
||||
|
||||
1. **Edit `docker-compose.yml`**
|
||||
```yaml
|
||||
# Change this line:
|
||||
- ENABLE_PROXY=false # or true
|
||||
```
|
||||
|
||||
2. **Restart the container**
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3. **Verify mode in logs**
|
||||
```bash
|
||||
docker compose logs mailcow-alias-manager | grep "ACCESS MODE"
|
||||
```
|
||||
|
||||
You should see either:
|
||||
- `ACCESS MODE: Direct IP:Port (ENABLE_PROXY=false)`
|
||||
- `ACCESS MODE: Reverse Proxy (ENABLE_PROXY=true)`
|
||||
|
||||
4. **Clear browser cookies** (IMPORTANT!)
|
||||
- Press F12 → Application → Cookies
|
||||
- Delete all cookies for your domain
|
||||
- Close and reopen browser
|
||||
|
||||
5. **Login again**
|
||||
|
||||
---
|
||||
|
||||
### **Quick Reference Table**
|
||||
|
||||
| Access Method | ENABLE_PROXY | Access URL | Cookie Mode |
|
||||
|--------------|--------------|------------|-------------|
|
||||
| Direct IP:port | `false` (default) | `http://192.168.1.100:5172` | HTTP, SameSite=Lax |
|
||||
| Nginx/Traefik | `true` | `https://alias.example.com` | HTTPS, SameSite=None |
|
||||
| Authelia + Zoraxy | `true` | `https://alias.example.com` | HTTPS, SameSite=None |
|
||||
| Caddy | `true` | `https://alias.example.com` | HTTPS, SameSite=None |
|
||||
| Local dev (`python3 app.py`) | N/A (not set) | `http://localhost:5172` | HTTP, SameSite=Lax |
|
||||
|
||||
---
|
||||
|
||||
## ✅ Built-in Proxy Support (When ENABLE_PROXY=true)
|
||||
|
||||
The application includes **ProxyFix middleware** that automatically handles:
|
||||
- ✅ HTTPS detection via `X-Forwarded-Proto`
|
||||
- ✅ Host header forwarding via `X-Forwarded-Host`
|
||||
- ✅ Client IP forwarding via `X-Forwarded-For`
|
||||
- ✅ Path prefix support via `X-Forwarded-Prefix`
|
||||
|
||||
**Works with 2 proxies in chain** (e.g., Zoraxy → Authelia → App)
|
||||
|
||||
---
|
||||
|
||||
## Common Proxy Configurations
|
||||
|
||||
### **Zoraxy** (Your Current Setup)
|
||||
|
||||
Zoraxy automatically sets the required headers. Just configure:
|
||||
|
||||
1. **Upstream Target**: `http://localhost:5172` (or your Docker IP)
|
||||
2. **Enable WebSocket**: Yes (optional, recommended)
|
||||
3. **Enable Proxy Headers**: Yes (should be default)
|
||||
|
||||
**Example Zoraxy Config:**
|
||||
```json
|
||||
{
|
||||
"name": "mailcow-alias-manager",
|
||||
"upstream": "http://localhost:5172",
|
||||
"domain": "aliases.yourdomain.com"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Authelia** (Authentication Proxy)
|
||||
|
||||
If using Authelia in front of the app:
|
||||
|
||||
1. **Authelia forwards the user after login** - this should work automatically
|
||||
2. **Make sure Authelia passes these headers**:
|
||||
- `X-Forwarded-Proto`
|
||||
- `X-Forwarded-Host`
|
||||
- `X-Forwarded-For`
|
||||
|
||||
**Common Issue**: Authelia redirects → App login page
|
||||
**Solution**: The app has its own authentication. You have two options:
|
||||
- **Option A**: Disable app login (requires code modification)
|
||||
- **Option B**: Use Authelia for network access, app login for API access
|
||||
|
||||
---
|
||||
|
||||
### **Nginx**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name aliases.yourdomain.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5172;
|
||||
|
||||
# Required headers for ProxyFix
|
||||
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 $host;
|
||||
|
||||
# Optional: WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Traefik**
|
||||
|
||||
**docker-compose.yml:**
|
||||
```yaml
|
||||
services:
|
||||
mailcow-alias-manager:
|
||||
image: gitlab.pm/rune/malias-web:latest
|
||||
container_name: mailcow-alias-manager
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.malias.rule=Host(`aliases.yourdomain.com`)"
|
||||
- "traefik.http.routers.malias.entrypoints=websecure"
|
||||
- "traefik.http.routers.malias.tls=true"
|
||||
- "traefik.http.routers.malias.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.malias.loadbalancer.server.port=5172"
|
||||
networks:
|
||||
- traefik
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
Traefik automatically sets `X-Forwarded-*` headers by default.
|
||||
|
||||
---
|
||||
|
||||
### **Caddy**
|
||||
|
||||
```caddy
|
||||
aliases.yourdomain.com {
|
||||
reverse_proxy localhost:5172
|
||||
}
|
||||
```
|
||||
|
||||
Caddy automatically handles all proxy headers - no configuration needed!
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### **Issue: "NetworkError when attempting to fetch resource"**
|
||||
|
||||
**Causes:**
|
||||
1. Mixed HTTP/HTTPS content
|
||||
2. CORS blocking requests
|
||||
3. Session cookie not being set
|
||||
4. Proxy not forwarding headers
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### **1. Check Proxy Headers**
|
||||
Your proxy MUST forward these headers:
|
||||
```
|
||||
X-Forwarded-Proto: https
|
||||
X-Forwarded-Host: aliases.yourdomain.com
|
||||
X-Forwarded-For: <client-ip>
|
||||
```
|
||||
|
||||
#### **2. Check Browser Console**
|
||||
Open browser DevTools (F12) → Console tab → Look for errors:
|
||||
- **CORS errors**: Proxy misconfiguration
|
||||
- **Mixed content**: HTTP resources on HTTPS page
|
||||
- **401 Unauthorized**: Session/cookie issue
|
||||
|
||||
#### **3. Test Direct Access**
|
||||
```bash
|
||||
# Test without proxy
|
||||
curl http://localhost:5172/
|
||||
|
||||
# Should return HTML (login page)
|
||||
```
|
||||
|
||||
If this works but proxy doesn't, it's a proxy configuration issue.
|
||||
|
||||
#### **4. Check Session Cookies**
|
||||
In browser DevTools → Application tab → Cookies:
|
||||
- **Domain**: Should match your proxy domain
|
||||
- **Path**: `/`
|
||||
- **HttpOnly**: Should be `true`
|
||||
- **Secure**: Depends on your setup
|
||||
|
||||
---
|
||||
|
||||
### **Issue: Login works but redirects to HTTP instead of HTTPS**
|
||||
|
||||
**Solution**: Already fixed in latest version via `PREFERRED_URL_SCHEME='https'` in app config.
|
||||
|
||||
If still happening:
|
||||
1. Verify proxy sends `X-Forwarded-Proto: https`
|
||||
2. Check `docker compose logs -f mailcow-alias-manager`
|
||||
3. Rebuild image: `docker compose up -d --build`
|
||||
|
||||
---
|
||||
|
||||
### **Issue: "Invalid password" but password is correct**
|
||||
|
||||
This is NOT a proxy issue - this is an authentication issue:
|
||||
```bash
|
||||
# Reset password
|
||||
docker exec -it mailcow-alias-manager python3 reset_password.py "newpass"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Issue: 502 Bad Gateway**
|
||||
|
||||
**Causes:**
|
||||
1. App not running
|
||||
2. Wrong upstream port
|
||||
3. Container network issue
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
# Is container running?
|
||||
docker ps | grep mailcow-alias-manager
|
||||
|
||||
# Check logs
|
||||
docker compose logs -f mailcow-alias-manager
|
||||
|
||||
# Test internal connectivity
|
||||
docker exec mailcow-alias-manager curl http://localhost:5172
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Best Practices
|
||||
|
||||
### **1. HTTPS Only**
|
||||
Always use HTTPS in production. HTTP is only for local testing.
|
||||
|
||||
### **2. Restrict Access**
|
||||
Use firewall or proxy rules to restrict access:
|
||||
```nginx
|
||||
# Nginx: Restrict to internal network only
|
||||
allow 192.168.1.0/24;
|
||||
deny all;
|
||||
```
|
||||
|
||||
### **3. Use Authelia/Authentik**
|
||||
Add SSO layer for additional security:
|
||||
- Users authenticate via Authelia
|
||||
- Then app login provides API access
|
||||
- Two-factor authentication recommended
|
||||
|
||||
### **4. Keep Session Cookies Secure**
|
||||
The app sets:
|
||||
- `HttpOnly=True` - Prevents JavaScript access
|
||||
- `SameSite=Lax` - CSRF protection
|
||||
|
||||
For HTTPS-only deployments, you can enforce secure cookies:
|
||||
```python
|
||||
# In app.py, change:
|
||||
SESSION_COOKIE_SECURE=True # Only send cookie over HTTPS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Testing Your Setup
|
||||
|
||||
### **1. Basic Connectivity**
|
||||
```bash
|
||||
curl -v https://aliases.yourdomain.com/
|
||||
# Should return 200 OK with HTML
|
||||
```
|
||||
|
||||
### **2. Login Test**
|
||||
```bash
|
||||
curl -v -X POST https://aliases.yourdomain.com/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"password":"yourpassword"}' \
|
||||
-c cookies.txt
|
||||
|
||||
# Check response - should be JSON with status: success
|
||||
```
|
||||
|
||||
### **3. Session Test**
|
||||
```bash
|
||||
curl -v https://aliases.yourdomain.com/ \
|
||||
-b cookies.txt
|
||||
|
||||
# Should return main page HTML (not login redirect)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Still Having Issues?
|
||||
|
||||
### **Enable Debug Logging**
|
||||
|
||||
**docker-compose.yml:**
|
||||
```yaml
|
||||
environment:
|
||||
- FLASK_DEBUG=True # Enable debug mode
|
||||
```
|
||||
|
||||
Restart:
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
### **Check Application Logs**
|
||||
```bash
|
||||
# View logs
|
||||
docker compose logs -f mailcow-alias-manager
|
||||
|
||||
# Check for errors
|
||||
docker compose logs mailcow-alias-manager | grep -i error
|
||||
```
|
||||
|
||||
### **Test Without Proxy**
|
||||
Temporarily bypass proxy to isolate the issue:
|
||||
```bash
|
||||
# Direct access test
|
||||
curl http://localhost:5172/
|
||||
```
|
||||
|
||||
If this works, the issue is with proxy configuration, not the app.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary
|
||||
|
||||
**The app is already configured for reverse proxies!**
|
||||
|
||||
Key points:
|
||||
- ✅ ProxyFix middleware is enabled
|
||||
- ✅ Handles X-Forwarded-* headers automatically
|
||||
- ✅ HTTPS redirect support built-in
|
||||
- ✅ Session cookies work behind proxies
|
||||
- ✅ No code changes needed
|
||||
|
||||
Just make sure your proxy forwards the standard headers and you're good to go! 🚀
|
||||
33
README.md
33
README.md
@@ -1,4 +1,4 @@
|
||||
# Mailcow Alias Manager Web Interface
|
||||
# Mailcow Alias Manager Web Interface..
|
||||
|
||||
A Python-based web application for managing mail aliases on a Mailcow instance. Built with Flask and a dark theme interface for internal network use. This application uses [malias](https://gitlab.pm/rune/malias) as base. Malias it self is a CLI application for managing aliases on a Mailcow Instance. If you already are using the CLI instance you can copy the the DB file over to your `/data` directory and you'll keep you API key etc. If you start with a blank DB you can use the `Sync Aliases` function to populate existing aliases to your local db.
|
||||
|
||||
@@ -68,7 +68,32 @@ The application will be available at: `http://localhost:5172`
|
||||
|
||||
## Configuration
|
||||
|
||||
### 1. Configure Mailcow Connection
|
||||
### 1. Access Mode Configuration
|
||||
|
||||
The application supports two access modes:
|
||||
|
||||
**Direct Access (Default)** - Access via IP:port
|
||||
- Set `ENABLE_PROXY=false` in `docker-compose.yml` (default)
|
||||
- Access at: `http://your-server-ip:5172`
|
||||
- Works over HTTP (no HTTPS required)
|
||||
- **Login:** Enter app password
|
||||
- Perfect for internal/LAN access
|
||||
|
||||
**Proxy Access** - Access via reverse proxy (Authelia, Nginx, Traefik, etc.)
|
||||
- Set `ENABLE_PROXY=true` in `docker-compose.yml`
|
||||
- Access at: `https://alias.yourdomain.com` (through your proxy)
|
||||
- Requires HTTPS
|
||||
- **Login:** Automatic when using Authelia (no password needed!)
|
||||
- See [PROXY_SETUP.md](PROXY_SETUP.md) for detailed configuration
|
||||
|
||||
**To switch modes:**
|
||||
1. Edit `docker-compose.yml` and change `ENABLE_PROXY` value
|
||||
2. Restart: `docker compose down && docker compose up -d`
|
||||
3. Clear browser cookies and login again
|
||||
|
||||
---
|
||||
|
||||
### 2. Configure Mailcow Connection
|
||||
|
||||
You can configure your Mailcow server connection either:
|
||||
|
||||
@@ -295,3 +320,7 @@ For manual Python installation:
|
||||
- Verify your Mailcow server is accessible from the machine running this app
|
||||
- Check that the API key is valid and has the correct permissions
|
||||
- Review logs in `data/malias2.log`
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
464
app.py
464
app.py
@@ -1,53 +1,350 @@
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, session
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, session, make_response
|
||||
from functools import wraps
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from datetime import timedelta
|
||||
import malias_wrapper as malias_w
|
||||
import os
|
||||
import argparse
|
||||
import sys
|
||||
import secrets
|
||||
import logging
|
||||
import json
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger('mailcow-alias-manager')
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.urandom(24) # Secret key for session management
|
||||
app.secret_key = os.getenv('SECRET_KEY', 'malias-default-secret-key-please-change') # Consistent secret key
|
||||
|
||||
# Configure proxy mode based on environment variable
|
||||
# ENABLE_PROXY=true: Behind reverse proxy (Authelia/Zoraxy/Nginx/etc.)
|
||||
# ENABLE_PROXY=false: Direct IP:port access (default)
|
||||
ENABLE_PROXY = os.getenv('ENABLE_PROXY', 'false').lower() in ('true', '1', 'yes')
|
||||
|
||||
if ENABLE_PROXY:
|
||||
# Mode: Behind reverse proxy (Authelia/Zoraxy/Nginx/etc.)
|
||||
logger.info("=" * 60)
|
||||
logger.info(" ACCESS MODE: Reverse Proxy (ENABLE_PROXY=true)")
|
||||
logger.info("=" * 60)
|
||||
logger.info(" Configuration:")
|
||||
logger.info(" - ProxyFix middleware: ACTIVE")
|
||||
logger.info(" - Trusted proxies: 2 (e.g., Zoraxy + Authelia)")
|
||||
logger.info(" - Cookie security: HTTPS only (Secure=True)")
|
||||
logger.info(" - SameSite policy: None (cross-origin auth)")
|
||||
logger.info(" - URL scheme: https://")
|
||||
logger.info("=" * 60)
|
||||
|
||||
app.wsgi_app = ProxyFix(
|
||||
app.wsgi_app,
|
||||
x_for=2, # Trust X-Forwarded-For with 2 proxies
|
||||
x_proto=2, # Trust X-Forwarded-Proto (http/https)
|
||||
x_host=2, # Trust X-Forwarded-Host
|
||||
x_prefix=2 # Trust X-Forwarded-Prefix
|
||||
)
|
||||
|
||||
cookie_secure = True
|
||||
cookie_samesite = 'None'
|
||||
preferred_scheme = 'https'
|
||||
else:
|
||||
# Mode: Direct IP:port access (no proxy)
|
||||
logger.info("=" * 60)
|
||||
logger.info(" ACCESS MODE: Direct IP:Port (ENABLE_PROXY=false)")
|
||||
logger.info("=" * 60)
|
||||
logger.info(" Configuration:")
|
||||
logger.info(" - ProxyFix middleware: DISABLED")
|
||||
logger.info(" - Cookie security: HTTP allowed (Secure=False)")
|
||||
logger.info(" - SameSite policy: Lax (standard mode)")
|
||||
logger.info(" - URL scheme: http://")
|
||||
logger.info(" - Access via: http://your-ip:5172")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# Don't apply ProxyFix for direct access
|
||||
cookie_secure = False
|
||||
cookie_samesite = 'Lax'
|
||||
preferred_scheme = 'http'
|
||||
|
||||
# Initialize database on startup
|
||||
malias_w.init_database()
|
||||
|
||||
# Session configuration - dynamic based on proxy mode
|
||||
app.config.update(
|
||||
PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
|
||||
SESSION_COOKIE_NAME='malias_session',
|
||||
SESSION_COOKIE_SECURE=cookie_secure,
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
SESSION_COOKIE_SAMESITE=cookie_samesite,
|
||||
SESSION_COOKIE_PATH='/',
|
||||
SESSION_COOKIE_DOMAIN=None,
|
||||
SESSION_REFRESH_EACH_REQUEST=True,
|
||||
PREFERRED_URL_SCHEME=preferred_scheme
|
||||
)
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
"""Process the response before it's sent"""
|
||||
if ENABLE_PROXY:
|
||||
# Set CORS headers for proxy mode (needed for Authelia/Zoraxy)
|
||||
response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
|
||||
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
||||
|
||||
# Cache control (applies to both modes)
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
|
||||
return response
|
||||
|
||||
def get_authelia_user():
|
||||
"""Helper to get authenticated user from Authelia headers or session cookie"""
|
||||
# Check several possible header variations (ORDER MATTERS - most specific first!)
|
||||
auth_headers = [
|
||||
'X-Authelia-Username', # Authelia standard header (used by Zoraxy)
|
||||
'Remote-User', # Common standard
|
||||
'X-Remote-User',
|
||||
'X-Forwarded-User',
|
||||
'REMOTE_USER',
|
||||
'Http-Remote-User',
|
||||
'Http-X-Remote-User',
|
||||
'X-Authenticated-User'
|
||||
]
|
||||
|
||||
for header in auth_headers:
|
||||
user = request.headers.get(header)
|
||||
if user:
|
||||
logger.info(f"✅ Authelia user detected via header '{header}': {user}")
|
||||
return user
|
||||
|
||||
# Check Zoraxy forwarded headers (sometimes encoded differently)
|
||||
if 'X-Forwarded-Headers' in request.headers:
|
||||
try:
|
||||
# Some reverse proxies encode headers as JSON
|
||||
fwd_headers = json.loads(request.headers.get('X-Forwarded-Headers'))
|
||||
for header in auth_headers:
|
||||
if header in fwd_headers:
|
||||
user = fwd_headers[header]
|
||||
logger.info(f"✅ Authelia user detected via forwarded headers - {header}: {user}")
|
||||
return user
|
||||
except:
|
||||
pass
|
||||
|
||||
# WORKAROUND: If Authelia session cookie exists and we're coming from auth.rune.pm,
|
||||
# assume user is authenticated (Authelia not forwarding headers properly)
|
||||
if ENABLE_PROXY and 'authelia_session' in request.cookies:
|
||||
referer = request.headers.get('Referer', '')
|
||||
if 'auth.rune.pm' in referer or request.headers.get('X-Forwarded-Proto') == 'https':
|
||||
# Valid Authelia session exists - assume authenticated
|
||||
# Use a generic identifier since we don't have the actual username
|
||||
pseudo_user = f"authelia_user_{request.cookies.get('authelia_session')[:8]}"
|
||||
logger.info(f"✅ Authelia authentication detected via session cookie (headers not forwarded)")
|
||||
logger.info(f" Using pseudo-user identifier: {pseudo_user}")
|
||||
logger.info(f" NOTE: Configure Authelia to forward Remote-User header for proper username")
|
||||
return pseudo_user
|
||||
|
||||
# Log when no Authelia user found (for debugging)
|
||||
if ENABLE_PROXY:
|
||||
logger.debug("⚠️ No Authelia headers found in request")
|
||||
logger.debug(f"Available headers: {list(request.headers.keys())}")
|
||||
|
||||
return None
|
||||
|
||||
def login_required(f):
|
||||
"""Decorator to require login for routes"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# Auto-login with Authelia (only when ENABLE_PROXY=true)
|
||||
if ENABLE_PROXY:
|
||||
authelia_user = get_authelia_user()
|
||||
|
||||
# If Authelia authenticated the user, auto-login
|
||||
if authelia_user:
|
||||
if not session.get('logged_in') or session.get('authelia_user') != authelia_user:
|
||||
logger.info(f"🔐 Auto-login via Authelia in API route: {authelia_user}")
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session['logged_in'] = True
|
||||
session['authelia_user'] = authelia_user
|
||||
session['user_token'] = secrets.token_urlsafe(32)
|
||||
session['auth_method'] = 'authelia'
|
||||
session.modified = True
|
||||
|
||||
# Store additional info (check both Remote-* and X-Authelia-* headers)
|
||||
session['remote_email'] = (request.headers.get('X-Authelia-Email') or
|
||||
request.headers.get('Remote-Email', ''))
|
||||
session['remote_name'] = (request.headers.get('X-Authelia-DisplayName') or
|
||||
request.headers.get('Remote-Name', ''))
|
||||
session['remote_groups'] = (request.headers.get('X-Authelia-Groups') or
|
||||
request.headers.get('Remote-Groups', ''))
|
||||
|
||||
logger.info(f"✅ Auto-login in API route: {authelia_user}")
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
# Regular session check (when ENABLE_PROXY=false or no Authelia headers)
|
||||
if not session.get('logged_in'):
|
||||
logger.warning("Access denied: User not authenticated")
|
||||
if request.is_json:
|
||||
return jsonify({'status': 'error', 'message': 'Not authenticated', 'redirect': '/login'}), 401
|
||||
return redirect(url_for('login'))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""Login page"""
|
||||
"""Login page or JSON login endpoint"""
|
||||
|
||||
# Validate session mode matches current proxy setting
|
||||
if session.get('logged_in'):
|
||||
session_mode = session.get('auth_method', 'unknown')
|
||||
|
||||
# Clear session if mode mismatch
|
||||
if ENABLE_PROXY and session_mode == 'local':
|
||||
logger.info("⚠️ Session mode mismatch: Clearing local session (proxy mode enabled)")
|
||||
session.clear()
|
||||
elif not ENABLE_PROXY and session_mode == 'authelia':
|
||||
logger.info("⚠️ Session mode mismatch: Clearing authelia session (proxy mode disabled)")
|
||||
session.clear()
|
||||
|
||||
# Auto-login when ENABLE_PROXY=true and Authelia headers are present
|
||||
if ENABLE_PROXY:
|
||||
authelia_user = get_authelia_user()
|
||||
|
||||
if authelia_user:
|
||||
# User authenticated by Authelia - auto-login
|
||||
if not session.get('logged_in'):
|
||||
logger.info(f"🔐 Auto-login: User '{authelia_user}' authenticated by Authelia")
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session['logged_in'] = True
|
||||
session['user_token'] = secrets.token_urlsafe(32)
|
||||
session['auth_method'] = 'authelia'
|
||||
session['authelia_user'] = authelia_user
|
||||
session.modified = True
|
||||
|
||||
# Get additional Authelia info (check both Remote-* and X-Authelia-* headers)
|
||||
session['remote_email'] = (request.headers.get('X-Authelia-Email') or
|
||||
request.headers.get('Remote-Email', ''))
|
||||
session['remote_name'] = (request.headers.get('X-Authelia-DisplayName') or
|
||||
request.headers.get('Remote-Name', ''))
|
||||
session['remote_groups'] = (request.headers.get('X-Authelia-Groups') or
|
||||
request.headers.get('Remote-Groups', ''))
|
||||
|
||||
logger.info(f"✅ Auto-login successful: {authelia_user} ({session.get('remote_email', 'no email')})")
|
||||
|
||||
# Already logged in via Authelia - redirect to main page
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
# ENABLE_PROXY=true but no Authelia headers found
|
||||
logger.warning("⚠️ ENABLE_PROXY=true but no Authelia headers detected!")
|
||||
logger.warning(" Make sure your reverse proxy forwards authentication headers")
|
||||
logger.warning(f" Available headers: {list(request.headers.keys())}")
|
||||
|
||||
# Handle form submission for local authentication (only when ENABLE_PROXY=false)
|
||||
if request.method == 'POST':
|
||||
password = request.json.get('password', '')
|
||||
logger.info("Login attempt with password (redacted)")
|
||||
|
||||
if malias_w.verify_password(password):
|
||||
logger.info("Login successful with local password")
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session['logged_in'] = True
|
||||
return jsonify({'status': 'success', 'message': 'Login successful'})
|
||||
session['user_token'] = secrets.token_urlsafe(32)
|
||||
session['auth_method'] = 'local'
|
||||
session.modified = True
|
||||
|
||||
# Return JSON response
|
||||
response = jsonify({'status': 'success', 'message': 'Login successful'})
|
||||
return response
|
||||
else:
|
||||
logger.warning("Login failed: Invalid password")
|
||||
return jsonify({'status': 'error', 'message': 'Invalid password'})
|
||||
|
||||
# Check if already logged in
|
||||
# If already logged in, redirect to index
|
||||
if session.get('logged_in'):
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Check cookies and client IP for troubleshooting
|
||||
if app.debug:
|
||||
logger.info(f"Cookies: {request.cookies}")
|
||||
logger.info(f"Client IP: {request.remote_addr}")
|
||||
logger.info(f"X-Forwarded-For: {request.headers.get('X-Forwarded-For')}")
|
||||
# Log all headers to see what's coming from Authelia
|
||||
logger.info(f"All headers: {dict(request.headers)}")
|
||||
|
||||
# Show login form
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
"""Logout"""
|
||||
session.pop('logged_in', None)
|
||||
return redirect(url_for('login'))
|
||||
# Get auth method before clearing session
|
||||
auth_method = session.get('auth_method')
|
||||
authelia_user = session.get('authelia_user')
|
||||
|
||||
# Clear local session
|
||||
session.clear()
|
||||
|
||||
# If user was authenticated via Authelia, redirect to app login (not Authelia logout)
|
||||
# This keeps the Authelia session active for other apps
|
||||
if ENABLE_PROXY and (auth_method == 'authelia' or authelia_user):
|
||||
logger.info(f"Logout for Authelia user - redirecting to app login page")
|
||||
# Just redirect back to login page - Authelia session stays active
|
||||
response = redirect(url_for('login'))
|
||||
response.set_cookie(app.config['SESSION_COOKIE_NAME'], '', expires=0)
|
||||
return response
|
||||
|
||||
# Default case: redirect to login page
|
||||
response = redirect(url_for('login'))
|
||||
|
||||
# Clear cookie by setting expired date
|
||||
response.set_cookie(app.config['SESSION_COOKIE_NAME'], '', expires=0)
|
||||
|
||||
return response
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Main page - requires login"""
|
||||
# Auto-login with Authelia (only when ENABLE_PROXY=true)
|
||||
if ENABLE_PROXY:
|
||||
authelia_user = get_authelia_user()
|
||||
|
||||
if authelia_user and not session.get('logged_in'):
|
||||
# Auto-login for users authenticated by Authelia
|
||||
logger.info(f"🔐 Auto-login via Authelia for user: {authelia_user}")
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session['logged_in'] = True
|
||||
session['authelia_user'] = authelia_user
|
||||
session['user_token'] = secrets.token_urlsafe(32)
|
||||
session['auth_method'] = 'authelia'
|
||||
session.modified = True
|
||||
|
||||
# Store additional Authelia info (check both Remote-* and X-Authelia-* headers)
|
||||
session['remote_email'] = (request.headers.get('X-Authelia-Email') or
|
||||
request.headers.get('Remote-Email', ''))
|
||||
session['remote_name'] = (request.headers.get('X-Authelia-DisplayName') or
|
||||
request.headers.get('Remote-Name', ''))
|
||||
session['remote_groups'] = (request.headers.get('X-Authelia-Groups') or
|
||||
request.headers.get('Remote-Groups', ''))
|
||||
|
||||
logger.info(f"✅ Auto-login successful: {authelia_user} ({session.get('remote_email', 'no email')})")
|
||||
return render_template('index.html')
|
||||
|
||||
# Check if logged in
|
||||
if not session.get('logged_in'):
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# Debug logging
|
||||
if app.debug and session.get('logged_in'):
|
||||
logger.info(f"User authenticated with method: {session.get('auth_method', 'unknown')}")
|
||||
|
||||
# Show main page
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/list_aliases', methods=['POST'])
|
||||
@@ -65,6 +362,7 @@ def list_aliases():
|
||||
result = malias_w.get_all_aliases(page=page, per_page=20)
|
||||
return jsonify({'status': 'success', 'data': result})
|
||||
except Exception as e:
|
||||
logger.exception("Error listing aliases")
|
||||
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
||||
|
||||
@app.route('/sync_aliases', methods=['POST'])
|
||||
@@ -76,6 +374,7 @@ def sync_aliases():
|
||||
result = f"Aliases synchronized successfully! Added {count} new aliases to local DB."
|
||||
return jsonify({'status': 'success', 'message': result})
|
||||
except Exception as e:
|
||||
logger.exception("Error syncing aliases")
|
||||
return jsonify({'status': 'error', 'message': f"Error syncing: {str(e)}"})
|
||||
|
||||
@app.route('/get_domains', methods=['POST'])
|
||||
@@ -88,6 +387,7 @@ def get_domains():
|
||||
result = f"Domains: {', '.join(domain_list)}"
|
||||
return jsonify({'status': 'success', 'message': result, 'domains': domain_list})
|
||||
except Exception as e:
|
||||
logger.exception("Error getting domains")
|
||||
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
||||
|
||||
@app.route('/create_alias', methods=['POST'])
|
||||
@@ -106,6 +406,7 @@ def create_alias():
|
||||
result = f"Alias {alias} created successfully for {goto}"
|
||||
return jsonify({'status': 'success', 'message': result})
|
||||
except Exception as e:
|
||||
logger.exception("Error creating alias")
|
||||
return jsonify({'status': 'error', 'message': f"Error creating alias: {str(e)}"})
|
||||
|
||||
@app.route('/delete_alias', methods=['POST'])
|
||||
@@ -123,6 +424,7 @@ def delete_alias_route():
|
||||
result = f"Alias {alias} deleted successfully"
|
||||
return jsonify({'status': 'success', 'message': result})
|
||||
except Exception as e:
|
||||
logger.exception("Error deleting alias")
|
||||
return jsonify({'status': 'error', 'message': f"Error deleting alias: {str(e)}"})
|
||||
|
||||
@app.route('/delete_aliases_bulk', methods=['POST'])
|
||||
@@ -177,6 +479,7 @@ def delete_aliases_bulk():
|
||||
'details': result
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("Error deleting aliases bulk")
|
||||
return jsonify({'status': 'error', 'message': f"Error deleting aliases: {str(e)}"})
|
||||
|
||||
@app.route('/create_timed_alias', methods=['POST'])
|
||||
@@ -195,6 +498,7 @@ def create_timed_alias():
|
||||
result = f"Timed alias created for {username} on domain {domain}"
|
||||
return jsonify({'status': 'success', 'message': result})
|
||||
except Exception as e:
|
||||
logger.exception("Error creating timed alias")
|
||||
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
||||
|
||||
@app.route('/search', methods=['POST'])
|
||||
@@ -217,6 +521,7 @@ def search():
|
||||
|
||||
return jsonify({'status': 'success', 'message': message, 'query': query})
|
||||
except Exception as e:
|
||||
logger.exception("Error searching aliases")
|
||||
return jsonify({'status': 'error', 'message': f"Search error: {str(e)}"})
|
||||
|
||||
@app.route('/config', methods=['GET', 'POST'])
|
||||
@@ -262,8 +567,148 @@ def change_password_route():
|
||||
malias_w.change_password(old_password, new_password)
|
||||
return jsonify({'status': 'success', 'message': 'Password changed successfully'})
|
||||
except Exception as e:
|
||||
logger.exception("Error changing password")
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
# Add a health check endpoint for proxies
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
"""Health check endpoint for proxies"""
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'authenticated': session.get('logged_in', False),
|
||||
'authelia_user': session.get('authelia_user', None),
|
||||
'auth_method': session.get('auth_method'),
|
||||
'version': '1.0.2',
|
||||
'cookies': {k: '***' for k in request.cookies.keys()}, # Only show cookie names
|
||||
'authelia_headers_present': get_authelia_user() is not None,
|
||||
'zoraxy_headers_present': 'X-Forwarded-Server' in request.headers
|
||||
})
|
||||
|
||||
# Add a debugging endpoint
|
||||
@app.route('/debug')
|
||||
def debug_info():
|
||||
"""Show debug information about the current request"""
|
||||
# Always allow this in production to help with troubleshooting
|
||||
debug_data = {
|
||||
'headers': dict(request.headers),
|
||||
'cookies': {k: '***' for k in request.cookies.keys()}, # Don't expose cookie values
|
||||
'session': {k: ('***' if k in ['user_token', 'csrf_token'] else v) for k, v in session.items()} if session else {},
|
||||
'remote_addr': request.remote_addr,
|
||||
'scheme': request.scheme,
|
||||
'host': request.host,
|
||||
'path': request.path,
|
||||
'is_secure': request.is_secure,
|
||||
'x_forwarded_for': request.headers.get('X-Forwarded-For'),
|
||||
'x_forwarded_proto': request.headers.get('X-Forwarded-Proto'),
|
||||
'x_forwarded_host': request.headers.get('X-Forwarded-Host'),
|
||||
'x_forwarded_prefix': request.headers.get('X-Forwarded-Prefix'),
|
||||
'remote_user': get_authelia_user(),
|
||||
}
|
||||
|
||||
# Additional server information
|
||||
debug_data['server_info'] = {
|
||||
'cookie_settings': {
|
||||
'SESSION_COOKIE_NAME': app.config['SESSION_COOKIE_NAME'],
|
||||
'SESSION_COOKIE_SECURE': app.config['SESSION_COOKIE_SECURE'],
|
||||
'SESSION_COOKIE_HTTPONLY': app.config['SESSION_COOKIE_HTTPONLY'],
|
||||
'SESSION_COOKIE_SAMESITE': app.config['SESSION_COOKIE_SAMESITE'],
|
||||
'SESSION_COOKIE_PATH': app.config['SESSION_COOKIE_PATH'],
|
||||
},
|
||||
'app_config': {
|
||||
'DEBUG': app.debug,
|
||||
'PREFERRED_URL_SCHEME': app.config['PREFERRED_URL_SCHEME'],
|
||||
'ENABLE_PROXY': ENABLE_PROXY
|
||||
}
|
||||
}
|
||||
|
||||
response = jsonify(debug_data)
|
||||
response.headers['Cache-Control'] = 'no-store, must-revalidate'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
return response
|
||||
|
||||
# Dedicated endpoint for header diagnostics
|
||||
@app.route('/headers')
|
||||
def show_headers():
|
||||
"""Show all request headers - useful for debugging proxies"""
|
||||
# Log headers to help diagnose issues with Zoraxy/Authelia
|
||||
logger.info(f"Headers endpoint: All headers received: {dict(request.headers)}")
|
||||
return jsonify({
|
||||
'headers': dict(request.headers),
|
||||
'remote_addr': request.remote_addr,
|
||||
'authelia_user': get_authelia_user()
|
||||
})
|
||||
|
||||
# Self-test endpoint for cookies
|
||||
@app.route('/cookie-test')
|
||||
def cookie_test():
|
||||
"""Test cookie handling"""
|
||||
# Clear any existing cookie
|
||||
resp = make_response(jsonify({'status': 'ok', 'message': 'Cookie set test'}))
|
||||
|
||||
# Set a test cookie with the same attributes as session
|
||||
resp.set_cookie(
|
||||
'malias_test_cookie',
|
||||
'test-value',
|
||||
max_age=3600,
|
||||
secure=app.config['SESSION_COOKIE_SECURE'],
|
||||
httponly=app.config['SESSION_COOKIE_HTTPONLY'],
|
||||
samesite='None',
|
||||
path=app.config['SESSION_COOKIE_PATH']
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
# Endpoint to check if test cookie is set
|
||||
@app.route('/cookie-check')
|
||||
def cookie_check():
|
||||
"""Check if test cookie was properly set"""
|
||||
test_cookie = request.cookies.get('malias_test_cookie')
|
||||
return jsonify({
|
||||
'test_cookie_present': test_cookie is not None,
|
||||
'test_cookie_value': test_cookie if test_cookie else None,
|
||||
'all_cookies': {k: '***' for k in request.cookies.keys()}
|
||||
})
|
||||
|
||||
# New endpoint to test Zoraxy auth configuration
|
||||
@app.route('/authelia-test')
|
||||
def authelia_test():
|
||||
"""Test if Authelia headers are correctly passed through Zoraxy"""
|
||||
all_headers = dict(request.headers)
|
||||
authelia_headers = {}
|
||||
|
||||
# Check for common Authelia-related headers
|
||||
auth_related_headers = [
|
||||
'Remote-User', 'X-Remote-User', 'Remote-Groups', 'X-Remote-Groups',
|
||||
'Remote-Name', 'X-Remote-Name', 'Remote-Email', 'X-Remote-Email',
|
||||
'X-Authelia-URL', 'X-Original-URL', 'X-Forwarded-Proto'
|
||||
]
|
||||
|
||||
for header in auth_related_headers:
|
||||
if header.lower() in [h.lower() for h in all_headers.keys()]:
|
||||
for actual_header in all_headers.keys():
|
||||
if header.lower() == actual_header.lower():
|
||||
authelia_headers[actual_header] = all_headers[actual_header]
|
||||
|
||||
# Check for auth cookies
|
||||
auth_cookies = {}
|
||||
for cookie_name in request.cookies:
|
||||
if 'auth' in cookie_name.lower():
|
||||
auth_cookies[cookie_name] = '***' # Hide actual value
|
||||
|
||||
return jsonify({
|
||||
'request_host': request.host,
|
||||
'authelia_user_detected': get_authelia_user() is not None,
|
||||
'authelia_user': get_authelia_user(),
|
||||
'authelia_headers': authelia_headers,
|
||||
'auth_cookies': auth_cookies,
|
||||
'all_headers_count': len(all_headers),
|
||||
'zoraxy_detected': any('zoraxy' in h.lower() for h in all_headers.keys()) or 'X-Forwarded-Server' in all_headers,
|
||||
'host_header': request.headers.get('Host'),
|
||||
'referer': request.headers.get('Referer'),
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Parse command-line arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -314,6 +759,9 @@ Environment Variables (Docker):
|
||||
else:
|
||||
debug_mode = os.getenv('FLASK_DEBUG', 'False').lower() in ('true', '1', 'yes')
|
||||
|
||||
# Set log level based on debug mode
|
||||
logger.setLevel(logging.DEBUG if debug_mode else logging.INFO)
|
||||
|
||||
# Port
|
||||
if args.port is not None:
|
||||
port = args.port
|
||||
@@ -343,4 +791,6 @@ Environment Variables (Docker):
|
||||
print('=' * 60)
|
||||
print()
|
||||
|
||||
logger.info(f"Starting Mailcow Alias Manager - Debug: {debug_mode}, Host: {host}, Port: {port}")
|
||||
|
||||
app.run(debug=debug_mode, host=host, port=port)
|
||||
@@ -8,6 +8,12 @@
|
||||
# Recommended Settings:
|
||||
# - Production: FLASK_ENV=production, FLASK_DEBUG=False (default)
|
||||
# - Development: FLASK_ENV=development, FLASK_DEBUG=True
|
||||
#
|
||||
# Reverse Proxy Support:
|
||||
# ----------------------
|
||||
# This application is configured to work behind reverse proxies (Nginx, Traefik, Zoraxy, Authelia, etc.)
|
||||
# The ProxyFix middleware automatically handles X-Forwarded-* headers for HTTPS detection
|
||||
# No additional configuration needed for most standard proxy setups
|
||||
|
||||
services:
|
||||
mailcow-alias-manager:
|
||||
@@ -33,3 +39,28 @@ services:
|
||||
|
||||
# Host binding (default: 0.0.0.0 for Docker)
|
||||
- FLASK_HOST=0.0.0.0
|
||||
|
||||
# Secret key for sessions - CRITICAL for multi-worker Gunicorn!
|
||||
# All workers must use the SAME secret key to decode session cookies
|
||||
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||
# IMPORATNT! The key below is just an example. GENERATE YOUR OWN KEY WITH COMMAND ABOVE!
|
||||
- SECRET_KEY=dca6920115b8bbc13c346c75d668a49849590c77d9baaf903296582f24ff816a
|
||||
|
||||
# Reverse Proxy Mode
|
||||
# ------------------
|
||||
# Set to 'true' when accessing through reverse proxy (Authelia, Zoraxy, Nginx, etc.)
|
||||
# Set to 'false' for direct IP:port access (e.g., http://192.168.1.100:5172)
|
||||
#
|
||||
# ENABLE_PROXY=true (Proxy Mode):
|
||||
# - ProxyFix middleware enabled (handles X-Forwarded-* headers)
|
||||
# - Secure cookies required (HTTPS only)
|
||||
# - SameSite=None (allows cross-origin authentication)
|
||||
# - Access via: https://alias.yourdomain.com
|
||||
#
|
||||
# ENABLE_PROXY=false (Direct Mode) - DEFAULT:
|
||||
# - ProxyFix middleware disabled
|
||||
# - Standard cookies (HTTP allowed)
|
||||
# - SameSite=Lax (standard browser behavior)
|
||||
# - Access via: http://your-server-ip:5172
|
||||
#
|
||||
- ENABLE_PROXY=false
|
||||
103
reset_password.py
Executable file
103
reset_password.py
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Password Reset Script for Mailcow Alias Manager
|
||||
Usage: python3 reset_password.py [new_password]
|
||||
"""
|
||||
|
||||
import sys
|
||||
import sqlite3
|
||||
import bcrypt
|
||||
from pathlib import Path
|
||||
|
||||
# Database location
|
||||
project_dir = Path(__file__).parent
|
||||
filepath = project_dir.joinpath('data')
|
||||
database = filepath.joinpath('malias2.db')
|
||||
|
||||
def reset_password(new_password):
|
||||
"""Reset the admin password"""
|
||||
|
||||
if not database.exists():
|
||||
print(f"❌ Error: Database not found at {database}")
|
||||
print(" Make sure the application has been run at least once to create the database.")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Hash the new password
|
||||
password_hash = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
|
||||
|
||||
# Update database
|
||||
conn = sqlite3.connect(database)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if auth table exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='auth'")
|
||||
if not cursor.fetchone():
|
||||
print("❌ Error: Auth table not found in database")
|
||||
conn.close()
|
||||
return False
|
||||
|
||||
# Update password
|
||||
cursor.execute('UPDATE auth SET password_hash = ? WHERE id = 0', (password_hash.decode('utf-8'),))
|
||||
|
||||
# Verify update
|
||||
if cursor.rowcount == 0:
|
||||
print("❌ Error: No password record found. Creating new one...")
|
||||
cursor.execute('INSERT INTO auth VALUES (?, ?)', (0, password_hash.decode('utf-8')))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
print("=" * 60)
|
||||
print(" ✅ Password Reset Successful!")
|
||||
print("=" * 60)
|
||||
print(f" New password: {new_password}")
|
||||
print(f" Database: {database}")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("You can now login with the new password.")
|
||||
print()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error resetting password: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(" Mailcow Alias Manager - Password Reset")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Get new password
|
||||
if len(sys.argv) > 1:
|
||||
new_password = sys.argv[1]
|
||||
else:
|
||||
print("Enter new password (or press Ctrl+C to cancel):")
|
||||
new_password = input("New password: ").strip()
|
||||
|
||||
if not new_password:
|
||||
print("❌ Error: Password cannot be empty")
|
||||
return 1
|
||||
|
||||
if len(new_password) < 6:
|
||||
print("⚠️ Warning: Password is less than 6 characters")
|
||||
response = input("Continue anyway? (y/n): ").strip().lower()
|
||||
if response != 'y':
|
||||
print("Password reset cancelled.")
|
||||
return 0
|
||||
|
||||
print()
|
||||
if reset_password(new_password):
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nPassword reset cancelled.")
|
||||
sys.exit(0)
|
||||
@@ -16,6 +16,12 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 16px !important;
|
||||
color: #707170FF;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #1a1a1a;
|
||||
@@ -273,6 +279,13 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: #707170FF;
|
||||
align: center;
|
||||
padding-top: 20%;
|
||||
}
|
||||
|
||||
/* Mobile Responsive Styles */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
@@ -942,6 +955,7 @@
|
||||
try {
|
||||
const response = await fetch('/list_aliases', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -970,6 +984,7 @@
|
||||
try {
|
||||
const response = await fetch(`/${actionName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
@@ -995,6 +1010,7 @@
|
||||
try {
|
||||
const response = await fetch('/search', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -1019,7 +1035,9 @@
|
||||
|
||||
async function openConfig() {
|
||||
try {
|
||||
const response = await fetch('/config');
|
||||
const response = await fetch('/config', {
|
||||
credentials: 'include'
|
||||
});
|
||||
const config = await response.json();
|
||||
|
||||
document.getElementById('mailcowServer').value = config.mailcow_server || '';
|
||||
@@ -1047,6 +1065,7 @@
|
||||
try {
|
||||
const response = await fetch('/config', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -1081,6 +1100,7 @@
|
||||
try {
|
||||
const response = await fetch('/change_password', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -1128,6 +1148,7 @@
|
||||
try {
|
||||
const response = await fetch('/create_alias', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -1176,6 +1197,7 @@
|
||||
try {
|
||||
const response = await fetch('/search', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -1368,6 +1390,7 @@
|
||||
try {
|
||||
const response = await fetch('/delete_aliases_bulk', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -1401,6 +1424,7 @@
|
||||
try {
|
||||
const response = await fetch('/delete_alias', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -1463,6 +1487,7 @@
|
||||
try {
|
||||
const response = await fetch('/create_timed_alias', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -1498,5 +1523,9 @@
|
||||
if (e.target === this) closeTimedAlias();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
<div class="footer">
|
||||
<p style="padding-top:2%">© <a href="https://opensource.org/license/mit">MIT</a> 2026 Rune Olsen <a href="https://blog.rune.pm">Blog</a> — <a href="https://gitlab.pm/rune/malias-web">Source Code</a></p>
|
||||
</div>
|
||||
</html>
|
||||
|
||||
@@ -192,6 +192,7 @@
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include', // CRITICAL: Include cookies in request
|
||||
body: JSON.stringify({ password: password })
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user