Files
MeWeb/src/MeWeb.Core/Net/SpeedCheck.cs
T
thepraandClaude Opus 5.5 eb8be87284
Build / Desktop, server and tools (push) Successful in 59s
Release / Every platform, the server, the release (push) Successful in 8m42s
me-web? 0.5: the shared exit measured, the route from the server, the network's upstreams
- Behind your provider's shared address, me-web? now shows how its exit hands out ports
  (kept, one after another, from one block, or at random), the range it gave this device,
  and how many customers may share the address at most; the estimate sharpens with each
  check on the same address.
- It tests whether two devices behind the same address can reach each other (hairpin), and
  how long an unused connection stays open: the card says "measuring" for about two minutes
  after the check, then the answer.
- With the test from outside, the publisher's server also traces the route back to your
  address: your provider's routers and the networks in between appear on the path, with
  their names.
- The provider card says which networks your provider reaches the internet through.
- On mobile data, the path shows the operator and the type of network (5G, 4G) read from the
  phone; on Android 13 and later the phone asks once for permission to read the network type.
- The speed card adds the round trip to the publisher's server.
- The network data now comes from the publisher's server, refreshed there from open sources:
  iptoasn.com, DB-IP Lite (CC BY 4.0), the University of Oregon Route Views Project (CC BY 4.0)
  and the Android Open Source Project's list of mobile operators (Apache 2.0); the release
  notes list each source's date and checksum.
- Source links in the cards wrap instead of being cut off.
- New strings, drafted in every language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-15 11:11:24 +02:00

99 lines
3.4 KiB
C#

using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using MeWeb.Shared;
namespace MeWeb.Core.Net;
public class SpeedReport
{
public double DownMbps { get; set; }
public double UpMbps { get; set; }
public int DownBytes { get; set; }
public int UpBytes { get; set; }
public bool Limited { get; set; }
public int RoundTripMs { get; set; }
}
public static class SpeedCheck
{
public static async Task<SpeedReport> Run(HttpClient https, LinkKind kind, CancellationToken ct)
{
var mobile = kind == LinkKind.Mobile;
var report = new SpeedReport { DownBytes = mobile ? 2 * 1024 * 1024 : 12 * 1024 * 1024, UpBytes = mobile ? 1024 * 1024 : 4 * 1024 * 1024 };
report.RoundTripMs = await RoundTrip(ct);
try
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(TimeSpan.FromSeconds(30));
var clock = Stopwatch.StartNew();
using (var response = await https.GetAsync(Endpoints.Base + Endpoints.Speed.GET_M_Down(report.DownBytes), HttpCompletionOption.ResponseHeadersRead, timeout.Token))
{
if ((int)response.StatusCode == 429)
{
report.Limited = true;
return report;
}
response.EnsureSuccessStatusCode();
await using var body = await response.Content.ReadAsStreamAsync(timeout.Token);
var buffer = new byte[64 * 1024];
var warm = report.DownBytes / 4;
long total = 0;
long warmBytes = 0;
var warmAt = TimeSpan.Zero;
int read;
while ((read = await body.ReadAsync(buffer, timeout.Token)) > 0)
{
total += read;
if (warmBytes > 0 || total < warm) continue;
warmBytes = total;
warmAt = clock.Elapsed;
}
var steady = clock.Elapsed - warmAt;
report.DownMbps = warmBytes > 0 && total > warmBytes && steady > TimeSpan.FromMilliseconds(50)
? Mbps(total - warmBytes, steady)
: Mbps(total, clock.Elapsed);
}
var payload = new byte[report.UpBytes];
Random.Shared.NextBytes(payload);
clock.Restart();
using (var response = await https.PostAsync(Endpoints.Base + Endpoints.Speed.POST_Up, new ByteArrayContent(payload), timeout.Token))
{
if (response.IsSuccessStatusCode) report.UpMbps = Mbps(payload.Length, clock.Elapsed);
}
}
catch (Exception) when (!ct.IsCancellationRequested)
{
}
return report;
}
static async Task<int> RoundTrip(CancellationToken ct)
{
try
{
var addresses = await Dns.GetHostAddressesAsync(Endpoints.Host, ct);
if (addresses.Length == 0) return 0;
var samples = new List<double>();
for (var i = 0; i < 3; i++)
{
using var client = new TcpClient(addresses[0].AddressFamily);
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(TimeSpan.FromSeconds(3));
var clock = Stopwatch.StartNew();
await client.ConnectAsync(addresses[0], 443, timeout.Token);
samples.Add(clock.Elapsed.TotalMilliseconds);
}
return (int)Math.Round(samples.Order().ElementAt(samples.Count / 2));
}
catch (Exception) when (!ct.IsCancellationRequested)
{
return 0;
}
}
static double Mbps(long bytes, TimeSpan elapsed) => elapsed.TotalSeconds <= 0 ? 0 : Math.Round(bytes * 8 / elapsed.TotalSeconds / 1_000_000, 1);
}