mirror of
https://github.com/librespeed/speedtest-cli.git
synced 2025-06-30 12:59:54 +00:00
Compare commits
63 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 |
18 changed files with 754 additions and 277 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -130,3 +130,5 @@ fabric.properties
|
|||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# 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
|
101
README.md
101
README.md
|
@ -1,7 +1,7 @@
|
|||

|
||||
|
||||
# LibreSpeed command line tool
|
||||
Don't have a GUI but wants to use LibreSpeed servers to test your Internet speed? 🚀
|
||||
Don't have a GUI but want to use LibreSpeed servers to test your Internet speed? 🚀
|
||||
|
||||
`librespeed-cli` comes to rescue!
|
||||
|
||||
|
@ -22,14 +22,19 @@ This is a command line interface for LibreSpeed speed test backends, written in
|
|||
[](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2)
|
||||
|
||||
## Requirements for compiling
|
||||
- Go 1.14
|
||||
- Go 1.18+
|
||||
|
||||
## Runtime requirements
|
||||
- Any [Go supported platforms](https://github.com/golang/go/wiki/MinimumRequirements)
|
||||
|
||||
## 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`
|
||||
|
||||
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 example, Arch Linux:
|
||||
|
@ -37,7 +42,7 @@ For Linux users, you can use either the archive from golang.org, or install from
|
|||
```shell script
|
||||
# pacman -S go
|
||||
```
|
||||
|
||||
|
||||
2. Then, clone the repository:
|
||||
|
||||
```shell script
|
||||
|
@ -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
|
||||
variables. Run `go tool dist list` to get a list of possible combinations of `GOOS` and `GOARCH`.
|
||||
|
||||
|
||||
Note: Technically, the CLI can be compiled with older Go versions that support Go modules, with `GO111MODULE=on`
|
||||
set. If you're compiling with an older Go runtime, you might have to change the Go version in `go.mod`.
|
||||
|
||||
```shell script
|
||||
# Let's say we're building for 64-bit Windows on Linux
|
||||
$ GOOS=windows GOARCH=amd64 ./build.sh
|
||||
|
@ -66,7 +74,7 @@ can now proceed to build `librespeed-cli` with the build script:
|
|||
$ ls out
|
||||
librespeed-cli-windows-amd64.exe
|
||||
```
|
||||
|
||||
|
||||
5. Now you can use the `librespeed-cli` and test your Internet speed!
|
||||
|
||||
## Install from AUR
|
||||
|
@ -89,11 +97,45 @@ $ makepkg -si
|
|||
|
||||
See the [librespeed-cli Homebrew tap](https://github.com/librespeed/homebrew-tap#setup).
|
||||
|
||||
## Install on Windows
|
||||
|
||||
If you have either [Scoop](https://scoop.sh/) or [Chocolatey](https://chocolatey.org/) installed you can use one of the following commands:
|
||||
|
||||
- Scoop (ensure you have the `extras` bucket added):
|
||||
```
|
||||
> scoop install librespeed-cli
|
||||
```
|
||||
|
||||
- Chocolatey:
|
||||
```
|
||||
> choco install librespeed-cli
|
||||
```
|
||||
|
||||
## Container Image
|
||||
|
||||
You can run `librespeed-cli` in a container.
|
||||
|
||||
1. Build the container image:
|
||||
|
||||
```shell script
|
||||
docker build -t librespeed-cli:latest .
|
||||
```
|
||||
|
||||
2. Run the container:
|
||||
|
||||
```shell script
|
||||
docker run --rm --name librespeed-cli librespeed-cli:latest
|
||||
# With options
|
||||
docker run --rm --name librespeed-cli librespeed-cli:latest --telemetry-level disabled --no-upload
|
||||
# To avoid "Failed to ping target host: socket: permission denied" errors when using --verbose
|
||||
docker run --rm --name librespeed-cli --sysctl net.ipv4.ping_group_range="0 2147483647" librespeed-cli:latest --verbose
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
You can see the full list of supported options with `librespeed-cli -h`:
|
||||
|
||||
```shell script
|
||||
```
|
||||
$ librespeed-cli -h
|
||||
NAME:
|
||||
librespeed-cli - Test your Internet speed with LibreSpeed 🚀
|
||||
|
@ -108,48 +150,63 @@ GLOBAL OPTIONS:
|
|||
--ipv6, -6 Force IPv6 only (default: false)
|
||||
--no-download Do not perform download test (default: false)
|
||||
--no-upload Do not perform upload test (default: false)
|
||||
--no-icmp Do not use ICMP ping. ICMP doesn't work well under Linux
|
||||
at this moment, so you might want to disable it (default: false)
|
||||
--concurrent value Concurrent HTTP requests being made (default: 3)
|
||||
--bytes Display values in bytes instead of bits. Does not affect
|
||||
the image generated by --share, nor output from
|
||||
--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,
|
||||
'km' for kilometres, 'NM' for nautical miles (default: "km")
|
||||
--share Generate and provide a URL to the LibreSpeed.org share results
|
||||
image, not displayed with --csv (default: false)
|
||||
--simple Suppress verbose output, only show basic information
|
||||
(default: false)
|
||||
(default: false)
|
||||
--csv Suppress verbose output, only show basic information in CSV
|
||||
format. Speeds listed in bit/s and not affected by --bytes
|
||||
(default: false)
|
||||
(default: false)
|
||||
--csv-delimiter CSV_DELIMITER Single character delimiter (CSV_DELIMITER) to use in
|
||||
CSV output. (default: ",")
|
||||
--csv-header Print CSV headers (default: false)
|
||||
--json Suppress verbose output, only show basic information
|
||||
in JSON format. Speeds listed in bit/s and not
|
||||
affected by --bytes (default: false)
|
||||
affected by --bytes (default: false)
|
||||
--list Display a list of LibreSpeed.org servers (default: false)
|
||||
--server SERVER Specify a SERVER ID to test against. Can be supplied
|
||||
multiple times. Cannot be used with --exclude
|
||||
--exclude EXCLUDE EXCLUDE a server from selection. Can be supplied
|
||||
multiple times. Cannot be used with --server
|
||||
--server-json value Use an alternative server list from remote JSON file
|
||||
--local-json value Use an alternative server list from local JSON file
|
||||
--source SOURCE SOURCE IP address to bind to
|
||||
--local-json value Use an alternative server list from local JSON file,
|
||||
or read from stdin with "--local-json -".
|
||||
--source SOURCE SOURCE IP address to bind to. Incompatible with --interface.
|
||||
--interface INTERFACE The name of the network interface to bind to. Example: "enp0s3".
|
||||
Not supported on Windows and incompatible with --source.
|
||||
Implies --no-icmp.
|
||||
--timeout TIMEOUT HTTP TIMEOUT in seconds. (default: 15)
|
||||
--duration value Upload and download test duration in seconds (default: 15)
|
||||
--chunks value Chunks to download from server, chunk size depends on server configuration (default: 100)
|
||||
--upload-size value Size of payload being uploaded in KiB (default: 1024)
|
||||
--secure Use HTTPS instead of HTTP when communicating with
|
||||
LibreSpeed.org operated servers (default: false)
|
||||
--ca-cert value Use the specified CA certificate PEM bundle file instead
|
||||
of the system certificate trust store
|
||||
--skip-cert-verify Skip verifying SSL certificate for HTTPS connections (self-signed certs) (default: false)
|
||||
--no-pre-allocate Do not pre allocate upload data. Pre allocation is
|
||||
enabled by default to improve upload performance. To
|
||||
support systems with insufficient memory, use this
|
||||
option to avoid out of memory errors (default: false)
|
||||
--telemetry-json value Load telemetry server settings from a JSON file. This
|
||||
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:
|
||||
disabled, basic, full, debug
|
||||
--telemetry-server value Set the telemetry server base URL
|
||||
--telemetry-path value Set the telemetry upload path
|
||||
--telemetry-share value Set the telemetry share link path
|
||||
--telemetry-extra value Send a custom message along with the telemetry results
|
||||
disabled, basic, full, debug. Implies --share
|
||||
--telemetry-server value Set the telemetry server base URL. Implies --share
|
||||
--telemetry-path value Set the telemetry upload path. Implies --share
|
||||
--telemetry-share value Set the telemetry share link path. Implies --share
|
||||
--telemetry-extra value Send a custom message along with the telemetry results.
|
||||
Implies --share
|
||||
```
|
||||
|
||||
## Use a custom backend server list
|
||||
|
@ -159,6 +216,7 @@ locally via `--local-json`). The format is as below:
|
|||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "PHP Backend",
|
||||
"server": "https://example.com/",
|
||||
"dlURL": "garbage.php",
|
||||
|
@ -167,6 +225,7 @@ locally via `--local-json`). The format is as below:
|
|||
"getIpURL": "getIP.php"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Go Backend",
|
||||
"server": "http://example.com/speedtest/",
|
||||
"dlURL": "garbage",
|
||||
|
@ -177,11 +236,15 @@ locally via `--local-json`). The format is as below:
|
|||
]
|
||||
```
|
||||
|
||||
The `--local-json` option can also read from `stdin`:
|
||||
|
||||
`echo '[{"id": 1,"name": "a","server": "https://speedtest.example.com/","dlURL": "garbage.php","ulURL": "empty.php","pingURL": "empty.php","getIpURL": "getIP.php"}]' | librespeed-cli --local-json - `
|
||||
|
||||
As you can see in the example, all servers have their schemes defined. In case of undefined scheme (e.g. `//example.com`),
|
||||
`librespeed-cli` will use `http` by default, or `https` when the `--secure` option is enabled.
|
||||
|
||||
## Use a custom telemetry server
|
||||
By default, the telemetry result will be sent to `librespeed.org` 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
|
||||
`--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
|
||||
|
||||
PROGNAME="librespeed-cli"
|
||||
DEFS_PATH="github.com/librespeed/speedtest-cli"
|
||||
BINARY=${PROGNAME}-$(go env GOOS)-$(go env GOARCH)
|
||||
BUILD_DATE=$(date -u "+%Y-%m-%d %H:%M:%S %Z")
|
||||
LDFLAGS="-w -s -X \"librespeed-cli/defs.ProgName=${PROGNAME}\" -X \"librespeed-cli/defs.ProgVersion=${PROGVER}\" -X \"librespeed-cli/defs.BuildDate=${BUILD_DATE}\""
|
||||
LDFLAGS="-w -s -X \"${DEFS_PATH}/defs.ProgName=${PROGNAME}\" -X \"${DEFS_PATH}/defs.ProgVersion=${PROGVER}\" -X \"${DEFS_PATH}/defs.BuildDate=${BUILD_DATE}\""
|
||||
|
||||
if [[ -n "${GOARM}" ]] && [[ "${GOARM}" -gt 0 ]]; then
|
||||
BINARY=${BINARY}v${GOARM}
|
||||
|
|
|
@ -6,22 +6,35 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BytesCounter implements io.Reader and io.Writer interface, for counting bytes being read/written in HTTP requests
|
||||
type BytesCounter struct {
|
||||
start time.Time
|
||||
total int
|
||||
payload []byte
|
||||
reader io.ReadSeeker
|
||||
mebi bool
|
||||
start time.Time
|
||||
pos int
|
||||
total uint64
|
||||
payload []byte
|
||||
reader io.ReadSeeker
|
||||
mebi bool
|
||||
uploadSize int
|
||||
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
func NewCounter() *BytesCounter {
|
||||
return &BytesCounter{
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements io.Writer
|
||||
func (c *BytesCounter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
c.total += n
|
||||
c.lock.Lock()
|
||||
c.total += uint64(n)
|
||||
c.lock.Unlock()
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
@ -29,7 +42,13 @@ func (c *BytesCounter) Write(p []byte) (int, error) {
|
|||
// Read implements io.Reader
|
||||
func (c *BytesCounter) Read(p []byte) (int, error) {
|
||||
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
|
||||
}
|
||||
|
@ -39,21 +58,28 @@ func (c *BytesCounter) SetMebi(mebi bool) {
|
|||
c.mebi = mebi
|
||||
}
|
||||
|
||||
// Average returns the average bytes/second
|
||||
func (c *BytesCounter) Average() float64 {
|
||||
// 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()
|
||||
}
|
||||
|
||||
// AvgMbps returns the average mbits/second
|
||||
func (c *BytesCounter) AvgMbps() float64 {
|
||||
var base float64 = 100000
|
||||
var base float64 = 125000
|
||||
if c.mebi {
|
||||
base = 131072
|
||||
}
|
||||
return c.Average() / base
|
||||
return c.AvgBytes() / base
|
||||
}
|
||||
|
||||
// AvgHumanize returns the average bytes/kilobytes/megabytes/gigabytes (or bytes/kibibytes/mebibytes/gibibytes) per second
|
||||
func (c *BytesCounter) AvgHumanize() string {
|
||||
val := c.Average()
|
||||
val := c.AvgBytes()
|
||||
|
||||
var base float64 = 1000
|
||||
if c.mebi {
|
||||
|
@ -74,12 +100,13 @@ func (c *BytesCounter) AvgHumanize() string {
|
|||
// GenerateBlob generates a random byte array of `uploadSize` in the `payload` field, and sets the `reader` field to
|
||||
// read from it
|
||||
func (c *BytesCounter) GenerateBlob() {
|
||||
c.payload = getRandomData(uploadSize)
|
||||
c.payload = getRandomData(c.uploadSize)
|
||||
c.reader = bytes.NewReader(c.payload)
|
||||
}
|
||||
|
||||
// ResetReader resets the `reader` field to 0 position
|
||||
func (c *BytesCounter) ResetReader() (int64, error) {
|
||||
// resetReader resets the `reader` field to 0 position
|
||||
func (c *BytesCounter) resetReader() (int64, error) {
|
||||
c.pos = 0
|
||||
return c.reader.Seek(0, 0)
|
||||
}
|
||||
|
||||
|
@ -89,7 +116,7 @@ func (c *BytesCounter) Start() {
|
|||
}
|
||||
|
||||
// Total returns the total bytes read/written
|
||||
func (c *BytesCounter) Total() int {
|
||||
func (c *BytesCounter) Total() uint64 {
|
||||
return c.total
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
package defs
|
||||
|
||||
const (
|
||||
// chunks to download in download test
|
||||
downloadChunks = 100
|
||||
// payload size per upload request
|
||||
uploadSize = 1024 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
// values to be filled in by build script
|
||||
BuildDate string
|
||||
|
|
|
@ -8,6 +8,8 @@ const (
|
|||
OptionIPv6Alt = "6"
|
||||
OptionNoDownload = "no-download"
|
||||
OptionNoUpload = "no-upload"
|
||||
OptionNoICMP = "no-icmp"
|
||||
OptionConcurrent = "concurrent"
|
||||
OptionBytes = "bytes"
|
||||
OptionMebiBytes = "mebibytes"
|
||||
OptionDistance = "distance"
|
||||
|
@ -22,8 +24,14 @@ const (
|
|||
OptionExclude = "exclude"
|
||||
OptionServerJSON = "server-json"
|
||||
OptionSource = "source"
|
||||
OptionInterface = "interface"
|
||||
OptionTimeout = "timeout"
|
||||
OptionChunks = "chunks"
|
||||
OptionUploadSize = "upload-size"
|
||||
OptionDuration = "duration"
|
||||
OptionSecure = "secure"
|
||||
OptionCACert = "ca-cert"
|
||||
OptionSkipCertVerify = "skip-cert-verify"
|
||||
OptionNoPreAllocate = "no-pre-allocate"
|
||||
OptionVersion = "version"
|
||||
OptionLocalJSON = "local-json"
|
||||
|
@ -34,4 +42,5 @@ const (
|
|||
OptionTelemetryPath = "telemetry-path"
|
||||
OptionTelemetryShare = "telemetry-share"
|
||||
OptionTelemetryExtra = "telemetry-extra"
|
||||
OptionFwmark = "fwmark"
|
||||
)
|
||||
|
|
113
defs/server.go
113
defs/server.go
|
@ -16,20 +16,24 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/go-ping/ping"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sparrc/go-ping"
|
||||
)
|
||||
|
||||
// Server represents a speed test server
|
||||
type Server struct {
|
||||
Name string `json:"name"`
|
||||
Server string `json:"server"`
|
||||
DownloadURL string `json:"dlURL"`
|
||||
UploadURL string `json:"ulURL"`
|
||||
PingURL string `json:"pingURL"`
|
||||
GetIPURL string `json:"getIpURL"`
|
||||
ICMPFail bool `json:"-"`
|
||||
TLog TelemetryLog `json:"-"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Server string `json:"server"`
|
||||
DownloadURL string `json:"dlURL"`
|
||||
UploadURL string `json:"ulURL"`
|
||||
PingURL string `json:"pingURL"`
|
||||
GetIPURL string `json:"getIpURL"`
|
||||
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
|
||||
|
@ -41,15 +45,26 @@ func (s *Server) IsUp() bool {
|
|||
|
||||
u, _ := s.GetURL()
|
||||
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 {
|
||||
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
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
if len(b) > 0 {
|
||||
log.Debugf("Failed when parsing get IP result: %s", b)
|
||||
}
|
||||
// only return online if the ping URL returns nothing and 200
|
||||
return len(b) == 0 && resp.StatusCode == http.StatusOK
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
|
||||
// ICMPPingAndJitter pings the server via ICMP echos and calculate the average ping and jitter
|
||||
|
@ -59,8 +74,8 @@ func (s *Server) ICMPPingAndJitter(count int, srcIp, network string) (float64, f
|
|||
s.TLog.Logf("ICMP ping took %s", time.Now().Sub(t).String())
|
||||
}()
|
||||
|
||||
if s.ICMPFail {
|
||||
log.Debug("ICMP ping failed already, using HTTP ping")
|
||||
if s.NoICMP {
|
||||
log.Debugf("Skipping ICMP for server %s, will use HTTP ping", s.Name)
|
||||
return s.PingAndJitter(count + 2)
|
||||
}
|
||||
|
||||
|
@ -70,12 +85,8 @@ func (s *Server) ICMPPingAndJitter(count int, srcIp, network string) (float64, f
|
|||
return 0, 0, err
|
||||
}
|
||||
|
||||
p, err := ping.NewPinger(u.Hostname(), network)
|
||||
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.Timeout = time.Duration(count) * time.Second
|
||||
if srcIp != "" {
|
||||
|
@ -108,7 +119,7 @@ func (s *Server) ICMPPingAndJitter(count int, srcIp, network string) (float64, f
|
|||
}
|
||||
|
||||
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())
|
||||
return s.PingAndJitter(count + 2)
|
||||
}
|
||||
|
@ -116,7 +127,7 @@ func (s *Server) ICMPPingAndJitter(count int, srcIp, network string) (float64, f
|
|||
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) {
|
||||
t := time.Now()
|
||||
defer func() {
|
||||
|
@ -159,9 +170,9 @@ func (s *Server) PingAndJitter(count int) (float64, float64, error) {
|
|||
}
|
||||
|
||||
var lastPing, jitter float64
|
||||
for idx, ping := range pings {
|
||||
for idx, p := range pings {
|
||||
if idx != 0 {
|
||||
instJitter := math.Abs(lastPing - ping)
|
||||
instJitter := math.Abs(lastPing - p)
|
||||
if idx > 1 {
|
||||
if jitter > instJitter {
|
||||
jitter = jitter*0.7 + instJitter*0.3
|
||||
|
@ -170,20 +181,20 @@ func (s *Server) PingAndJitter(count int) (float64, float64, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
lastPing = ping
|
||||
lastPing = p
|
||||
}
|
||||
|
||||
return getAvg(pings), jitter, nil
|
||||
}
|
||||
|
||||
// Download performs the actual download test
|
||||
func (s *Server) Download(silent bool, useBytes, useMebi 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()
|
||||
defer func() {
|
||||
s.TLog.Logf("Download took %s", time.Now().Sub(t).String())
|
||||
}()
|
||||
|
||||
counter := &BytesCounter{}
|
||||
counter := NewCounter()
|
||||
counter.SetMebi(useMebi)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
@ -202,12 +213,12 @@ func (s *Server) Download(silent bool, useBytes, useMebi bool) (float64, int, er
|
|||
return 0, 0, err
|
||||
}
|
||||
q := req.URL.Query()
|
||||
q.Set("ckSize", strconv.Itoa(downloadChunks))
|
||||
q.Set("ckSize", strconv.Itoa(chunks))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
|
||||
downloadDone := make(chan struct{})
|
||||
downloadDone := make(chan struct{}, requests)
|
||||
|
||||
doDownload := func() {
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
|
@ -249,8 +260,11 @@ func (s *Server) Download(silent bool, useBytes, useMebi bool) (float64, int, er
|
|||
}()
|
||||
}
|
||||
|
||||
go doDownload()
|
||||
timeout := time.After(15 * time.Second)
|
||||
for i := 0; i < requests; i++ {
|
||||
go doDownload()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
timeout := time.After(duration)
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
|
@ -266,14 +280,15 @@ Loop:
|
|||
}
|
||||
|
||||
// Upload performs the actual upload test
|
||||
func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi 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()
|
||||
defer func() {
|
||||
s.TLog.Logf("Upload took %s", time.Now().Sub(t).String())
|
||||
}()
|
||||
|
||||
counter := &BytesCounter{}
|
||||
counter := NewCounter()
|
||||
counter.SetMebi(useMebi)
|
||||
counter.SetUploadSize(uploadSize)
|
||||
|
||||
if noPrealloc {
|
||||
log.Info("Pre-allocation is disabled, performance might be lower!")
|
||||
|
@ -299,7 +314,7 @@ func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi bool) (float64, in
|
|||
req.Header.Set("User-Agent", UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
|
||||
uploadDone := make(chan struct{})
|
||||
uploadDone := make(chan struct{}, requests)
|
||||
|
||||
doUpload := func() {
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
|
@ -311,7 +326,6 @@ func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi bool) (float64, in
|
|||
log.Debugf("Failed when reading HTTP response: %s", err)
|
||||
}
|
||||
|
||||
counter.ResetReader()
|
||||
uploadDone <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
@ -339,8 +353,11 @@ func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi bool) (float64, in
|
|||
}()
|
||||
}
|
||||
|
||||
go doUpload()
|
||||
timeout := time.After(15 * time.Second)
|
||||
for i := 0; i < requests; i++ {
|
||||
go doUpload()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
timeout := time.After(duration)
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
|
@ -379,6 +396,8 @@ func (s *Server) GetIPInfo(distanceUnit string) (*GetIPResult, error) {
|
|||
log.Debugf("Failed when creating HTTP request: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Debugf("Failed when making HTTP request: %s", err)
|
||||
|
@ -396,6 +415,7 @@ func (s *Server) GetIPInfo(distanceUnit string) (*GetIPResult, error) {
|
|||
if err := json.Unmarshal(b, &ipInfo); err != nil {
|
||||
log.Debugf("Failed when parsing get IP result: %s", err)
|
||||
log.Debugf("Received payload: %s", b)
|
||||
ipInfo.ProcessedString = string(b[:])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -416,3 +436,24 @@ func (s *Server) GetURL() (*url.URL, error) {
|
|||
}
|
||||
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-20200311033510-5e9a13ec8da6
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/briandowns/spinner v1.9.0
|
||||
github.com/gocarina/gocsv v0.0.0-20200302151839-87c60d755c58
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.1.1
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect
|
||||
github.com/briandowns/spinner v1.23.1
|
||||
github.com/go-ping/ping v1.2.0
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/urfave/cli/v2 v2.27.4
|
||||
golang.org/x/sys v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
)
|
||||
|
|
88
go.sum
88
go.sum
|
@ -1,46 +1,56 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/briandowns/spinner v1.9.0 h1:+OMAisemaHar1hjuJ3Z2hIvNhQl9Y7GLPWUwwz2Pxo8=
|
||||
github.com/briandowns/spinner v1.9.0/go.mod h1://Zf9tMcxfRUA36V23M6YGEAv+kECGfvpnLTnb8n4XQ=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650=
|
||||
github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/gocarina/gocsv v0.0.0-20200302151839-87c60d755c58 h1:rRQm5os6ffGTukb42ZgKWOFfdFrocrNkLcjPYRP1tm0=
|
||||
github.com/gocarina/gocsv v0.0.0-20200302151839-87c60d755c58/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/maddie/go-ping v0.0.0-20200311033510-5e9a13ec8da6 h1:ApVK0ZXs0wyZmj4dSelnorxkJguUhVLXxT+ghrXtNQY=
|
||||
github.com/maddie/go-ping v0.0.0-20200311033510-5e9a13ec8da6/go.mod h1:IYhVIzcOSIO1fLDdEz4JFidYhLPmmW3dchKuengmL9s=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ=
|
||||
github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
|
||||
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.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=
|
||||
|
|
72
main.go
72
main.go
|
@ -6,8 +6,8 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"librespeed-cli/defs"
|
||||
"librespeed-cli/speedtest"
|
||||
"github.com/librespeed/speedtest-cli/defs"
|
||||
"github.com/librespeed/speedtest-cli/speedtest"
|
||||
)
|
||||
|
||||
// init sets up the essential bits on start up
|
||||
|
@ -20,6 +20,7 @@ func init() {
|
|||
// warn level is for suppress modes
|
||||
// error level is for errors
|
||||
|
||||
log.SetOutput(os.Stderr)
|
||||
log.SetFormatter(formatter)
|
||||
log.SetLevel(log.InfoLevel)
|
||||
}
|
||||
|
@ -28,7 +29,7 @@ func main() {
|
|||
// define cli options
|
||||
app := &cli.App{
|
||||
Name: "librespeed-cli",
|
||||
Usage: "Test your Internet speed with LibreSpeed 🚀",
|
||||
Usage: "Test your Internet speed with LibreSpeed",
|
||||
Action: speedtest.SpeedTest,
|
||||
HideHelp: true,
|
||||
Flags: []cli.Flag{
|
||||
|
@ -55,6 +56,16 @@ func main() {
|
|||
Name: defs.OptionNoUpload,
|
||||
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{
|
||||
Name: defs.OptionBytes,
|
||||
Usage: "Display values in bytes instead of bits. Does not affect\n" +
|
||||
|
@ -99,7 +110,7 @@ func main() {
|
|||
Name: defs.OptionJSON,
|
||||
Usage: "Suppress verbose output, only show basic information\n" +
|
||||
"\tin JSON format. Speeds listed in bit/s and not\n" +
|
||||
"\t affected by --bytes",
|
||||
"\taffected by --bytes",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: defs.OptionList,
|
||||
|
@ -120,23 +131,52 @@ func main() {
|
|||
Usage: "Use an alternative server list from remote JSON file",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defs.OptionLocalJSON,
|
||||
Usage: "Use an alternative server list from local JSON file",
|
||||
Name: defs.OptionLocalJSON,
|
||||
Usage: "Use an alternative server list from local JSON file,\n" +
|
||||
"\tor read from stdin with \"--" + defs.OptionLocalJSON + " -\".",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defs.OptionSource,
|
||||
Usage: "`SOURCE` IP address to bind to",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defs.OptionInterface,
|
||||
Usage: "network INTERFACE to bind to",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: defs.OptionTimeout,
|
||||
Usage: "HTTP `TIMEOUT` in seconds.",
|
||||
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{
|
||||
Name: defs.OptionSecure,
|
||||
Usage: "Use HTTPS instead of HTTP when communicating with\n" +
|
||||
"\tLibreSpeed.org operated servers",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defs.OptionCACert,
|
||||
Usage: "Use the specified CA certificate PEM bundle file instead\n" +
|
||||
"\tof the system certificate trust store",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: defs.OptionSkipCertVerify,
|
||||
Usage: "Skip verifying SSL certificate for HTTPS connections (self-signed certs)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: defs.OptionNoPreAllocate,
|
||||
Usage: "Do not pre allocate upload data. Pre allocation is\n" +
|
||||
|
@ -154,28 +194,34 @@ func main() {
|
|||
Name: defs.OptionTelemetryJSON,
|
||||
Usage: "Load telemetry server settings from a JSON file. This\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{
|
||||
Name: defs.OptionTelemetryLevel,
|
||||
Usage: "Set telemetry data verbosity, available values are:\n" +
|
||||
"\tdisabled, basic, full, debug",
|
||||
"\tdisabled, basic, full, debug. Implies --" + defs.OptionShare,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defs.OptionTelemetryServer,
|
||||
Usage: "Set the telemetry server base URL",
|
||||
Usage: "Set the telemetry server base URL. Implies --" + defs.OptionShare,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defs.OptionTelemetryPath,
|
||||
Usage: "Set the telemetry upload path",
|
||||
Usage: "Set the telemetry upload path. Implies --" + defs.OptionShare,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defs.OptionTelemetryShare,
|
||||
Usage: "Set the telemetry share link path",
|
||||
Usage: "Set the telemetry share link path. Implies --" + defs.OptionShare,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defs.OptionTelemetryExtra,
|
||||
Usage: "Send a custom message along with the telemetry results",
|
||||
Name: defs.OptionTelemetryExtra,
|
||||
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 (
|
||||
"time"
|
||||
|
||||
"librespeed-cli/defs"
|
||||
"github.com/librespeed/speedtest-cli/defs"
|
||||
)
|
||||
|
||||
// JSONReport represents the output data fields in a JSON file
|
||||
|
@ -11,8 +11,8 @@ type JSONReport struct {
|
|||
Timestamp time.Time `json:"timestamp"`
|
||||
Server Server `json:"server"`
|
||||
Client Client `json:"client"`
|
||||
BytesSent int `json:"bytes_sent"`
|
||||
BytesReceived int `json:"bytes_received"`
|
||||
BytesSent uint64 `json:"bytes_sent"`
|
||||
BytesReceived uint64 `json:"bytes_received"`
|
||||
Ping float64 `json:"ping"`
|
||||
Jitter float64 `json:"jitter"`
|
||||
Upload float64 `json:"upload"`
|
||||
|
|
|
@ -8,17 +8,17 @@ import (
|
|||
"math"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/gocarina/gocsv"
|
||||
"github.com/librespeed/speedtest-cli/defs"
|
||||
"github.com/librespeed/speedtest-cli/report"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"librespeed-cli/defs"
|
||||
"librespeed-cli/report"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -27,11 +27,14 @@ const (
|
|||
)
|
||||
|
||||
// doSpeedTest is where the actual speed test happens
|
||||
func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.TelemetryServer, network string, silent bool) error {
|
||||
func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.TelemetryServer, network string, silent bool, noICMP bool) error {
|
||||
if serverCount := len(servers); serverCount > 1 {
|
||||
log.Infof("Testing against %d servers", serverCount)
|
||||
}
|
||||
|
||||
var reps_json []report.JSONReport
|
||||
var reps_csv []report.CSVReport
|
||||
|
||||
// fetch current user's IP info
|
||||
for _, currentServer := range servers {
|
||||
// get telemetry level
|
||||
|
@ -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())
|
||||
|
||||
if sponsorMsg := currentServer.Sponsor(); sponsorMsg != "" {
|
||||
log.Infof("Sponsored by: %s", sponsorMsg)
|
||||
}
|
||||
|
||||
if currentServer.IsUp() {
|
||||
ispInfo, err := currentServer.GetIPInfo(c.String(defs.OptionDistance))
|
||||
if err != nil {
|
||||
|
@ -61,6 +68,9 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
|
|||
pb.Start()
|
||||
}
|
||||
|
||||
// skip ICMP if option given
|
||||
currentServer.NoICMP = noICMP
|
||||
|
||||
p, jitter, err := currentServer.ICMPPingAndJitter(pingCount, c.String(defs.OptionSource), network)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get ping and jitter: %s", err)
|
||||
|
@ -68,53 +78,53 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
|
|||
}
|
||||
|
||||
if pb != nil {
|
||||
pb.FinalMSG = fmt.Sprintf("Ping: %.0f ms\tJitter: %.0f ms\n", p, jitter)
|
||||
pb.FinalMSG = fmt.Sprintf("Ping: %.2f ms\tJitter: %.2f ms\n", p, jitter)
|
||||
pb.Stop()
|
||||
}
|
||||
|
||||
// get download value
|
||||
var downloadValue float64
|
||||
var bytesRead int
|
||||
var bytesRead uint64
|
||||
if c.Bool(defs.OptionNoDownload) {
|
||||
log.Info("Download test is disabled")
|
||||
} else {
|
||||
download, br, err := currentServer.Download(silent, c.Bool(defs.OptionBytes), c.Bool(defs.OptionMebiBytes))
|
||||
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 {
|
||||
log.Errorf("Failed to get download speed: %s", err)
|
||||
return err
|
||||
}
|
||||
downloadValue = download
|
||||
bytesRead = br
|
||||
bytesRead = uint64(br)
|
||||
}
|
||||
|
||||
// get upload value
|
||||
var uploadValue float64
|
||||
var bytesWritten int
|
||||
var bytesWritten uint64
|
||||
if c.Bool(defs.OptionNoUpload) {
|
||||
log.Info("Upload test is disabled")
|
||||
} else {
|
||||
upload, bw, err := currentServer.Upload(c.Bool(defs.OptionNoPreAllocate), silent, c.Bool(defs.OptionBytes), c.Bool(defs.OptionMebiBytes))
|
||||
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 {
|
||||
log.Errorf("Failed to get upload speed: %s", err)
|
||||
return err
|
||||
}
|
||||
uploadValue = upload
|
||||
bytesWritten = bw
|
||||
bytesWritten = uint64(bw)
|
||||
}
|
||||
|
||||
// print result if --simple is given
|
||||
if c.Bool(defs.OptionSimple) {
|
||||
if c.Bool(defs.OptionBytes) {
|
||||
useMebi := c.Bool(defs.OptionMebiBytes)
|
||||
log.Warnf("Ping:\t%.0f ms\tJitter:\t%.0f ms\nDownload rate:\t%s\nUpload rate:\t%s", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi))
|
||||
log.Warnf("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%s\nUpload rate:\t%s", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi))
|
||||
} else {
|
||||
log.Warnf("Ping:\t%.0f ms\tJitter:\t%.0f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps", p, jitter, downloadValue, uploadValue)
|
||||
log.Warnf("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps", p, jitter, downloadValue, uploadValue)
|
||||
}
|
||||
}
|
||||
|
||||
// print share link if --share is given
|
||||
var shareLink string
|
||||
if c.Bool(defs.OptionShare) && !c.Bool(defs.OptionCSV) && telemetryServer.GetLevel() > 0 {
|
||||
if telemetryServer.GetLevel() > 0 {
|
||||
var extra defs.TelemetryExtra
|
||||
extra.ServerName = currentServer.Name
|
||||
extra.Extra = c.String(defs.OptionTelemetryExtra)
|
||||
|
@ -123,41 +133,35 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
|
|||
log.Errorf("Error when sending telemetry data: %s", err)
|
||||
} else {
|
||||
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
|
||||
if c.Bool(defs.OptionCSV) {
|
||||
// print csv if --csv is given
|
||||
var reps []report.CSVReport
|
||||
|
||||
var rep report.CSVReport
|
||||
rep.Timestamp = time.Now()
|
||||
|
||||
rep.Name = currentServer.Name
|
||||
rep.Address = u.String()
|
||||
rep.Ping = p
|
||||
rep.Ping = math.Round(p*100) / 100
|
||||
rep.Jitter = math.Round(jitter*100) / 100
|
||||
rep.Download = math.Round(downloadValue*100) / 100
|
||||
rep.Upload = math.Round(uploadValue*100) / 100
|
||||
rep.Share = shareLink
|
||||
rep.IP = ispInfo.RawISPInfo.IP
|
||||
|
||||
reps = append(reps, rep)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := gocsv.MarshalWithoutHeaders(&reps, &buf); err != nil {
|
||||
log.Errorf("Error generating CSV report: %s", err)
|
||||
} else {
|
||||
log.Warn(buf.String())
|
||||
}
|
||||
reps_csv = append(reps_csv, rep)
|
||||
} else if c.Bool(defs.OptionJSON) {
|
||||
// print json if --json is given
|
||||
var rep report.JSONReport
|
||||
rep.Timestamp = time.Now()
|
||||
|
||||
rep.Ping = p
|
||||
rep.Ping = math.Round(p*100) / 100
|
||||
rep.Jitter = math.Round(jitter*100) / 100
|
||||
rep.Download = math.Round(downloadValue*100) / 100
|
||||
rep.Upload = math.Round(uploadValue*100) / 100
|
||||
|
@ -171,22 +175,34 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel
|
|||
rep.Client = report.Client{ispInfo.RawISPInfo}
|
||||
rep.Client.Readme = ""
|
||||
|
||||
if b, err := json.Marshal(&rep); err != nil {
|
||||
log.Errorf("Error generating JSON report: %s", err)
|
||||
} else {
|
||||
log.Warnf("%s", b)
|
||||
}
|
||||
reps_json = append(reps_json, rep)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Selected server %s (%s) is not responding at the moment, try again later", currentServer.Name, u.Hostname())
|
||||
}
|
||||
|
||||
// add a new line after each test if testing multiple servers
|
||||
if len(servers) > 1 {
|
||||
//add a new line after each test if testing multiple servers
|
||||
if len(servers) > 1 && !silent {
|
||||
log.Warn()
|
||||
}
|
||||
}
|
||||
|
||||
// check for --csv or --json. the program prioritize the --csv before the --json. this is the same behavior as speedtest-cli
|
||||
if c.Bool(defs.OptionCSV) {
|
||||
var buf bytes.Buffer
|
||||
if err := gocsv.MarshalWithoutHeaders(&reps_csv, &buf); err != nil {
|
||||
log.Errorf("Error generating CSV report: %s", err)
|
||||
} else {
|
||||
os.Stdout.WriteString(buf.String())
|
||||
}
|
||||
} else if c.Bool(defs.OptionJSON) {
|
||||
if b, err := json.Marshal(&reps_json); err != nil {
|
||||
log.Errorf("Error generating JSON report: %s", err)
|
||||
} else {
|
||||
os.Stdout.Write(b[:])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -223,7 +239,7 @@ func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResu
|
|||
if fPing, err := wr.CreateFormField("ping"); err != nil {
|
||||
log.Debugf("Error creating form field: %s", err)
|
||||
return "", err
|
||||
} else if _, err = fPing.Write([]byte(strconv.Itoa(int(pingVal)))); err != nil {
|
||||
} else if _, err = fPing.Write([]byte(strconv.FormatFloat(pingVal, 'f', 2, 64))); err != nil {
|
||||
log.Debugf("Error writing form field: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
@ -231,7 +247,7 @@ func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResu
|
|||
if fJitter, err := wr.CreateFormField("jitter"); err != nil {
|
||||
log.Debugf("Error creating form field: %s", err)
|
||||
return "", err
|
||||
} else if _, err = fJitter.Write([]byte(strconv.Itoa(int(jitter)))); err != nil {
|
||||
} else if _, err = fJitter.Write([]byte(strconv.FormatFloat(jitter, 'f', 2, 64))); err != nil {
|
||||
log.Debugf("Error writing form field: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -2,11 +2,16 @@ package speedtest
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -15,18 +20,18 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"librespeed-cli/defs"
|
||||
"librespeed-cli/report"
|
||||
"github.com/librespeed/speedtest-cli/defs"
|
||||
"github.com/librespeed/speedtest-cli/report"
|
||||
)
|
||||
|
||||
const (
|
||||
// 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"
|
||||
defaultTelemetryServer = "https://librespeed.org"
|
||||
defaultTelemetryPath = "/results/telemetry.php"
|
||||
defaultTeleemtryShare = "/results/"
|
||||
defaultTelemetryShare = "/results/"
|
||||
)
|
||||
|
||||
type PingJob struct {
|
||||
|
@ -60,6 +65,7 @@ func SpeedTest(c *cli.Context) error {
|
|||
|
||||
// print version
|
||||
if c.Bool(defs.OptionVersion) {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.Warnf("%s %s (built on %s)", defs.ProgName, defs.ProgVersion, defs.BuildDate)
|
||||
log.Warn("https://github.com/librespeed/speedtest-cli")
|
||||
log.Warn("Licensed under GNU Lesser General Public License v3.0")
|
||||
|
@ -69,6 +75,10 @@ func SpeedTest(c *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if c.String(defs.OptionSource) != "" && c.String(defs.OptionInterface) != "" {
|
||||
return fmt.Errorf("incompatible options '%s' and '%s'", defs.OptionSource, defs.OptionInterface)
|
||||
}
|
||||
|
||||
// set CSV delimiter
|
||||
gocsv.TagSeparator = c.String(defs.OptionCSVDelimiter)
|
||||
|
||||
|
@ -76,53 +86,65 @@ func SpeedTest(c *cli.Context) error {
|
|||
if c.Bool(defs.OptionCSVHeader) {
|
||||
var rep []report.CSVReport
|
||||
b, _ := gocsv.MarshalBytes(&rep)
|
||||
log.Warnf("%s", b)
|
||||
os.Stdout.WriteString(string(b))
|
||||
return nil
|
||||
}
|
||||
|
||||
// read telemetry settings if --share is given
|
||||
// read telemetry settings if --share or any --telemetry option is given
|
||||
var telemetryServer defs.TelemetryServer
|
||||
if c.Bool(defs.OptionShare) {
|
||||
if filename := c.String(defs.OptionTelemetryJSON); filename != "" {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
telemetryJSON := c.String(defs.OptionTelemetryJSON)
|
||||
telemetryLevel := c.String(defs.OptionTelemetryLevel)
|
||||
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 {
|
||||
log.Errorf("Cannot read %s: %s", filename, err)
|
||||
log.Errorf("Cannot read %s: %s", telemetryJSON, err)
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(b, &telemetryServer); err != nil {
|
||||
log.Errorf("Error parsing %s: %s", err)
|
||||
log.Errorf("Error parsing %s: %s", telemetryJSON, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if str := c.String(defs.OptionTelemetryLevel); str != "" {
|
||||
if str != "disabled" && str != "basic" && str != "full" && str != "debug" {
|
||||
log.Fatalf("Unsupported telemetry level: %s", str)
|
||||
if telemetryLevel != "" {
|
||||
if telemetryLevel != "disabled" && telemetryLevel != "basic" && telemetryLevel != "full" && telemetryLevel != "debug" {
|
||||
log.Fatalf("Unsupported telemetry level: %s", telemetryLevel)
|
||||
}
|
||||
telemetryServer.Level = str
|
||||
telemetryServer.Level = telemetryLevel
|
||||
} else if telemetryServer.Level == "" {
|
||||
telemetryServer.Level = defaultTelemetryLevel
|
||||
}
|
||||
|
||||
if str := c.String(defs.OptionTelemetryServer); str != "" {
|
||||
telemetryServer.Server = str
|
||||
if telemetryServerString != "" {
|
||||
telemetryServer.Server = telemetryServerString
|
||||
} else if telemetryServer.Server == "" {
|
||||
telemetryServer.Server = defaultTelemetryServer
|
||||
}
|
||||
|
||||
if str := c.String(defs.OptionTelemetryPath); str != "" {
|
||||
telemetryServer.Path = str
|
||||
if telemetryPath != "" {
|
||||
telemetryServer.Path = telemetryPath
|
||||
} else if telemetryServer.Path == "" {
|
||||
telemetryServer.Path = defaultTelemetryPath
|
||||
}
|
||||
|
||||
if str := c.String(defs.OptionTelemetryShare); str != "" {
|
||||
telemetryServer.Share = str
|
||||
if telemetryShare != "" {
|
||||
telemetryServer.Share = telemetryShare
|
||||
} 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.DefaultClient.Timeout = time.Duration(c.Int(defs.OptionTimeout)) * time.Second
|
||||
|
||||
|
@ -139,83 +161,102 @@ func SpeedTest(c *cli.Context) error {
|
|||
network = "ip"
|
||||
}
|
||||
|
||||
// bind to source IP address if given, or if ipv4/ipv6 is forced
|
||||
if src := c.String(defs.OptionSource); src != "" || (forceIPv4 || forceIPv6) {
|
||||
var localTCPAddr *net.TCPAddr
|
||||
if src != "" {
|
||||
// first we parse the IP to see if it's valid
|
||||
addr, err := net.ResolveIPAddr(network, src)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no suitable address") {
|
||||
if forceIPv6 {
|
||||
log.Errorf("Address %s is not a valid IPv6 address", src)
|
||||
} else {
|
||||
log.Errorf("Address %s is not a valid IPv4 address", src)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("Error parsing source IP: %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
log.Debugf("Using %s as source IP", src)
|
||||
localTCPAddr = &net.TCPAddr{IP: addr.IP}
|
||||
if caCertFileName := c.String(defs.OptionCACert); caCertFileName != "" {
|
||||
caCert, err := ioutil.ReadFile(caCertFileName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
var dialContext func(context.Context, string, string) (net.Conn, error)
|
||||
defaultDialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify),
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
|
||||
if localTCPAddr != nil {
|
||||
defaultDialer.LocalAddr = localTCPAddr
|
||||
} else {
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify),
|
||||
}
|
||||
|
||||
switch {
|
||||
case forceIPv4:
|
||||
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||
return defaultDialer.DialContext(ctx, "tcp4", address)
|
||||
}
|
||||
case forceIPv6:
|
||||
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||
return defaultDialer.DialContext(ctx, "tcp6", address)
|
||||
}
|
||||
default:
|
||||
dialContext = defaultDialer.DialContext
|
||||
}
|
||||
|
||||
// set default HTTP client's Transport to the one that binds the source address
|
||||
// this is modified from http.DefaultTransport
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: dialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
http.DefaultClient.Transport = transport
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
// bind to source IP address if given
|
||||
if src := c.String(defs.OptionSource); src != "" {
|
||||
var err error
|
||||
dialer, err = newDialerAddressBound(src, network)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// bind to interface if given
|
||||
// bind to interface if given
|
||||
iface := c.String(defs.OptionInterface)
|
||||
fwmark := c.Int(defs.OptionFwmark)
|
||||
|
||||
if iface != "" || fwmark > 0 {
|
||||
var err error
|
||||
dialer, err = newDialerInterfaceOrFwmarkBound(iface, fwmark)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ICMP ping does not support interface binding.
|
||||
noICMP = true
|
||||
}
|
||||
|
||||
// enforce if ipv4/ipv6 is forced
|
||||
var dialContext func(context.Context, string, string) (net.Conn, error)
|
||||
switch {
|
||||
case forceIPv4:
|
||||
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||
return dialer.DialContext(ctx, "tcp4", address)
|
||||
}
|
||||
case forceIPv6:
|
||||
dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||
return dialer.DialContext(ctx, "tcp6", address)
|
||||
}
|
||||
default:
|
||||
dialContext = dialer.DialContext
|
||||
}
|
||||
|
||||
// set default HTTP client's Transport to the one that binds the source address
|
||||
// this is modified from http.DefaultTransport
|
||||
transport.DialContext = dialContext
|
||||
http.DefaultClient.Transport = transport
|
||||
|
||||
// load server list
|
||||
var servers []defs.Server
|
||||
var err error
|
||||
if str := c.String(defs.OptionLocalJSON); str != "" {
|
||||
// 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))
|
||||
switch str {
|
||||
case "-":
|
||||
// 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 {
|
||||
// fetch the server list JSON and parse it into the `servers` array
|
||||
log.Info("Retrieving LibreSpeed.org server list")
|
||||
serverUrl := serverListUrl
|
||||
if str := c.String(defs.OptionServerJSON); 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))
|
||||
|
||||
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 {
|
||||
log.Errorf("Error when fetching server list: %s", err)
|
||||
|
@ -224,29 +265,33 @@ func SpeedTest(c *cli.Context) error {
|
|||
|
||||
// if --list is given, list all the servers fetched and exit
|
||||
if c.Bool(defs.OptionList) {
|
||||
for idx, svr := range servers {
|
||||
log.Warnf("%d: %s (%s)", idx, svr.Name, svr.Server)
|
||||
for _, svr := range servers {
|
||||
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
|
||||
}
|
||||
|
||||
// if --server is given, do speed tests with all of them
|
||||
if len(c.IntSlice(defs.OptionServer)) > 0 {
|
||||
return doSpeedTest(c, servers, telemetryServer, network, silent)
|
||||
return doSpeedTest(c, servers, telemetryServer, network, silent, noICMP)
|
||||
} else {
|
||||
// else select the fastest server from the list
|
||||
log.Info("Selecting the fastest server based on ping")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
jobs := make(chan PingJob, 10)
|
||||
results := make(chan PingResult, 10)
|
||||
jobs := make(chan PingJob, len(servers))
|
||||
results := make(chan PingResult, len(servers))
|
||||
done := make(chan struct{})
|
||||
|
||||
pingList := make(map[int]float64)
|
||||
|
||||
// spawn 10 concurrent pingers
|
||||
for i := 0; i < 10; i++ {
|
||||
go pingWorker(jobs, results, &wg, c.String(defs.OptionSource), network)
|
||||
go pingWorker(jobs, results, &wg, c.String(defs.OptionSource), network, noICMP)
|
||||
}
|
||||
|
||||
// send ping jobs to workers
|
||||
|
@ -283,11 +328,11 @@ func SpeedTest(c *cli.Context) error {
|
|||
}
|
||||
|
||||
// do speed test on the server
|
||||
return doSpeedTest(c, []defs.Server{servers[serverIdx]}, telemetryServer, network, silent)
|
||||
return doSpeedTest(c, []defs.Server{servers[serverIdx]}, telemetryServer, network, silent, noICMP)
|
||||
}
|
||||
}
|
||||
|
||||
func pingWorker(jobs <-chan PingJob, results chan<- PingResult, wg *sync.WaitGroup, srcIp, network string) {
|
||||
func pingWorker(jobs <-chan PingJob, results chan<- PingResult, wg *sync.WaitGroup, srcIp, network string, noICMP bool) {
|
||||
for {
|
||||
job := <-jobs
|
||||
server := job.Server
|
||||
|
@ -301,6 +346,9 @@ 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
|
||||
if server.IsUp() {
|
||||
// skip ICMP if option given
|
||||
server.NoICMP = noICMP
|
||||
|
||||
// if server is up, get ping
|
||||
ping, _, err := server.ICMPPingAndJitter(1, srcIp, network)
|
||||
if err != nil {
|
||||
|
@ -327,7 +375,13 @@ func getServerList(forceHTTPS bool, serverList string, excludes, specific []int,
|
|||
|
||||
// getting the server list from remote
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -345,10 +399,13 @@ func getServerList(forceHTTPS bool, serverList string, excludes, specific []int,
|
|||
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) {
|
||||
// getLocalServersReader loads the server JSON from an io.Reader
|
||||
func getLocalServersReader(forceHTTPS bool, reader io.ReadCloser, excludes, specific []int, filter bool) ([]defs.Server, error) {
|
||||
defer reader.Close()
|
||||
|
||||
var servers []defs.Server
|
||||
b, err := ioutil.ReadFile(jsonFile)
|
||||
|
||||
b, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -360,6 +417,15 @@ func getLocalServers(forceHTTPS bool, jsonFile string, excludes, specific []int,
|
|||
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
|
||||
func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specific []int, filter bool) ([]defs.Server, error) {
|
||||
for i := range servers {
|
||||
|
@ -389,8 +455,8 @@ func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specifi
|
|||
// exclude servers from --exclude
|
||||
if len(excludes) > 0 {
|
||||
var ret []defs.Server
|
||||
for idx, server := range servers {
|
||||
if contains(excludes, idx) {
|
||||
for _, server := range servers {
|
||||
if contains(excludes, server.ID) {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, server)
|
||||
|
@ -402,11 +468,15 @@ func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specifi
|
|||
// special value -1 will test all servers
|
||||
if len(specific) > 0 && !contains(specific, -1) {
|
||||
var ret []defs.Server
|
||||
for idx, server := range servers {
|
||||
if contains(specific, idx) {
|
||||
for _, server := range servers {
|
||||
if contains(specific, server.ID) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -423,3 +493,31 @@ func contains(arr []int, val int) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func newDialerAddressBound(src string, network string) (dialer *net.Dialer, err error) {
|
||||
// first we parse the IP to see if it's valid
|
||||
addr, err := net.ResolveIPAddr(network, src)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no suitable address") {
|
||||
if network == "ip6" {
|
||||
log.Errorf("Address %s is not a valid IPv6 address", src)
|
||||
} else {
|
||||
log.Errorf("Address %s is not a valid IPv4 address", src)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("Error parsing source IP: %s", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Using %s as source IP", src)
|
||||
localTCPAddr := &net.TCPAddr{IP: addr.IP}
|
||||
|
||||
defaultDialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
|
||||
defaultDialer.LocalAddr = localTCPAddr
|
||||
return defaultDialer, nil
|
||||
}
|
||||
|
|
13
speedtest/util.go
Normal file
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