using System.Net; using System.Net.Http.Json; using System.Net.Sockets; using MeWeb.Shared; namespace MeWeb.Core.Checks; public enum OnlineState { Online, Captive, Rewritten, NoNames, Offline, } public class OnlineReport { public OnlineState State { get; set; } public ViewInfo Server { get; set; } public string Portal { get; set; } } public class ServerProbe(HttpClient https, HttpClient plain) { static readonly TimeSpan Patience = TimeSpan.FromSeconds(6); public async Task Online(CancellationToken ct) { var names = Resolves(Endpoints.Host, ct); var info = Info(ct); var web = Plain(ct); await Task.WhenAll(names, info, web); var (plainClean, portal) = web.Result; if (info.Result is not null) return new OnlineReport { State = plainClean is false ? OnlineState.Rewritten : OnlineState.Online, Server = info.Result, Portal = portal }; if (plainClean is false) return new OnlineReport { State = OnlineState.Captive, Portal = portal }; return new OnlineReport { State = names.Result ? OnlineState.Offline : OnlineState.NoNames }; } public async Task Echo(bool v6, CancellationToken ct) { try { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(Patience); var origin = v6 ? Endpoints.BaseV6 : Endpoints.BaseV4; return await https.GetFromJsonAsync(origin + Endpoints.Server.GET_Ip, MeWebJsonContext.Default.ViewIp, timeout.Token); } catch (Exception) when (!ct.IsCancellationRequested) { return null; } } public async Task> Ticket(bool v6, CancellationToken ct) { var origin = v6 ? Endpoints.BaseV6 : Endpoints.BaseV4; return await Send(HttpMethod.Post, origin + Endpoints.Probe.POST_Ticket, null, MeWebJsonContext.Default.ViewProbeTicket, Patience, ct); } public async Task> Knock(bool v6, string id, List ports, CancellationToken ct) { var origin = v6 ? Endpoints.BaseV6 : Endpoints.BaseV4; var form = JsonContent.Create(new ProbeForm { Ports = ports }, MeWebJsonContext.Default.ProbeForm); return await Send(HttpMethod.Post, origin + Endpoints.Probe.POST_M_Run(id), form, MeWebJsonContext.Default.ViewProbe, TimeSpan.FromSeconds(30), ct); } public async Task> Share(string json, CancellationToken ct) { var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); return await Send(HttpMethod.Post, Endpoints.Base + Endpoints.Share.POST_Create, content, MeWebJsonContext.Default.ViewShared, Patience, ct); } public async Task Unshare(string id, string token, CancellationToken ct) { try { using var request = new HttpRequestMessage(HttpMethod.Delete, Endpoints.Base + Endpoints.Share.DELETE_M_One(id)); request.Headers.TryAddWithoutValidation(DeleteTokenHeader, token); using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(Patience); using var response = await https.SendAsync(request, timeout.Token); return response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NotFound; } catch (Exception) when (!ct.IsCancellationRequested) { return false; } } public const string DeleteTokenHeader = "X-Delete-Token"; async Task> Send(HttpMethod method, string url, HttpContent content, System.Text.Json.Serialization.Metadata.JsonTypeInfo type, TimeSpan patience, CancellationToken ct) where T : class { var result = new WebResult(); try { using var request = new HttpRequestMessage(method, url) { Content = content }; using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(patience); using var response = await https.SendAsync(request, timeout.Token); if (response.IsSuccessStatusCode) { result.Data = await response.Content.ReadFromJsonAsync(type, timeout.Token); return result; } var refusal = response.Content.Headers.ContentType?.MediaType == "application/json" ? await response.Content.ReadFromJsonAsync(MeWebJsonContext.Default.ViewRefusal, timeout.Token) : null; return result.Invalidate(refusal ?? new ViewRefusal { Reason = $"http-{(int)response.StatusCode}" }, (int)response.StatusCode); } catch (Exception) when (!ct.IsCancellationRequested) { return result.Invalidate("unreachable"); } } public static async Task ResolveV4(CancellationToken ct) { try { var addresses = await Dns.GetHostAddressesAsync(Endpoints.HostV4, AddressFamily.InterNetwork, ct); return addresses.FirstOrDefault(); } catch (Exception) when (!ct.IsCancellationRequested) { return null; } } public static async Task ServerAddresses(CancellationToken ct) { try { return await Dns.GetHostAddressesAsync(Endpoints.Host, ct); } catch (Exception) when (!ct.IsCancellationRequested) { return []; } } static async Task Resolves(string host, CancellationToken ct) { try { return (await Dns.GetHostAddressesAsync(host, ct)).Length > 0; } catch (Exception) when (!ct.IsCancellationRequested) { return false; } } async Task Info(CancellationToken ct) { try { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(Patience); return await https.GetFromJsonAsync(Endpoints.Base + Endpoints.Server.GET_Info, MeWebJsonContext.Default.ViewInfo, timeout.Token); } catch (Exception) when (!ct.IsCancellationRequested) { return null; } } async Task<(bool? Clean, string Portal)> Plain(CancellationToken ct) { try { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(Patience); using var response = await plain.GetAsync($"http://{Endpoints.Host}{Endpoints.Server.GET_Info}", HttpCompletionOption.ResponseHeadersRead, timeout.Token); var location = response.Headers.Location; if ((int)response.StatusCode is >= 300 and < 400 && location is not null) { var target = location.IsAbsoluteUri ? location : new Uri(new Uri($"http://{Endpoints.Host}"), location); var clean = target.Scheme == Uri.UriSchemeHttps && string.Equals(target.Host, Endpoints.Host, StringComparison.OrdinalIgnoreCase); return (clean, clean ? null : target.Host); } if (response.IsSuccessStatusCode && response.Content.Headers.ContentType?.MediaType == "application/json") return (true, null); return (false, null); } catch (Exception) when (!ct.IsCancellationRequested) { return (null, null); } } }