528 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
//Зависит от SettingsManager
[System.Serializable]
public class ServerResponse
{
public string message;
public string timestamp;
}
public class WebServer : MonoBehaviour
{
private HttpListener listener;
private bool isRunning = true;
private static byte[] css;
private string AdminPassword;
public List<SettingAction> settingActions;
async void Start()
{
if (ConfigManager.Config.StartServer == false)
{
//Debug.Log("Web сервер не запущен (по настройке)");
return;
}
AdminPassword = ConfigManager.Config.ServerPassword;
LoadCss();
settingActions = GameManager.instance.AdminPanel.GetComponent<SettingsScreen>().settingActions;
await StartWebServerAsync();
}
async Task StartWebServerAsync()
{
listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8080/");
listener.Prefixes.Add("http://*:8080/");
listener.Start();
Debug.Log("Async Web сервер запущен");
while (isRunning)
{
try
{
var context = await listener.GetContextAsync();
_ = Task.Run(() => ProcessRequestAsync(context));
}
catch (Exception e)
{
Debug.LogError($"Ошибка: {e.Message}");
}
}
}
async Task ProcessRequestAsync(HttpListenerContext context)
{
try
{
string path = context.Request.Url.LocalPath;
string method = context.Request.HttpMethod;
// 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;
}
// Рилтайм апдейт 1 настройки
if (method == "POST" && path == "/api/setting")
{
await HandlePostSingleSettingAsync(context);
return;
}
// Кнопка "Добавить монеты"
if (method == "POST" && path == "/api/coins")
{
await HandlePostCoinsAsync(context);
return;
}
// LEGACY (оставил, если вдруг где-то дергается)
if (method == "POST" && path == "/upd")
{
await HandlePostUpdateAsync(context);
return;
}
if (method == "POST" && path == "/api/action")
{
await HandlePostActionAsync(context);
return;
}
SendHtmlResponse(context, "<h1>404 Not Found</h1>", 404);
}
catch (Exception ex)
{
Debug.LogError($"Error processing request: {ex.Message}");
SendHtmlResponse(context, $"<h1>Error: {ex.Message}</h1>", 500);
}
}
async Task HandleGetSettingsAsync(HttpListenerContext context)
{
await Task.Run(() =>
{
try
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var htmlTask = Task.Run(() =>
{
var actionsSnapshot = settingActions != null ? new List<SettingAction>(settingActions) : new List<SettingAction>();
return HtmlBuilder.GetSettingsPage(SettingsManager.Container.Settings, actionsSnapshot);
});
if (htmlTask.Wait(TimeSpan.FromSeconds(5)))
{
string htmlContent = htmlTask.Result;
stopwatch.Stop();
if (string.IsNullOrEmpty(htmlContent))
htmlContent = "<h1>Error: Method returned empty string</h1>";
SendHtmlResponse(context, htmlContent);
}
else
{
Debug.LogError("HTML generation TIMEOUT (5 seconds)!");
SendHtmlResponse(context, "<h1>Error: Settings generation timeout</h1>", 500);
}
}
catch (Exception ex)
{
Debug.LogError($"Error in settings html: {ex.Message}");
SendHtmlResponse(context, $"<h1>Error: {ex.Message}</h1>", 500);
}
});
}
async Task HandleCssRequestAsync(HttpListenerContext context)
{
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();
}
private static void LoadCss()
{
TextAsset cssAsset = Resources.Load<TextAsset>("style");
if (cssAsset != null)
{
css = Encoding.UTF8.GetBytes(cssAsset.text);
Debug.Log("CSS OK");
}
else
{
Debug.LogWarning("CSS NOT OK");
css = Array.Empty<byte>();
}
}
// =========================
// 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<SettingUpdateDto>(body);
if (dto == null || string.IsNullOrEmpty(dto.id))
{
SendJson(context, 400, "{\"ok\":false,\"message\":\"Bad payload\"}");
return;
}
var (ok, log) = SettingsApplier.ApplySettings(
new Dictionary<string, string> { { 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<SettingsScreen>().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<CoinsDto>(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\"}");
}
}
async Task HandlePostActionAsync(HttpListenerContext context)
{
try
{
string body = await ReadBodyAsStringAsync(context.Request);
var dto = JsonUtility.FromJson<ActionDto>(body);
string name = dto?.name ?? "";
if (string.IsNullOrWhiteSpace(name))
{
SendJson(context, 400, "{\"ok\":false,\"message\":\"name is empty\"}");
return;
}
bool exists = settingActions != null && settingActions.Exists(a => a != null && a.Name == name);
if (!exists)
{
SendJson(context, 404, "{\"ok\":false,\"message\":\"action not found\"}");
return;
}
// Важно: UnityEvent только на главном потоке
UnityMainThreadDispatcher.Instance.Enqueue(() => ExecuteSettingAction(name));
SendJson(context, 200, "{\"ok\":true}");
}
catch (Exception ex)
{
Debug.LogError($"POST /api/action error: {ex.Message}\n{ex.StackTrace}");
SendJson(context, 500, "{\"ok\":false,\"message\":\"Server error\"}");
}
}
private void OnAddCoins(int amount) => PaymentSystem.AddCreditNoCounters(amount);
// =========================
// LEGACY /upd (как было)
// =========================
async Task HandlePostUpdateAsync(HttpListenerContext context)
{
try
{
Encoding encoding = Encoding.UTF8;
string contentType = context.Request.ContentType?.ToLower() ?? "";
if (contentType.Contains("charset="))
{
try
{
string charset = contentType.Split("charset=")[1].Trim().Split(';')[0];
encoding = Encoding.GetEncoding(charset);
}
catch
{
encoding = Encoding.UTF8;
}
}
string requestBody;
using (var reader = new StreamReader(context.Request.InputStream, encoding))
requestBody = await reader.ReadToEndAsync();
var formDict = new Dictionary<string, string>();
try
{
var formData = System.Web.HttpUtility.ParseQueryString(requestBody, encoding);
foreach (string key in formData.AllKeys)
{
if (key != null)
formDict[key] = formData[key] ?? "";
}
}
catch (Exception parseEx)
{
Debug.LogWarning($"ParseQueryString ошибка: {parseEx.Message}");
}
SettingsApplier.ApplySettings(formDict, assumeMissingBoolOff: true);
UnityMainThreadDispatcher.Instance.Enqueue(() => SettingsManager.OnSettingsChanged?.Invoke());
UnityMainThreadDispatcher.Instance.Enqueue(() => GameManager.instance.AdminPanel.GetComponent<SettingsScreen>().RefreshViews());
SendHtmlResponse(context, "<html><body><h1>OK</h1><a href='/'>Back</a></body></html>");
}
catch (Exception ex)
{
Debug.LogError($"Ошибка обработки POST /upd: {ex.Message}\n{ex.StackTrace}");
SendHtmlResponse(context, $"<h1 style='color:red;'>Ошибка: {ex.Message}</h1>", 500);
}
}
// =========================
// Helpers
// =========================
private async Task<string> 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)
{
try
{
byte[] buffer = Encoding.UTF8.GetBytes(html);
context.Response.StatusCode = statusCode;
context.Response.ContentType = "text/html; 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 response: {ex.Message}");
}
}
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();
}
public void ExecuteSettingAction(string name)
{
SettingAction action = settingActions.Find(s => s.Name == name);
if (action != null)
{
action.OnPressed?.Invoke();
}
else
{
Debug.Log($"Действие с именем {name} не найдено.");
}
}
void OnDestroy()
{
if (!isRunning) return;
isRunning = false;
listener?.Stop();
listener?.Close();
}
private void OnApplicationQuit()
{
isRunning = false;
listener?.Stop();
listener?.Close();
}
[Serializable]
private class ActionDto
{
public string name;
}
}