using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Shared.Engine.Utilities; using Shared.Models; using Shared.Models.Base; using Shared.Models.Events; using System.Buffers; using System.Collections.Concurrent; using System.Text; using System.Text.RegularExpressions; using System.Threading; namespace Shared.Engine { public class RchClientInfo { public int version { get; set; } public string host { get; set; } public string rchtype { get; set; } public int apkVersion { get; set; } public string player { get; set; } public string account_email { get; set; } public string unic_id { get; set; } public string profile_id { get; set; } public string token { get; set; } public object ob { get; set; } public Dictionary obs { get; set; } = new Dictionary(); } public class RchClient { #region static static readonly Timer _checkConnectionTimer = new Timer(CheckConnection, null, TimeSpan.FromMinutes(2), TimeSpan.FromMinutes(4)); static int _cronCheckConnectionWork = 0; static readonly ThreadLocal _serializerDefault = new ThreadLocal(JsonSerializer.CreateDefault); static readonly ThreadLocal _serializerIgnoreDeserialize = new ThreadLocal(() => JsonSerializer.Create(new JsonSerializerSettings { Error = (se, ev) => { ev.ErrorContext.Handled = true; } })); public static string ErrorMsg => AppInit.conf.rch.enable ? "rhub не работает с данным балансером" : "Включите rch в init.conf"; public record class hubEntry(string connectionId, string rchId, string url, string data, Dictionary headers, bool returnHeaders); public static EventHandler hub = null; public record class clientEntry(string ip, string host, RchClientInfo info, NwsConnection connection); public static readonly ConcurrentDictionary clients = new(); public record class rchIdEntry(MemoryStream ms, TaskCompletionSource tcs); public static readonly ConcurrentDictionary rchIds = new(); async static void CheckConnection(object state) { if (clients.IsEmpty) return; if (Interlocked.Exchange(ref _cronCheckConnectionWork, 1) == 1) return; try { string[] wsKeys = clients .Where(c => c.Value.connection != null) .Select(k => k.Key) .ToArray(); if (wsKeys.Length == 0) return; await Parallel.ForEachAsync(wsKeys, new ParallelOptions { MaxDegreeOfParallelism = Math.Max(2, Environment.ProcessorCount) }, async (connectionId, cancellationToken) => { if (clients.TryGetValue(connectionId, out var client) && client.connection == null) { var rch = new RchClient(connectionId); string result = await rch.SendHub("ping", useDefaultHeaders: false); if (result != "pong") OnDisconnected(connectionId); } }); } catch { } finally { Volatile.Write(ref _cronCheckConnectionWork, 0); } } public static void Registry(string ip, string connectionId, string host = null, string json = null, NwsConnection connection = null) { var info = new RchClientInfo(); if (json != null) { try { info = System.Text.Json.JsonSerializer.Deserialize(json); } catch { } } if (AppInit.conf.rch.blacklistHost != null && info.host != null) { foreach (string h in AppInit.conf.rch.blacklistHost) { if (info.host.Contains(h)) return; } } if (info == null) info = new RchClientInfo() { version = -1 }; clients.AddOrUpdate(connectionId, new clientEntry(ip, host, info, connection), (i, j) => new clientEntry(ip, host, info, connection)); if (InvkEvent.IsRchRegistry()) InvkEvent.RchRegistry(new EventRchRegistry(connectionId, ip, host, info, connection)); } public static void OnDisconnected(string connectionId) { if (clients.TryRemove(connectionId, out _)) { if (InvkEvent.IsRchDisconnected()) InvkEvent.RchDisconnected(new EventRchDisconnected(connectionId)); } } #endregion BaseSettings init; HttpContext httpContext; string ip, connectionId; bool enableRhub, rhub_fallback; public bool enable => init != null && init.rhub && enableRhub; public string connectionMsg { get; private set; } public string ipkey(string key) => enableRhub ? $"{key}:{ip}" : key; public string ipkey(string key, ProxyManager proxy) => $"{key}:{(enableRhub ? ip : proxy?.CurrentProxyIp)}"; public RchClient(string connectionId) { this.connectionId = connectionId; } public RchClient(HttpContext context, string host, BaseSettings init, RequestModel requestInfo, int? keepalive = null) { this.init = init; httpContext = context; enableRhub = init.rhub; rhub_fallback = init.rhub_fallback; ip = requestInfo.IP; if (enableRhub && rhub_fallback && init.rhub_geo_disable != null) { if (requestInfo.Country != null && init.rhub_geo_disable.Contains(requestInfo.Country)) { enableRhub = false; init.rhub = false; } } connectionMsg = System.Text.Json.JsonSerializer.Serialize(new { rch = true, ws = $"{host}/ws", nws = $"{(host.StartsWith("https") ? "wss" : "ws")}://{Regex.Replace(host, "^https?://", "")}/nws" }); } public void Disabled() { enableRhub = false; } #region Eval public void EvalRun(string data) { _= SendHub("evalrun", data).ConfigureAwait(false); } public Task Eval(string data) { return SendHub("eval", data); } async public Task Eval(string data, bool IgnoreDeserializeObject = false) { try { T result = default; await SendHub("eval", data, useDefaultHeaders: false, msAction: ms => { try { using (var streamReader = new StreamReader(ms, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: PoolInvk.bufferSize, leaveOpen: true)) { using (var jsonReader = new JsonTextReader(streamReader) { ArrayPool = NewtonsoftPool.Array }) { var serializer = IgnoreDeserializeObject ? _serializerIgnoreDeserialize.Value : _serializerDefault.Value; result = serializer.Deserialize(jsonReader); } } } catch { } }).ConfigureAwait(false); return result; } catch { return default; } } #endregion #region Headers async public Task<(JObject headers, string currentUrl, string body)> Headers(string url, string data, List headers = null, bool useDefaultHeaders = true) { try { // на версиях ниже java.lang.OutOfMemoryError if (484 > InfoConnected()?.apkVersion) return default; (JObject headers, string currentUrl, string body) result = default; await SendHub(url, data, headers, useDefaultHeaders, true, msAction: ms => { try { using (var streamReader = new StreamReader(ms, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: PoolInvk.bufferSize, leaveOpen: true)) { using (var jsonReader = new JsonTextReader(streamReader) { ArrayPool = NewtonsoftPool.Array }) { var serializer = _serializerDefault.Value; var job = serializer.Deserialize(jsonReader); if (!job.ContainsKey("body")) return; result = (job.Value("headers"), job.Value("currentUrl"), job.Value("body")); } } } catch { } }).ConfigureAwait(false); return result; } catch { return default; } } #endregion #region Get public Task Get(string url, List headers = null, bool useDefaultHeaders = true) { return SendHub(url, null, headers, useDefaultHeaders); } async public Task Get(string url, List headers = null, bool IgnoreDeserializeObject = false, bool useDefaultHeaders = true) { try { T result = default; await SendHub(url, null, headers, useDefaultHeaders, msAction: ms => { try { using (var streamReader = new StreamReader(ms, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: PoolInvk.bufferSize, leaveOpen: true)) { using (var jsonReader = new JsonTextReader(streamReader) { ArrayPool = NewtonsoftPool.Array }) { var serializer = IgnoreDeserializeObject ? _serializerIgnoreDeserialize.Value : _serializerDefault.Value; result = serializer.Deserialize(jsonReader); } } } catch { } }).ConfigureAwait(false); return result; } catch { return default; } } #endregion #region Span public Task GetSpan(Action> spanAction, string url, List headers = null, bool useDefaultHeaders = true) { return SendHub(url, null, headers, useDefaultHeaders, spanAction: spanAction); } public Task PostSpan(Action> spanAction, string url, string data, List headers = null, bool useDefaultHeaders = true) { return SendHub(url, data, headers, useDefaultHeaders, spanAction: spanAction); } #endregion #region Post public Task Post(string url, string data, List headers = null, bool useDefaultHeaders = true) { return SendHub(url, data, headers, useDefaultHeaders); } async public Task Post(string url, string data, List headers = null, bool IgnoreDeserializeObject = false, bool useDefaultHeaders = true) { try { T result = default; await SendHub(url, data, headers, useDefaultHeaders, msAction: ms => { try { using (var streamReader = new StreamReader(ms, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: PoolInvk.bufferSize, leaveOpen: true)) { using (var jsonReader = new JsonTextReader(streamReader) { ArrayPool = NewtonsoftPool.Array }) { var serializer = IgnoreDeserializeObject ? _serializerIgnoreDeserialize.Value : _serializerDefault.Value; result = serializer.Deserialize(jsonReader); } } } catch { } }).ConfigureAwait(false); return result; } catch { return default; } } #endregion #region SendHub async Task SendHub(string url, string data = null, List headers = null, bool useDefaultHeaders = true, bool returnHeaders = false, bool waiting = true, Action> spanAction = null, Action msAction = null) { if (hub == null) return null; var clientInfo = SocketClient(); if (string.IsNullOrEmpty(connectionId) || !clients.ContainsKey(connectionId)) connectionId = clientInfo.connectionId; if (string.IsNullOrEmpty(connectionId)) return null; string rchId = Guid.NewGuid().ToString(); var ms = PoolInvk.msm.GetStream(); try { var rchHub = rchIds.GetOrAdd(rchId, _ => new rchIdEntry(ms, new TaskCompletionSource())); #region send_headers Dictionary send_headers = null; if (useDefaultHeaders && clientInfo.data?.info?.rchtype == "apk") { send_headers = new Dictionary(Http.defaultUaHeaders, StringComparer.OrdinalIgnoreCase) { { "accept-language", "ru-RU,ru;q=0.9,uk-UA;q=0.8,uk;q=0.7,en-US;q=0.6,en;q=0.5" } }; } if (headers != null) { if (send_headers == null) send_headers = new Dictionary(headers.Count, StringComparer.OrdinalIgnoreCase); foreach (var h in headers) send_headers[h.name] = h.val; } if (send_headers != null && send_headers.Count > 0 && clientInfo.data?.info?.rchtype != "apk") { var new_headers = new Dictionary(Math.Min(10, send_headers.Count)); foreach (var h in send_headers) { var key = h.Key; if (key.StartsWith("sec-ch-", StringComparison.OrdinalIgnoreCase) || key.StartsWith("sec-fetch-", StringComparison.OrdinalIgnoreCase)) continue; if (key.Equals("user-agent", StringComparison.OrdinalIgnoreCase) || key.Equals("cookie", StringComparison.OrdinalIgnoreCase) || key.Equals("referer", StringComparison.OrdinalIgnoreCase) || key.Equals("origin", StringComparison.OrdinalIgnoreCase) || key.Equals("accept", StringComparison.OrdinalIgnoreCase) || key.Equals("accept-language", StringComparison.OrdinalIgnoreCase) || key.Equals("accept-encoding", StringComparison.OrdinalIgnoreCase) || key.Equals("cache-control", StringComparison.OrdinalIgnoreCase) || key.Equals("dnt", StringComparison.OrdinalIgnoreCase) || key.Equals("pragma", StringComparison.OrdinalIgnoreCase) || key.Equals("priority", StringComparison.OrdinalIgnoreCase) || key.Equals("upgrade-insecure-requests", StringComparison.OrdinalIgnoreCase)) continue; new_headers[key] = h.Value; } send_headers = new_headers; } #endregion hub.Invoke(null, new hubEntry(connectionId, rchId, url, data, Http.NormalizeHeaders(send_headers), returnHeaders)); if (!waiting) return null; string stringValue = await rchHub.tcs.Task.WaitAsync(TimeSpan.FromSeconds(rhub_fallback ? 8 : 12)).ConfigureAwait(false); if (stringValue != null) { if (string.IsNullOrWhiteSpace(stringValue)) return null; spanAction?.Invoke(stringValue); return stringValue; } else { if (ms.Length == 0) return null; if (msAction != null) { msAction.Invoke(ms); return null; } string resultString = null; OwnerTo.Span(ms, Encoding.UTF8, span => { if (span.IsEmpty) return; if (spanAction != null) { spanAction.Invoke(span); return; } resultString = span.ToString(); }); return resultString; } } catch { return null; } finally { ms.Dispose(); rchIds.TryRemove(rchId, out _); } } #endregion #region IsNotConnected public bool IsNotConnected() => IsNotConnected(ip); bool IsNotConnected(string ip) { if (!enableRhub) return false; // rch не используется if (httpContext != null && httpContext.Request.QueryString.Value.Contains("&checksearch=true")) return true; // заглушка для checksearch return SocketClient().connectionId == null; } #endregion #region IsRequiredConnected public bool IsRequiredConnected() { if (httpContext != null) { var requestInfo = httpContext.Features.Get(); if (requestInfo.IsLocalRequest) return false; } if (AppInit.conf.rch.requiredConnected // Обязательное подключение || (init.rchstreamproxy != null && !init.streamproxy)) // Нужно знать rchtype устройства return SocketClient().connectionId == null; return false; } #endregion #region IsNotSupport public bool IsNotSupport(out string rch_msg) { rch_msg = null; if (!enableRhub) return false; // rch не используется if (httpContext != null && httpContext.Request.QueryString.Value.Contains("&checksearch=true")) return false; // заглушка для checksearch if (IsNotSupportRchAccess(init.RchAccessNotSupport(nocheck: true), out rch_msg)) return true; return IsNotSupportStreamAccess(init.StreamAccessNotSupport(), out rch_msg); } public bool IsNotSupportRchAccess(string rch_deny, out string rch_msg) { rch_msg = null; if (rch_deny == null) return false; if (!enableRhub) return false; // rch не используется if (httpContext != null && httpContext.Request.QueryString.Value.Contains("&checksearch=true")) return false; // заглушка для checksearch var info = InfoConnected(); if (info == null || string.IsNullOrEmpty(info.rchtype)) return false; // клиент не в сети // разрешен возврат на сервер if (rhub_fallback) { if (rch_deny.Contains(info.rchtype)) { enableRhub = false; init.rhub = false; } return false; } // указан webcorshost или включен corseu if (!string.IsNullOrWhiteSpace(init.webcorshost) || init.corseu) return false; if (AppInit.conf.rch.notSupportMsg != null) rch_msg = AppInit.conf.rch.notSupportMsg; else if (info.rchtype == "web") rch_msg = "На MSX недоступно"; else rch_msg = "Только на android"; return rch_deny.Contains(info.rchtype); } public bool IsNotSupportStreamAccess(string deny, out string rch_msg) { rch_msg = null; if (deny == null) return false; if (!enableRhub) return false; // rch не используется if (httpContext != null && httpContext.Request.QueryString.Value.Contains("&checksearch=true")) return false; // заглушка для checksearch var info = InfoConnected(); if (info == null || string.IsNullOrEmpty(info.rchtype)) return false; // клиент не в сети if (AppInit.conf.rch.notSupportMsg != null) rch_msg = AppInit.conf.rch.notSupportMsg; else if (info.rchtype == "web") rch_msg = "На MSX недоступно"; else rch_msg = "Только на android"; return deny.Contains(info.rchtype); } #endregion #region InfoConnected public RchClientInfo InfoConnected() { return SocketClient().data?.info; } #endregion #region SocketClient public (string connectionId, clientEntry data) SocketClient() { if (AppInit.conf.WebSocket.type == "nws") { if (!string.IsNullOrEmpty(connectionId) && clients.TryGetValue(connectionId, out var _client)) { if (ip == _client.ip) return (connectionId, _client); } if (httpContext != null && httpContext.Request.Query.TryGetValue("nws_id", out var _nwsid)) { string nws_id = _nwsid.ToString(); if (!string.IsNullOrEmpty(nws_id) && clients.TryGetValue(nws_id, out _client)) { if (ip == _client.ip) return (nws_id, _client); } } } else { var client = clients.LastOrDefault(i => i.Value.ip == ip); if (client.Value.info?.rchtype != null) return (client.Key, client.Value); } return default; } #endregion } }