Files
MeWeb/src/MeWeb.Core/Checks/ServerProbe.cs
T

194 lines
6.8 KiB
C#
Raw Normal View History

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<OnlineReport> 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<ViewIp> 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<WebResult<ViewProbeTicket>> 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<WebResult<ViewProbe>> Knock(bool v6, string id, List<int> 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<WebResult<ViewShared>> 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<bool> 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<WebResult<T>> Send<T>(HttpMethod method, string url, HttpContent content, System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> type, TimeSpan patience, CancellationToken ct) where T : class
{
var result = new WebResult<T>();
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<IPAddress> 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<IPAddress[]> ServerAddresses(CancellationToken ct)
{
try
{
return await Dns.GetHostAddressesAsync(Endpoints.Host, ct);
}
catch (Exception) when (!ct.IsCancellationRequested)
{
return [];
}
}
static async Task<bool> Resolves(string host, CancellationToken ct)
{
try
{
return (await Dns.GetHostAddressesAsync(host, ct)).Length > 0;
}
catch (Exception) when (!ct.IsCancellationRequested)
{
return false;
}
}
async Task<ViewInfo> 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);
}
}
}