pax_global_header00006660000000000000000000000064146700620720014516gustar00rootroot0000000000000052 comment=d33e431b58774807f608f7a38bd5e5fd9cc3dfc4 speedtest-cli-1.0.11/000077500000000000000000000000001467006207200143435ustar00rootroot00000000000000speedtest-cli-1.0.11/.gitignore000066400000000000000000000050511467006207200163340ustar00rootroot00000000000000# Created by .ignore support plugin (hsz.mobi) ### Linux template *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS template # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### Go template # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ dist/ speedtest-cli-1.0.11/.goreleaser.yml000066400000000000000000000032211467006207200172720ustar00rootroot00000000000000version: 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 gomips: - hardfloat - softfloat ignore: - goos: linux goarch: "386" - goos: linux goarch: amd64 - goos: linux goarch: arm64 - goos: darwin goarch: "386" - goos: darwin goarch: amd64 archives: - format_overrides: - goos: windows format: zip files: - LICENSE checksum: name_template: "checksums.txt" changelog: disable: false sort: asc release: github: owner: librespeed name: speedtest-cli disable: false speedtest-cli-1.0.11/LICENSE000066400000000000000000000167441467006207200153640ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. speedtest-cli-1.0.11/README.md000066400000000000000000000276061467006207200156350ustar00rootroot00000000000000![LibreSpeed Logo](https://github.com/librespeed/speedtest/blob/master/.logo/logo3.png?raw=true) # LibreSpeed command line tool Don't have a GUI but want to use LibreSpeed servers to test your Internet speed? 🚀 `librespeed-cli` comes to rescue! This is a command line interface for LibreSpeed speed test backends, written in Go. ## Features - Ping - Jitter - Download - Upload - IP address - ISP Information - Result sharing (telemetry) *[optional]* - Test with multiple servers in a single run - Use your own server list or telemetry endpoints - Tested with PHP and Go backends [![asciicast](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2.svg)](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2) ## Requirements for compiling - Go 1.14+ ## 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 (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: ```shell script # pacman -S go ``` 2. Then, clone the repository: ```shell script $ git clone -b v1.0.0 https://github.com/librespeed/speedtest-cli ``` 3. After you have Go installed on your system (and added to your `$PATH` if you're using the archive from golang.org), you can now proceed to build `librespeed-cli` with the build script: ```shell script $ cd speedtest-cli $ ./build.sh ``` 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 ``` 4. When the build script finishes, if everything went smoothly, you can find the built binary under directory `out`. ```shell script $ ls out librespeed-cli-windows-amd64.exe ``` 5. Now you can use the `librespeed-cli` and test your Internet speed! ## Install from AUR To install `librespeed-cli` from AUR, use your favorite AUR helper and install package `librespeed-cli-bin` ```shell script $ yay librespeed-cli-bin ``` ... or, clone it and build it yourself: ```shell script $ git clone https://aur.archlinux.org/librespeed-cli-bin.git $ cd librespeed-cli-bin $ makepkg -si ``` ## Install from Homebrew See the [librespeed-cli Homebrew tap](https://github.com/librespeed/homebrew-tap#setup). ## Install on Windows If you have either [Scoop](https://scoop.sh/) or [Chocolatey](https://chocolatey.org/) installed you can use one of the following commands: - Scoop (ensure you have the `extras` bucket added): ``` > scoop install librespeed-cli ``` - Chocolatey: ``` > choco install librespeed-cli ``` ## Container Image You can run `librespeed-cli` in a container. 1. Build the container image: ```shell script docker build -t librespeed-cli:latest . ``` 2. Run the container: ```shell script docker run --rm --name librespeed-cli librespeed-cli:latest # With options docker run --rm --name librespeed-cli librespeed-cli:latest --telemetry-level disabled --no-upload # To avoid "Failed to ping target host: socket: permission denied" errors when using --verbose docker run --rm --name librespeed-cli --sysctl net.ipv4.ping_group_range="0 2147483647" librespeed-cli:latest --verbose ``` ## Usage You can see the full list of supported options with `librespeed-cli -h`: ``` $ librespeed-cli -h NAME: librespeed-cli - Test your Internet speed with LibreSpeed 🚀 USAGE: librespeed-cli [global options] [arguments...] GLOBAL OPTIONS: --help, -h show help (default: false) --version Show the version number and exit (default: false) --ipv4, -4 Force IPv4 only (default: false) --ipv6, -6 Force IPv6 only (default: false) --no-download Do not perform download test (default: false) --no-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) --csv Suppress verbose output, only show basic information in CSV format. Speeds listed in bit/s and not affected by --bytes (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) --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, 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. Implies --share --telemetry-level value Set telemetry data verbosity, available values are: 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 The `librespeed-cli` supports loading custom backend server list from a JSON file (remotely via `--server-json` or locally via `--local-json`). The format is as below: ```json [ { "id": 1, "name": "PHP Backend", "server": "https://example.com/", "dlURL": "garbage.php", "ulURL": "empty.php", "pingURL": "empty.php", "getIpURL": "getIP.php" }, { "id": 2, "name": "Go Backend", "server": "http://example.com/speedtest/", "dlURL": "garbage", "ulURL": "empty", "pingURL": "empty", "getIpURL": "getIP" } ] ``` The `--local-json` option can also read from `stdin`: `echo '[{"id": 1,"name": "a","server": "https://speedtest.example.com/","dlURL": "garbage.php","ulURL": "empty.php","pingURL": "empty.php","getIpURL": "getIP.php"}]' | librespeed-cli --local-json - ` As you can see in the example, all servers have their schemes defined. In case of undefined scheme (e.g. `//example.com`), `librespeed-cli` will use `http` by default, or `https` when the `--secure` option is enabled. ## Use a custom telemetry server By default, the telemetry result will be sent to `librespeed.org`. You can also customize your telemetry settings 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: ```json { "telemetryLevel": "full", "server": "https://example.com", "path": "/results/telemetry.php", "shareURL": "/results/" } ``` For `telemetryLevel`, four values are available: - `disabled` to disable telemetry - `basic` to enable telemetry with result only) - `full` to enable telemetry with result and timing - `debug` to enable the most verbose telemetry information `server` defines the base URL for the backend's endpoints. `path` is the path for uploading the telemetry result, and `shareURL` is the path for fetching the uploaded result in PNG format. Currently, `--telemetry-json` only supports loading a local JSON file. ## Bugs? Although we have tested the cli, it's still in its early days. Please open an issue if you encounter any bugs, or even better, submit a PR. ## How to contribute If you have some good ideas on improving `librespeed-cli`, you can always submit a PR via GitHub. ## License `librespeed-cli` is licensed under [GNU Lesser General Public License v3.0](https://github.com/librespeed/speedtest-cli/blob/master/LICENSE) speedtest-cli-1.0.11/build.sh000077500000000000000000000021011467006207200157730ustar00rootroot00000000000000#!/usr/bin/env bash if [[ -z "$1" ]]; then PROGVER="$(git describe --tag)" else PROGVER="$1" fi 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 \"${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} fi if [[ -n "${GOMIPS}" ]]; then BINARY=${BINARY}-${GOMIPS} fi if [[ -n "${GOMIPS64}" ]]; then BINARY=${BINARY}-${GOMIPS64} fi if [[ "$(go env GOOS)" = "windows" ]]; then BINARY=${BINARY}.exe fi if [[ ! -d ${OUT_DIR} ]]; then mkdir "${OUT_DIR}" fi if [[ -e ${OUT_DIR}/${BINARY} ]]; then rm -f "${OUT_DIR}/${BINARY}" fi go build -o "${OUT_DIR}/${BINARY}" -ldflags "${LDFLAGS}" -trimpath main.go if [[ ! $(go env GOARCH) == mips64* ]] && [[ -x $(command -v upx) ]]; then upx -qqq -9 "${OUT_DIR}/${BINARY}" fi speedtest-cli-1.0.11/defs/000077500000000000000000000000001467006207200152645ustar00rootroot00000000000000speedtest-cli-1.0.11/defs/bytes_counter.go000066400000000000000000000066721467006207200205130ustar00rootroot00000000000000package defs import ( "bytes" "crypto/rand" "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 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.lock.Lock() c.total += uint64(n) c.lock.Unlock() return n, nil } // Read implements io.Reader func (c *BytesCounter) Read(p []byte) (int, error) { n, err := c.reader.Read(p) c.lock.Lock() c.total += uint64(n) c.pos += n if c.pos == c.uploadSize { c.resetReader() } c.lock.Unlock() return n, err } // SetBase sets the base for dividing bytes into megabyte or mebibyte func (c *BytesCounter) SetMebi(mebi bool) { c.mebi = mebi } // SetUploadSize sets the size of payload being uploaded func (c *BytesCounter) SetUploadSize(uploadSize int) { c.uploadSize = uploadSize * 1024 } // AvgBytes returns the average bytes/second func (c *BytesCounter) AvgBytes() float64 { return float64(c.total) / time.Now().Sub(c.start).Seconds() } // AvgMbps returns the average mbits/second func (c *BytesCounter) AvgMbps() float64 { var base float64 = 125000 if c.mebi { base = 131072 } return c.AvgBytes() / base } // AvgHumanize returns the average bytes/kilobytes/megabytes/gigabytes (or bytes/kibibytes/mebibytes/gibibytes) per second func (c *BytesCounter) AvgHumanize() string { val := c.AvgBytes() var base float64 = 1000 if c.mebi { base = 1024 } if val < base { return fmt.Sprintf("%.2f bytes/s", val) } else if val/base < base { return fmt.Sprintf("%.2f KB/s", val/base) } else if val/base/base < base { return fmt.Sprintf("%.2f MB/s", val/base/base) } else { return fmt.Sprintf("%.2f GB/s", val/base/base/base) } } // GenerateBlob generates a random byte array of `uploadSize` in the `payload` field, and sets the `reader` field to // read from it func (c *BytesCounter) GenerateBlob() { 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) { c.pos = 0 return c.reader.Seek(0, 0) } // Start will set the `start` field to current time func (c *BytesCounter) Start() { c.start = time.Now() } // Total returns the total bytes read/written func (c *BytesCounter) Total() uint64 { return c.total } // CurrentSpeed returns the current bytes/second func (c *BytesCounter) CurrentSpeed() float64 { return float64(c.total) / time.Now().Sub(c.start).Seconds() } // SeekWrapper is a wrapper around io.Reader to give it a noop io.Seeker interface type SeekWrapper struct { io.Reader } // Seek implements the io.Seeker interface func (r *SeekWrapper) Seek(offset int64, whence int) (int64, error) { return 0, nil } // getAvg returns the average value of an float64 array func getAvg(vals []float64) float64 { var total float64 for _, v := range vals { total += v } return total / float64(len(vals)) } // getRandomData returns an `length` sized array of random bytes func getRandomData(length int) []byte { data := make([]byte, length) if _, err := rand.Read(data); err != nil { log.Fatalf("Failed to generate random data: %s", err) } return data } speedtest-cli-1.0.11/defs/defs.go000066400000000000000000000015421467006207200165360ustar00rootroot00000000000000package defs var ( // values to be filled in by build script BuildDate string ProgName string ProgVersion string UserAgent = ProgName + "/" + ProgVersion ) // GetIPResults represents the returned JSON from backend server's getIP.php endpoint type GetIPResult struct { ProcessedString string `json:"processedString"` RawISPInfo IPInfoResponse `json:"rawIspInfo"` } // IPInfoResponse represents the returned JSON from IPInfo.io's API type IPInfoResponse struct { IP string `json:"ip"` Hostname string `json:"hostname"` City string `json:"city"` Region string `json:"region"` Country string `json:"country"` Location string `json:"loc"` Organization string `json:"org"` Postal string `json:"postal"` Timezone string `json:"timezone"` Readme string `json:"readme,omitempty"` } speedtest-cli-1.0.11/defs/log_formatter.go000066400000000000000000000005071467006207200204610ustar00rootroot00000000000000package defs import ( "fmt" log "github.com/sirupsen/logrus" ) // NoFormatter is the formatter for logrus type NoFormatter struct{} // Format prints the log message without timestamp/log level etc. func (f *NoFormatter) Format(entry *log.Entry) ([]byte, error) { return []byte(fmt.Sprintf("%s\n", entry.Message)), nil } speedtest-cli-1.0.11/defs/options.go000066400000000000000000000027541467006207200173160ustar00rootroot00000000000000package defs const ( OptionHelp = "help" OptionIPv4 = "ipv4" OptionIPv4Alt = "4" OptionIPv6 = "ipv6" OptionIPv6Alt = "6" OptionNoDownload = "no-download" OptionNoUpload = "no-upload" OptionNoICMP = "no-icmp" OptionConcurrent = "concurrent" OptionBytes = "bytes" OptionMebiBytes = "mebibytes" OptionDistance = "distance" OptionShare = "share" OptionSimple = "simple" OptionCSV = "csv" OptionCSVDelimiter = "csv-delimiter" OptionCSVHeader = "csv-header" OptionJSON = "json" OptionList = "list" OptionServer = "server" 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" OptionDebug = "debug" OptionTelemetryJSON = "telemetry-json" OptionTelemetryLevel = "telemetry-level" OptionTelemetryServer = "telemetry-server" OptionTelemetryPath = "telemetry-path" OptionTelemetryShare = "telemetry-share" OptionTelemetryExtra = "telemetry-extra" ) speedtest-cli-1.0.11/defs/server.go000066400000000000000000000260021467006207200171210ustar00rootroot00000000000000package defs import ( "context" "crypto/rand" "encoding/json" "errors" "fmt" "io" "io/ioutil" "math" "net/http" "net/url" "path" "strconv" "time" "github.com/briandowns/spinner" "github.com/go-ping/ping" log "github.com/sirupsen/logrus" ) // Server represents a speed test server type Server struct { 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 func (s *Server) IsUp() bool { t := time.Now() defer func() { s.TLog.Logf("Check backend is up took %s", time.Now().Sub(t).String()) }() u, _ := s.GetURL() u.Path = path.Join(u.Path, s.PingURL) 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 resp.StatusCode == http.StatusOK } // ICMPPingAndJitter pings the server via ICMP echos and calculate the average ping and jitter func (s *Server) ICMPPingAndJitter(count int, srcIp, network string) (float64, float64, error) { t := time.Now() defer func() { s.TLog.Logf("ICMP ping took %s", time.Now().Sub(t).String()) }() if s.NoICMP { log.Debugf("Skipping ICMP for server %s, will use HTTP ping", s.Name) return s.PingAndJitter(count + 2) } u, err := s.GetURL() if err != nil { log.Debugf("Failed to get server URL: %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 != "" { p.Source = srcIp } if log.GetLevel() == log.DebugLevel { p.Debug = true } if err := p.Run(); err != nil { log.Debugf("Failed to ping target host: %s", err) log.Debug("Will try TCP ping") return s.PingAndJitter(count + 2) } stats := p.Statistics() var lastPing, jitter float64 for idx, rtt := range stats.Rtts { if idx != 0 { instJitter := math.Abs(lastPing - float64(rtt.Milliseconds())) if idx > 1 { if jitter > instJitter { jitter = jitter*0.7 + instJitter*0.3 } else { jitter = instJitter*0.2 + jitter*0.8 } } } lastPing = float64(rtt.Milliseconds()) } if len(stats.Rtts) == 0 { 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) } return float64(stats.AvgRtt.Milliseconds()), jitter, nil } // 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() { s.TLog.Logf("TCP ping took %s", time.Now().Sub(t).String()) }() u, err := s.GetURL() if err != nil { log.Debugf("Failed to get server URL: %s", err) return 0, 0, err } u.Path = path.Join(u.Path, s.PingURL) var pings []float64 req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { log.Debugf("Failed when creating HTTP request: %s", err) return 0, 0, err } req.Header.Set("User-Agent", UserAgent) for i := 0; i < count; i++ { start := time.Now() resp, err := http.DefaultClient.Do(req) if err != nil { log.Debugf("Failed when making HTTP request: %s", err) return 0, 0, err } io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() end := time.Now() pings = append(pings, float64(end.Sub(start).Milliseconds())) } // discard first result due to handshake overhead if len(pings) > 1 { pings = pings[1:] } var lastPing, jitter float64 for idx, p := range pings { if idx != 0 { instJitter := math.Abs(lastPing - p) if idx > 1 { if jitter > instJitter { jitter = jitter*0.7 + instJitter*0.3 } else { jitter = instJitter*0.2 + jitter*0.8 } } } lastPing = p } return getAvg(pings), jitter, nil } // Download performs the actual download test 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 := NewCounter() counter.SetMebi(useMebi) ctx, cancel := context.WithCancel(context.Background()) defer cancel() u, err := s.GetURL() if err != nil { log.Debugf("Failed to get server URL: %s", err) return 0, 0, err } u.Path = path.Join(u.Path, s.DownloadURL) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { log.Debugf("Failed when creating HTTP request: %s", err) return 0, 0, err } q := req.URL.Query() 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{}, requests) doDownload := func() { resp, err := http.DefaultClient.Do(req) if err != nil { log.Debugf("Failed when making HTTP request: %s", err) } else { defer resp.Body.Close() if _, err = io.Copy(ioutil.Discard, io.TeeReader(resp.Body, counter)); err != nil { if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { log.Debugf("Failed when reading HTTP response: %s", err) } } downloadDone <- struct{}{} } } counter.Start() if !silent { pb := spinner.New(spinner.CharSets[11], 100*time.Millisecond) pb.Prefix = "Downloading... " pb.PostUpdate = func(s *spinner.Spinner) { if useBytes { s.Suffix = fmt.Sprintf(" %s", counter.AvgHumanize()) } else { s.Suffix = fmt.Sprintf(" %.2f Mbps", counter.AvgMbps()) } } pb.Start() defer func() { if useBytes { pb.FinalMSG = fmt.Sprintf("Download rate:\t%s\n", counter.AvgHumanize()) } else { pb.FinalMSG = fmt.Sprintf("Download rate:\t%.2f Mbps\n", counter.AvgMbps()) } pb.Stop() }() } for i := 0; i < requests; i++ { go doDownload() time.Sleep(200 * time.Millisecond) } timeout := time.After(duration) Loop: for { select { case <-timeout: ctx.Done() break Loop case <-downloadDone: go doDownload() } } return counter.AvgMbps(), counter.Total(), nil } // Upload performs the actual upload test 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 := NewCounter() counter.SetMebi(useMebi) counter.SetUploadSize(uploadSize) if noPrealloc { log.Info("Pre-allocation is disabled, performance might be lower!") counter.reader = &SeekWrapper{rand.Reader} } else { counter.GenerateBlob() } u, err := s.GetURL() if err != nil { log.Debugf("Failed to get server URL: %s", err) return 0, 0, err } ctx, cancel := context.WithCancel(context.Background()) defer cancel() u.Path = path.Join(u.Path, s.UploadURL) req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), counter) if err != nil { log.Debugf("Failed when creating HTTP request: %s", err) return 0, 0, err } req.Header.Set("User-Agent", UserAgent) req.Header.Set("Accept-Encoding", "identity") uploadDone := make(chan struct{}, requests) doUpload := func() { resp, err := http.DefaultClient.Do(req) if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { log.Debugf("Failed when making HTTP request: %s", err) } else if err == nil { defer resp.Body.Close() if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil { log.Debugf("Failed when reading HTTP response: %s", err) } uploadDone <- struct{}{} } } counter.Start() if !silent { pb := spinner.New(spinner.CharSets[11], 100*time.Millisecond) pb.Prefix = "Uploading... " pb.PostUpdate = func(s *spinner.Spinner) { if useBytes { s.Suffix = fmt.Sprintf(" %s", counter.AvgHumanize()) } else { s.Suffix = fmt.Sprintf(" %.2f Mbps", counter.AvgMbps()) } } pb.Start() defer func() { if useBytes { pb.FinalMSG = fmt.Sprintf("Upload rate:\t%s\n", counter.AvgHumanize()) } else { pb.FinalMSG = fmt.Sprintf("Upload rate:\t%.2f Mbps\n", counter.AvgMbps()) } pb.Stop() }() } for i := 0; i < requests; i++ { go doUpload() time.Sleep(200 * time.Millisecond) } timeout := time.After(duration) Loop: for { select { case <-timeout: ctx.Done() break Loop case <-uploadDone: go doUpload() } } return counter.AvgMbps(), counter.Total(), nil } // GetIPInfo accesses the backend's getIP.php endpoint and get current client's IP information func (s *Server) GetIPInfo(distanceUnit string) (*GetIPResult, error) { t := time.Now() defer func() { s.TLog.Logf("Get IP info took %s", time.Now().Sub(t).String()) }() var ipInfo GetIPResult u, err := s.GetURL() if err != nil { log.Debugf("Failed to get server URL: %s", err) return nil, err } u.Path = path.Join(u.Path, s.GetIPURL) q := u.Query() q.Set("isp", "true") q.Set("distance", distanceUnit) u.RawQuery = q.Encode() req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { 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) return nil, err } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { log.Debugf("Failed when reading HTTP response: %s", err) return nil, err } if len(b) > 0 { 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[:]) } } return &ipInfo, nil } // GetURL parses the server's URL into a url.URL func (s *Server) GetURL() (*url.URL, error) { t := time.Now() defer func() { s.TLog.Logf("Parse server URL took %s", time.Now().Sub(t).String()) }() u, err := url.Parse(s.Server) if err != nil { log.Debugf("Failed when parsing server URL: %s", err) return u, err } 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 } speedtest-cli-1.0.11/defs/telemetry.go000066400000000000000000000060541467006207200176320ustar00rootroot00000000000000package defs import ( "fmt" "net/url" "path" "strings" "time" ) const ( TelemetryLevelDisabled = "disabled" TelemetryLevelBasic = "basic" TelemetryLevelFull = "full" TelemetryLevelDebug = "debug" ) // TelemetryLog is the logger for `log` field in telemetry data type TelemetryLog struct { level int content []string } // SetLevel sets the log level func (t *TelemetryLog) SetLevel(level int) { t.level = level } // Logf logs when log level is higher than or equal to "full" func (t *TelemetryLog) Logf(format string, a ...interface{}) { if t.level >= 2 { t.content = append(t.content, fmt.Sprintf("%s: %s", time.Now().String(), fmt.Sprintf(format, a...))) } } // Warnf logs when log level is higher than or equal to "full", with a WARN prefix func (t *TelemetryLog) Warnf(format string, a ...interface{}) { if t.level >= 2 { t.content = append(t.content, fmt.Sprintf("%s: WARN: %s", time.Now().String(), fmt.Sprintf(format, a...))) } } // Verbosef logs when log level is higher than or equal to "debug" func (t *TelemetryLog) Verbosef(format string, a ...interface{}) { if t.level >= 3 { t.content = append(t.content, fmt.Sprintf("%s: %s", time.Now().String(), fmt.Sprintf(format, a...))) } } // String returns the concatenated string of field `content` func (t *TelemetryLog) String() string { return strings.Join(t.content, "\n") } // TelemetryExtra represents the `extra` field in the telemetry data type TelemetryExtra struct { ServerName string `json:"server"` Extra string `json:"extra,omitempty"` } // TelemetryServer represents the telemetry server configuration type TelemetryServer struct { Level string `json:"telemetryLevel"` Server string `json:"server"` Path string `json:"path"` Share string `json:"shareURL"` } // GetLevel translates the level string to corresponding integer value func (t *TelemetryServer) GetLevel() int { switch t.Level { default: fallthrough case TelemetryLevelDisabled: return 0 case TelemetryLevelBasic: return 1 case TelemetryLevelFull: return 2 case TelemetryLevelDebug: return 3 } } // Disabled checks if the telemetry level is "disabled" func (t *TelemetryServer) Disabled() bool { return t.Level == TelemetryLevelDisabled } // Basic checks if the telemetry level is "basic" func (t *TelemetryServer) Basic() bool { return t.Level == TelemetryLevelBasic } // Full checks if the telemetry level is "full" func (t *TelemetryServer) Full() bool { return t.Level == TelemetryLevelFull } // Debug checks if the telemetry level is "debug" func (t *TelemetryServer) Debug() bool { return t.Level == TelemetryLevelDebug } // GetPath parses and returns the telemetry path func (t *TelemetryServer) GetPath() (*url.URL, error) { u, err := url.Parse(t.Server) if err != nil { return nil, err } u.Path = path.Join(u.Path, t.Path) return u, nil } // GetShare parses and returns the telemetry share path func (t *TelemetryServer) GetShare() (*url.URL, error) { u, err := url.Parse(t.Server) if err != nil { return nil, err } u.Path = path.Join(u.Path, t.Share) + "/" return u, nil } speedtest-cli-1.0.11/dockerfile000066400000000000000000000005471467006207200164030ustar00rootroot00000000000000FROM 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"] speedtest-cli-1.0.11/go.mod000066400000000000000000000007201467006207200154500ustar00rootroot00000000000000module github.com/librespeed/speedtest-cli go 1.14 require ( github.com/briandowns/spinner v1.23.0 github.com/fatih/color v1.15.0 // indirect github.com/go-ping/ping v1.1.0 github.com/gocarina/gocsv v0.0.0-20230406101422-6445c2b15027 github.com/google/uuid v1.3.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/sirupsen/logrus v1.9.0 github.com/urfave/cli/v2 v2.25.1 golang.org/x/net v0.9.0 // indirect golang.org/x/sys v0.7.0 ) speedtest-cli-1.0.11/go.sum000066400000000000000000000201501467006207200154740ustar00rootroot00000000000000github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= github.com/gocarina/gocsv v0.0.0-20230406101422-6445c2b15027 h1:LCGzZb4kMUUjMUzLxxqSJBwo9szUO0tK8cOxnEOT4Jc= github.com/gocarina/gocsv v0.0.0-20230406101422-6445c2b15027/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/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.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.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= speedtest-cli-1.0.11/main.go000066400000000000000000000154261467006207200156260ustar00rootroot00000000000000package main import ( "os" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "github.com/librespeed/speedtest-cli/defs" "github.com/librespeed/speedtest-cli/speedtest" ) // init sets up the essential bits on start up func init() { // set logrus formatter and default log level formatter := &defs.NoFormatter{} // debug level is for --debug messages // info level is for non-suppress mode // warn level is for suppress modes // error level is for errors log.SetOutput(os.Stderr) log.SetFormatter(formatter) log.SetLevel(log.InfoLevel) } func main() { // define cli options app := &cli.App{ Name: "librespeed-cli", Usage: "Test your Internet speed with LibreSpeed", Action: speedtest.SpeedTest, HideHelp: true, Flags: []cli.Flag{ cli.HelpFlag, &cli.BoolFlag{ Name: defs.OptionVersion, Usage: "Show the version number and exit", }, &cli.BoolFlag{ Name: defs.OptionIPv4, Aliases: []string{defs.OptionIPv4Alt}, Usage: "Force IPv4 only", }, &cli.BoolFlag{ Name: defs.OptionIPv6, Aliases: []string{defs.OptionIPv6Alt}, Usage: "Force IPv6 only", }, &cli.BoolFlag{ Name: defs.OptionNoDownload, Usage: "Do not perform download test", }, &cli.BoolFlag{ 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" + "\tthe image generated by --share, nor output from\n" + "\t--json or --csv", }, &cli.BoolFlag{ Name: defs.OptionMebiBytes, Usage: "Use 1024 bytes as 1 kilobyte instead of 1000", }, &cli.StringFlag{ Name: defs.OptionDistance, Usage: "Change distance unit shown in ISP info, use 'mi' for miles,\n" + "\t'km' for kilometres, 'NM' for nautical miles", Value: "km", }, &cli.BoolFlag{ Name: defs.OptionShare, Usage: "Generate and provide a URL to the LibreSpeed.org share results\n" + "\timage, not displayed with --csv", }, &cli.BoolFlag{ Name: defs.OptionSimple, Usage: "Suppress verbose output, only show basic information\n\t", }, &cli.BoolFlag{ Name: defs.OptionCSV, Usage: "Suppress verbose output, only show basic information in CSV\n" + "\tformat. Speeds listed in bit/s and not affected by --bytes\n\t", }, &cli.StringFlag{ Name: defs.OptionCSVDelimiter, Usage: "Single character delimiter (`CSV_DELIMITER`) to use in\n" + "\tCSV output.", Value: ",", }, &cli.BoolFlag{ Name: defs.OptionCSVHeader, Usage: "Print CSV headers", }, &cli.BoolFlag{ Name: defs.OptionJSON, Usage: "Suppress verbose output, only show basic information\n" + "\tin JSON format. Speeds listed in bit/s and not\n" + "\taffected by --bytes", }, &cli.BoolFlag{ Name: defs.OptionList, Usage: "Display a list of LibreSpeed.org servers", }, &cli.IntSliceFlag{ Name: defs.OptionServer, Usage: "Specify a `SERVER` ID to test against. Can be supplied\n" + "\tmultiple times. Cannot be used with --exclude", }, &cli.IntSliceFlag{ Name: defs.OptionExclude, Usage: "`EXCLUDE` a server from selection. Can be supplied\n" + "\tmultiple times. Cannot be used with --server", }, &cli.StringFlag{ Name: defs.OptionServerJSON, 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,\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" + "\tenabled by default to improve upload performance. To\n" + "\tsupport systems with insufficient memory, use this\n" + "\toption to avoid out of memory errors", }, &cli.BoolFlag{ Name: defs.OptionDebug, Aliases: []string{"verbose"}, Usage: "Debug mode (verbose logging)", Hidden: true, }, &cli.StringFlag{ 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 + ". Implies --" + defs.OptionShare, }, &cli.StringFlag{ Name: defs.OptionTelemetryLevel, Usage: "Set telemetry data verbosity, available values are:\n" + "\tdisabled, basic, full, debug. Implies --" + defs.OptionShare, }, &cli.StringFlag{ Name: defs.OptionTelemetryServer, Usage: "Set the telemetry server base URL. Implies --" + defs.OptionShare, }, &cli.StringFlag{ Name: defs.OptionTelemetryPath, Usage: "Set the telemetry upload path. Implies --" + defs.OptionShare, }, &cli.StringFlag{ Name: defs.OptionTelemetryShare, 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.\n" + "\tImplies --" + defs.OptionShare, }, }, } // run main function with cli options err := app.Run(os.Args) if err != nil { log.Fatal("Terminated due to error") } } speedtest-cli-1.0.11/report/000077500000000000000000000000001467006207200156565ustar00rootroot00000000000000speedtest-cli-1.0.11/report/csv.go000066400000000000000000000007031467006207200170000ustar00rootroot00000000000000package report import ( "time" ) // CSVReport represents the output data fields in a CSV file type CSVReport struct { Timestamp time.Time `csv:"Timestamp"` Name string `csv:"Server Name"` Address string `csv:"Address"` Ping float64 `csv:"Ping"` Jitter float64 `csv:"Jitter"` Download float64 `csv:"Download"` Upload float64 `csv:"Upload"` Share string `csv:"Share"` IP string `csv:"IP"` } speedtest-cli-1.0.11/report/json.go000066400000000000000000000014771467006207200171670ustar00rootroot00000000000000package report import ( "time" "github.com/librespeed/speedtest-cli/defs" ) // JSONReport represents the output data fields in a JSON file type JSONReport struct { Timestamp time.Time `json:"timestamp"` Server Server `json:"server"` Client Client `json:"client"` BytesSent uint64 `json:"bytes_sent"` BytesReceived uint64 `json:"bytes_received"` Ping float64 `json:"ping"` Jitter float64 `json:"jitter"` Upload float64 `json:"upload"` Download float64 `json:"download"` Share string `json:"share"` } // Server represents the speed test server's information type Server struct { Name string `json:"name"` URL string `json:"url"` } // Client represents the speed test client's information type Client struct { defs.IPInfoResponse } speedtest-cli-1.0.11/speedtest/000077500000000000000000000000001467006207200163435ustar00rootroot00000000000000speedtest-cli-1.0.11/speedtest/helper.go000066400000000000000000000241141467006207200201530ustar00rootroot00000000000000package speedtest import ( "bytes" "encoding/json" "fmt" "io/ioutil" "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" ) const ( // the default ping count for measuring ping and jitter pingCount = 10 ) // doSpeedTest is where the actual speed test happens 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 currentServer.TLog.SetLevel(telemetryServer.GetLevel()) u, err := currentServer.GetURL() if err != nil { log.Errorf("Failed to get server URL: %s", err) return err } 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 { log.Errorf("Failed to get IP info: %s", err) return err } log.Infof("You're testing from: %s", ispInfo.ProcessedString) // get ping and jitter value var pb *spinner.Spinner if !silent { pb = spinner.New(spinner.CharSets[11], 100*time.Millisecond) pb.Prefix = "Pinging server... " 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) return err } if pb != nil { pb.FinalMSG = fmt.Sprintf("Ping: %.2f ms\tJitter: %.2f ms\n", p, jitter) pb.Stop() } // get download value var downloadValue float64 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), 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 = uint64(br) } // get upload value var uploadValue float64 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), 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 = 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%.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%.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 telemetryServer.GetLevel() > 0 { var extra defs.TelemetryExtra extra.ServerName = currentServer.Name extra.Extra = c.String(defs.OptionTelemetryExtra) if link, err := sendTelemetry(telemetryServer, ispInfo, downloadValue, uploadValue, p, jitter, currentServer.TLog.String(), extra); err != nil { log.Errorf("Error when sending telemetry data: %s", err) } else { shareLink = 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 rep report.CSVReport rep.Timestamp = time.Now() rep.Name = currentServer.Name rep.Address = u.String() 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_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 = 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.BytesReceived = bytesRead rep.BytesSent = bytesWritten rep.Share = shareLink rep.Server.Name = currentServer.Name rep.Server.URL = u.String() rep.Client = report.Client{ispInfo.RawISPInfo} rep.Client.Readme = "" 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 && !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 } // sendTelemetry sends the telemetry result to server, if --share is given func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResult, download, upload, pingVal, jitter float64, logs string, extra defs.TelemetryExtra) (string, error) { var buf bytes.Buffer wr := multipart.NewWriter(&buf) b, _ := json.Marshal(ispInfo) if fIspInfo, err := wr.CreateFormField("ispinfo"); err != nil { log.Debugf("Error creating form field: %s", err) return "", err } else if _, err = fIspInfo.Write(b); err != nil { log.Debugf("Error writing form field: %s", err) return "", err } if fDownload, err := wr.CreateFormField("dl"); err != nil { log.Debugf("Error creating form field: %s", err) return "", err } else if _, err = fDownload.Write([]byte(strconv.FormatFloat(download, 'f', 2, 64))); err != nil { log.Debugf("Error writing form field: %s", err) return "", err } if fUpload, err := wr.CreateFormField("ul"); err != nil { log.Debugf("Error creating form field: %s", err) return "", err } else if _, err = fUpload.Write([]byte(strconv.FormatFloat(upload, 'f', 2, 64))); err != nil { log.Debugf("Error writing form field: %s", err) return "", err } 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.FormatFloat(pingVal, 'f', 2, 64))); err != nil { log.Debugf("Error writing form field: %s", err) return "", err } 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.FormatFloat(jitter, 'f', 2, 64))); err != nil { log.Debugf("Error writing form field: %s", err) return "", err } if fLog, err := wr.CreateFormField("log"); err != nil { log.Debugf("Error creating form field: %s", err) return "", err } else if _, err = fLog.Write([]byte(logs)); err != nil { log.Debugf("Error writing form field: %s", err) return "", err } b, _ = json.Marshal(extra) if fExtra, err := wr.CreateFormField("extra"); err != nil { log.Debugf("Error creating form field: %s", err) return "", err } else if _, err = fExtra.Write(b); err != nil { log.Debugf("Error writing form field: %s", err) return "", err } if err := wr.Close(); err != nil { log.Debugf("Error flushing form field writer: %s", err) return "", err } telemetryUrl, err := telemetryServer.GetPath() if err != nil { return "", err } req, err := http.NewRequest(http.MethodPost, telemetryUrl.String(), &buf) if err != nil { log.Debugf("Error when creating HTTP request: %s", err) return "", err } req.Header.Set("Content-Type", wr.FormDataContentType()) req.Header.Set("User-Agent", defs.UserAgent) resp, err := http.DefaultClient.Do(req) if err != nil { log.Debugf("Error when making HTTP request: %s", err) return "", err } defer resp.Body.Close() id, err := ioutil.ReadAll(resp.Body) if err != nil { log.Errorf("Error when reading HTTP request: %s", err) return "", err } resultUrl, err := telemetryServer.GetShare() if err != nil { return "", err } if str := strings.Split(string(id), " "); len(str) != 2 { return "", fmt.Errorf("server returned invalid response: %s", id) } else { q := resultUrl.Query() q.Set("id", str[1]) resultUrl.RawQuery = q.Encode() return resultUrl.String(), nil } } func humanizeMbps(mbps float64, useMebi bool) string { val := mbps / 8 var base float64 = 1000 if useMebi { base = 1024 } if val < 1 { if kb := val * base; kb < 1 { return fmt.Sprintf("%.2f bytes/s", kb*base) } else { return fmt.Sprintf("%.2f KB/s", kb) } } else if val > base { return fmt.Sprintf("%.2f GB/s", val/base) } else { return fmt.Sprintf("%.2f MB/s", val) } } speedtest-cli-1.0.11/speedtest/speedtest.go000066400000000000000000000347711467006207200207060ustar00rootroot00000000000000package speedtest import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "os" "strings" "sync" "time" "github.com/gocarina/gocsv" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "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.php` defaultTelemetryLevel = "basic" defaultTelemetryServer = "https://librespeed.org" defaultTelemetryPath = "/results/telemetry.php" defaultTelemetryShare = "/results/" ) type PingJob struct { Index int Server defs.Server } type PingResult struct { Index int Ping float64 } // SpeedTest is the actual main function that handles the speed test(s) func SpeedTest(c *cli.Context) error { // check for suppressed output flags var silent bool if c.Bool(defs.OptionSimple) || c.Bool(defs.OptionJSON) || c.Bool(defs.OptionCSV) { log.SetLevel(log.WarnLevel) silent = true } // check for debug flag if c.Bool(defs.OptionDebug) { log.SetLevel(log.DebugLevel) } // print help if c.Bool(defs.OptionHelp) { return cli.ShowAppHelp(c) } // 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") log.Warn("LibreSpeed\tCopyright (C) 2016-2020 Federico Dossena") log.Warn("librespeed-cli\tCopyright (C) 2020 Maddie Zhan") log.Warn("librespeed.org\tCopyright (C)") 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) // if --csv-header is given, print the header and exit (same behavior speedtest-cli) if c.Bool(defs.OptionCSVHeader) { var rep []report.CSVReport b, _ := gocsv.MarshalBytes(&rep) os.Stdout.WriteString(string(b)) return nil } // read telemetry settings if --share or any --telemetry option is given var telemetryServer defs.TelemetryServer 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", telemetryJSON, err) return err } if err := json.Unmarshal(b, &telemetryServer); err != nil { log.Errorf("Error parsing %s: %s", err) return err } } if telemetryLevel != "" { if telemetryLevel != "disabled" && telemetryLevel != "basic" && telemetryLevel != "full" && telemetryLevel != "debug" { log.Fatalf("Unsupported telemetry level: %s", telemetryLevel) } telemetryServer.Level = telemetryLevel } else if telemetryServer.Level == "" { telemetryServer.Level = defaultTelemetryLevel } if telemetryServerString != "" { telemetryServer.Server = telemetryServerString } else if telemetryServer.Server == "" { telemetryServer.Server = defaultTelemetryServer } if telemetryPath != "" { telemetryServer.Path = telemetryPath } else if telemetryServer.Path == "" { telemetryServer.Path = defaultTelemetryPath } if telemetryShare != "" { telemetryServer.Share = telemetryShare } else if telemetryServer.Share == "" { 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 forceIPv4 := c.Bool(defs.OptionIPv4) forceIPv6 := c.Bool(defs.OptionIPv6) var network string switch { case forceIPv4: network = "ip4" case forceIPv6: network = "ip6" default: network = "ip" } transport := http.DefaultTransport.(*http.Transport).Clone() if caCertFileName := c.String(defs.OptionCACert); caCertFileName != "" { caCert, err := ioutil.ReadFile(caCertFileName) if err != nil { log.Fatal(err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify), RootCAs: caCertPool, } } else { transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify), } } dialer := &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, } // bind to source IP address if given 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 if iface := c.String(defs.OptionInterface); iface != "" { var err error dialer, err = newDialerInterfaceBound(iface) 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 != "" { 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 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) return err } // if --list is given, list all the servers fetched and exit if c.Bool(defs.OptionList) { 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, 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, 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, noICMP) } // send ping jobs to workers for idx, server := range servers { wg.Add(1) jobs <- PingJob{Index: idx, Server: server} } go func() { wg.Wait() close(done) }() Loop: for { select { case result := <-results: pingList[result.Index] = result.Ping case <-done: break Loop } } if len(pingList) == 0 { log.Fatal("No server is currently available, please try again later.") } // get the fastest server's index in the `servers` array var serverIdx int for idx, ping := range pingList { if ping > 0 && ping <= pingList[serverIdx] { serverIdx = idx } } // do speed test on the server 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, noICMP bool) { for { job := <-jobs server := job.Server // get the URL of the speed test server from the JSON u, err := server.GetURL() if err != nil { log.Debugf("Server URL is invalid for %s (%s), skipping", server.Name, server.Server) wg.Done() return } // 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 { log.Debugf("Can't ping server %s (%s), skipping", server.Name, u.Hostname()) wg.Done() return } // return result results <- PingResult{Index: job.Index, Ping: ping} wg.Done() } else { log.Debugf("Server %s (%s) doesn't seem to be up, skipping", server.Name, u.Hostname()) wg.Done() } } } // getServerList fetches the server JSON from a remote server func getServerList(forceHTTPS bool, serverList string, excludes, specific []int, filter bool) ([]defs.Server, error) { // --exclude and --server cannot be used at the same time if len(excludes) > 0 && len(specific) > 0 { return nil, errors.New("either --exclude or --server can be used") } // getting the server list from remote var servers []defs.Server 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 } b, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } defer resp.Body.Close() if err := json.Unmarshal(b, &servers); err != nil { return nil, err } return preprocessServers(servers, forceHTTPS, excludes, specific, filter) } // 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.ReadAll(reader) if err != nil { return nil, err } if err := json.Unmarshal(b, &servers); err != nil { return nil, err } 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 { u, err := servers[i].GetURL() if err != nil { return nil, err } // if no scheme is defined, use http as default, or https when --secure is given in cli options // if the scheme is predefined and --secure is not given, we will use it as-is if forceHTTPS { u.Scheme = "https" } else if u.Scheme == "" { // if `secure` is not used and no scheme is defined, use http u.Scheme = "http" } // modify the server struct in the array in place servers[i].Server = u.String() } if len(excludes) > 0 && len(specific) > 0 { return nil, errors.New("either --exclude or --specific can be used") } if filter { // exclude servers from --exclude if len(excludes) > 0 { var ret []defs.Server for _, server := range servers { if contains(excludes, server.ID) { continue } ret = append(ret, server) } return ret, nil } // use only servers from --server // special value -1 will test all servers if len(specific) > 0 && !contains(specific, -1) { var ret []defs.Server 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 } } return servers, nil } // contains is a helper function to check if an int is in an int array func contains(arr []int, val int) bool { for _, v := range arr { if v == val { return true } } 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 } speedtest-cli-1.0.11/speedtest/util.go000066400000000000000000000003461467006207200176520ustar00rootroot00000000000000//go:build !linux // +build !linux package speedtest import ( "fmt" "net" ) func newDialerInterfaceBound(iface string) (dialer *net.Dialer, err error) { return nil, fmt.Errorf("cannot bound to interface on this platform") } speedtest-cli-1.0.11/speedtest/util_linux.go000066400000000000000000000013311467006207200210640ustar00rootroot00000000000000package speedtest import ( "net" "syscall" "time" "golang.org/x/sys/unix" ) func newDialerInterfaceBound(iface string) (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) { errSock = unix.BindToDevice(int(fd), iface) })) if err != nil { return err } return errSock } dialer = &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, Control: control, } return dialer, nil } speedtest-cli-1.0.11/upx.sh000077500000000000000000000000321467006207200155110ustar00rootroot00000000000000#!/bin/sh upx "$@" || true