mirror of
https://github.com/librespeed/speedtest-cli.git
synced 2025-07-01 06:30:26 +00:00
Compare commits
69 commits
Author | SHA1 | Date | |
---|---|---|---|
|
14717ac78b | ||
|
95f2c8207a | ||
|
7ed6fe234c | ||
|
0c565b724b | ||
|
c9decb3fda | ||
|
78c9095ca9 | ||
|
63c75be0fc | ||
|
7573b65ebc | ||
|
d33e431b58 | ||
|
d23c1b7b72 | ||
|
058cd387c1 | ||
|
b1daf1c451 | ||
|
67adaa2956 | ||
|
e5c131fe62 | ||
|
11183cbf98 | ||
|
6103965f44 | ||
|
6059f16e57 | ||
|
24b7826d78 | ||
|
092760f344 | ||
|
0f908e806c | ||
|
af2908a51d | ||
|
c996e515b1 | ||
|
954e973203 | ||
|
c2af01baf5 | ||
|
df77b3ee21 | ||
|
8e95ecefd7 | ||
|
9a8bca8fa0 | ||
|
08d21d65ff | ||
|
9459cbb51d | ||
|
99c0446381 | ||
|
fd5905b72f | ||
|
f6506bc122 | ||
|
67b82aec33 | ||
|
5354b7fc8e | ||
|
c9903d395a | ||
|
5cf383b64d | ||
|
fd8af1557c | ||
|
1190984d53 | ||
|
4676b26441 | ||
|
dc6eb0fbce | ||
|
eb7f5cb43a | ||
|
02de79bf8d | ||
|
06859d9a86 | ||
|
7063aa41df | ||
|
6199090285 | ||
|
166771ba3f | ||
|
1f8dc728d7 | ||
|
4898bfecc9 | ||
|
c9bfcbc770 | ||
|
2c4cb90e66 | ||
|
e56ed18c3b | ||
|
9b7219a900 | ||
|
9dc19eb3a3 | ||
|
f0ed59d53f | ||
|
c3ad222d1a | ||
|
0ebda01e7e | ||
|
c250a39a15 | ||
|
e61ed2ce5a | ||
|
5bb81358cf | ||
|
6b0fcc5acc | ||
|
eafdee6d31 | ||
|
799afff015 | ||
|
55bc44329f | ||
|
d5d4fb3389 | ||
|
9df434e8f2 | ||
|
8cafe323bc | ||
|
a4afee365f | ||
|
5866efba70 | ||
|
30c5ffbb75 |
18 changed files with 853 additions and 272 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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
87
.goreleaser.yml
Normal 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
123
README.md
|
@ -1,7 +1,7 @@
|
||||||

|

|
||||||
|
|
||||||
# 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
|
||||||
[](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2)
|
[](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:
|
||||||
|
|
||||||
|
|
3
build.sh
3
build.sh
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
133
defs/server.go
133
defs/server.go
|
@ -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
19
dockerfile
Normal 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
32
go.mod
|
@ -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
90
go.sum
|
@ -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
86
main.go
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
13
speedtest/util.go
Normal 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
38
speedtest/util_linux.go
Normal 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
2
upx.sh
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
upx "$@" || true
|
Loading…
Add table
Add a link
Reference in a new issue