From 755367018a521083a5314d275f31717174392876 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 7 Jan 2026 11:29:00 +0100 Subject: [PATCH] feat: Implement core server application with API routing, streaming, and static content serving. --- bin/api/proxy_handler.dart | 60 +++++++++++++++++++------------------- bin/server.dart | 14 ++++----- task.md | 33 +++++++++++---------- 3 files changed, 55 insertions(+), 52 deletions(-) diff --git a/bin/api/proxy_handler.dart b/bin/api/proxy_handler.dart index 4b3dde3..295f625 100644 --- a/bin/api/proxy_handler.dart +++ b/bin/api/proxy_handler.dart @@ -108,41 +108,41 @@ class ProxyHandler { final proxyHeaders = { 'User-Agent': 'VLC/3.0.18 LibVLC/3.0.18', 'Accept': '*/*', - 'Accept-Encoding': 'identity', - 'Connection': 'keep-alive', + 'Accept-Encoding': 'identity', // Explicitly request non-chunked + 'Connection': 'close', // Don't keep-alive to avoid chunked issues }; final client = http.Client(); - final proxyRequest = http.Request(request.method, targetUrl); - proxyRequest.headers.addAll(proxyHeaders); - - // Forward body if POST - if (request.method == 'POST') { - final bodyBytes = await request.read().toList(); - proxyRequest.bodyBytes = bodyBytes.expand((i) => i).toList(); + try { + // Use simple GET/POST instead of streaming to avoid chunked encoding issues + http.Response response; + if (request.method == 'POST') { + final bodyBytes = await request.read().toList(); + final body = bodyBytes.expand((i) => i).toList(); + response = await client.post(targetUrl, headers: proxyHeaders, body: body); + } else { + response = await client.get(targetUrl, headers: proxyHeaders); + } + + // Build response headers + final responseHeaders = { + 'access-control-allow-origin': '*', + 'content-type': response.headers['content-type'] ?? 'application/json', + }; + + if (response.headers['content-length'] != null) { + responseHeaders['content-length'] = response.headers['content-length']!; + } + + return Response( + response.statusCode, + body: response.bodyBytes, + headers: responseHeaders, + ); + } finally { + client.close(); } - // Streaming Response - final streamedResponse = await client.send(proxyRequest); - - // Filter headers - final responseHeaders = { - 'access-control-allow-origin': '*', - }; - - streamedResponse.headers.forEach((key, value) { - if (_allowedHeaders.contains(key.toLowerCase())) { - responseHeaders[key] = value; - } - }); - - // Pipe the stream directly to the response - return Response( - streamedResponse.statusCode, - body: streamedResponse.stream, - headers: responseHeaders, - ); - } catch (e, stackTrace) { print('Proxy error: $e'); print(stackTrace); diff --git a/bin/server.dart b/bin/server.dart index 83dbef5..83cc683 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -46,24 +46,23 @@ void main(List args) async { final user = request.context['user'] as User?; // If we have a user from auth middleware, prefer their playlists if (user != null) { + print('[getPlaylist] User from context: ${user.username}'); final playlists = db.getPlaylists(user.id); if (playlists.isNotEmpty) playlist = playlists.first; } else { - // Fallback: This leg should effectively be unreachable for /api/xtream if we use authMiddleware, - // but might be used by other handlers or if auth is optional somewhere. - // For VOD/Live which currently might not have auth (URL tokenizing is complex), we keep fallback? - // WAIT: The plan said "Secure /api/xtream using authMiddleware". - // If we do that, user is GUARANTEED for proxy. - // But for /api/live and /api/vod, we might need a different strategy (token in URL). - // For now, let's keep the fallback logic for non-proxy parts if any. + // Fallback for proxy: get first user's first playlist + print('[getPlaylist] No user in context, using fallback'); final users = db.getAllUsers(); + print('[getPlaylist] Total users in DB: ${users.length}'); if (users.isNotEmpty) { final playlists = db.getPlaylists(users[0].id); + print('[getPlaylist] User ${users[0].username} has ${playlists.length} playlists'); if (playlists.isNotEmpty) playlist = playlists.first; } } if (playlist != null) { + print('[getPlaylist] Returning playlist: ${playlist.name} (DNS: ${playlist.serverUrl})'); return PlaylistConfig( id: playlist.id, name: playlist.name, @@ -74,6 +73,7 @@ void main(List args) async { isActive: true, ); } + print('[getPlaylist] WARNING: No playlist found, returning null'); return null; } diff --git a/task.md b/task.md index ed9293b..6eec729 100644 --- a/task.md +++ b/task.md @@ -1,21 +1,24 @@ -# Fix Docker Database & Proxy Issues +# Fix Docker & Proxy Issues -## Context +## Completed -1. SQLite database read-only error on login (permissions) -2. Xtream proxy 401 Unauthorized errors +- [x] Database permissions (`entrypoint.sh` + `Dockerfile`) +- [x] Proxy 401 errors (removed broken mount in `server.dart`) +- [x] Proxy chunked encoding error (buffered response in `proxy_handler.dart`) -## Master Plan +## Changes -- [x] Analyze database readonly error → volume permissions mismatch -- [x] Create `entrypoint.sh` with gosu for privilege drop -- [x] Update `Dockerfile` to use entrypoint -- [x] Analyze proxy 401 errors → broken mount in apiRouter -- [x] Remove duplicate `/api/xtream` mount from `server.dart` -- [ ] Rebuild and deploy +| File | Change | +|------|--------| +| `entrypoint.sh` | NEW - fixes /app/data permissions, runs as xtremuser via gosu | +| `Dockerfile` | Added gosu, ENTRYPOINT | +| `bin/server.dart` | Removed broken /api/xtream mount, added logging | +| `bin/api/proxy_handler.dart` | Changed from streaming to buffered response | -## Changes Made +## Deploy -- `entrypoint.sh`: Fixes /app/data permissions at startup, then runs as xtremuser -- `Dockerfile`: Added gosu, ENTRYPOINT, removed USER directive -- `server.dart`: Removed broken /api/xtream mount that was blocking proxy +```bash +git add . +git commit -m "fix: database permissions + proxy errors" +git push && docker compose up --build -d +```