Compare commits

...

69 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
Maddie Zhan
9459cbb51d Update dependencies 2021-04-23 15:28:37 +08:00
Maddie Zhan
99c0446381 Do not UPX windows and darwin/arm64 binaries 2021-04-23 15:27:48 +08:00
Maddie Zhan
fd5905b72f Removed emoji in help message for better compatibility
Fixes #28
2021-04-23 15:14:14 +08:00
Maddie Zhan
f6506bc122 Only print share link when not in JSON/CSV mode
Fixes #30
2021-04-23 15:13:08 +08:00
Maddie Zhan
67b82aec33 Update dependencies 2021-03-18 22:34:52 +08:00
Maddie Zhan
5354b7fc8e Support reading local JSON from stdin
Fixes #19
2021-03-18 22:34:02 +08:00
Maddie Zhan
c9903d395a Allow skipping ICMP pings 2021-01-15 10:09:24 +08:00
Maddie Zhan
5cf383b64d Clone the DefaultTransport directly from stdlib 2020-12-07 09:45:47 +08:00
Andonome
fd8af1557c
remove readme 'by default' repetition (#15) 2020-12-07 09:39:02 +08:00
Maddie Zhan
1190984d53 Update dependencies 2020-12-07 09:36:32 +08:00
Maddie Zhan
4676b26441 Allow self signed certs for HTTPS backends (#16) 2020-12-07 09:35:40 +08:00
Maddie Zhan
dc6eb0fbce Configurable download chunks, upload size and test duration 2020-11-25 10:24:25 +08:00
Maddie Zhan
eb7f5cb43a Update go-ping module 2020-09-22 09:45:27 +08:00
Maddie Zhan
02de79bf8d Fix variable name collision 2020-09-16 14:23:01 +08:00
Maddie Zhan
06859d9a86 Show correct User Agent for IsUp and GetIP 2020-07-15 17:40:39 +08:00
Maddie Zhan
7063aa41df Update goreleaser.yml 2020-07-14 17:40:49 +08:00
Maddie Zhan
6199090285 Use ID from server JSON instead of slice index 2020-07-14 17:28:13 +08:00
Maddie Zhan
166771ba3f Fix user-agent on fetching server list JSON
Fixes #11
2020-06-29 12:44:13 +08:00
mildis
1f8dc728d7
handle a .well-known URI (#9)
* reformat message to match custom url

* retry remote fetch with .well-known URI

Instead of having to type a whole URI, expect the JSON server list
to be served at /.well-known/librespeed

Thus, only the base URL should be necessary.
2020-05-15 09:08:55 +08:00
Maddie Zhan
4898bfecc9 Send outputs to stdout instead of stderr
Fixes #5
2020-03-31 18:08:11 +08:00
Maddie Zhan
c9bfcbc770 Use new backend server list URL 2020-03-30 16:52:29 +08:00
Maddie Zhan
2c4cb90e66 Un-export ICMP failed field 2020-03-26 15:54:49 +08:00
Maddie Zhan
e56ed18c3b Also show sponsor info in server list 2020-03-26 15:38:50 +08:00
Maddie Zhan
9b7219a900 Print server sponsor when available 2020-03-26 15:03:29 +08:00
Maddie Zhan
9dc19eb3a3 Update README.md for clarity 2020-03-24 16:16:20 +08:00
Maddie Zhan
f0ed59d53f Update README.md for binary releases 2020-03-19 10:33:57 +08:00
Maddie Zhan
c3ad222d1a Add GoReleaser configuration 2020-03-19 10:26:52 +08:00
Maddie Zhan
0ebda01e7e Update README.md 2020-03-18 22:39:44 +08:00
Maddie Zhan
c250a39a15 Update options descriptions 2020-03-18 22:39:38 +08:00
Maddie Zhan
e61ed2ce5a Enable telemetry when any telemetry option is given 2020-03-18 21:28:01 +08:00
Maddie Zhan
5bb81358cf Use a variable to track current reader position
... instead of using `math.Mod`
2020-03-17 21:31:41 +08:00
Maddie Zhan
6b0fcc5acc Correctly reset upload reader 2020-03-17 21:22:49 +08:00
Maddie Zhan
eafdee6d31 Add --concurrent option and support concurrent requests 2020-03-17 17:45:34 +08:00
Maddie Zhan
799afff015 Fix mbps calculation 2020-03-13 18:16:34 +08:00
Maddie Zhan
55bc44329f Update README.md 2020-03-13 10:51:43 +08:00
Maddie Zhan
d5d4fb3389 Add option --mebibytes
Fixes #1
2020-03-13 10:26:21 +08:00
Maddie Zhan
9df434e8f2 Add instruction for installing from Homebrew 2020-03-11 15:50:05 +08:00
Maddie Zhan
8cafe323bc Update dependencies 2020-03-11 15:11:32 +08:00
Maddie Zhan
a4afee365f Update README.md 2020-03-11 12:48:06 +08:00
Maddie Zhan
5866efba70 Add force IPv4/IPv6 option 2020-03-11 11:55:36 +08:00
Maddie Zhan
30c5ffbb75 Add instruction for installing from AUR 2020-03-09 15:30:30 +08:00
18 changed files with 853 additions and 272 deletions

2
.gitignore vendored
View file

@ -130,3 +130,5 @@ fabric.properties
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
dist/

87
.goreleaser.yml Normal file
View file

@ -0,0 +1,87 @@
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
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
formats: ['zip']
files:
- LICENSE
checksum:
name_template: "checksums.txt"
changelog:
disable: false
sort: asc
release:
github:
owner: librespeed
name: speedtest-cli
disable: false

123
README.md
View file

@ -1,7 +1,7 @@
![LibreSpeed Logo](https://github.com/librespeed/speedtest/blob/master/.logo/logo3.png?raw=true) ![LibreSpeed Logo](https://github.com/librespeed/speedtest/blob/master/.logo/logo3.png?raw=true)
# LibreSpeed command line tool # 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! `librespeed-cli` comes to rescue!
@ -22,14 +22,19 @@ 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) [![asciicast](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2.svg)](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2)
## Requirements for compiling ## Requirements for compiling
- Go 1.14 - Go 1.18+
## Runtime requirements ## Runtime requirements
- Any [Go supported platforms](https://github.com/golang/go/wiki/MinimumRequirements) - Any [Go supported platforms](https://github.com/golang/go/wiki/MinimumRequirements)
## Use prebuilt binaries
If you don't want to build `librespeed-cli` yourself, you can find different binaries compiled for various platforms in
the [releases page](https://github.com/librespeed/speedtest-cli/releases).
## Building `librespeed-cli` ## Building `librespeed-cli`
1. First, you'll have to install Go. For Windows users, [you can download an installer from golang.org](https://golang.org/dl/). 1. First, you'll have to install Go (at least version 1.11). For Windows users, [you can download an installer from golang.org](https://golang.org/dl/).
For Linux users, you can use either the archive from golang.org, or install from your distribution's package manager. For Linux users, you can use either the archive from golang.org, or install from your distribution's package manager.
For example, Arch Linux: For example, Arch Linux:
@ -37,7 +42,7 @@ For Linux users, you can use either the archive from golang.org, or install from
```shell script ```shell script
# pacman -S go # pacman -S go
``` ```
2. Then, clone the repository: 2. Then, clone the repository:
```shell script ```shell script
@ -54,7 +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 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`. 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 ```shell script
# Let's say we're building for 64-bit Windows on Linux # Let's say we're building for 64-bit Windows on Linux
$ GOOS=windows GOARCH=amd64 ./build.sh $ GOOS=windows GOARCH=amd64 ./build.sh
@ -66,18 +74,68 @@ can now proceed to build `librespeed-cli` with the build script:
$ ls out $ ls out
librespeed-cli-windows-amd64.exe librespeed-cli-windows-amd64.exe
``` ```
5. Now you can use the `librespeed-cli` and test your Internet speed! 5. Now you can use the `librespeed-cli` and test your Internet speed!
## Install from AUR/Homebrew ## Install from AUR
TODO To install `librespeed-cli` from AUR, use your favorite AUR helper and install package `librespeed-cli-bin`
```shell script
$ yay librespeed-cli-bin
```
... or, clone it and build it yourself:
```shell script
$ git clone https://aur.archlinux.org/librespeed-cli-bin.git
$ cd librespeed-cli-bin
$ makepkg -si
```
## Install from Homebrew
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 ## Usage
You can see the full list of supported options with `librespeed-cli -h`: You can see the full list of supported options with `librespeed-cli -h`:
```shell script ```
$ librespeed-cli -h $ librespeed-cli -h
NAME: NAME:
librespeed-cli - Test your Internet speed with LibreSpeed 🚀 librespeed-cli - Test your Internet speed with LibreSpeed 🚀
@ -88,50 +146,67 @@ USAGE:
GLOBAL OPTIONS: GLOBAL OPTIONS:
--help, -h show help (default: false) --help, -h show help (default: false)
--version Show the version number and exit (default: false) --version Show the version number and exit (default: false)
--ipv4, -4 Force IPv4 only (default: false)
--ipv6, -6 Force IPv6 only (default: false)
--no-download Do not perform download test (default: false) --no-download Do not perform download test (default: false)
--no-upload Do not perform upload 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 --bytes Display values in bytes instead of bits. Does not affect
the image generated by --share, nor output from the image generated by --share, nor output from
--json or --csv (default: false) --json or --csv (default: false)
--mebibytes Use 1024 bytes as 1 kilobyte instead of 1000 (default: false)
--distance value Change distance unit shown in ISP info, use 'mi' for miles, --distance value Change distance unit shown in ISP info, use 'mi' for miles,
'km' for kilometres, 'NM' for nautical miles (default: "km") 'km' for kilometres, 'NM' for nautical miles (default: "km")
--share Generate and provide a URL to the LibreSpeed.org share results --share Generate and provide a URL to the LibreSpeed.org share results
image, not displayed with --csv (default: false) image, not displayed with --csv (default: false)
--simple Suppress verbose output, only show basic information --simple Suppress verbose output, only show basic information
(default: false) (default: false)
--csv Suppress verbose output, only show basic information in CSV --csv Suppress verbose output, only show basic information in CSV
format. Speeds listed in bit/s and not affected by --bytes 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-delimiter CSV_DELIMITER Single character delimiter (CSV_DELIMITER) to use in
CSV output. (default: ",") CSV output. (default: ",")
--csv-header Print CSV headers (default: false) --csv-header Print CSV headers (default: false)
--json Suppress verbose output, only show basic information --json Suppress verbose output, only show basic information
in JSON format. Speeds listed in bit/s and not 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) --list Display a list of LibreSpeed.org servers (default: false)
--server SERVER Specify a SERVER ID to test against. Can be supplied --server SERVER Specify a SERVER ID to test against. Can be supplied
multiple times. Cannot be used with --exclude multiple times. Cannot be used with --exclude
--exclude EXCLUDE EXCLUDE a server from selection. Can be supplied --exclude EXCLUDE EXCLUDE a server from selection. Can be supplied
multiple times. Cannot be used with --server multiple times. Cannot be used with --server
--server-json value Use an alternative server list from remote JSON file --server-json value Use an alternative server list from remote JSON file
--local-json value Use an alternative server list from local JSON file --local-json value Use an alternative server list from local JSON file,
--source SOURCE SOURCE IP address to bind to 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) --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 --secure Use HTTPS instead of HTTP when communicating with
LibreSpeed.org operated servers (default: false) 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 --no-pre-allocate Do not pre allocate upload data. Pre allocation is
enabled by default to improve upload performance. To enabled by default to improve upload performance. To
support systems with insufficient memory, use this support systems with insufficient memory, use this
option to avoid out of memory errors (default: false) option to avoid out of memory errors (default: false)
--telemetry-json value Load telemetry server settings from a JSON file. This --telemetry-json value Load telemetry server settings from a JSON file. This
options overrides --telemetry-level, --telemetry-server, options overrides --telemetry-level, --telemetry-server,
--telemetry-path, and --telemetry-share --telemetry-path, and --telemetry-share. Implies --share
--telemetry-level value Set telemetry data verbosity, available values are: --telemetry-level value Set telemetry data verbosity, available values are:
disabled, basic, full, debug disabled, basic, full, debug. Implies --share
--telemetry-server value Set the telemetry server base URL --telemetry-server value Set the telemetry server base URL. Implies --share
--telemetry-path value Set the telemetry upload path --telemetry-path value Set the telemetry upload path. Implies --share
--telemetry-share value Set the telemetry share link path --telemetry-share value Set the telemetry share link path. Implies --share
--telemetry-extra value Send a custom message along with the telemetry results --telemetry-extra value Send a custom message along with the telemetry results.
Implies --share
``` ```
## Use a custom backend server list ## Use a custom backend server list
@ -141,6 +216,7 @@ locally via `--local-json`). The format is as below:
```json ```json
[ [
{ {
"id": 1,
"name": "PHP Backend", "name": "PHP Backend",
"server": "https://example.com/", "server": "https://example.com/",
"dlURL": "garbage.php", "dlURL": "garbage.php",
@ -149,6 +225,7 @@ locally via `--local-json`). The format is as below:
"getIpURL": "getIP.php" "getIpURL": "getIP.php"
}, },
{ {
"id": 2,
"name": "Go Backend", "name": "Go Backend",
"server": "http://example.com/speedtest/", "server": "http://example.com/speedtest/",
"dlURL": "garbage", "dlURL": "garbage",
@ -159,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`), 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. `librespeed-cli` will use `http` by default, or `https` when the `--secure` option is enabled.
## Use a custom telemetry server ## Use a custom telemetry server
By default, the telemetry result will be sent to `librespeed.org` by default. 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 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: `--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 OUT_DIR=${CURRENT_DIR}/out
PROGNAME="librespeed-cli" PROGNAME="librespeed-cli"
DEFS_PATH="github.com/librespeed/speedtest-cli"
BINARY=${PROGNAME}-$(go env GOOS)-$(go env GOARCH) BINARY=${PROGNAME}-$(go env GOOS)-$(go env GOARCH)
BUILD_DATE=$(date -u "+%Y-%m-%d %H:%M:%S %Z") 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 if [[ -n "${GOARM}" ]] && [[ "${GOARM}" -gt 0 ]]; then
BINARY=${BINARY}v${GOARM} BINARY=${BINARY}v${GOARM}

View file

@ -6,21 +6,35 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"sync"
"time" "time"
) )
// BytesCounter implements io.Reader and io.Writer interface, for counting bytes being read/written in HTTP requests // BytesCounter implements io.Reader and io.Writer interface, for counting bytes being read/written in HTTP requests
type BytesCounter struct { type BytesCounter struct {
start time.Time start time.Time
total int pos int
payload []byte total uint64
reader io.ReadSeeker payload []byte
reader io.ReadSeeker
mebi bool
uploadSize int
lock *sync.Mutex
}
func NewCounter() *BytesCounter {
return &BytesCounter{
lock: &sync.Mutex{},
}
} }
// Write implements io.Writer // Write implements io.Writer
func (c *BytesCounter) Write(p []byte) (int, error) { func (c *BytesCounter) Write(p []byte) (int, error) {
n := len(p) n := len(p)
c.total += n c.lock.Lock()
c.total += uint64(n)
c.lock.Unlock()
return n, nil return n, nil
} }
@ -28,43 +42,71 @@ func (c *BytesCounter) Write(p []byte) (int, error) {
// Read implements io.Reader // Read implements io.Reader
func (c *BytesCounter) Read(p []byte) (int, error) { func (c *BytesCounter) Read(p []byte) (int, error) {
n, err := c.reader.Read(p) n, err := c.reader.Read(p)
c.total += n c.lock.Lock()
c.total += uint64(n)
c.pos += n
if c.pos == c.uploadSize {
c.resetReader()
}
c.lock.Unlock()
return n, err return n, err
} }
// Average returns the average bytes/second // SetBase sets the base for dividing bytes into megabyte or mebibyte
func (c *BytesCounter) Average() float64 { func (c *BytesCounter) SetMebi(mebi bool) {
c.mebi = mebi
}
// SetUploadSize sets the size of payload being uploaded
func (c *BytesCounter) SetUploadSize(uploadSize int) {
c.uploadSize = uploadSize * 1024
}
// AvgBytes returns the average bytes/second
func (c *BytesCounter) AvgBytes() float64 {
return float64(c.total) / time.Now().Sub(c.start).Seconds() return float64(c.total) / time.Now().Sub(c.start).Seconds()
} }
func (c *BytesCounter) AvgMbits() string { // AvgMbps returns the average mbits/second
return fmt.Sprintf("%.02f Mbps", c.Average()/131072) func (c *BytesCounter) AvgMbps() float64 {
var base float64 = 125000
if c.mebi {
base = 131072
}
return c.AvgBytes() / base
} }
// AvgHumanize returns the average bytes/kilobytes/megabytes/gigabytes (or bytes/kibibytes/mebibytes/gibibytes) per second
func (c *BytesCounter) AvgHumanize() string { func (c *BytesCounter) AvgHumanize() string {
val := c.Average() val := c.AvgBytes()
if val < 1024 { var base float64 = 1000
if c.mebi {
base = 1024
}
if val < base {
return fmt.Sprintf("%.2f bytes/s", val) return fmt.Sprintf("%.2f bytes/s", val)
} else if val/1024 < 1024 { } else if val/base < base {
return fmt.Sprintf("%.2f KB/s", val/1024) return fmt.Sprintf("%.2f KB/s", val/base)
} else if val/1024/1024 < 1024 { } else if val/base/base < base {
return fmt.Sprintf("%.2f MB/s", val/1024/1024) return fmt.Sprintf("%.2f MB/s", val/base/base)
} else { } else {
return fmt.Sprintf("%.2f GB/s", val/1024/1024/1024) return fmt.Sprintf("%.2f GB/s", val/base/base/base)
} }
} }
// GenerateBlob generates a random byte array of `uploadSize` in the `payload` field, and sets the `reader` field to // GenerateBlob generates a random byte array of `uploadSize` in the `payload` field, and sets the `reader` field to
// read from it // read from it
func (c *BytesCounter) GenerateBlob() { func (c *BytesCounter) GenerateBlob() {
c.payload = getRandomData(uploadSize) c.payload = getRandomData(c.uploadSize)
c.reader = bytes.NewReader(c.payload) c.reader = bytes.NewReader(c.payload)
} }
// ResetReader resets the `reader` field to 0 position // resetReader resets the `reader` field to 0 position
func (c *BytesCounter) ResetReader() (int64, error) { func (c *BytesCounter) resetReader() (int64, error) {
c.pos = 0
return c.reader.Seek(0, 0) return c.reader.Seek(0, 0)
} }
@ -74,7 +116,7 @@ func (c *BytesCounter) Start() {
} }
// Total returns the total bytes read/written // Total returns the total bytes read/written
func (c *BytesCounter) Total() int { func (c *BytesCounter) Total() uint64 {
return c.total return c.total
} }

View file

@ -1,12 +1,5 @@
package defs package defs
const (
// chunks to download in download test
downloadChunks = 100
// payload size per upload request
uploadSize = 1024 * 1024
)
var ( var (
// values to be filled in by build script // values to be filled in by build script
BuildDate string BuildDate string

View file

@ -2,9 +2,16 @@ package defs
const ( const (
OptionHelp = "help" OptionHelp = "help"
OptionIPv4 = "ipv4"
OptionIPv4Alt = "4"
OptionIPv6 = "ipv6"
OptionIPv6Alt = "6"
OptionNoDownload = "no-download" OptionNoDownload = "no-download"
OptionNoUpload = "no-upload" OptionNoUpload = "no-upload"
OptionNoICMP = "no-icmp"
OptionConcurrent = "concurrent"
OptionBytes = "bytes" OptionBytes = "bytes"
OptionMebiBytes = "mebibytes"
OptionDistance = "distance" OptionDistance = "distance"
OptionShare = "share" OptionShare = "share"
OptionSimple = "simple" OptionSimple = "simple"
@ -17,8 +24,14 @@ const (
OptionExclude = "exclude" OptionExclude = "exclude"
OptionServerJSON = "server-json" OptionServerJSON = "server-json"
OptionSource = "source" OptionSource = "source"
OptionInterface = "interface"
OptionTimeout = "timeout" OptionTimeout = "timeout"
OptionChunks = "chunks"
OptionUploadSize = "upload-size"
OptionDuration = "duration"
OptionSecure = "secure" OptionSecure = "secure"
OptionCACert = "ca-cert"
OptionSkipCertVerify = "skip-cert-verify"
OptionNoPreAllocate = "no-pre-allocate" OptionNoPreAllocate = "no-pre-allocate"
OptionVersion = "version" OptionVersion = "version"
OptionLocalJSON = "local-json" OptionLocalJSON = "local-json"
@ -29,4 +42,5 @@ const (
OptionTelemetryPath = "telemetry-path" OptionTelemetryPath = "telemetry-path"
OptionTelemetryShare = "telemetry-share" OptionTelemetryShare = "telemetry-share"
OptionTelemetryExtra = "telemetry-extra" OptionTelemetryExtra = "telemetry-extra"
OptionFwmark = "fwmark"
) )

View file

@ -16,20 +16,24 @@ import (
"time" "time"
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/go-ping/ping"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/sparrc/go-ping"
) )
// Server represents a speed test server // Server represents a speed test server
type Server struct { type Server struct {
Name string `json:"name"` ID int `json:"id"`
Server string `json:"server"` Name string `json:"name"`
DownloadURL string `json:"dlURL"` Server string `json:"server"`
UploadURL string `json:"ulURL"` DownloadURL string `json:"dlURL"`
PingURL string `json:"pingURL"` UploadURL string `json:"ulURL"`
GetIPURL string `json:"getIpURL"` PingURL string `json:"pingURL"`
ICMPFail bool `json:"-"` GetIPURL string `json:"getIpURL"`
TLog TelemetryLog `json:"-"` SponsorName string `json:"sponsorName"`
SponsorURL string `json:"sponsorURL"`
NoICMP bool `json:"-"`
TLog TelemetryLog `json:"-"`
} }
// IsUp checks the speed test backend is up by accessing the ping URL // IsUp checks the speed test backend is up by accessing the ping URL
@ -41,25 +45,37 @@ func (s *Server) IsUp() bool {
u, _ := s.GetURL() u, _ := s.GetURL()
u.Path = path.Join(u.Path, s.PingURL) u.Path = path.Join(u.Path, s.PingURL)
resp, err := http.Get(u.String())
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil { if err != nil {
log.Debugf("Failed when creating HTTP request: %s", err)
return false
}
req.Header.Set("User-Agent", UserAgent)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Debugf("Error checking for server status: %s", err)
return false return false
} }
defer resp.Body.Close() defer resp.Body.Close()
b, _ := ioutil.ReadAll(resp.Body) 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 // 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 // ICMPPingAndJitter pings the server via ICMP echos and calculate the average ping and jitter
func (s *Server) ICMPPingAndJitter(count int, srcIp string) (float64, float64, error) { func (s *Server) ICMPPingAndJitter(count int, srcIp, network string) (float64, float64, error) {
t := time.Now() t := time.Now()
defer func() { defer func() {
s.TLog.Logf("ICMP ping took %s", time.Now().Sub(t).String()) s.TLog.Logf("ICMP ping took %s", time.Now().Sub(t).String())
}() }()
if s.ICMPFail { if s.NoICMP {
log.Debug("ICMP ping failed already, using HTTP ping") log.Debugf("Skipping ICMP for server %s, will use HTTP ping", s.Name)
return s.PingAndJitter(count + 2) return s.PingAndJitter(count + 2)
} }
@ -68,12 +84,9 @@ func (s *Server) ICMPPingAndJitter(count int, srcIp string) (float64, float64, e
log.Debugf("Failed to get server URL: %s", err) log.Debugf("Failed to get server URL: %s", err)
return 0, 0, err return 0, 0, err
} }
p, err := ping.NewPinger(u.Hostname())
if err != nil {
log.Debugf("Failed to initialize pinger: %s", err)
return 0, 0, err
}
p := ping.New(u.Hostname())
p.SetNetwork(network)
p.Count = count p.Count = count
p.Timeout = time.Duration(count) * time.Second p.Timeout = time.Duration(count) * time.Second
if srcIp != "" { if srcIp != "" {
@ -106,7 +119,7 @@ func (s *Server) ICMPPingAndJitter(count int, srcIp string) (float64, float64, e
} }
if len(stats.Rtts) == 0 { if len(stats.Rtts) == 0 {
s.ICMPFail = true s.NoICMP = true
log.Debugf("No ICMP pings returned for server %s (%s), trying TCP ping", s.Name, u.Hostname()) log.Debugf("No ICMP pings returned for server %s (%s), trying TCP ping", s.Name, u.Hostname())
return s.PingAndJitter(count + 2) return s.PingAndJitter(count + 2)
} }
@ -114,7 +127,7 @@ func (s *Server) ICMPPingAndJitter(count int, srcIp string) (float64, float64, e
return float64(stats.AvgRtt.Milliseconds()), jitter, nil return float64(stats.AvgRtt.Milliseconds()), jitter, nil
} }
// ICMPPingAndJitter pings the server via accessing ping URL and calculate the average ping and jitter // PingAndJitter pings the server via accessing ping URL and calculate the average ping and jitter
func (s *Server) PingAndJitter(count int) (float64, float64, error) { func (s *Server) PingAndJitter(count int) (float64, float64, error) {
t := time.Now() t := time.Now()
defer func() { defer func() {
@ -157,9 +170,9 @@ func (s *Server) PingAndJitter(count int) (float64, float64, error) {
} }
var lastPing, jitter float64 var lastPing, jitter float64
for idx, ping := range pings { for idx, p := range pings {
if idx != 0 { if idx != 0 {
instJitter := math.Abs(lastPing - ping) instJitter := math.Abs(lastPing - p)
if idx > 1 { if idx > 1 {
if jitter > instJitter { if jitter > instJitter {
jitter = jitter*0.7 + instJitter*0.3 jitter = jitter*0.7 + instJitter*0.3
@ -168,20 +181,21 @@ func (s *Server) PingAndJitter(count int) (float64, float64, error) {
} }
} }
} }
lastPing = ping lastPing = p
} }
return getAvg(pings), jitter, nil return getAvg(pings), jitter, nil
} }
// Download performs the actual download test // Download performs the actual download test
func (s *Server) Download(silent bool, useBytes bool) (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() t := time.Now()
defer func() { defer func() {
s.TLog.Logf("Download took %s", time.Now().Sub(t).String()) s.TLog.Logf("Download took %s", time.Now().Sub(t).String())
}() }()
counter := &BytesCounter{} counter := NewCounter()
counter.SetMebi(useMebi)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -199,12 +213,12 @@ func (s *Server) Download(silent bool, useBytes bool) (float64, int, error) {
return 0, 0, err return 0, 0, err
} }
q := req.URL.Query() q := req.URL.Query()
q.Set("ckSize", strconv.Itoa(downloadChunks)) q.Set("ckSize", strconv.Itoa(chunks))
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
req.Header.Set("User-Agent", UserAgent) req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Accept-Encoding", "identity") req.Header.Set("Accept-Encoding", "identity")
downloadDone := make(chan struct{}) downloadDone := make(chan struct{}, requests)
doDownload := func() { doDownload := func() {
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
@ -214,7 +228,7 @@ func (s *Server) Download(silent bool, useBytes bool) (float64, int, error) {
defer resp.Body.Close() defer resp.Body.Close()
if _, err = io.Copy(ioutil.Discard, io.TeeReader(resp.Body, counter)); err != nil { if _, err = io.Copy(ioutil.Discard, io.TeeReader(resp.Body, counter)); err != nil {
if !errors.Is(err, context.Canceled) { if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
log.Debugf("Failed when reading HTTP response: %s", err) log.Debugf("Failed when reading HTTP response: %s", err)
} }
} }
@ -231,7 +245,7 @@ func (s *Server) Download(silent bool, useBytes bool) (float64, int, error) {
if useBytes { if useBytes {
s.Suffix = fmt.Sprintf(" %s", counter.AvgHumanize()) s.Suffix = fmt.Sprintf(" %s", counter.AvgHumanize())
} else { } else {
s.Suffix = fmt.Sprintf(" %s", counter.AvgMbits()) s.Suffix = fmt.Sprintf(" %.2f Mbps", counter.AvgMbps())
} }
} }
@ -240,14 +254,17 @@ func (s *Server) Download(silent bool, useBytes bool) (float64, int, error) {
if useBytes { if useBytes {
pb.FinalMSG = fmt.Sprintf("Download rate:\t%s\n", counter.AvgHumanize()) pb.FinalMSG = fmt.Sprintf("Download rate:\t%s\n", counter.AvgHumanize())
} else { } else {
pb.FinalMSG = fmt.Sprintf("Download rate:\t%s\n", counter.AvgMbits()) pb.FinalMSG = fmt.Sprintf("Download rate:\t%.2f Mbps\n", counter.AvgMbps())
} }
pb.Stop() pb.Stop()
}() }()
} }
go doDownload() for i := 0; i < requests; i++ {
timeout := time.After(15 * time.Second) go doDownload()
time.Sleep(200 * time.Millisecond)
}
timeout := time.After(duration)
Loop: Loop:
for { for {
select { select {
@ -259,17 +276,19 @@ Loop:
} }
} }
return counter.Average() / 131072, counter.Total(), nil return counter.AvgMbps(), counter.Total(), nil
} }
// Upload performs the actual upload test // Upload performs the actual upload test
func (s *Server) Upload(noPrealloc, silent, useBytes bool) (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() t := time.Now()
defer func() { defer func() {
s.TLog.Logf("Upload took %s", time.Now().Sub(t).String()) s.TLog.Logf("Upload took %s", time.Now().Sub(t).String())
}() }()
counter := &BytesCounter{} counter := NewCounter()
counter.SetMebi(useMebi)
counter.SetUploadSize(uploadSize)
if noPrealloc { if noPrealloc {
log.Info("Pre-allocation is disabled, performance might be lower!") log.Info("Pre-allocation is disabled, performance might be lower!")
@ -295,11 +314,11 @@ func (s *Server) Upload(noPrealloc, silent, useBytes bool) (float64, int, error)
req.Header.Set("User-Agent", UserAgent) req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Accept-Encoding", "identity") req.Header.Set("Accept-Encoding", "identity")
uploadDone := make(chan struct{}) uploadDone := make(chan struct{}, requests)
doUpload := func() { doUpload := func() {
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil && !errors.Is(err, context.Canceled) { if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
log.Debugf("Failed when making HTTP request: %s", err) log.Debugf("Failed when making HTTP request: %s", err)
} else if err == nil { } else if err == nil {
defer resp.Body.Close() defer resp.Body.Close()
@ -307,7 +326,6 @@ func (s *Server) Upload(noPrealloc, silent, useBytes bool) (float64, int, error)
log.Debugf("Failed when reading HTTP response: %s", err) log.Debugf("Failed when reading HTTP response: %s", err)
} }
counter.ResetReader()
uploadDone <- struct{}{} uploadDone <- struct{}{}
} }
} }
@ -320,7 +338,7 @@ func (s *Server) Upload(noPrealloc, silent, useBytes bool) (float64, int, error)
if useBytes { if useBytes {
s.Suffix = fmt.Sprintf(" %s", counter.AvgHumanize()) s.Suffix = fmt.Sprintf(" %s", counter.AvgHumanize())
} else { } else {
s.Suffix = fmt.Sprintf(" %s", counter.AvgMbits()) s.Suffix = fmt.Sprintf(" %.2f Mbps", counter.AvgMbps())
} }
} }
@ -329,14 +347,17 @@ func (s *Server) Upload(noPrealloc, silent, useBytes bool) (float64, int, error)
if useBytes { if useBytes {
pb.FinalMSG = fmt.Sprintf("Upload rate:\t%s\n", counter.AvgHumanize()) pb.FinalMSG = fmt.Sprintf("Upload rate:\t%s\n", counter.AvgHumanize())
} else { } else {
pb.FinalMSG = fmt.Sprintf("Upload rate:\t%s\n", counter.AvgMbits()) pb.FinalMSG = fmt.Sprintf("Upload rate:\t%.2f Mbps\n", counter.AvgMbps())
} }
pb.Stop() pb.Stop()
}() }()
} }
go doUpload() for i := 0; i < requests; i++ {
timeout := time.After(15 * time.Second) go doUpload()
time.Sleep(200 * time.Millisecond)
}
timeout := time.After(duration)
Loop: Loop:
for { for {
select { select {
@ -348,7 +369,7 @@ Loop:
} }
} }
return counter.Average() / 131072, counter.Total(), nil return counter.AvgMbps(), counter.Total(), nil
} }
// GetIPInfo accesses the backend's getIP.php endpoint and get current client's IP information // GetIPInfo accesses the backend's getIP.php endpoint and get current client's IP information
@ -375,6 +396,8 @@ func (s *Server) GetIPInfo(distanceUnit string) (*GetIPResult, error) {
log.Debugf("Failed when creating HTTP request: %s", err) log.Debugf("Failed when creating HTTP request: %s", err)
return nil, err return nil, err
} }
req.Header.Set("User-Agent", UserAgent)
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
log.Debugf("Failed when making HTTP request: %s", err) log.Debugf("Failed when making HTTP request: %s", err)
@ -392,6 +415,7 @@ func (s *Server) GetIPInfo(distanceUnit string) (*GetIPResult, error) {
if err := json.Unmarshal(b, &ipInfo); err != nil { if err := json.Unmarshal(b, &ipInfo); err != nil {
log.Debugf("Failed when parsing get IP result: %s", err) log.Debugf("Failed when parsing get IP result: %s", err)
log.Debugf("Received payload: %s", b) log.Debugf("Received payload: %s", b)
ipInfo.ProcessedString = string(b[:])
} }
} }
@ -412,3 +436,24 @@ func (s *Server) GetURL() (*url.URL, error) {
} }
return u, nil return u, nil
} }
// Sponsor returns the sponsor's info
func (s *Server) Sponsor() string {
var sponsorMsg string
if s.SponsorName != "" {
sponsorMsg += s.SponsorName
if s.SponsorURL != "" {
su, err := url.Parse(s.SponsorURL)
if err != nil {
log.Debugf("Sponsor URL is invalid: %s", s.SponsorURL)
} else {
if su.Scheme == "" {
su.Scheme = "https"
}
sponsorMsg += " @ " + su.String()
}
}
}
return sponsorMsg
}

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"]

