The checks engine, the path and the cards: me-web? 0.1 reads the connection

MeWeb.Core runs seven checks concurrently and reports each as it settles:
online or behind a login page, this connection, the public IPv4 and IPv6
seen from v4./v6.meweb, the provider from a built-in map, the router
(UPnP IGD, NAT-PMP and PCP, read-only), address sharing (router WAN
against the echo, TTL hops only as a hint), and the history kept on the
device. The app draws the path from this device to the internet and lights
it node by node, with a card per check and a sheet listing every host it
contacts.

The provider map is iptoasn's table compressed to 2.1 MB by
tools/knowledge.cs and built before every app release; eleven Italian
provider profiles carry each fact with who said it, the source and the
month. The server gains a download page (en, it) and /download/{platform}
from the latest Gitea release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-15 04:39:03 +02:00
1 parent 16a1cecaae
commit 0dffbc27d6
61 files changed
+3750 -149

No files matched your search

+28 -15
View File
@@ -8,6 +8,7 @@ using MeWeb.App;
using MeWeb.App.Services;
using MeWeb.App.ViewModels;
using MeWeb.App.Views;
using MeWeb.Core.Checks;
using Microsoft.Extensions.DependencyInjection;
@@ -16,31 +17,43 @@ var codes = args.Length > 1 ? args[1].Split(',', StringSplitOptions.RemoveEmptyE
Directory.CreateDirectory(output);
var session = HeadlessUnitTestSession.StartNew(typeof(ScreenshotEntry));
await session.Dispatch(() =>
await session.Dispatch(async () =>
{
foreach (var (width, height, label) in new[] { (1100, 760, "desktop"), (400, 820, "phone") })
var run = await App.Services.GetRequiredService<CheckRunner>().Run((_, _) => { }, CancellationToken.None);
Console.WriteLine($"run: {string.Join(", ", run.Findings.Select(f => $"{f.Id}={f.Outcome}"))}");
foreach (var (width, height, label) in new[] { (1100, 1500, "desktop"), (400, 2600, "phone") })
{
foreach (var code in codes)
{
Localizer.Instance.SetLanguage(Languages.Find(code) ?? Languages.Fallback);
var model = App.Services.GetRequiredService<MainViewModel>();
var window = new Window { Width = width, Height = height, Content = new MainView { DataContext = model } };
window.Show();
Dispatcher.UIThread.RunJobs();
var frame = window.CaptureRenderedFrame();
var path = Path.Combine(output, $"main-{label}-{code}.png");
if (frame is not null)
{
using var file = File.Create(path);
frame.Save(file, new PngBitmapEncoderOptions());
}
Console.WriteLine($"{path} {(frame is null ? "no frame" : "ok")}");
window.Close();
Shoot(width, height, $"main-{label}-{code}", model => model.Replay(run));
if (label == "desktop") Shoot(width, 900, $"contacts-{code}", model => model.ShowContacts = true);
}
}
return 0;
}, CancellationToken.None);
Environment.Exit(0);
void Shoot(int width, int height, string name, Action<MainViewModel> prepare)
{
var model = App.Services.GetRequiredService<MainViewModel>();
var view = new MainView { DataContext = model };
var window = new Window { Width = width, Height = height, Content = view };
window.Show();
model.AvailableWidth = width;
prepare(model);
for (var i = 0; i < 6; i++) Dispatcher.UIThread.RunJobs();
var frame = window.CaptureRenderedFrame();
var path = Path.Combine(output, $"{name}.png");
if (frame is not null)
{
using var file = File.Create(path);
frame.Save(file, new PngBitmapEncoderOptions());
}
Console.WriteLine($"{path} {(frame is null ? "no frame" : "ok")}");
window.Close();
}
public static class ScreenshotEntry
{
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<MeWeb.App.App>()
+62
View File
@@ -0,0 +1,62 @@
#:project ../src/MeWeb.Core/MeWeb.Core.csproj
using System.IO.Compression;
using System.Net;
using System.Security.Cryptography;
using MeWeb.Core.Knowledge;
const string Source = "https://iptoasn.com/data/ip2asn-combined.tsv.gz";
var root = FindRoot();
var cache = Path.Combine(root, "tools", "cache");
var output = Path.Combine(root, "artifacts", "knowledge", "ip2asn.bin");
Directory.CreateDirectory(cache);
Directory.CreateDirectory(Path.GetDirectoryName(output));
var download = Path.Combine(cache, "ip2asn-combined.tsv.gz");
using var http = new HttpClient();
http.DefaultRequestHeaders.UserAgent.ParseAdd("me-web-knowledge/1 (+https://git.thepra.dev/Arasaka/MeWeb)");
var fresh = !File.Exists(download) || File.GetLastWriteTimeUtc(download) < DateTime.UtcNow.AddHours(-12) || args.Contains("--refresh");
var sourceDate = File.Exists(download) ? new DateTimeOffset(File.GetLastWriteTimeUtc(download), TimeSpan.Zero) : DateTimeOffset.UtcNow;
if (fresh)
{
using var response = await http.GetAsync(Source, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
sourceDate = response.Content.Headers.LastModified ?? DateTimeOffset.UtcNow;
await using (var file = File.Create(download + ".tmp"))
await response.Content.CopyToAsync(file);
File.Move(download + ".tmp", download, overwrite: true);
File.SetLastWriteTimeUtc(download, sourceDate.UtcDateTime);
}
var hash = Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(download)));
var ranges = new List<AsnRange>();
using (var reader = new StreamReader(new GZipStream(File.OpenRead(download), CompressionMode.Decompress)))
{
string line;
while ((line = reader.ReadLine()) is not null)
{
var parts = line.Split('\t');
if (parts.Length < 5 || !uint.TryParse(parts[2], out var number) || number == 0) continue;
if (!IPAddress.TryParse(parts[0], out var start) || !IPAddress.TryParse(parts[1], out var end)) continue;
ranges.Add(new AsnRange { Start = start, End = end, Number = number, Country = parts[3] == "None" ? "" : parts[3], Name = parts[4] });
}
}
await using (var file = File.Create(output + ".tmp"))
AsnTable.Write(file, sourceDate, ranges);
File.Move(output + ".tmp", output, overwrite: true);
using var check = File.OpenRead(output);
var table = AsnTable.Read(check);
var probe = table.Find(IPAddress.Parse("1.1.1.1"));
Console.WriteLine($"knowledge: {ranges.Count} routed ranges, {table.Count} networks, source {sourceDate:yyyy-MM-dd HH:mm} UTC, sha256 {hash}");
Console.WriteLine($"knowledge: {output} {new FileInfo(output).Length / 1024} KiB; 1.1.1.1 -> AS{probe?.Number} {probe?.Name}");
File.WriteAllText(Path.Combine(root, "artifacts", "knowledge", "SOURCE.txt"), $"{Source}\nlast-modified {sourceDate:O}\nsha256 {hash}\n");
return 0;
static string FindRoot()
{
var dir = new DirectoryInfo(Directory.GetCurrentDirectory());
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "MeWeb.slnx"))) dir = dir.Parent;
return dir?.FullName ?? throw new InvalidOperationException("run from inside the repository (MeWeb.slnx not found)");
}
+22
View File
@@ -39,6 +39,11 @@ Directory.CreateDirectory(output);
Console.WriteLine($"me-web? {version} ({commit}): {string.Join(", ", targets)} -> {output}");
var failed = false;
if (targets.Any(t => t != "server"))
{
Console.WriteLine("== knowledge");
RunTool("knowledge.cs");
}
foreach (var target in targets)
{
try
@@ -61,8 +66,25 @@ foreach (var target in targets)
}
}
WriteSums();
WriteNotes();
return failed ? 1 : 0;
void WriteNotes()
{
var source = Path.Combine(root, "artifacts", "knowledge", "SOURCE.txt");
var lines = new List<string>
{
$"me-web? {version}, commit {commit}, built {builtAt}.",
"",
"Built with the provider map from iptoasn.com (PDDL 1.0):",
"",
};
lines.AddRange(File.Exists(source) ? File.ReadAllLines(source).Where(l => l.Length > 0).Select(l => $" {l}") : [" (no provider map in this build)"]);
lines.AddRange(["", "SHA256SUMS.txt lists every file. The APK's signing certificate SHA-256 is 5f52cff4f52f4e0b6da6cd044bfceaf78d1d42db24f2470602db7f19cbfdf846."]);
Directory.CreateDirectory(Path.Combine(root, "artifacts"));
File.WriteAllLines(Path.Combine(root, "artifacts", "release-notes.md"), lines);
}
string[] Stamp() => [$"-p:Version={version}", $"-p:BuildCommit={commit}", $"-p:BuildTimeUtc={builtAt}"];
string Linux()
+58 -1
View File
@@ -10,7 +10,11 @@ var placeholder = new Regex(@"\{\d+\}");
if (args.Length >= 1 && args[0] == "check") return Check();
if (args.Length == 3 && args[0] == "set") return Set(args[1], args[2]);
Console.Error.WriteLine("usage: dotnet run tools/strings.cs -- check | set <Key> <values.json> (values.json maps a language code to its text)");
if (args.Length == 2 && args[0] == "merge") return Merge(args[1]);
if (args.Length == 3 && args[0] == "import") return Import(args[1], args[2]);
if (args.Length == 2 && args[0] == "drop") return Drop(args[1]);
Console.Error.WriteLine("usage: dotnet run tools/strings.cs -- check | set <Key> <values.json> | merge <keys.json> | import <code> <strings.json> | drop <Key>");
Console.Error.WriteLine(" values.json maps a language code to its text; keys.json maps keys to such maps; strings.json maps keys to one language's text");
return 2;
int Check()
@@ -48,6 +52,59 @@ int Set(string key, string valuesPath)
return 0;
}
int Merge(string path)
{
var keys = JsonNode.Parse(File.ReadAllText(path)).AsObject();
var byLanguage = new Dictionary<string, SortedDictionary<string, string>>();
foreach (var (key, values) in keys)
{
if (values?["en"] is null) throw new InvalidOperationException($"{key} carries no \"en\"");
foreach (var (code, value) in values.AsObject())
{
if (!byLanguage.TryGetValue(code, out var strings)) byLanguage[code] = strings = Read(code);
strings[key] = (string)value;
}
}
foreach (var (code, strings) in byLanguage) Write(code, strings);
Console.WriteLine($"merge: {keys.Count} key(s) into {byLanguage.Count} language(s)");
return 0;
}
int Import(string code, string path)
{
var english = Read("en");
var incoming = JsonNode.Parse(File.ReadAllText(path)).AsObject();
var strings = Read(code);
var unknown = new List<string>();
foreach (var (key, value) in incoming)
{
if (!english.ContainsKey(key))
{
unknown.Add(key);
continue;
}
strings[key] = (string)value;
}
Write(code, strings);
Console.WriteLine($"import {code}: {incoming.Count - unknown.Count} key(s){(unknown.Count > 0 ? $", ignored unknown [{string.Join(", ", unknown)}]" : "")}");
return unknown.Count == 0 ? 0 : 1;
}
int Drop(string key)
{
var count = 0;
foreach (var file in Directory.GetFiles(folder, "*.json"))
{
var code = Path.GetFileNameWithoutExtension(file);
var strings = Read(code);
if (!strings.Remove(key)) continue;
Write(code, strings);
count++;
}
Console.WriteLine($"{key}: dropped from {count} language(s)");
return 0;
}
SortedDictionary<string, string> Read(string code)
{
var path = Path.Combine(folder, $"{code}.json");