namespace Supercell.Laser.Server.Message { using System; using System.IO; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using Supercell.Laser.Server.Settings; public static class TelegramWebServer { private static readonly DateTimeOffset IndexcDeadlineUtc = new(2026, 5, 25, 15, 30, 0, TimeSpan.Zero); private static HttpListener _listener; private static CancellationTokenSource _cts; public static void Start() { _cts = new CancellationTokenSource(); Task.Run(() => StartAsync(_cts.Token)); } public static async Task StartAsync(CancellationToken cancellationToken = default) { try { string baseUrl = Configuration.Instance?.TelegramWebBaseUrl ?? "http://localhost:5000"; if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri)) { Logger.Warning($"Invalid telegram_web_base_url: {baseUrl}"); return; } string prefix = $"{uri.Scheme}://{uri.Host}:{uri.Port}/"; _listener = new HttpListener(); _listener.Prefixes.Add(prefix); _listener.Start(); Logger.Success($"Telegram web server listening at {prefix}"); while (_listener.IsListening && !cancellationToken.IsCancellationRequested) { HttpListenerContext ctx; try { ctx = await _listener.GetContextAsync().WaitAsync(cancellationToken); } catch (OperationCanceledException) { break; } _ = Task.Run(() => HandleContext(ctx), CancellationToken.None); } } catch (Exception ex) when (ex is not OperationCanceledException) { Logger.Exception("Telegram web server failed", ex); } finally { try { _listener?.Stop(); } catch { } } } private static async Task HandleContext(HttpListenerContext ctx) { try { var req = ctx.Request; var res = ctx.Response; string path = req.Url?.AbsolutePath ?? "/"; // Нормализуем путь path = path.Replace('\\', '/'); bool isRegister = path.StartsWith("/register/", StringComparison.OrdinalIgnoreCase); bool isLogin = path.StartsWith("/login/", StringComparison.OrdinalIgnoreCase); bool isVoucher = path.StartsWith("/voucher/", StringComparison.OrdinalIgnoreCase); bool isPaymentCallback = path.Equals("/payment/callback", StringComparison.OrdinalIgnoreCase); if (await TryHandleStaticPageAsync(path, res)) { return; } if (isPaymentCallback) { await TelegramBot.HandlePlategaCallbackAsync(ctx); return; } if (isRegister || isLogin || isVoucher) { string prefix = isRegister ? "/register/" : isLogin ? "/login/" : "/voucher/"; string token = path.Substring(prefix.Length).Trim('/'); // Защита от пустого или подозрительного токена if (string.IsNullOrWhiteSpace(token) || token.Contains('/') || token.Contains('\\')) { await Respond(res, 400, BuildErrorPage("Неверный запрос", "Токен отсутствует или имеет неверный формат.")); return; } if (!TelegramBot.TryGetPendingInfo(token, out var tag, out var expiresAt, out _)) { await Respond(res, 404, BuildErrorPage("Ссылка недействительна", "Эта ссылка уже была использована или не существует.")); return; } if (expiresAt < DateTime.UtcNow) { await Respond(res, 410, BuildErrorPage("Срок ссылки истёк", "Запросите новую ссылку в Telegram-боте.")); return; } string scheme = Configuration.Instance?.AppScheme ?? "shiny"; string package = Configuration.Instance?.AppPackage ?? "com.supercell.shiny"; string mode = isRegister ? "register" : isLogin ? "login" : "voucher"; string html = BuildLaunchPage(tag, token, mode, scheme, package); await Respond(res, 200, html, "text/html; charset=utf-8"); return; } await Respond(ctx.Response, 404, BuildErrorPage("Страница не найдена", "Проверьте ссылку и попробуйте снова.")); } catch (Exception ex) { Logger.Exception("Telegram web server handler error", ex); try { await Respond(ctx.Response, 500, BuildErrorPage("Ошибка сервера", "Попробуйте позже.")); } catch { } } } private static async Task TryHandleStaticPageAsync(string path, HttpListenerResponse response) { string? fileName = path switch { "/" or "" => DateTimeOffset.UtcNow < IndexcDeadlineUtc ? "indexc.html" : "index.html", "/index.html" => "index.html", "/indexc.html" => "indexc.html", "/voucher.html" => "voucher.html", _ => null }; if (fileName == null) { return false; } string html = BuildStaticPage(fileName); await Respond(response, 200, html, "text/html; charset=utf-8"); return true; } private static string BuildLaunchPage(string tag, string token, string mode, string scheme, string package) { string encodedToken = Uri.EscapeDataString(token); string displayTag = WebUtility.HtmlEncode(tag ?? ""); string modeLabel = mode == "register" ? "Привязка аккаунта" : mode == "login" ? "Восстановление аккаунта" : "Открыть в игре"; string buttonLabel = mode == "register" ? "Привязать аккаунт" : mode == "login" ? "Восстановить аккаунт" : "Открыть в игре"; // Heroicons — outline string iconSvg = mode == "register" ? @"" : mode == "login" ? @"" : @""; return ReadHtmlFile("launch.html") .Replace("{{MODE_LABEL}}", WebUtility.HtmlEncode(modeLabel), StringComparison.Ordinal) .Replace("{{DISPLAY_TAG}}", displayTag, StringComparison.Ordinal) .Replace("{{BUTTON_LABEL}}", WebUtility.HtmlEncode(buttonLabel), StringComparison.Ordinal) .Replace("{{ICON_SVG}}", iconSvg, StringComparison.Ordinal) .Replace("{{TOKEN}}", EscapeJsString(encodedToken), StringComparison.Ordinal) .Replace("{{MODE}}", EscapeJsString(mode), StringComparison.Ordinal) .Replace("{{SCHEME}}", EscapeJsString(scheme), StringComparison.Ordinal) .Replace("{{PACKAGE}}", EscapeJsString(package), StringComparison.Ordinal); } private static string BuildErrorPage(string title, string description) { return ReadHtmlFile("error.html") .Replace("{{TITLE}}", WebUtility.HtmlEncode(title), StringComparison.Ordinal) .Replace("{{DESCRIPTION}}", WebUtility.HtmlEncode(description), StringComparison.Ordinal); } private static string BuildStaticPage(string fileName) { string html = ReadHtmlFile(fileName); if (fileName.Equals("voucher.html", StringComparison.OrdinalIgnoreCase)) { string scheme = Configuration.Instance?.AppScheme ?? "shiny"; string package = Configuration.Instance?.AppPackage ?? "com.supercell.shiny"; html = html .Replace("{{SCHEME}}", EscapeJsString(scheme), StringComparison.Ordinal) .Replace("{{PACKAGE}}", EscapeJsString(package), StringComparison.Ordinal); } return html; } private static string ReadHtmlFile(string fileName) { string path = Path.Combine(AppContext.BaseDirectory, "HTMLS", fileName); if (!File.Exists(path)) { throw new FileNotFoundException($"HTML template not found: {path}"); } return File.ReadAllText(path, Encoding.UTF8); } private static string EscapeJsString(string value) { return value .Replace("\\", "\\\\", StringComparison.Ordinal) .Replace("'", "\\'", StringComparison.Ordinal); } private static async Task Respond(HttpListenerResponse response, int statusCode, string content, string contentType = "text/html; charset=utf-8") { try { response.StatusCode = statusCode; response.ContentType = contentType; byte[] data = Encoding.UTF8.GetBytes(content); response.ContentLength64 = data.Length; await response.OutputStream.WriteAsync(data, 0, data.Length); } finally { try { response.OutputStream.Close(); } catch { } } } public static void Stop() { try { _cts?.Cancel(); } catch { } try { _listener?.Stop(); } catch { } } } }