2026-10-15 04:39:03 +02:00
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 18:10:19 +02:00
|
|
|
public async Task<ViewIp> Echo(bool v6, CancellationToken ct)
|
2026-10-15 04:39:03 +02:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
|
|
|
timeout.CancelAfter(Patience);
|
|
|
|
|
var origin = v6 ? Endpoints.BaseV6 : Endpoints.BaseV4;
|
2026-10-16 18:10:19 +02:00
|
|
|
return await https.GetFromJsonAsync(origin + Endpoints.Server.GET_Ip, MeWebJsonContext.Default.ViewIp, timeout.Token);
|
2026-10-15 04:39:03 +02:00
|
|
|
}
|
2026-10-15 09:10:37 +02:00
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
2026-10-15 04:39:03 +02:00
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-10 05:32:04 +01:00
|
|
|
public async Task<WebResult<ViewProbeTicket>> Ticket(bool v6, CancellationToken ct)
|
2026-10-15 05:41:44 +02:00
|
|
|
{
|
2026-10-15 22:49:40 +02:00
|
|
|
var origin = v6 ? Endpoints.BaseV6 : Endpoints.BaseV4;
|
2026-11-10 05:32:04 +01:00
|
|
|
return await Send(HttpMethod.Post, origin + Endpoints.Probe.POST_Ticket, null, MeWebJsonContext.Default.ViewProbeTicket, Patience, ct);
|
2026-10-15 05:41:44 +02:00
|
|
|
}
|
|
|
|
|
|
2026-11-10 05:32:04 +01:00
|
|
|
public async Task<WebResult<ViewProbe>> Knock(bool v6, string id, List<int> ports, CancellationToken ct)
|
2026-10-15 05:41:44 +02:00
|
|
|
{
|
2026-10-15 22:49:40 +02:00
|
|
|
var origin = v6 ? Endpoints.BaseV6 : Endpoints.BaseV4;
|
2026-10-15 05:41:44 +02:00
|
|
|
var form = JsonContent.Create(new ProbeForm { Ports = ports }, MeWebJsonContext.Default.ProbeForm);
|
2026-11-10 05:32:04 +01:00
|
|
|
return await Send(HttpMethod.Post, origin + Endpoints.Probe.POST_M_Run(id), form, MeWebJsonContext.Default.ViewProbe, TimeSpan.FromSeconds(30), ct);
|
2026-10-15 05:41:44 +02:00
|
|
|
}
|
|
|
|
|
|
2026-11-10 05:32:04 +01:00
|
|
|
public async Task<WebResult<ViewShared>> Share(string json, CancellationToken ct)
|
2026-10-15 05:41:44 +02:00
|
|
|
{
|
|
|
|
|
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
|
2026-11-10 05:32:04 +01:00
|
|
|
return await Send(HttpMethod.Post, Endpoints.Base + Endpoints.Share.POST_Create, content, MeWebJsonContext.Default.ViewShared, Patience, ct);
|
2026-10-15 05:41:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-10-15 09:10:37 +02:00
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
2026-10-15 05:41:44 +02:00
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public const string DeleteTokenHeader = "X-Delete-Token";
|
|
|
|
|
|
2026-11-10 05:32:04 +01:00
|
|
|
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
|
2026-10-15 05:41:44 +02:00
|
|
|
{
|
2026-11-10 05:32:04 +01:00
|
|
|
var result = new WebResult<T>();
|
2026-10-15 05:41:44 +02:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using var request = new HttpRequestMessage(method, url) { Content = content };
|
|
|
|
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
2026-11-10 05:32:04 +01:00
|
|
|
timeout.CancelAfter(patience);
|
2026-10-15 05:41:44 +02:00
|
|
|
using var response = await https.SendAsync(request, timeout.Token);
|
2026-11-10 05:32:04 +01:00
|
|
|
if (response.IsSuccessStatusCode)
|
|
|
|
|
{
|
|
|
|
|
result.Data = await response.Content.ReadFromJsonAsync(type, timeout.Token);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2026-10-15 05:41:44 +02:00
|
|
|
var refusal = response.Content.Headers.ContentType?.MediaType == "application/json"
|
|
|
|
|
? await response.Content.ReadFromJsonAsync(MeWebJsonContext.Default.ViewRefusal, timeout.Token)
|
|
|
|
|
: null;
|
2026-11-10 05:32:04 +01:00
|
|
|
return result.Invalidate(refusal ?? new ViewRefusal { Reason = $"http-{(int)response.StatusCode}" }, (int)response.StatusCode);
|
2026-10-15 05:41:44 +02:00
|
|
|
}
|
2026-10-15 09:10:37 +02:00
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
2026-10-15 05:41:44 +02:00
|
|
|
{
|
2026-11-10 05:32:04 +01:00
|
|
|
return result.Invalidate("unreachable");
|
2026-10-15 05:41:44 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 04:39:03 +02:00
|
|
|
public static async Task<IPAddress> ResolveV4(CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var addresses = await Dns.GetHostAddressesAsync(Endpoints.HostV4, AddressFamily.InterNetwork, ct);
|
|
|
|
|
return addresses.FirstOrDefault();
|
|
|
|
|
}
|
2026-10-15 09:10:37 +02:00
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
2026-10-15 04:39:03 +02:00
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 12:59:42 +02:00
|
|
|
public static async Task<IPAddress[]> ServerAddresses(CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await Dns.GetHostAddressesAsync(Endpoints.Host, ct);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 04:39:03 +02:00
|
|
|
static async Task<bool> Resolves(string host, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return (await Dns.GetHostAddressesAsync(host, ct)).Length > 0;
|
|
|
|
|
}
|
2026-10-15 09:10:37 +02:00
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
2026-10-15 04:39:03 +02:00
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-10-15 09:10:37 +02:00
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
2026-10-15 04:39:03 +02:00
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-10-15 09:10:37 +02:00
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
2026-10-15 04:39:03 +02:00
|
|
|
{
|
|
|
|
|
return (null, null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|