528 lines
16 KiB
C#
528 lines
16 KiB
C#
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;
|
||
}
|
||
}
|