HTTPS/CORS improvements: Enable CORS for player connections, secure session cookies, add certificate endpoint, nginx CORS headers
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user