- The provider card now names the address block your connection belongs to in Asia and the Pacific, in Africa, and in Latin America and the Caribbean, not only in Europe and the Middle East. The blocks come from the address registries APNIC, AFRINIC and LACNIC, used with their permission, and travel inside the app, so this works without a connection too. - In Latin America and the Caribbean the registry gives no names for its blocks, so the card says how the block was handed out instead, for example "passed on by the provider to one of its customers". - The app is about 6 MB larger, because it carries these blocks. - The app's name no longer has quotation marks where it stands on its own: the window title, the name under the icon, the version line and the website's titles. Inside a sentence it keeps them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
516 lines
24 KiB
C#
516 lines
24 KiB
C#
using System.Globalization;
|
|
|
|
using MeWeb.Core.History;
|
|
using MeWeb.Core.Knowledge;
|
|
using MeWeb.Core.Net;
|
|
using MeWeb.Core.Router;
|
|
using MeWeb.Shared;
|
|
|
|
namespace MeWeb.Core.Checks;
|
|
|
|
public static class Verdicts
|
|
{
|
|
public static Finding Online(OnlineReport report, LocalSnapshot local)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Online };
|
|
switch (report.State)
|
|
{
|
|
case OnlineState.Online:
|
|
finding.Outcome = Outcome.Good;
|
|
finding.Summary = "OnlineGood";
|
|
break;
|
|
case OnlineState.Rewritten:
|
|
finding.Outcome = Outcome.Caution;
|
|
finding.Summary = "OnlineRewritten";
|
|
finding.With("FactRedirectedTo", report.Portal);
|
|
break;
|
|
case OnlineState.Captive:
|
|
finding.Outcome = Outcome.Caution;
|
|
finding.Summary = "OnlineCaptive";
|
|
finding.With("FactRedirectedTo", report.Portal);
|
|
break;
|
|
case OnlineState.NoNames when local.Active is not null:
|
|
finding.Outcome = Outcome.Bad;
|
|
finding.Summary = "OnlineNoNames";
|
|
break;
|
|
default:
|
|
finding.Outcome = Outcome.Bad;
|
|
finding.Summary = local.Active is null ? "OnlineNoNetwork" : "OnlineOffline";
|
|
break;
|
|
}
|
|
return finding;
|
|
}
|
|
|
|
public static Finding Connection(LocalSnapshot local, CellularReport cellular, KnowledgeBase knowledge)
|
|
{
|
|
var active = local.Active;
|
|
if (active is null) return new Finding { Id = CheckId.Connection, Outcome = Outcome.Bad, Summary = "ConnectionNone" };
|
|
var finding = new Finding
|
|
{
|
|
Id = CheckId.Connection,
|
|
Outcome = Outcome.Info,
|
|
Summary = active.Kind switch
|
|
{
|
|
LinkKind.Wifi => "ConnectionWifi",
|
|
LinkKind.Cable => "ConnectionCable",
|
|
LinkKind.Mobile => "ConnectionMobile",
|
|
_ => "ConnectionOther",
|
|
},
|
|
};
|
|
finding.With("FactAdapter", active.Name);
|
|
if (active.SpeedMbps > 0) finding.WithKey("FactLinkSpeed", "ValueMbps", active.SpeedMbps.ToString(CultureInfo.InvariantCulture));
|
|
finding.With("FactLocalAddress", local.LocalV4);
|
|
var v6 = local.GlobalV6;
|
|
if (v6.Count > 0) finding.With("FactLocalIpv6", string.Join("\n", v6));
|
|
finding.With("FactGateway", local.GatewayV4 ?? active.Gateways.FirstOrDefault());
|
|
if (local.Dns.Count > 0) finding.With("FactDns", string.Join(", ", local.Dns.Take(3)));
|
|
if (cellular is null) return finding;
|
|
var name = !string.IsNullOrWhiteSpace(cellular.Operator) ? cellular.Operator.Trim() : knowledge.CarrierFor(cellular.NetworkCode) ?? cellular.Carrier;
|
|
var code = cellular.NetworkCode is { Length: >= 5 } network ? $"{network[..3]} {network[3..]}" : null;
|
|
finding.With("FactOperator", code is null ? name : name is null ? code : $"{name} ({code})");
|
|
finding.With("FactRadio", cellular.Radio);
|
|
if (cellular.Roaming is not null) finding.WithKey("FactRoaming", cellular.Roaming == true ? "ValueYes" : "ValueNo");
|
|
return finding;
|
|
}
|
|
|
|
public static Finding Addresses(string v4, string v6, LocalSnapshot local, RouterReport router)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Addresses };
|
|
(finding.Outcome, finding.Summary) = (v4, v6) switch
|
|
{
|
|
(not null, not null) => (Outcome.Good, "AddressesBoth"),
|
|
(not null, null) => (Outcome.Caution, "AddressesV4Only"),
|
|
(null, not null) => (Outcome.Caution, "AddressesV6Only"),
|
|
_ => (Outcome.Unknown, "AddressesNone"),
|
|
};
|
|
if (v4 is not null)
|
|
{
|
|
finding.With("FactPublicV4", v4);
|
|
finding.WithKey("FactWhoHasIt", Holder(NatOf(local, router, v4)));
|
|
}
|
|
if (v6 is not null)
|
|
{
|
|
finding.With("FactPublicV6", v6);
|
|
var own = local.Active?.Addresses.Any(a => Net.Addresses.SameAddress(a, v6)) == true;
|
|
finding.WithKey("FactWhoHasIpv6", own ? "ValueOnThisDevice" : "ValueIpv6NotThisDevice");
|
|
}
|
|
return finding;
|
|
}
|
|
|
|
public static Finding Provider(KnowledgeBase knowledge, AsnEntry v4, AsnEntry v6, RegistryBlock blockV4, RegistryBlock blockV6, bool anyAddress, bool mobile, RunReport run = null)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Provider };
|
|
if (!anyAddress)
|
|
{
|
|
finding.Outcome = Outcome.Unknown;
|
|
finding.Summary = "ProviderNoAddress";
|
|
return finding;
|
|
}
|
|
if (!knowledge.HasAsnTable)
|
|
{
|
|
finding.Outcome = Outcome.Unknown;
|
|
finding.Summary = "ProviderNoMap";
|
|
return finding;
|
|
}
|
|
var main = v4 ?? v6;
|
|
if (main is null)
|
|
{
|
|
finding.Outcome = Outcome.Unknown;
|
|
finding.Summary = "ProviderNotFound";
|
|
return finding;
|
|
}
|
|
var profile = knowledge.ProviderFor(main.Number);
|
|
var observed = knowledge.ObservedFor(main.Number, mobile);
|
|
var hand = mobile && observed is not null ? null : profile;
|
|
finding.Outcome = Outcome.Info;
|
|
finding.Summary = profile is null && observed is null ? "ProviderUnlisted" : mobile && observed is null ? "ProviderListedMobile" : "ProviderListed";
|
|
finding.Args = [profile?.Name ?? Display(main)];
|
|
finding.With("FactNetwork", Describe(main));
|
|
if (v6 is not null && v4 is not null && v6.Number != v4.Number) finding.With("FactNetworkIpv6", Describe(v6));
|
|
Block(finding, "FactBlock", v4 is not null ? blockV4 : blockV6);
|
|
if (v4 is not null && blockV6 is not null && (blockV6.Name != blockV4?.Name || blockV6.Status != blockV4?.Status)) Block(finding, "FactBlockIpv6", blockV6);
|
|
var upstreams = knowledge.NetworkFor(main.Number)?.Upstreams ?? [];
|
|
if (upstreams.Count > 0) finding.With("FactUpstreams", string.Join("\n", upstreams.Select(u => $"{knowledge.NameOf(u) ?? "AS" + u} (AS{u})")));
|
|
if (hand is null && observed is null) return finding;
|
|
var usual = AddFact(finding, "FactProviderIpv4", "ProviderIpv4", hand?.Ipv4, observed?.Ipv4, observed?.To);
|
|
var sharedHere = run?.Nat is NatKind.Shared or NatKind.Translated;
|
|
var ownHere = run?.Nat is NatKind.HomeRouter or NatKind.Direct or NatKind.DoubleNat;
|
|
if ((usual is "Shared" or "SharedPorts" && ownHere) || (usual is "PublicDynamic" or "Public" && sharedHere))
|
|
finding.WithKey("FactThisLine", Holder(run.Nat));
|
|
AddFact(finding, "FactProviderPublic", "ProviderPublic", hand?.PublicAddress, null, null);
|
|
AddFact(finding, "FactProviderIpv6", "ProviderIpv6", hand?.Ipv6, observed?.Ipv6, observed?.To);
|
|
var port25 = AddFact(finding, "FactProviderPort25", "ProviderPort25", hand?.Port25, observed?.Port25, observed?.To);
|
|
if (run?.Mail?.Port25 == Core.Mail.PortState.Open && port25 is "Blocked" or "BlockedOnRequest" or "Closed")
|
|
finding.WithKey("FactThisLine", "ValuePort25Open", run.Mail.Banner ?? "");
|
|
else if (run?.Mail?.Port25 == Core.Mail.PortState.Blocked && port25 is "Open")
|
|
finding.WithKey("FactThisLine", "ValuePort25Blocked");
|
|
if (hand?.Sheet is not null) finding.Facts.Add(new Fact { Label = "FactProviderSheet", Key = "ValueOpenSheet", Link = hand.Sheet });
|
|
return finding;
|
|
}
|
|
|
|
public static Finding Router(RouterReport router, LocalSnapshot local)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Router };
|
|
if (router.Gateway is null)
|
|
{
|
|
finding.Outcome = Outcome.Info;
|
|
finding.Summary = local.Active?.Kind == LinkKind.Mobile ? "RouterMobile" : "RouterNone";
|
|
return finding;
|
|
}
|
|
(finding.Outcome, finding.Summary) = router switch
|
|
{
|
|
{ AnyMapping: true } => (Outcome.Good, "RouterOpens"),
|
|
{ AnyAnswer: true } => (Outcome.Caution, "RouterRefuses"),
|
|
_ => (Outcome.Caution, "RouterSilent"),
|
|
};
|
|
finding.With("FactRouter", Join(router.Make, router.Label));
|
|
finding.With("FactGateway", router.Gateway);
|
|
finding.WithKey("FactUpnp", AnswerKey(router.Upnp), router.Upnp == Answer.Refused ? router.UpnpError.ToString(CultureInfo.InvariantCulture) : router.UpnpVersion.ToString(CultureInfo.InvariantCulture));
|
|
finding.WithKey("FactNatPmp", AnswerKey(router.NatPmp), router.NatPmpResult.ToString(CultureInfo.InvariantCulture));
|
|
finding.WithKey("FactPcp", AnswerKey(router.Pcp), router.PcpResult.ToString(CultureInfo.InvariantCulture));
|
|
finding.With("FactRouterWan", router.WanAddress);
|
|
return finding;
|
|
}
|
|
|
|
public static (Finding Finding, NatKind Kind) Nat(LocalSnapshot local, RouterReport router, string v4, string v6, List<Hop> hops)
|
|
{
|
|
var kind = NatOf(local, router, v4);
|
|
var finding = new Finding { Id = CheckId.Nat };
|
|
(finding.Outcome, finding.Summary) = kind switch
|
|
{
|
|
NatKind.Direct => (Outcome.Good, "NatDirect"),
|
|
NatKind.HomeRouter => (Outcome.Good, "NatHomeRouter"),
|
|
NatKind.DoubleNat => (Outcome.Caution, "NatDouble"),
|
|
NatKind.Shared => (Outcome.Bad, "NatShared"),
|
|
NatKind.Translated => (Outcome.Bad, "NatTranslated"),
|
|
NatKind.NoIpv4 => (Outcome.Info, "NatNoIpv4"),
|
|
_ => (Outcome.Unknown, "NatUnknown"),
|
|
};
|
|
finding.With("FactRouterWan", router?.WanAddress);
|
|
finding.With("FactPublicV4", v4);
|
|
var before = Route.BeforeInternet(hops);
|
|
if (hops.Count > 0) finding.With("FactRoute", string.Join(" → ", before.Select(h => h.Address ?? "*").Append(hops.FirstOrDefault(h => h.Kind == AddressKind.Public)?.Address ?? "…")));
|
|
if (kind == NatKind.Unknown && before.Any(h => h.Kind == AddressKind.Shared)) finding.WithKey("FactHint", "HintSharedHop");
|
|
|
|
var v6Direct = v6 is not null && local.Active?.Addresses.Any(a => Net.Addresses.SameAddress(a, v6)) == true;
|
|
if (v6 is not null) finding.WithKey("FactIpv6", v6Direct ? "ValueIpv6Direct" : "ValueIpv6Translated");
|
|
if (v6Direct && finding.Outcome is Outcome.Bad or Outcome.Unknown && kind != NatKind.NoIpv4)
|
|
{
|
|
finding.Outcome = Outcome.Caution;
|
|
finding.WithKey("FactWayOut", "ValueIpv6Way");
|
|
}
|
|
return (finding, kind);
|
|
}
|
|
|
|
static string Holder(NatKind nat) => nat switch
|
|
{
|
|
NatKind.Direct => "ValueOnThisDevice",
|
|
NatKind.Shared or NatKind.Translated => "ValueSharedProvider",
|
|
_ => "ValueSharedAtHome",
|
|
};
|
|
|
|
public static NatKind NatOf(LocalSnapshot local, RouterReport router, string v4)
|
|
{
|
|
if (v4 is null) return NatKind.NoIpv4;
|
|
if (local.Active?.Addresses.Any(a => Net.Addresses.SameAddress(a, v4)) == true) return NatKind.Direct;
|
|
var localV4 = Net.Addresses.Parse(local.LocalV4);
|
|
if (router?.Gateway is null && localV4 is not null && Net.Addresses.Classify(localV4) is AddressKind.Shared or AddressKind.Private && local.Active?.Kind == LinkKind.Mobile)
|
|
return NatKind.Shared;
|
|
var wan = Net.Addresses.Parse(router?.WanAddress);
|
|
if (wan is null && localV4 is not null && Net.Addresses.Classify(localV4) == AddressKind.Shared) return NatKind.Shared;
|
|
if (wan is null) return NatKind.Unknown;
|
|
if (Net.Addresses.SameAddress(router.WanAddress, v4)) return NatKind.HomeRouter;
|
|
return Net.Addresses.Classify(wan) switch
|
|
{
|
|
AddressKind.Shared => NatKind.Shared,
|
|
AddressKind.Private => NatKind.DoubleNat,
|
|
AddressKind.Public => NatKind.Translated,
|
|
_ => NatKind.Unknown,
|
|
};
|
|
}
|
|
|
|
public static Finding Behaviour(StunReport stun, NatKind nat, bool ownNetwork)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Behaviour };
|
|
(finding.Outcome, finding.Summary) = stun.Behaviour switch
|
|
{
|
|
_ when ownNetwork => (Outcome.Unknown, "ReachOwnNetwork"),
|
|
NatBehaviour.None => (Outcome.Good, "BehaviourNone"),
|
|
NatBehaviour.Easy => (Outcome.Good, "BehaviourEasy"),
|
|
NatBehaviour.Hard => (Outcome.Caution, "BehaviourHard"),
|
|
_ => (Outcome.Unknown, "BehaviourUnknown"),
|
|
};
|
|
finding.With("FactStunFirst", stun.FirstMapped is null ? null : $"{stun.FirstServer} → {stun.FirstMapped}");
|
|
finding.With("FactStunSecond", stun.SecondMapped is null ? null : $"{stun.SecondServer} → {stun.SecondMapped}");
|
|
finding.With("FactLocalPort", stun.LocalPort > 0 ? stun.LocalPort.ToString(CultureInfo.InvariantCulture) : null);
|
|
var ports = stun.Ports;
|
|
if (ports is null) return finding;
|
|
if (ports.Pattern != PortPattern.Unknown)
|
|
{
|
|
var low = ports.Low.ToString(CultureInfo.InvariantCulture);
|
|
var high = ports.High.ToString(CultureInfo.InvariantCulture);
|
|
finding.WithKey("FactPortSlice", ports.Pattern switch
|
|
{
|
|
PortPattern.Kept => "ValuePortsKept",
|
|
PortPattern.InOrder => "ValuePortsInOrder",
|
|
PortPattern.Block => "ValuePortsBlock",
|
|
_ => "ValuePortsScattered",
|
|
}, low, high);
|
|
if (nat == NatKind.Shared && ports.BlockSize >= 64)
|
|
finding.WithKey("FactSharers", "ValueUpTo", (64512 / ports.BlockSize).ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
if (ports.Hairpin is not null) finding.WithKey("FactHairpinExit", ports.Hairpin == true ? "ValueYes" : "ValueNo");
|
|
if (!ports.LifetimeDone) finding.WithKey("FactMappingLife", "ValueMeasuring");
|
|
else if (ports.KeptAtLeast > 0 && ports.LostBefore > ports.KeptAtLeast)
|
|
finding.WithKey("FactMappingLife", "ValueBetweenSeconds", ports.KeptAtLeast.ToString(CultureInfo.InvariantCulture), ports.LostBefore.ToString(CultureInfo.InvariantCulture));
|
|
else if (ports.KeptAtLeast > 0) finding.WithKey("FactMappingLife", "ValueAtLeastSeconds", ports.KeptAtLeast.ToString(CultureInfo.InvariantCulture));
|
|
else if (ports.LostBefore > 0) finding.WithKey("FactMappingLife", "ValueUnderSeconds", ports.LostBefore.ToString(CultureInfo.InvariantCulture));
|
|
if (ports.LifetimeDone && ports.LostBefore is > 0 and <= 30 && finding.Outcome == Outcome.Good) finding.Outcome = Outcome.Caution;
|
|
return finding;
|
|
}
|
|
|
|
public static Finding Reach(ReachReport reach, RunReport run)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Reach };
|
|
if (reach is null || reach.Choice == ReachChoice.Skip)
|
|
{
|
|
finding.Outcome = Outcome.Unknown;
|
|
finding.Summary = "ReachSkipped";
|
|
return finding;
|
|
}
|
|
var refusal = reach.V4Refusal ?? reach.V6Refusal;
|
|
if (reach.V4 is null && reach.V6 is null)
|
|
{
|
|
finding.Outcome = Outcome.Unknown;
|
|
finding.Summary = refusal switch
|
|
{
|
|
"own-network" => "ReachOwnNetwork",
|
|
"too-many" => "ReachTooMany",
|
|
"no-listener" => "ReachNoListener",
|
|
_ => "ReachUnavailable",
|
|
};
|
|
if (refusal == "too-many") finding.Args = [Math.Max(1, (int)Math.Ceiling(reach.RetryAfterSeconds / 60.0)).ToString(CultureInfo.InvariantCulture)];
|
|
return finding;
|
|
}
|
|
|
|
var reached4 = reach.V4?.Ports.Any(p => p.Outcome == ProbeOutcome.Reached) == true;
|
|
var reached6 = reach.V6?.Ports.Any(p => p.Outcome == ProbeOutcome.Reached) == true;
|
|
var mapped = reach.Mappings.Any(m => m.Done);
|
|
(finding.Outcome, finding.Summary) = (reached4, reached6) switch
|
|
{
|
|
(true, true) => (Outcome.Good, "ReachBoth"),
|
|
(true, false) => (Outcome.Good, "ReachV4"),
|
|
(false, true) => (Outcome.Caution, "ReachV6Only"),
|
|
_ when reach.Choice == ReachChoice.OpenPorts && mapped => (Outcome.Bad, "ReachBlockedAfterMapping"),
|
|
_ when reach.Choice == ReachChoice.OpenPorts => (Outcome.Bad, "ReachNoMapping"),
|
|
_ => (Outcome.Caution, "ReachNothingAsIs"),
|
|
};
|
|
foreach (var attempt in reach.Mappings)
|
|
finding.WithKey("FactMapping", attempt.Done ? "ValueMapped" : "ValueNotMapped", attempt.ExternalPort.ToString(CultureInfo.InvariantCulture), attempt.Via.ToString(), attempt.Error.ToString(CultureInfo.InvariantCulture));
|
|
foreach (var port in reach.V4?.Ports ?? [])
|
|
finding.WithKey("FactReachV4", "Probe" + port.Outcome, port.Port.ToString(CultureInfo.InvariantCulture));
|
|
foreach (var port in reach.V6?.Ports ?? [])
|
|
finding.WithKey("FactReachV6", "Probe" + port.Outcome, port.Port.ToString(CultureInfo.InvariantCulture));
|
|
if (reach.V4 is null && reach.V4Refusal is not null) finding.WithKey("FactReachV4", "Refusal" + Pascal(reach.V4Refusal));
|
|
if (reach.V6 is null && reach.V6Refusal is not null) finding.WithKey("FactReachV6", "Refusal" + Pascal(reach.V6Refusal));
|
|
if (reach.Hairpin is not null) finding.WithKey("FactHairpin", reach.Hairpin == true ? "ValueHairpinYes" : "ValueHairpinNo");
|
|
var route = reach.V4?.Route ?? reach.V6?.Route ?? [];
|
|
if (route.Count > 0) finding.With("FactRouteBack", string.Join(" → ", route.Select(h => h.Name ?? h.Address ?? "*")));
|
|
if (reach.V4?.Ports.Any(p => p.Outcome == ProbeOutcome.OpenNotYou && p.Port is 80 or 443) == true && !mapped)
|
|
finding.WithKey("FactHint", "HintSomethingAnswers");
|
|
return finding;
|
|
}
|
|
|
|
public static Finding Mail(Mail.MailReport mail, bool hasV4)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Mail };
|
|
if (!hasV4)
|
|
{
|
|
finding.Outcome = Outcome.Unknown;
|
|
finding.Summary = "MailNoIpv4";
|
|
return finding;
|
|
}
|
|
var listed = mail.Listings.Where(l => l.State == Core.Mail.ListingState.Listed && !l.Residential && !l.Wide).ToList();
|
|
var wide = mail.Listings.Where(l => l.State == Core.Mail.ListingState.Listed && l.Wide).ToList();
|
|
(finding.Outcome, finding.Summary) = mail switch
|
|
{
|
|
{ Port25: Core.Mail.PortState.Blocked } => (Outcome.Bad, "MailBlocked"),
|
|
_ when listed.Count > 0 => (Outcome.Bad, "MailListed"),
|
|
_ when mail.Listings.Any(l => l.Residential) => (Outcome.Caution, "MailResidential"),
|
|
_ when wide.Count > 0 => (Outcome.Caution, "MailNetworkListed"),
|
|
{ ReverseName: null } => (Outcome.Caution, "MailNoName"),
|
|
{ ReverseLooksDynamic: true } => (Outcome.Caution, "MailDynamicName"),
|
|
{ Port25: Core.Mail.PortState.Open } => (Outcome.Good, "MailClean"),
|
|
_ => (Outcome.Unknown, "MailUnknown"),
|
|
};
|
|
if (listed.Count > 0) finding.Args = [string.Join(", ", listed.Select(l => l.List))];
|
|
else if (wide.Count > 0) finding.Args = [string.Join(", ", wide.Select(l => l.List))];
|
|
finding.WithKey("FactPort25", mail.Port25 switch
|
|
{
|
|
Core.Mail.PortState.Open => "ValuePort25Open",
|
|
Core.Mail.PortState.Blocked => "ValuePort25Blocked",
|
|
_ => "ValueCantTell",
|
|
}, mail.Banner ?? "");
|
|
if (mail.ReverseName is null) finding.WithKey("FactReverseName", "ValueNoReverse");
|
|
else
|
|
{
|
|
finding.With("FactReverseName", mail.ReverseName);
|
|
finding.WithKey("FactReverseLooks", mail.ReverseLooksDynamic ? "ValueLooksHome" : "ValueLooksServer");
|
|
finding.WithKey("FactReverseConfirmed", mail.ReverseConfirmed ? "ValueYes" : "ValueNo");
|
|
}
|
|
foreach (var listing in mail.Listings)
|
|
finding.Facts.Add(new Fact
|
|
{
|
|
Label = "FactList",
|
|
Key = listing.State switch
|
|
{
|
|
Core.Mail.ListingState.Listed when listing.Residential => "ValueListedHome",
|
|
Core.Mail.ListingState.Listed when listing.Wide => "ValueListedNetwork",
|
|
Core.Mail.ListingState.Listed => "ValueListed",
|
|
Core.Mail.ListingState.Clean => "ValueNotListed",
|
|
_ => "ValueCantTellResolver",
|
|
},
|
|
Args = [listing.List, listing.Code ?? ""],
|
|
});
|
|
return finding;
|
|
}
|
|
|
|
public static Finding Dns(DnsReport dns)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Dns };
|
|
(finding.Outcome, finding.Summary) = dns switch
|
|
{
|
|
{ Hijacks: true } => (Outcome.Bad, "DnsHijack"),
|
|
{ Validates: false } => (Outcome.Caution, "DnsNoValidation"),
|
|
{ Validates: true, Hijacks: false } => (Outcome.Good, "DnsGood"),
|
|
_ => (Outcome.Unknown, "DnsUnknown"),
|
|
};
|
|
if (dns.Resolvers.Count > 0) finding.With("FactDns", string.Join(", ", dns.Resolvers.Take(3)));
|
|
if (dns.Validates is not null) finding.WithKey("FactDnssec", dns.Validates == true ? "ValueDnssecYes" : "ValueDnssecNo");
|
|
if (dns.Hijacks is not null) finding.WithKey("FactMissingNames", dns.Hijacks == true ? "ValueHijacked" : "ValueAnsweredMissing");
|
|
if (dns.Ipv6Names is not null) finding.WithKey("FactIpv6Names", dns.Ipv6Names == true ? "ValueYes" : "ValueNo");
|
|
return finding;
|
|
}
|
|
|
|
public static Finding Speed(SpeedReport speed)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Speed };
|
|
if (speed.Limited)
|
|
{
|
|
finding.Outcome = Outcome.Unknown;
|
|
finding.Summary = "SpeedLimited";
|
|
return finding;
|
|
}
|
|
if (speed.DownMbps <= 0)
|
|
{
|
|
finding.Outcome = Outcome.Unknown;
|
|
finding.Summary = "SpeedFailed";
|
|
return finding;
|
|
}
|
|
finding.Outcome = Outcome.Info;
|
|
finding.Summary = "SpeedResult";
|
|
finding.Args = [speed.DownMbps.ToString("0.#", CultureInfo.InvariantCulture), speed.UpMbps.ToString("0.#", CultureInfo.InvariantCulture)];
|
|
finding.WithKey("FactDownload", "ValueMbpsDecimal", speed.DownMbps.ToString("0.#", CultureInfo.InvariantCulture));
|
|
finding.WithKey("FactUpload", "ValueMbpsDecimal", speed.UpMbps.ToString("0.#", CultureInfo.InvariantCulture));
|
|
finding.WithKey("FactSample", "ValueSample", (speed.DownBytes / 1048576).ToString(CultureInfo.InvariantCulture), (speed.UpBytes / 1048576).ToString(CultureInfo.InvariantCulture));
|
|
if (speed.RoundTripMs > 0) finding.WithKey("FactRoundTrip", "ValueMilliseconds", speed.RoundTripMs.ToString(CultureInfo.InvariantCulture));
|
|
return finding;
|
|
}
|
|
|
|
public static Finding Device(DeviceReport device)
|
|
{
|
|
var finding = new Finding { Id = CheckId.Device };
|
|
var small = device.Cores < 2 || device.MemoryMb < 1800 || device.FreeDiskGb is > 0 and < 20;
|
|
(finding.Outcome, finding.Summary) = device switch
|
|
{
|
|
{ Kind: DeviceKind.Phone } => (Outcome.Caution, "DevicePhone"),
|
|
_ when small => (Outcome.Caution, "DeviceSmall"),
|
|
{ OnBattery: true } => (Outcome.Info, "DeviceLaptop"),
|
|
_ => (Outcome.Good, "DeviceGood"),
|
|
};
|
|
finding.With("FactSystem", device.System);
|
|
finding.WithKey("FactCores", "ValueCount", device.Cores.ToString(CultureInfo.InvariantCulture));
|
|
finding.WithKey("FactMemory", "ValueGb", Math.Round(device.MemoryMb / 1024.0, 1).ToString("0.#", CultureInfo.InvariantCulture));
|
|
if (device.FreeDiskGb > 0) finding.WithKey("FactDisk", "ValueGbFree", device.FreeDiskGb.ToString(CultureInfo.InvariantCulture));
|
|
if (device.OnBattery is not null) finding.WithKey("FactBattery", device.OnBattery == true ? "ValueYes" : "ValueNo");
|
|
return finding;
|
|
}
|
|
|
|
public static Finding History(HistorySummary summary)
|
|
{
|
|
var finding = new Finding { Id = CheckId.History, Outcome = Outcome.Info };
|
|
var since = summary.Since.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
|
if (summary.Checks <= 1)
|
|
{
|
|
finding.Summary = "HistoryFirst";
|
|
return finding;
|
|
}
|
|
finding.Summary = summary.Changes == 0 ? "HistoryStable" : "HistoryChanged";
|
|
finding.Args = [summary.Checks.ToString(CultureInfo.InvariantCulture), since, summary.Changes.ToString(CultureInfo.InvariantCulture)];
|
|
finding.WithKey("FactChecks", "ValueCount", summary.Checks.ToString(CultureInfo.InvariantCulture));
|
|
finding.Facts.Add(new Fact { Label = "FactSince", Date = since });
|
|
finding.WithKey("FactAddressesSeen", "ValueCount", summary.AddressesSeen.ToString(CultureInfo.InvariantCulture));
|
|
return finding;
|
|
}
|
|
|
|
static string AddFact(Finding finding, string label, string prefix, ProviderFact fact, ObservedFact seen, string month)
|
|
{
|
|
if (fact?.Value is not null)
|
|
{
|
|
finding.Facts.Add(new Fact
|
|
{
|
|
Label = label,
|
|
Key = prefix + Pascal(fact.Value),
|
|
Amount = fact.Price,
|
|
Currency = fact.Currency,
|
|
Link = fact.Source,
|
|
Basis = fact.Basis is null ? null : "Basis" + Pascal(fact.Basis),
|
|
Date = fact.Date,
|
|
});
|
|
return Pascal(fact.Value);
|
|
}
|
|
if (seen?.Value is null) return null;
|
|
finding.Facts.Add(new Fact { Label = label, Key = prefix + Pascal(seen.Value), Basis = "BasisChecks", Agree = seen.Agree, Of = seen.Of, Date = month });
|
|
return Pascal(seen.Value);
|
|
}
|
|
|
|
static string AnswerKey(Answer answer) => answer switch
|
|
{
|
|
Answer.Answered => "ValueAnswered",
|
|
Answer.Refused => "ValueRefused",
|
|
_ => "ValueSilent",
|
|
};
|
|
|
|
static string Display(AsnEntry entry) => string.IsNullOrWhiteSpace(entry.Name) ? $"AS{entry.Number}" : entry.Name;
|
|
|
|
static string Describe(AsnEntry entry) => $"{Display(entry)} (AS{entry.Number})";
|
|
|
|
static void Block(Finding finding, string label, RegistryBlock block)
|
|
{
|
|
if (block is null) return;
|
|
if (!string.IsNullOrEmpty(block.Name))
|
|
{
|
|
finding.With(label, string.IsNullOrEmpty(block.Country) ? block.Name : $"{block.Name} ({block.Country})");
|
|
return;
|
|
}
|
|
var key = block.Status switch
|
|
{
|
|
BlockStatus.Allocated => "ValueBlockAllocated",
|
|
BlockStatus.Assigned => "ValueBlockAssigned",
|
|
BlockStatus.Reallocated => "ValueBlockReallocated",
|
|
BlockStatus.Reassigned => "ValueBlockReassigned",
|
|
_ => null,
|
|
};
|
|
if (key is not null) finding.WithKey(label, key, block.Country ?? "");
|
|
}
|
|
|
|
static string Join(string make, string label)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(make)) return label;
|
|
if (string.IsNullOrWhiteSpace(label) || label.Contains(make.Split(' ')[0], StringComparison.OrdinalIgnoreCase)) return label ?? make;
|
|
return $"{make} {label}";
|
|
}
|
|
|
|
static string Pascal(string value) => string.Concat(value.Split('-').Select(p => p.Length == 0 ? p : char.ToUpperInvariant(p[0]) + p[1..]));
|
|
}
|