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

190 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<string> Echo(bool v6, CancellationToken ct)
{
try
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(Patience);
var origin = v6 ? Endpoints.BaseV6 : Endpoints.BaseV4;
var answer = await https.GetFromJsonAsync(origin + Endpoints.Server.GET_Ip, MeWebJsonContext.Default.ViewIp, timeout.Token);
return answer?.Address;
}
catch (Exception) when (!ct.IsCancellationRequested)
{
return null;
}
}
public async Task<(ViewProbeTicket Ticket, ViewRefusal Refusal)> 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, ct);
}
public async Task<(ViewProbe Probe, ViewRefusal Refusal)> 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, ct, TimeSpan.FromSeconds(30));
}
public async Task<(ViewShared Shared, ViewRefusal Refusal)> 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, 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<(T Value, ViewRefusal Refusal)> Send<T>(HttpMethod method, string url, HttpContent content, System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> type, CancellationToken ct, TimeSpan? patience = null) where T : class
{
try
{
using var request = new HttpRequestMessage(method, url) { Content = content };
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(patience ?? Patience);
using var response = await https.SendAsync(request, timeout.Token);
if (response.IsSuccessStatusCode) return (await response.Content.ReadFromJsonAsync(type, timeout.Token), null);
var refusal = response.Content.Headers.ContentType?.MediaType == "application/json"
? await response.Content.ReadFromJsonAsync(MeWebJsonContext.Default.ViewRefusal, timeout.Token)
: null;
return (null, refusal ?? new ViewRefusal { Reason = $"http-{(int)response.StatusCode}" });
}
catch (Exception) when (!ct.IsCancellationRequested)
{
return (null, new ViewRefusal { Reason = "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);
}
}
}