Files
MeWeb/src/MeWeb.Core/Checks/ServerProbe.cs
T
thepraandClaude Opus 5.5 d408edea98
Build / Desktop, server and tools (push) Successful in 1m8s
House rules: the build checks them, and every refusal from the server answers 400, 403 or 404
The server no longer answers 409, 413 or 429: a busy probe, a share or upload that is too large and a request over
the limit now answer 400 with the same reason as before, and still say when to try again. The app reads the reason,
so nothing changes for it; versions up to 0.15.4 report a limited speed test as unmeasured until they update.
The app's commands can now be cancelled all the way down, and the house rules are written into PATTERNS.md and
checked on every build, Android included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4YtEJrgxG12Pc39Fai8Bd
2026-11-10 05:32:04 +01:00

195 lines
6.8 KiB
C#

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);
}
}
}