Compare commits

..

28 commits

Author SHA1 Message Date
Maddie Zhan
14717ac78b chore: update github.com/go-ping/ping dependency
update due to CVE in indirect dependency golang.org/x/net
2025-06-12 17:40:03 +08:00
Maddie Zhan
95f2c8207a fix: fix build for non-linux platform 2025-06-12 17:36:59 +08:00
Maddie Zhan
7ed6fe234c fix: goreleaser archives.format_overrides.format deprecation 2025-06-12 17:33:06 +08:00
Vivek Kumar
0c565b724b
Update Go version requirement to 1.18+ in README (#101) 2025-06-12 17:30:49 +08:00
Meng Zhuo
c9decb3fda
Enable riscv64 release (#95) 2025-06-12 17:30:36 +08:00
An
78c9095ca9
add fwmark (#92) 2025-06-12 17:29:06 +08:00
Anton Kudriavtsev
63c75be0fc
Fix telemetry parsing error log (#91) 2025-06-12 17:28:34 +08:00
Maddie Zhan
7573b65ebc Upgrade to Go 1.18 as minimum required version
With dependency upgrade.

Fixes issue #90
2024-09-11 17:18:19 +08:00
Maddie Zhan
d33e431b58 fix build for non-windows/linux platforms 2024-09-10 23:22:34 +08:00
Maddie Zhan
d23c1b7b72 go mod tidy to fix build 2024-09-10 23:09:08 +08:00
Maddie Zhan
058cd387c1 Update goreleaser config to v2 2024-09-10 23:06:31 +08:00
Mkhanyisi Madlavana
b1daf1c451
add --ca-cert option (#81) 2024-09-10 22:53:48 +08:00
Mkhanyisi Madlavana
67adaa2956
Fix integer overflow issues at high speeds (#73) 2024-09-10 22:53:33 +08:00
Salim B
e5c131fe62
Fix code-highlighting (#68) 2024-09-10 22:53:16 +08:00
Luca Magnabosco
11183cbf98
Add interface option (#65)
* speedtest: move source bound Dialer setup to newDialerAddressBound().

* Add "--interface" option.
2024-09-10 22:52:45 +08:00
czechbol
6103965f44
Reduced container image size (#64)
* reduced container image size

* added upx to builder
2024-09-10 22:52:29 +08:00
czechbol
6059f16e57
Updated dependencies (#62)
* updated dependencies

* fixed goreleaser config errors

* formatted goreleaser config
2024-09-10 22:51:57 +08:00
Mkhanyisi Madlavana
24b7826d78
make --version to report the binary version again (#58) 2024-09-10 22:51:43 +08:00
Mka Madlavana
092760f344
throw an error if the specified --server is not in server list (#56) 2022-07-23 20:21:18 +08:00
tech189
0f908e806c
Add Windows installation options to README (#54) 2022-07-21 22:22:45 +08:00
Maddie Zhan
af2908a51d goreleaser: don't upx compress Linux MIPS binaries 2022-07-09 00:50:40 +08:00
Daniel Brennand
c996e515b1
Feat/dockerfile (#47)
* feat: Add dockerfile to run librespeed-cli in a container.

* docs: Instructions to build and run container.
2022-07-09 00:43:44 +08:00
Sebastian Bönning
954e973203
Decimal places at jitter and ping. (#48)
* Update README.md

ups

* stdout for --csv-header and --version

Changed output type to stdout for --version and --csv-header.
Addition to "send --json and --csv results to stdout and logs to stderr #39"

* decimal places at jitter and ping

better values with --share
The browser version doesn't really seem to use decimal places at ping but this
limits/extended ping and jitter to .2 decimal places at --csv / --json and stdout.
this prevent 1.0909090909090908 ping times within csv and json and extend those values in rest of the data.
The ping value could be adjusted to .0 to match the web version but this way the values are more accurate. +1 for the cli.

* Update README.md

Formatting adjusted
2022-07-09 00:43:19 +08:00
Sebastian Bönning
c2af01baf5
Update Readme (#46)
* Update Readme

main.go Formatting
README.md Formatting and update of Parameters added in the meantime. --no-icmp, --local-json, --duration, --chunks, --upload-size, --skip-cert-verify

* Update README.md

syntax
2022-01-27 19:26:47 +08:00
Dries Michiels
df77b3ee21
Use full link for the module directive in go.mod (#20) 2022-01-18 17:36:48 +08:00
Mka Madlavana
8e95ecefd7
send --json and --csv results to stdout and logs to stderr (#39) 2021-07-05 14:03:18 +08:00
Cisco Cervellera
9a8bca8fa0
Compatibility and Improved output (#34)
* Added compatibility for cambridge-fibre backend

GetIP for speedtest.cambridgefibre.uk retuns
* a body which is not empty when queried with empty parameters
* Just a string with IP informaiton (no distance)
The code will not stop in this case but will printout a debug message.

* Improved format for JSON and CSV output for multiple server.

The JSON output is now a list of objects one per server.
The CSV output has no empty rows.

Co-authored-by: Cisco Cervellera <ciscoski@users.noreply.github.com>
2021-05-15 21:24:05 +08:00
blnprasad
08d21d65ff
Make ping job workers channel buffer length same as len(servers)
Original title: changes to avoid race condition seen with ping job workers (#32)

Authored-by: blnprasad <blnprasad@nile-global.com>
2021-05-07 11:54:26 +08:00
15 changed files with 455 additions and 238 deletions

View file

@ -1,74 +1,84 @@
project_name: 'librespeed-cli'
version: 2
project_name: "librespeed-cli"
#dist: ./out
before:
hooks:
- go mod download
builds:
- main: ./main.go
id: upx
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}"
goos:
- linux
- darwin
- freebsd
goarch:
- 386
- amd64
- arm
- arm64
- mips
- mipsle
- mips64
- mips64le
goarm:
- 5
- 6
- 7
gomips:
- hardfloat
- softfloat
ignore:
- goos: darwin
goarch: 386
- goos: darwin
goarch: arm64
hooks:
post: ./upx.sh -9 "{{ .Path }}"
- main: ./main.go
id: no-upx
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}"
goos:
- windows
- darwin
goarch:
- 386
- amd64
- arm64
ignore:
- goos: darwin
goarch: 386
- goos: darwin
goarch: amd64
- main: ./main.go
id: upx
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}"
goos:
- linux
- darwin
- freebsd
goarch:
- "386"
- amd64
- arm
- arm64
goarm:
- "5"
- "6"
- "7"
ignore:
- goos: darwin
goarch: "386"
- goos: darwin
goarch: arm64
hooks:
post:
- ./upx.sh -9 "{{ .Path }}"
- main: ./main.go
id: no-upx
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}"
goos:
- linux
- windows
- darwin
goarch:
- "386"
- amd64
- arm64
- mips
- mipsle
- mips64
- mips64le
- riscv64
gomips:
- hardfloat
- softfloat
ignore:
- goos: linux
goarch: "386"
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: "386"
- goos: darwin
goarch: amd64
archives:
- format_overrides:
- goos: windows
format: zip
formats: ['zip']
files:
- LICENSE
checksum:
name_template: 'checksums.txt'
name_template: "checksums.txt"
changelog:
skip: false
disable: false
sort: asc
release:
github:

View file

@ -1,7 +1,7 @@
![LibreSpeed Logo](https://github.com/librespeed/speedtest/blob/master/.logo/logo3.png?raw=true)
# LibreSpeed command line tool
Don't have a GUI but wants to use LibreSpeed servers to test your Internet speed? 🚀
Don't have a GUI but want to use LibreSpeed servers to test your Internet speed? 🚀
`librespeed-cli` comes to rescue!
@ -22,7 +22,7 @@ This is a command line interface for LibreSpeed speed test backends, written in
[![asciicast](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2.svg)](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2)
## Requirements for compiling
- Go 1.14+
- Go 1.18+
## Runtime requirements
- Any [Go supported platforms](https://github.com/golang/go/wiki/MinimumRequirements)
@ -42,7 +42,7 @@ For Linux users, you can use either the archive from golang.org, or install from
```shell script
# pacman -S go
```
2. Then, clone the repository:
```shell script
@ -59,10 +59,10 @@ can now proceed to build `librespeed-cli` with the build script:
If you want to build for another operating system or system architecture, use the `GOOS` and `GOARCH` environment
variables. Run `go tool dist list` to get a list of possible combinations of `GOOS` and `GOARCH`.
Note: Technically, the CLI can be compiled with older Go versions that support Go modules, with `GO111MODULE=on`
set. If you're compiling with an older Go runtime, you might have to change the Go version in `go.mod`.
```shell script
# Let's say we're building for 64-bit Windows on Linux
$ GOOS=windows GOARCH=amd64 ./build.sh
@ -74,7 +74,7 @@ can now proceed to build `librespeed-cli` with the build script:
$ ls out
librespeed-cli-windows-amd64.exe
```
5. Now you can use the `librespeed-cli` and test your Internet speed!
## Install from AUR
@ -97,11 +97,45 @@ $ makepkg -si
See the [librespeed-cli Homebrew tap](https://github.com/librespeed/homebrew-tap#setup).
## Install on Windows
If you have either [Scoop](https://scoop.sh/) or [Chocolatey](https://chocolatey.org/) installed you can use one of the following commands:
- Scoop (ensure you have the `extras` bucket added):
```
> scoop install librespeed-cli
```
- Chocolatey:
```
> choco install librespeed-cli
```
## Container Image
You can run `librespeed-cli` in a container.
1. Build the container image:
```shell script
docker build -t librespeed-cli:latest .
```
2. Run the container:
```shell script
docker run --rm --name librespeed-cli librespeed-cli:latest
# With options
docker run --rm --name librespeed-cli librespeed-cli:latest --telemetry-level disabled --no-upload
# To avoid "Failed to ping target host: socket: permission denied" errors when using --verbose
docker run --rm --name librespeed-cli --sysctl net.ipv4.ping_group_range="0 2147483647" librespeed-cli:latest --verbose
```
## Usage
You can see the full list of supported options with `librespeed-cli -h`:
```shell script
```
$ librespeed-cli -h
NAME:
librespeed-cli - Test your Internet speed with LibreSpeed 🚀
@ -116,6 +150,8 @@ GLOBAL OPTIONS:
--ipv6, -6 Force IPv6 only (default: false)
--no-download Do not perform download test (default: false)
--no-upload Do not perform upload test (default: false)
--no-icmp Do not use ICMP ping. ICMP doesn't work well under Linux
at this moment, so you might want to disable it (default: false)
--concurrent value Concurrent HTTP requests being made (default: 3)
--bytes Display values in bytes instead of bits. Does not affect
the image generated by --share, nor output from
@ -126,27 +162,37 @@ GLOBAL OPTIONS:
--share Generate and provide a URL to the LibreSpeed.org share results
image, not displayed with --csv (default: false)
--simple Suppress verbose output, only show basic information
(default: false)
(default: false)
--csv Suppress verbose output, only show basic information in CSV
format. Speeds listed in bit/s and not affected by --bytes
(default: false)
(default: false)
--csv-delimiter CSV_DELIMITER Single character delimiter (CSV_DELIMITER) to use in
CSV output. (default: ",")
--csv-header Print CSV headers (default: false)
--json Suppress verbose output, only show basic information
in JSON format. Speeds listed in bit/s and not
affected by --bytes (default: false)
affected by --bytes (default: false)
--list Display a list of LibreSpeed.org servers (default: false)
--server SERVER Specify a SERVER ID to test against. Can be supplied
multiple times. Cannot be used with --exclude
--exclude EXCLUDE EXCLUDE a server from selection. Can be supplied
multiple times. Cannot be used with --server
--server-json value Use an alternative server list from remote JSON file
--local-json value Use an alternative server list from local JSON file
--source SOURCE SOURCE IP address to bind to
--local-json value Use an alternative server list from local JSON file,
or read from stdin with "--local-json -".
--source SOURCE SOURCE IP address to bind to. Incompatible with --interface.
--interface INTERFACE The name of the network interface to bind to. Example: "enp0s3".
Not supported on Windows and incompatible with --source.
Implies --no-icmp.
--timeout TIMEOUT HTTP TIMEOUT in seconds. (default: 15)
--duration value Upload and download test duration in seconds (default: 15)
--chunks value Chunks to download from server, chunk size depends on server configuration (default: 100)
--upload-size value Size of payload being uploaded in KiB (default: 1024)
--secure Use HTTPS instead of HTTP when communicating with
LibreSpeed.org operated servers (default: false)
--ca-cert value Use the specified CA certificate PEM bundle file instead
of the system certificate trust store
--skip-cert-verify Skip verifying SSL certificate for HTTPS connections (self-signed certs) (default: false)
--no-pre-allocate Do not pre allocate upload data. Pre allocation is
enabled by default to improve upload performance. To
support systems with insufficient memory, use this
@ -190,11 +236,15 @@ locally via `--local-json`). The format is as below:
]
```
The `--local-json` option can also read from `stdin`:
`echo '[{"id": 1,"name": "a","server": "https://speedtest.example.com/","dlURL": "garbage.php","ulURL": "empty.php","pingURL": "empty.php","getIpURL": "getIP.php"}]' | librespeed-cli --local-json - `
As you can see in the example, all servers have their schemes defined. In case of undefined scheme (e.g. `//example.com`),
`librespeed-cli` will use `http` by default, or `https` when the `--secure` option is enabled.
## Use a custom telemetry server
By default, the telemetry result will be sent to `librespeed.org`. You can also customize your telemetry settings
By default, the telemetry result will be sent to `librespeed.org`. You can also customize your telemetry settings
via the `--telemetry` prefixed options. In order to load a custom telemetry endpoint configuration, you'll have to use the
`--telemetry-json` option to specify a local JSON file containing the configuration bits. The format is as below:

View file

@ -10,9 +10,10 @@ CURRENT_DIR=$(pwd)
OUT_DIR=${CURRENT_DIR}/out
PROGNAME="librespeed-cli"
DEFS_PATH="github.com/librespeed/speedtest-cli"
BINARY=${PROGNAME}-$(go env GOOS)-$(go env GOARCH)
BUILD_DATE=$(date -u "+%Y-%m-%d %H:%M:%S %Z")
LDFLAGS="-w -s -X \"librespeed-cli/defs.ProgName=${PROGNAME}\" -X \"librespeed-cli/defs.ProgVersion=${PROGVER}\" -X \"librespeed-cli/defs.BuildDate=${BUILD_DATE}\""
LDFLAGS="-w -s -X \"${DEFS_PATH}/defs.ProgName=${PROGNAME}\" -X \"${DEFS_PATH}/defs.ProgVersion=${PROGVER}\" -X \"${DEFS_PATH}/defs.BuildDate=${BUILD_DATE}\""
if [[ -n "${GOARM}" ]] && [[ "${GOARM}" -gt 0 ]]; then
BINARY=${BINARY}v${GOARM}

View file

@ -14,7 +14,7 @@ import (
type BytesCounter struct {
start time.Time
pos int
total int
total uint64
payload []byte
reader io.ReadSeeker
mebi bool
@ -33,7 +33,7 @@ func NewCounter() *BytesCounter {
func (c *BytesCounter) Write(p []byte) (int, error) {
n := len(p)
c.lock.Lock()
c.total += n
c.total += uint64(n)
c.lock.Unlock()
return n, nil
@ -43,7 +43,7 @@ func (c *BytesCounter) Write(p []byte) (int, error) {
func (c *BytesCounter) Read(p []byte) (int, error) {
n, err := c.reader.Read(p)
c.lock.Lock()
c.total += n
c.total += uint64(n)
c.pos += n
if c.pos == c.uploadSize {
c.resetReader()
@ -116,7 +116,7 @@ func (c *BytesCounter) Start() {
}
// Total returns the total bytes read/written
func (c *BytesCounter) Total() int {
func (c *BytesCounter) Total() uint64 {
return c.total
}

View file

@ -24,11 +24,13 @@ const (
OptionExclude = "exclude"
OptionServerJSON = "server-json"
OptionSource = "source"
OptionInterface = "interface"
OptionTimeout = "timeout"
OptionChunks = "chunks"
OptionUploadSize = "upload-size"
OptionDuration = "duration"
OptionSecure = "secure"
OptionCACert = "ca-cert"
OptionSkipCertVerify = "skip-cert-verify"
OptionNoPreAllocate = "no-pre-allocate"
OptionVersion = "version"
@ -40,4 +42,5 @@ const (
OptionTelemetryPath = "telemetry-path"
OptionTelemetryShare = "telemetry-share"
OptionTelemetryExtra = "telemetry-extra"
OptionFwmark = "fwmark"
)

View file

@ -60,8 +60,11 @@ func (s *Server) IsUp() bool {
}
defer resp.Body.Close()
b, _ := ioutil.ReadAll(resp.Body)
if len(b) > 0 {
log.Debugf("Failed when parsing get IP result: %s", b)
}
// only return online if the ping URL returns nothing and 200
return len(b) == 0 && resp.StatusCode == http.StatusOK
return resp.StatusCode == http.StatusOK
}
// ICMPPingAndJitter pings the server via ICMP echos and calculate the average ping and jitter
@ -185,7 +188,7 @@ func (s *Server) PingAndJitter(count int) (float64, float64, error) {
}
// Download performs the actual download test
func (s *Server) Download(silent bool, useBytes, useMebi bool, requests int, chunks int, duration time.Duration) (float64, int, error) {
func (s *Server) Download(silent bool, useBytes, useMebi bool, requests int, chunks int, duration time.Duration) (float64, uint64, error) {
t := time.Now()
defer func() {
s.TLog.Logf("Download took %s", time.Now().Sub(t).String())
@ -277,7 +280,7 @@ Loop:
}
// Upload performs the actual upload test
func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi bool, requests int, uploadSize int, duration time.Duration) (float64, int, error) {
func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi bool, requests int, uploadSize int, duration time.Duration) (float64, uint64, error) {
t := time.Now()
defer func() {
s.TLog.Logf("Upload took %s", time.Now().Sub(t).String())
@ -412,6 +415,7 @@ func (s *Server) GetIPInfo(distanceUnit string) (*GetIPResult, error) {
if err := json.Unmarshal(b, &ipInfo); err != nil {
log.Debugf("Failed when parsing get IP result: %s", err)
log.Debugf("Received payload: %s", b)
ipInfo.ProcessedString = string(b[:])
}
}

19
dockerfile Normal file
View file

@ -0,0 +1,19 @@
FROM golang:1.20.3-alpine as builder
RUN apk add --no-cache bash upx
# Set working directory
WORKDIR /usr/src/librespeed-cli
# Copy librespeed-cli
COPY . .
# Build librespeed-cli
RUN ./build.sh
FROM alpine:3.17
# Copy librespeed-cli binary
COPY --from=builder /usr/src/librespeed-cli/out/librespeed-cli* /bin/librespeed-cli
CMD ["/bin/librespeed-cli"]

36
go.mod
View file

@ -1,17 +1,27 @@
module librespeed-cli
module github.com/librespeed/speedtest-cli
go 1.14
go 1.23.0
toolchain go1.24.2
require (
github.com/briandowns/spinner v1.12.0
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/fatih/color v1.10.0 // indirect
github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741
github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.3.0 // indirect
github.com/urfave/cli/v2 v2.3.0
golang.org/x/net v0.0.0-20210421230115-4e50805a0758 // indirect
golang.org/x/sys v0.0.0-20210421221651-33663a62ff08 // indirect
github.com/briandowns/spinner v1.23.1
github.com/go-ping/ping v1.2.0
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/sirupsen/logrus v1.9.3
github.com/urfave/cli/v2 v2.27.4
golang.org/x/sys v0.33.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/term v0.32.0 // indirect
)

91
go.sum
View file

@ -1,59 +1,56 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/briandowns/spinner v1.12.0 h1:72O0PzqGJb6G3KgrcIOtL/JAGGZ5ptOMCn9cUHmqsmw=
github.com/briandowns/spinner v1.12.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650=
github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741 h1:b0sLP++Tsle+s57tqg5sUk1/OQsC6yMCciVeqNzOcwU=
github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI=
github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d h1:r3mStZSyjKhEcgbJ5xtv7kT5PZw/tDiFBTMgQx2qsXE=
github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ=
github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758 h1:aEpZnXcAmXkd6AvLb2OPt+EN1Zu/8Ne3pCqPjja5PXY=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210421221651-33663a62ff08 h1:qyN5bV+96OX8pL78eXDuz6YlDPzCYgdW74H5yE9BoSU=
golang.org/x/sys v0.0.0-20210421221651-33663a62ff08/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

26
main.go
View file

@ -6,8 +6,8 @@ import (
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"librespeed-cli/defs"
"librespeed-cli/speedtest"
"github.com/librespeed/speedtest-cli/defs"
"github.com/librespeed/speedtest-cli/speedtest"
)
// init sets up the essential bits on start up
@ -20,7 +20,7 @@ func init() {
// warn level is for suppress modes
// error level is for errors
log.SetOutput(os.Stdout)
log.SetOutput(os.Stderr)
log.SetFormatter(formatter)
log.SetLevel(log.InfoLevel)
}
@ -59,7 +59,7 @@ func main() {
&cli.BoolFlag{
Name: defs.OptionNoICMP,
Usage: "Do not use ICMP ping. ICMP doesn't work well under Linux\n" +
"at this moment, so you might want to disable it",
"\tat this moment, so you might want to disable it",
},
&cli.IntFlag{
Name: defs.OptionConcurrent,
@ -110,7 +110,7 @@ func main() {
Name: defs.OptionJSON,
Usage: "Suppress verbose output, only show basic information\n" +
"\tin JSON format. Speeds listed in bit/s and not\n" +
"\t affected by --bytes",
"\taffected by --bytes",
},
&cli.BoolFlag{
Name: defs.OptionList,
@ -139,9 +139,13 @@ func main() {
Name: defs.OptionSource,
Usage: "`SOURCE` IP address to bind to",
},
&cli.StringFlag{
Name: defs.OptionInterface,
Usage: "network INTERFACE to bind to",
},
&cli.IntFlag{
Name: defs.OptionTimeout,
Usage: "HTTP `TIMEOUT` in seconds",
Usage: "HTTP `TIMEOUT` in seconds.",
Value: 15,
},
&cli.IntFlag{
@ -164,6 +168,11 @@ func main() {
Usage: "Use HTTPS instead of HTTP when communicating with\n" +
"\tLibreSpeed.org operated servers",
},
&cli.StringFlag{
Name: defs.OptionCACert,
Usage: "Use the specified CA certificate PEM bundle file instead\n" +
"\tof the system certificate trust store",
},
&cli.BoolFlag{
Name: defs.OptionSkipCertVerify,
Usage: "Skip verifying SSL certificate for HTTPS connections (self-signed certs)",
@ -209,6 +218,11 @@ func main() {
Usage: "Send a custom message along with the telemetry results.\n" +
"\tImplies --" + defs.OptionShare,
},
&cli.IntFlag{
Name: defs.OptionFwmark,
Usage: "firewall mark to set on socket.",
Value: 0,
},
},
}

View file

@ -3,7 +3,7 @@ package report
import (
"time"
"librespeed-cli/defs"
"github.com/librespeed/speedtest-cli/defs"
)
// JSONReport represents the output data fields in a JSON file
@ -11,8 +11,8 @@ type JSONReport struct {
Timestamp time.Time `json:"timestamp"`
Server Server `json:"server"`
Client Client `json:"client"`
BytesSent int `json:"bytes_sent"`
BytesReceived int `json:"bytes_received"`
BytesSent uint64 `json:"bytes_sent"`
BytesReceived uint64 `json:"bytes_received"`
Ping float64 `json:"ping"`
Jitter float64 `json:"jitter"`
Upload float64 `json:"upload"`

View file

@ -8,17 +8,17 @@ import (
"math"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/gocarina/gocsv"
"github.com/librespeed/speedtest-cli/defs"
"github.com/librespeed/speedtest-cli/report"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"librespeed-cli/defs"
"librespeed-cli/report"
"github.com/urfave/cli/v2"
)
const (
@ -27,11 +27,14 @@ const (
)
// doSpeedTest is where the actual speed test happens
func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.TelemetryServer, network string, silent bool) error {
func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.TelemetryServer, network string, silent bool, noICMP bool) error {
if serverCount := len(servers); serverCount > 1 {
log.Infof("Testing against %d servers", serverCount)
}
var reps_json []report.JSONReport
var reps_csv []report.CSVReport
// fetch current user's IP info
for _, currentServer := range servers {
// get telemetry level
@ -66,7 +69,7 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
}
// skip ICMP if option given
currentServer.NoICMP = c.Bool(defs.OptionNoICMP)
currentServer.NoICMP = noICMP
p, jitter, err := currentServer.ICMPPingAndJitter(pingCount, c.String(defs.OptionSource), network)
if err != nil {
@ -75,13 +78,13 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
}
if pb != nil {
pb.FinalMSG = fmt.Sprintf("Ping: %.0f ms\tJitter: %.0f ms\n", p, jitter)
pb.FinalMSG = fmt.Sprintf("Ping: %.2f ms\tJitter: %.2f ms\n", p, jitter)
pb.Stop()
}
// get download value
var downloadValue float64
var bytesRead int
var bytesRead uint64
if c.Bool(defs.OptionNoDownload) {
log.Info("Download test is disabled")
} else {
@ -91,12 +94,12 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
return err
}
downloadValue = download
bytesRead = br
bytesRead = uint64(br)
}
// get upload value
var uploadValue float64
var bytesWritten int
var bytesWritten uint64
if c.Bool(defs.OptionNoUpload) {
log.Info("Upload test is disabled")
} else {
@ -106,16 +109,16 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
return err
}
uploadValue = upload
bytesWritten = bw
bytesWritten = uint64(bw)
}
// print result if --simple is given
if c.Bool(defs.OptionSimple) {
if c.Bool(defs.OptionBytes) {
useMebi := c.Bool(defs.OptionMebiBytes)
log.Warnf("Ping:\t%.0f ms\tJitter:\t%.0f ms\nDownload rate:\t%s\nUpload rate:\t%s", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi))
log.Warnf("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%s\nUpload rate:\t%s", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi))
} else {
log.Warnf("Ping:\t%.0f ms\tJitter:\t%.0f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps", p, jitter, downloadValue, uploadValue)
log.Warnf("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps", p, jitter, downloadValue, uploadValue)
}
}
@ -140,34 +143,25 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
// check for --csv or --json. the program prioritize the --csv before the --json. this is the same behavior as speedtest-cli
if c.Bool(defs.OptionCSV) {
// print csv if --csv is given
var reps []report.CSVReport
var rep report.CSVReport
rep.Timestamp = time.Now()
rep.Name = currentServer.Name
rep.Address = u.String()
rep.Ping = p
rep.Ping = math.Round(p*100) / 100
rep.Jitter = math.Round(jitter*100) / 100
rep.Download = math.Round(downloadValue*100) / 100
rep.Upload = math.Round(uploadValue*100) / 100
rep.Share = shareLink
rep.IP = ispInfo.RawISPInfo.IP
reps = append(reps, rep)
var buf bytes.Buffer
if err := gocsv.MarshalWithoutHeaders(&reps, &buf); err != nil {
log.Errorf("Error generating CSV report: %s", err)
} else {
log.Warn(buf.String())
}
reps_csv = append(reps_csv, rep)
} else if c.Bool(defs.OptionJSON) {
// print json if --json is given
var rep report.JSONReport
rep.Timestamp = time.Now()
rep.Ping = p
rep.Ping = math.Round(p*100) / 100
rep.Jitter = math.Round(jitter*100) / 100
rep.Download = math.Round(downloadValue*100) / 100
rep.Upload = math.Round(uploadValue*100) / 100
@ -181,22 +175,34 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
rep.Client = report.Client{ispInfo.RawISPInfo}
rep.Client.Readme = ""
if b, err := json.Marshal(&rep); err != nil {
log.Errorf("Error generating JSON report: %s", err)
} else {
log.Warnf("%s", b)
}
reps_json = append(reps_json, rep)
}
} else {
log.Infof("Selected server %s (%s) is not responding at the moment, try again later", currentServer.Name, u.Hostname())
}
// add a new line after each test if testing multiple servers
if len(servers) > 1 {
//add a new line after each test if testing multiple servers
if len(servers) > 1 && !silent {
log.Warn()
}
}
// check for --csv or --json. the program prioritize the --csv before the --json. this is the same behavior as speedtest-cli
if c.Bool(defs.OptionCSV) {
var buf bytes.Buffer
if err := gocsv.MarshalWithoutHeaders(&reps_csv, &buf); err != nil {
log.Errorf("Error generating CSV report: %s", err)
} else {
os.Stdout.WriteString(buf.String())
}
} else if c.Bool(defs.OptionJSON) {
if b, err := json.Marshal(&reps_json); err != nil {
log.Errorf("Error generating JSON report: %s", err)
} else {
os.Stdout.Write(b[:])
}
}
return nil
}
@ -233,7 +239,7 @@ func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResu
if fPing, err := wr.CreateFormField("ping"); err != nil {
log.Debugf("Error creating form field: %s", err)
return "", err
} else if _, err = fPing.Write([]byte(strconv.Itoa(int(pingVal)))); err != nil {
} else if _, err = fPing.Write([]byte(strconv.FormatFloat(pingVal, 'f', 2, 64))); err != nil {
log.Debugf("Error writing form field: %s", err)
return "", err
}
@ -241,7 +247,7 @@ func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResu
if fJitter, err := wr.CreateFormField("jitter"); err != nil {
log.Debugf("Error creating form field: %s", err)
return "", err
} else if _, err = fJitter.Write([]byte(strconv.Itoa(int(jitter)))); err != nil {
} else if _, err = fJitter.Write([]byte(strconv.FormatFloat(jitter, 'f', 2, 64))); err != nil {
log.Debugf("Error writing form field: %s", err)
return "", err
}

View file

@ -3,6 +3,7 @@ package speedtest
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
@ -19,8 +20,8 @@ import (
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"librespeed-cli/defs"
"librespeed-cli/report"
"github.com/librespeed/speedtest-cli/defs"
"github.com/librespeed/speedtest-cli/report"
)
const (
@ -64,6 +65,7 @@ func SpeedTest(c *cli.Context) error {
// print version
if c.Bool(defs.OptionVersion) {
log.SetOutput(os.Stdout)
log.Warnf("%s %s (built on %s)", defs.ProgName, defs.ProgVersion, defs.BuildDate)
log.Warn("https://github.com/librespeed/speedtest-cli")
log.Warn("Licensed under GNU Lesser General Public License v3.0")
@ -73,6 +75,10 @@ func SpeedTest(c *cli.Context) error {
return nil
}
if c.String(defs.OptionSource) != "" && c.String(defs.OptionInterface) != "" {
return fmt.Errorf("incompatible options '%s' and '%s'", defs.OptionSource, defs.OptionInterface)
}
// set CSV delimiter
gocsv.TagSeparator = c.String(defs.OptionCSVDelimiter)
@ -80,7 +86,7 @@ func SpeedTest(c *cli.Context) error {
if c.Bool(defs.OptionCSVHeader) {
var rep []report.CSVReport
b, _ := gocsv.MarshalBytes(&rep)
log.Warnf("%s", b)
os.Stdout.WriteString(string(b))
return nil
}
@ -99,7 +105,7 @@ func SpeedTest(c *cli.Context) error {
return err
}
if err := json.Unmarshal(b, &telemetryServer); err != nil {
log.Errorf("Error parsing %s: %s", err)
log.Errorf("Error parsing %s: %s", telemetryJSON, err)
return err
}
}
@ -137,6 +143,8 @@ func SpeedTest(c *cli.Context) error {
return errors.New("invalid concurrent requests setting")
}
noICMP := c.Bool(defs.OptionNoICMP)
// HTTP requests timeout
http.DefaultClient.Timeout = time.Duration(c.Int(defs.OptionTimeout)) * time.Second
@ -154,59 +162,71 @@ func SpeedTest(c *cli.Context) error {
}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify)}
// bind to source IP address if given, or if ipv4/ipv6 is forced
if src := c.String(defs.OptionSource); src != "" || (forceIPv4 || forceIPv6) {
var localTCPAddr *net.TCPAddr
if src != "" {
// first we parse the IP to see if it's valid
addr, err := net.ResolveIPAddr(network, src)
if err != nil {
if strings.Contains(err.Error(), "no suitable address") {
if forceIPv6 {
log.Errorf("Address %s is not a valid IPv6 address", src)
} else {
log.Errorf("Address %s is not a valid IPv4 address", src)
}
} else {
log.Errorf("Error parsing source IP: %s", err)
}
return err
}
log.Debugf("Using %s as source IP", src)
localTCPAddr = &net.TCPAddr{IP: addr.IP}
if caCertFileName := c.String(defs.OptionCACert); caCertFileName != "" {
caCert, err := ioutil.ReadFile(caCertFileName)
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
var dialContext func(context.Context, string, string) (net.Conn, error)
defaultDialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify),
RootCAs: caCertPool,
}
if localTCPAddr != nil {
defaultDialer.LocalAddr = localTCPAddr
} else {
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify),
}
switch {
case forceIPv4:
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
return defaultDialer.DialContext(ctx, "tcp4", address)
}
case forceIPv6:
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
return defaultDialer.DialContext(ctx, "tcp6", address)
}
default:
dialContext = defaultDialer.DialContext
}
// set default HTTP client's Transport to the one that binds the source address
// this is modified from http.DefaultTransport
transport.DialContext = dialContext
}
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
// bind to source IP address if given
if src := c.String(defs.OptionSource); src != "" {
var err error
dialer, err = newDialerAddressBound(src, network)
if err != nil {
return err
}
}
// bind to interface if given
// bind to interface if given
iface := c.String(defs.OptionInterface)
fwmark := c.Int(defs.OptionFwmark)
if iface != "" || fwmark > 0 {
var err error
dialer, err = newDialerInterfaceOrFwmarkBound(iface, fwmark)
if err != nil {
return err
}
// ICMP ping does not support interface binding.
noICMP = true
}
// enforce if ipv4/ipv6 is forced
var dialContext func(context.Context, string, string) (net.Conn, error)
switch {
case forceIPv4:
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
return dialer.DialContext(ctx, "tcp4", address)
}
case forceIPv6:
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
return dialer.DialContext(ctx, "tcp6", address)
}
default:
dialContext = dialer.DialContext
}
// set default HTTP client's Transport to the one that binds the source address
// this is modified from http.DefaultTransport
transport.DialContext = dialContext
http.DefaultClient.Transport = transport
// load server list
@ -257,21 +277,21 @@ func SpeedTest(c *cli.Context) error {
// if --server is given, do speed tests with all of them
if len(c.IntSlice(defs.OptionServer)) > 0 {
return doSpeedTest(c, servers, telemetryServer, network, silent)
return doSpeedTest(c, servers, telemetryServer, network, silent, noICMP)
} else {
// else select the fastest server from the list
log.Info("Selecting the fastest server based on ping")
var wg sync.WaitGroup
jobs := make(chan PingJob, 10)
results := make(chan PingResult, 10)
jobs := make(chan PingJob, len(servers))
results := make(chan PingResult, len(servers))
done := make(chan struct{})
pingList := make(map[int]float64)
// spawn 10 concurrent pingers
for i := 0; i < 10; i++ {
go pingWorker(jobs, results, &wg, c.String(defs.OptionSource), network, c.Bool(defs.OptionNoICMP))
go pingWorker(jobs, results, &wg, c.String(defs.OptionSource), network, noICMP)
}
// send ping jobs to workers
@ -308,7 +328,7 @@ func SpeedTest(c *cli.Context) error {
}
// do speed test on the server
return doSpeedTest(c, []defs.Server{servers[serverIdx]}, telemetryServer, network, silent)
return doSpeedTest(c, []defs.Server{servers[serverIdx]}, telemetryServer, network, silent, noICMP)
}
}
@ -453,6 +473,10 @@ func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specifi
ret = append(ret, server)
}
}
if len(ret) == 0 {
error_message := fmt.Sprintf("specified server(s) not found: %v", specific)
return nil, errors.New(error_message)
}
return ret, nil
}
}
@ -469,3 +493,31 @@ func contains(arr []int, val int) bool {
}
return false
}
func newDialerAddressBound(src string, network string) (dialer *net.Dialer, err error) {
// first we parse the IP to see if it's valid
addr, err := net.ResolveIPAddr(network, src)
if err != nil {
if strings.Contains(err.Error(), "no suitable address") {
if network == "ip6" {
log.Errorf("Address %s is not a valid IPv6 address", src)
} else {
log.Errorf("Address %s is not a valid IPv4 address", src)
}
} else {
log.Errorf("Error parsing source IP: %s", err)
}
return nil, err
}
log.Debugf("Using %s as source IP", src)
localTCPAddr := &net.TCPAddr{IP: addr.IP}
defaultDialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
defaultDialer.LocalAddr = localTCPAddr
return defaultDialer, nil
}

13
speedtest/util.go Normal file
View file

@ -0,0 +1,13 @@
//go:build !linux
// +build !linux
package speedtest
import (
"fmt"
"net"
)
func newDialerInterfaceOrFwmarkBound(iface string, fwmark int) (dialer *net.Dialer, err error) {
return nil, fmt.Errorf("cannot bound to interface on this platform")
}

38
speedtest/util_linux.go Normal file
View file

@ -0,0 +1,38 @@
package speedtest
import (
"net"
"syscall"
"time"
"golang.org/x/sys/unix"
)
func newDialerInterfaceOrFwmarkBound(iface string, fwmark int) (dialer *net.Dialer, err error) {
// In linux there is the socket option SO_BINDTODEVICE.
// Therefore we can really bind the socket to the device instead of binding to the address that
// would be affected by the default routes.
control := func(network, address string, c syscall.RawConn) error {
var errSock error
err := c.Control((func(fd uintptr) {
if iface != "" {
errSock = unix.BindToDevice(int(fd), iface)
}
if fwmark > 0 {
errSock = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, fwmark)
}
}))
if err != nil {
return err
}
return errSock
}
dialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
Control: control,
}
return dialer, nil
}