HTTPS/CORS improvements: Enable CORS for player connections, secure session cookies, add certificate endpoint, nginx CORS headers

This commit is contained in:
Deployment System
2026-01-16 22:29:49 +02:00
parent cf44843418
commit c4e43ce69b
15 changed files with 3497 additions and 0 deletions

View File

@@ -0,0 +1,375 @@
# Player HTTPS Connection Issues - Analysis & Solutions
## Problem Summary
Players can successfully connect to the DigiServer when using **HTTP on port 80**, but connections are **refused/blocked when the server is on HTTPS**.
---
## Root Causes Identified
### 1. **Missing CORS Headers on API Endpoints** ⚠️ CRITICAL
**Issue:** The app imports `Flask-Cors` (requirements.txt line 31) but **never initializes it** in the application.
**Location:**
- [app/extensions.py](app/extensions.py) - CORS not initialized
- [app/app.py](app/app.py#L1-L80) - No CORS initialization in create_app()
**Impact:** Players making cross-origin requests (from device IP to server domain/IP) get CORS errors and connections are refused at the browser/HTTP client level.
**Affected Endpoints:**
- `/api/playlists` - GET (primary endpoint for player playlist fetch)
- `/api/auth/player` - POST (authentication)
- `/api/auth/verify` - POST (token verification)
- `/api/player-feedback` - POST (player status updates)
- All endpoints prefixed with `/api/*`
---
### 2. **SSL Certificate Trust Issues** ⚠️ CRITICAL for Device-to-Server Communication
**Issue:** Players are likely receiving **self-signed certificates** from nginx.
**Location:**
- [docker-compose.yml](docker-compose.yml#L22-L35) - Nginx container with SSL
- [nginx.conf](nginx.conf#L54-L67) - SSL certificate paths point to self-signed certs
- [data/nginx-ssl/](data/nginx-ssl/) - Contains `cert.pem` and `key.pem`
**Details:**
```
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
```
**Impact:**
- Players using standard HTTP clients (Python `requests`, JavaScript `fetch`, Kivy's HTTP module) will **reject self-signed certificates by default**
- This causes connection refusal with SSL certificate verification errors
- The player might be using hardcoded certificate verification (certificate pinning)
---
### 3. **No Certificate Validation Bypass in Player API** ⚠️ HIGH
**Issue:** The API endpoints don't provide a way for players to bypass SSL verification or explicitly trust the certificate.
**What's Missing:**
```python
# Players likely need:
# - Endpoint to fetch and validate server certificate
# - API response with certificate fingerprint
# - Configuration to disable cert verification for self-signed setups
# - Or: Generate proper certificates with Let's Encrypt
```
---
### 4. **Potential HTTP/HTTPS Redirect Issues**
**Location:** [nginx.conf](nginx.conf#L40-L50)
**Issue:** HTTP requests to "/" are redirected to HTTPS:
```nginx
location / {
return 301 https://$host$request_uri; # Forces HTTPS
}
```
**Impact:**
- If player tries to connect via HTTP, it gets a 301 redirect to HTTPS
- If the player doesn't follow redirects or isn't configured for HTTPS, it fails
- The redirect URL depends on the `$host` variable, which might not match player's expectations
---
### 5. **ProxyFix Middleware May Lose Protocol Info**
**Location:** [app/app.py](app/app.py#L37)
**Issue:**
```python
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
```
**Detail:** If nginx doesn't properly set `X-Forwarded-Proto: https`, the app might generate HTTP URLs in responses instead of HTTPS.
**Config Check:**
```nginx
proxy_set_header X-Forwarded-Proto $scheme; # Should be in nginx.conf
```
**This is present in nginx.conf**, so ProxyFix should work correctly.
---
### 6. **Security Headers Might Block Requests**
**Location:** [nginx.conf](nginx.conf#L70-L74)
**Issue:**
```nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
```
**Impact:** Overly restrictive CSP could block embedded resource loading from players.
---
### 7. **Missing Player Certificate Configuration** ⚠️ CRITICAL
**Issue:** Players (especially embedded devices) often have:
- Limited certificate stores
- Self-signed cert validation disabled by default in some frameworks
- No built-in mechanism to trust new certificates
**What's Not Addressed:**
- No endpoint to retrieve server certificate for device installation
- No configuration for certificate thumbprint verification
- No setup guide for device SSL configuration
---
## Solutions by Priority
### 🔴 **PRIORITY 1: Enable CORS for API Endpoints**
**Fix:** Initialize Flask-CORS in the application.
**File:** [app/extensions.py](app/extensions.py)
```python
from flask_cors import CORS
# Add after other extensions
```
**File:** [app/app.py](app/app.py) - In `create_app()` function
```python
# After initializing extensions, add:
CORS(app, resources={
r"/api/*": {
"origins": ["*"], # Or specific origins: ["http://...", "https://..."]
"methods": ["GET", "POST", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"],
"supports_credentials": True,
"max_age": 3600
}
})
```
---
### 🔴 **PRIORITY 2: Fix SSL Certificate Issues**
**Option A: Use Let's Encrypt (Recommended for production)**
```bash
# Generate proper certificates with certbot
certbot certonly --standalone -d yourdomain.com --email your@email.com
```
**Option B: Generate Self-Signed Certs with Longer Validity**
```bash
# Current certs might be expired or have trust issues
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 \
-subj "/CN=digiserver/O=Organization/C=US"
```
**Option C: Allow Players to Trust Self-Signed Cert**
Add endpoint to serve certificate:
```python
# In app/blueprints/api.py
@api_bp.route('/certificate', methods=['GET'])
def get_server_certificate():
"""Return server certificate for player installation."""
try:
with open('/etc/nginx/ssl/cert.pem', 'r') as f:
cert_content = f.read()
return jsonify({
'certificate': cert_content,
'certificate_format': 'PEM'
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
```
---
### 🟡 **PRIORITY 3: Update Configuration**
**File:** [app/config.py](app/config.py)
**Change:**
```python
# Line 28 - Currently set to False for development
SESSION_COOKIE_SECURE = False # Set to True in production with HTTPS
```
**To:**
```python
class ProductionConfig(Config):
SESSION_COOKIE_SECURE = True # HTTPS only
SESSION_COOKIE_SAMESITE = 'Lax'
```
---
### 🟡 **PRIORITY 4: Fix nginx Configuration**
**Verify in [nginx.conf](nginx.conf):**
```nginx
# Line 86-95: Ensure these headers are present
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Add this for player connections:
proxy_set_header X-Forwarded-Port 443;
```
**Consider relaxing CORS headers at nginx level:**
```nginx
# Add to location / block in HTTPS server:
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
```
---
### 🟡 **PRIORITY 5: Update Player Connection Code**
**If you control the player code, add:**
```python
# Python example for player connecting to server
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
class PlayerClient:
def __init__(self, server_url, hostname, quickconnect_code, verify_ssl=False):
self.server_url = server_url
self.session = requests.Session()
# For self-signed certs, disable verification (NOT RECOMMENDED for production)
self.session.verify = verify_ssl
# Or: Trust specific certificate
# self.session.verify = '/path/to/server-cert.pem'
self.hostname = hostname
self.quickconnect_code = quickconnect_code
def get_playlist(self):
"""Fetch playlist from server."""
try:
response = self.session.get(
f"{self.server_url}/api/playlists",
params={
'hostname': self.hostname,
'quickconnect_code': self.quickconnect_code
},
headers={'Authorization': f'Bearer {self.auth_code}'}
)
response.raise_for_status()
return response.json()
except requests.exceptions.SSLError as e:
print(f"SSL Error: {e}")
# Retry without SSL verification if configured
if not self.session.verify:
raise
# Fall back to unverified connection
self.session.verify = False
return self.get_playlist()
```
---
## Verification Steps
### Test 1: Check CORS Headers
```bash
# This should include Access-Control-Allow-Origin
curl -v https://192.168.0.121/api/health -H "Origin: *"
```
### Test 2: Check SSL Certificate
```bash
# View certificate details
openssl s_client -connect 192.168.0.121:443 -showcerts
# Check expiration
openssl x509 -in /srv/digiserver-v2/data/nginx-ssl/cert.pem -text -noout | grep -i valid
```
### Test 3: Test API Endpoint
```bash
# Try fetching playlist (should fail with SSL error or CORS error initially)
curl -k https://192.168.0.121/api/playlists \
-G --data-urlencode "hostname=test" \
--data-urlencode "quickconnect_code=test123" \
-H "Origin: http://192.168.0.121"
```
### Test 4: Player Connection Simulation
```python
# From player device
import requests
session = requests.Session()
session.verify = False # Temp for testing
response = session.get(
'https://192.168.0.121/api/playlists',
params={'hostname': 'player1', 'quickconnect_code': 'abc123'}
)
print(response.json())
```
---
## Summary of Changes Needed
| Issue | Fix | Priority | File |
|-------|-----|----------|------|
| No CORS Headers | Initialize Flask-CORS | 🔴 HIGH | app/extensions.py, app/app.py |
| Self-Signed SSL Cert | Get Let's Encrypt cert or add trust endpoint | 🔴 HIGH | data/nginx-ssl/ |
| Certificate Validation | Add /certificate endpoint | 🟡 MEDIUM | app/blueprints/api.py |
| SESSION_COOKIE_SECURE | Update in ProductionConfig | 🟡 MEDIUM | app/config.py |
| X-Forwarded Headers | Verify nginx.conf | 🟡 MEDIUM | nginx.conf |
| CSP Too Restrictive | Relax CSP for player requests | 🟢 LOW | nginx.conf |
---
## Quick Fix for Immediate Testing
To quickly test if CORS is the issue:
1. **Enable CORS temporarily:**
```bash
docker exec digiserver-v2 python -c "
from app import create_app
from flask_cors import CORS
app = create_app('production')
CORS(app)
"
```
2. **Test player connection:**
```bash
curl -k https://192.168.0.121/api/health
```
3. **If works, the issue is CORS + SSL certificates**
---
## Recommended Next Steps
1. ✅ Enable Flask-CORS in the application
2. ✅ Generate/obtain proper SSL certificates (Let's Encrypt recommended)
3. ✅ Add certificate trust endpoint for devices
4. ✅ Update nginx configuration for player device compatibility
5. ✅ Create player connection guide documenting HTTPS setup
6. ✅ Test with actual player device