mirror of
https://github.com/librespeed/speedtest-cli.git
synced 2025-06-30 12:59:54 +00:00
Compare commits
27 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 |
15 changed files with 453 additions and 236 deletions
128
.goreleaser.yml
128
.goreleaser.yml
|
@ -1,74 +1,84 @@
|
||||||
project_name: 'librespeed-cli'
|
version: 2
|
||||||
|
project_name: "librespeed-cli"
|
||||||
#dist: ./out
|
#dist: ./out
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod download
|
- go mod download
|
||||||
builds:
|
builds:
|
||||||
- main: ./main.go
|
- main: ./main.go
|
||||||
id: upx
|
id: upx
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}"
|
- -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}"
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
- darwin
|
- darwin
|
||||||
- freebsd
|
- freebsd
|
||||||
goarch:
|
goarch:
|
||||||
- 386
|
- "386"
|
||||||
- amd64
|
- amd64
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
- mips
|
goarm:
|
||||||
- mipsle
|
- "5"
|
||||||
- mips64
|
- "6"
|
||||||
- mips64le
|
- "7"
|
||||||
goarm:
|
ignore:
|
||||||
- 5
|
- goos: darwin
|
||||||
- 6
|
goarch: "386"
|
||||||
- 7
|
- goos: darwin
|
||||||
gomips:
|
goarch: arm64
|
||||||
- hardfloat
|
hooks:
|
||||||
- softfloat
|
post:
|
||||||
ignore:
|
- ./upx.sh -9 "{{ .Path }}"
|
||||||
- goos: darwin
|
- main: ./main.go
|
||||||
goarch: 386
|
id: no-upx
|
||||||
- goos: darwin
|
env:
|
||||||
goarch: arm64
|
- CGO_ENABLED=0
|
||||||
hooks:
|
flags:
|
||||||
post: ./upx.sh -9 "{{ .Path }}"
|
- -trimpath
|
||||||
- main: ./main.go
|
ldflags:
|
||||||
id: no-upx
|
- -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}"
|
||||||
env:
|
goos:
|
||||||
- CGO_ENABLED=0
|
- linux
|
||||||
flags:
|
- windows
|
||||||
- -trimpath
|
- darwin
|
||||||
ldflags:
|
goarch:
|
||||||
- -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}"
|
- "386"
|
||||||
goos:
|
- amd64
|
||||||
- windows
|
- arm64
|
||||||
- darwin
|
- mips
|
||||||
goarch:
|
- mipsle
|
||||||
- 386
|
- mips64
|
||||||
- amd64
|
- mips64le
|
||||||
- arm64
|
- riscv64
|
||||||
ignore:
|
gomips:
|
||||||
- goos: darwin
|
- hardfloat
|
||||||
goarch: 386
|
- softfloat
|
||||||
- goos: darwin
|
ignore:
|
||||||
goarch: amd64
|
- goos: linux
|
||||||
|
goarch: "386"
|
||||||
|
- goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
- goos: darwin
|
||||||
|
goarch: "386"
|
||||||
|
- goos: darwin
|
||||||
|
goarch: amd64
|
||||||
archives:
|
archives:
|
||||||
- format_overrides:
|
- format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats: ['zip']
|
||||||
files:
|
files:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: "checksums.txt"
|
||||||
changelog:
|
changelog:
|
||||||
skip: false
|
disable: false
|
||||||
sort: asc
|
sort: asc
|
||||||
release:
|
release:
|
||||||
github:
|
github:
|
||||||
|
|
76
README.md
76
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,7 +22,7 @@ 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)
|
||||||
|
@ -42,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
|
||||||
|
@ -59,10 +59,10 @@ can now proceed to build `librespeed-cli` with the build script:
|
||||||
|
|
||||||
If you want to build for another operating system or system architecture, use the `GOOS` and `GOARCH` environment
|
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`
|
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`.
|
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
|
||||||
|
@ -74,7 +74,7 @@ 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
|
## Install from AUR
|
||||||
|
@ -97,11 +97,45 @@ $ makepkg -si
|
||||||
|
|
||||||
See the [librespeed-cli Homebrew tap](https://github.com/librespeed/homebrew-tap#setup).
|
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 🚀
|
||||||
|
@ -116,6 +150,8 @@ GLOBAL OPTIONS:
|
||||||
--ipv6, -6 Force IPv6 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)
|
--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
|
||||||
|
@ -126,27 +162,37 @@ GLOBAL OPTIONS:
|
||||||
--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
|
||||||
|
@ -190,11 +236,15 @@ locally via `--local-json`). The format is as below:
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `--local-json` option can also read from `stdin`:
|
||||||
|
|
||||||
|
`echo '[{"id": 1,"name": "a","server": "https://speedtest.example.com/","dlURL": "garbage.php","ulURL": "empty.php","pingURL": "empty.php","getIpURL": "getIP.php"}]' | librespeed-cli --local-json - `
|
||||||
|
|
||||||
As you can see in the example, all servers have their schemes defined. In case of undefined scheme (e.g. `//example.com`),
|
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`. 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}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
type BytesCounter struct {
|
type BytesCounter struct {
|
||||||
start time.Time
|
start time.Time
|
||||||
pos int
|
pos int
|
||||||
total int
|
total uint64
|
||||||
payload []byte
|
payload []byte
|
||||||
reader io.ReadSeeker
|
reader io.ReadSeeker
|
||||||
mebi bool
|
mebi bool
|
||||||
|
@ -33,7 +33,7 @@ func NewCounter() *BytesCounter {
|
||||||
func (c *BytesCounter) Write(p []byte) (int, error) {
|
func (c *BytesCounter) Write(p []byte) (int, error) {
|
||||||
n := len(p)
|
n := len(p)
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
c.total += n
|
c.total += uint64(n)
|
||||||
c.lock.Unlock()
|
c.lock.Unlock()
|
||||||
|
|
||||||
return n, nil
|
return n, nil
|
||||||
|
@ -43,7 +43,7 @@ func (c *BytesCounter) Write(p []byte) (int, error) {
|
||||||
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.lock.Lock()
|
c.lock.Lock()
|
||||||
c.total += n
|
c.total += uint64(n)
|
||||||
c.pos += n
|
c.pos += n
|
||||||
if c.pos == c.uploadSize {
|
if c.pos == c.uploadSize {
|
||||||
c.resetReader()
|
c.resetReader()
|
||||||
|
@ -116,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,13 @@ const (
|
||||||
OptionExclude = "exclude"
|
OptionExclude = "exclude"
|
||||||
OptionServerJSON = "server-json"
|
OptionServerJSON = "server-json"
|
||||||
OptionSource = "source"
|
OptionSource = "source"
|
||||||
|
OptionInterface = "interface"
|
||||||
OptionTimeout = "timeout"
|
OptionTimeout = "timeout"
|
||||||
OptionChunks = "chunks"
|
OptionChunks = "chunks"
|
||||||
OptionUploadSize = "upload-size"
|
OptionUploadSize = "upload-size"
|
||||||
OptionDuration = "duration"
|
OptionDuration = "duration"
|
||||||
OptionSecure = "secure"
|
OptionSecure = "secure"
|
||||||
|
OptionCACert = "ca-cert"
|
||||||
OptionSkipCertVerify = "skip-cert-verify"
|
OptionSkipCertVerify = "skip-cert-verify"
|
||||||
OptionNoPreAllocate = "no-pre-allocate"
|
OptionNoPreAllocate = "no-pre-allocate"
|
||||||
OptionVersion = "version"
|
OptionVersion = "version"
|
||||||
|
@ -40,4 +42,5 @@ const (
|
||||||
OptionTelemetryPath = "telemetry-path"
|
OptionTelemetryPath = "telemetry-path"
|
||||||
OptionTelemetryShare = "telemetry-share"
|
OptionTelemetryShare = "telemetry-share"
|
||||||
OptionTelemetryExtra = "telemetry-extra"
|
OptionTelemetryExtra = "telemetry-extra"
|
||||||
|
OptionFwmark = "fwmark"
|
||||||
)
|
)
|
||||||
|
|
|
@ -60,8 +60,11 @@ func (s *Server) IsUp() bool {
|
||||||
}
|
}
|
||||||
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
|
||||||
|
@ -185,7 +188,7 @@ func (s *Server) PingAndJitter(count int) (float64, float64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download performs the actual download test
|
// Download performs the actual download test
|
||||||
func (s *Server) Download(silent bool, useBytes, useMebi bool, requests int, chunks int, duration time.Duration) (float64, int, error) {
|
func (s *Server) Download(silent bool, useBytes, useMebi bool, requests int, chunks int, duration time.Duration) (float64, uint64, error) {
|
||||||
t := time.Now()
|
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())
|
||||||
|
@ -277,7 +280,7 @@ Loop:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload performs the actual upload test
|
// Upload performs the actual upload test
|
||||||
func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi bool, requests int, uploadSize int, duration time.Duration) (float64, int, error) {
|
func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi bool, requests int, uploadSize int, duration time.Duration) (float64, uint64, error) {
|
||||||
t := time.Now()
|
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())
|
||||||
|
@ -412,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[:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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"]
|
36
go.mod
36
go.mod
|
@ -1,17 +1,27 @@
|
||||||
module librespeed-cli
|
module github.com/librespeed/speedtest-cli
|
||||||
|
|
||||||
go 1.14
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/briandowns/spinner v1.12.0
|
github.com/briandowns/spinner v1.23.1
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
github.com/go-ping/ping v1.2.0
|
||||||
github.com/fatih/color v1.10.0 // indirect
|
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
|
||||||
github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d
|
github.com/urfave/cli/v2 v2.27.4
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
golang.org/x/sys v0.33.0
|
||||||
github.com/sirupsen/logrus v1.8.1
|
)
|
||||||
github.com/stretchr/testify v1.3.0 // indirect
|
|
||||||
github.com/urfave/cli/v2 v2.3.0
|
require (
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||||
golang.org/x/sys v0.0.0-20210421221651-33663a62ff08 // indirect
|
github.com/fatih/color v1.17.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
|
golang.org/x/net v0.41.0 // indirect
|
||||||
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
|
golang.org/x/term v0.32.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
91
go.sum
91
go.sum
|
@ -1,59 +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.12.0 h1:72O0PzqGJb6G3KgrcIOtL/JAGGZ5ptOMCn9cUHmqsmw=
|
github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
|
||||||
github.com/briandowns/spinner v1.12.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
|
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/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
|
||||||
github.com/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/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ=
|
||||||
github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741 h1:b0sLP++Tsle+s57tqg5sUk1/OQsC6yMCciVeqNzOcwU=
|
github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
||||||
github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI=
|
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
|
||||||
github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d h1:r3mStZSyjKhEcgbJ5xtv7kT5PZw/tDiFBTMgQx2qsXE=
|
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||||
github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/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/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
|
||||||
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
|
||||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
|
||||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758 h1:aEpZnXcAmXkd6AvLb2OPt+EN1Zu/8Ne3pCqPjja5PXY=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210421221651-33663a62ff08 h1:qyN5bV+96OX8pL78eXDuz6YlDPzCYgdW74H5yE9BoSU=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210421221651-33663a62ff08/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
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=
|
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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
26
main.go
26
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,7 +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.Stdout)
|
log.SetOutput(os.Stderr)
|
||||||
log.SetFormatter(formatter)
|
log.SetFormatter(formatter)
|
||||||
log.SetLevel(log.InfoLevel)
|
log.SetLevel(log.InfoLevel)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func main() {
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: defs.OptionNoICMP,
|
Name: defs.OptionNoICMP,
|
||||||
Usage: "Do not use ICMP ping. ICMP doesn't work well under Linux\n" +
|
Usage: "Do not use ICMP ping. ICMP doesn't work well under Linux\n" +
|
||||||
"at this moment, so you might want to disable it",
|
"\tat this moment, so you might want to disable it",
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
Name: defs.OptionConcurrent,
|
Name: defs.OptionConcurrent,
|
||||||
|
@ -110,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,
|
||||||
|
@ -139,9 +139,13 @@ func main() {
|
||||||
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{
|
&cli.IntFlag{
|
||||||
|
@ -164,6 +168,11 @@ func main() {
|
||||||
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{
|
&cli.BoolFlag{
|
||||||
Name: defs.OptionSkipCertVerify,
|
Name: defs.OptionSkipCertVerify,
|
||||||
Usage: "Skip verifying SSL certificate for HTTPS connections (self-signed certs)",
|
Usage: "Skip verifying SSL certificate for HTTPS connections (self-signed certs)",
|
||||||
|
@ -209,6 +218,11 @@ func main() {
|
||||||
Usage: "Send a custom message along with the telemetry results.\n" +
|
Usage: "Send a custom message along with the telemetry results.\n" +
|
||||||
"\tImplies --" + defs.OptionShare,
|
"\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, network string, silent bool) error {
|
func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.TelemetryServer, network string, silent bool, noICMP bool) error {
|
||||||
if serverCount := len(servers); serverCount > 1 {
|
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
|
||||||
|
@ -66,7 +69,7 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip ICMP if option given
|
// skip ICMP if option given
|
||||||
currentServer.NoICMP = c.Bool(defs.OptionNoICMP)
|
currentServer.NoICMP = noICMP
|
||||||
|
|
||||||
p, jitter, err := currentServer.ICMPPingAndJitter(pingCount, c.String(defs.OptionSource), network)
|
p, jitter, err := currentServer.ICMPPingAndJitter(pingCount, c.String(defs.OptionSource), network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -75,13 +78,13 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -91,12 +94,12 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
|
||||||
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 {
|
||||||
|
@ -106,16 +109,16 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
|
||||||
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) {
|
||||||
useMebi := c.Bool(defs.OptionMebiBytes)
|
useMebi := c.Bool(defs.OptionMebiBytes)
|
||||||
log.Warnf("Ping:\t%.0f ms\tJitter:\t%.0f ms\nDownload rate:\t%s\nUpload rate:\t%s", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi))
|
log.Warnf("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%s\nUpload rate:\t%s", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi))
|
||||||
} else {
|
} else {
|
||||||
log.Warnf("Ping:\t%.0f ms\tJitter:\t%.0f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps", p, jitter, downloadValue, uploadValue)
|
log.Warnf("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps", p, jitter, downloadValue, uploadValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,34 +143,25 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
|
||||||
// check for --csv or --json. the program prioritize the --csv before the --json. this is the same behavior as speedtest-cli
|
// 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
|
||||||
|
@ -181,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,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
|
||||||
}
|
}
|
||||||
|
@ -241,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
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package speedtest
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -19,8 +20,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/report"
|
"github.com/librespeed/speedtest-cli/report"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -64,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")
|
||||||
|
@ -73,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)
|
||||||
|
|
||||||
|
@ -80,7 +86,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +105,7 @@ func SpeedTest(c *cli.Context) error {
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,6 +143,8 @@ func SpeedTest(c *cli.Context) error {
|
||||||
return errors.New("invalid concurrent requests setting")
|
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
|
||||||
|
|
||||||
|
@ -154,59 +162,71 @@ func SpeedTest(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify)}
|
|
||||||
|
|
||||||
// bind to source IP address if given, or if ipv4/ipv6 is forced
|
if caCertFileName := c.String(defs.OptionCACert); caCertFileName != "" {
|
||||||
if src := c.String(defs.OptionSource); src != "" || (forceIPv4 || forceIPv6) {
|
caCert, err := ioutil.ReadFile(caCertFileName)
|
||||||
var localTCPAddr *net.TCPAddr
|
if err != nil {
|
||||||
if src != "" {
|
log.Fatal(err)
|
||||||
// first we parse the IP to see if it's valid
|
|
||||||
addr, err := net.ResolveIPAddr(network, src)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "no suitable address") {
|
|
||||||
if forceIPv6 {
|
|
||||||
log.Errorf("Address %s is not a valid IPv6 address", src)
|
|
||||||
} else {
|
|
||||||
log.Errorf("Address %s is not a valid IPv4 address", src)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Errorf("Error parsing source IP: %s", err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Using %s as source IP", src)
|
|
||||||
localTCPAddr = &net.TCPAddr{IP: addr.IP}
|
|
||||||
}
|
}
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AppendCertsFromPEM(caCert)
|
||||||
|
|
||||||
var dialContext func(context.Context, string, string) (net.Conn, error)
|
transport.TLSClientConfig = &tls.Config{
|
||||||
defaultDialer := &net.Dialer{
|
InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify),
|
||||||
Timeout: 30 * time.Second,
|
RootCAs: caCertPool,
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if localTCPAddr != nil {
|
transport.TLSClientConfig = &tls.Config{
|
||||||
defaultDialer.LocalAddr = localTCPAddr
|
InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
|
||||||
case forceIPv4:
|
|
||||||
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
|
||||||
return defaultDialer.DialContext(ctx, "tcp4", address)
|
|
||||||
}
|
|
||||||
case forceIPv6:
|
|
||||||
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
|
||||||
return defaultDialer.DialContext(ctx, "tcp6", address)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
dialContext = defaultDialer.DialContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// set default HTTP client's Transport to the one that binds the source address
|
|
||||||
// this is modified from http.DefaultTransport
|
|
||||||
transport.DialContext = dialContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}
|
||||||
|
// bind to source IP address if given
|
||||||
|
if src := c.String(defs.OptionSource); src != "" {
|
||||||
|
var err error
|
||||||
|
dialer, err = newDialerAddressBound(src, network)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind to interface if given
|
||||||
|
// bind to interface if given
|
||||||
|
iface := c.String(defs.OptionInterface)
|
||||||
|
fwmark := c.Int(defs.OptionFwmark)
|
||||||
|
|
||||||
|
if iface != "" || fwmark > 0 {
|
||||||
|
var err error
|
||||||
|
dialer, err = newDialerInterfaceOrFwmarkBound(iface, fwmark)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// ICMP ping does not support interface binding.
|
||||||
|
noICMP = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforce if ipv4/ipv6 is forced
|
||||||
|
var dialContext func(context.Context, string, string) (net.Conn, error)
|
||||||
|
switch {
|
||||||
|
case forceIPv4:
|
||||||
|
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||||
|
return dialer.DialContext(ctx, "tcp4", address)
|
||||||
|
}
|
||||||
|
case forceIPv6:
|
||||||
|
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||||
|
return dialer.DialContext(ctx, "tcp6", address)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
dialContext = dialer.DialContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default HTTP client's Transport to the one that binds the source address
|
||||||
|
// this is modified from http.DefaultTransport
|
||||||
|
transport.DialContext = dialContext
|
||||||
http.DefaultClient.Transport = transport
|
http.DefaultClient.Transport = transport
|
||||||
|
|
||||||
// load server list
|
// load server list
|
||||||
|
@ -257,7 +277,7 @@ func SpeedTest(c *cli.Context) error {
|
||||||
|
|
||||||
// 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, network, 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")
|
||||||
|
@ -271,7 +291,7 @@ func SpeedTest(c *cli.Context) error {
|
||||||
|
|
||||||
// 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), network, c.Bool(defs.OptionNoICMP))
|
go pingWorker(jobs, results, &wg, c.String(defs.OptionSource), network, noICMP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// send ping jobs to workers
|
// send ping jobs to workers
|
||||||
|
@ -308,7 +328,7 @@ 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, network, silent)
|
return doSpeedTest(c, []defs.Server{servers[serverIdx]}, telemetryServer, network, silent, noICMP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,6 +473,10 @@ func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specifi
|
||||||
ret = append(ret, server)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -469,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
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue