FEAT: Рилтайм обновление настроек на сайте
1. Настройки на сайте обновляются без кнопки отправить 2. Добавлена кнопка для начисления монет через сайт(пока заглушка)
This commit is contained in:
parent
0ba6c842b2
commit
a8b26773e8
@ -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%);
|
||||
|
||||
191
Assets/Scripts/Server/AuthManager.cs
Normal file
191
Assets/Scripts/Server/AuthManager.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Server/AuthManager.cs.meta
Normal file
11
Assets/Scripts/Server/AuthManager.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be4e78088aed1e049a925227fdc3b2da
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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<Setting> settings)
|
||||
{
|
||||
return Decorate(GetSettingsString(settings));
|
||||
return Decorate(GetSettingsString(settings), title: "Настройки Фишки");
|
||||
}
|
||||
|
||||
public static string GetLoginPage()
|
||||
{
|
||||
string content = @"
|
||||
<div class='login-root'>
|
||||
<h1>Вход</h1>
|
||||
<div class='form-group'>
|
||||
<label for='pass'>Пароль</label>
|
||||
<input id='pass' type='password' autocomplete='current-password' />
|
||||
</div>
|
||||
<button id='loginBtn' type='button'>Войти</button>
|
||||
<p id='loginStatus' style='margin-top:12px;'></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const pass = document.getElementById('pass');
|
||||
const btn = document.getElementById('loginBtn');
|
||||
const st = document.getElementById('loginStatus');
|
||||
|
||||
async function doLogin(){
|
||||
st.textContent = '';
|
||||
try{
|
||||
const r = await fetch('/api/login', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ password: pass.value || '' })
|
||||
});
|
||||
const t = await r.text();
|
||||
if(!r.ok){
|
||||
st.textContent = 'Ошибка входа';
|
||||
try{
|
||||
const j = JSON.parse(t);
|
||||
if(j && j.message) st.textContent = j.message;
|
||||
}catch(e){}
|
||||
return;
|
||||
}
|
||||
window.location.href = '/';
|
||||
}catch(e){
|
||||
st.textContent = 'Сервер недоступен';
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', doLogin);
|
||||
pass.addEventListener('keydown', (e)=>{ if(e.key === 'Enter') doLogin(); });
|
||||
})();
|
||||
</script>
|
||||
";
|
||||
return Decorate(content, title: "Вход");
|
||||
}
|
||||
|
||||
public static string GetSettingsString(List<Setting> settings)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("<h1>Настройки фишки</h1>");
|
||||
|
||||
sb.AppendLine("<form action=\"/upd\" method=\"POST\" class=\"form-root\">");
|
||||
// блок монет
|
||||
sb.AppendLine(@"
|
||||
<div class='panel'>
|
||||
<h2>Монеты</h2>
|
||||
<div class='coins-row'>
|
||||
<input id='coinsAmount' type='number' value='10' min='1' step='1' class='coins-input'/>
|
||||
<button id='addCoinsBtn' type='button' class='coins-btn'>Добавить монеты</button>
|
||||
</div>
|
||||
<div id='coinsStatus' class='status'></div>
|
||||
</div>
|
||||
");
|
||||
|
||||
sb.AppendLine("<div class='panel'>");
|
||||
sb.AppendLine("<h2>Параметры</h2>");
|
||||
|
||||
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 = $"<p>{setting.Alias}: {setting.IntValue.ToString()}</p>";
|
||||
}
|
||||
SettingType.Bool => GetBoolView(setting),
|
||||
SettingType.Float => GetFloatView(setting),
|
||||
SettingType.String => GetStringView(setting),
|
||||
SettingType.Int => GetIntView(setting),
|
||||
_ => $"<p>{Escape(setting.Alias)}: (unknown)</p>"
|
||||
};
|
||||
sb.AppendLine(view);
|
||||
}
|
||||
sb.AppendLine("<input type=\"submit\">");
|
||||
sb.AppendLine("</form>");
|
||||
|
||||
sb.AppendLine("</div>");
|
||||
|
||||
// JS для realtime отправки
|
||||
sb.AppendLine(@"
|
||||
<script>
|
||||
(function(){
|
||||
const status = (txt)=>{
|
||||
const el = document.getElementById('globalStatus');
|
||||
if(!el) return;
|
||||
el.textContent = txt || '';
|
||||
if(txt){ setTimeout(()=>{ el.textContent=''; }, 1200); }
|
||||
};
|
||||
|
||||
const debounceMap = new Map();
|
||||
function debounce(key, fn, ms){
|
||||
if(debounceMap.has(key)) clearTimeout(debounceMap.get(key));
|
||||
debounceMap.set(key, setTimeout(fn, ms));
|
||||
}
|
||||
|
||||
async function sendSetting(id, value){
|
||||
try{
|
||||
const r = await fetch('/api/setting', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ id, value })
|
||||
});
|
||||
if(!r.ok){
|
||||
status('Ошибка применения');
|
||||
return;
|
||||
}
|
||||
status('Сохранено');
|
||||
}catch(e){
|
||||
status('Сервер недоступен');
|
||||
}
|
||||
}
|
||||
|
||||
// Все элементы настроек
|
||||
document.querySelectorAll('[data-setting-id]').forEach(el=>{
|
||||
const id = el.getAttribute('data-setting-id');
|
||||
|
||||
if(el.type === 'checkbox'){
|
||||
el.addEventListener('change', ()=>{
|
||||
sendSetting(id, el.checked ? 'on' : 'off');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(el.type === 'range'){
|
||||
const valueSpanId = el.getAttribute('data-value-span');
|
||||
const valueSpan = valueSpanId ? document.getElementById(valueSpanId) : null;
|
||||
|
||||
const apply = ()=>{
|
||||
if(valueSpan) valueSpan.textContent = el.value;
|
||||
// range шлём с небольшой задержкой, чтобы не заспамить
|
||||
debounce('rng_'+id, ()=>sendSetting(id, el.value), 120);
|
||||
};
|
||||
|
||||
el.addEventListener('input', apply);
|
||||
el.addEventListener('change', apply);
|
||||
return;
|
||||
}
|
||||
|
||||
if(el.type === 'radio'){
|
||||
el.addEventListener('change', ()=>{
|
||||
if(el.checked) sendSetting(id, el.value);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// number/text (int)
|
||||
el.addEventListener('change', ()=>{
|
||||
sendSetting(id, el.value);
|
||||
});
|
||||
});
|
||||
|
||||
// Монеты
|
||||
const coinsAmount = document.getElementById('coinsAmount');
|
||||
const addBtn = document.getElementById('addCoinsBtn');
|
||||
const coinsStatus = document.getElementById('coinsStatus');
|
||||
|
||||
async function addCoins(){
|
||||
coinsStatus.textContent = '';
|
||||
const v = parseInt(coinsAmount.value || '0', 10);
|
||||
if(!Number.isFinite(v) || v <= 0){
|
||||
coinsStatus.textContent = 'Введи число > 0';
|
||||
return;
|
||||
}
|
||||
try{
|
||||
const r = await fetch('/api/coins', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ amount: v })
|
||||
});
|
||||
if(!r.ok){
|
||||
coinsStatus.textContent = 'Ошибка запроса';
|
||||
return;
|
||||
}
|
||||
coinsStatus.textContent = 'Ок';
|
||||
setTimeout(()=>{ coinsStatus.textContent=''; }, 1200);
|
||||
}catch(e){
|
||||
coinsStatus.textContent = 'Сервер недоступен';
|
||||
}
|
||||
}
|
||||
|
||||
addBtn.addEventListener('click', addCoins);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div id='globalStatus' class='status'></div>
|
||||
");
|
||||
|
||||
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 += $"<div class=\"form-group\">";
|
||||
res += $"<label for=\"{id}\">{setting.Alias}</label>";
|
||||
string ischecked = setting.BoolValue ? "checked" : "";
|
||||
res += $"<input type=\"checkbox\" name=\"{name}\" id=\"{id}\" {ischecked}>";
|
||||
res += "</div>";
|
||||
|
||||
return res;
|
||||
return $@"
|
||||
<div class='form-group'>
|
||||
<label for='{id}'>{Escape(setting.Alias)}</label>
|
||||
<input type='checkbox' id='{id}' data-setting-id='{EscapeAttr(name)}' {isChecked}>
|
||||
</div>";
|
||||
}
|
||||
|
||||
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 += $"<div class=\"form-group\">";
|
||||
res += $"<label for=\"{id}\">{setting.Alias}</label>";
|
||||
res += $"<input id=\"{id}\" type=\"range\" min=\"{min}\" max=\"{max}\" value=\"{value}\" step=\"{step}\" name=\"{name}\">";
|
||||
res += "</div>";
|
||||
|
||||
return res;
|
||||
|
||||
return $@"
|
||||
<div class='form-group'>
|
||||
<label for='{id}'>{Escape(setting.Alias)}: <span id='{spanId}'>{value}</span></label>
|
||||
<input id='{id}' type='range' min='{min}' max='{max}' value='{value}' step='{step}'
|
||||
data-setting-id='{EscapeAttr(name)}' data-value-span='{spanId}'>
|
||||
</div>";
|
||||
}
|
||||
|
||||
private static string GetStringView(Setting setting)
|
||||
{
|
||||
string res = string.Empty;
|
||||
|
||||
res += $"<div class=\"form-group\">";
|
||||
res += $"<p>{setting.Alias}</p>";
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<div class='form-group'>");
|
||||
sb.AppendLine($"<p>{Escape(setting.Alias)}</p>");
|
||||
|
||||
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 += "<div>";
|
||||
|
||||
res += $"<label for=\"{id}\">{stringValue}</label>";
|
||||
res += $"<input type=\"radio\" id=\"{id}\" name=\"{name}\" value=\"{stringValue}\" {Checked}>";
|
||||
|
||||
|
||||
res += "</div>";
|
||||
|
||||
|
||||
sb.AppendLine("<div class='radio-row'>");
|
||||
sb.AppendLine($"<label for='{EscapeAttr(id)}'>{Escape(stringValue)}</label>");
|
||||
sb.AppendLine(
|
||||
$"<input type='radio' id='{EscapeAttr(id)}' " +
|
||||
$"name='{EscapeAttr(name)}' value='{EscapeAttr(stringValue)}' {Checked} data-setting-id='{EscapeAttr(name)}'>"
|
||||
);
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
res += "</div>";
|
||||
|
||||
return res;
|
||||
sb.AppendLine("</div>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetIntView(Setting setting)
|
||||
{
|
||||
string name = setting.Id;
|
||||
string id = name + "ID";
|
||||
|
||||
return string.Empty;
|
||||
return $@"
|
||||
<div class='form-group'>
|
||||
<label for='{id}'>{Escape(setting.Alias)}</label>
|
||||
<input id='{id}' type='number' value='{setting.IntValue}' step='1'
|
||||
data-setting-id='{EscapeAttr(name)}'>
|
||||
</div>";
|
||||
}
|
||||
|
||||
private static string Decorate(string content)
|
||||
private static string Decorate(string content, string title)
|
||||
{
|
||||
string res = "<!DOCTYPE html>"
|
||||
+ "<html lang=\"ru\">"
|
||||
+ "<head>"
|
||||
+ "<meta charset=\"utf-8\">"
|
||||
+ "<link rel=\"stylesheet\" href=\"style.css\">"
|
||||
+ "<title>Настройки Фишки</title>"
|
||||
+ "</head>"
|
||||
+ "<body>";
|
||||
string res = "<!DOCTYPE html>"
|
||||
+ "<html lang='ru'>"
|
||||
+ "<head>"
|
||||
+ "<meta charset='utf-8'>"
|
||||
+ "<meta name='viewport' content='width=device-width, initial-scale=1'>"
|
||||
+ "<link rel='stylesheet' href='style.css'>"
|
||||
+ $"<title>{Escape(title)}</title>"
|
||||
+ "</head>"
|
||||
+ "<body>";
|
||||
|
||||
res += content;
|
||||
|
||||
res += "</body>" +
|
||||
"</html>";
|
||||
|
||||
res += "</body></html>";
|
||||
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("'", "'");
|
||||
}
|
||||
|
||||
@ -6,40 +6,37 @@ using UnityEngine;
|
||||
|
||||
public static class SettingsApplier
|
||||
{
|
||||
public static (bool OK, string log) ApplySettings(Dictionary<string, string> keyValuePairs)
|
||||
public static (bool OK, string log) ApplySettings(
|
||||
Dictionary<string, string> keyValuePairs,
|
||||
bool assumeMissingBoolOff
|
||||
)
|
||||
{
|
||||
List<Setting> 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<Setting> 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<string> FalseBoolSettingIds(List<Setting> settings, List<string> entry)
|
||||
{
|
||||
List<string> ids = new();
|
||||
|
||||
var missing = new List<string>();
|
||||
foreach (Setting setting in settings)
|
||||
{
|
||||
if (entry.Contains(setting.Id)) continue;
|
||||
|
||||
ids.Add(setting.Id);
|
||||
missing.Add(setting.Id);
|
||||
}
|
||||
|
||||
List<string> BoolSettings = new List<string>();
|
||||
foreach (string id in ids)
|
||||
var boolIds = new List<string>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, "<h1>404 Not Found</h1>", 404);
|
||||
}
|
||||
|
||||
SendHtmlResponse(context, "<h1>404 Not Found</h1>", 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 = "<h1>Error: Method returned empty string</h1>";
|
||||
}
|
||||
|
||||
SendHtmlResponse(context, htmlContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("GetSettingsString TIMEOUT (5 seconds)!");
|
||||
Debug.LogError("HTML generation TIMEOUT (5 seconds)!");
|
||||
SendHtmlResponse(context, "<h1>Error: Settings generation timeout</h1>", 500);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Error in GetSettingsString: {ex.Message}");
|
||||
Debug.LogError($"Error in settings html: {ex.Message}");
|
||||
SendHtmlResponse(context, $"<h1>Error: {ex.Message}</h1>", 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<TextAsset>("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<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\"}");
|
||||
}
|
||||
}
|
||||
|
||||
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<string, string>();
|
||||
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<SettingsScreen>().RefreshViews());
|
||||
|
||||
// Отправляем ответ обратно
|
||||
string responseHtml = @"
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<title>Обновлено</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||
.success { color: green; font-weight: bold; }
|
||||
.info { margin: 20px 0; padding: 10px; background: #f0f0f0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class='success'>✓ Данные успешно получены!</h1>
|
||||
<div class='info'>
|
||||
<p>Сервер обработал ваш запрос.</p>
|
||||
<p>Полученные данные:</p>
|
||||
<ul>";
|
||||
|
||||
// Добавляем список полученных данных в ответ
|
||||
foreach (var kvp in formDict)
|
||||
{
|
||||
responseHtml += $"<li><strong>{kvp.Key}</strong>: {kvp.Value}</li>";
|
||||
}
|
||||
|
||||
responseHtml += @"
|
||||
</ul>
|
||||
</div>
|
||||
<a href='/' style='display: inline-block; margin-top: 20px; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px;'>
|
||||
Вернуться к настройкам
|
||||
</a>
|
||||
<script>
|
||||
// Через 5 секунд возвращаемся назад
|
||||
setTimeout(function() {
|
||||
window.location.href = '/';
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
SendHtmlResponse(context, responseHtml);
|
||||
SendHtmlResponse(context, "<html><body><h1>OK</h1><a href='/'>Back</a></body></html>");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Ошибка обработки POST /upd: {ex.Message}");
|
||||
Debug.LogError($"StackTrace: {ex.StackTrace}");
|
||||
SendHtmlResponse(context,
|
||||
$"<html><head><meta charset='UTF-8'><title>Ошибка</title></head><body>" +
|
||||
$"<h1 style='color:red;'>Ошибка: {ex.Message}</h1>" +
|
||||
$"<pre>{ex.StackTrace}</pre>" +
|
||||
$"<a href='/'>Вернуться назад</a></body></html>",
|
||||
500);
|
||||
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)
|
||||
{
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user