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;
[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();//надо будет убирать
LoadCss();
await StartWebServerAsync();
}
async Task StartWebServerAsync()
{
listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8080/");
//тест
listener.Prefixes.Add("http://fishka:8080/");
// 4. ВСЕ сетевые интерфейсы (самый важный!)
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;
}
SendHtmlResponse(context, "
404 Not Found
", 404);
}
catch (Exception ex)
{
Debug.LogError($"Error processing request: {ex.Message}");
SendHtmlResponse(context, $"Error: {ex.Message}
", 500);
}
}
async Task HandleGetSettingsAsync(HttpListenerContext context)
{
await Task.Run(() =>
{
try
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var htmlTask = Task.Run(() =>
HtmlBuilder.GetSettingsPage(SettingsManager.Container.Settings)
);
if (htmlTask.Wait(TimeSpan.FromSeconds(5)))
{
string htmlContent = htmlTask.Result;
stopwatch.Stop();
if (string.IsNullOrEmpty(htmlContent))
htmlContent = "Error: Method returned empty string
";
SendHtmlResponse(context, htmlContent);
}
else
{
Debug.LogError("HTML generation TIMEOUT (5 seconds)!");
SendHtmlResponse(context, "Error: Settings generation timeout
", 500);
}
}
catch (Exception ex)
{
Debug.LogError($"Error in settings html: {ex.Message}");
SendHtmlResponse(context, $"Error: {ex.Message}
", 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("style");
if (cssAsset != null)
{
css = Encoding.UTF8.GetBytes(cssAsset.text);
Debug.Log("CSS OK");
}
else
{
Debug.LogWarning("CSS NOT OK");
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
{
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();
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().RefreshViews());
SendHtmlResponse(context, "OK
Back");
}
catch (Exception ex)
{
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)
{
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();
}
void OnDestroy()
{
isRunning = false;
listener?.Stop();
listener?.Close();
}
private void OnApplicationQuit()
{
isRunning = false;
listener?.Stop();
listener?.Close();
}
}