using System.Linq; using System.Security.Cryptography; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; 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; private readonly Action? _unsubscribeQueueChanged; private readonly MdnsAdvertiser? _mdns; public int Port { get; } /// QR 에 담을 페어링 URL 형식: http://<lan-ip>:<port>/#t=<token> public string Token => _token; /// mDNS 광고(_videodl._tcp) 동작 여부. EnableMdns=true 인데 false 면 MdnsError 를 본다. public bool MdnsActive => _mdns != null; /// mDNS 시작 실패 원인(발견 보조 기능이라 서버 기동은 막지 않고 여기로 드러냄). public Exception? MdnsError { get; } private ServerHost(WebApplication app, int port, string token, ServerOptions options, Action? unsubscribeQueueChanged, MdnsAdvertiser? mdns, Exception? mdnsError) { _app = app; Port = port; _token = token; _options = options; _unsubscribeQueueChanged = unsubscribeQueueChanged; _mdns = mdns; MdnsError = mdnsError; } 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); builder.Services.AddSingleton(options.Queue); builder.Services.AddSignalR(); var app = builder.Build(); app.UseWebSockets(); // SignalR 웹소켓 전송용 (없으면 롱폴링만 됨) // 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") || ctx.Request.Path.StartsWithSegments("/hubs"))) { var auth = ctx.Request.Headers.Authorization.ToString(); var provided = auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) ? auth[7..].Trim() : ""; //