├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── release.yml │ └── usage_demo.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src ├── distance.rs ├── error.rs ├── lib.rs ├── log.rs ├── main.rs ├── speedtest.rs ├── speedtest_config.rs ├── speedtest_csv.rs └── speedtest_servers_config.rs └── tests └── config ├── 2020-07-speedtest-config.xml ├── 2021-07-speedtest-config.xml ├── config.php.xml ├── geo-test-servers-static.php.xml ├── servers-static.php.xml ├── stripped-config.php.xml └── stripped-servers-static.php.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" # See documentation for possible values 13 | directory: "/" # Location of package manifests 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | # Trigger the workflow on push or pull request, 3 | # but only for the master branch 4 | push: 5 | pull_request: 6 | 7 | name: Continuous integration 8 | 9 | jobs: 10 | ci: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | rust: 15 | - stable 16 | - beta 17 | - nightly 18 | os: 19 | - ubuntu-latest 20 | - macOS-latest 21 | - windows-latest 22 | features: 23 | - "rustls-tls" 24 | - "" 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | profile: minimal 32 | toolchain: ${{ matrix.rust }} 33 | override: true 34 | components: rustfmt, clippy 35 | 36 | - uses: Swatinem/rust-cache@v2 37 | 38 | - uses: actions-rs/cargo@v1 39 | name: Build 40 | with: 41 | command: build 42 | args: --features=${{ matrix.features }} 43 | 44 | - uses: actions-rs/cargo@v1 45 | name: Unit Test 46 | with: 47 | command: test 48 | args: --features=${{ matrix.features }} 49 | 50 | - uses: actions-rs/cargo@v1 51 | name: Run 52 | env: 53 | RUST_LOG: "debug" 54 | with: 55 | command: run 56 | args: --features=${{ matrix.features }} 57 | 58 | - uses: actions-rs/cargo@v1 59 | name: Format Check 60 | with: 61 | command: fmt 62 | args: --all -- --check 63 | 64 | - uses: actions-rs/cargo@v1 65 | name: Clippy 66 | with: 67 | command: clippy 68 | args: -- -D warnings 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # From https://github.com/paskausks/rust-bin-github-workflows/blob/master/.github/workflows/release.yml 2 | # With some cribbing from GitHub readmes 3 | 4 | on: 5 | push: 6 | # Sequence of patterns matched against refs/tags 7 | tags: 8 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 9 | 10 | name: Create Release 11 | 12 | env: 13 | # Could, potentially automatically parse 14 | # the bin name, but let's do it automatically for now. 15 | RELEASE_BIN: speedtest-rs 16 | 17 | # Space separated paths to include in the archive. 18 | # Start relative paths with a dot if you don't want 19 | # paths to be preserved. Use "/" as a delimiter. 20 | RELEASE_ADDS: README.md LICENSE-APACHE LICENSE-MIT 21 | jobs: 22 | build: 23 | name: Build Release 24 | 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | build: [linux, macos, windows] 29 | include: 30 | - build: linux 31 | os: ubuntu-latest 32 | rust: stable 33 | - build: macos 34 | os: macos-latest 35 | rust: stable 36 | - build: windows 37 | os: windows-latest 38 | rust: stable 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - uses: actions-rs/toolchain@v1 44 | with: 45 | profile: minimal 46 | toolchain: ${{ matrix.rust }} 47 | override: true 48 | 49 | - name: Build 50 | run: cargo build --verbose --release 51 | 52 | - name: Create artifact directory 53 | run: mkdir artifacts 54 | 55 | - name: Create archive for Linux 56 | run: 7z a -ttar -so -an ./target/release/${{ env.RELEASE_BIN }} ${{ env.RELEASE_ADDS }} | 7z a -si ./artifacts/${{ env.RELEASE_BIN }}-linux-x86_64.tar.gz 57 | if: matrix.os == 'ubuntu-latest' 58 | 59 | - name: Create archive for Windows 60 | run: 7z a -tzip ./artifacts/${{ env.RELEASE_BIN }}-windows-x86_64.zip ./target/release/${{ env.RELEASE_BIN }}.exe ${{ env.RELEASE_ADDS }} 61 | if: matrix.os == 'windows-latest' 62 | 63 | - name: Install p7zip 64 | # 7Zip not available on MacOS, install p7zip via homebrew. 65 | run: brew install p7zip 66 | if: matrix.os == 'macos-latest' 67 | 68 | - name: Create archive for MacOS 69 | run: 7z a -tzip ./artifacts/${{ env.RELEASE_BIN }}-mac-x86_64.zip ./target/release/${{ env.RELEASE_BIN }} ${{ env.RELEASE_ADDS }} 70 | if: matrix.os == 'macos-latest' 71 | 72 | # This will double-zip 73 | # See - https://github.com/actions/upload-artifact/issues/39 74 | - uses: actions/upload-artifact@v4 75 | name: Upload archive 76 | with: 77 | name: ${{ runner.os }} 78 | path: artifacts/ 79 | 80 | release: 81 | name: Create Release 82 | needs: build 83 | runs-on: ubuntu-latest 84 | 85 | steps: 86 | 87 | - uses: actions/download-artifact@v4 88 | with: 89 | name: Linux 90 | - uses: actions/download-artifact@v4 91 | with: 92 | name: macOS 93 | - uses: actions/download-artifact@v4 94 | with: 95 | name: Windows 96 | 97 | - name: Create Release 98 | id: create_release 99 | uses: actions/create-release@v1 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 102 | with: 103 | tag_name: ${{ github.ref }} 104 | release_name: Release ${{ github.ref }} 105 | draft: false 106 | prerelease: false 107 | 108 | - name: Upload Release Asset (Linux) 109 | uses: actions/upload-release-asset@v1 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | with: 113 | upload_url: ${{ steps.create_release.outputs.upload_url }} 114 | asset_path: ${{ env.RELEASE_BIN }}-linux-x86_64.tar.gz 115 | asset_name: ${{ env.RELEASE_BIN }}-linux-x86_64.tar.gz 116 | asset_content_type: application/zip 117 | 118 | - name: Upload Release Asset (Windows) 119 | uses: actions/upload-release-asset@v1 120 | env: 121 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 122 | with: 123 | upload_url: ${{ steps.create_release.outputs.upload_url }} 124 | asset_path: ${{ env.RELEASE_BIN }}-windows-x86_64.zip 125 | asset_name: ${{ env.RELEASE_BIN }}-windows-x86_64.zip 126 | asset_content_type: application/zip 127 | 128 | - name: Upload Release Asset (Mac) 129 | uses: actions/upload-release-asset@v1 130 | env: 131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 132 | with: 133 | upload_url: ${{ steps.create_release.outputs.upload_url }} 134 | asset_path: ${{ env.RELEASE_BIN }}-mac-x86_64.zip 135 | asset_name: ${{ env.RELEASE_BIN }}-mac-x86_64.zip 136 | asset_content_type: application/zip 137 | -------------------------------------------------------------------------------- /.github/workflows/usage_demo.yml: -------------------------------------------------------------------------------- 1 | on: 2 | # Trigger the workflow on push or pull request on all branches 3 | push: 4 | pull_request: 5 | # And every day at midnight 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | name: Usage Demo 10 | 11 | jobs: 12 | usage-demo: 13 | name: "Usage Demo" 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | profile: minimal 23 | override: true 24 | 25 | - uses: Swatinem/rust-cache@v2 26 | 27 | - uses: actions-rs/cargo@v1 28 | name: Build 29 | with: 30 | command: build 31 | args: --features=${{ matrix.features }} 32 | 33 | - uses: actions-rs/cargo@v1 34 | name: Run (Typical) 35 | with: 36 | command: run 37 | 38 | - uses: actions-rs/cargo@v1 39 | name: Run (CSV Output) 40 | with: 41 | command: run 42 | args: -- --csv 43 | - uses: actions-rs/cargo@v1 44 | name: Run (CSV Header) 45 | with: 46 | command: run 47 | args: -- --csv-header 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea/ 3 | .vscode/settings.json 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.2.0] - 2024-07-27 8 | 9 | ### Changed 10 | 11 | - [Update dependencies in general by @dilawar](https://github.com/nelsonjchen/speedtest-rs/pull/165) 12 | 13 | ## [0.1.5] - 2024-02-11 14 | 15 | ### Added 16 | 17 | - [feat: hide log behind a feature by @radiohertz](https://github.com/nelsonjchen/speedtest-rs/pull/144) 18 | 19 | ### Changed 20 | 21 | - Update dependencies in general 22 | 23 | 24 | ## [0.1.4] - 2023-02-25 25 | 26 | ### Changed 27 | 28 | - Update dependencies in general 29 | 30 | ## [0.1.3] - 2021-07-24 31 | ### Fixed 32 | - [Don't log finish parsed configuration if it fails](https://github.com/nelsonjchen/speedtest-rs/pull/84) 33 | 34 | ## [0.1.2] - 2021-04-14 35 | ### Fixed 36 | - [Check whether ignore_server str is empty.](https://github.com/nelsonjchen/speedtest-rs/pull/78) Thanks [@pcmid](https://github.com/pcmid)! 37 | 38 | ## [0.1.1] - 2020-07-26 39 | ### Added 40 | - Add a plain `lib.rs` with no guarantees on stability 41 | 42 | ## [0.1.0] - 2020-07-23 43 | ### Changed 44 | - [Major reimplementation of download and upload test implementation to be more accurate to speedtest-cli.](https://github.com/nelsonjchen/speedtest-rs/pull/74) 45 | That said, speedtest-cli isn't too accurate on high bandwidth connections. 46 | A future alternate or default accurate implementation will need to be like socket based speedtest tools. 47 | - Replaced xml-rs implementation with roxmltree implementation. 48 | 49 | ### Added 50 | - Upload tests no longer pre-allocate the test upload content. It is now iterator based and generates the uploaded bytes on the fly. 51 | Speedtest-cli takes ~300-400MB of RAM to preallocate upload request data on my connection to do its upload tests. 52 | Speedtest-rs now takes ~8MB. 53 | 54 | ## [0.0.15] - 2020-07-11 55 | ### Added 56 | - [Mini server support with `--mini`](https://github.com/nelsonjchen/speedtest-rs/pull/72) 57 | 58 | ## [0.0.14] - 2020-03-05 59 | ### Added 60 | - CSV output support in the form of `--csv` and `--csv-header` 61 | 62 | ## [0.0.13] - 2020-02-09 63 | ### Changed 64 | - Swapped out MD5 crate to simpler version 65 | - Replaced Error Chain with plain old error enums. 66 | 67 | ### Added 68 | - `rustls-tls` feature to use `rustls-tls` in reqwest. 69 | 70 | ## [0.0.12] - 2019-10-13 71 | ### Fixed 72 | - [Skip servers if the latency test failed.](https://github.com/nelsonjchen/speedtest-rs/pull/22) 73 | ### Changed 74 | - Update dependencies 75 | 76 | ## [0.0.11] - 2019-02-04 77 | ### Changed 78 | - Update dependencies and followed the API changes 79 | - Updated to Rust 2018 80 | 81 | ## [0.0.10] - 2017-12-03 82 | ### Changed 83 | - Update infrastructure and ensure things still build on beta and nightly as of 84 | release. 85 | - Lay out initial foundation for a "error-chain" approach instead of unwraps 86 | everywhere. This may be replaced later with the "failure" crate. WIP. 87 | - Update some internal formatting to modern Rust. WIP. 88 | 89 | ## [0.0.9] - 2016-12-22 90 | ### Changed 91 | - Swap out usage of hyper directly with reqwest. 92 | 93 | - `speedtest-rs` now uses the platform's native TLS implementation. Compile 94 | issues on Windows or Mac due to OpenSSL issues or sheanigans are no 95 | longer an issue. 96 | 97 | ## [0.0.8] - 2016-08-14 98 | 99 | ### Changed 100 | 101 | - Updated dependencies. In particular, updated to the new `url` crate API. 102 | 103 | ## [0.0.7] - 2016-01-27 104 | 105 | ### Changed 106 | 107 | - Update progress bar behavior to be more like `speedtest-cli` by displaying 108 | attempts to upload a file piece instead of completion. 109 | 110 | ## [0.0.6] - 2016-01-25 111 | 112 | ### Changed 113 | 114 | - Correct issue with confusion on maths used to calculate bits and bytes. I 115 | should probably code when I'm awake and not when I'm tired, exhausted, and 116 | delirious. Fix was put in while I'm delirious so who knows if this works! 117 | - Fixed issue where not using `--bytes` results in "Mbytes/s" output even 118 | though output is "Mbit/s". 119 | 120 | ## [0.0.5] - 2016-01-15 121 | 122 | ### Changed 123 | 124 | - Also applied omitted 10 second test run limit to download. 125 | 126 | ## [0.0.4] - 2015-12-24 127 | 128 | ### Added 129 | 130 | - Added `--share` to generate and provide an URL to the speedtest.net share 131 | results image. 132 | 133 | ## [0.0.3] - 2015-12-23 134 | 135 | ### Changed 136 | 137 | - Server list URL changed to non-static version. The static version appears to 138 | have been taken down for good this time. 139 | 140 | 141 | ## [0.0.2] - 2015-12-04 142 | 143 | ### Added 144 | 145 | - Add `--simple` flag which prints out results but not progress bars simular to 146 | `speedtest-cli`. 147 | - Generate User Agent string from crate version 148 | 149 | ### Changed 150 | - Made latency test determination a lot more like `speedtest-cli`'s weird 151 | metric for "averaging". Not sure if fix but they say it was intentional. 152 | 153 | 154 | ## [0.0.1] - 2015-11-18 155 | 156 | ### Added 157 | 158 | - Progress indicators and "TUI" like `speedtest-cli` 159 | - Test download speed like `speedtest-cli` 160 | - Test upload speed like `speedtest-cli` 161 | - Option to display values in bytes instead.... also like `speedtest-cli`. 162 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android-tzdata" 31 | version = "0.1.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 34 | 35 | [[package]] 36 | name = "android_system_properties" 37 | version = "0.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "anstream" 46 | version = "0.6.14" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 49 | dependencies = [ 50 | "anstyle", 51 | "anstyle-parse", 52 | "anstyle-query", 53 | "anstyle-wincon", 54 | "colorchoice", 55 | "is_terminal_polyfill", 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle" 61 | version = "1.0.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 64 | 65 | [[package]] 66 | name = "anstyle-parse" 67 | version = "0.2.4" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 70 | dependencies = [ 71 | "utf8parse", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-query" 76 | version = "1.1.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 79 | dependencies = [ 80 | "windows-sys 0.52.0", 81 | ] 82 | 83 | [[package]] 84 | name = "anstyle-wincon" 85 | version = "3.0.3" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 88 | dependencies = [ 89 | "anstyle", 90 | "windows-sys 0.52.0", 91 | ] 92 | 93 | [[package]] 94 | name = "assert-json-diff" 95 | version = "2.0.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" 98 | dependencies = [ 99 | "serde", 100 | "serde_json", 101 | ] 102 | 103 | [[package]] 104 | name = "atomic-waker" 105 | version = "1.1.2" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 108 | 109 | [[package]] 110 | name = "autocfg" 111 | version = "1.3.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 114 | 115 | [[package]] 116 | name = "backtrace" 117 | version = "0.3.73" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 120 | dependencies = [ 121 | "addr2line", 122 | "cc", 123 | "cfg-if", 124 | "libc", 125 | "miniz_oxide", 126 | "object", 127 | "rustc-demangle", 128 | ] 129 | 130 | [[package]] 131 | name = "base64" 132 | version = "0.22.1" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 135 | 136 | [[package]] 137 | name = "bitflags" 138 | version = "1.3.2" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 141 | 142 | [[package]] 143 | name = "bitflags" 144 | version = "2.6.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 147 | 148 | [[package]] 149 | name = "bumpalo" 150 | version = "3.16.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 153 | 154 | [[package]] 155 | name = "bytes" 156 | version = "1.6.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" 159 | 160 | [[package]] 161 | name = "cc" 162 | version = "1.1.6" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" 165 | 166 | [[package]] 167 | name = "cfg-if" 168 | version = "1.0.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 171 | 172 | [[package]] 173 | name = "chrono" 174 | version = "0.4.38" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 177 | dependencies = [ 178 | "android-tzdata", 179 | "iana-time-zone", 180 | "js-sys", 181 | "num-traits", 182 | "wasm-bindgen", 183 | "windows-targets 0.52.6", 184 | ] 185 | 186 | [[package]] 187 | name = "clap" 188 | version = "4.5.10" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142" 191 | dependencies = [ 192 | "clap_builder", 193 | "clap_derive", 194 | ] 195 | 196 | [[package]] 197 | name = "clap_builder" 198 | version = "4.5.10" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac" 201 | dependencies = [ 202 | "anstream", 203 | "anstyle", 204 | "clap_lex", 205 | "strsim", 206 | ] 207 | 208 | [[package]] 209 | name = "clap_derive" 210 | version = "4.5.8" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" 213 | dependencies = [ 214 | "heck", 215 | "proc-macro2", 216 | "quote", 217 | "syn", 218 | ] 219 | 220 | [[package]] 221 | name = "clap_lex" 222 | version = "0.7.1" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" 225 | 226 | [[package]] 227 | name = "colorchoice" 228 | version = "1.0.1" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 231 | 232 | [[package]] 233 | name = "colored" 234 | version = "2.1.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 237 | dependencies = [ 238 | "lazy_static", 239 | "windows-sys 0.48.0", 240 | ] 241 | 242 | [[package]] 243 | name = "core-foundation" 244 | version = "0.9.4" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 247 | dependencies = [ 248 | "core-foundation-sys", 249 | "libc", 250 | ] 251 | 252 | [[package]] 253 | name = "core-foundation-sys" 254 | version = "0.8.6" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 257 | 258 | [[package]] 259 | name = "crossbeam-deque" 260 | version = "0.8.5" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 263 | dependencies = [ 264 | "crossbeam-epoch", 265 | "crossbeam-utils", 266 | ] 267 | 268 | [[package]] 269 | name = "crossbeam-epoch" 270 | version = "0.9.18" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 273 | dependencies = [ 274 | "crossbeam-utils", 275 | ] 276 | 277 | [[package]] 278 | name = "crossbeam-utils" 279 | version = "0.8.20" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 282 | 283 | [[package]] 284 | name = "csv" 285 | version = "1.3.0" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" 288 | dependencies = [ 289 | "csv-core", 290 | "itoa", 291 | "ryu", 292 | "serde", 293 | ] 294 | 295 | [[package]] 296 | name = "csv-core" 297 | version = "0.1.11" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 300 | dependencies = [ 301 | "memchr", 302 | ] 303 | 304 | [[package]] 305 | name = "either" 306 | version = "1.13.0" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 309 | 310 | [[package]] 311 | name = "encoding_rs" 312 | version = "0.8.33" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 315 | dependencies = [ 316 | "cfg-if", 317 | ] 318 | 319 | [[package]] 320 | name = "env_filter" 321 | version = "0.1.1" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "c6dc8c8ff84895b051f07a0e65f975cf225131742531338752abfb324e4449ff" 324 | dependencies = [ 325 | "log", 326 | "regex", 327 | ] 328 | 329 | [[package]] 330 | name = "env_logger" 331 | version = "0.11.4" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "06676b12debf7bba6903559720abca942d3a66b8acb88815fd2c7c6537e9ade1" 334 | dependencies = [ 335 | "anstream", 336 | "anstyle", 337 | "env_filter", 338 | "humantime", 339 | "log", 340 | ] 341 | 342 | [[package]] 343 | name = "equivalent" 344 | version = "1.0.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 347 | 348 | [[package]] 349 | name = "errno" 350 | version = "0.3.8" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 353 | dependencies = [ 354 | "libc", 355 | "windows-sys 0.52.0", 356 | ] 357 | 358 | [[package]] 359 | name = "fastrand" 360 | version = "2.0.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 363 | 364 | [[package]] 365 | name = "fnv" 366 | version = "1.0.7" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 369 | 370 | [[package]] 371 | name = "foreign-types" 372 | version = "0.3.2" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 375 | dependencies = [ 376 | "foreign-types-shared", 377 | ] 378 | 379 | [[package]] 380 | name = "foreign-types-shared" 381 | version = "0.1.1" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 384 | 385 | [[package]] 386 | name = "form_urlencoded" 387 | version = "1.2.1" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 390 | dependencies = [ 391 | "percent-encoding", 392 | ] 393 | 394 | [[package]] 395 | name = "futures-channel" 396 | version = "0.3.30" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 399 | dependencies = [ 400 | "futures-core", 401 | "futures-sink", 402 | ] 403 | 404 | [[package]] 405 | name = "futures-core" 406 | version = "0.3.30" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 409 | 410 | [[package]] 411 | name = "futures-io" 412 | version = "0.3.30" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 415 | 416 | [[package]] 417 | name = "futures-sink" 418 | version = "0.3.30" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 421 | 422 | [[package]] 423 | name = "futures-task" 424 | version = "0.3.30" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 427 | 428 | [[package]] 429 | name = "futures-util" 430 | version = "0.3.30" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 433 | dependencies = [ 434 | "futures-core", 435 | "futures-io", 436 | "futures-sink", 437 | "futures-task", 438 | "memchr", 439 | "pin-project-lite", 440 | "pin-utils", 441 | "slab", 442 | ] 443 | 444 | [[package]] 445 | name = "getrandom" 446 | version = "0.2.15" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 449 | dependencies = [ 450 | "cfg-if", 451 | "libc", 452 | "wasi", 453 | ] 454 | 455 | [[package]] 456 | name = "gimli" 457 | version = "0.29.0" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 460 | 461 | [[package]] 462 | name = "h2" 463 | version = "0.3.26" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 466 | dependencies = [ 467 | "bytes", 468 | "fnv", 469 | "futures-core", 470 | "futures-sink", 471 | "futures-util", 472 | "http 0.2.12", 473 | "indexmap", 474 | "slab", 475 | "tokio", 476 | "tokio-util", 477 | "tracing", 478 | ] 479 | 480 | [[package]] 481 | name = "h2" 482 | version = "0.4.5" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" 485 | dependencies = [ 486 | "atomic-waker", 487 | "bytes", 488 | "fnv", 489 | "futures-core", 490 | "futures-sink", 491 | "http 1.1.0", 492 | "indexmap", 493 | "slab", 494 | "tokio", 495 | "tokio-util", 496 | "tracing", 497 | ] 498 | 499 | [[package]] 500 | name = "hashbrown" 501 | version = "0.14.5" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 504 | 505 | [[package]] 506 | name = "heck" 507 | version = "0.5.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 510 | 511 | [[package]] 512 | name = "hermit-abi" 513 | version = "0.3.9" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 516 | 517 | [[package]] 518 | name = "http" 519 | version = "0.2.12" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 522 | dependencies = [ 523 | "bytes", 524 | "fnv", 525 | "itoa", 526 | ] 527 | 528 | [[package]] 529 | name = "http" 530 | version = "1.1.0" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 533 | dependencies = [ 534 | "bytes", 535 | "fnv", 536 | "itoa", 537 | ] 538 | 539 | [[package]] 540 | name = "http-body" 541 | version = "0.4.6" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 544 | dependencies = [ 545 | "bytes", 546 | "http 0.2.12", 547 | "pin-project-lite", 548 | ] 549 | 550 | [[package]] 551 | name = "http-body" 552 | version = "1.0.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" 555 | dependencies = [ 556 | "bytes", 557 | "http 1.1.0", 558 | ] 559 | 560 | [[package]] 561 | name = "http-body-util" 562 | version = "0.1.2" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 565 | dependencies = [ 566 | "bytes", 567 | "futures-util", 568 | "http 1.1.0", 569 | "http-body 1.0.0", 570 | "pin-project-lite", 571 | ] 572 | 573 | [[package]] 574 | name = "httparse" 575 | version = "1.9.4" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" 578 | 579 | [[package]] 580 | name = "httpdate" 581 | version = "1.0.3" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 584 | 585 | [[package]] 586 | name = "humantime" 587 | version = "2.1.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 590 | 591 | [[package]] 592 | name = "hyper" 593 | version = "0.14.30" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" 596 | dependencies = [ 597 | "bytes", 598 | "futures-channel", 599 | "futures-core", 600 | "futures-util", 601 | "h2 0.3.26", 602 | "http 0.2.12", 603 | "http-body 0.4.6", 604 | "httparse", 605 | "httpdate", 606 | "itoa", 607 | "pin-project-lite", 608 | "tokio", 609 | "tower-service", 610 | "tracing", 611 | "want", 612 | ] 613 | 614 | [[package]] 615 | name = "hyper" 616 | version = "1.4.1" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" 619 | dependencies = [ 620 | "bytes", 621 | "futures-channel", 622 | "futures-util", 623 | "h2 0.4.5", 624 | "http 1.1.0", 625 | "http-body 1.0.0", 626 | "httparse", 627 | "itoa", 628 | "pin-project-lite", 629 | "smallvec", 630 | "tokio", 631 | "want", 632 | ] 633 | 634 | [[package]] 635 | name = "hyper-rustls" 636 | version = "0.27.2" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" 639 | dependencies = [ 640 | "futures-util", 641 | "http 1.1.0", 642 | "hyper 1.4.1", 643 | "hyper-util", 644 | "rustls", 645 | "rustls-pki-types", 646 | "tokio", 647 | "tokio-rustls", 648 | "tower-service", 649 | "webpki-roots", 650 | ] 651 | 652 | [[package]] 653 | name = "hyper-tls" 654 | version = "0.6.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 657 | dependencies = [ 658 | "bytes", 659 | "http-body-util", 660 | "hyper 1.4.1", 661 | "hyper-util", 662 | "native-tls", 663 | "tokio", 664 | "tokio-native-tls", 665 | "tower-service", 666 | ] 667 | 668 | [[package]] 669 | name = "hyper-util" 670 | version = "0.1.6" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" 673 | dependencies = [ 674 | "bytes", 675 | "futures-channel", 676 | "futures-util", 677 | "http 1.1.0", 678 | "http-body 1.0.0", 679 | "hyper 1.4.1", 680 | "pin-project-lite", 681 | "socket2", 682 | "tokio", 683 | "tower", 684 | "tower-service", 685 | "tracing", 686 | ] 687 | 688 | [[package]] 689 | name = "iana-time-zone" 690 | version = "0.1.60" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 693 | dependencies = [ 694 | "android_system_properties", 695 | "core-foundation-sys", 696 | "iana-time-zone-haiku", 697 | "js-sys", 698 | "wasm-bindgen", 699 | "windows-core", 700 | ] 701 | 702 | [[package]] 703 | name = "iana-time-zone-haiku" 704 | version = "0.1.2" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 707 | dependencies = [ 708 | "cc", 709 | ] 710 | 711 | [[package]] 712 | name = "idna" 713 | version = "0.5.0" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 716 | dependencies = [ 717 | "unicode-bidi", 718 | "unicode-normalization", 719 | ] 720 | 721 | [[package]] 722 | name = "indexmap" 723 | version = "2.2.6" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 726 | dependencies = [ 727 | "equivalent", 728 | "hashbrown", 729 | ] 730 | 731 | [[package]] 732 | name = "ipnet" 733 | version = "2.9.0" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 736 | 737 | [[package]] 738 | name = "is_terminal_polyfill" 739 | version = "1.70.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 742 | 743 | [[package]] 744 | name = "iter-read" 745 | version = "1.0.1" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "a598c1abae8e3456ebda517868b254b6bc2a9bb6501ffd5b9d0875bf332e048b" 748 | 749 | [[package]] 750 | name = "itoa" 751 | version = "1.0.11" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 754 | 755 | [[package]] 756 | name = "js-sys" 757 | version = "0.3.69" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 760 | dependencies = [ 761 | "wasm-bindgen", 762 | ] 763 | 764 | [[package]] 765 | name = "lazy_static" 766 | version = "1.5.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 769 | 770 | [[package]] 771 | name = "libc" 772 | version = "0.2.155" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 775 | 776 | [[package]] 777 | name = "linux-raw-sys" 778 | version = "0.4.13" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 781 | 782 | [[package]] 783 | name = "lock_api" 784 | version = "0.4.12" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 787 | dependencies = [ 788 | "autocfg", 789 | "scopeguard", 790 | ] 791 | 792 | [[package]] 793 | name = "log" 794 | version = "0.4.22" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 797 | 798 | [[package]] 799 | name = "md5" 800 | version = "0.7.0" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 803 | 804 | [[package]] 805 | name = "memchr" 806 | version = "2.7.4" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 809 | 810 | [[package]] 811 | name = "mime" 812 | version = "0.3.17" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 815 | 816 | [[package]] 817 | name = "miniz_oxide" 818 | version = "0.7.4" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 821 | dependencies = [ 822 | "adler", 823 | ] 824 | 825 | [[package]] 826 | name = "mio" 827 | version = "1.0.1" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" 830 | dependencies = [ 831 | "hermit-abi", 832 | "libc", 833 | "wasi", 834 | "windows-sys 0.52.0", 835 | ] 836 | 837 | [[package]] 838 | name = "mockito" 839 | version = "1.4.0" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "d2f6e023aa5bdf392aa06c78e4a4e6d498baab5138d0c993503350ebbc37bf1e" 842 | dependencies = [ 843 | "assert-json-diff", 844 | "colored", 845 | "futures-core", 846 | "hyper 0.14.30", 847 | "log", 848 | "rand", 849 | "regex", 850 | "serde_json", 851 | "serde_urlencoded", 852 | "similar", 853 | "tokio", 854 | ] 855 | 856 | [[package]] 857 | name = "native-tls" 858 | version = "0.2.11" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 861 | dependencies = [ 862 | "lazy_static", 863 | "libc", 864 | "log", 865 | "openssl", 866 | "openssl-probe", 867 | "openssl-sys", 868 | "schannel", 869 | "security-framework", 870 | "security-framework-sys", 871 | "tempfile", 872 | ] 873 | 874 | [[package]] 875 | name = "num-traits" 876 | version = "0.2.19" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 879 | dependencies = [ 880 | "autocfg", 881 | ] 882 | 883 | [[package]] 884 | name = "object" 885 | version = "0.36.1" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" 888 | dependencies = [ 889 | "memchr", 890 | ] 891 | 892 | [[package]] 893 | name = "once_cell" 894 | version = "1.19.0" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 897 | 898 | [[package]] 899 | name = "openssl" 900 | version = "0.10.63" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" 903 | dependencies = [ 904 | "bitflags 2.6.0", 905 | "cfg-if", 906 | "foreign-types", 907 | "libc", 908 | "once_cell", 909 | "openssl-macros", 910 | "openssl-sys", 911 | ] 912 | 913 | [[package]] 914 | name = "openssl-macros" 915 | version = "0.1.1" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 918 | dependencies = [ 919 | "proc-macro2", 920 | "quote", 921 | "syn", 922 | ] 923 | 924 | [[package]] 925 | name = "openssl-probe" 926 | version = "0.1.5" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 929 | 930 | [[package]] 931 | name = "openssl-sys" 932 | version = "0.9.99" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" 935 | dependencies = [ 936 | "cc", 937 | "libc", 938 | "pkg-config", 939 | "vcpkg", 940 | ] 941 | 942 | [[package]] 943 | name = "parking_lot" 944 | version = "0.12.3" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 947 | dependencies = [ 948 | "lock_api", 949 | "parking_lot_core", 950 | ] 951 | 952 | [[package]] 953 | name = "parking_lot_core" 954 | version = "0.9.10" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 957 | dependencies = [ 958 | "cfg-if", 959 | "libc", 960 | "redox_syscall", 961 | "smallvec", 962 | "windows-targets 0.52.6", 963 | ] 964 | 965 | [[package]] 966 | name = "percent-encoding" 967 | version = "2.3.1" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 970 | 971 | [[package]] 972 | name = "pin-project" 973 | version = "1.1.5" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 976 | dependencies = [ 977 | "pin-project-internal", 978 | ] 979 | 980 | [[package]] 981 | name = "pin-project-internal" 982 | version = "1.1.5" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 985 | dependencies = [ 986 | "proc-macro2", 987 | "quote", 988 | "syn", 989 | ] 990 | 991 | [[package]] 992 | name = "pin-project-lite" 993 | version = "0.2.14" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 996 | 997 | [[package]] 998 | name = "pin-utils" 999 | version = "0.1.0" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1002 | 1003 | [[package]] 1004 | name = "pkg-config" 1005 | version = "0.3.29" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" 1008 | 1009 | [[package]] 1010 | name = "ppv-lite86" 1011 | version = "0.2.17" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1014 | 1015 | [[package]] 1016 | name = "proc-macro2" 1017 | version = "1.0.86" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 1020 | dependencies = [ 1021 | "unicode-ident", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "quinn" 1026 | version = "0.11.2" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" 1029 | dependencies = [ 1030 | "bytes", 1031 | "pin-project-lite", 1032 | "quinn-proto", 1033 | "quinn-udp", 1034 | "rustc-hash", 1035 | "rustls", 1036 | "thiserror", 1037 | "tokio", 1038 | "tracing", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "quinn-proto" 1043 | version = "0.11.3" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" 1046 | dependencies = [ 1047 | "bytes", 1048 | "rand", 1049 | "ring", 1050 | "rustc-hash", 1051 | "rustls", 1052 | "slab", 1053 | "thiserror", 1054 | "tinyvec", 1055 | "tracing", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "quinn-udp" 1060 | version = "0.5.2" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" 1063 | dependencies = [ 1064 | "libc", 1065 | "once_cell", 1066 | "socket2", 1067 | "tracing", 1068 | "windows-sys 0.52.0", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "quote" 1073 | version = "1.0.36" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 1076 | dependencies = [ 1077 | "proc-macro2", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "rand" 1082 | version = "0.8.5" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1085 | dependencies = [ 1086 | "libc", 1087 | "rand_chacha", 1088 | "rand_core", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "rand_chacha" 1093 | version = "0.3.1" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1096 | dependencies = [ 1097 | "ppv-lite86", 1098 | "rand_core", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "rand_core" 1103 | version = "0.6.4" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1106 | dependencies = [ 1107 | "getrandom", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "rayon" 1112 | version = "1.10.0" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 1115 | dependencies = [ 1116 | "either", 1117 | "rayon-core", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "rayon-core" 1122 | version = "1.12.1" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 1125 | dependencies = [ 1126 | "crossbeam-deque", 1127 | "crossbeam-utils", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "redox_syscall" 1132 | version = "0.5.3" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 1135 | dependencies = [ 1136 | "bitflags 2.6.0", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "regex" 1141 | version = "1.10.5" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 1144 | dependencies = [ 1145 | "aho-corasick", 1146 | "memchr", 1147 | "regex-automata", 1148 | "regex-syntax", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "regex-automata" 1153 | version = "0.4.7" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 1156 | dependencies = [ 1157 | "aho-corasick", 1158 | "memchr", 1159 | "regex-syntax", 1160 | ] 1161 | 1162 | [[package]] 1163 | name = "regex-syntax" 1164 | version = "0.8.4" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 1167 | 1168 | [[package]] 1169 | name = "reqwest" 1170 | version = "0.12.5" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" 1173 | dependencies = [ 1174 | "base64", 1175 | "bytes", 1176 | "encoding_rs", 1177 | "futures-channel", 1178 | "futures-core", 1179 | "futures-util", 1180 | "h2 0.4.5", 1181 | "http 1.1.0", 1182 | "http-body 1.0.0", 1183 | "http-body-util", 1184 | "hyper 1.4.1", 1185 | "hyper-rustls", 1186 | "hyper-tls", 1187 | "hyper-util", 1188 | "ipnet", 1189 | "js-sys", 1190 | "log", 1191 | "mime", 1192 | "native-tls", 1193 | "once_cell", 1194 | "percent-encoding", 1195 | "pin-project-lite", 1196 | "quinn", 1197 | "rustls", 1198 | "rustls-pemfile", 1199 | "rustls-pki-types", 1200 | "serde", 1201 | "serde_json", 1202 | "serde_urlencoded", 1203 | "sync_wrapper", 1204 | "system-configuration", 1205 | "tokio", 1206 | "tokio-native-tls", 1207 | "tokio-rustls", 1208 | "tower-service", 1209 | "url", 1210 | "wasm-bindgen", 1211 | "wasm-bindgen-futures", 1212 | "web-sys", 1213 | "webpki-roots", 1214 | "winreg", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "ring" 1219 | version = "0.17.7" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" 1222 | dependencies = [ 1223 | "cc", 1224 | "getrandom", 1225 | "libc", 1226 | "spin", 1227 | "untrusted", 1228 | "windows-sys 0.48.0", 1229 | ] 1230 | 1231 | [[package]] 1232 | name = "roxmltree" 1233 | version = "0.20.0" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" 1236 | 1237 | [[package]] 1238 | name = "rustc-demangle" 1239 | version = "0.1.24" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1242 | 1243 | [[package]] 1244 | name = "rustc-hash" 1245 | version = "1.1.0" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1248 | 1249 | [[package]] 1250 | name = "rustix" 1251 | version = "0.38.31" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" 1254 | dependencies = [ 1255 | "bitflags 2.6.0", 1256 | "errno", 1257 | "libc", 1258 | "linux-raw-sys", 1259 | "windows-sys 0.52.0", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "rustls" 1264 | version = "0.23.11" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" 1267 | dependencies = [ 1268 | "once_cell", 1269 | "ring", 1270 | "rustls-pki-types", 1271 | "rustls-webpki", 1272 | "subtle", 1273 | "zeroize", 1274 | ] 1275 | 1276 | [[package]] 1277 | name = "rustls-pemfile" 1278 | version = "2.1.2" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" 1281 | dependencies = [ 1282 | "base64", 1283 | "rustls-pki-types", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "rustls-pki-types" 1288 | version = "1.7.0" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" 1291 | 1292 | [[package]] 1293 | name = "rustls-webpki" 1294 | version = "0.102.5" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" 1297 | dependencies = [ 1298 | "ring", 1299 | "rustls-pki-types", 1300 | "untrusted", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "ryu" 1305 | version = "1.0.18" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1308 | 1309 | [[package]] 1310 | name = "schannel" 1311 | version = "0.1.23" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" 1314 | dependencies = [ 1315 | "windows-sys 0.52.0", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "scopeguard" 1320 | version = "1.2.0" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1323 | 1324 | [[package]] 1325 | name = "security-framework" 1326 | version = "2.9.2" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" 1329 | dependencies = [ 1330 | "bitflags 1.3.2", 1331 | "core-foundation", 1332 | "core-foundation-sys", 1333 | "libc", 1334 | "security-framework-sys", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "security-framework-sys" 1339 | version = "2.9.1" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" 1342 | dependencies = [ 1343 | "core-foundation-sys", 1344 | "libc", 1345 | ] 1346 | 1347 | [[package]] 1348 | name = "serde" 1349 | version = "1.0.204" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" 1352 | dependencies = [ 1353 | "serde_derive", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "serde_derive" 1358 | version = "1.0.204" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" 1361 | dependencies = [ 1362 | "proc-macro2", 1363 | "quote", 1364 | "syn", 1365 | ] 1366 | 1367 | [[package]] 1368 | name = "serde_json" 1369 | version = "1.0.120" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" 1372 | dependencies = [ 1373 | "itoa", 1374 | "ryu", 1375 | "serde", 1376 | ] 1377 | 1378 | [[package]] 1379 | name = "serde_urlencoded" 1380 | version = "0.7.1" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1383 | dependencies = [ 1384 | "form_urlencoded", 1385 | "itoa", 1386 | "ryu", 1387 | "serde", 1388 | ] 1389 | 1390 | [[package]] 1391 | name = "similar" 1392 | version = "2.6.0" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" 1395 | 1396 | [[package]] 1397 | name = "slab" 1398 | version = "0.4.9" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1401 | dependencies = [ 1402 | "autocfg", 1403 | ] 1404 | 1405 | [[package]] 1406 | name = "smallvec" 1407 | version = "1.13.2" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1410 | 1411 | [[package]] 1412 | name = "socket2" 1413 | version = "0.5.7" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1416 | dependencies = [ 1417 | "libc", 1418 | "windows-sys 0.52.0", 1419 | ] 1420 | 1421 | [[package]] 1422 | name = "speedtest-rs" 1423 | version = "0.2.0" 1424 | dependencies = [ 1425 | "chrono", 1426 | "clap", 1427 | "csv", 1428 | "env_logger", 1429 | "iter-read", 1430 | "log", 1431 | "md5", 1432 | "mockito", 1433 | "rayon", 1434 | "reqwest", 1435 | "roxmltree", 1436 | "serde", 1437 | "url", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "spin" 1442 | version = "0.9.8" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1445 | 1446 | [[package]] 1447 | name = "strsim" 1448 | version = "0.11.1" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1451 | 1452 | [[package]] 1453 | name = "subtle" 1454 | version = "2.6.1" 1455 | source = "registry+https://github.com/rust-lang/crates.io-index" 1456 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1457 | 1458 | [[package]] 1459 | name = "syn" 1460 | version = "2.0.72" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" 1463 | dependencies = [ 1464 | "proc-macro2", 1465 | "quote", 1466 | "unicode-ident", 1467 | ] 1468 | 1469 | [[package]] 1470 | name = "sync_wrapper" 1471 | version = "1.0.1" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 1474 | 1475 | [[package]] 1476 | name = "system-configuration" 1477 | version = "0.5.1" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1480 | dependencies = [ 1481 | "bitflags 1.3.2", 1482 | "core-foundation", 1483 | "system-configuration-sys", 1484 | ] 1485 | 1486 | [[package]] 1487 | name = "system-configuration-sys" 1488 | version = "0.5.0" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1491 | dependencies = [ 1492 | "core-foundation-sys", 1493 | "libc", 1494 | ] 1495 | 1496 | [[package]] 1497 | name = "tempfile" 1498 | version = "3.10.0" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" 1501 | dependencies = [ 1502 | "cfg-if", 1503 | "fastrand", 1504 | "rustix", 1505 | "windows-sys 0.52.0", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "thiserror" 1510 | version = "1.0.62" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" 1513 | dependencies = [ 1514 | "thiserror-impl", 1515 | ] 1516 | 1517 | [[package]] 1518 | name = "thiserror-impl" 1519 | version = "1.0.62" 1520 | source = "registry+https://github.com/rust-lang/crates.io-index" 1521 | checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" 1522 | dependencies = [ 1523 | "proc-macro2", 1524 | "quote", 1525 | "syn", 1526 | ] 1527 | 1528 | [[package]] 1529 | name = "tinyvec" 1530 | version = "1.8.0" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1533 | dependencies = [ 1534 | "tinyvec_macros", 1535 | ] 1536 | 1537 | [[package]] 1538 | name = "tinyvec_macros" 1539 | version = "0.1.1" 1540 | source = "registry+https://github.com/rust-lang/crates.io-index" 1541 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1542 | 1543 | [[package]] 1544 | name = "tokio" 1545 | version = "1.39.1" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" 1548 | dependencies = [ 1549 | "backtrace", 1550 | "bytes", 1551 | "libc", 1552 | "mio", 1553 | "parking_lot", 1554 | "pin-project-lite", 1555 | "socket2", 1556 | "windows-sys 0.52.0", 1557 | ] 1558 | 1559 | [[package]] 1560 | name = "tokio-native-tls" 1561 | version = "0.3.1" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1564 | dependencies = [ 1565 | "native-tls", 1566 | "tokio", 1567 | ] 1568 | 1569 | [[package]] 1570 | name = "tokio-rustls" 1571 | version = "0.26.0" 1572 | source = "registry+https://github.com/rust-lang/crates.io-index" 1573 | checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" 1574 | dependencies = [ 1575 | "rustls", 1576 | "rustls-pki-types", 1577 | "tokio", 1578 | ] 1579 | 1580 | [[package]] 1581 | name = "tokio-util" 1582 | version = "0.7.11" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" 1585 | dependencies = [ 1586 | "bytes", 1587 | "futures-core", 1588 | "futures-sink", 1589 | "pin-project-lite", 1590 | "tokio", 1591 | ] 1592 | 1593 | [[package]] 1594 | name = "tower" 1595 | version = "0.4.13" 1596 | source = "registry+https://github.com/rust-lang/crates.io-index" 1597 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1598 | dependencies = [ 1599 | "futures-core", 1600 | "futures-util", 1601 | "pin-project", 1602 | "pin-project-lite", 1603 | "tokio", 1604 | "tower-layer", 1605 | "tower-service", 1606 | ] 1607 | 1608 | [[package]] 1609 | name = "tower-layer" 1610 | version = "0.3.2" 1611 | source = "registry+https://github.com/rust-lang/crates.io-index" 1612 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 1613 | 1614 | [[package]] 1615 | name = "tower-service" 1616 | version = "0.3.2" 1617 | source = "registry+https://github.com/rust-lang/crates.io-index" 1618 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1619 | 1620 | [[package]] 1621 | name = "tracing" 1622 | version = "0.1.40" 1623 | source = "registry+https://github.com/rust-lang/crates.io-index" 1624 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1625 | dependencies = [ 1626 | "pin-project-lite", 1627 | "tracing-attributes", 1628 | "tracing-core", 1629 | ] 1630 | 1631 | [[package]] 1632 | name = "tracing-attributes" 1633 | version = "0.1.27" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1636 | dependencies = [ 1637 | "proc-macro2", 1638 | "quote", 1639 | "syn", 1640 | ] 1641 | 1642 | [[package]] 1643 | name = "tracing-core" 1644 | version = "0.1.32" 1645 | source = "registry+https://github.com/rust-lang/crates.io-index" 1646 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1647 | dependencies = [ 1648 | "once_cell", 1649 | ] 1650 | 1651 | [[package]] 1652 | name = "try-lock" 1653 | version = "0.2.5" 1654 | source = "registry+https://github.com/rust-lang/crates.io-index" 1655 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1656 | 1657 | [[package]] 1658 | name = "unicode-bidi" 1659 | version = "0.3.15" 1660 | source = "registry+https://github.com/rust-lang/crates.io-index" 1661 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1662 | 1663 | [[package]] 1664 | name = "unicode-ident" 1665 | version = "1.0.12" 1666 | source = "registry+https://github.com/rust-lang/crates.io-index" 1667 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1668 | 1669 | [[package]] 1670 | name = "unicode-normalization" 1671 | version = "0.1.23" 1672 | source = "registry+https://github.com/rust-lang/crates.io-index" 1673 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1674 | dependencies = [ 1675 | "tinyvec", 1676 | ] 1677 | 1678 | [[package]] 1679 | name = "untrusted" 1680 | version = "0.9.0" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1683 | 1684 | [[package]] 1685 | name = "url" 1686 | version = "2.5.2" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 1689 | dependencies = [ 1690 | "form_urlencoded", 1691 | "idna", 1692 | "percent-encoding", 1693 | ] 1694 | 1695 | [[package]] 1696 | name = "utf8parse" 1697 | version = "0.2.2" 1698 | source = "registry+https://github.com/rust-lang/crates.io-index" 1699 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1700 | 1701 | [[package]] 1702 | name = "vcpkg" 1703 | version = "0.2.15" 1704 | source = "registry+https://github.com/rust-lang/crates.io-index" 1705 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1706 | 1707 | [[package]] 1708 | name = "want" 1709 | version = "0.3.1" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1712 | dependencies = [ 1713 | "try-lock", 1714 | ] 1715 | 1716 | [[package]] 1717 | name = "wasi" 1718 | version = "0.11.0+wasi-snapshot-preview1" 1719 | source = "registry+https://github.com/rust-lang/crates.io-index" 1720 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1721 | 1722 | [[package]] 1723 | name = "wasm-bindgen" 1724 | version = "0.2.92" 1725 | source = "registry+https://github.com/rust-lang/crates.io-index" 1726 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1727 | dependencies = [ 1728 | "cfg-if", 1729 | "wasm-bindgen-macro", 1730 | ] 1731 | 1732 | [[package]] 1733 | name = "wasm-bindgen-backend" 1734 | version = "0.2.92" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1737 | dependencies = [ 1738 | "bumpalo", 1739 | "log", 1740 | "once_cell", 1741 | "proc-macro2", 1742 | "quote", 1743 | "syn", 1744 | "wasm-bindgen-shared", 1745 | ] 1746 | 1747 | [[package]] 1748 | name = "wasm-bindgen-futures" 1749 | version = "0.4.41" 1750 | source = "registry+https://github.com/rust-lang/crates.io-index" 1751 | checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" 1752 | dependencies = [ 1753 | "cfg-if", 1754 | "js-sys", 1755 | "wasm-bindgen", 1756 | "web-sys", 1757 | ] 1758 | 1759 | [[package]] 1760 | name = "wasm-bindgen-macro" 1761 | version = "0.2.92" 1762 | source = "registry+https://github.com/rust-lang/crates.io-index" 1763 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1764 | dependencies = [ 1765 | "quote", 1766 | "wasm-bindgen-macro-support", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "wasm-bindgen-macro-support" 1771 | version = "0.2.92" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1774 | dependencies = [ 1775 | "proc-macro2", 1776 | "quote", 1777 | "syn", 1778 | "wasm-bindgen-backend", 1779 | "wasm-bindgen-shared", 1780 | ] 1781 | 1782 | [[package]] 1783 | name = "wasm-bindgen-shared" 1784 | version = "0.2.92" 1785 | source = "registry+https://github.com/rust-lang/crates.io-index" 1786 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1787 | 1788 | [[package]] 1789 | name = "web-sys" 1790 | version = "0.3.68" 1791 | source = "registry+https://github.com/rust-lang/crates.io-index" 1792 | checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" 1793 | dependencies = [ 1794 | "js-sys", 1795 | "wasm-bindgen", 1796 | ] 1797 | 1798 | [[package]] 1799 | name = "webpki-roots" 1800 | version = "0.26.3" 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" 1802 | checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" 1803 | dependencies = [ 1804 | "rustls-pki-types", 1805 | ] 1806 | 1807 | [[package]] 1808 | name = "windows-core" 1809 | version = "0.52.0" 1810 | source = "registry+https://github.com/rust-lang/crates.io-index" 1811 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1812 | dependencies = [ 1813 | "windows-targets 0.52.6", 1814 | ] 1815 | 1816 | [[package]] 1817 | name = "windows-sys" 1818 | version = "0.48.0" 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" 1820 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1821 | dependencies = [ 1822 | "windows-targets 0.48.5", 1823 | ] 1824 | 1825 | [[package]] 1826 | name = "windows-sys" 1827 | version = "0.52.0" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1830 | dependencies = [ 1831 | "windows-targets 0.52.6", 1832 | ] 1833 | 1834 | [[package]] 1835 | name = "windows-targets" 1836 | version = "0.48.5" 1837 | source = "registry+https://github.com/rust-lang/crates.io-index" 1838 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1839 | dependencies = [ 1840 | "windows_aarch64_gnullvm 0.48.5", 1841 | "windows_aarch64_msvc 0.48.5", 1842 | "windows_i686_gnu 0.48.5", 1843 | "windows_i686_msvc 0.48.5", 1844 | "windows_x86_64_gnu 0.48.5", 1845 | "windows_x86_64_gnullvm 0.48.5", 1846 | "windows_x86_64_msvc 0.48.5", 1847 | ] 1848 | 1849 | [[package]] 1850 | name = "windows-targets" 1851 | version = "0.52.6" 1852 | source = "registry+https://github.com/rust-lang/crates.io-index" 1853 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1854 | dependencies = [ 1855 | "windows_aarch64_gnullvm 0.52.6", 1856 | "windows_aarch64_msvc 0.52.6", 1857 | "windows_i686_gnu 0.52.6", 1858 | "windows_i686_gnullvm", 1859 | "windows_i686_msvc 0.52.6", 1860 | "windows_x86_64_gnu 0.52.6", 1861 | "windows_x86_64_gnullvm 0.52.6", 1862 | "windows_x86_64_msvc 0.52.6", 1863 | ] 1864 | 1865 | [[package]] 1866 | name = "windows_aarch64_gnullvm" 1867 | version = "0.48.5" 1868 | source = "registry+https://github.com/rust-lang/crates.io-index" 1869 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1870 | 1871 | [[package]] 1872 | name = "windows_aarch64_gnullvm" 1873 | version = "0.52.6" 1874 | source = "registry+https://github.com/rust-lang/crates.io-index" 1875 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1876 | 1877 | [[package]] 1878 | name = "windows_aarch64_msvc" 1879 | version = "0.48.5" 1880 | source = "registry+https://github.com/rust-lang/crates.io-index" 1881 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1882 | 1883 | [[package]] 1884 | name = "windows_aarch64_msvc" 1885 | version = "0.52.6" 1886 | source = "registry+https://github.com/rust-lang/crates.io-index" 1887 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1888 | 1889 | [[package]] 1890 | name = "windows_i686_gnu" 1891 | version = "0.48.5" 1892 | source = "registry+https://github.com/rust-lang/crates.io-index" 1893 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1894 | 1895 | [[package]] 1896 | name = "windows_i686_gnu" 1897 | version = "0.52.6" 1898 | source = "registry+https://github.com/rust-lang/crates.io-index" 1899 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1900 | 1901 | [[package]] 1902 | name = "windows_i686_gnullvm" 1903 | version = "0.52.6" 1904 | source = "registry+https://github.com/rust-lang/crates.io-index" 1905 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1906 | 1907 | [[package]] 1908 | name = "windows_i686_msvc" 1909 | version = "0.48.5" 1910 | source = "registry+https://github.com/rust-lang/crates.io-index" 1911 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1912 | 1913 | [[package]] 1914 | name = "windows_i686_msvc" 1915 | version = "0.52.6" 1916 | source = "registry+https://github.com/rust-lang/crates.io-index" 1917 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1918 | 1919 | [[package]] 1920 | name = "windows_x86_64_gnu" 1921 | version = "0.48.5" 1922 | source = "registry+https://github.com/rust-lang/crates.io-index" 1923 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1924 | 1925 | [[package]] 1926 | name = "windows_x86_64_gnu" 1927 | version = "0.52.6" 1928 | source = "registry+https://github.com/rust-lang/crates.io-index" 1929 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1930 | 1931 | [[package]] 1932 | name = "windows_x86_64_gnullvm" 1933 | version = "0.48.5" 1934 | source = "registry+https://github.com/rust-lang/crates.io-index" 1935 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1936 | 1937 | [[package]] 1938 | name = "windows_x86_64_gnullvm" 1939 | version = "0.52.6" 1940 | source = "registry+https://github.com/rust-lang/crates.io-index" 1941 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1942 | 1943 | [[package]] 1944 | name = "windows_x86_64_msvc" 1945 | version = "0.48.5" 1946 | source = "registry+https://github.com/rust-lang/crates.io-index" 1947 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1948 | 1949 | [[package]] 1950 | name = "windows_x86_64_msvc" 1951 | version = "0.52.6" 1952 | source = "registry+https://github.com/rust-lang/crates.io-index" 1953 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1954 | 1955 | [[package]] 1956 | name = "winreg" 1957 | version = "0.52.0" 1958 | source = "registry+https://github.com/rust-lang/crates.io-index" 1959 | checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" 1960 | dependencies = [ 1961 | "cfg-if", 1962 | "windows-sys 0.48.0", 1963 | ] 1964 | 1965 | [[package]] 1966 | name = "zeroize" 1967 | version = "1.8.1" 1968 | source = "registry+https://github.com/rust-lang/crates.io-index" 1969 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1970 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Nelson Chen "] 3 | description = "Speedtest.net testing utility and crate" 4 | exclude = ["tests/config/*"] 5 | license = "MIT OR Apache-2.0" 6 | name = "speedtest-rs" 7 | repository = "https://github.com/nelsonjchen/speedtest-rs" 8 | version = "0.2.0" 9 | edition = "2021" 10 | 11 | [dependencies] 12 | clap = { version = "4.5.10", features = ["derive"] } 13 | chrono = "0.4.38" 14 | env_logger = "0.11.4" 15 | log = { version = "0.4.22", optional = true } 16 | url = "2.5.2" 17 | mockito = "1.4.0" 18 | md5 = "0.7.0" 19 | csv = "1.3.0" 20 | serde = { version = "1.0.204", features = ["derive"] } 21 | roxmltree = "0.20.0" 22 | rayon = "1.10.0" 23 | iter-read = "1.0.1" 24 | 25 | [dependencies.reqwest] 26 | version = "0.12" 27 | features = ["blocking"] 28 | 29 | [features] 30 | # default = ["log"] 31 | rustls-tls = ["reqwest/rustls-tls"] 32 | log = ["dep:log"] 33 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maintenance Message 2 | 3 | I’m sorry for the pun, but I no longer have the bandwidth to maintain or develop this project. I also don’t have the bandwidth to search for or evaluate new maintainers—and, frankly, I’m not terribly interested in doing so after hearing about stuff like xz. This project isn't "xz", but I don't want to make a bad choice. 4 | 5 | When I originally built this project, the goal was to port speedtest-cli (a Python tool) to Rust for use on an ARM9/ARM926EJ-S receipt printer. Ironically, although I now work for an ISP, my interest in further developing or supporting this project hasn’t increased. This tool technically violates Ookla’s current Terms of Service, and Ookla now provides its own binaries for speed tests. Besides, I’m also not on the ISP team that handles speed testing and/or uses iperf. 6 | 7 | As of now, this project is officially mothballed. I will not be accepting pull requests for code changes or updating the crate, at least for the foreseeable future. However, I will accept pull requests related to the list of alternative projects below. You’re welcome to fork this project and give it a new name, which I’d be happy to add to this list. 8 | 9 | Alternatives (ordered by GitHub stars at the time of PR): 10 | 11 | * to be filled 12 | 13 | # speedtest-rs 14 | 15 | *a tool like `speedtest-cli`, but in Rust* 16 | 17 | ![Continuous integration](https://github.com/nelsonjchen/speedtest-rs/workflows/Continuous%20integration/badge.svg) 18 | [![](https://img.shields.io/crates/v/speedtest-rs.svg)](https://crates.io/crates/speedtest-rs) 19 | 20 | Status: This is usable for lower-end residential connections using ["HTTP Legacy Fallback"][http_legacy_fallback] 21 | 22 | ## Install from AUR 23 | 24 | ```sh 25 | paru -S speedtest-rs 26 | ``` 27 | 28 | or 29 | 30 | ```sh 31 | paru -S speedtest-rs-bin 32 | ``` 33 | 34 | ## [HTTP Legacy Fallback][http_legacy_fallback] 35 | 36 | This tool currently only supports [HTTP Legacy Fallback][http_legacy_fallback] for testing. 37 | 38 | High bandwidth connections higher than ~200Mbps may return incorrect results! 39 | 40 | The testing operations are different from socket versions of tools connecting to speedtest.net infrastructure. In the many FOSS Go versions, tests are done to find an amount of data that can run for a default of 3 seconds over some TCP connection. In particular, `speedtest-cli` and `speedtest-rs` tests with what Ookla calls the ["HTTP Legacy Fallback"][http_legacy_fallback] for hosts that cannot establish a direct TCP connection. 41 | 42 | ### Ookla speedtest now has their own non-FOSS CLI tool that's native and available for many platforms. 43 | 44 | * TCP-based 45 | * Higher Bandwidth capable. 46 | 47 | https://www.speedtest.net/apps/cli 48 | 49 | Please look here. Unfortunately, it is not FOSS. Still, it is supported by them and can be used for non-commercial purposes. 50 | 51 | ## Purpose 52 | 53 | This is a learning exercise for me to learn Rust and keeping up with its ecosystem. 54 | 55 | The [HTTP Legacy Fallback][http_legacy_fallback] is currently based on the popular Python implementation: 56 | 57 | https://github.com/sivel/speedtest-cli @ 2.1.2 58 | 59 | There are also other speedtest.net using tools using different approaches to be stolen from in the future. For example: 60 | 61 | https://github.com/traetox/speedtest 62 | 63 | This example seems different as it appears to just use TCP connections and some protocol. It's probably more suitable to high-speed connections. TODO: Add a default TCP-mode. 64 | 65 | ## Use as a Library 66 | 67 | The API is very much not stable. Use at your own risk. Semver adherence definitely not guaranteed. Please lock to exact versions if you must. 68 | 69 | ## License 70 | 71 | Licensed under either of 72 | 73 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 74 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 75 | 76 | at your option. 77 | 78 | ### Contribution 79 | 80 | Unless you explicitly state otherwise, any contribution intentionally submitted 81 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 82 | additional terms or conditions. 83 | 84 | [http_legacy_fallback]: https://web.archive.org/web/20161109011118/http://www.ookla.com/support/a84541858 85 | -------------------------------------------------------------------------------- /src/distance.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts; 2 | 3 | #[derive(Clone, Debug, PartialEq, Default)] 4 | pub struct EarthLocation { 5 | pub latitude: f32, 6 | pub longitude: f32, 7 | } 8 | 9 | pub fn compute_distance(origin: &EarthLocation, destination: &EarthLocation) -> f32 { 10 | let radius: f32 = 6371.0; 11 | let d_lat = to_radians(origin.latitude - destination.latitude); 12 | let d_long = to_radians(origin.longitude - destination.longitude); 13 | let a = (d_lat / 2.0).sin() * (d_lat / 2.0).sin() 14 | + to_radians(origin.latitude).cos() 15 | * to_radians(destination.latitude).cos() 16 | * (d_long / 2.0).sin() 17 | * (d_long / 2.0).sin(); 18 | let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt()); 19 | radius * c 20 | } 21 | 22 | fn to_radians(degree: f32) -> f32 { 23 | let value: f32 = consts::PI; 24 | degree * (value / 180.0f32) 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | 31 | #[test] 32 | fn test_distance() { 33 | let origin = EarthLocation { 34 | latitude: 32.9545, 35 | longitude: -117.2333, 36 | }; 37 | let destination = EarthLocation { 38 | latitude: 70.0733, 39 | longitude: 29.7497, 40 | }; 41 | let distance = compute_distance(&origin, &destination); 42 | let diff = (distance - 8255.1).abs(); 43 | println!("distance: {distance} diff: {diff}"); 44 | assert!(diff < 0.2); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[derive(Debug)] 4 | pub enum SpeedTestError { 5 | Reqwest(reqwest::Error), 6 | Io(::std::io::Error), 7 | Csv(csv::Error), 8 | ParseFloatError(std::num::ParseFloatError), 9 | ParseIntError(std::num::ParseIntError), 10 | AddrParseError(std::net::AddrParseError), 11 | RoXmlTreeError(roxmltree::Error), 12 | ConfigParseError, 13 | ServerParseError, 14 | LatencyTestInvalidPath, 15 | LatencyTestNoServerError, 16 | LatencyTestClosestError, 17 | UrlParseError(url::ParseError), 18 | SystemTimeError(std::time::SystemTimeError), 19 | ParseShareUrlError, 20 | ThreadPoolBuildError(rayon::ThreadPoolBuildError), 21 | } 22 | 23 | impl From for SpeedTestError { 24 | fn from(err: reqwest::Error) -> SpeedTestError { 25 | SpeedTestError::Reqwest(err) 26 | } 27 | } 28 | 29 | impl From<::std::io::Error> for SpeedTestError { 30 | fn from(err: ::std::io::Error) -> SpeedTestError { 31 | SpeedTestError::Io(err) 32 | } 33 | } 34 | 35 | impl From for SpeedTestError { 36 | fn from(err: csv::Error) -> SpeedTestError { 37 | SpeedTestError::Csv(err) 38 | } 39 | } 40 | 41 | impl From for SpeedTestError { 42 | fn from(err: std::num::ParseFloatError) -> SpeedTestError { 43 | SpeedTestError::ParseFloatError(err) 44 | } 45 | } 46 | 47 | impl From for SpeedTestError { 48 | fn from(err: std::num::ParseIntError) -> SpeedTestError { 49 | SpeedTestError::ParseIntError(err) 50 | } 51 | } 52 | 53 | impl From for SpeedTestError { 54 | fn from(err: std::net::AddrParseError) -> SpeedTestError { 55 | SpeedTestError::AddrParseError(err) 56 | } 57 | } 58 | 59 | impl From for SpeedTestError { 60 | fn from(err: roxmltree::Error) -> SpeedTestError { 61 | SpeedTestError::RoXmlTreeError(err) 62 | } 63 | } 64 | 65 | impl From for SpeedTestError { 66 | fn from(err: url::ParseError) -> SpeedTestError { 67 | SpeedTestError::UrlParseError(err) 68 | } 69 | } 70 | 71 | impl From for SpeedTestError { 72 | fn from(err: std::time::SystemTimeError) -> SpeedTestError { 73 | SpeedTestError::SystemTimeError(err) 74 | } 75 | } 76 | 77 | impl From for SpeedTestError { 78 | fn from(err: rayon::ThreadPoolBuildError) -> SpeedTestError { 79 | SpeedTestError::ThreadPoolBuildError(err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // This crate really isn't meant to be stable. 2 | 3 | pub mod distance; 4 | pub mod error; 5 | pub mod speedtest; 6 | pub mod speedtest_config; 7 | pub mod speedtest_csv; 8 | pub mod speedtest_servers_config; 9 | 10 | #[cfg(not(feature = "log"))] 11 | mod log; 12 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | macro_rules! info { 2 | // info!(target: "my_target", key1 = 42, key2 = true; "a {} event", "log") 3 | // info!(target: "my_target", "a {} event", "log") 4 | (target: $target:expr, $($arg:tt)+) => {}; 5 | 6 | // info!("a {} event", "log") 7 | ($($arg:tt)+) => {}; 8 | } 9 | 10 | pub(crate) use info; 11 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod distance; 2 | mod error; 3 | #[cfg(not(feature = "log"))] 4 | mod log; 5 | mod speedtest; 6 | mod speedtest_config; 7 | mod speedtest_csv; 8 | mod speedtest_servers_config; 9 | 10 | use crate::speedtest_csv::SpeedTestCsvResult; 11 | use chrono::Utc; 12 | use clap::Parser; 13 | #[cfg(feature = "log")] 14 | use log::info; 15 | #[cfg(not(feature = "log"))] 16 | use log::info; 17 | use std::io::{self, Write}; 18 | use url::Url; 19 | 20 | #[derive(Parser)] 21 | #[command(version, about, long_about = None)] 22 | struct Cli { 23 | /// Don't run download test 24 | #[arg(long, default_value_t = false)] 25 | no_download: bool, 26 | 27 | /// Don't run upload test 28 | #[arg(long, default_value_t = false)] 29 | no_upload: bool, 30 | 31 | /// Display a list of speedtest.net servers sorted by distance 32 | #[arg(long, default_value_t = false)] 33 | list: bool, 34 | 35 | /// Generate and provide an URL to the speedtest.net share results image 36 | #[arg(long, default_value_t = false)] 37 | share: bool, 38 | 39 | /// Display values in bytes instead of bits. 40 | #[arg(long, default_value_t = false)] 41 | bytes: bool, 42 | 43 | /// Suppress verbose output, only show basic information 44 | #[arg(long, default_value_t = false)] 45 | simple: bool, 46 | 47 | /// Suppress verbose output, only show basic information in CSV format. 48 | /// Speeds listed in bit/s and not affected by --bytes. 49 | #[arg(long, default_value_t = false)] 50 | csv: bool, 51 | 52 | /// Print CSV headers 53 | #[arg(long, default_value_t = false)] 54 | csv_header: bool, 55 | 56 | /// Single character delimiter to use in CSV output 57 | #[arg(long, default_value_t = ',')] 58 | csv_delimiter: char, 59 | 60 | /// Address of speedtest-mini server 61 | #[arg(short, long)] 62 | mini: Option, 63 | } 64 | 65 | fn main() -> Result<(), error::SpeedTestError> { 66 | env_logger::init(); 67 | 68 | let matches = Cli::parse(); 69 | 70 | // This appears to be purely informational. 71 | if matches.csv_header { 72 | let results = speedtest_csv::SpeedTestCsvResult::default(); 73 | 74 | println!("{}", results.header_serialize()); 75 | return Ok(()); 76 | } 77 | 78 | let machine_format = matches.csv; 79 | 80 | if !matches.simple && !machine_format { 81 | println!("Retrieving speedtest.net configuration..."); 82 | } 83 | let mut config = speedtest::get_configuration()?; 84 | 85 | let mut server_list_sorted; 86 | if let Some(mini) = matches.mini { 87 | let mini_url = Url::parse(&mini).unwrap(); 88 | 89 | // matches.value_of("mini").unwrap().to_string() 90 | 91 | let host = mini_url.host().unwrap().to_string(); 92 | let hostport = mini_url // 93 | .port() 94 | .map_or_else( 95 | || format!("{}:{}", mini_url.host().unwrap(), mini_url.port().unwrap()), 96 | |_| host.to_string(), 97 | ); 98 | 99 | let mut path = mini_url.path(); 100 | if path == "/" { 101 | path = "/speedtest/upload.php"; 102 | } 103 | 104 | let url = format!("{}://{hostport}{path}", mini_url.scheme()); 105 | 106 | server_list_sorted = vec![speedtest::SpeedTestServer { 107 | country: host.to_string(), 108 | host: hostport, 109 | id: 0, 110 | location: distance::EarthLocation { 111 | latitude: 0.0, 112 | longitude: 0.0, 113 | }, 114 | distance: None, 115 | name: host.to_string(), 116 | sponsor: host, 117 | url, 118 | }] 119 | } else { 120 | if !matches.simple && !machine_format { 121 | println!("Retrieving speedtest.net server list..."); 122 | } 123 | let server_list = speedtest::get_server_list_with_config(&config)?; 124 | server_list_sorted = server_list.servers_sorted_by_distance(&config); 125 | 126 | if matches.list { 127 | for server in server_list_sorted { 128 | println!( 129 | "{:4}) {} ({}, {}) [{}]", 130 | server.id, 131 | server.sponsor, 132 | server.name, 133 | server.country, 134 | server 135 | .distance 136 | .map_or_else(|| "None".to_string(), |d| format!("{d:.2} km")), 137 | ); 138 | } 139 | return Ok(()); 140 | } 141 | if !matches.simple && !machine_format { 142 | println!( 143 | "Testing from {} ({})...", 144 | config.client.isp, config.client.ip 145 | ); 146 | println!("Selecting best server based on latency..."); 147 | } 148 | 149 | info!("Five Closest Servers"); 150 | server_list_sorted.truncate(5); 151 | for _server in &server_list_sorted { 152 | info!("Close Server: {_server:?}"); 153 | } 154 | } 155 | let latency_test_result = speedtest::get_best_server_based_on_latency(&server_list_sorted[..])?; 156 | 157 | if !machine_format { 158 | if !matches.simple { 159 | println!( 160 | "Hosted by {} ({}){}: {}.{} ms", 161 | latency_test_result.server.sponsor, 162 | latency_test_result.server.name, 163 | latency_test_result 164 | .server 165 | .distance 166 | .map_or("".to_string(), |d| format!(" [{d:.2} km]")), 167 | latency_test_result.latency.as_millis(), 168 | latency_test_result.latency.as_micros() % 1000, 169 | ); 170 | } else { 171 | println!( 172 | "Ping: {}.{} ms", 173 | latency_test_result.latency.as_millis(), 174 | latency_test_result.latency.as_millis() % 1000, 175 | ); 176 | } 177 | } 178 | 179 | let best_server = latency_test_result.server; 180 | 181 | let download_measurement; 182 | let inner_download_measurement; 183 | 184 | if !matches.no_download { 185 | if !matches.simple && !machine_format { 186 | print!("Testing download speed"); 187 | inner_download_measurement = speedtest::test_download_with_progress_and_config( 188 | best_server, 189 | print_dot, 190 | &mut config, 191 | )?; 192 | println!(); 193 | } else { 194 | inner_download_measurement = 195 | speedtest::test_download_with_progress_and_config(best_server, || {}, &mut config)?; 196 | } 197 | 198 | if !machine_format { 199 | if matches.bytes { 200 | println!( 201 | "Download: {:.2} Mbyte/s", 202 | ((inner_download_measurement.kbps() / 8) as f32 / 1000.00) 203 | ); 204 | } else { 205 | println!( 206 | "Download: {:.2} Mbit/s", 207 | (inner_download_measurement.kbps()) as f32 / 1000.00 208 | ); 209 | } 210 | } 211 | download_measurement = Some(&inner_download_measurement); 212 | } else { 213 | download_measurement = None; 214 | } 215 | 216 | let upload_measurement; 217 | let inner_upload_measurement; 218 | 219 | if !matches.no_upload { 220 | if !matches.simple && !machine_format { 221 | print!("Testing upload speed"); 222 | inner_upload_measurement = 223 | speedtest::test_upload_with_progress_and_config(best_server, print_dot, &config)?; 224 | println!(); 225 | } else { 226 | inner_upload_measurement = 227 | speedtest::test_upload_with_progress_and_config(best_server, || {}, &config)?; 228 | } 229 | 230 | if !machine_format { 231 | if matches.bytes { 232 | println!( 233 | "Upload: {:.2} Mbyte/s", 234 | ((inner_upload_measurement.kbps() / 8) as f32 / 1000.00) 235 | ); 236 | } else { 237 | println!( 238 | "Upload: {:.2} Mbit/s", 239 | (inner_upload_measurement.kbps() as f32 / 1000.00) 240 | ); 241 | } 242 | } 243 | upload_measurement = Some(&inner_upload_measurement); 244 | } else { 245 | upload_measurement = None; 246 | } 247 | 248 | let speedtest_result = speedtest::SpeedTestResult { 249 | download_measurement, 250 | upload_measurement, 251 | server: best_server, 252 | latency_measurement: &latency_test_result, 253 | }; 254 | 255 | if matches.csv { 256 | let speedtest_csv_result = SpeedTestCsvResult { 257 | server_id: &best_server.id.to_string(), 258 | sponsor: &best_server.sponsor, 259 | server_name: &best_server.name, 260 | timestamp: &Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true), 261 | distance: &(latency_test_result 262 | .server 263 | .distance 264 | .map_or("".to_string(), |d| format!("{d:.14}")))[..], 265 | ping: &format!( 266 | "{}.{}", 267 | latency_test_result.latency.as_millis(), 268 | latency_test_result.latency.as_micros() % 1000 269 | ), 270 | download: &download_measurement 271 | .map_or(0.0, |x| x.bps_f64()) 272 | .to_string(), 273 | upload: &upload_measurement.map_or(0.0, |x| x.bps_f64()).to_string(), 274 | share: &if matches.share { 275 | speedtest::get_share_url(&speedtest_result)? 276 | } else { 277 | "".to_string() 278 | }, 279 | ip_address: &config.client.ip.to_string(), 280 | }; 281 | let mut wtr = csv::WriterBuilder::new() 282 | .has_headers(false) 283 | .delimiter(matches.csv_delimiter as u8) 284 | .from_writer(io::stdout()); 285 | wtr.serialize(speedtest_csv_result)?; 286 | wtr.flush()?; 287 | return Ok(()); 288 | } 289 | 290 | if matches.share && !machine_format { 291 | info!("Share Request {speedtest_result:?}",); 292 | println!( 293 | "Share results: {}", 294 | speedtest::get_share_url(&speedtest_result)? 295 | ); 296 | } 297 | 298 | if let (Some(download_measurement), Some(upload_measurement)) = 299 | (download_measurement, upload_measurement) 300 | { 301 | if !machine_format 302 | && ((download_measurement.kbps() as f32 / 1000.00) > 200.0 303 | || (upload_measurement.kbps() as f32 / 1000.00) > 200.0) 304 | { 305 | println!("WARNING: This tool may not be accurate for high bandwidth connections! Consider using a socket-based client alternative.") 306 | } 307 | } 308 | Ok(()) 309 | } 310 | 311 | fn print_dot() { 312 | print!("."); 313 | io::stdout().flush().unwrap(); 314 | } 315 | -------------------------------------------------------------------------------- /src/speedtest.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{ 4 | io::Read, 5 | path::Path, 6 | sync::atomic::{AtomicBool, Ordering}, 7 | time::{Duration, SystemTime, UNIX_EPOCH}, 8 | }; 9 | 10 | #[cfg(feature = "log")] 11 | use log::info; 12 | 13 | #[cfg(not(feature = "log"))] 14 | use super::log::info; 15 | 16 | use reqwest::blocking::{Body, Client, Request, Response}; 17 | use reqwest::header::{HeaderValue, CONNECTION, CONTENT_TYPE, REFERER, USER_AGENT}; 18 | use reqwest::Url; 19 | 20 | use crate::distance::EarthLocation; 21 | use crate::error::SpeedTestError; 22 | use crate::speedtest_config::SpeedTestConfig; 23 | use crate::speedtest_servers_config::SpeedTestServersConfig; 24 | use rayon::prelude::*; 25 | 26 | const ST_USER_AGENT: &str = concat!("reqwest/speedtest-rs ", env!("CARGO_PKG_VERSION")); 27 | 28 | #[derive(Clone, Debug)] 29 | pub struct SpeedTestServer { 30 | pub country: String, 31 | pub host: String, 32 | pub id: u32, 33 | pub location: EarthLocation, 34 | pub distance: Option, 35 | pub name: String, 36 | pub sponsor: String, 37 | pub url: String, 38 | } 39 | 40 | pub fn download_configuration() -> Result { 41 | info!("Downloading Configuration from speedtest.net"); 42 | 43 | let mut _server = mockito::Server::new(); 44 | 45 | #[cfg(not(test))] 46 | let url = "http://www.speedtest.net/speedtest-config.php"; 47 | #[cfg(test)] 48 | let url = &format!("{}/speedtest-config.php", &_server.url()); 49 | 50 | let client = Client::new(); 51 | // Creating an outgoing request. 52 | let res = client 53 | .get(url) 54 | .header(CONNECTION, "close") 55 | .header(USER_AGENT, ST_USER_AGENT.to_owned()) 56 | .send()?; 57 | info!("Downloaded Configuration from speedtest.net"); 58 | Ok(res) 59 | } 60 | 61 | pub fn get_configuration() -> Result { 62 | let config_body = download_configuration()?; 63 | info!("Parsing Configuration"); 64 | let spt_config = SpeedTestConfig::parse(&(config_body.text()?))?; 65 | info!("Parsed Configuration"); 66 | Ok(spt_config) 67 | } 68 | 69 | pub fn download_server_list() -> Result { 70 | info!("Download Server List"); 71 | let mut _server = mockito::Server::new(); 72 | 73 | #[cfg(not(test))] 74 | let url = "http://www.speedtest.net/speedtest-servers.php"; 75 | #[cfg(test)] 76 | let url = &format!("{}/speedtest-servers.php", &_server.url()); 77 | 78 | let client = Client::new(); 79 | let server_res = client 80 | .get(url) 81 | .header(CONNECTION, "close") 82 | .header(USER_AGENT, ST_USER_AGENT) 83 | .send()?; 84 | info!("Downloaded Server List"); 85 | Ok(server_res) 86 | } 87 | 88 | pub fn get_server_list_with_config( 89 | config: &SpeedTestConfig, 90 | ) -> Result { 91 | let config_body = download_server_list()?; 92 | info!("Parsing Server List"); 93 | let server_config_string = config_body.text()?; 94 | 95 | info!("Parsed Server List"); 96 | SpeedTestServersConfig::parse_with_config(&server_config_string, config) 97 | } 98 | 99 | #[derive(Debug)] 100 | pub struct SpeedTestLatencyTestResult<'a> { 101 | pub server: &'a SpeedTestServer, 102 | pub latency: Duration, 103 | } 104 | 105 | pub fn get_best_server_based_on_latency( 106 | servers: &[SpeedTestServer], 107 | ) -> Result { 108 | info!("Testing for fastest server"); 109 | let client = Client::new(); 110 | let mut fastest_server = None; 111 | let mut fastest_latency = Duration::new(u64::MAX, 0); 112 | // Return error if no servers are available. 113 | if servers.is_empty() { 114 | return Err(SpeedTestError::LatencyTestNoServerError); 115 | } 116 | 'server_loop: for server in servers { 117 | let path = Path::new(&server.url); 118 | let latency_path = format!( 119 | "{}/latency.txt", 120 | path.parent() 121 | .ok_or(SpeedTestError::LatencyTestInvalidPath)? 122 | .display() 123 | ); 124 | info!("Downloading: {:?}", latency_path); 125 | let mut latency_measurements = vec![]; 126 | for _ in 0..3 { 127 | let start_time = SystemTime::now(); 128 | let res = client 129 | .get(&latency_path) 130 | .header(CONNECTION, "close") 131 | .header(USER_AGENT, ST_USER_AGENT.to_owned()) 132 | .send(); 133 | if res.is_err() { 134 | // Log the error and continue to the next server. 135 | info!("Error: {:?}", res.err()); 136 | continue 'server_loop; 137 | } 138 | let _ = res?.bytes()?.last(); 139 | let latency_measurement = SystemTime::now().duration_since(start_time)?; 140 | info!("Sampled {} ms", latency_measurement.as_millis()); 141 | latency_measurements.push(latency_measurement); 142 | } 143 | // Divide by the double to get the non-RTT time but the trip time. 144 | // NOT PING or RTT 145 | // https://github.com/sivel/speedtest-cli/pull/199 146 | let latency = latency_measurements 147 | .iter() 148 | .fold(Duration::new(0, 0), |a, &i| a + i) 149 | / ((latency_measurements.len() as u32) * 2); 150 | info!("Trip calculated to {} ms", latency.as_millis()); 151 | 152 | if latency < fastest_latency { 153 | fastest_server = Some(server); 154 | fastest_latency = latency; 155 | } 156 | } 157 | info!( 158 | "Fastest Server @ {}ms : {fastest_server:?}", 159 | fastest_latency.as_millis(), 160 | ); 161 | Ok(SpeedTestLatencyTestResult { 162 | server: fastest_server.ok_or(SpeedTestError::LatencyTestClosestError)?, 163 | latency: fastest_latency, 164 | }) 165 | } 166 | 167 | #[derive(Debug)] 168 | pub struct SpeedMeasurement { 169 | pub size: usize, 170 | pub duration: Duration, 171 | } 172 | 173 | impl SpeedMeasurement { 174 | pub fn kbps(&self) -> u32 { 175 | (self.size as u32 * 8) / self.duration.as_millis() as u32 176 | } 177 | 178 | pub fn bps_f64(&self) -> f64 { 179 | (self.size as f64 * 8.0) / (self.duration.as_millis() as f64 / (1000.0)) 180 | } 181 | } 182 | 183 | pub fn test_download_with_progress_and_config( 184 | server: &SpeedTestServer, 185 | progress_callback: F, 186 | config: &mut SpeedTestConfig, 187 | ) -> Result 188 | where 189 | F: Fn() + Send + Sync + 'static, 190 | { 191 | info!("Testing Download speed"); 192 | let root_url = Url::parse(&server.url)?; 193 | 194 | let mut urls = vec![]; 195 | for size in &config.sizes.download { 196 | let mut download_with_size_url = root_url.clone(); 197 | { 198 | let mut path_segments_mut = download_with_size_url 199 | .path_segments_mut() 200 | .map_err(|_| SpeedTestError::ServerParseError)?; 201 | path_segments_mut.push(&format!("random{size}x{size}.jpg")); 202 | } 203 | for _ in 0..config.counts.download { 204 | urls.push(download_with_size_url.clone()); 205 | } 206 | } 207 | 208 | let _request_count = urls.len(); 209 | 210 | let requests = urls 211 | .iter() 212 | .enumerate() 213 | .map(|(i, url)| { 214 | let mut cache_busting_url = url.clone(); 215 | cache_busting_url.query_pairs_mut().append_pair( 216 | "x", 217 | &format!( 218 | "{}.{i}", 219 | SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis(), 220 | ), 221 | ); 222 | let mut request = Request::new(reqwest::Method::GET, url.clone()); 223 | request.headers_mut().insert( 224 | reqwest::header::CACHE_CONTROL, 225 | HeaderValue::from_static("no-cache"), 226 | ); 227 | request.headers_mut().insert( 228 | reqwest::header::USER_AGENT, 229 | HeaderValue::from_static(ST_USER_AGENT), 230 | ); 231 | request.headers_mut().insert( 232 | reqwest::header::CONNECTION, 233 | HeaderValue::from_static("close"), 234 | ); 235 | Ok(request) 236 | }) 237 | .collect::, SpeedTestError>>()?; 238 | 239 | // TODO: Setup Ctrl-C Termination to use this "event". 240 | let early_termination = AtomicBool::new(false); 241 | 242 | // Start Timer 243 | let start_time = SystemTime::now(); 244 | 245 | info!("Download Threads: {}", config.threads.download); 246 | let pool = rayon::ThreadPoolBuilder::new() 247 | .num_threads(config.threads.download) 248 | .build()?; 249 | 250 | info!("Total to be requested {requests:?}"); 251 | 252 | let total_transferred_per_thread = pool.install(|| { 253 | requests 254 | .into_iter() 255 | // Make it sequential like the original. Ramp up the file sizes. 256 | .par_bridge() 257 | .map(|r| { 258 | let client = Client::new(); 259 | // let downloaded_count = vec![]; 260 | progress_callback(); 261 | info!("Requesting {}", r.url()); 262 | let mut response = client.execute(r)?; 263 | let mut buf = [0u8; 10240]; 264 | let mut read_amounts = vec![]; 265 | while (SystemTime::now().duration_since(start_time)? < config.length.upload) 266 | && !early_termination.load(Ordering::Relaxed) 267 | { 268 | let read_amount = response.read(&mut buf)?; 269 | read_amounts.push(read_amount); 270 | if read_amount == 0 { 271 | break; 272 | } 273 | } 274 | let total_transfered = read_amounts.iter().sum::(); 275 | progress_callback(); 276 | 277 | Ok(total_transfered) 278 | }) 279 | .collect::, SpeedTestError>>() 280 | }); 281 | 282 | let total_transferred: usize = total_transferred_per_thread?.iter().sum(); 283 | 284 | let end_time = SystemTime::now(); 285 | 286 | let measurement = SpeedMeasurement { 287 | size: total_transferred, 288 | duration: end_time.duration_since(start_time)?, 289 | }; 290 | 291 | if measurement.bps_f64() > 100000.0 { 292 | config.threads.upload = 8 293 | } 294 | 295 | Ok(measurement) 296 | } 297 | 298 | #[derive(Debug)] 299 | pub struct SpeedTestUploadRequest { 300 | pub request: Request, 301 | pub size: usize, 302 | } 303 | 304 | pub fn test_upload_with_progress_and_config( 305 | server: &SpeedTestServer, 306 | progress_callback: F, 307 | config: &SpeedTestConfig, 308 | ) -> Result 309 | where 310 | F: Fn() + Send + Sync + 'static, 311 | { 312 | info!("Testing Upload speed"); 313 | 314 | let mut sizes = vec![]; 315 | for &size in &config.sizes.upload { 316 | for _ in 0..config.counts.upload { 317 | sizes.push(size) 318 | } 319 | } 320 | 321 | let best_url = Url::parse(&server.url)?; 322 | 323 | let request_count = config.upload_max; 324 | 325 | let requests = sizes 326 | .into_iter() 327 | .map(|size| { 328 | let content_iter = b"content1=" 329 | .iter() 330 | .chain(b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".iter().cycle()) 331 | .take(size); 332 | let content_iter_read = iter_read::IterRead::new(content_iter); 333 | let body = Body::sized(content_iter_read, size as u64); 334 | let mut request = Request::new(reqwest::Method::POST, best_url.clone()); 335 | request.headers_mut().insert( 336 | reqwest::header::USER_AGENT, 337 | HeaderValue::from_static(ST_USER_AGENT), 338 | ); 339 | request.headers_mut().insert( 340 | reqwest::header::CONNECTION, 341 | HeaderValue::from_static("close"), 342 | ); 343 | *request.body_mut() = Some(body); 344 | Ok(SpeedTestUploadRequest { request, size }) 345 | }) 346 | .collect::, SpeedTestError>>()?; 347 | // TODO: Setup Ctrl-C Termination to use this "event". 348 | let early_termination = AtomicBool::new(false); 349 | 350 | // Start Timer 351 | let start_time = SystemTime::now(); 352 | 353 | info!("Upload Threads: {}", config.threads.upload); 354 | let pool = rayon::ThreadPoolBuilder::new() 355 | .num_threads(config.threads.upload) 356 | .build()?; 357 | 358 | info!("Total to be requested {:?}", requests.len()); 359 | let total_transferred_per_thread = pool.install(|| { 360 | requests 361 | .into_iter() 362 | .take(request_count) 363 | // Make it sequential like the original. Ramp up the file sizes. 364 | .par_bridge() 365 | .map(|r| { 366 | progress_callback(); 367 | 368 | if (SystemTime::now().duration_since(start_time)? < config.length.upload) 369 | && !early_termination.load(Ordering::Relaxed) 370 | { 371 | let client = Client::new(); 372 | info!("Requesting {}", r.request.url()); 373 | let response = client.execute(r.request); 374 | if response.is_err() { 375 | return Ok(r.size); 376 | }; 377 | } else { 378 | return Ok(0); 379 | } 380 | progress_callback(); 381 | 382 | Ok(r.size) 383 | }) 384 | .collect::, SpeedTestError>>() 385 | }); 386 | 387 | let total_transferred: usize = total_transferred_per_thread?.iter().sum(); 388 | 389 | let end_time = SystemTime::now(); 390 | 391 | let measurement = SpeedMeasurement { 392 | size: total_transferred, 393 | duration: end_time.duration_since(start_time)?, 394 | }; 395 | 396 | Ok(measurement) 397 | } 398 | 399 | #[derive(Debug)] 400 | pub struct SpeedTestResult<'a, 'b, 'c> { 401 | pub download_measurement: Option<&'a SpeedMeasurement>, 402 | pub upload_measurement: Option<&'b SpeedMeasurement>, 403 | pub server: &'c SpeedTestServer, 404 | pub latency_measurement: &'c SpeedTestLatencyTestResult<'c>, 405 | } 406 | 407 | impl<'a, 'b, 'c> SpeedTestResult<'a, 'b, 'c> { 408 | pub fn hash(&self) -> String { 409 | let hashed_str = format!( 410 | "{}-{}-{}-{}", 411 | self.latency_measurement.latency.as_millis(), 412 | self.upload_measurement.map_or(0, |x| x.kbps()), 413 | self.download_measurement.map_or(0, |x| x.kbps()), 414 | "297aae72" 415 | ); 416 | 417 | format!("{:x}", md5::compute(hashed_str)) 418 | } 419 | } 420 | 421 | pub fn get_share_url(speedtest_result: &SpeedTestResult) -> Result { 422 | info!("Generating share URL"); 423 | 424 | let download = speedtest_result 425 | .download_measurement 426 | .map_or(0, |x| x.kbps()); 427 | info!("Download parameter is {download:?}"); 428 | let upload = speedtest_result.upload_measurement.map_or(0, |x| x.kbps()); 429 | info!("Upload parameter is {upload:?}"); 430 | let server = speedtest_result.server.id; 431 | info!("Server parameter is {server:?}"); 432 | let ping = speedtest_result.latency_measurement.latency; 433 | info!("Ping parameter is {ping:?}"); 434 | 435 | let pairs = [ 436 | ("download", download.to_string()), 437 | ("ping", ping.as_millis().to_string()), 438 | ("upload", upload.to_string()), 439 | ("promo", String::new()), 440 | ("startmode", "pingselect".to_string()), 441 | ("recommendedserverid", format!("{server}")), 442 | ("accuracy", "1".to_string()), 443 | ("serverid", format!("{server}")), 444 | ("hash", speedtest_result.hash()), 445 | ]; 446 | 447 | let body = url::form_urlencoded::Serializer::new(String::new()) 448 | .extend_pairs(pairs.iter()) 449 | .finish(); 450 | 451 | info!("Share Body Request: {body:?}"); 452 | 453 | let client = Client::new(); 454 | let res = client 455 | .post("http://www.speedtest.net/api/api.php") 456 | .header(CONNECTION, "close") 457 | .header(REFERER, "http://c.speedtest.net/flash/speedtest.swf") 458 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 459 | .body(body) 460 | .send(); 461 | let encode_return = res?.text()?; 462 | let response_id = parse_share_request_response_id(encode_return.as_bytes())?; 463 | Ok(format!("http://www.speedtest.net/result/{response_id}.png")) 464 | } 465 | 466 | pub fn parse_share_request_response_id(input: &[u8]) -> Result { 467 | url::form_urlencoded::parse(input) 468 | .find(|pair| pair.0 == "resultid") 469 | .map_or_else( 470 | || Err(SpeedTestError::ParseShareUrlError), 471 | |pair| Ok(pair.1.into_owned()), 472 | ) 473 | } 474 | 475 | #[cfg(test)] 476 | mod tests { 477 | use super::*; 478 | 479 | #[test] 480 | fn test_parse_share_request_response_id() { 481 | let resp = "resultid=4932415710&date=12%2F21%2F2015&time=5%3A10+AM&rating=0".as_bytes(); 482 | assert_eq!(parse_share_request_response_id(resp).unwrap(), "4932415710"); 483 | } 484 | 485 | #[test] 486 | fn test_share_url_hash() { 487 | let download_measurement = SpeedMeasurement { 488 | size: (6096 * 100) as usize, 489 | duration: Duration::new(1, 0), 490 | }; 491 | println!("Download: {:?}", download_measurement); 492 | let upload_measurement = SpeedMeasurement { 493 | size: (1861 * 100) as usize, 494 | duration: Duration::new(1, 0), 495 | }; 496 | println!("Upload: {:?}", upload_measurement); 497 | let server = SpeedTestServer { 498 | country: "".to_owned(), 499 | host: "".to_owned(), 500 | id: 5116, 501 | location: EarthLocation { 502 | latitude: 0.0, 503 | longitude: 0.0, 504 | }, 505 | distance: None, 506 | name: "".to_owned(), 507 | sponsor: "".to_owned(), 508 | url: "".to_owned(), 509 | }; 510 | println!("Server: {server:?}"); 511 | let latency_measurement = SpeedTestLatencyTestResult { 512 | server: &server, 513 | latency: Duration::from_millis(26), 514 | }; 515 | println!("Latency: {latency_measurement:?}"); 516 | let request = SpeedTestResult { 517 | download_measurement: Some(&download_measurement), 518 | upload_measurement: Some(&upload_measurement), 519 | server: &server, 520 | latency_measurement: &latency_measurement, 521 | }; 522 | assert_eq!(request.hash(), "f10eb3dd8d3c38a221e823d859680045"); 523 | } 524 | 525 | #[test] 526 | fn test_construct_share_form() {} 527 | 528 | #[test] 529 | fn test_get_configuration() { 530 | let mut server = mockito::Server::new(); 531 | 532 | let _m = server 533 | .mock("GET", "/speedtest-config.php") 534 | .with_status(200) 535 | .with_body_from_file("tests/config/stripped-config.php.xml") 536 | .create(); 537 | let _config = get_configuration(); 538 | } 539 | 540 | #[test] 541 | fn test_get_server_list_with_config() { 542 | let mut server = mockito::Server::new(); 543 | 544 | let _m = server 545 | .mock("GET", "/speedtest-config.php") 546 | .with_status(200) 547 | .with_body_from_file("tests/config/servers-static.php.xml") 548 | .create(); 549 | 550 | let _server_list_config = get_server_list_with_config(&SpeedTestConfig::default()); 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /src/speedtest_config.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use crate::{distance::EarthLocation, error::SpeedTestError}; 4 | use std::{net::Ipv4Addr, time::Duration}; 5 | 6 | pub struct SpeedTestClientConfig { 7 | pub ip: Ipv4Addr, 8 | pub isp: String, 9 | } 10 | 11 | impl Default for SpeedTestClientConfig { 12 | fn default() -> Self { 13 | SpeedTestClientConfig { 14 | ip: Ipv4Addr::new(127, 0, 0, 1), 15 | isp: String::default(), 16 | } 17 | } 18 | } 19 | 20 | #[derive(Default)] 21 | pub struct SpeedTestSizeConfig { 22 | pub upload: Vec, 23 | pub download: Vec, 24 | } 25 | 26 | #[derive(Default)] 27 | pub struct SpeedTestCountsConfig { 28 | pub upload: usize, 29 | pub download: usize, 30 | } 31 | 32 | #[derive(Default)] 33 | pub struct SpeedTestThreadsConfig { 34 | pub upload: usize, 35 | pub download: usize, 36 | } 37 | 38 | pub struct SpeedTestLengthConfig { 39 | pub upload: Duration, 40 | pub download: Duration, 41 | } 42 | 43 | impl Default for SpeedTestLengthConfig { 44 | fn default() -> Self { 45 | SpeedTestLengthConfig { 46 | upload: Duration::from_secs(10), 47 | download: Duration::from_secs(10), 48 | } 49 | } 50 | } 51 | 52 | #[derive(Default)] 53 | pub struct SpeedTestConfig { 54 | pub client: SpeedTestClientConfig, 55 | pub ignore_servers: Vec, 56 | pub sizes: SpeedTestSizeConfig, 57 | pub counts: SpeedTestCountsConfig, 58 | pub threads: SpeedTestThreadsConfig, 59 | pub length: SpeedTestLengthConfig, 60 | pub upload_max: usize, 61 | pub location: EarthLocation, 62 | } 63 | 64 | impl SpeedTestConfig { 65 | pub fn parse(config_xml: &str) -> Result { 66 | let document = roxmltree::Document::parse(config_xml)?; 67 | 68 | let server_config_node = document 69 | .descendants() 70 | .find(|n| n.has_tag_name("server-config")) 71 | .ok_or(SpeedTestError::ConfigParseError)?; 72 | let download_node = document 73 | .descendants() 74 | .find(|n| n.has_tag_name("download")) 75 | .ok_or(SpeedTestError::ConfigParseError)?; 76 | let upload_node = document 77 | .descendants() 78 | .find(|n| n.has_tag_name("upload")) 79 | .ok_or(SpeedTestError::ConfigParseError)?; 80 | let client_node = document 81 | .descendants() 82 | .find(|n| n.has_tag_name("client")) 83 | .ok_or(SpeedTestError::ConfigParseError)?; 84 | 85 | let ignore_servers: Vec = server_config_node 86 | .attribute("ignoreids") 87 | .ok_or(SpeedTestError::ConfigParseError)? 88 | .split(',') 89 | .filter(|s| !s.is_empty()) 90 | .map(|s| s.parse::()) 91 | .collect::, _>>()?; 92 | 93 | let ratio = upload_node 94 | .attribute("ratio") 95 | .ok_or(SpeedTestError::ConfigParseError)? 96 | .parse::()?; 97 | 98 | let upload_max = upload_node 99 | .attribute("maxchunkcount") 100 | .ok_or(SpeedTestError::ConfigParseError)? 101 | .parse::()?; 102 | 103 | let up_sizes = [32768usize, 65536, 131072, 262144, 524288, 1048576, 7340032]; 104 | 105 | let sizes = SpeedTestSizeConfig { 106 | upload: up_sizes 107 | .get(ratio - 1..) 108 | .ok_or(SpeedTestError::ConfigParseError)? 109 | .to_vec(), 110 | download: vec![350usize, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000], 111 | }; 112 | 113 | let size_count = sizes.upload.len(); 114 | 115 | let upload_count = (upload_max as f32 / size_count as f32).ceil() as usize; 116 | 117 | let counts = SpeedTestCountsConfig { 118 | upload: upload_count, 119 | download: download_node 120 | .attribute("threadsperurl") 121 | .ok_or(SpeedTestError::ConfigParseError)? 122 | .parse::()?, 123 | }; 124 | 125 | let threads = SpeedTestThreadsConfig { 126 | upload: upload_node 127 | .attribute("threads") 128 | .ok_or(SpeedTestError::ConfigParseError)? 129 | .parse::()?, 130 | download: server_config_node 131 | .attribute("threadcount") 132 | .ok_or(SpeedTestError::ConfigParseError)? 133 | .parse::()? 134 | * 2, 135 | }; 136 | 137 | let length = SpeedTestLengthConfig { 138 | upload: upload_node 139 | .attribute("testlength") 140 | .ok_or(SpeedTestError::ConfigParseError)? 141 | .parse::() 142 | .map(Duration::from_secs)?, 143 | download: download_node 144 | .attribute("testlength") 145 | .ok_or(SpeedTestError::ConfigParseError)? 146 | .parse::() 147 | .map(Duration::from_secs)?, 148 | }; 149 | 150 | let client = SpeedTestClientConfig { 151 | ip: client_node 152 | .attribute("ip") 153 | .ok_or(SpeedTestError::ConfigParseError)? 154 | .parse()?, 155 | isp: client_node 156 | .attribute("isp") 157 | .ok_or(SpeedTestError::ConfigParseError)? 158 | .to_string(), 159 | }; 160 | 161 | Ok(SpeedTestConfig { 162 | client, 163 | ignore_servers, 164 | sizes, 165 | counts, 166 | threads, 167 | length, 168 | upload_max, 169 | location: EarthLocation { 170 | latitude: client_node 171 | .attribute("lat") 172 | .ok_or(SpeedTestError::ConfigParseError)? 173 | .parse()?, 174 | longitude: client_node 175 | .attribute("lon") 176 | .ok_or(SpeedTestError::ConfigParseError)? 177 | .parse()?, 178 | }, 179 | }) 180 | } 181 | } 182 | 183 | #[cfg(test)] 184 | mod tests { 185 | use super::*; 186 | 187 | #[test] 188 | fn test_parse_config_xml() { 189 | let config = 190 | SpeedTestConfig::parse(include_str!("../tests/config/config.php.xml")).unwrap(); 191 | assert_eq!("174.79.12.26", config.client.ip.to_string()); 192 | assert_eq!( 193 | EarthLocation { 194 | latitude: 32.9954, 195 | longitude: -117.0753, 196 | }, 197 | config.location 198 | ); 199 | assert_eq!("Cox Communications", config.client.isp); 200 | } 201 | 202 | #[test] 203 | fn test_parse_config_xml_83() { 204 | let config = 205 | SpeedTestConfig::parse(include_str!("../tests/config/2021-07-speedtest-config.xml")) 206 | .unwrap(); 207 | assert_eq!("Cox Communications", config.client.isp); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/speedtest_csv.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Debug, Serialize, Default)] 4 | pub struct SpeedTestCsvResult<'a> { 5 | #[serde(rename = "Server ID")] 6 | pub server_id: &'a str, 7 | #[serde(rename = "Sponsor")] 8 | pub sponsor: &'a str, 9 | #[serde(rename = "Server Name")] 10 | pub server_name: &'a str, 11 | #[serde(rename = "Timestamp")] 12 | pub timestamp: &'a str, 13 | #[serde(rename = "Distance")] 14 | pub distance: &'a str, 15 | #[serde(rename = "Ping")] 16 | pub ping: &'a str, 17 | #[serde(rename = "Download")] 18 | pub download: &'a str, 19 | #[serde(rename = "Upload")] 20 | pub upload: &'a str, 21 | #[serde(rename = "Share")] 22 | pub share: &'a str, 23 | #[serde(rename = "IP Address")] 24 | pub ip_address: &'a str, 25 | } 26 | 27 | impl<'a> SpeedTestCsvResult<'a> { 28 | pub fn header_serialize(self) -> String { 29 | // Un-dynamic for now 30 | // Blocked on: 31 | // * https://github.com/BurntSushi/rust-csv/issues/161 being implemented or solved 32 | // * https://github.com/BurntSushi/rust-csv/pull/193/files, like in this? 33 | "Server ID,Sponsor,Server Name,Timestamp,Distance,Ping,Download,Upload,Share,IP Address" 34 | .to_string() 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | use std::error::Error; 42 | 43 | #[test] 44 | fn test_header_serialize() -> Result<(), Box> { 45 | let original = "Server ID,Sponsor,Server Name,Timestamp,Distance,Ping,Download,Upload,Share,IP Address"; 46 | 47 | let results = SpeedTestCsvResult::default(); 48 | 49 | assert_eq!(results.header_serialize(), original); 50 | Ok(()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/speedtest_servers_config.rs: -------------------------------------------------------------------------------- 1 | use crate::distance::{self, EarthLocation}; 2 | use crate::{error::SpeedTestError, speedtest::SpeedTestServer, speedtest_config::SpeedTestConfig}; 3 | use std::cmp::Ordering::Less; 4 | 5 | pub struct SpeedTestServersConfig { 6 | pub servers: Vec, 7 | } 8 | 9 | impl SpeedTestServersConfig { 10 | pub fn parse_with_config( 11 | server_config_xml: &str, 12 | config: &SpeedTestConfig, 13 | ) -> Result { 14 | let document = roxmltree::Document::parse(server_config_xml)?; 15 | let servers = document 16 | .descendants() 17 | .filter(|node| node.tag_name().name() == "server") 18 | .map::, _>(|n| { 19 | let location = EarthLocation { 20 | latitude: n 21 | .attribute("lat") 22 | .ok_or(SpeedTestError::ServerParseError)? 23 | .parse()?, 24 | longitude: n 25 | .attribute("lon") 26 | .ok_or(SpeedTestError::ServerParseError)? 27 | .parse()?, 28 | }; 29 | Ok(SpeedTestServer { 30 | country: n 31 | .attribute("country") 32 | .ok_or(SpeedTestError::ServerParseError)? 33 | .to_string(), 34 | host: n 35 | .attribute("host") 36 | .ok_or(SpeedTestError::ServerParseError)? 37 | .to_string(), 38 | id: n 39 | .attribute("id") 40 | .ok_or(SpeedTestError::ServerParseError)? 41 | .parse()?, 42 | location: location.clone(), 43 | distance: Some(distance::compute_distance(&config.location, &location)), 44 | name: n 45 | .attribute("name") 46 | .ok_or(SpeedTestError::ServerParseError)? 47 | .to_string(), 48 | sponsor: n 49 | .attribute("sponsor") 50 | .ok_or(SpeedTestError::ServerParseError)? 51 | .to_string(), 52 | url: n 53 | .attribute("url") 54 | .ok_or(SpeedTestError::ServerParseError)? 55 | .to_string(), 56 | }) 57 | }) 58 | .filter_map(Result::ok) 59 | .filter(|server| !config.ignore_servers.contains(&server.id)) 60 | .collect(); 61 | Ok(SpeedTestServersConfig { servers }) 62 | } 63 | 64 | pub fn servers_sorted_by_distance(&self, config: &SpeedTestConfig) -> Vec { 65 | let location = &config.location; 66 | let mut sorted_servers = self.servers.clone(); 67 | sorted_servers.sort_by(|a, b| { 68 | let a_distance = distance::compute_distance(location, &a.location); 69 | let b_distance = distance::compute_distance(location, &b.location); 70 | a_distance.partial_cmp(&b_distance).unwrap_or(Less) 71 | }); 72 | sorted_servers 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::*; 79 | use crate::speedtest_config::*; 80 | 81 | fn sample_spt_config() -> SpeedTestConfig { 82 | // Somewhere in Los Angeles 83 | SpeedTestConfig { 84 | location: EarthLocation { 85 | latitude: 32.9954, 86 | longitude: -117.0753, 87 | }, 88 | ..SpeedTestConfig::default() 89 | } 90 | } 91 | 92 | #[test] 93 | fn test_parse_speedtest_servers_xml() { 94 | let spt_config = sample_spt_config(); 95 | let config_str = include_str!("../tests/config/geo-test-servers-static.php.xml"); 96 | 97 | let server_config = 98 | SpeedTestServersConfig::parse_with_config(config_str, &spt_config).unwrap(); 99 | assert!(server_config.servers.len() > 5); 100 | let server = server_config.servers.get(1).unwrap(); 101 | assert!(!server.url.is_empty()); 102 | assert!(!server.country.is_empty()); 103 | } 104 | 105 | #[test] 106 | fn test_parse_speedtest_servers_xml_with_ignore_servers() { 107 | let spt_config = SpeedTestConfig { 108 | ignore_servers: vec![5905], 109 | ..sample_spt_config() 110 | }; 111 | let config_str = include_str!("../tests/config/geo-test-servers-static.php.xml"); 112 | 113 | let server_config = 114 | SpeedTestServersConfig::parse_with_config(config_str, &spt_config).unwrap(); 115 | assert!(server_config.servers.iter().all(|s| s.id != 5905)); 116 | assert!(server_config.servers.len() > 5); 117 | let server = server_config.servers.get(1).unwrap(); 118 | assert!(!server.url.is_empty()); 119 | assert!(!server.country.is_empty()); 120 | } 121 | 122 | #[test] 123 | fn test_fastest_server() { 124 | let spt_config = sample_spt_config(); 125 | let config_str = include_str!("../tests/config/geo-test-servers-static.php.xml"); 126 | 127 | let config = SpeedTestServersConfig::parse_with_config(config_str, &spt_config).unwrap(); 128 | let closest_server = &config.servers_sorted_by_distance(&spt_config)[0]; 129 | assert_eq!("Los Angeles, CA", closest_server.name); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/config/2020-07-speedtest-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | f7a45ced624d3a70-1df5b7cd427370f7-b91ee21d6cb22d7b 6 | speedtest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Rate Your ISP 22 | COPY IP 23 | kilobits 24 | megabits 25 | NEW SERVER 26 | TEST AGAIN 27 | UPLOAD SPEED 28 | DOWNLOAD SPEED 29 | kbps 30 | Mbps 31 | BEGIN TEST 32 | START TEST TO RECOMMENDED SERVER 33 | megabytes 34 | kilobytes 35 | kB/s 36 | MB/s 37 | Mbps 38 | How happy are you with your current Internet service provider? 39 | Very unhappy 40 | Unhappy 41 | Neutral 42 | Happy 43 | Very happy 44 | YOUR RESULT WILL BECOME PART OF A SPEED WAVE 45 | PING 46 | Hosted by 47 | TOTAL TESTS 48 | TO DATE 49 | COPIED 50 | AUTO STARTING SPEED TEST IN 51 | SECONDS 52 | SECOND 53 | ERROR 54 | Try Again 55 | START A SPEED WAVE 56 | Speed Wave Name 57 | Your result is now part of the Speed Wave! 58 | Your Result 59 | Help Us Understand Broadband Costs 60 | Download Package 61 | Upload Package 62 | How much do you pay? 63 | Includes: 64 | Is this your postal code? 65 | SUBMIT 66 | GET A FREE OOKLA SPEEDTEST ACCOUNT 67 | Being logged in would allow you to start a Speed Wave here! 68 | Registration is free and only requires a valid email address. 69 | Your Email Address 70 | https://twitter.com/share?text=Check%20out%20my%20%40Ookla%20Speedtest%20result!%20What%27s%20your%20speed%3F&url=http%3A%2F%2Fwww.speedtest.net%2Fmy-result%2F{RESULTID}&related=ookla%3ACreators%20of%20Ookla%20Speedtest&hashtags=speedtest 71 | https://www.facebook.com/dialog/feed?app_id=581657151866321&link=http://www.speedtest.net/my-result/{RESULTID}&description=This%20is%20my%20Ookla%20Speedtest%20result.%20Compare%20your%20speed%20to%20mine!&redirect_uri=http://www.speedtest.net&name=Check%20out%20my%20Ookla%20Speedtest%20results.%20What%27s%20your%20speed%3F 72 | VIEW SPEED WAVE 73 | CREATE 74 | Speed 75 | Phone 76 | TV 77 | What speeds do you pay for? 78 | Thanks for participating in the survey! 79 | SELECTING BEST SERVER BASED ON PING 80 | MY RESULTS 81 | CREATE 82 | YOUR PREFERRED SERVER 83 | RECOMMENDED SERVER 84 | CONNECTING 85 | COPY 86 | SHARE THIS RESULT 87 | COMPARE 88 | YOUR RESULT 89 | CONTRIBUTE 90 | TO NET INDEX 91 | CLOSE 92 | RETAKE THE 93 | SURVEY 94 | IMAGE 95 | FORUM 96 | Use this test result to begin your own Speed Wave! 97 | Fastest ISPs 98 | wave 99 | share 100 | link:{LANG_CODE}/results.php?source=compare 101 | contribute 102 | bits per second 103 | standard 104 | en 105 | http://pinterest.com/pin/create/button/?url=http%3A%2F%2Fwww.speedtest.net%2F&media=http%3A%2F%2Fspeedtest.net%2Fresult%2F{RESULTID}.png&description=Check%20out%20my%20result%20from%20Ookla%20Speedtest! 106 | 107 | Continue 108 | EDIT 109 | Download 110 | Download: 111 | Upload 112 | Upload: 113 | Connection Type? 114 | Home 115 | Business 116 | School 117 | Public Wi-Fi 118 | Other 119 | My ISP is: 120 | Yes 121 | Wrong 122 | Yes 123 | Wrong 124 | Please enter your postal code 125 | Please enter your ISP name 126 | OK 127 | Please check your upload speed. 128 | This seems faster than expected. 129 | Please check the amount entered. 130 | This seems higher than expected. 131 | Please check your Download speed. 132 | This seems faster than expected. 133 | WEB 134 | EMBED 135 | Are you on 136 | Take our Broadband Internet Survey! 137 | TEST AGAIN 138 | COMPARE 139 | 140 | 141 | -------------------------------------------------------------------------------- /tests/config/2021-07-speedtest-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | f7a45ced624d3a70-1df5b7cd427370f7-b91ee21d6cb22d7b 6 | speedtest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/config/config.php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9c1687ea58e5e770-1df5b7cd427370f7-4b62a84526ea1f56 9 | speedtest 10 | 11 | 13 | 14 | 16 | 17 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Rate Your ISP 31 | COPY IP 32 | kilobits 33 | megabits 34 | NEW SERVER 35 | TEST AGAIN 36 | UPLOAD SPEED 37 | DOWNLOAD SPEED 38 | kbps 39 | Mbps 40 | BEGIN TEST 41 | START TEST TO RECOMMENDED SERVER 42 | megabytes 43 | kilobytes 44 | kB/s 45 | MB/s 46 | Mbps 47 | How happy are you with your current Internet service 48 | provider? 49 | Very unhappy 50 | Unhappy 51 | Neutral 52 | Happy 53 | Very happy 54 | YOUR RESULT WILL BECOME PART OF A SPEED WAVE 55 | PING 56 | Hosted by 57 | TOTAL TESTS TO DATE 58 | COPIED 59 | AUTO STARTING SPEED TEST IN 60 | SECONDS 61 | SECOND 62 | ERROR 63 | Try Again 64 | START A SPEED WAVE 65 | Speed Wave Name 66 | Your result is now part of the Speed Wave! 67 | Your Result 68 | Help Us Understand Broadband Costs 69 | Download Package 70 | Upload Package 71 | How much do you pay? 72 | Includes: 73 | Is this your postal code? 74 | SUBMIT 75 | GET A FREE OOKLA SPEEDTEST ACCOUNT 76 | Being logged in would allow you to start a 77 | Speed Wave here! Registration is free and only requires a valid email 78 | address. 79 | Your Email Address 80 | https://twitter.com/share?text=Check%20out%20my%20%40Ookla%20Speedtest%20result!%20What%27s%20your%20speed%3F&url=http%3A%2F%2Fwww.speedtest.net%2Fmy-result%2F{RESULTID}&related=ookla%3ACreators%20of%20Ookla%20Speedtest&hashtags=speedtest 81 | https://www.facebook.com/dialog/feed?app_id=581657151866321&link=http://www.speedtest.net/my-result/{RESULTID}&description=This%20is%20my%20Ookla%20Speedtest%20result.%20Compare%20your%20speed%20to%20mine!&redirect_uri=http://www.speedtest.net&name=Check%20out%20my%20Ookla%20Speedtest%20results.%20What%27s%20your%20speed%3F 82 | VIEW SPEED WAVE 83 | CREATE 84 | Speed 85 | Phone 86 | TV 87 | What speeds do you pay for? 88 | Thanks for participating in the survey! 89 | SELECTING BEST SERVER BASED ON PING 90 | MY RESULTS 91 | CREATE 92 | YOUR PREFERRED SERVER 93 | RECOMMENDED SERVER 94 | CONNECTING 95 | COPY 96 | SHARE THIS RESULT 97 | COMPARE YOUR RESULT 98 | CONTRIBUTE TO NET INDEX 99 | CLOSE 100 | RETAKE THE SURVEY 101 | IMAGE 102 | FORUM 103 | Use this test result to begin your own Speed 104 | Wave! 105 | Fastest ISPs 106 | wave 107 | share 108 | link:{LANG_CODE}/results.php?source=compare 109 | contribute 110 | bits per second 111 | standard 112 | en 113 | http://pinterest.com/pin/create/button/?url=http%3A%2F%2Fwww.speedtest.net%2F&media=http%3A%2F%2Fspeedtest.net%2Fresult%2F{RESULTID}.png&description=Check%20out%20my%20result%20from%20Ookla%20Speedtest! 114 | 115 | Continue 116 | EDIT 117 | Download 118 | Download: 119 | Upload 120 | Upload: 121 | Connection Type? 122 | Home 123 | Business 124 | School 125 | Public Wi-Fi 126 | Other 127 | My ISP is: 128 | Yes 129 | Wrong 130 | Yes 131 | Wrong 132 | Please enter your postal code 133 | Please enter your ISP name 134 | OK 135 | Please check your upload speed. This 136 | seems faster than expected. 137 | Please check the amount entered. This 138 | seems higher than expected. 139 | Please check your Download speed. This 140 | seems faster than expected. 141 | WEB 142 | EMBED 143 | Are you on 144 | Take our Broadband Internet 145 | Survey! 146 | 147 | 148 | 149 | 150 | 152 | 153 | -------------------------------------------------------------------------------- /tests/config/geo-test-servers-static.php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/config/stripped-config.php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/config/stripped-servers-static.php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------