32
go.mod
View file

@ -1,15 +1,27 @@
module librespeed-cli module github.com/librespeed/speedtest-cli
go 1.14 go 1.23.0
replace github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c => github.com/maddie/go-ping v0.0.0-20200305135031-f8c069280206 toolchain go1.24.2
require ( require (
github.com/briandowns/spinner v1.9.0 github.com/briandowns/spinner v1.23.1
github.com/gocarina/gocsv v0.0.0-20200302151839-87c60d755c58 github.com/go-ping/ping v1.2.0
github.com/sirupsen/logrus v1.4.2 github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.3.0 // indirect github.com/urfave/cli/v2 v2.27.4
github.com/urfave/cli/v2 v2.1.1 golang.org/x/sys v0.33.0
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect )
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
) )

90
go.sum
View file

@ -1,48 +1,56 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650=
github.com/briandowns/spinner v1.9.0 h1:+OMAisemaHar1hjuJ3Z2hIvNhQl9Y7GLPWUwwz2Pxo8= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
github.com/briandowns/spinner v1.9.0/go.mod h1://Zf9tMcxfRUA36V23M6YGEAv+kECGfvpnLTnb8n4XQ= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/gocarina/gocsv v0.0.0-20200302151839-87c60d755c58 h1:rRQm5os6ffGTukb42ZgKWOFfdFrocrNkLcjPYRP1tm0= github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ=
github.com/gocarina/gocsv v0.0.0-20200302151839-87c60d755c58/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/maddie/go-ping v0.0.0-20200305135031-f8c069280206 h1:wcRUWd5aN1arYHnxsAXx56VEV9RKQxVay24fKBv0s8M= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/maddie/go-ping v0.0.0-20200305135031-f8c069280206/go.mod h1:IYhVIzcOSIO1fLDdEz4JFidYhLPmmW3dchKuengmL9s= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c h1:gqEdF4VwBu3lTKGHS9rXE9x1/pEaSwCXRLOZRF6qtlw=
github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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/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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/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=

86
main.go
View file

@ -6,8 +6,8 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"librespeed-cli/defs" "github.com/librespeed/speedtest-cli/defs"
"librespeed-cli/speedtest" "github.com/librespeed/speedtest-cli/speedtest"
) )
// init sets up the essential bits on start up // init sets up the essential bits on start up
@ -20,6 +20,7 @@ func init() {
// warn level is for suppress modes // warn level is for suppress modes
// error level is for errors // error level is for errors
log.SetOutput(os.Stderr)
log.SetFormatter(formatter) log.SetFormatter(formatter)
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
} }
@ -28,7 +29,7 @@ func main() {
// define cli options // define cli options
app := &cli.App{ app := &cli.App{
Name: "librespeed-cli", Name: "librespeed-cli",
Usage: "Test your Internet speed with LibreSpeed 🚀", Usage: "Test your Internet speed with LibreSpeed",
Action: speedtest.SpeedTest, Action: speedtest.SpeedTest,
HideHelp: true, HideHelp: true,
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -37,6 +38,16 @@ func main() {
Name: defs.OptionVersion, Name: defs.OptionVersion,
Usage: "Show the version number and exit", Usage: "Show the version number and exit",
}, },
&cli.BoolFlag{
Name: defs.OptionIPv4,
Aliases: []string{defs.OptionIPv4Alt},
Usage: "Force IPv4 only",
},
&cli.BoolFlag{
Name: defs.OptionIPv6,
Aliases: []string{defs.OptionIPv6Alt},
Usage: "Force IPv6 only",
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: defs.OptionNoDownload, Name: defs.OptionNoDownload,
Usage: "Do not perform download test", Usage: "Do not perform download test",
@ -45,12 +56,26 @@ func main() {
Name: defs.OptionNoUpload, Name: defs.OptionNoUpload,
Usage: "Do not perform upload test", Usage: "Do not perform upload test",
}, },
&cli.BoolFlag{
Name: defs.OptionNoICMP,
Usage: "Do not use ICMP ping. ICMP doesn't work well under Linux\n" +
"\tat this moment, so you might want to disable it",
},
&cli.IntFlag{
Name: defs.OptionConcurrent,
Usage: "Concurrent HTTP requests being made",
Value: 3,
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: defs.OptionBytes, Name: defs.OptionBytes,
Usage: "Display values in bytes instead of bits. Does not affect\n" + Usage: "Display values in bytes instead of bits. Does not affect\n" +
"\tthe image generated by --share, nor output from\n" + "\tthe image generated by --share, nor output from\n" +
"\t--json or --csv", "\t--json or --csv",
}, },
&cli.BoolFlag{
Name: defs.OptionMebiBytes,
Usage: "Use 1024 bytes as 1 kilobyte instead of 1000",
},
&cli.StringFlag{ &cli.StringFlag{
Name: defs.OptionDistance, Name: defs.OptionDistance,
Usage: "Change distance unit shown in ISP info, use 'mi' for miles,\n" + Usage: "Change distance unit shown in ISP info, use 'mi' for miles,\n" +
@ -85,7 +110,7 @@ func main() {
Name: defs.OptionJSON, Name: defs.OptionJSON,
Usage: "Suppress verbose output, only show basic information\n" + Usage: "Suppress verbose output, only show basic information\n" +
"\tin JSON format. Speeds listed in bit/s and not\n" + "\tin JSON format. Speeds listed in bit/s and not\n" +
"\t affected by --bytes", "\taffected by --bytes",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: defs.OptionList, Name: defs.OptionList,
@ -106,23 +131,52 @@ func main() {
Usage: "Use an alternative server list from remote JSON file", Usage: "Use an alternative server list from remote JSON file",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: defs.OptionLocalJSON, Name: defs.OptionLocalJSON,
Usage: "Use an alternative server list from local JSON file", Usage: "Use an alternative server list from local JSON file,\n" +
"\tor read from stdin with \"--" + defs.OptionLocalJSON + " -\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: defs.OptionSource, Name: defs.OptionSource,
Usage: "`SOURCE` IP address to bind to", Usage: "`SOURCE` IP address to bind to",
}, },
&cli.StringFlag{
Name: defs.OptionInterface,
Usage: "network INTERFACE to bind to",
},
&cli.IntFlag{ &cli.IntFlag{
Name: defs.OptionTimeout, Name: defs.OptionTimeout,
Usage: "HTTP `TIMEOUT` in seconds.", Usage: "HTTP `TIMEOUT` in seconds.",
Value: 15, Value: 15,
}, },
&cli.IntFlag{
Name: defs.OptionDuration,
Usage: "Upload and download test duration in seconds",
Value: 15,
},
&cli.IntFlag{
Name: defs.OptionChunks,
Usage: "Chunks to download from server, chunk size depends on server configuration",
Value: 100,
},
&cli.IntFlag{
Name: defs.OptionUploadSize,
Usage: "Size of payload being uploaded in KiB",
Value: 1024,
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: defs.OptionSecure, Name: defs.OptionSecure,
Usage: "Use HTTPS instead of HTTP when communicating with\n" + Usage: "Use HTTPS instead of HTTP when communicating with\n" +
"\tLibreSpeed.org operated servers", "\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)",
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: defs.OptionNoPreAllocate, Name: defs.OptionNoPreAllocate,
Usage: "Do not pre allocate upload data. Pre allocation is\n" + Usage: "Do not pre allocate upload data. Pre allocation is\n" +
@ -140,28 +194,34 @@ func main() {
Name: defs.OptionTelemetryJSON, Name: defs.OptionTelemetryJSON,
Usage: "Load telemetry server settings from a JSON file. This\n" + Usage: "Load telemetry server settings from a JSON file. This\n" +
"\toptions overrides --" + defs.OptionTelemetryLevel + ", --" + defs.OptionTelemetryServer + ",\n" + "\toptions overrides --" + defs.OptionTelemetryLevel + ", --" + defs.OptionTelemetryServer + ",\n" +
"\t--" + defs.OptionTelemetryPath + ", and --" + defs.OptionTelemetryShare, "\t--" + defs.OptionTelemetryPath + ", and --" + defs.OptionTelemetryShare + ". Implies --" + defs.OptionShare,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: defs.OptionTelemetryLevel, Name: defs.OptionTelemetryLevel,
Usage: "Set telemetry data verbosity, available values are:\n" + Usage: "Set telemetry data verbosity, available values are:\n" +
"\tdisabled, basic, full, debug", "\tdisabled, basic, full, debug. Implies --" + defs.OptionShare,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: defs.OptionTelemetryServer, Name: defs.OptionTelemetryServer,
Usage: "Set the telemetry server base URL", Usage: "Set the telemetry server base URL. Implies --" + defs.OptionShare,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: defs.OptionTelemetryPath, Name: defs.OptionTelemetryPath,
Usage: "Set the telemetry upload path", Usage: "Set the telemetry upload path. Implies --" + defs.OptionShare,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: defs.OptionTelemetryShare, Name: defs.OptionTelemetryShare,
Usage: "Set the telemetry share link path", Usage: "Set the telemetry share link path. Implies --" + defs.OptionShare,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: defs.OptionTelemetryExtra, Name: defs.OptionTelemetryExtra,
Usage: "Send a custom message along with the telemetry results", 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 ( import (
"time" "time"
"librespeed-cli/defs" "github.com/librespeed/speedtest-cli/defs"
) )
// JSONReport represents the output data fields in a JSON file // JSONReport represents the output data fields in a JSON file
@ -11,8 +11,8 @@ type JSONReport struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Server Server `json:"server"` Server Server `json:"server"`
Client Client `json:"client"` Client Client `json:"client"`
BytesSent int `json:"bytes_sent"` BytesSent uint64 `json:"bytes_sent"`
BytesReceived int `json:"bytes_received"` BytesReceived uint64 `json:"bytes_received"`
Ping float64 `json:"ping"` Ping float64 `json:"ping"`
Jitter float64 `json:"jitter"` Jitter float64 `json:"jitter"`
Upload float64 `json:"upload"` Upload float64 `json:"upload"`

View file

@ -8,17 +8,17 @@ import (
"math" "math"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/gocarina/gocsv" "github.com/gocarina/gocsv"
"github.com/librespeed/speedtest-cli/defs"
"github.com/librespeed/speedtest-cli/report"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"librespeed-cli/defs"
"librespeed-cli/report"
) )
const ( const (
@ -27,11 +27,14 @@ const (
) )
// doSpeedTest is where the actual speed test happens // doSpeedTest is where the actual speed test happens
func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.TelemetryServer, 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 { if serverCount := len(servers); serverCount > 1 {
log.Infof("Testing against %d servers", serverCount) log.Infof("Testing against %d servers", serverCount)
} }
var reps_json []report.JSONReport
var reps_csv []report.CSVReport
// fetch current user's IP info // fetch current user's IP info
for _, currentServer := range servers { for _, currentServer := range servers {
// get telemetry level // get telemetry level
@ -45,6 +48,10 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
log.Infof("Selected server: %s [%s]", currentServer.Name, u.Hostname()) log.Infof("Selected server: %s [%s]", currentServer.Name, u.Hostname())
if sponsorMsg := currentServer.Sponsor(); sponsorMsg != "" {
log.Infof("Sponsored by: %s", sponsorMsg)
}
if currentServer.IsUp() { if currentServer.IsUp() {
ispInfo, err := currentServer.GetIPInfo(c.String(defs.OptionDistance)) ispInfo, err := currentServer.GetIPInfo(c.String(defs.OptionDistance))
if err != nil { if err != nil {
@ -61,59 +68,63 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
pb.Start() pb.Start()
} }
p, jitter, err := currentServer.ICMPPingAndJitter(pingCount, c.String(defs.OptionSource)) // skip ICMP if option given
currentServer.NoICMP = noICMP
p, jitter, err := currentServer.ICMPPingAndJitter(pingCount, c.String(defs.OptionSource), network)
if err != nil { if err != nil {
log.Errorf("Failed to get ping and jitter: %s", err) log.Errorf("Failed to get ping and jitter: %s", err)
return err return err
} }
if pb != nil { 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() pb.Stop()
} }
// get download value // get download value
var downloadValue float64 var downloadValue float64
var bytesRead int var bytesRead uint64
if c.Bool(defs.OptionNoDownload) { if c.Bool(defs.OptionNoDownload) {
log.Info("Download test is disabled") log.Info("Download test is disabled")
} else { } else {
download, br, err := currentServer.Download(silent, c.Bool(defs.OptionBytes)) 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 { if err != nil {
log.Errorf("Failed to get download speed: %s", err) log.Errorf("Failed to get download speed: %s", err)
return err return err
} }
downloadValue = download downloadValue = download
bytesRead = br bytesRead = uint64(br)
} }
// get upload value // get upload value
var uploadValue float64 var uploadValue float64
var bytesWritten int var bytesWritten uint64
if c.Bool(defs.OptionNoUpload) { if c.Bool(defs.OptionNoUpload) {
log.Info("Upload test is disabled") log.Info("Upload test is disabled")
} else { } else {
upload, bw, err := currentServer.Upload(c.Bool(defs.OptionNoPreAllocate), silent, c.Bool(defs.OptionBytes)) 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 { if err != nil {
log.Errorf("Failed to get upload speed: %s", err) log.Errorf("Failed to get upload speed: %s", err)
return err return err
} }
uploadValue = upload uploadValue = upload
bytesWritten = bw bytesWritten = uint64(bw)
} }
// print result if --simple is given // print result if --simple is given
if c.Bool(defs.OptionSimple) { if c.Bool(defs.OptionSimple) {
if c.Bool(defs.OptionBytes) { if c.Bool(defs.OptionBytes) {
log.Warnf("Ping:\t%.0f ms\tJitter:\t%.0f ms\nDownload rate:\t%s\nUpload rate:\t%s", p, jitter, humanizeMbps(downloadValue), humanizeMbps(uploadValue)) useMebi := c.Bool(defs.OptionMebiBytes)
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 { } 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)
} }
} }
// print share link if --share is given // print share link if --share is given
var shareLink string var shareLink string
if c.Bool(defs.OptionShare) && !c.Bool(defs.OptionCSV) && telemetryServer.GetLevel() > 0 { if telemetryServer.GetLevel() > 0 {
var extra defs.TelemetryExtra var extra defs.TelemetryExtra
extra.ServerName = currentServer.Name extra.ServerName = currentServer.Name
extra.Extra = c.String(defs.OptionTelemetryExtra) extra.Extra = c.String(defs.OptionTelemetryExtra)
@ -122,41 +133,35 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
log.Errorf("Error when sending telemetry data: %s", err) log.Errorf("Error when sending telemetry data: %s", err)
} else { } else {
shareLink = link shareLink = link
log.Warnf("Share your result: %s", link) // only print to stdout when --json and --csv are not used
if !c.Bool(defs.OptionJSON) && !c.Bool(defs.OptionCSV) {
log.Warnf("Share your result: %s", link)
}
} }
} }
// check for --csv or --json. the program prioritize the --csv before the --json. this is the same behavior as speedtest-cli // 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) { if c.Bool(defs.OptionCSV) {
// print csv if --csv is given // print csv if --csv is given
var reps []report.CSVReport
var rep report.CSVReport var rep report.CSVReport
rep.Timestamp = time.Now() rep.Timestamp = time.Now()
rep.Name = currentServer.Name rep.Name = currentServer.Name
rep.Address = u.String() rep.Address = u.String()
rep.Ping = p rep.Ping = math.Round(p*100) / 100
rep.Jitter = math.Round(jitter*100) / 100 rep.Jitter = math.Round(jitter*100) / 100
rep.Download = math.Round(downloadValue*100) / 100 rep.Download = math.Round(downloadValue*100) / 100
rep.Upload = math.Round(uploadValue*100) / 100 rep.Upload = math.Round(uploadValue*100) / 100
rep.Share = shareLink rep.Share = shareLink
rep.IP = ispInfo.RawISPInfo.IP rep.IP = ispInfo.RawISPInfo.IP
reps = append(reps, rep) reps_csv = append(reps_csv, 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())
}
} else if c.Bool(defs.OptionJSON) { } else if c.Bool(defs.OptionJSON) {
// print json if --json is given // print json if --json is given
var rep report.JSONReport var rep report.JSONReport
rep.Timestamp = time.Now() rep.Timestamp = time.Now()
rep.Ping = p rep.Ping = math.Round(p*100) / 100
rep.Jitter = math.Round(jitter*100) / 100 rep.Jitter = math.Round(jitter*100) / 100
rep.Download = math.Round(downloadValue*100) / 100 rep.Download = math.Round(downloadValue*100) / 100
rep.Upload = math.Round(uploadValue*100) / 100 rep.Upload = math.Round(uploadValue*100) / 100
@ -170,22 +175,34 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
rep.Client = report.Client{ispInfo.RawISPInfo} rep.Client = report.Client{ispInfo.RawISPInfo}
rep.Client.Readme = "" rep.Client.Readme = ""
if b, err := json.Marshal(&rep); err != nil { reps_json = append(reps_json, rep)
log.Errorf("Error generating JSON report: %s", err)
} else {
log.Warnf("%s", b)
}
} }
} else { } else {
log.Infof("Selected server %s (%s) is not responding at the moment, try again later", currentServer.Name, u.Hostname()) 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 //add a new line after each test if testing multiple servers
if len(servers) > 1 { if len(servers) > 1 && !silent {
log.Warn() 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 return nil
} }
@ -222,7 +239,7 @@ func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResu
if fPing, err := wr.CreateFormField("ping"); err != nil { if fPing, err := wr.CreateFormField("ping"); err != nil {
log.Debugf("Error creating form field: %s", err) log.Debugf("Error creating form field: %s", err)
return "", 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) log.Debugf("Error writing form field: %s", err)
return "", err return "", err
} }
@ -230,7 +247,7 @@ func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResu
if fJitter, err := wr.CreateFormField("jitter"); err != nil { if fJitter, err := wr.CreateFormField("jitter"); err != nil {
log.Debugf("Error creating form field: %s", err) log.Debugf("Error creating form field: %s", err)
return "", 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) log.Debugf("Error writing form field: %s", err)
return "", err return "", err
} }
@ -299,16 +316,21 @@ func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResu
} }
} }
func humanizeMbps(mbps float64) string { func humanizeMbps(mbps float64, useMebi bool) string {
val := mbps / 8 val := mbps / 8
var base float64 = 1000
if useMebi {
base = 1024
}
if val < 1 { if val < 1 {
if kb := val * 1024; kb < 1 { if kb := val * base; kb < 1 {
return fmt.Sprintf("%.2f bytes/s", kb*1024) return fmt.Sprintf("%.2f bytes/s", kb*base)
} else { } else {
return fmt.Sprintf("%.2f KB/s", kb) return fmt.Sprintf("%.2f KB/s", kb)
} }
} else if val > 1024 { } else if val > base {
return fmt.Sprintf("%.2f GB/s", val/1024) return fmt.Sprintf("%.2f GB/s", val/base)
} else { } else {
return fmt.Sprintf("%.2f MB/s", val) return fmt.Sprintf("%.2f MB/s", val)
} }

View file

@ -1,11 +1,18 @@
package speedtest package speedtest
import ( import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"os"
"strings"
"sync" "sync"
"time" "time"
@ -13,18 +20,18 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"librespeed-cli/defs" "github.com/librespeed/speedtest-cli/defs"
"librespeed-cli/report" "github.com/librespeed/speedtest-cli/report"
) )
const ( const (
// serverListUrl is the default remote server JSON URL // serverListUrl is the default remote server JSON URL
serverListUrl = `https://librespeed.org/backend-servers/servers.json` serverListUrl = `https://librespeed.org/backend-servers/servers.php`
defaultTelemetryLevel = "basic" defaultTelemetryLevel = "basic"
defaultTelemetryServer = "https://librespeed.org" defaultTelemetryServer = "https://librespeed.org"
defaultTelemetryPath = "/results/telemetry.php" defaultTelemetryPath = "/results/telemetry.php"
defaultTeleemtryShare = "/results/" defaultTelemetryShare = "/results/"
) )
type PingJob struct { type PingJob struct {
@ -58,6 +65,7 @@ func SpeedTest(c *cli.Context) error {
// print version // print version
if c.Bool(defs.OptionVersion) { if c.Bool(defs.OptionVersion) {
log.SetOutput(os.Stdout)
log.Warnf("%s %s (built on %s)", defs.ProgName, defs.ProgVersion, defs.BuildDate) log.Warnf("%s %s (built on %s)", defs.ProgName, defs.ProgVersion, defs.BuildDate)
log.Warn("https://github.com/librespeed/speedtest-cli") log.Warn("https://github.com/librespeed/speedtest-cli")
log.Warn("Licensed under GNU Lesser General Public License v3.0") log.Warn("Licensed under GNU Lesser General Public License v3.0")
@ -67,6 +75,10 @@ func SpeedTest(c *cli.Context) error {
return nil 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 // set CSV delimiter
gocsv.TagSeparator = c.String(defs.OptionCSVDelimiter) gocsv.TagSeparator = c.String(defs.OptionCSVDelimiter)
@ -74,104 +86,177 @@ func SpeedTest(c *cli.Context) error {
if c.Bool(defs.OptionCSVHeader) { if c.Bool(defs.OptionCSVHeader) {
var rep []report.CSVReport var rep []report.CSVReport
b, _ := gocsv.MarshalBytes(&rep) b, _ := gocsv.MarshalBytes(&rep)
log.Warnf("%s", b) os.Stdout.WriteString(string(b))
return nil return nil
} }
// read telemetry settings if --share is given // read telemetry settings if --share or any --telemetry option is given
var telemetryServer defs.TelemetryServer var telemetryServer defs.TelemetryServer
if c.Bool(defs.OptionShare) { telemetryJSON := c.String(defs.OptionTelemetryJSON)
if filename := c.String(defs.OptionTelemetryJSON); filename != "" { telemetryLevel := c.String(defs.OptionTelemetryLevel)
b, err := ioutil.ReadFile(filename) telemetryServerString := c.String(defs.OptionTelemetryServer)
telemetryPath := c.String(defs.OptionTelemetryPath)
telemetryShare := c.String(defs.OptionTelemetryShare)
if c.Bool(defs.OptionShare) || telemetryJSON != "" || telemetryLevel != "" || telemetryServerString != "" || telemetryPath != "" || telemetryShare != "" {
if telemetryJSON != "" {
b, err := ioutil.ReadFile(telemetryJSON)
if err != nil { if err != nil {
log.Errorf("Cannot read %s: %s", filename, err) log.Errorf("Cannot read %s: %s", telemetryJSON, err)
return err return err
} }
if err := json.Unmarshal(b, &telemetryServer); err != nil { 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 return err
} }
} }
if str := c.String(defs.OptionTelemetryLevel); str != "" { if telemetryLevel != "" {
if str != "disabled" && str != "basic" && str != "full" && str != "debug" { if telemetryLevel != "disabled" && telemetryLevel != "basic" && telemetryLevel != "full" && telemetryLevel != "debug" {
log.Fatalf("Unsupported telemetry level: %s", str) log.Fatalf("Unsupported telemetry level: %s", telemetryLevel)
} }
telemetryServer.Level = str telemetryServer.Level = telemetryLevel
} else if telemetryServer.Level == "" { } else if telemetryServer.Level == "" {
telemetryServer.Level = defaultTelemetryLevel telemetryServer.Level = defaultTelemetryLevel
} }
if str := c.String(defs.OptionTelemetryServer); str != "" { if telemetryServerString != "" {
telemetryServer.Server = str telemetryServer.Server = telemetryServerString
} else if telemetryServer.Server == "" { } else if telemetryServer.Server == "" {
telemetryServer.Server = defaultTelemetryServer telemetryServer.Server = defaultTelemetryServer
} }
if str := c.String(defs.OptionTelemetryPath); str != "" { if telemetryPath != "" {
telemetryServer.Path = str telemetryServer.Path = telemetryPath
} else if telemetryServer.Path == "" { } else if telemetryServer.Path == "" {
telemetryServer.Path = defaultTelemetryPath telemetryServer.Path = defaultTelemetryPath
} }
if str := c.String(defs.OptionTelemetryShare); str != "" { if telemetryShare != "" {
telemetryServer.Share = str telemetryServer.Share = telemetryShare
} else if telemetryServer.Share == "" { } else if telemetryServer.Share == "" {
telemetryServer.Share = defaultTeleemtryShare telemetryServer.Share = defaultTelemetryShare
} }
} }
if req := c.Int(defs.OptionConcurrent); req <= 0 {
log.Errorf("Concurrent requests cannot be lower than 1: %d is given", req)
return errors.New("invalid concurrent requests setting")
}
noICMP := c.Bool(defs.OptionNoICMP)
// HTTP requests timeout // HTTP requests timeout
http.DefaultClient.Timeout = time.Duration(c.Int(defs.OptionTimeout)) * time.Second http.DefaultClient.Timeout = time.Duration(c.Int(defs.OptionTimeout)) * time.Second
forceIPv4 := c.Bool(defs.OptionIPv4)
forceIPv6 := c.Bool(defs.OptionIPv6)
var network string
switch {
case forceIPv4:
network = "ip4"
case forceIPv6:
network = "ip6"
default:
network = "ip"
}
transport := http.DefaultTransport.(*http.Transport).Clone()
if caCertFileName := c.String(defs.OptionCACert); caCertFileName != "" {
caCert, err := ioutil.ReadFile(caCertFileName)
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify),
RootCAs: caCertPool,
}
} else {
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify),
}
}
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
// bind to source IP address if given // bind to source IP address if given
if src := c.String(defs.OptionSource); src != "" { if src := c.String(defs.OptionSource); src != "" {
// first we parse the IP to see if it's valid var err error
localAddr, err := net.ResolveIPAddr("ip", src) dialer, err = newDialerAddressBound(src, network)
if err != nil { if err != nil {
log.Errorf("Error parsing source IP: %s", err)
return err return err
} }
localTCPAddr := net.TCPAddr{IP: localAddr.IP}
// set default HTTP client's Transport to the one that binds the source address
// this is modified from http.DefaultTransport
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
LocalAddr: &localTCPAddr,
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
// although this option is marked deprecated, but it's still used in http.DefaultTransport, keeping as-is
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
http.DefaultClient.Transport = transport
} }
// 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 // load server list
var servers []defs.Server var servers []defs.Server
var err error var err error
if str := c.String(defs.OptionLocalJSON); str != "" { if str := c.String(defs.OptionLocalJSON); str != "" {
// load server list from local JSON file switch str {
log.Infof("Using local JSON server list: %s", str) case "-":
servers, err = getLocalServers(c.Bool(defs.OptionSecure), str, c.IntSlice(defs.OptionExclude), c.IntSlice(defs.OptionServer), !c.Bool(defs.OptionList)) // load server list from stdin
log.Info("Using local JSON server list from stdin")
servers, err = getLocalServersReader(c.Bool(defs.OptionSecure), os.Stdin, c.IntSlice(defs.OptionExclude), c.IntSlice(defs.OptionServer), !c.Bool(defs.OptionList))
default:
// load server list from local JSON file
log.Infof("Using local JSON server list: %s", str)
servers, err = getLocalServers(c.Bool(defs.OptionSecure), str, c.IntSlice(defs.OptionExclude), c.IntSlice(defs.OptionServer), !c.Bool(defs.OptionList))
}
} else { } else {
// fetch the server list JSON and parse it into the `servers` array // fetch the server list JSON and parse it into the `servers` array
log.Info("Retrieving LibreSpeed.org server list")
serverUrl := serverListUrl serverUrl := serverListUrl
if str := c.String(defs.OptionServerJSON); str != "" { if str := c.String(defs.OptionServerJSON); str != "" {
serverUrl = str serverUrl = str
} }
log.Infof("Retrieving server list from %s", serverUrl)
servers, err = getServerList(c.Bool(defs.OptionSecure), serverUrl, c.IntSlice(defs.OptionExclude), c.IntSlice(defs.OptionServer), !c.Bool(defs.OptionList)) servers, err = getServerList(c.Bool(defs.OptionSecure), serverUrl, c.IntSlice(defs.OptionExclude), c.IntSlice(defs.OptionServer), !c.Bool(defs.OptionList))
if err != nil {
log.Info("Retry with /.well-known/librespeed")
servers, err = getServerList(c.Bool(defs.OptionSecure), serverUrl+"/.well-known/librespeed", c.IntSlice(defs.OptionExclude), c.IntSlice(defs.OptionServer), !c.Bool(defs.OptionList))
}
} }
if err != nil { if err != nil {
log.Errorf("Error when fetching server list: %s", err) log.Errorf("Error when fetching server list: %s", err)
@ -180,29 +265,33 @@ func SpeedTest(c *cli.Context) error {
// if --list is given, list all the servers fetched and exit // if --list is given, list all the servers fetched and exit
if c.Bool(defs.OptionList) { if c.Bool(defs.OptionList) {
for idx, svr := range servers { for _, svr := range servers {
log.Warnf("%d: %s (%s)", idx, svr.Name, svr.Server) var sponsorMsg string
if svr.Sponsor() != "" {
sponsorMsg = fmt.Sprintf(" [Sponsor: %s]", svr.Sponsor())
}
log.Warnf("%d: %s (%s) %s", svr.ID, svr.Name, svr.Server, sponsorMsg)
} }
return nil return nil
} }
// if --server is given, do speed tests with all of them // if --server is given, do speed tests with all of them
if len(c.IntSlice(defs.OptionServer)) > 0 { if len(c.IntSlice(defs.OptionServer)) > 0 {
return doSpeedTest(c, servers, telemetryServer, silent) return doSpeedTest(c, servers, telemetryServer, network, silent, noICMP)
} else { } else {
// else select the fastest server from the list // else select the fastest server from the list
log.Info("Selecting the fastest server based on ping") log.Info("Selecting the fastest server based on ping")
var wg sync.WaitGroup var wg sync.WaitGroup
jobs := make(chan PingJob, 10) jobs := make(chan PingJob, len(servers))
results := make(chan PingResult, 10) results := make(chan PingResult, len(servers))
done := make(chan struct{}) done := make(chan struct{})
pingList := make(map[int]float64) pingList := make(map[int]float64)
// spawn 10 concurrent pingers // spawn 10 concurrent pingers
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
go pingWorker(jobs, results, &wg, c.String(defs.OptionSource)) go pingWorker(jobs, results, &wg, c.String(defs.OptionSource), network, noICMP)
} }
// send ping jobs to workers // send ping jobs to workers
@ -239,11 +328,11 @@ func SpeedTest(c *cli.Context) error {
} }
// do speed test on the server // do speed test on the server
return doSpeedTest(c, []defs.Server{servers[serverIdx]}, telemetryServer, silent) return doSpeedTest(c, []defs.Server{servers[serverIdx]}, telemetryServer, network, silent, noICMP)
} }
} }
func pingWorker(jobs <-chan PingJob, results chan<- PingResult, wg *sync.WaitGroup, srcIp string) { func pingWorker(jobs <-chan PingJob, results chan<- PingResult, wg *sync.WaitGroup, srcIp, network string, noICMP bool) {
for { for {
job := <-jobs job := <-jobs
server := job.Server server := job.Server
@ -257,8 +346,11 @@ func pingWorker(jobs <-chan PingJob, results chan<- PingResult, wg *sync.WaitGro
// check the server is up by accessing the ping URL and checking its returned value == empty and status code == 200 // check the server is up by accessing the ping URL and checking its returned value == empty and status code == 200
if server.IsUp() { if server.IsUp() {
// skip ICMP if option given
server.NoICMP = noICMP
// if server is up, get ping // if server is up, get ping
ping, _, err := server.ICMPPingAndJitter(1, srcIp) ping, _, err := server.ICMPPingAndJitter(1, srcIp, network)
if err != nil { if err != nil {
log.Debugf("Can't ping server %s (%s), skipping", server.Name, u.Hostname()) log.Debugf("Can't ping server %s (%s), skipping", server.Name, u.Hostname())
wg.Done() wg.Done()
@ -283,7 +375,13 @@ func getServerList(forceHTTPS bool, serverList string, excludes, specific []int,
// getting the server list from remote // getting the server list from remote
var servers []defs.Server var servers []defs.Server
resp, err := http.DefaultClient.Get(serverList) req, err := http.NewRequest(http.MethodGet, serverList, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", defs.UserAgent)
resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -301,10 +399,13 @@ func getServerList(forceHTTPS bool, serverList string, excludes, specific []int,
return preprocessServers(servers, forceHTTPS, excludes, specific, filter) return preprocessServers(servers, forceHTTPS, excludes, specific, filter)
} }
// getLocalServers loads the server JSON from a local file // getLocalServersReader loads the server JSON from an io.Reader
func getLocalServers(forceHTTPS bool, jsonFile string, excludes, specific []int, filter bool) ([]defs.Server, error) { func getLocalServersReader(forceHTTPS bool, reader io.ReadCloser, excludes, specific []int, filter bool) ([]defs.Server, error) {
defer reader.Close()
var servers []defs.Server var servers []defs.Server
b, err := ioutil.ReadFile(jsonFile)
b, err := ioutil.ReadAll(reader)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -316,6 +417,15 @@ func getLocalServers(forceHTTPS bool, jsonFile string, excludes, specific []int,
return preprocessServers(servers, forceHTTPS, excludes, specific, filter) return preprocessServers(servers, forceHTTPS, excludes, specific, filter)
} }
// getLocalServers loads the server JSON from a local file
func getLocalServers(forceHTTPS bool, jsonFile string, excludes, specific []int, filter bool) ([]defs.Server, error) {
f, err := os.OpenFile(jsonFile, os.O_RDONLY, 0644)
if err != nil {
return nil, err
}
return getLocalServersReader(forceHTTPS, f, excludes, specific, filter)
}
// preprocessServers makes some needed modifications to the servers fetched // preprocessServers makes some needed modifications to the servers fetched
func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specific []int, filter bool) ([]defs.Server, error) { func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specific []int, filter bool) ([]defs.Server, error) {
for i := range servers { for i := range servers {
@ -345,8 +455,8 @@ func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specifi
// exclude servers from --exclude // exclude servers from --exclude
if len(excludes) > 0 { if len(excludes) > 0 {
var ret []defs.Server var ret []defs.Server
for idx, server := range servers { for _, server := range servers {
if contains(excludes, idx) { if contains(excludes, server.ID) {
continue continue
} }
ret = append(ret, server) ret = append(ret, server)
@ -358,11 +468,15 @@ func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specifi
// special value -1 will test all servers // special value -1 will test all servers
if len(specific) > 0 && !contains(specific, -1) { if len(specific) > 0 && !contains(specific, -1) {
var ret []defs.Server var ret []defs.Server
for idx, server := range servers { for _, server := range servers {
if contains(specific, idx) { if contains(specific, server.ID) {
ret = append(ret, server) 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 return ret, nil
} }
} }
@ -379,3 +493,31 @@ func contains(arr []int, val int) bool {
} }
return false 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
}

2
upx.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
upx "$@" || true