]> git.99rst.org Git - openwrt-packages.git/commitdiff
librespeed-cli: update to 1.0.14
authorJosef Schlehofer <redacted>
Mon, 17 Aug 2026 07:12:36 +0000 (09:12 +0200)
committerJosef Schlehofer <redacted>
Mon, 17 Aug 2026 09:03:03 +0000 (11:03 +0200)
All twelve backported patches are part of this release, so dropping them.
Release notes: https://github.com/librespeed/speedtest-cli/releases/tag/v1.0.14

Signed-off-by: Josef Schlehofer <redacted>
13 files changed:
utils/librespeed-cli/Makefile
utils/librespeed-cli/patches/0001-style-gofmt-helper.go-and-speedtest.go.patch [deleted file]
utils/librespeed-cli/patches/0002-fix-fix-data-race-goroutine-leak-non-TTY-output-and-.patch [deleted file]
utils/librespeed-cli/patches/0003-fix-align-upload-cancellation-handling-and-add-tests.patch [deleted file]
utils/librespeed-cli/patches/0004-feat-report-test-progress-under-debug.patch [deleted file]
utils/librespeed-cli/patches/0005-docs-describe-what-secure-and-insecure-actually-do.patch [deleted file]
utils/librespeed-cli/patches/0006-fix-terminate-json-and-csv-output-with-a-newline.patch [deleted file]
utils/librespeed-cli/patches/0007-fix-drop-the-empty-copyright-line-from-version.patch [deleted file]
utils/librespeed-cli/patches/0008-feat-report-the-negotiated-TLS-version-and-cipher-un.patch [deleted file]
utils/librespeed-cli/patches/0009-feat-report-which-address-each-ping-test-used-under-.patch [deleted file]
utils/librespeed-cli/patches/0010-fix-keep-sub-millisecond-precision-in-ping-and-jitte.patch [deleted file]
utils/librespeed-cli/patches/0011-feat-report-the-ping-distribution-under-debug.patch [deleted file]
utils/librespeed-cli/patches/0012-feat-name-the-platform-in-the-User-Agent.patch [deleted file]

index ff1be9c668c6ddfa5d103d8a7c09e341d3b8108f..ec0cae409837b31ce5d7db8b4fd88fb3cc8c20e5 100644 (file)
@@ -8,12 +8,12 @@
 include $(TOPDIR)/rules.mk
 
 PKG_NAME:=librespeed-cli
-PKG_VERSION:=1.0.13
+PKG_VERSION:=1.0.14
 PKG_RELEASE:=1
 
 PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION).tar.gz
 PKG_SOURCE_URL:=https://codeload.github.com/librespeed/speedtest-cli/tar.gz/v${PKG_VERSION}?
-PKG_HASH:=5ad938b61e3edc0ca95e2ccff0c06e97a69383f3cbb0243bd47b21b9865f9f55
+PKG_HASH:=3031e0f7babd7f9c51a1c49b95026d12532668455e8cc459049160f1bd525bf3
 
 PKG_MAINTAINER:=Michal Vasilek <michal.vasilek@nic.cz>
 PKG_LICENSE:=LGPL-3.0-only
