using System.Linq; using System.Security.Cryptography; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; namespace VideoDownloader.Server; /// /// M0 원격 접근 서버: 데스크톱 앱에 내장되는 Kestrel. /// QR 페어링 토큰(Bearer) 인증 + 큐 API + 라이브러리/Range 스트리밍 + 제로 설치 웹 컴패니언. /// LAN 직결(계정/클라우드 없음) — MOBILE_PLAN §3 아키텍처. /// public sealed class ServerHost : IAsyncDisposable { private readonly WebApplication _app; private readonly string _token; private readonly ServerOptions _options; public int Port { get; } /// QR 에 담을 페어링 URL 형식: http://<lan-ip>:<port>/#t=<token> public string Token => _token; private ServerHost(WebApplication app, int port, string token, ServerOptions options) { _app = app; Port = port; _token = token; _options = options; } public static async Task StartAsync(ServerOptions options) { var token = string.IsNullOrEmpty(options.Token) ? WebEncoders.Base64UrlEncode(RandomNumberGenerator.GetBytes(32)) : options.Token; var builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = new[] { "--urls", $"http://0.0.0.0:{(options.Port == 0 ? GetFreePort() : options.Port)}" } }); foreach (var prov in builder.Services.Where(sd => sd.ServiceType.FullName!.Contains("LoggerProvider")).ToList()) builder.Services.Remove(prov); var app = builder.Build(); // Bearer 토큰 인증(상수시간 비교) — / 와 /api/* 전체 적용 app.Use(async (ctx, next) => { if (!ctx.Request.Path.StartsWithSegments("/api/pair") && (ctx.Request.Path.StartsWithSegments("/api") || ctx.Request.Path.StartsWithSegments("/stream") || ctx.Request.Path.StartsWithSegments("/file"))) { var auth = ctx.Request.Headers.Authorization.ToString(); var provided = auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) ? auth[7..].Trim() : ""; //