- 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>
87 lines
4.2 KiB
C#
87 lines
4.2 KiB
C#
#:project ../src/MeWeb.Core/MeWeb.Core.csproj
|
|
#:project ../src/MeWeb.Shared/MeWeb.Shared.csproj
|
|
using System.Net.Http.Json;
|
|
using System.Security.Cryptography;
|
|
|
|
using MeWeb.Core.Knowledge;
|
|
using MeWeb.Shared;
|
|
|
|
const string Fallback = "https://iptoasn.com/data/ip2asn-combined.tsv.gz";
|
|
var root = FindRoot();
|
|
var output = Path.Combine(root, "artifacts", "knowledge");
|
|
var cache = Path.Combine(root, "tools", "cache");
|
|
Directory.CreateDirectory(output);
|
|
Directory.CreateDirectory(cache);
|
|
var server = Environment.GetEnvironmentVariable("MEWEB_KNOWLEDGE_URL") ?? Endpoints.Base;
|
|
|
|
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
|
|
http.DefaultRequestHeaders.UserAgent.ParseAdd("me-web-knowledge/2 (+https://git.thepra.dev/Arasaka/MeWeb)");
|
|
|
|
if (!args.Contains("--local"))
|
|
{
|
|
try
|
|
{
|
|
var manifest = await http.GetFromJsonAsync(server + Endpoints.Knowledge.GET_Manifest, MeWebJsonContext.Default.ViewKnowledge);
|
|
if (manifest is { Files.Count: > 0 })
|
|
{
|
|
foreach (var file in manifest.Files)
|
|
{
|
|
var bytes = await http.GetByteArrayAsync(server + Endpoints.Knowledge.GET_M_File(file.Name));
|
|
var hash = Convert.ToHexStringLower(SHA256.HashData(bytes));
|
|
if (hash != file.Sha256) throw new InvalidDataException($"{file.Name}: sha256 {hash}, the manifest says {file.Sha256}");
|
|
File.WriteAllBytes(Path.Combine(output, file.Name), bytes);
|
|
Console.WriteLine($"knowledge: {file.Name} {bytes.Length / 1024} KiB from {server}");
|
|
}
|
|
File.Move(Path.Combine(output, "SOURCES.txt"), Path.Combine(output, "SOURCE.txt"), overwrite: true);
|
|
Report();
|
|
return 0;
|
|
}
|
|
}
|
|
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or InvalidDataException or System.Text.Json.JsonException)
|
|
{
|
|
Console.Error.WriteLine($"knowledge: the server's files could not be used ({ex.Message}); building the provider map alone");
|
|
}
|
|
}
|
|
|
|
var download = Path.Combine(cache, "ip2asn-combined.tsv.gz");
|
|
var sourceDate = File.Exists(download) ? new DateTimeOffset(File.GetLastWriteTimeUtc(download), TimeSpan.Zero) : DateTimeOffset.UtcNow;
|
|
if (!File.Exists(download) || File.GetLastWriteTimeUtc(download) < DateTime.UtcNow.AddHours(-12) || args.Contains("--refresh"))
|
|
{
|
|
using var response = await http.GetAsync(Fallback, 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);
|
|
}
|
|
using (var input = File.OpenRead(download))
|
|
using (var file = File.Create(Path.Combine(output, "ip2asn.bin")))
|
|
AsnTable.Write(file, sourceDate, Sources.IpToAsn(input));
|
|
foreach (var stale in new[] { "networks.bin", "carriers.json", "registry.bin" }) File.Delete(Path.Combine(output, stale));
|
|
string sha;
|
|
using (var input = File.OpenRead(download)) sha = Convert.ToHexStringLower(SHA256.HashData(input));
|
|
File.WriteAllText(Path.Combine(output, "SOURCE.txt"), $"iptoasn.com, Public Domain Dedication and License (PDDL) 1.0\n{Fallback}\nlast-modified {sourceDate:O}\nsha256 {sha}\n");
|
|
Report();
|
|
return 0;
|
|
|
|
void Report()
|
|
{
|
|
using var stream = File.OpenRead(Path.Combine(output, "ip2asn.bin"));
|
|
var table = AsnTable.Read(stream);
|
|
var path = Path.Combine(output, "networks.bin");
|
|
NetworkTable networks = null;
|
|
if (File.Exists(path))
|
|
using (var input = File.OpenRead(path)) networks = NetworkTable.Read(input);
|
|
path = Path.Combine(output, "registry.bin");
|
|
using var registry = File.Exists(path) ? RegistryTable.Open(File.OpenRead(path)) : null;
|
|
Console.WriteLine($"knowledge: {table.Count} networks in the provider map (source {table.SourceDate:yyyy-MM-dd}), {networks?.Count ?? 0} with names or upstreams, {registry?.Count ?? 0} address blocks");
|
|
}
|
|
|
|
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)");
|
|
}
|