diff --git a/utils/librespeed-cli/patches/0001-style-gofmt-helper.go-and-speedtest.go.patch b/utils/librespeed-cli/patches/0001-style-gofmt-helper.go-and-speedtest.go.patch
deleted file mode 100644 (file)
index 3301a24..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Date: Fri, 7 Aug 2026 09:42:39 +0200
-Subject: [PATCH] style: gofmt helper.go and speedtest.go
-
-Import order, --simple block indentation and const alignment. No behaviour
-change; split out to keep the next commit's diff readable.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- speedtest/helper.go    | 16 ++++++++--------
- speedtest/speedtest.go |  4 ++--
- 2 files changed, 10 insertions(+), 10 deletions(-)
-
---- a/speedtest/helper.go
-+++ b/speedtest/helper.go
-@@ -16,8 +16,8 @@ import (
-       "github.com/briandowns/spinner"
-       "github.com/gocarina/gocsv"
-       "github.com/librespeed/speedtest-cli/defs"
--      "github.com/librespeed/speedtest-cli/report"
-       "github.com/librespeed/speedtest-cli/output"
-+      "github.com/librespeed/speedtest-cli/report"
-       "github.com/urfave/cli/v2"
- )
-@@ -113,14 +113,14 @@ func doSpeedTest(c *cli.Context, servers
-                       }
-                       // print result if --simple is given
--              if c.Bool(defs.OptionSimple) {
--                      if c.Bool(defs.OptionBytes) {
--                              useMebi := c.Bool(defs.OptionMebiBytes)
--                              output.WriteOut("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%s\nUpload rate:\t%s\n", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi))
--                      } else {
--                              output.WriteOut("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps\n", p, jitter, downloadValue, uploadValue)
-+                      if c.Bool(defs.OptionSimple) {
-+                              if c.Bool(defs.OptionBytes) {
-+                                      useMebi := c.Bool(defs.OptionMebiBytes)
-+                                      output.WriteOut("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%s\nUpload rate:\t%s\n", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi))
-+                              } else {
-+                                      output.WriteOut("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps\n", p, jitter, downloadValue, uploadValue)
-+                              }
-                       }
--              }
-                       // print share link if --share is given
-                       var shareLink string
---- a/speedtest/speedtest.go
-+++ b/speedtest/speedtest.go
-@@ -33,8 +33,8 @@ const (
-       defaultTelemetryShare  = "/results/"
-       forceNothing = 0
--      forceHttps = 1
--      forceHttp = 2
-+      forceHttps   = 1
-+      forceHttp    = 2
- )
- type PingJob struct {
diff --git a/utils/librespeed-cli/patches/0002-fix-fix-data-race-goroutine-leak-non-TTY-output-and-.patch b/utils/librespeed-cli/patches/0002-fix-fix-data-race-goroutine-leak-non-TTY-output-and-.patch
deleted file mode 100644 (file)
index 23ca77a..0000000
+++ /dev/null
@@ -1,526 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Date: Fri, 7 Aug 2026 09:42:39 +0200
-Subject: [PATCH] fix: fix data race, goroutine leak, non-TTY output and escape
- injection
-
-- BytesCounter.total mixed atomic and plain access; now an atomic.Uint64.
-- Download/Upload leaked in-flight goroutines past "break Loop"; the done
-  signal is now ctx-aware and both wait on a WaitGroup before reading the
-  counter.
-- Results rode on spinner.FinalMSG, which never prints without a TTY, so
-  piping produced no output at all. Printed via output.WriteUI now, and the
-  spinner moved off stdout onto stderr.
-- Server-supplied strings were printed raw, allowing ANSI escapes and a
-  forged --list entry. output.Sanitize strips C0/DEL/C1 at every display
-  site; url.Parse only rejects ASCII controls, so C1 still needs stripping.
-- BytesCounter.Read and its reader/pos/lock fields became unreachable when
-  Upload started building a body reader per request; dropped.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- defs/bytes_counter.go  |  47 ++++-------------
- defs/server.go         | 112 +++++++++++++++++++++++++++++------------
- output/output.go       |  24 +++++++++
- speedtest/helper.go    |  15 +++---
- speedtest/speedtest.go |  12 +++--
- 5 files changed, 129 insertions(+), 81 deletions(-)
-
---- a/defs/bytes_counter.go
-+++ b/defs/bytes_counter.go
-@@ -1,55 +1,34 @@
- package defs
- import (
--      "bytes"
-       "fmt"
-       "io"
-       "math/rand/v2"
--      "sync"
-       "sync/atomic"
-       "time"
- )
--// BytesCounter implements io.Reader and io.Writer interface, for counting bytes being read/written in HTTP requests
-+// BytesCounter implements the io.Writer interface, for counting bytes being read/written in HTTP requests.
-+// It is only ever plugged into an io.TeeReader, so the transfer itself is driven by the wrapped reader.
- type BytesCounter struct {
-       start      time.Time
--      pos        int
--      total      uint64
-+      total      atomic.Uint64
-       payload    []byte
--      reader     io.ReadSeeker
-       mebi       bool
-       uploadSize int
--
--      lock *sync.Mutex
- }
- func NewCounter() *BytesCounter {
--      return &BytesCounter{
--              lock: &sync.Mutex{},
--      }
-+      return &BytesCounter{}
- }
- // Write implements io.Writer
- func (c *BytesCounter) Write(p []byte) (int, error) {
-       n := len(p)
--      atomic.AddUint64(&c.total, uint64(n))
-+      c.total.Add(uint64(n))
-       return n, nil
- }
--// Read implements io.Reader
--func (c *BytesCounter) Read(p []byte) (int, error) {
--      c.lock.Lock()
--      n, err := c.reader.Read(p)
--      c.total += uint64(n)
--      c.pos += n
--      if c.pos == c.uploadSize {
--              c.resetReader()
--      }
--      c.lock.Unlock()
--
--      return n, err
--}
--
- // SetBase sets the base for dividing bytes into megabyte or mebibyte
- func (c *BytesCounter) SetMebi(mebi bool) {
-       c.mebi = mebi
-@@ -62,7 +41,7 @@ func (c *BytesCounter) SetUploadSize(upl
- // AvgBytes returns the average bytes/second
- func (c *BytesCounter) AvgBytes() float64 {
--      return float64(c.total) / time.Since(c.start).Seconds()
-+      return float64(c.total.Load()) / time.Since(c.start).Seconds()
- }
- // AvgMbps returns the average mbits/second
-@@ -99,17 +78,9 @@ func (c *BytesCounter) Payload() []byte
-       return c.payload
- }
--// GenerateBlob generates a random byte array of `uploadSize` in the `payload` field, and sets the `reader` field to
--// read from it
-+// GenerateBlob generates a random byte array of `uploadSize` in the `payload` field
- func (c *BytesCounter) GenerateBlob() {
-       c.payload = getRandomData(c.uploadSize)
--      c.reader = bytes.NewReader(c.payload)
--}
--
--// resetReader resets the `reader` field to 0 position
--func (c *BytesCounter) resetReader() (int64, error) {
--      c.pos = 0
--      return c.reader.Seek(0, 0)
- }
- // Start will set the `start` field to current time
-@@ -119,12 +90,12 @@ func (c *BytesCounter) Start() {
- // Total returns the total bytes read/written
- func (c *BytesCounter) Total() uint64 {
--      return atomic.LoadUint64(&c.total)
-+      return c.total.Load()
- }
- // CurrentSpeed returns the current bytes/second
- func (c *BytesCounter) CurrentSpeed() float64 {
--      return float64(c.total) / time.Since(c.start).Seconds()
-+      return float64(c.total.Load()) / time.Since(c.start).Seconds()
- }
- // SeekWrapper is a wrapper around io.Reader to give it a noop io.Seeker interface
---- a/defs/server.go
-+++ b/defs/server.go
-@@ -11,8 +11,10 @@ import (
-       "math"
-       "net/http"
-       "net/url"
-+      "os"
-       "path"
-       "strconv"
-+      "sync"
-       "time"
-       "github.com/briandowns/spinner"
-@@ -61,7 +63,7 @@ func (s *Server) IsUp() bool {
-       defer resp.Body.Close()
-       b, err := io.ReadAll(resp.Body)
-       if err != nil || len(b) > 0 {
--              output.WriteDebug("Failed when parsing get IP result: %s\n", b)
-+              output.WriteDebug("Failed when parsing get IP result: %s\n", output.Sanitize(string(b)))
-               return false
-       }
-       // only return online if the ping URL returns nothing and 200
-@@ -76,7 +78,7 @@ func (s *Server) ICMPPingAndJitter(count
-       }()
-       if s.NoICMP {
--              output.WriteDebug("Skipping ICMP for server %s, will use HTTP ping\n", s.Name)
-+              output.WriteDebug("Skipping ICMP for server %s, will use HTTP ping\n", output.Sanitize(s.Name))
-               return s.PingAndJitter(count + 2)
-       }
-@@ -126,7 +128,7 @@ func (s *Server) ICMPPingAndJitter(count
-       if len(stats.Rtts) == 0 {
-               s.NoICMP = true
--              output.WriteDebug("No ICMP pings returned for server %s (%s), trying TCP ping\n", s.Name, u.Hostname())
-+              output.WriteDebug("No ICMP pings returned for server %s (%s), trying TCP ping\n", output.Sanitize(s.Name), output.Sanitize(u.Hostname()))
-               return s.PingAndJitter(count + 2)
-       }
-@@ -226,29 +228,43 @@ func (s *Server) Download(silent bool, u
-       downloadDone := make(chan struct{}, requests)
-+      var wg sync.WaitGroup
-+
-       doDownload := func() {
-+              defer wg.Done()
-+
-               reqClone := req.Clone(ctx)
-               resp, err := http.DefaultClient.Do(reqClone)
-               if err != nil {
-                       if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
-                               output.WriteDebug("Failed when making HTTP request: %s\n", err)
-                       }
--              } else {
--                      defer resp.Body.Close()
-+                      return
-+              }
-+              defer resp.Body.Close()
--                      if _, err = io.Copy(io.Discard, io.TeeReader(resp.Body, counter)); err != nil {
--                              if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
--                                      output.WriteDebug("Failed when reading HTTP response: %s\n", err)
--                              }
-+              if _, err = io.Copy(io.Discard, io.TeeReader(resp.Body, counter)); err != nil {
-+                      if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
-+                              output.WriteDebug("Failed when reading HTTP response: %s\n", err)
-                       }
-+              }
--                      downloadDone <- struct{}{}
-+              // let the main loop start a replacement request, but never block on it
-+              // once the test is over, otherwise this goroutine is leaked
-+              select {
-+              case downloadDone <- struct{}{}:
-+              case <-ctx.Done():
-               }
-       }
-+      spawnDownload := func() {
-+              wg.Add(1)
-+              go doDownload()
-+      }
-+
-       counter.Start()
-       if !silent {
--              pb := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
-+              pb := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriterFile(os.Stderr))
-               pb.Prefix = "Downloading...  "
-               pb.PostUpdate = func(s *spinner.Spinner) {
-                       if useBytes {
-@@ -259,18 +275,21 @@ func (s *Server) Download(silent bool, u
-               }
-               pb.Start()
-+              // print the rate ourselves instead of via pb.FinalMSG: the spinner only
-+              // prints it when it was actually running, which it isn't when stderr is
-+              // not a terminal
-               defer func() {
-+                      pb.Stop()
-                       if useBytes {
--                              pb.FinalMSG = fmt.Sprintf("Download rate:\t%s\n", counter.AvgHumanize())
-+                              output.WriteUI("Download rate:\t%s\n", counter.AvgHumanize())
-                       } else {
--                              pb.FinalMSG = fmt.Sprintf("Download rate:\t%.2f Mbps\n", counter.AvgMbps())
-+                              output.WriteUI("Download rate:\t%.2f Mbps\n", counter.AvgMbps())
-                       }
--                      pb.Stop()
-               }()
-       }
-       for i := 0; i < requests; i++ {
--              go doDownload()
-+              spawnDownload()
-               time.Sleep(200 * time.Millisecond)
-       }
-       timeout := time.After(duration)
-@@ -281,10 +300,14 @@ Loop:
-                       cancel()
-                       break Loop
-               case <-downloadDone:
--                      go doDownload()
-+                      spawnDownload()
-               }
-       }
-+      // let the cancelled requests unwind before reading the counter, so the
-+      // result doesn't change under us while it's being reported
-+      wg.Wait()
-+
-       return counter.AvgMbps(), counter.Total(), nil
- }
-@@ -301,8 +324,9 @@ func (s *Server) Upload(noPrealloc, sile
-       if noPrealloc {
-               output.WriteUI("Pre-allocation is disabled, performance might be lower!\n")
--              counter.reader = &SeekWrapper{rand.Reader}
-       } else {
-+              // each request reads from this shared payload; without it they stream
-+              // straight from crypto/rand instead
-               counter.GenerateBlob()
-       }
-@@ -318,7 +342,11 @@ func (s *Server) Upload(noPrealloc, sile
-       uploadDone := make(chan struct{}, requests)
-+      var wg sync.WaitGroup
-+
-       doUpload := func() {
-+              defer wg.Done()
-+
-               var bodyReader io.Reader
-               if noPrealloc {
-                       bodyReader = &SeekWrapper{rand.Reader}
-@@ -336,21 +364,34 @@ func (s *Server) Upload(noPrealloc, sile
-               uploadReq.Header.Set("Accept-Encoding", "identity")
-               resp, err := http.DefaultClient.Do(uploadReq)
--              if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
--                      output.WriteDebug("Failed when making HTTP request: %s\n", err)
--              } else if err == nil {
--                      defer resp.Body.Close()
--                      if _, err := io.Copy(io.Discard, resp.Body); err != nil {
--                              output.WriteDebug("Failed when reading HTTP response: %s\n", err)
-+              if err != nil {
-+                      if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
-+                              output.WriteDebug("Failed when making HTTP request: %s\n", err)
-                       }
-+                      return
-+              }
-+              defer resp.Body.Close()
-+
-+              if _, err := io.Copy(io.Discard, resp.Body); err != nil {
-+                      output.WriteDebug("Failed when reading HTTP response: %s\n", err)
-+              }
--                      uploadDone <- struct{}{}
-+              // let the main loop start a replacement request, but never block on it
-+              // once the test is over, otherwise this goroutine is leaked
-+              select {
-+              case uploadDone <- struct{}{}:
-+              case <-ctx.Done():
-               }
-       }
-+      spawnUpload := func() {
-+              wg.Add(1)
-+              go doUpload()
-+      }
-+
-       counter.Start()
-       if !silent {
--              pb := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
-+              pb := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriterFile(os.Stderr))
-               pb.Prefix = "Uploading...  "
-               pb.PostUpdate = func(s *spinner.Spinner) {
-                       if useBytes {
-@@ -361,18 +402,21 @@ func (s *Server) Upload(noPrealloc, sile
-               }
-               pb.Start()
-+              // print the rate ourselves instead of via pb.FinalMSG: the spinner only
-+              // prints it when it was actually running, which it isn't when stderr is
-+              // not a terminal
-               defer func() {
-+                      pb.Stop()
-                       if useBytes {
--                              pb.FinalMSG = fmt.Sprintf("Upload rate:\t%s\n", counter.AvgHumanize())
-+                              output.WriteUI("Upload rate:\t%s\n", counter.AvgHumanize())
-                       } else {
--                              pb.FinalMSG = fmt.Sprintf("Upload rate:\t%.2f Mbps\n", counter.AvgMbps())
-+                              output.WriteUI("Upload rate:\t%.2f Mbps\n", counter.AvgMbps())
-                       }
--                      pb.Stop()
-               }()
-       }
-       for i := 0; i < requests; i++ {
--              go doUpload()
-+              spawnUpload()
-               time.Sleep(200 * time.Millisecond)
-       }
-       timeout := time.After(duration)
-@@ -383,10 +427,14 @@ Loop:
-                       cancel()
-                       break Loop
-               case <-uploadDone:
--                      go doUpload()
-+                      spawnUpload()
-               }
-       }
-+      // let the cancelled requests unwind before reading the counter, so the
-+      // result doesn't change under us while it's being reported
-+      wg.Wait()
-+
-       return counter.AvgMbps(), counter.Total(), nil
- }
-@@ -432,7 +480,7 @@ func (s *Server) GetIPInfo(distanceUnit
-       if len(b) > 0 {
-               if err := json.Unmarshal(b, &ipInfo); err != nil {
-                       output.WriteDebug("Failed when parsing get IP result: %s\n", err)
--                      output.WriteDebug("Received payload: %s\n", b)
-+                      output.WriteDebug("Received payload: %s\n", output.Sanitize(string(b)))
-                       // try to extract processedString even if full parse fails
-                       // (e.g. when rawIspInfo is "" instead of an object)
-                       var partial struct {
-@@ -473,7 +521,7 @@ func (s *Server) Sponsor() string {
-               if s.SponsorURL != "" {
-                       su, err := url.Parse(s.SponsorURL)
-                       if err != nil {
--                              output.WriteDebug("Sponsor URL is invalid: %s\n", s.SponsorURL)
-+                              output.WriteDebug("Sponsor URL is invalid: %s\n", output.Sanitize(s.SponsorURL))
-                       } else {
-                               if su.Scheme == "" {
-                                       su.Scheme = "https"
---- a/output/output.go
-+++ b/output/output.go
-@@ -11,6 +11,7 @@ import (
-       "fmt"
-       "io"
-       "os"
-+      "strings"
- )
- // Default is the package-level output writer used by all package functions.
-@@ -127,3 +128,26 @@ func (w *Writer) Fatalf(format string, a
-       fmt.Fprintln(w.ui)
-       os.Exit(1)
- }
-+
-+// --- Sanitize: make server-supplied strings safe to display ---
-+
-+// Sanitize strips control characters from a string so it can be printed
-+// without letting a remote party drive the terminal.
-+//
-+// Server names, sponsor strings and the getIP response all come off the wire
-+// (over plain HTTP for schemeless servers), so they are attacker-influenced.
-+// Left raw, an embedded ESC sequence can rewrite earlier lines, hide text or
-+// recolour the output, and an embedded newline can forge an extra entry in
-+// --list output that a script would then parse as real.
-+//
-+// Dropped: C0 controls (including ESC, CR, LF and TAB), DEL, and C1 controls
-+// (0x80-0x9F, where 0x9B doubles as CSI on some terminals). Printable Unicode
-+// is left alone.
-+func Sanitize(s string) string {
-+      return strings.Map(func(r rune) rune {
-+              if r < 0x20 || r == 0x7f || (r >= 0x80 && r <= 0x9f) {
-+                      return -1
-+              }
-+              return r
-+      }, s)
-+}
---- a/speedtest/helper.go
-+++ b/speedtest/helper.go
-@@ -46,10 +46,10 @@ func doSpeedTest(c *cli.Context, servers
-                       return err
-               }
--              output.WriteUI("Selected server: %s [%s]\n", currentServer.Name, u.Hostname())
-+              output.WriteUI("Selected server: %s [%s]\n", output.Sanitize(currentServer.Name), output.Sanitize(u.Hostname()))
-               if sponsorMsg := currentServer.Sponsor(); sponsorMsg != "" {
--                      output.WriteUI("Sponsored by: %s\n", sponsorMsg)
-+                      output.WriteUI("Sponsored by: %s\n", output.Sanitize(sponsorMsg))
-               }
-               if currentServer.IsUp() {
-@@ -58,12 +58,12 @@ func doSpeedTest(c *cli.Context, servers
-                               output.WriteError("Failed to get IP info: %s\n", err)
-                               return err
-                       }
--                      output.WriteUI("You're testing from: %s\n", ispInfo.ProcessedString)
-+                      output.WriteUI("You're testing from: %s\n", output.Sanitize(ispInfo.ProcessedString))
-                       // get ping and jitter value
-                       var pb *spinner.Spinner
-                       if !silent {
--                              pb = spinner.New(spinner.CharSets[11], 100*time.Millisecond)
-+                              pb = spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriterFile(os.Stderr))
-                               pb.Prefix = "Pinging server...  "
-                               pb.Start()
-                       }
-@@ -78,8 +78,11 @@ func doSpeedTest(c *cli.Context, servers
-                       }
-                       if pb != nil {
--                              pb.FinalMSG = fmt.Sprintf("Ping: %.2f ms\tJitter: %.2f ms\n", p, jitter)
-+                              // print the result ourselves instead of via pb.FinalMSG: the
-+                              // spinner only prints it when it was actually running, which it
-+                              // isn't when stderr is not a terminal
-                               pb.Stop()
-+                              output.WriteUI("Ping: %.2f ms\tJitter: %.2f ms\n", p, jitter)
-                       }
-                       // get download value
-@@ -182,7 +185,7 @@ func doSpeedTest(c *cli.Context, servers
-                               reps_json = append(reps_json, rep)
-                       }
-               } else {
--                      output.WriteUI("Selected server %s (%s) is not responding at the moment, try again later\n", currentServer.Name, u.Hostname())
-+                      output.WriteUI("Selected server %s (%s) is not responding at the moment, try again later\n", output.Sanitize(currentServer.Name), output.Sanitize(u.Hostname()))
-               }
-               //add a new line after each test if testing multiple servers
---- a/speedtest/speedtest.go
-+++ b/speedtest/speedtest.go
-@@ -283,9 +283,11 @@ func SpeedTest(c *cli.Context) error {
-               for _, svr := range servers {
-                       var sponsorMsg string
-                       if svr.Sponsor() != "" {
--                              sponsorMsg = fmt.Sprintf(" [Sponsor: %s]", svr.Sponsor())
-+                              sponsorMsg = fmt.Sprintf(" [Sponsor: %s]", output.Sanitize(svr.Sponsor()))
-                       }
--                      output.WriteOut("%d: %s (%s) %s\n", svr.ID, svr.Name, svr.Server, sponsorMsg)
-+                      // --list goes to stdout, so a newline smuggled into a server name
-+                      // would forge an entry for anything parsing it
-+                      output.WriteOut("%d: %s (%s) %s\n", svr.ID, output.Sanitize(svr.Name), output.Sanitize(svr.Server), sponsorMsg)
-               }
-               return nil
-       }
-@@ -360,7 +362,7 @@ func pingWorker(jobs <-chan PingJob, res
-               // get the URL of the speed test server from the JSON
-               u, err := server.GetURL()
-               if err != nil {
--                      output.WriteDebug("Server URL is invalid for %s (%s), skipping\n", server.Name, server.Server)
-+                      output.WriteDebug("Server URL is invalid for %s (%s), skipping\n", output.Sanitize(server.Name), output.Sanitize(server.Server))
-                       wg.Done()
-                       continue
-               }
-@@ -373,7 +375,7 @@ func pingWorker(jobs <-chan PingJob, res
-                       // if server is up, get ping
-                       ping, _, err := server.ICMPPingAndJitter(1, srcIp, network)
-                       if err != nil {
--                              output.WriteDebug("Can't ping server %s (%s), skipping\n", server.Name, u.Hostname())
-+                              output.WriteDebug("Can't ping server %s (%s), skipping\n", output.Sanitize(server.Name), output.Sanitize(u.Hostname()))
-                               wg.Done()
-                               continue
-                       }
-@@ -381,7 +383,7 @@ func pingWorker(jobs <-chan PingJob, res
-                       results <- PingResult{Index: job.Index, Ping: ping}
-                       wg.Done()
-               } else {
--                      output.WriteDebug("Server %s (%s) doesn't seem to be up, skipping\n", server.Name, u.Hostname())
-+                      output.WriteDebug("Server %s (%s) doesn't seem to be up, skipping\n", output.Sanitize(server.Name), output.Sanitize(u.Hostname()))
-                       wg.Done()
-               }
-       }
diff --git a/utils/librespeed-cli/patches/0003-fix-align-upload-cancellation-handling-and-add-tests.patch b/utils/librespeed-cli/patches/0003-fix-align-upload-cancellation-handling-and-add-tests.patch
deleted file mode 100644 (file)
index 46a86ee..0000000
+++ /dev/null
@@ -1,321 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Date: Fri, 7 Aug 2026 09:50:07 +0200
-Subject: [PATCH] fix: align upload cancellation handling and add tests
-
-- Upload's response-body copy now filters context.Canceled/DeadlineExceeded
-  the way the download path already did, so ending the test no longer logs a
-  spurious debug failure.
-- The two raw-payload debug dumps use %q instead of Sanitize: newlines are
-  legitimate in a response body, and quoting escapes them without losing them.
-- TestSanitize covers C0, DEL, C1 and the printable boundaries, exhaustively
-  rather than by example.
-- TestBytesCounterConcurrentAccess reproduces the spinner-vs-transfer access
-  pattern. Verified: it reports DATA RACE against the pre-fix counter and
-  passes against the fixed one.
-- Download and Upload are exercised against a server that never ends a
-  request, so a hanging wg.Wait() fails the test rather than a release.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- defs/bytes_counter_test.go |  74 +++++++++++++++++++++++
- defs/server.go             |  12 +++-
- defs/server_test.go        | 120 +++++++++++++++++++++++++++++++++++++
- output/output_test.go      |  54 +++++++++++++++++
- 4 files changed, 257 insertions(+), 3 deletions(-)
- create mode 100644 defs/bytes_counter_test.go
- create mode 100644 defs/server_test.go
- create mode 100644 output/output_test.go
-
---- /dev/null
-+++ b/defs/bytes_counter_test.go
-@@ -0,0 +1,74 @@
-+package defs
-+
-+import (
-+      "sync"
-+      "testing"
-+)
-+
-+// TestBytesCounterConcurrentAccess exercises the pattern the counter is used
-+// in: several transfer goroutines writing while the progress spinner polls the
-+// averages. It is the regression test for the mixed atomic/non-atomic access
-+// that used to make this racy, so it is only meaningful under -race.
-+func TestBytesCounterConcurrentAccess(t *testing.T) {
-+      const (
-+              writers    = 8
-+              writes     = 2000
-+              chunkSize  = 16
-+              pollers    = 4
-+              wantTotal  = uint64(writers * writes * chunkSize)
-+              pollBudget = 1 << 20
-+      )
-+
-+      c := NewCounter()
-+      c.Start()
-+
-+      stop := make(chan struct{})
-+
-+      var polling sync.WaitGroup
-+      for i := 0; i < pollers; i++ {
-+              polling.Add(1)
-+              go func() {
-+                      defer polling.Done()
-+                      for n := 0; n < pollBudget; n++ {
-+                              select {
-+                              case <-stop:
-+                                      return
-+                              default:
-+                              }
-+                              _ = c.AvgBytes()
-+                              _ = c.AvgMbps()
-+                              _ = c.AvgHumanize()
-+                              _ = c.CurrentSpeed()
-+                              _ = c.Total()
-+                      }
-+              }()
-+      }
-+
-+      var writing sync.WaitGroup
-+      for i := 0; i < writers; i++ {
-+              writing.Add(1)
-+              go func() {
-+                      defer writing.Done()
-+                      chunk := make([]byte, chunkSize)
-+                      for j := 0; j < writes; j++ {
-+                              n, err := c.Write(chunk)
-+                              if err != nil {
-+                                      t.Errorf("Write returned error: %v", err)
-+                                      return
-+                              }
-+                              if n != chunkSize {
-+                                      t.Errorf("Write returned %d, want %d", n, chunkSize)
-+                                      return
-+                              }
-+                      }
-+              }()
-+      }
-+
-+      writing.Wait()
-+      close(stop)
-+      polling.Wait()
-+
-+      if got := c.Total(); got != wantTotal {
-+              t.Errorf("Total() = %d, want %d (lost updates indicate a broken counter)", got, wantTotal)
-+      }
-+}
---- a/defs/server.go
-+++ b/defs/server.go
-@@ -63,7 +63,9 @@ func (s *Server) IsUp() bool {
-       defer resp.Body.Close()
-       b, err := io.ReadAll(resp.Body)
-       if err != nil || len(b) > 0 {
--              output.WriteDebug("Failed when parsing get IP result: %s\n", output.Sanitize(string(b)))
-+              // %q rather than Sanitize: this is a raw response body where newlines are
-+              // legitimate, and quoting escapes control chars without losing them
-+              output.WriteDebug("Failed when parsing get IP result: %q\n", b)
-               return false
-       }
-       // only return online if the ping URL returns nothing and 200
-@@ -373,7 +375,10 @@ func (s *Server) Upload(noPrealloc, sile
-               defer resp.Body.Close()
-               if _, err := io.Copy(io.Discard, resp.Body); err != nil {
--                      output.WriteDebug("Failed when reading HTTP response: %s\n", err)
-+                      // cancellation is how the test ends, so it is not a failure
-+                      if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
-+                              output.WriteDebug("Failed when reading HTTP response: %s\n", err)
-+                      }
-               }
-               // let the main loop start a replacement request, but never block on it
-@@ -480,7 +485,8 @@ func (s *Server) GetIPInfo(distanceUnit
-       if len(b) > 0 {
-               if err := json.Unmarshal(b, &ipInfo); err != nil {
-                       output.WriteDebug("Failed when parsing get IP result: %s\n", err)
--                      output.WriteDebug("Received payload: %s\n", output.Sanitize(string(b)))
-+                      // %q rather than Sanitize: see IsUp
-+                      output.WriteDebug("Received payload: %q\n", b)
-                       // try to extract processedString even if full parse fails
-                       // (e.g. when rawIspInfo is "" instead of an object)
-                       var partial struct {
---- /dev/null
-+++ b/defs/server_test.go
-@@ -0,0 +1,120 @@
-+package defs
-+
-+import (
-+      "io"
-+      "net/http"
-+      "net/http/httptest"
-+      "testing"
-+      "time"
-+)
-+
-+// The transfer tests below cover the timing semantics introduced by wg.Wait():
-+// Download and Upload now return only once every in-flight request has
-+// unwound. Against a server that never ends a request on its own, the only
-+// thing that can end them is the context cancellation, so these fail (by
-+// timing out) if wg.Wait() can hang.
-+
-+const (
-+      testRequests = 2
-+      // long enough that the requests are still in flight when the test ends
-+      testDuration = 500 * time.Millisecond
-+      // The spawn loop sleeps 200ms per request before the duration timer even
-+      // starts, so a healthy run is ~900ms; measured unwind after cancel() is
-+      // 10-40ms, with or without -race. The rest is slack for a loaded runner --
-+      // kept tight so a hanging wg.Wait() fails fast instead of stalling CI.
-+      returnBudget = testRequests*200*time.Millisecond + testDuration + 2*time.Second
-+)
-+
-+// hangingDownloadServer streams forever until the client goes away.
-+func hangingDownloadServer(t *testing.T) *httptest.Server {
-+      t.Helper()
-+      return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-+              chunk := make([]byte, 32*1024)
-+              for {
-+                      select {
-+                      case <-r.Context().Done():
-+                              return
-+                      default:
-+                      }
-+                      if _, err := w.Write(chunk); err != nil {
-+                              return
-+                      }
-+                      if f, ok := w.(http.Flusher); ok {
-+                              f.Flush()
-+                      }
-+              }
-+      }))
-+}
-+
-+// hangingUploadServer drains the body, then holds the request open.
-+func hangingUploadServer(t *testing.T) *httptest.Server {
-+      t.Helper()
-+      return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-+              io.Copy(io.Discard, r.Body)
-+              <-r.Context().Done()
-+      }))
-+}
-+
-+func runWithinBudget(t *testing.T, name string, fn func() error) {
-+      t.Helper()
-+
-+      done := make(chan error, 1)
-+      start := time.Now()
-+      go func() { done <- fn() }()
-+
-+      select {
-+      case err := <-done:
-+              if err != nil {
-+                      t.Fatalf("%s returned error: %v", name, err)
-+              }
-+              if elapsed := time.Since(start); elapsed > returnBudget {
-+                      t.Errorf("%s took %s, budget was %s", name, elapsed, returnBudget)
-+              }
-+      case <-time.After(returnBudget):
-+              t.Fatalf("%s did not return within %s: wg.Wait() is hanging on cancelled requests", name, returnBudget)
-+      }
-+}
-+
-+func TestDownloadReturnsWhenServerNeverEndsTheResponse(t *testing.T) {
-+      ts := hangingDownloadServer(t)
-+      defer ts.Close()
-+
-+      s := &Server{Server: ts.URL, DownloadURL: "/"}
-+
-+      var total uint64
-+      runWithinBudget(t, "Download", func() error {
-+              _, n, err := s.Download(true, false, false, testRequests, 100, testDuration)
-+              total = n
-+              return err
-+      })
-+
-+      if total == 0 {
-+              t.Error("Download reported 0 bytes, expected the counter to have seen traffic")
-+      }
-+}
-+
-+func TestUploadReturnsWhenServerNeverResponds(t *testing.T) {
-+      ts := hangingUploadServer(t)
-+      defer ts.Close()
-+
-+      s := &Server{Server: ts.URL, UploadURL: "/"}
-+
-+      runWithinBudget(t, "Upload", func() error {
-+              _, _, err := s.Upload(false, true, false, false, testRequests, 32, testDuration)
-+              return err
-+      })
-+}
-+
-+// The no-prealloc path streams from crypto/rand, so the request body never
-+// ends on its own either; only cancellation can stop it.
-+func TestUploadNoPreallocReturnsWhenServerNeverResponds(t *testing.T) {
-+      ts := hangingUploadServer(t)
-+      defer ts.Close()
-+
-+      s := &Server{Server: ts.URL, UploadURL: "/"}
-+
-+      runWithinBudget(t, "Upload(noPrealloc)", func() error {
-+              _, _, err := s.Upload(true, true, false, false, testRequests, 32, testDuration)
-+              return err
-+      })
-+}
---- /dev/null
-+++ b/output/output_test.go
-@@ -0,0 +1,54 @@
-+package output
-+
-+import "testing"
-+
-+func TestSanitize(t *testing.T) {
-+      tests := []struct {
-+              name string
-+              in   string
-+              want string
-+      }{
-+              {"empty", "", ""},
-+              {"plain text is untouched", "normal text", "normal text"},
-+              {"newline is dropped", "hello\nworld", "helloworld"},
-+              {"carriage return is dropped", "hello\rworld", "helloworld"},
-+              {"tab is dropped", "hello\tworld", "helloworld"},
-+              {"nul is dropped", "hello\x00world", "helloworld"},
-+              {"ANSI colour sequence loses its ESC", "\x1b[31mred\x1b[0m", "[31mred[0m"},
-+              {"OSC window title sequence loses ESC and BEL", "\x1b]0;pwned\x07", "]0;pwned"},
-+              {"DEL is dropped", "foo\x7fbar", "foobar"},
-+              {"C1 CSI is dropped", "foo\u009b31mbar", "foo31mbar"},
-+              {"C1 lower bound U+0080 is dropped", "foo\u0080bar", "foobar"},
-+              {"C1 upper bound U+009F is dropped", "foo\u009fbar", "foobar"},
-+              {"U+00A0 just past C1 is kept", "foo\u00a0bar", "foo\u00a0bar"},
-+              {"space just past C0 is kept", "foo bar", "foo bar"},
-+              {"non-ASCII text is kept", "ÄŒeský server \U0001f1e8\U0001f1ff", "ÄŒeský server \U0001f1e8\U0001f1ff"},
-+              {"forged --list entry is collapsed onto one line", "Server A\n999: Fake", "Server A999: Fake"},
-+      }
-+
-+      for _, tt := range tests {
-+              t.Run(tt.name, func(t *testing.T) {
-+                      if got := Sanitize(tt.in); got != tt.want {
-+                              t.Errorf("Sanitize(%q) = %q, want %q", tt.in, got, tt.want)
-+                      }
-+              })
-+      }
-+}
-+
-+// TestSanitizeDropsEveryControlRune asserts the boundaries exhaustively rather
-+// than by example, so a future rewrite cannot silently let one class through.
-+func TestSanitizeDropsEveryControlRune(t *testing.T) {
-+      for r := rune(0); r <= 0x9f; r++ {
-+              if r >= 0x20 && r <= 0x7e {
-+                      continue // printable ASCII
-+              }
-+              if got := Sanitize(string(r)); got != "" {
-+                      t.Errorf("Sanitize(%U) = %q, want it to be dropped", r, got)
-+              }
-+      }
-+      for _, r := range []rune{0x20, 0x7e, 0xa0, 0xa1, 0x2028, 0x1f600} {
-+              if got := Sanitize(string(r)); got != string(r) {
-+                      t.Errorf("Sanitize(%U) = %q, want it kept", r, got)
-+              }
-+      }
-+}
diff --git a/utils/librespeed-cli/patches/0004-feat-report-test-progress-under-debug.patch b/utils/librespeed-cli/patches/0004-feat-report-test-progress-under-debug.patch
deleted file mode 100644 (file)
index 559a442..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe@bloodkings.eu>
-Date: Sat, 8 Aug 2026 00:54:35 +0200
-Subject: [PATCH] feat: report test progress under --debug
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
---json, --csv and --simple suppress the spinner, so a run prints nothing
-between launch and the final result. --debug did not help: every existing
-debug call sits on an error path, so a run that succeeds stays quiet.
-
-Report each phase instead â€” server, IP info, ping, download, upload â€” with
-the timings, byte counts and stream settings the spinner cannot carry.
-
-Rates follow the run's own reporting mode, Mbps normally and MB/s under
---bytes, so a debug line cannot contradict the result printed beside it.
-
-Nothing changes unless --debug is given.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- speedtest/helper.go | 34 ++++++++++++++++++++++++++++++++++
- 1 file changed, 34 insertions(+)
-
---- a/speedtest/helper.go
-+++ b/speedtest/helper.go
-@@ -47,18 +47,21 @@ func doSpeedTest(c *cli.Context, servers
-               }
-               output.WriteUI("Selected server: %s [%s]\n", output.Sanitize(currentServer.Name), output.Sanitize(u.Hostname()))
-+              output.WriteDebug("Testing against %s (%s)\n", output.Sanitize(currentServer.Name), output.Sanitize(u.String()))
-               if sponsorMsg := currentServer.Sponsor(); sponsorMsg != "" {
-                       output.WriteUI("Sponsored by: %s\n", output.Sanitize(sponsorMsg))
-               }
-               if currentServer.IsUp() {
-+                      output.WriteDebug("Fetching IP info\n")
-                       ispInfo, err := currentServer.GetIPInfo(c.String(defs.OptionDistance))
-                       if err != nil {
-                               output.WriteError("Failed to get IP info: %s\n", err)
-                               return err
-                       }
-                       output.WriteUI("You're testing from: %s\n", output.Sanitize(ispInfo.ProcessedString))
-+                      output.WriteDebug("IP info: %s\n", output.Sanitize(ispInfo.ProcessedString))
-                       // get ping and jitter value
-                       var pb *spinner.Spinner
-@@ -71,12 +74,21 @@ func doSpeedTest(c *cli.Context, servers
-                       // skip ICMP if option given
-                       currentServer.NoICMP = noICMP
-+                      // The spinner is the only sign of progress, and it is not started
-+                      // in silent mode, so --json, --csv and --simple runs otherwise show
-+                      // nothing at all until they finish. Report each phase under --debug
-+                      // instead, with the timings and counts the spinner cannot carry.
-+                      output.WriteDebug("Ping test starting: %d pings, ICMP: %t\n", pingCount, !noICMP)
-+                      pingStart := time.Now()
-+
-                       p, jitter, err := currentServer.ICMPPingAndJitter(pingCount, c.String(defs.OptionSource), network)
-                       if err != nil {
-                               output.WriteError("Failed to get ping and jitter: %s\n", err)
-                               return err
-                       }
-+                      output.WriteDebug("Ping test finished in %s: ping %.2f ms, jitter %.2f ms\n", time.Since(pingStart).Round(time.Millisecond), p, jitter)
-+
-                       if pb != nil {
-                               // print the result ourselves instead of via pb.FinalMSG: the
-                               // spinner only prints it when it was actually running, which it
-@@ -90,7 +102,11 @@ func doSpeedTest(c *cli.Context, servers
-                       var bytesRead uint64
-                       if c.Bool(defs.OptionNoDownload) {
-                               output.WriteUI("Download test is disabled\n")
-+                              output.WriteDebug("Download test skipped\n")
-                       } else {
-+                              output.WriteDebug("Download test starting: %d stream(s), %d chunk(s), up to %ds\n", c.Int(defs.OptionConcurrent), c.Int(defs.OptionChunks), c.Int(defs.OptionDuration))
-+                              downloadStart := time.Now()
-+
-                               download, br, err := currentServer.Download(silent, c.Bool(defs.OptionBytes), c.Bool(defs.OptionMebiBytes), c.Int(defs.OptionConcurrent), c.Int(defs.OptionChunks), time.Duration(c.Int(defs.OptionDuration))*time.Second)
-                               if err != nil {
-                                       output.WriteError("Failed to get download speed: %s\n", err)
-@@ -98,6 +114,8 @@ func doSpeedTest(c *cli.Context, servers
-                               }
-                               downloadValue = download
-                               bytesRead = br
-+
-+                              output.WriteDebug("Download test finished in %s: %s, %d byte(s) received\n", time.Since(downloadStart).Round(time.Millisecond), humanizeRate(download, c), br)
-                       }
-                       // get upload value
-@@ -105,7 +123,11 @@ func doSpeedTest(c *cli.Context, servers
-                       var bytesWritten uint64
-                       if c.Bool(defs.OptionNoUpload) {
-                               output.WriteUI("Upload test is disabled\n")
-+                              output.WriteDebug("Upload test skipped\n")
-                       } else {
-+                              output.WriteDebug("Upload test starting: %d stream(s), %d KiB per request, up to %ds\n", c.Int(defs.OptionConcurrent), c.Int(defs.OptionUploadSize), c.Int(defs.OptionDuration))
-+                              uploadStart := time.Now()
-+
-                               upload, bw, err := currentServer.Upload(c.Bool(defs.OptionNoPreAllocate), silent, c.Bool(defs.OptionBytes), c.Bool(defs.OptionMebiBytes), c.Int(defs.OptionConcurrent), c.Int(defs.OptionUploadSize), time.Duration(c.Int(defs.OptionDuration))*time.Second)
-                               if err != nil {
-                                       output.WriteError("Failed to get upload speed: %s\n", err)
-@@ -113,6 +135,8 @@ func doSpeedTest(c *cli.Context, servers
-                               }
-                               uploadValue = upload
-                               bytesWritten = bw
-+
-+                              output.WriteDebug("Upload test finished in %s: %s, %d byte(s) sent\n", time.Since(uploadStart).Round(time.Millisecond), humanizeRate(upload, c), bw)
-                       }
-                       // print result if --simple is given
-@@ -323,6 +347,16 @@ func sendTelemetry(telemetryServer defs.
-       }
- }
-+// humanizeRate formats a rate the same way the run's own result will be
-+// reported, so a debug line cannot appear to contradict the JSON, CSV or
-+// --simple output sitting next to it.
-+func humanizeRate(mbps float64, c *cli.Context) string {
-+      if c.Bool(defs.OptionBytes) {
-+              return humanizeMbps(mbps, c.Bool(defs.OptionMebiBytes))
-+      }
-+      return fmt.Sprintf("%.2f Mbps", mbps)
-+}
-+
- func humanizeMbps(mbps float64, useMebi bool) string {
-       val := mbps / 8
-       var base float64 = 1000
diff --git a/utils/librespeed-cli/patches/0005-docs-describe-what-secure-and-insecure-actually-do.patch b/utils/librespeed-cli/patches/0005-docs-describe-what-secure-and-insecure-actually-do.patch
deleted file mode 100644 (file)
index 7361788..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe@bloodkings.eu>
-Date: Sat, 8 Aug 2026 00:57:36 +0200
-Subject: [PATCH] docs: describe what --secure and --insecure actually do
-
-Both said they applied "when communicating with LibreSpeed.org operated
-servers". They apply to every server in the list, and they do not affect
-fetching the list itself.
-
-Help text only.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- main.go | 11 +++++++----
- 1 file changed, 7 insertions(+), 4 deletions(-)
-
---- a/main.go
-+++ b/main.go
-@@ -150,13 +150,16 @@ func main() {
-                       },
-                       &cli.BoolFlag{
-                               Name: defs.OptionSecure,
--                              Usage: "Use HTTPS instead of HTTP when communicating with\n" +
--                                      "\tLibreSpeed.org operated servers",
-+                              Usage: "Force HTTPS for every test server, whichever scheme the\n" +
-+                                      "\tserver list gives. Does not affect how the server list\n" +
-+                                      "\titself is fetched",
-                       },
-                       &cli.BoolFlag{
-                               Name: defs.OptionInsecure,
--                              Usage: "Use HTTP instead of HTTPS when communicating with\n" +
--                                      "\tLibreSpeed.org operated servers",
-+                              Usage: "Force HTTP for every test server, whichever scheme the\n" +
-+                                      "\tserver list gives. Does not affect how the server list\n" +
-+                                      "\titself is fetched; use --" + defs.OptionServerJSON + " with an\n" +
-+                                      "\thttp:// URL for that",
-                       },
-                       &cli.StringFlag{
-                               Name: defs.OptionCACert,
diff --git a/utils/librespeed-cli/patches/0006-fix-terminate-json-and-csv-output-with-a-newline.patch b/utils/librespeed-cli/patches/0006-fix-terminate-json-and-csv-output-with-a-newline.patch
deleted file mode 100644 (file)
index 750466c..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe@bloodkings.eu>
-Date: Sun, 9 Aug 2026 10:11:55 +0200
-Subject: [PATCH] fix: terminate --json and --csv output with a newline
-
-Neither ended with one: --json wrote the marshalled bytes as-is and --csv
-explicitly trimmed the trailing newline. The shell prompt lands on the same
-line as the result, and --csv-header already ends with a newline, so the two
-CSV modes could not be concatenated into a valid file.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- speedtest/helper.go | 3 ++-
- 1 file changed, 2 insertions(+), 1 deletion(-)
-
---- a/speedtest/helper.go
-+++ b/speedtest/helper.go
-@@ -224,13 +224,14 @@ func doSpeedTest(c *cli.Context, servers
-               if err := gocsv.MarshalWithoutHeaders(&reps_csv, &buf); err != nil {
-                       output.WriteError("Error generating CSV report: %s\n", err)
-               } else {
--                      os.Stdout.WriteString(strings.TrimRight(buf.String(), "\n\r"))
-+                      os.Stdout.WriteString(strings.TrimRight(buf.String(), "\n\r") + "\n")
-               }
-       } else if c.Bool(defs.OptionJSON) {
-               if b, err := json.Marshal(&reps_json); err != nil {
-                       output.WriteError("Error generating JSON report: %s\n", err)
-               } else {
-                       os.Stdout.Write(b[:])
-+                      os.Stdout.WriteString("\n")
-               }
-       }
diff --git a/utils/librespeed-cli/patches/0007-fix-drop-the-empty-copyright-line-from-version.patch b/utils/librespeed-cli/patches/0007-fix-drop-the-empty-copyright-line-from-version.patch
deleted file mode 100644 (file)
index 9347c28..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe@bloodkings.eu>
-Date: Sun, 9 Aug 2026 10:11:55 +0200
-Subject: [PATCH] fix: drop the empty copyright line from --version
-
-"librespeed.org        Copyright (C)" has named no holder and no year since the
-first commit. It says nothing that the two lines above it do not.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- speedtest/speedtest.go | 1 -
- 1 file changed, 1 deletion(-)
-
---- a/speedtest/speedtest.go
-+++ b/speedtest/speedtest.go
-@@ -73,7 +73,6 @@ func SpeedTest(c *cli.Context) error {
-               output.WriteOut("Licensed under GNU Lesser General Public License v3.0\n")
-               output.WriteOut("LibreSpeed\tCopyright (C) 2016-2020 Federico Dossena\n")
-               output.WriteOut("librespeed-cli\tCopyright (C) 2020 Maddie Zhan\n")
--              output.WriteOut("librespeed.org\tCopyright (C)\n")
-               return nil
-       }
diff --git a/utils/librespeed-cli/patches/0008-feat-report-the-negotiated-TLS-version-and-cipher-un.patch b/utils/librespeed-cli/patches/0008-feat-report-the-negotiated-TLS-version-and-cipher-un.patch
deleted file mode 100644 (file)
index b950b59..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe@bloodkings.eu>
-Date: Sun, 9 Aug 2026 23:27:01 +0200
-Subject: [PATCH] feat: report the negotiated TLS version and cipher under
- --debug
-
-On hardware without AES acceleration the cipher, not the link, is what
-bounds an HTTPS result, and the server chooses it. Two runs can differ
-several-fold for a reason the numbers alone do not show.
-
-Measured on a CZ.NIC Turris 1.x, whose e500v2 core has no crypto
-instructions: AES-256-GCM tops out around 150 Mbps while ChaCha20-Poly1305
-reaches about 390 Mbps on the same core. A server that picks the first will
-look like a slow link next to one that picks the second.
-
-Print it once, from the backend check that already runs before every test.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- defs/server.go | 16 ++++++++++++++++
- 1 file changed, 16 insertions(+)
-
---- a/defs/server.go
-+++ b/defs/server.go
-@@ -4,6 +4,7 @@ import (
-       "bytes"
-       "context"
-       "crypto/rand"
-+      "crypto/tls"
-       "encoding/json"
-       "errors"
-       "fmt"
-@@ -61,6 +62,21 @@ func (s *Server) IsUp() bool {
-               return false
-       }
-       defer resp.Body.Close()
-+
-+      // Report what the connection actually negotiated. On hardware without AES
-+      // acceleration the cipher, not the link, is what bounds the result, and
-+      // under TLS 1.3 the server picks it: the client offers a set and has no say
-+      // in the choice, and Go does not allow that set to be configured at all.
-+      // Two runs can therefore differ several-fold for a reason the numbers alone
-+      // do not show, which is what this line is for.
-+      if resp.TLS != nil {
-+              output.WriteDebug("Negotiated %s with %s\n",
-+                      tls.VersionName(resp.TLS.Version),
-+                      tls.CipherSuiteName(resp.TLS.CipherSuite))
-+      } else {
-+              output.WriteDebug("Connection is not encrypted\n")
-+      }
-+
-       b, err := io.ReadAll(resp.Body)
-       if err != nil || len(b) > 0 {
-               // %q rather than Sanitize: this is a raw response body where newlines are
diff --git a/utils/librespeed-cli/patches/0009-feat-report-which-address-each-ping-test-used-under-.patch b/utils/librespeed-cli/patches/0009-feat-report-which-address-each-ping-test-used-under-.patch
deleted file mode 100644 (file)
index 20d7d28..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe@bloodkings.eu>
-Date: Mon, 10 Aug 2026 01:03:30 +0200
-Subject: [PATCH] feat: report which address each ping test used under --debug
-
-IPv4 and IPv6 can take different paths through the network, so a result is
-not fully described by the hostname it was measured against. Nothing in the
-output says which was used: --json carries no client address, and the
-human-readable line only shows it by accident, in the ISP string.
-
-It is not academic. Against speedtest.cesnet.cz the ICMP ping goes over
-IPv4 while the transfers go over IPv6, so the reported latency and the
-reported throughput describe two different paths.
-
-The ICMP path reports the address the pinger resolved and probed; the TCP
-path reports every distinct peer the sockets connected to, taken from
-httptrace. The requests usually share one connection, but a reconnect can
-land on a different address, and reporting only the first would describe a
-connection the later samples did not use.
-
-Both paths are covered because --no-icmp is common enough that doing only
-the ICMP one would leave most users with nothing.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- defs/server.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++
- 1 file changed, 51 insertions(+)
-
---- a/defs/server.go
-+++ b/defs/server.go
-@@ -10,10 +10,13 @@ import (
-       "fmt"
-       "io"
-       "math"
-+      "net"
-       "net/http"
-+      "net/http/httptrace"
-       "net/url"
-       "os"
-       "path"
-+      "slices"
-       "strconv"
-       "sync"
-       "time"
-@@ -129,6 +132,15 @@ func (s *Server) ICMPPingAndJitter(count
-       stats := p.Statistics()
-+      // Say which address the test actually reached. IPv4 and IPv6 can take
-+      // different paths through the network, so a result is not fully described
-+      // by the hostname it was measured against, and --json carries no client
-+      // address to infer it from.
-+      if stats.IPAddr != nil {
-+              output.WriteDebug("Pinging %s over ICMP (%s)\n",
-+                      stats.IPAddr.String(), addressFamily(stats.IPAddr.String()))
-+      }
-+
-       var lastPing, jitter float64
-       for idx, rtt := range stats.Rtts {
-               if idx != 0 {
-@@ -153,6 +165,24 @@ func (s *Server) ICMPPingAndJitter(count
-       return float64(stats.AvgRtt.Milliseconds()), jitter, nil
- }
-+// addressFamily names the IP version of an address, for reporting which path a
-+// measurement took. Accepts either a bare address or one with a port.
-+func addressFamily(addr string) string {
-+      host := addr
-+      if h, _, err := net.SplitHostPort(addr); err == nil {
-+              host = h
-+      }
-+      ip := net.ParseIP(host)
-+      switch {
-+      case ip == nil:
-+              return "unknown"
-+      case ip.To4() != nil:
-+              return "IPv4"
-+      default:
-+              return "IPv6"
-+      }
-+}
-+
- // PingAndJitter pings the server via accessing ping URL and calculate the average ping and jitter
- func (s *Server) PingAndJitter(count int) (float64, float64, error) {
-       t := time.Now()
-@@ -176,6 +206,23 @@ func (s *Server) PingAndJitter(count int
-       }
-       req.Header.Set("User-Agent", UserAgent)
-+      // Collect every distinct peer, not just the first. The requests usually
-+      // share one connection, but a reconnect can land on a different address --
-+      // a different family, even -- and reporting only the first would then
-+      // describe a connection the later samples did not use.
-+      var remotes []string
-+      req = req.WithContext(httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
-+              GotConn: func(info httptrace.GotConnInfo) {
-+                      if info.Conn == nil {
-+                              return
-+                      }
-+                      addr := info.Conn.RemoteAddr().String()
-+                      if !slices.Contains(remotes, addr) {
-+                              remotes = append(remotes, addr)
-+                      }
-+              },
-+      }))
-+
-       for i := 0; i < count; i++ {
-               start := time.Now()
-               resp, err := http.DefaultClient.Do(req)
-@@ -190,6 +237,10 @@ func (s *Server) PingAndJitter(count int
-               pings = append(pings, float64(end.Sub(start).Milliseconds()))
-       }
-+      for _, addr := range remotes {
-+              output.WriteDebug("Pinging %s over TCP (%s)\n", addr, addressFamily(addr))
-+      }
-+
-       // discard first result due to handshake overhead
-       if len(pings) > 1 {
-               pings = pings[1:]
diff --git a/utils/librespeed-cli/patches/0010-fix-keep-sub-millisecond-precision-in-ping-and-jitte.patch b/utils/librespeed-cli/patches/0010-fix-keep-sub-millisecond-precision-in-ping-and-jitte.patch
deleted file mode 100644 (file)
index 68e7b4b..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe@bloodkings.eu>
-Date: Mon, 10 Aug 2026 01:35:20 +0200
-Subject: [PATCH] fix: keep sub-millisecond precision in ping and jitter
-
-Duration.Milliseconds truncates to whole milliseconds, and every round-trip
-sample went through it -- in the ICMP path and in the TCP fallback that
---no-icmp selects. On a link faster than a millisecond that leaves nothing.
-
-Against a server on loopback, reporting only ping and jitter:
-
-  before   ping 0     jitter 0
-  after    ping 0.11  jitter 0.01
-
-Over a wider link the reported ping still looks fractional, because it is an
-average of the samples, but its resolution is a whole millisecond and jitter
-is a difference of those samples. Three consecutive ICMP runs against the
-same server:
-
-  before   ping 5, 6, 5          jitter 0.59, 4.40, 0.02
-  after    ping 5.48, 5.68, 5.45 jitter 0.27, 0.32, 0.12
-
-Divide by time.Millisecond instead, which keeps the fraction.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- defs/server.go | 15 +++++++++++----
- 1 file changed, 11 insertions(+), 4 deletions(-)
-
---- a/defs/server.go
-+++ b/defs/server.go
-@@ -91,6 +91,13 @@ func (s *Server) IsUp() bool {
-       return resp.StatusCode == http.StatusOK
- }
-+// rttMillis converts a round-trip time to milliseconds without discarding the
-+// fraction. Duration.Milliseconds truncates to whole milliseconds, which on a
-+// link faster than that leaves nothing to report.
-+func rttMillis(d time.Duration) float64 {
-+      return float64(d) / float64(time.Millisecond)
-+}
-+
- // ICMPPingAndJitter pings the server via ICMP echos and calculate the average ping and jitter
- func (s *Server) ICMPPingAndJitter(count int, srcIp, network string) (float64, float64, error) {
-       t := time.Now()
-@@ -144,7 +151,7 @@ func (s *Server) ICMPPingAndJitter(count
-       var lastPing, jitter float64
-       for idx, rtt := range stats.Rtts {
-               if idx != 0 {
--                      instJitter := math.Abs(lastPing - float64(rtt.Milliseconds()))
-+                      instJitter := math.Abs(lastPing - rttMillis(rtt))
-                       if idx > 1 {
-                               if jitter > instJitter {
-                                       jitter = jitter*0.7 + instJitter*0.3
-@@ -153,7 +160,7 @@ func (s *Server) ICMPPingAndJitter(count
-                               }
-                       }
-               }
--              lastPing = float64(rtt.Milliseconds())
-+              lastPing = rttMillis(rtt)
-       }
-       if len(stats.Rtts) == 0 {
-@@ -162,7 +169,7 @@ func (s *Server) ICMPPingAndJitter(count
-               return s.PingAndJitter(count + 2)
-       }
--      return float64(stats.AvgRtt.Milliseconds()), jitter, nil
-+      return rttMillis(stats.AvgRtt), jitter, nil
- }
- // addressFamily names the IP version of an address, for reporting which path a
-@@ -234,7 +241,7 @@ func (s *Server) PingAndJitter(count int
-               resp.Body.Close()
-               end := time.Now()
--              pings = append(pings, float64(end.Sub(start).Milliseconds()))
-+              pings = append(pings, rttMillis(end.Sub(start)))
-       }
-       for _, addr := range remotes {
diff --git a/utils/librespeed-cli/patches/0011-feat-report-the-ping-distribution-under-debug.patch b/utils/librespeed-cli/patches/0011-feat-report-the-ping-distribution-under-debug.patch
deleted file mode 100644 (file)
index 119b5c7..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Date: Mon, 10 Aug 2026 02:11:41 +0200
-Subject: [PATCH] feat: report the ping distribution under --debug
-
-The test reports one average, which says nothing about how the samples were
-spread. A link averaging 5 ms because every probe took 5 ms and one averaging
-5 ms because probes ranged from 1 to 30 ms are not the same link, and only the
-second explains a connection that feels unsteady.
-
-pro-bing already works min, max and standard deviation out in Statistics(),
-so this prints what is on hand.
-
-Replies are shown as a raw count rather than a loss percentage. Ten probes are
-too few to express as a rate, and ICMP is frequently policed separately from
-the data path, so a percentage would describe the server's ICMP handling more
-than the network.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- defs/server.go | 13 +++++++++++++
- 1 file changed, 13 insertions(+)
-
---- a/defs/server.go
-+++ b/defs/server.go
-@@ -148,6 +148,19 @@ func (s *Server) ICMPPingAndJitter(count
-                       stats.IPAddr.String(), addressFamily(stats.IPAddr.String()))
-       }
-+      // A single figure hides how the samples were spread. The pinger already
-+      // works these out, and the spread is what says whether a link is steady or
-+      // merely fast on average. Raw counts rather than a loss percentage: ten
-+      // probes are too few for a rate, and ICMP is often policed independently of
-+      // the data path, so a percentage would say more about the server's ICMP
-+      // handling than about the network.
-+      output.WriteDebug("Ping over ICMP: min %.2f ms, avg %.2f ms, max %.2f ms, stddev %.2f ms, %d/%d replies\n",
-+              rttMillis(stats.MinRtt),
-+              rttMillis(stats.AvgRtt),
-+              rttMillis(stats.MaxRtt),
-+              rttMillis(stats.StdDevRtt),
-+              stats.PacketsRecv, stats.PacketsSent)
-+
-       var lastPing, jitter float64
-       for idx, rtt := range stats.Rtts {
-               if idx != 0 {
diff --git a/utils/librespeed-cli/patches/0012-feat-name-the-platform-in-the-User-Agent.patch b/utils/librespeed-cli/patches/0012-feat-name-the-platform-in-the-User-Agent.patch
deleted file mode 100644 (file)
index ba3ec99..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Date: Mon, 10 Aug 2026 08:36:47 +0200
-Subject: [PATCH] feat: name the platform in the User-Agent
-
-A server's telemetry stores the User-Agent header already, but the client only
-identifies itself by name and version, so an operator cannot tell which kinds
-of machine are measuring against them. Browsers have always reported this.
-
-Append the operating system and architecture, which runtime.GOOS and
-runtime.GOARCH give as compile-time constants at no cost:
-
-    librespeed-cli/1.0.12 (linux; arm64)
-
-It stays coarse on purpose. The kernel version or the hostname would identify
-the machine rather than describe it, and nothing here needs that.
-Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
-Upstream-Status: Submitted
----
- defs/defs.go | 10 +++++++++-
- 1 file changed, 9 insertions(+), 1 deletion(-)
-
---- a/defs/defs.go
-+++ b/defs/defs.go
-@@ -1,11 +1,19 @@
- package defs
-+import "runtime"
-+
- var (
-       // values to be filled in by build script
-       BuildDate   string
-       ProgName    string
-       ProgVersion string
--      UserAgent   = ProgName + "/" + ProgVersion
-+      // UserAgent names the platform as well as the program, the way a browser
-+      // does. A server's telemetry already stores this header, so reporting the
-+      // operating system and architecture here is what lets an operator tell
-+      // which kinds of machine are measuring against them without any change to
-+      // what the client sends. It stays coarse deliberately: the kernel version
-+      // or the hostname would identify the machine rather than describe it.
-+      UserAgent = ProgName + "/" + ProgVersion + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"
- )
- // GetIPResults represents the returned JSON from backend server's getIP.php endpoint
git clone https://git.99rst.org/PROJECT