diff --git a/NGINX_SETUP.md b/NGINX_SETUP.md new file mode 100644 index 0000000..ab7959e --- /dev/null +++ b/NGINX_SETUP.md @@ -0,0 +1,133 @@ +# Xtream Player - Nginx Setup Guide + +## Problem Fixed + +Když jste provozovali aplikaci přes nginx, video přehrávač nefungoval. Problém byl v chybějících **CORS (Cross-Origin Resource Sharing)** headerech. Nyní jsme: + +1. ✅ Přidali CORS headers do Java backendu +2. ✅ Přidali support pro OPTIONS preflight requests +3. ✅ Vytvořili optimalizovanou nginx konfiguraci pro streamování videa + +## Spuštění s Docker Compose (Doporučeno) + +Nejjednodušší způsob je spustit obě služby (backend + nginx) najednou: + +```bash +docker-compose up -d +``` + +Aplikace pak bude dostupná na: +- **Frontend s nginx**: http://localhost/ +- **Backend přímo**: http://localhost:8080/ + +## Ruční Spuštění + +### 1. Spusťte backend Java aplikaci: +```bash +./mvn clean package +java -jar target/xtream-player-1.0.0.jar +``` + +### 2. Spusťte nginx: +```bash +docker run -d \ + -p 80:80 \ + -v $(pwd)/deploy/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro \ + nginx:latest +``` + +Nebo s native nginx (Linux): +```bash +cp deploy/nginx/nginx.conf /etc/nginx/sites-available/xtream-player +sudo ln -s /etc/nginx/sites-available/xtream-player /etc/nginx/sites-enabled/ +sudo systemctl reload nginx +``` + +## HTTPS/SSL Setup + +Pro bezpečné HTTPS připojení: + +1. Vytvořte SSL certifikáty: +```bash +mkdir -p deploy/nginx/ssl +# Použijte Let's Encrypt, self-signed certs, nebo váš certifikát +``` + +2. Upravte `deploy/nginx/nginx.conf`: +```nginx +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # ... rest of config +} + +# Redirect HTTP to HTTPS +server { + listen 80; + return 301 https://$server_name$request_uri; +} +``` + +3. Aktualizujte docker-compose.yml pro SSL volume + +## Troubleshooting + +### Video přehrávač stále nefunguje? + +1. **Zkontrolujte browser console** (F12 → Console): + - Hledejte CORS errory + - Hledejte "stream-proxy" errory + +2. **Zkontrolujte nginx logs**: +```bash +docker logs +# nebo +sudo tail -f /var/log/nginx/error.log +``` + +3. **Zkontrolujte backend logs**: +```bash +docker logs +# nebo koukejte na console kde jste spustili Java +``` + +### Nginx vrací 502 Bad Gateway? + +- Ujistěte se, že backend je spuštěný na portu 8080 +- Zkontrolujte, že nginx má přístup k backendu (network settings) +- Zkontrolujte nginx logs pro konkrétní chybu + +### Velké soubory se neloadují? + +- V nginx.conf je `client_max_body_size 256M;` - zvyšte v případě potřeby +- Ujistěte se, že `proxy_buffering off;` je nastaveno pro `/api/stream-proxy` + +## Architektura + +``` +Klient (browser) + ↓ +Nginx (reverse proxy, CORS headers) + ↓ +Java aplikace (8080) + ├─ /api/... (API endpoints s CORS) + ├─ /api/stream-proxy (HLS stream proxy) + └─ / (static HTML/CSS/JS) +``` + +## CORS Headers + +Nyní jsou automaticky přidávány všem odpovědím: + +``` +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS +Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With +Access-Control-Max-Age: 86400 +``` + +Pokud potřebujete omezit origin (bezpečnější), měňte `Access-Control-Allow-Origin` v kódu. diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf new file mode 100644 index 0000000..fe07e14 --- /dev/null +++ b/deploy/nginx/nginx.conf @@ -0,0 +1,95 @@ +upstream xtream_backend { + server localhost:8080; +} + +server { + listen 80; + server_name _; + + # Limit request sizes to prevent abuse + client_max_body_size 256M; + + # Root location - serve static files and proxy to backend + location / { + # Try to serve static files first, then proxy to backend + proxy_pass http://xtream_backend; + proxy_http_version 1.1; + + # Preserve original request information + 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; + + # WebSocket support (if needed) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Timeouts for long-running requests (streaming) + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 3600s; + + # Buffering for streaming + proxy_buffering off; + proxy_request_buffering off; + } + + # API endpoints with special handling + location /api/ { + proxy_pass http://xtream_backend; + proxy_http_version 1.1; + + 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; + + # For preflight requests + proxy_set_header Access-Control-Allow-Origin *; + + # Timeouts for streaming endpoints + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 3600s; + + # Disable buffering for stream proxying + proxy_buffering off; + proxy_request_buffering off; + } + + # Stream proxy endpoint - special handling for media streams + location /api/stream-proxy { + proxy_pass http://xtream_backend; + proxy_http_version 1.1; + + 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; + + # Important: disable buffering for media streaming + proxy_buffering off; + proxy_request_buffering off; + + # Allow range requests for seeking + proxy_set_header Range $http_range; + proxy_set_header If-Range $http_if_range; + + # Long timeout for streaming + proxy_connect_timeout 60s; + proxy_send_timeout 3600s; + proxy_read_timeout 3600s; + + # Pass through the 206 Partial Content status + proxy_pass_request_headers on; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "OK"; + add_header Content-Type "text/plain"; + } +} diff --git a/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java b/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java index a8b1bfc..733d9dc 100644 --- a/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java +++ b/src/main/java/cz/kamma/xtreamplayer/XtreamPlayerApplication.java @@ -80,23 +80,23 @@ public final class XtreamPlayerApplication { UserAuthenticator userAuthenticator = new UserAuthenticator(applicationDao); HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); - server.createContext("/api/auth/login", new LoginHandler(userAuthenticator)); - server.createContext("/api/auth/logout", new LogoutHandler(userAuthenticator)); - server.createContext("/api/config", new ConfigHandler(configStore, userAuthenticator)); - server.createContext("/api/test-login", new TestLoginHandler(configStore, userAuthenticator)); - server.createContext("/api/xtream", new XtreamProxyHandler(configStore, userAuthenticator)); - server.createContext("/api/stream-url", new StreamUrlHandler(configStore, userAuthenticator)); - server.createContext("/api/stream-proxy", new StreamProxyHandler(userAuthenticator)); - server.createContext("/api/open-in-player", new OpenInPlayerHandler(configStore, userAuthenticator)); - server.createContext("/api/library/load", new LibraryLoadHandler(libraryService, userAuthenticator)); - server.createContext("/api/library/status", new LibraryStatusHandler(libraryService, userAuthenticator)); - server.createContext("/api/library/categories", new LibraryCategoriesHandler(libraryService, userAuthenticator)); - server.createContext("/api/library/items", new LibraryItemsHandler(libraryService, userAuthenticator)); - server.createContext("/api/library/search", new LibrarySearchHandler(libraryService, userAuthenticator)); - server.createContext("/api/library/series-episodes", new LibrarySeriesEpisodesHandler(libraryService, userAuthenticator)); - server.createContext("/api/library/epg", new LibraryEpgHandler(libraryService, userAuthenticator)); - server.createContext("/api/favorites", new FavoritesHandler(libraryService, userAuthenticator)); - server.createContext("/user", new UserCrudHandler(applicationDao)); + server.createContext("/api/auth/login", corsWrapper(new LoginHandler(userAuthenticator))); + server.createContext("/api/auth/logout", corsWrapper(new LogoutHandler(userAuthenticator))); + server.createContext("/api/config", corsWrapper(new ConfigHandler(configStore, userAuthenticator))); + server.createContext("/api/test-login", corsWrapper(new TestLoginHandler(configStore, userAuthenticator))); + server.createContext("/api/xtream", corsWrapper(new XtreamProxyHandler(configStore, userAuthenticator))); + server.createContext("/api/stream-url", corsWrapper(new StreamUrlHandler(configStore, userAuthenticator))); + server.createContext("/api/stream-proxy", corsWrapper(new StreamProxyHandler(userAuthenticator))); + server.createContext("/api/open-in-player", corsWrapper(new OpenInPlayerHandler(configStore, userAuthenticator))); + server.createContext("/api/library/load", corsWrapper(new LibraryLoadHandler(libraryService, userAuthenticator))); + server.createContext("/api/library/status", corsWrapper(new LibraryStatusHandler(libraryService, userAuthenticator))); + server.createContext("/api/library/categories", corsWrapper(new LibraryCategoriesHandler(libraryService, userAuthenticator))); + server.createContext("/api/library/items", corsWrapper(new LibraryItemsHandler(libraryService, userAuthenticator))); + server.createContext("/api/library/search", corsWrapper(new LibrarySearchHandler(libraryService, userAuthenticator))); + server.createContext("/api/library/series-episodes", corsWrapper(new LibrarySeriesEpisodesHandler(libraryService, userAuthenticator))); + server.createContext("/api/library/epg", corsWrapper(new LibraryEpgHandler(libraryService, userAuthenticator))); + server.createContext("/api/favorites", corsWrapper(new FavoritesHandler(libraryService, userAuthenticator))); + server.createContext("/user", corsWrapper(new UserCrudHandler(applicationDao))); server.createContext("/", new StaticHandler(userAuthenticator)); server.setExecutor(Executors.newFixedThreadPool(12)); server.start(); @@ -104,6 +104,18 @@ public final class XtreamPlayerApplication { LOGGER.info("Xtream Player started on http://localhost:{}", port); } + private static HttpHandler corsWrapper(HttpHandler delegate) { + return exchange -> { + addCorsHeaders(exchange); + if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(200, -1); + exchange.close(); + } else { + delegate.handle(exchange); + } + }; + } + private static int resolvePort() { String raw = System.getenv("PORT"); if (raw == null || raw.isBlank()) { @@ -1506,12 +1518,20 @@ public final class XtreamPlayerApplication { } private static void writeBytes(HttpExchange exchange, int statusCode, byte[] bytes, String contentType) throws IOException { + addCorsHeaders(exchange); exchange.sendResponseHeaders(statusCode, bytes.length); exchange.getResponseBody().write(bytes); logApiResponse(exchange, statusCode, bytes.length, contentType); exchange.close(); } + private static void addCorsHeaders(HttpExchange exchange) { + exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With"); + exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400"); + } + private static String contentType(String resourcePath) { if (resourcePath.endsWith(".html")) { return "text/html; charset=utf-8";