mirror of
https://github.com/Horizontal42/netsleuth.git
synced 2026-11-11 01:02:10 +03:00
Merge f32c53738d into 8c99ac78b7
This commit is contained in:
commit
27731eacd9
3 changed files with 62 additions and 22 deletions
|
|
@ -78,7 +78,7 @@ class Speedtest(BaseModel):
|
|||
upload_sizes_bytes: list[int] = Field(default_factory=lambda: [1_000_000, 5_000_000])
|
||||
cloudflare_base_url: str = "https://speed.cloudflare.com"
|
||||
fastcom_api_url: str = "https://api.fast.com/netflix/speedtest/v2"
|
||||
fastcom_api_token: str = "YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm"
|
||||
fastcom_api_token: SecretStr | None = None
|
||||
ndt7_locate_url: str = "https://locate.measurementlab.net/v2/nearest/ndt/ndt7"
|
||||
bufferbloat_probe_interval_seconds: float = 0.2
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ def parse_server_timing_cfl4(header: str) -> CfL4Stats | None:
|
|||
match = _CFL4_RE.search(header or "")
|
||||
if not match:
|
||||
return None
|
||||
fields = {k: v[0] for k, v in parse_qs(match.group("query"), keep_blank_values=True).items()}
|
||||
fields = {
|
||||
k: v[0]
|
||||
for k, v in parse_qs(match.group("query"), keep_blank_values=True).items()
|
||||
}
|
||||
return CfL4Stats(
|
||||
rtt_ms=_us_to_ms(fields.get("rtt")),
|
||||
min_rtt_ms=_us_to_ms(fields.get("min_rtt")),
|
||||
|
|
@ -51,7 +54,9 @@ def parse_server_timing_cfl4(header: str) -> CfL4Stats | None:
|
|||
)
|
||||
|
||||
|
||||
def bufferbloat_delta(idle_rtt_ms: float | None, loaded_rtts_ms: list[float]) -> float | None:
|
||||
def bufferbloat_delta(
|
||||
idle_rtt_ms: float | None, loaded_rtts_ms: list[float]
|
||||
) -> float | None:
|
||||
if idle_rtt_ms is None or not loaded_rtts_ms:
|
||||
return None
|
||||
loaded = percentile(sorted(loaded_rtts_ms), 95.0)
|
||||
|
|
@ -93,10 +98,17 @@ async def run_speed_cascade(
|
|||
duration_ms = int((time.perf_counter() - began) * 1000)
|
||||
if not result.download_mbps:
|
||||
attempts.append(
|
||||
TierAttempt(tier=name, ok=False, reason="no throughput measured", duration_ms=duration_ms)
|
||||
TierAttempt(
|
||||
tier=name,
|
||||
ok=False,
|
||||
reason="no throughput measured",
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
)
|
||||
continue
|
||||
attempts.append(TierAttempt(tier=name, ok=True, reason=None, duration_ms=duration_ms))
|
||||
attempts.append(
|
||||
TierAttempt(tier=name, ok=True, reason=None, duration_ms=duration_ms)
|
||||
)
|
||||
result.tier_attempts = attempts
|
||||
return result
|
||||
return SpeedResult(method="none", tier_attempts=attempts)
|
||||
|
|
@ -138,15 +150,21 @@ async def tier_ookla(
|
|||
server_info = payload.get("server") or {}
|
||||
return SpeedResult(
|
||||
method="ookla_bin",
|
||||
download_mbps=round(mbps(download.get("bytes", 0), (download.get("elapsed", 0) or 0) / 1000), 3),
|
||||
upload_mbps=round(mbps(upload.get("bytes", 0), (upload.get("elapsed", 0) or 0) / 1000), 3),
|
||||
download_mbps=round(
|
||||
mbps(download.get("bytes", 0), (download.get("elapsed", 0) or 0) / 1000), 3
|
||||
),
|
||||
upload_mbps=round(
|
||||
mbps(upload.get("bytes", 0), (upload.get("elapsed", 0) or 0) / 1000), 3
|
||||
),
|
||||
server=f"{server_info.get('name', '')} ({server_info.get('location', '')})".strip(),
|
||||
idle_rtt_ms=(payload.get("ping") or {}).get("latency"),
|
||||
server_country=server_info.get("country") or None,
|
||||
)
|
||||
|
||||
|
||||
async def tier_cloudflare(client: httpx.AsyncClient, cfg: Speedtest, timeout: float) -> SpeedResult:
|
||||
async def tier_cloudflare(
|
||||
client: httpx.AsyncClient, cfg: Speedtest, timeout: float
|
||||
) -> SpeedResult:
|
||||
down_samples: list[tuple[int, float]] = []
|
||||
cfl4: CfL4Stats | None = None
|
||||
for size in cfg.download_sizes_bytes:
|
||||
|
|
@ -156,12 +174,16 @@ async def tier_cloudflare(client: httpx.AsyncClient, cfg: Speedtest, timeout: fl
|
|||
)
|
||||
response.raise_for_status()
|
||||
down_samples.append((len(response.content), time.perf_counter() - began))
|
||||
cfl4 = parse_server_timing_cfl4(response.headers.get("server-timing", "")) or cfl4
|
||||
cfl4 = (
|
||||
parse_server_timing_cfl4(response.headers.get("server-timing", "")) or cfl4
|
||||
)
|
||||
up_samples: list[tuple[int, float]] = []
|
||||
for size in cfg.upload_sizes_bytes:
|
||||
payload = b"\x00" * size
|
||||
began = time.perf_counter()
|
||||
response = await client.post(f"{cfg.cloudflare_base_url}/__up", content=payload, timeout=timeout)
|
||||
response = await client.post(
|
||||
f"{cfg.cloudflare_base_url}/__up", content=payload, timeout=timeout
|
||||
)
|
||||
response.raise_for_status()
|
||||
up_samples.append((size, time.perf_counter() - began))
|
||||
return SpeedResult(
|
||||
|
|
@ -173,10 +195,13 @@ async def tier_cloudflare(client: httpx.AsyncClient, cfg: Speedtest, timeout: fl
|
|||
)
|
||||
|
||||
|
||||
async def tier_fastcom(client: httpx.AsyncClient, cfg: Speedtest, timeout: float) -> SpeedResult:
|
||||
async def tier_fastcom(
|
||||
client: httpx.AsyncClient, cfg: Speedtest, timeout: float
|
||||
) -> SpeedResult:
|
||||
token = cfg.fastcom_api_token.get_secret_value() if cfg.fastcom_api_token else ""
|
||||
response = await client.get(
|
||||
cfg.fastcom_api_url,
|
||||
params={"https": "true", "token": cfg.fastcom_api_token, "urlCount": "3"},
|
||||
params={"https": "true", "token": token, "urlCount": "3"},
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
|
@ -197,14 +222,21 @@ async def tier_fastcom(client: httpx.AsyncClient, cfg: Speedtest, timeout: float
|
|||
method="fastcom",
|
||||
download_mbps=throughput_from_samples(samples),
|
||||
upload_mbps=None,
|
||||
server=targets[0].get("url").split("/")[2] if targets and targets[0].get("url") else None,
|
||||
server=targets[0].get("url").split("/")[2]
|
||||
if targets and targets[0].get("url")
|
||||
else None,
|
||||
netflix_oca_onnet=on_net,
|
||||
server_country=(targets[0].get("location") or {}).get("country") if targets else None,
|
||||
server_country=(targets[0].get("location") or {}).get("country")
|
||||
if targets
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
async def tier_ndt7(
|
||||
client: httpx.AsyncClient, cfg: Speedtest, timeout: float, source_ip: str | None = None
|
||||
client: httpx.AsyncClient,
|
||||
cfg: Speedtest,
|
||||
timeout: float,
|
||||
source_ip: str | None = None,
|
||||
) -> SpeedResult:
|
||||
import websockets
|
||||
|
||||
|
|
@ -225,7 +257,11 @@ async def tier_ndt7(
|
|||
message = await asyncio.wait_for(socket.recv(), timeout=timeout)
|
||||
except (TimeoutError, Exception):
|
||||
break
|
||||
total += len(message) if isinstance(message, (bytes, bytearray)) else len(message.encode())
|
||||
total += (
|
||||
len(message)
|
||||
if isinstance(message, (bytes, bytearray))
|
||||
else len(message.encode())
|
||||
)
|
||||
elapsed = time.perf_counter() - began
|
||||
return SpeedResult(
|
||||
method="ndt7",
|
||||
|
|
@ -276,7 +312,11 @@ async def measure_with_bufferbloat(
|
|||
result.bufferbloat_down_ms = bufferbloat_delta(idle_rtt_ms, down_samples)
|
||||
result.bufferbloat_up_ms = bufferbloat_delta(idle_rtt_ms, up_samples)
|
||||
worst_delta = max(
|
||||
[d for d in (result.bufferbloat_down_ms, result.bufferbloat_up_ms) if d is not None],
|
||||
[
|
||||
d
|
||||
for d in (result.bufferbloat_down_ms, result.bufferbloat_up_ms)
|
||||
if d is not None
|
||||
],
|
||||
default=None,
|
||||
)
|
||||
result.bufferbloat_grade = grade_bufferbloat(worst_delta, bands)
|
||||
|
|
|
|||
|
|
@ -463,7 +463,7 @@ async def test_tier_fastcom_happy_path(httpx_mock):
|
|||
config = Speedtest()
|
||||
api_url = config.fastcom_api_url
|
||||
httpx_mock.add_response(
|
||||
url=f"{api_url}?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=3",
|
||||
url=f"{api_url}?https=true&token=&urlCount=3",
|
||||
json={
|
||||
"targets": [
|
||||
{
|
||||
|
|
@ -498,7 +498,7 @@ async def test_tier_fastcom_no_targets(httpx_mock):
|
|||
config = Speedtest()
|
||||
api_url = config.fastcom_api_url
|
||||
httpx_mock.add_response(
|
||||
url=f"{api_url}?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=3",
|
||||
url=f"{api_url}?https=true&token=&urlCount=3",
|
||||
json={"targets": []},
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
|
@ -513,7 +513,7 @@ async def test_tier_fastcom_missing_urls(httpx_mock):
|
|||
config = Speedtest()
|
||||
api_url = config.fastcom_api_url
|
||||
httpx_mock.add_response(
|
||||
url=f"{api_url}?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=3",
|
||||
url=f"{api_url}?https=true&token=&urlCount=3",
|
||||
json={
|
||||
"targets": [
|
||||
{"location": {"country": "US"}},
|
||||
|
|
@ -540,7 +540,7 @@ async def test_tier_fastcom_api_error(httpx_mock):
|
|||
config = Speedtest()
|
||||
api_url = config.fastcom_api_url
|
||||
httpx_mock.add_response(
|
||||
url=f"{api_url}?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=3",
|
||||
url=f"{api_url}?https=true&token=&urlCount=3",
|
||||
status_code=500,
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
|
@ -553,7 +553,7 @@ async def test_tier_fastcom_download_error(httpx_mock):
|
|||
config = Speedtest()
|
||||
api_url = config.fastcom_api_url
|
||||
httpx_mock.add_response(
|
||||
url=f"{api_url}?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=3",
|
||||
url=f"{api_url}?https=true&token=&urlCount=3",
|
||||
json={
|
||||
"targets": [
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue