diff --git a/Assets/Resources/style.txt b/Assets/Resources/style.txt index 250b0d4..4091fca 100644 --- a/Assets/Resources/style.txt +++ b/Assets/Resources/style.txt @@ -5,6 +5,16 @@ box-sizing: border-box; } +.panel { padding: 16px; margin: 16px 0; border-radius: 12px; background: rgba(255,255,255,0.04); } +.coins-row { display:flex; gap:12px; align-items:center; } +.coins-input { width: 140px; } +.coins-btn { padding: 8px 14px; cursor:pointer; } +.status { margin-top: 10px; opacity: 0.85; font-size: 14px; } +.radio-row { display:flex; gap:10px; align-items:center; margin: 6px 0; } +.login-root { max-width: 360px; margin: 60px auto; padding: 18px; border-radius: 12px; background: rgba(255,255,255,0.04); } + + + body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); diff --git a/Assets/Scripts/Server/AuthManager.cs b/Assets/Scripts/Server/AuthManager.cs new file mode 100644 index 0000000..1ce5d50 --- /dev/null +++ b/Assets/Scripts/Server/AuthManager.cs @@ -0,0 +1,191 @@ +using System; +using System.IO; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using UnityEngine; + +public static class AuthManager +{ + private const string CookieName = "fishka_auth"; + private const string TokenSeparator = "."; + private const string AppSalt = "FISHKA_WEB_ADMIN_V1"; // поменяй на своё + + // Берем пароль из файла (если есть), иначе fallback из инспектора + public static string LoadPasswordOrFallback(string fallback, string fileName) + { + try + { + string path = Path.Combine(Application.persistentDataPath, fileName); + if (File.Exists(path)) + { + var s = File.ReadAllText(path, Encoding.UTF8).Trim(); + if (!string.IsNullOrEmpty(s)) return s; + } + } + catch (Exception e) + { + Debug.LogWarning($"Password file read failed: {e.Message}"); + } + return fallback ?? ""; + } + + public static bool VerifyPassword(string entered, string actual) + { + if (entered == null) entered = ""; + if (actual == null) actual = ""; + return ConstantTimeEquals(entered, actual); + } + + public static bool IsAuthorized(HttpListenerRequest request, string password) + { + try + { + var cookie = request.Cookies?[CookieName]; + if (cookie == null || string.IsNullOrEmpty(cookie.Value)) return false; + return ValidateToken(cookie.Value, password); + } + catch + { + return false; + } + } + + public static void SetAuthCookie(HttpListenerResponse response, string token, int daysValid) + { + // Cookie через header руками, чтобы не упираться в SameSite API разных .NET в Unity + int maxAge = Mathf.Clamp(daysValid, 1, 3650) * 24 * 60 * 60; + string header = $"{CookieName}={token}; Path=/; Max-Age={maxAge}; HttpOnly; SameSite=Lax"; + response.Headers.Add("Set-Cookie", header); + } + + public static string GenerateToken(string password, int daysValid) + { + long exp = DateTimeOffset.UtcNow.AddDays(daysValid).ToUnixTimeSeconds(); + string nonce = Guid.NewGuid().ToString("N"); + string payload = $"{exp}:{nonce}"; + byte[] sig = HmacSha256(GetSecretKey(password), payload); + return Base64UrlEncode(Encoding.UTF8.GetBytes(payload)) + TokenSeparator + Base64UrlEncode(sig); + } + + private static bool ValidateToken(string token, string password) + { + if (string.IsNullOrEmpty(token)) return false; + + var parts = token.Split(new[] { TokenSeparator }, StringSplitOptions.None); + if (parts.Length != 2) return false; + + byte[] payloadBytes, sigBytes; + try + { + payloadBytes = Base64UrlDecode(parts[0]); + sigBytes = Base64UrlDecode(parts[1]); + } + catch { return false; } + + string payload = Encoding.UTF8.GetString(payloadBytes); + byte[] expected = HmacSha256(GetSecretKey(password), payload); + + if (!FixedTimeEquals(sigBytes, expected)) return false; + + // exp + var idx = payload.IndexOf(':'); + if (idx <= 0) return false; + + if (!long.TryParse(payload.Substring(0, idx), out long exp)) return false; + long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (now > exp) return false; + + return true; + } + + private static byte[] GetSecretKey(string password) + { + // Если поменяешь пароль — старые куки станут невалидными (это плюс). + string s = $"{AppSalt}:{password}"; + return Sha256(Encoding.UTF8.GetBytes(s)); + } + + private static byte[] Sha256(byte[] data) + { + using var sha = SHA256.Create(); + return sha.ComputeHash(data); + } + + private static byte[] HmacSha256(byte[] key, string data) + { + using var h = new HMACSHA256(key); + return h.ComputeHash(Encoding.UTF8.GetBytes(data)); + } + + // Достаём пароль из body: либо form-url-encoded (password=...), либо json {"password":"..."} + public static string ExtractPassword(string body, string contentType) + { + if (string.IsNullOrEmpty(body)) return ""; + + contentType = (contentType ?? "").ToLower(); + + if (contentType.Contains("application/json")) + { + // очень простой парсер под {"password":"..."} + // если хочешь — можно заменить на DTO+JsonUtility + const string key = "\"password\""; + int i = body.IndexOf(key, StringComparison.OrdinalIgnoreCase); + if (i < 0) return ""; + int colon = body.IndexOf(':', i); + if (colon < 0) return ""; + int q1 = body.IndexOf('"', colon + 1); + if (q1 < 0) return ""; + int q2 = body.IndexOf('"', q1 + 1); + if (q2 < 0) return ""; + return body.Substring(q1 + 1, q2 - q1 - 1); + } + + // form + try + { + var nvc = System.Web.HttpUtility.ParseQueryString(body, Encoding.UTF8); + return nvc["password"] ?? ""; + } + catch + { + return ""; + } + } + + private static string Base64UrlEncode(byte[] data) + { + return Convert.ToBase64String(data) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + } + + private static byte[] Base64UrlDecode(string s) + { + s = s.Replace('-', '+').Replace('_', '/'); + switch (s.Length % 4) + { + case 2: s += "=="; break; + case 3: s += "="; break; + } + return Convert.FromBase64String(s); + } + + private static bool ConstantTimeEquals(string a, string b) + { + var ba = Encoding.UTF8.GetBytes(a); + var bb = Encoding.UTF8.GetBytes(b); + return FixedTimeEquals(ba, bb); + } + + private static bool FixedTimeEquals(byte[] a, byte[] b) + { + if (a == null || b == null) return false; + if (a.Length != b.Length) return false; + int diff = 0; + for (int i = 0; i < a.Length; i++) + diff |= a[i] ^ b[i]; + return diff == 0; + } +} diff --git a/Assets/Scripts/Server/AuthManager.cs.meta b/Assets/Scripts/Server/AuthManager.cs.meta new file mode 100644 index 0000000..ec6e744 --- /dev/null +++ b/Assets/Scripts/Server/AuthManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be4e78088aed1e049a925227fdc3b2da +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Server/HtmlBuilder.cs b/Assets/Scripts/Server/HtmlBuilder.cs index cbe131a..4ab9bab 100644 --- a/Assets/Scripts/Server/HtmlBuilder.cs +++ b/Assets/Scripts/Server/HtmlBuilder.cs @@ -1,48 +1,208 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; -using System.Security.Cryptography; using System.Text; -using UnityEngine; -public class HtmlBuilder : MonoBehaviour +public class HtmlBuilder { public static string GetSettingsPage(List settings) { - return Decorate(GetSettingsString(settings)); + return Decorate(GetSettingsString(settings), title: "Настройки Фишки"); + } + + public static string GetLoginPage() + { + string content = @" +
+

Вход

+
+ + +
+ +

+
+ + +"; + return Decorate(content, title: "Вход"); } public static string GetSettingsString(List settings) { - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); sb.AppendLine("

Настройки фишки

"); - sb.AppendLine("
"); + // блок монет + sb.AppendLine(@" +
+

Монеты

+
+ + +
+
+
+"); + + sb.AppendLine("
"); + sb.AppendLine("

Параметры

"); + foreach (Setting setting in settings) { - string view = string.Empty; - - if(setting.Type== SettingType.Bool) + string view = setting.Type switch { - view = GetBoolView(setting); - } - else if(setting.Type == SettingType.Float) - { - view = GetFloatView(setting); - } - else if(setting.Type == SettingType.String) - { - view = GetStringView(setting); - } - else - { - view = $"

{setting.Alias}: {setting.IntValue.ToString()}

"; - } + SettingType.Bool => GetBoolView(setting), + SettingType.Float => GetFloatView(setting), + SettingType.String => GetStringView(setting), + SettingType.Int => GetIntView(setting), + _ => $"

{Escape(setting.Alias)}: (unknown)

" + }; sb.AppendLine(view); } - sb.AppendLine(""); - sb.AppendLine(""); + + sb.AppendLine("
"); + + // JS для realtime отправки + sb.AppendLine(@" + + +
+"); return sb.ToString(); } @@ -52,96 +212,99 @@ public class HtmlBuilder : MonoBehaviour string name = setting.Id; string id = name + "ID"; + string isChecked = setting.BoolValue ? "checked" : ""; - string res = string.Empty; - res += $"
"; - res += $""; - string ischecked = setting.BoolValue ? "checked" : ""; - res += $""; - res += "
"; - - return res; + return $@" +
+ + +
"; } private static string GetFloatView(Setting setting) { string name = setting.Id; string id = name + "ID"; + string spanId = id + "_VAL"; - string res = string.Empty; string min = setting.FloatMin.ToString(CultureInfo.InvariantCulture); string max = setting.FloatMax.ToString(CultureInfo.InvariantCulture); string value = setting.FloatValue.ToString(CultureInfo.InvariantCulture); string step = setting.FloatStep.ToString(CultureInfo.InvariantCulture); - res += $"
"; - res += $""; - res += $""; - res += "
"; - - return res; - + return $@" +
+ + +
"; } private static string GetStringView(Setting setting) { - string res = string.Empty; - - res += $"
"; - res += $"

{setting.Alias}

"; + var sb = new StringBuilder(); + sb.AppendLine("
"); + sb.AppendLine($"

{Escape(setting.Alias)}

"); for (int i = 0; i < setting.AllowedValues.Count; i++) { string stringValue = setting.AllowedValues[i]; - string name = setting.Id; - string id = name+ "ID"+i; - + string id = name + "ID" + i; bool isChecked = setting.StringValue == stringValue; string Checked = isChecked ? "checked" : ""; - - - res += "
"; - - res += $""; - res += $""; - - - res += "
"; - - + sb.AppendLine("
"); + sb.AppendLine($""); + sb.AppendLine( + $"" + ); + sb.AppendLine("
"); } - res += "
"; - return res; + sb.AppendLine("
"); + return sb.ToString(); } private static string GetIntView(Setting setting) { + string name = setting.Id; + string id = name + "ID"; - return string.Empty; + return $@" +
+ + +
"; } - private static string Decorate(string content) + private static string Decorate(string content, string title) { - string res = "" - + "" - + "" - + "" - + "" - + "Настройки Фишки" - + "" - + ""; + string res = "" + + "" + + "" + + "" + + "" + + "" + + $"{Escape(title)}" + + "" + + ""; res += content; - res += "" + - ""; - + res += ""; return res; } - + // минимальная защита от мусора в html + private static string Escape(string s) + { + if (string.IsNullOrEmpty(s)) return ""; + return s.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); + } + + private static string EscapeAttr(string s) => Escape(s).Replace("'", "'"); } diff --git a/Assets/Scripts/Server/SettingsApplier.cs b/Assets/Scripts/Server/SettingsApplier.cs index 0666ade..fd78a83 100644 --- a/Assets/Scripts/Server/SettingsApplier.cs +++ b/Assets/Scripts/Server/SettingsApplier.cs @@ -6,40 +6,37 @@ using UnityEngine; public static class SettingsApplier { - public static (bool OK, string log) ApplySettings(Dictionary keyValuePairs) + public static (bool OK, string log) ApplySettings( + Dictionary keyValuePairs, + bool assumeMissingBoolOff + ) { List settings = SettingsManager.Container.Settings; - - //тут надо еще взять все id из настроек и если какое то значение не пришло значит оно false - foreach (var kv in keyValuePairs) { var searchResult = GetSettingById(kv.Key, settings); if (searchResult.found) - {//есть такая настройка в менеджере - InjectNewValueToSetting(kv.Value, searchResult.setting); - } - else - {//нет такой настройки в менеджере - - } } - foreach(string id in FalseBoolSettingIds(settings, keyValuePairs.Keys.ToList())) + // ВАЖНО: + // - true: старое поведение для submit формы — то чего нет в POST, считаем false. + // - false: новое поведение для realtime — ничего не трогаем кроме пришедшего id. + if (assumeMissingBoolOff) { - InjectNewValueToSetting("off", GetSettingById(id, settings).setting); + foreach (string id in FalseBoolSettingIds(settings, keyValuePairs.Keys.ToList())) + { + InjectNewValueToSetting("off", GetSettingById(id, settings).setting); + } } - return (OK: true, log: ""); } private static (bool found, Setting setting) GetSettingById(string settingId, List settings) { Setting setting = settings.Find(s => s.Id == settingId); - if (setting != null) return (found: true, setting); else @@ -48,74 +45,58 @@ public static class SettingsApplier private static void InjectNewValueToSetting(string newValue, Setting setting) { + if (setting == null) return; + if (setting.Type == SettingType.String) { if (setting.AllowedValues.Contains(newValue)) - { setting.StringValue = newValue; - } else - { - Debug.Log($"{setting.Id}: Попытка установить недопустимое значение \"{newValue}\""); - } + Debug.Log($"{setting.Id}: недопустимое значение \"{newValue}\""); } else if (setting.Type == SettingType.Float) { - float value=0; - try + if (!float.TryParse(newValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { - value = float.Parse(newValue, CultureInfo.InvariantCulture); - } - catch (System.Exception e) - { - Debug.Log($"{setting.Id}: Ошибка парсинга значения \"{newValue}\". {e}");//тут парсится неправильно + Debug.Log($"{setting.Id}: ошибка парсинга float \"{newValue}\""); return; } - //если спарсилось, проверяем пределы - if(value <= setting.FloatMax && value >= setting.FloatMin) - { + if (value <= setting.FloatMax && value >= setting.FloatMin) setting.FloatValue = value; - } else - {//не подходит под интервал - Debug.Log($"{setting.Id}: Новое значение за пределами ограничителей\"{newValue}\"."); - } + Debug.Log($"{setting.Id}: float вне диапазона \"{newValue}\""); } else if (setting.Type == SettingType.Bool) { - bool value=false; - if(newValue == "on") { value = true; } - - setting.BoolValue = value; + setting.BoolValue = (newValue == "on" || newValue == "true" || newValue == "1"); } else if (setting.Type == SettingType.Int) { - setting.IntValue = int.Parse(newValue); + if (int.TryParse(newValue, out int iv)) + setting.IntValue = iv; + else + Debug.Log($"{setting.Id}: ошибка парсинга int \"{newValue}\""); } } private static List FalseBoolSettingIds(List settings, List entry) { - List ids = new(); - + var missing = new List(); foreach (Setting setting in settings) { if (entry.Contains(setting.Id)) continue; - - ids.Add(setting.Id); + missing.Add(setting.Id); } - List BoolSettings = new List(); - foreach (string id in ids) + var boolIds = new List(); + foreach (string id in missing) { var searchResult = GetSettingById(id, settings); if (searchResult.found && searchResult.setting.Type == SettingType.Bool) - { - BoolSettings.Add(id); - } + boolIds.Add(id); } - return BoolSettings; - } + return boolIds; + } } diff --git a/Assets/Scripts/Server/WebServer.cs b/Assets/Scripts/Server/WebServer.cs index 8459430..8b74f02 100644 --- a/Assets/Scripts/Server/WebServer.cs +++ b/Assets/Scripts/Server/WebServer.cs @@ -21,9 +21,15 @@ public class WebServer : MonoBehaviour private static byte[] css; + [Header("Auth")] + [SerializeField] private string adminPasswordFallback = "1234"; // если файла нет + [SerializeField] private string passwordFileName = "admin_password.txt"; // лежит в persistentDataPath + + private string AdminPassword => AuthManager.LoadPasswordOrFallback(adminPasswordFallback, passwordFileName); + async void Start() { - SettingsManager.Initialize();//надо будет убирать + // SettingsManager.Initialize();//надо будет убирать LoadCss(); await StartWebServerAsync(); @@ -49,7 +55,7 @@ public class WebServer : MonoBehaviour var context = await listener.GetContextAsync(); _ = Task.Run(() => ProcessRequestAsync(context)); } - catch (System.Exception e) + catch (Exception e) { Debug.LogError($"Ошибка: {e.Message}"); } @@ -63,24 +69,64 @@ public class WebServer : MonoBehaviour string path = context.Request.Url.LocalPath; string method = context.Request.HttpMethod; - Debug.Log($"Запрос: {method} {path}"); + // Debug.Log($"Запрос: {method} {path}"); + // Публичные маршруты (без авторизации) + if (method == "GET" && path == "/style.css") + { + await HandleCssRequestAsync(context); + return; + } + + if (method == "GET" && path == "/login") + { + SendHtmlResponse(context, HtmlBuilder.GetLoginPage()); + return; + } + + if (method == "POST" && path == "/api/login") + { + await HandlePostLoginAsync(context); + return; + } + + // Дальше всё — только с авторизацией + if (!AuthManager.IsAuthorized(context.Request, AdminPassword)) + { + // красивый редирект на /login + Redirect(context, "/login"); + return; + } + + // Главная страница if (method == "GET" && path == "/") { await HandleGetSettingsAsync(context); + return; } - else if (method == "GET" && path == "/style.css") + + // Рилтайм апдейт 1 настройки + if (method == "POST" && path == "/api/setting") { - await HandleCssRequestAsync(context); + await HandlePostSingleSettingAsync(context); + return; } - else if (method == "POST" && path == "/upd") + + // Кнопка "Добавить монеты" + if (method == "POST" && path == "/api/coins") + { + await HandlePostCoinsAsync(context); + return; + } + + // LEGACY (оставил, если вдруг где-то дергается) + if (method == "POST" && path == "/upd") { await HandlePostUpdateAsync(context); + return; } - else - { - SendHtmlResponse(context, "

404 Not Found

", 404); - } + + SendHtmlResponse(context, "

404 Not Found

", 404); } catch (Exception ex) { @@ -95,35 +141,31 @@ public class WebServer : MonoBehaviour { try { - Debug.Log("Starting SettingsToHtml..."); - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var htmlTask = Task.Run(() => - HtmlBuilder.GetSettingsPage(SettingsManager.Container.Settings)); - //SettingsToHtml.GetSettingsString(SettingsManager.Container.Settings)); + HtmlBuilder.GetSettingsPage(SettingsManager.Container.Settings) + ); if (htmlTask.Wait(TimeSpan.FromSeconds(5))) { string htmlContent = htmlTask.Result; stopwatch.Stop(); - Debug.Log($"GetSettingsString выполнился за {stopwatch.ElapsedMilliseconds}ms"); if (string.IsNullOrEmpty(htmlContent)) - { htmlContent = "

Error: Method returned empty string

"; - } SendHtmlResponse(context, htmlContent); } else { - Debug.LogError("GetSettingsString TIMEOUT (5 seconds)!"); + Debug.LogError("HTML generation TIMEOUT (5 seconds)!"); SendHtmlResponse(context, "

Error: Settings generation timeout

", 500); } } catch (Exception ex) { - Debug.LogError($"Error in GetSettingsString: {ex.Message}"); + Debug.LogError($"Error in settings html: {ex.Message}"); SendHtmlResponse(context, $"

Error: {ex.Message}

", 500); } }); @@ -131,7 +173,7 @@ public class WebServer : MonoBehaviour async Task HandleCssRequestAsync(HttpListenerContext context) { - context.Response.ContentType = "text/css"; + context.Response.ContentType = "text/css; charset=utf-8"; context.Response.ContentLength64 = css.Length; await context.Response.OutputStream.WriteAsync(css, 0, css.Length); context.Response.OutputStream.Close(); @@ -141,28 +183,149 @@ public class WebServer : MonoBehaviour { TextAsset cssAsset = Resources.Load("style"); - try - { - print(cssAsset.text); - } - catch (Exception) - { - - throw; - } - if (cssAsset != null) { - css= Encoding.UTF8.GetBytes(cssAsset.text); + css = Encoding.UTF8.GetBytes(cssAsset.text); Debug.Log("CSS OK"); } else { Debug.LogWarning("CSS NOT OK"); - css = new byte[0]; - } + css = Array.Empty(); + } } + // ========================= + // AUTH + // ========================= + + async Task HandlePostLoginAsync(HttpListenerContext context) + { + try + { + string body = await ReadBodyAsStringAsync(context.Request); + // принимаем и form-url-encoded, и json + string pass = AuthManager.ExtractPassword(body, context.Request.ContentType); + + if (string.IsNullOrEmpty(pass)) + { + SendJson(context, 400, "{\"ok\":false,\"message\":\"Пароль пустой\"}"); + return; + } + + if (!AuthManager.VerifyPassword(pass, AdminPassword)) + { + SendJson(context, 401, "{\"ok\":false,\"message\":\"Неверный пароль\"}"); + return; + } + + string token = AuthManager.GenerateToken(AdminPassword, daysValid: 30); + AuthManager.SetAuthCookie(context.Response, token, daysValid: 30); + + SendJson(context, 200, "{\"ok\":true}"); + } + catch (Exception ex) + { + Debug.LogError($"Login error: {ex.Message}"); + SendJson(context, 500, "{\"ok\":false,\"message\":\"Server error\"}"); + } + } + + // ========================= + // REALTIME SETTINGS + // ========================= + + [Serializable] + private class SettingUpdateDto + { + public string id; + public string value; + } + + async Task HandlePostSingleSettingAsync(HttpListenerContext context) + { + try + { + string body = await ReadBodyAsStringAsync(context.Request); + + // Ждем JSON: {"id":"SETTING_ID","value":"on/off/123/1.5/SomeString"} + var dto = JsonUtility.FromJson(body); + if (dto == null || string.IsNullOrEmpty(dto.id)) + { + SendJson(context, 400, "{\"ok\":false,\"message\":\"Bad payload\"}"); + return; + } + + var (ok, log) = SettingsApplier.ApplySettings( + new Dictionary { { dto.id, dto.value ?? "" } }, + assumeMissingBoolOff: false // важно! иначе вырубишь все остальные bool + ); + + if (!ok) + { + SendJson(context, 400, "{\"ok\":false,\"message\":\"Apply failed\"}"); + return; + } + + // применяем на главном потоке + UnityMainThreadDispatcher.Instance.Enqueue(() => SettingsManager.OnSettingsChanged?.Invoke()); + UnityMainThreadDispatcher.Instance.Enqueue(() => GameManager.instance.AdminPanel + .GetComponent().RefreshViews()); + + SendJson(context, 200, "{\"ok\":true}"); + } + catch (Exception ex) + { + Debug.LogError($"POST /api/setting error: {ex.Message}\n{ex.StackTrace}"); + SendJson(context, 500, "{\"ok\":false,\"message\":\"Server error\"}"); + } + } + + // ========================= + // COINS HANDLER + // ========================= + + [Serializable] + private class CoinsDto + { + public int amount; + } + + async Task HandlePostCoinsAsync(HttpListenerContext context) + { + try + { + string body = await ReadBodyAsStringAsync(context.Request); + var dto = JsonUtility.FromJson(body); + + int amount = dto?.amount ?? 0; + if (amount <= 0) + { + SendJson(context, 400, "{\"ok\":false,\"message\":\"amount must be > 0\"}"); + return; + } + + // ЗАГОТОВКА: сюда ты впишешь реальную логику + UnityMainThreadDispatcher.Instance.Enqueue(() => OnAddCoins(amount)); + + SendJson(context, 200, "{\"ok\":true}"); + } + catch (Exception ex) + { + Debug.LogError($"POST /api/coins error: {ex.Message}\n{ex.StackTrace}"); + SendJson(context, 500, "{\"ok\":false,\"message\":\"Server error\"}"); + } + } + + private void OnAddCoins(int amount) + { + // TODO: ты сам впишешь функционал + Debug.Log($"[COINS] Add coins requested: {amount}"); + } + + // ========================= + // LEGACY /upd (как было) + // ========================= async Task HandlePostUpdateAsync(HttpListenerContext context) { try @@ -176,42 +339,25 @@ public class WebServer : MonoBehaviour { string charset = contentType.Split("charset=")[1].Trim().Split(';')[0]; encoding = Encoding.GetEncoding(charset); - Debug.Log($"Определена кодировка из Content-Type: {charset}"); } catch { encoding = Encoding.UTF8; - Debug.Log($"Не удалось определить кодировку из Content-Type, используем UTF-8"); } } - // Читаем тело POST-запроса string requestBody; using (var reader = new StreamReader(context.Request.InputStream, encoding)) - { requestBody = await reader.ReadToEndAsync(); - } var formDict = new Dictionary(); try { var formData = System.Web.HttpUtility.ParseQueryString(requestBody, encoding); - Debug.Log("\n=== Данные через ParseQueryString ==="); foreach (string key in formData.AllKeys) { if (key != null) - { - string value = formData[key] ?? ""; - formDict[key] = value; - - Debug.Log($" {key} = {formDict[key]}"); - - if (key.Contains("%")) - { - string decodedKey = Uri.UnescapeDataString(key); - Debug.Log($"РУЧНОЙ ДЕКОД: {decodedKey}"); - } - } + formDict[key] = formData[key] ?? ""; } } catch (Exception parseEx) @@ -219,66 +365,46 @@ public class WebServer : MonoBehaviour Debug.LogWarning($"ParseQueryString ошибка: {parseEx.Message}"); } - SettingsApplier.ApplySettings(formDict); - //SettingsManager.OnSettingsChanged?.Invoke(); + SettingsApplier.ApplySettings(formDict, assumeMissingBoolOff: true); UnityMainThreadDispatcher.Instance.Enqueue(() => SettingsManager.OnSettingsChanged?.Invoke()); UnityMainThreadDispatcher.Instance.Enqueue(() => GameManager.instance.AdminPanel.GetComponent().RefreshViews()); - // Отправляем ответ обратно - string responseHtml = @" - - - - Обновлено - - - -

✓ Данные успешно получены!

-
-

Сервер обработал ваш запрос.

-

Полученные данные:

-
    "; - - // Добавляем список полученных данных в ответ - foreach (var kvp in formDict) - { - responseHtml += $"
  • {kvp.Key}: {kvp.Value}
  • "; - } - - responseHtml += @" -
-
- - Вернуться к настройкам - - - - "; - - SendHtmlResponse(context, responseHtml); + SendHtmlResponse(context, "

OK

Back"); } catch (Exception ex) { - Debug.LogError($"Ошибка обработки POST /upd: {ex.Message}"); - Debug.LogError($"StackTrace: {ex.StackTrace}"); - SendHtmlResponse(context, - $"Ошибка" + - $"

Ошибка: {ex.Message}

" + - $"
{ex.StackTrace}
" + - $"Вернуться назад", - 500); + Debug.LogError($"Ошибка обработки POST /upd: {ex.Message}\n{ex.StackTrace}"); + SendHtmlResponse(context, $"

Ошибка: {ex.Message}

", 500); } - } + } + + // ========================= + // Helpers + // ========================= + + private async Task ReadBodyAsStringAsync(HttpListenerRequest request) + { + Encoding encoding = Encoding.UTF8; + + // иногда Unity/браузер шлет json без charset — ок + if (!string.IsNullOrEmpty(request.ContentType)) + { + var ct = request.ContentType.ToLower(); + if (ct.Contains("charset=")) + { + try + { + string charset = ct.Split("charset=")[1].Trim().Split(';')[0]; + encoding = Encoding.GetEncoding(charset); + } + catch { encoding = Encoding.UTF8; } + } + } + + using var reader = new StreamReader(request.InputStream, encoding); + return await reader.ReadToEndAsync(); + } void SendHtmlResponse(HttpListenerContext context, string html, int statusCode = 200) { @@ -297,6 +423,30 @@ public class WebServer : MonoBehaviour } } + private void SendJson(HttpListenerContext context, int statusCode, string json) + { + try + { + byte[] buffer = Encoding.UTF8.GetBytes(json); + context.Response.StatusCode = statusCode; + context.Response.ContentType = "application/json; charset=utf-8"; + context.Response.ContentLength64 = buffer.Length; + context.Response.OutputStream.Write(buffer, 0, buffer.Length); + context.Response.OutputStream.Close(); + } + catch (Exception ex) + { + Debug.LogError($"Error sending json: {ex.Message}"); + } + } + + private void Redirect(HttpListenerContext context, string location, int statusCode = 302) + { + context.Response.StatusCode = statusCode; + context.Response.Headers["Location"] = location; + context.Response.OutputStream.Close(); + } + void OnDestroy() { isRunning = false; @@ -310,4 +460,4 @@ public class WebServer : MonoBehaviour listener?.Stop(); listener?.Close(); } -} \ No newline at end of file +}