diff --git a/Dockerfile b/Dockerfile index aa30ca5..0328a37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,8 @@ RUN mkdir -p /app/downloads COPY --from=build /app/publish . +# Only expose the main proxy port (8080) +# Admin UI runs on 5275 but is NOT exposed - access via docker exec or SSH tunnel EXPOSE 8080 -ENV ASPNETCORE_URLS=http://+:8080 ENTRYPOINT ["dotnet", "allstarr.dll"] diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index ea38a0c..f13416b 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Options; using allstarr.Models.Settings; using allstarr.Services.Spotify; using allstarr.Services.Common; +using allstarr.Filters; using System.Text.Json; using System.Text.RegularExpressions; @@ -11,9 +12,11 @@ namespace allstarr.Controllers; /// /// Admin API controller for the web dashboard. /// Provides endpoints for viewing status, playlists, and modifying configuration. +/// Only accessible on internal admin port (5275) - not exposed through reverse proxy. /// [ApiController] [Route("api/admin")] +[ServiceFilter(typeof(AdminPortFilter))] public class AdminController : ControllerBase { private readonly ILogger _logger; diff --git a/allstarr/Filters/AdminPortFilter.cs b/allstarr/Filters/AdminPortFilter.cs new file mode 100644 index 0000000..b69cb8c --- /dev/null +++ b/allstarr/Filters/AdminPortFilter.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace allstarr.Filters; + +/// +/// Filter that restricts access to admin endpoints to only the admin port (5275). +/// This prevents the admin API from being accessed through the main proxy port. +/// +public class AdminPortFilter : IActionFilter +{ + private const int AdminPort = 5275; + + public void OnActionExecuting(ActionExecutingContext context) + { + var requestPort = context.HttpContext.Connection.LocalPort; + + if (requestPort != AdminPort) + { + context.Result = new NotFoundResult(); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + // No action needed after execution + } +} diff --git a/allstarr/Middleware/AdminStaticFilesMiddleware.cs b/allstarr/Middleware/AdminStaticFilesMiddleware.cs new file mode 100644 index 0000000..14e7965 --- /dev/null +++ b/allstarr/Middleware/AdminStaticFilesMiddleware.cs @@ -0,0 +1,51 @@ +namespace allstarr.Middleware; + +/// +/// Middleware that only serves static files on the admin port (5275). +/// This keeps the admin UI isolated from the main proxy port. +/// +public class AdminStaticFilesMiddleware +{ + private readonly RequestDelegate _next; + private readonly StaticFileMiddleware _staticFileMiddleware; + private readonly DefaultFilesMiddleware _defaultFilesMiddleware; + private const int AdminPort = 5275; + + public AdminStaticFilesMiddleware( + RequestDelegate next, + IWebHostEnvironment env, + ILoggerFactory loggerFactory) + { + _next = next; + + var staticFileOptions = new StaticFileOptions(); + var defaultFilesOptions = new DefaultFilesOptions(); + + _staticFileMiddleware = new StaticFileMiddleware( + _next, + env, + Microsoft.Extensions.Options.Options.Create(staticFileOptions), + loggerFactory); + + _defaultFilesMiddleware = new DefaultFilesMiddleware( + (ctx) => _staticFileMiddleware.Invoke(ctx), + env, + Microsoft.Extensions.Options.Options.Create(defaultFilesOptions)); + } + + public async Task InvokeAsync(HttpContext context) + { + var port = context.Connection.LocalPort; + + if (port == AdminPort) + { + // Serve static files on admin port + await _defaultFilesMiddleware.Invoke(context); + } + else + { + // Skip static files on main port + await _next(context); + } + } +} diff --git a/allstarr/Program.cs b/allstarr/Program.cs index 4febdde..ac3af6c 100644 --- a/allstarr/Program.cs +++ b/allstarr/Program.cs @@ -43,11 +43,18 @@ static List DecodeSquidWtfUrls() var backendType = builder.Configuration.GetValue("Backend:Type"); // Configure Kestrel for large responses over VPN/Tailscale +// Also configure admin port on 5275 (internal only, not exposed) builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.Limits.MaxResponseBufferSize = null; // Disable response buffering limit serverOptions.Limits.MaxRequestBodySize = null; // Allow large request bodies serverOptions.Limits.MinResponseDataRate = null; // Disable minimum data rate for slow connections + + // Main proxy port (exposed) + serverOptions.ListenAnyIP(8080); + + // Admin UI port (internal only - do NOT expose through reverse proxy) + serverOptions.ListenAnyIP(5275); }); // Add response compression for large JSON responses (helps with Tailscale/VPN MTU issues) @@ -99,6 +106,9 @@ builder.Services.AddHttpContextAccessor(); builder.Services.AddExceptionHandler(); builder.Services.AddProblemDetails(); +// Admin port filter (restricts admin API to port 5275) +builder.Services.AddScoped(); + // Configuration - register both settings, active one determined by backend type builder.Services.Configure( builder.Configuration.GetSection("Subsonic")); @@ -565,9 +575,8 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); -// Serve static files from wwwroot -app.UseDefaultFiles(); -app.UseStaticFiles(); +// Serve static files only on admin port (5275) +app.UseMiddleware(); app.UseAuthorization(); @@ -578,9 +587,6 @@ app.MapControllers(); // Health check endpoint for monitoring app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow })); -// Admin dashboard redirect -app.MapGet("/admin", () => Results.Redirect("/index.html")); - app.Run(); /// diff --git a/docker-compose.yml b/docker-compose.yml index ffc39c4..2016eed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,9 @@ services: restart: unless-stopped ports: - "5274:8080" + # Admin UI on port 5275 - ONLY expose if you need local access + # DO NOT expose through reverse proxy - contains sensitive config + # - "5275:5275" depends_on: redis: condition: service_healthy