├── .github ├── dependabot.yml └── workflows │ ├── CI.yml │ ├── pre-commit.yml │ ├── release.yaml │ ├── rust-clippy.yml │ └── security-checks.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.txt ├── README.md ├── examples ├── download_test.rs ├── latency_test.rs └── simple_speedtest.rs └── src ├── boxplot.rs ├── lib.rs ├── main.rs ├── measurements.rs ├── progress.rs └── speedtest.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | assignees: 8 | - "code-inflation" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Ensure 'cargo fmt' 19 | run: cargo fmt -- --check 20 | - name: Build 21 | run: cargo build --verbose 22 | - name: Run tests 23 | run: cargo test --verbose 24 | - name: Run example - simple_speedtest 25 | run: cargo run --example simple_speedtest 26 | - name: Run example - download_test 27 | run: cargo run --example download_test 28 | - name: Run example - latency_test 29 | run: cargo run --example latency_test 30 | - name: Run CLI 31 | run: cargo run 32 | 33 | docker-build: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v3 39 | - name: Build and push Docker image 40 | uses: docker/build-push-action@v5 41 | with: 42 | platforms: linux/amd64,linux/arm64 43 | push: false 44 | tags: | 45 | cybuerg/cfspeedtest:${{ github.sha }} 46 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | pre-commit: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-python@v5 17 | - uses: pre-commit/action@v3.0.1 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | upload-assets: 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: 25 | - windows-latest 26 | include: 27 | - target: aarch64-unknown-linux-gnu 28 | os: ubuntu-latest 29 | - target: aarch64-apple-darwin 30 | os: macos-latest 31 | - target: x86_64-unknown-linux-gnu 32 | os: ubuntu-latest 33 | - target: x86_64-apple-darwin 34 | os: macos-latest 35 | - target: universal-apple-darwin 36 | os: macos-latest 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: taiki-e/upload-rust-binary-action@v1 41 | with: 42 | bin: cfspeedtest 43 | target: ${{ matrix.target }} 44 | tar: unix 45 | zip: windows 46 | token: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | docker-build-and-push: 49 | needs: [create-release, upload-assets] 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Set up Docker Buildx 54 | uses: docker/setup-buildx-action@v3 55 | - name: Log in to DockerHub 56 | uses: docker/login-action@v3 57 | with: 58 | username: ${{ secrets.DOCKERHUB_USERNAME }} 59 | password: ${{ secrets.DOCKERHUB_TOKEN }} 60 | - name: Build and push Docker image 61 | uses: docker/build-push-action@v5 62 | with: 63 | platforms: linux/amd64,linux/arm64 64 | push: true 65 | tags: | 66 | cybuerg/cfspeedtest:${{ github.ref_name }} 67 | cybuerg/cfspeedtest:latest 68 | -------------------------------------------------------------------------------- /.github/workflows/rust-clippy.yml: -------------------------------------------------------------------------------- 1 | # source: https://doc.rust-lang.org/nightly/clippy/continuous_integration/github_actions.html 2 | name: Clippy check 3 | 4 | on: 5 | push: 6 | branches: [ "master" ] 7 | pull_request: 8 | branches: [ "master" ] 9 | schedule: 10 | - cron: '0 3 * * 1' 11 | 12 | # Make sure CI fails on all warnings, including Clippy lints 13 | env: 14 | RUSTFLAGS: "-Dwarnings" 15 | 16 | jobs: 17 | clippy_check: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Run Clippy 22 | run: cargo clippy --all-targets --all-features -------------------------------------------------------------------------------- /.github/workflows/security-checks.yml: -------------------------------------------------------------------------------- 1 | name: Rust Security Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: '0 3 * * 1' 12 | 13 | jobs: 14 | cargo_audit: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install cargo audit 19 | run: cargo install cargo-audit 20 | - name: Run Cargo Audit 21 | run: cargo audit 22 | continue-on-error: true 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .aider* 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-yaml 6 | stages: [pre-commit] 7 | - id: check-json 8 | stages: [pre-commit] 9 | - id: check-toml 10 | stages: [pre-commit] 11 | - id: check-merge-conflict 12 | stages: [pre-commit] 13 | - id: check-case-conflict 14 | stages: [pre-commit] 15 | - id: detect-private-key 16 | stages: [pre-commit] 17 | - repo: https://github.com/crate-ci/typos 18 | rev: v1.16.20 19 | hooks: 20 | - id: typos 21 | stages: [pre-commit] 22 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 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 = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys 0.59.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell", 76 | "windows-sys 0.59.0", 77 | ] 78 | 79 | [[package]] 80 | name = "autocfg" 81 | version = "1.4.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 84 | 85 | [[package]] 86 | name = "backtrace" 87 | version = "0.3.74" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 90 | dependencies = [ 91 | "addr2line", 92 | "cfg-if", 93 | "libc", 94 | "miniz_oxide", 95 | "object", 96 | "rustc-demangle", 97 | "windows-targets 0.52.6", 98 | ] 99 | 100 | [[package]] 101 | name = "base64" 102 | version = "0.22.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 105 | 106 | [[package]] 107 | name = "bitflags" 108 | version = "2.9.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 111 | 112 | [[package]] 113 | name = "bumpalo" 114 | version = "3.17.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 117 | 118 | [[package]] 119 | name = "bytes" 120 | version = "1.10.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 123 | 124 | [[package]] 125 | name = "cc" 126 | version = "1.2.18" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" 129 | dependencies = [ 130 | "shlex", 131 | ] 132 | 133 | [[package]] 134 | name = "cfg-if" 135 | version = "1.0.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 138 | 139 | [[package]] 140 | name = "cfg_aliases" 141 | version = "0.2.1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 144 | 145 | [[package]] 146 | name = "cfspeedtest" 147 | version = "1.3.3" 148 | dependencies = [ 149 | "clap", 150 | "clap_complete", 151 | "csv", 152 | "env_logger", 153 | "indexmap", 154 | "log", 155 | "regex", 156 | "reqwest", 157 | "serde", 158 | "serde_json", 159 | ] 160 | 161 | [[package]] 162 | name = "clap" 163 | version = "4.5.37" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 166 | dependencies = [ 167 | "clap_builder", 168 | "clap_derive", 169 | ] 170 | 171 | [[package]] 172 | name = "clap_builder" 173 | version = "4.5.37" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 176 | dependencies = [ 177 | "anstream", 178 | "anstyle", 179 | "clap_lex", 180 | "strsim", 181 | ] 182 | 183 | [[package]] 184 | name = "clap_complete" 185 | version = "4.5.47" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6" 188 | dependencies = [ 189 | "clap", 190 | ] 191 | 192 | [[package]] 193 | name = "clap_derive" 194 | version = "4.5.32" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 197 | dependencies = [ 198 | "heck", 199 | "proc-macro2", 200 | "quote", 201 | "syn", 202 | ] 203 | 204 | [[package]] 205 | name = "clap_lex" 206 | version = "0.7.4" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 209 | 210 | [[package]] 211 | name = "colorchoice" 212 | version = "1.0.3" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 215 | 216 | [[package]] 217 | name = "csv" 218 | version = "1.3.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" 221 | dependencies = [ 222 | "csv-core", 223 | "itoa", 224 | "ryu", 225 | "serde", 226 | ] 227 | 228 | [[package]] 229 | name = "csv-core" 230 | version = "0.1.12" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" 233 | dependencies = [ 234 | "memchr", 235 | ] 236 | 237 | [[package]] 238 | name = "displaydoc" 239 | version = "0.2.5" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 242 | dependencies = [ 243 | "proc-macro2", 244 | "quote", 245 | "syn", 246 | ] 247 | 248 | [[package]] 249 | name = "env_filter" 250 | version = "0.1.3" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 253 | dependencies = [ 254 | "log", 255 | "regex", 256 | ] 257 | 258 | [[package]] 259 | name = "env_logger" 260 | version = "0.11.8" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 263 | dependencies = [ 264 | "anstream", 265 | "anstyle", 266 | "env_filter", 267 | "jiff", 268 | "log", 269 | ] 270 | 271 | [[package]] 272 | name = "equivalent" 273 | version = "1.0.2" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 276 | 277 | [[package]] 278 | name = "fnv" 279 | version = "1.0.7" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 282 | 283 | [[package]] 284 | name = "form_urlencoded" 285 | version = "1.2.1" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 288 | dependencies = [ 289 | "percent-encoding", 290 | ] 291 | 292 | [[package]] 293 | name = "futures-channel" 294 | version = "0.3.31" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 297 | dependencies = [ 298 | "futures-core", 299 | "futures-sink", 300 | ] 301 | 302 | [[package]] 303 | name = "futures-core" 304 | version = "0.3.31" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 307 | 308 | [[package]] 309 | name = "futures-io" 310 | version = "0.3.31" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 313 | 314 | [[package]] 315 | name = "futures-sink" 316 | version = "0.3.31" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 319 | 320 | [[package]] 321 | name = "futures-task" 322 | version = "0.3.31" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 325 | 326 | [[package]] 327 | name = "futures-util" 328 | version = "0.3.31" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 331 | dependencies = [ 332 | "futures-core", 333 | "futures-io", 334 | "futures-sink", 335 | "futures-task", 336 | "memchr", 337 | "pin-project-lite", 338 | "pin-utils", 339 | "slab", 340 | ] 341 | 342 | [[package]] 343 | name = "getrandom" 344 | version = "0.2.15" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 347 | dependencies = [ 348 | "cfg-if", 349 | "js-sys", 350 | "libc", 351 | "wasi 0.11.0+wasi-snapshot-preview1", 352 | "wasm-bindgen", 353 | ] 354 | 355 | [[package]] 356 | name = "getrandom" 357 | version = "0.3.2" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 360 | dependencies = [ 361 | "cfg-if", 362 | "js-sys", 363 | "libc", 364 | "r-efi", 365 | "wasi 0.14.2+wasi-0.2.4", 366 | "wasm-bindgen", 367 | ] 368 | 369 | [[package]] 370 | name = "gimli" 371 | version = "0.31.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 374 | 375 | [[package]] 376 | name = "hashbrown" 377 | version = "0.15.2" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 380 | 381 | [[package]] 382 | name = "heck" 383 | version = "0.5.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 386 | 387 | [[package]] 388 | name = "http" 389 | version = "1.3.1" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 392 | dependencies = [ 393 | "bytes", 394 | "fnv", 395 | "itoa", 396 | ] 397 | 398 | [[package]] 399 | name = "http-body" 400 | version = "1.0.1" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 403 | dependencies = [ 404 | "bytes", 405 | "http", 406 | ] 407 | 408 | [[package]] 409 | name = "http-body-util" 410 | version = "0.1.3" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 413 | dependencies = [ 414 | "bytes", 415 | "futures-core", 416 | "http", 417 | "http-body", 418 | "pin-project-lite", 419 | ] 420 | 421 | [[package]] 422 | name = "httparse" 423 | version = "1.10.1" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 426 | 427 | [[package]] 428 | name = "hyper" 429 | version = "1.6.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 432 | dependencies = [ 433 | "bytes", 434 | "futures-channel", 435 | "futures-util", 436 | "http", 437 | "http-body", 438 | "httparse", 439 | "itoa", 440 | "pin-project-lite", 441 | "smallvec", 442 | "tokio", 443 | "want", 444 | ] 445 | 446 | [[package]] 447 | name = "hyper-rustls" 448 | version = "0.27.5" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 451 | dependencies = [ 452 | "futures-util", 453 | "http", 454 | "hyper", 455 | "hyper-util", 456 | "rustls", 457 | "rustls-pki-types", 458 | "tokio", 459 | "tokio-rustls", 460 | "tower-service", 461 | "webpki-roots", 462 | ] 463 | 464 | [[package]] 465 | name = "hyper-util" 466 | version = "0.1.11" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" 469 | dependencies = [ 470 | "bytes", 471 | "futures-channel", 472 | "futures-util", 473 | "http", 474 | "http-body", 475 | "hyper", 476 | "libc", 477 | "pin-project-lite", 478 | "socket2", 479 | "tokio", 480 | "tower-service", 481 | "tracing", 482 | ] 483 | 484 | [[package]] 485 | name = "icu_collections" 486 | version = "1.5.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 489 | dependencies = [ 490 | "displaydoc", 491 | "yoke", 492 | "zerofrom", 493 | "zerovec", 494 | ] 495 | 496 | [[package]] 497 | name = "icu_locid" 498 | version = "1.5.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 501 | dependencies = [ 502 | "displaydoc", 503 | "litemap", 504 | "tinystr", 505 | "writeable", 506 | "zerovec", 507 | ] 508 | 509 | [[package]] 510 | name = "icu_locid_transform" 511 | version = "1.5.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 514 | dependencies = [ 515 | "displaydoc", 516 | "icu_locid", 517 | "icu_locid_transform_data", 518 | "icu_provider", 519 | "tinystr", 520 | "zerovec", 521 | ] 522 | 523 | [[package]] 524 | name = "icu_locid_transform_data" 525 | version = "1.5.1" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 528 | 529 | [[package]] 530 | name = "icu_normalizer" 531 | version = "1.5.0" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 534 | dependencies = [ 535 | "displaydoc", 536 | "icu_collections", 537 | "icu_normalizer_data", 538 | "icu_properties", 539 | "icu_provider", 540 | "smallvec", 541 | "utf16_iter", 542 | "utf8_iter", 543 | "write16", 544 | "zerovec", 545 | ] 546 | 547 | [[package]] 548 | name = "icu_normalizer_data" 549 | version = "1.5.1" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 552 | 553 | [[package]] 554 | name = "icu_properties" 555 | version = "1.5.1" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 558 | dependencies = [ 559 | "displaydoc", 560 | "icu_collections", 561 | "icu_locid_transform", 562 | "icu_properties_data", 563 | "icu_provider", 564 | "tinystr", 565 | "zerovec", 566 | ] 567 | 568 | [[package]] 569 | name = "icu_properties_data" 570 | version = "1.5.1" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 573 | 574 | [[package]] 575 | name = "icu_provider" 576 | version = "1.5.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 579 | dependencies = [ 580 | "displaydoc", 581 | "icu_locid", 582 | "icu_provider_macros", 583 | "stable_deref_trait", 584 | "tinystr", 585 | "writeable", 586 | "yoke", 587 | "zerofrom", 588 | "zerovec", 589 | ] 590 | 591 | [[package]] 592 | name = "icu_provider_macros" 593 | version = "1.5.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 596 | dependencies = [ 597 | "proc-macro2", 598 | "quote", 599 | "syn", 600 | ] 601 | 602 | [[package]] 603 | name = "idna" 604 | version = "1.0.3" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 607 | dependencies = [ 608 | "idna_adapter", 609 | "smallvec", 610 | "utf8_iter", 611 | ] 612 | 613 | [[package]] 614 | name = "idna_adapter" 615 | version = "1.2.0" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 618 | dependencies = [ 619 | "icu_normalizer", 620 | "icu_properties", 621 | ] 622 | 623 | [[package]] 624 | name = "indexmap" 625 | version = "2.9.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 628 | dependencies = [ 629 | "equivalent", 630 | "hashbrown", 631 | ] 632 | 633 | [[package]] 634 | name = "ipnet" 635 | version = "2.11.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 638 | 639 | [[package]] 640 | name = "is_terminal_polyfill" 641 | version = "1.70.1" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 644 | 645 | [[package]] 646 | name = "itoa" 647 | version = "1.0.15" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 650 | 651 | [[package]] 652 | name = "jiff" 653 | version = "0.2.5" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" 656 | dependencies = [ 657 | "jiff-static", 658 | "log", 659 | "portable-atomic", 660 | "portable-atomic-util", 661 | "serde", 662 | ] 663 | 664 | [[package]] 665 | name = "jiff-static" 666 | version = "0.2.5" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" 669 | dependencies = [ 670 | "proc-macro2", 671 | "quote", 672 | "syn", 673 | ] 674 | 675 | [[package]] 676 | name = "js-sys" 677 | version = "0.3.77" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 680 | dependencies = [ 681 | "once_cell", 682 | "wasm-bindgen", 683 | ] 684 | 685 | [[package]] 686 | name = "libc" 687 | version = "0.2.171" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 690 | 691 | [[package]] 692 | name = "litemap" 693 | version = "0.7.5" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 696 | 697 | [[package]] 698 | name = "log" 699 | version = "0.4.27" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 702 | 703 | [[package]] 704 | name = "memchr" 705 | version = "2.7.4" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 708 | 709 | [[package]] 710 | name = "mime" 711 | version = "0.3.17" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 714 | 715 | [[package]] 716 | name = "miniz_oxide" 717 | version = "0.8.7" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" 720 | dependencies = [ 721 | "adler2", 722 | ] 723 | 724 | [[package]] 725 | name = "mio" 726 | version = "1.0.3" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 729 | dependencies = [ 730 | "libc", 731 | "wasi 0.11.0+wasi-snapshot-preview1", 732 | "windows-sys 0.52.0", 733 | ] 734 | 735 | [[package]] 736 | name = "object" 737 | version = "0.36.7" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 740 | dependencies = [ 741 | "memchr", 742 | ] 743 | 744 | [[package]] 745 | name = "once_cell" 746 | version = "1.21.3" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 749 | 750 | [[package]] 751 | name = "percent-encoding" 752 | version = "2.3.1" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 755 | 756 | [[package]] 757 | name = "pin-project-lite" 758 | version = "0.2.16" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 761 | 762 | [[package]] 763 | name = "pin-utils" 764 | version = "0.1.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 767 | 768 | [[package]] 769 | name = "portable-atomic" 770 | version = "1.11.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 773 | 774 | [[package]] 775 | name = "portable-atomic-util" 776 | version = "0.2.4" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 779 | dependencies = [ 780 | "portable-atomic", 781 | ] 782 | 783 | [[package]] 784 | name = "ppv-lite86" 785 | version = "0.2.21" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 788 | dependencies = [ 789 | "zerocopy", 790 | ] 791 | 792 | [[package]] 793 | name = "proc-macro2" 794 | version = "1.0.94" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 797 | dependencies = [ 798 | "unicode-ident", 799 | ] 800 | 801 | [[package]] 802 | name = "quinn" 803 | version = "0.11.7" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" 806 | dependencies = [ 807 | "bytes", 808 | "cfg_aliases", 809 | "pin-project-lite", 810 | "quinn-proto", 811 | "quinn-udp", 812 | "rustc-hash", 813 | "rustls", 814 | "socket2", 815 | "thiserror", 816 | "tokio", 817 | "tracing", 818 | "web-time", 819 | ] 820 | 821 | [[package]] 822 | name = "quinn-proto" 823 | version = "0.11.10" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" 826 | dependencies = [ 827 | "bytes", 828 | "getrandom 0.3.2", 829 | "rand", 830 | "ring", 831 | "rustc-hash", 832 | "rustls", 833 | "rustls-pki-types", 834 | "slab", 835 | "thiserror", 836 | "tinyvec", 837 | "tracing", 838 | "web-time", 839 | ] 840 | 841 | [[package]] 842 | name = "quinn-udp" 843 | version = "0.5.11" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" 846 | dependencies = [ 847 | "cfg_aliases", 848 | "libc", 849 | "once_cell", 850 | "socket2", 851 | "tracing", 852 | "windows-sys 0.59.0", 853 | ] 854 | 855 | [[package]] 856 | name = "quote" 857 | version = "1.0.40" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 860 | dependencies = [ 861 | "proc-macro2", 862 | ] 863 | 864 | [[package]] 865 | name = "r-efi" 866 | version = "5.2.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 869 | 870 | [[package]] 871 | name = "rand" 872 | version = "0.9.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 875 | dependencies = [ 876 | "rand_chacha", 877 | "rand_core", 878 | "zerocopy", 879 | ] 880 | 881 | [[package]] 882 | name = "rand_chacha" 883 | version = "0.9.0" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 886 | dependencies = [ 887 | "ppv-lite86", 888 | "rand_core", 889 | ] 890 | 891 | [[package]] 892 | name = "rand_core" 893 | version = "0.9.3" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 896 | dependencies = [ 897 | "getrandom 0.3.2", 898 | ] 899 | 900 | [[package]] 901 | name = "regex" 902 | version = "1.11.1" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 905 | dependencies = [ 906 | "aho-corasick", 907 | "memchr", 908 | "regex-automata", 909 | "regex-syntax", 910 | ] 911 | 912 | [[package]] 913 | name = "regex-automata" 914 | version = "0.4.9" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 917 | dependencies = [ 918 | "aho-corasick", 919 | "memchr", 920 | "regex-syntax", 921 | ] 922 | 923 | [[package]] 924 | name = "regex-syntax" 925 | version = "0.8.5" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 928 | 929 | [[package]] 930 | name = "reqwest" 931 | version = "0.12.15" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 934 | dependencies = [ 935 | "base64", 936 | "bytes", 937 | "futures-channel", 938 | "futures-core", 939 | "futures-util", 940 | "http", 941 | "http-body", 942 | "http-body-util", 943 | "hyper", 944 | "hyper-rustls", 945 | "hyper-util", 946 | "ipnet", 947 | "js-sys", 948 | "log", 949 | "mime", 950 | "once_cell", 951 | "percent-encoding", 952 | "pin-project-lite", 953 | "quinn", 954 | "rustls", 955 | "rustls-pemfile", 956 | "rustls-pki-types", 957 | "serde", 958 | "serde_json", 959 | "serde_urlencoded", 960 | "sync_wrapper", 961 | "tokio", 962 | "tokio-rustls", 963 | "tower", 964 | "tower-service", 965 | "url", 966 | "wasm-bindgen", 967 | "wasm-bindgen-futures", 968 | "web-sys", 969 | "webpki-roots", 970 | "windows-registry", 971 | ] 972 | 973 | [[package]] 974 | name = "ring" 975 | version = "0.17.14" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 978 | dependencies = [ 979 | "cc", 980 | "cfg-if", 981 | "getrandom 0.2.15", 982 | "libc", 983 | "untrusted", 984 | "windows-sys 0.52.0", 985 | ] 986 | 987 | [[package]] 988 | name = "rustc-demangle" 989 | version = "0.1.24" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 992 | 993 | [[package]] 994 | name = "rustc-hash" 995 | version = "2.1.1" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 998 | 999 | [[package]] 1000 | name = "rustls" 1001 | version = "0.23.25" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" 1004 | dependencies = [ 1005 | "once_cell", 1006 | "ring", 1007 | "rustls-pki-types", 1008 | "rustls-webpki", 1009 | "subtle", 1010 | "zeroize", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "rustls-pemfile" 1015 | version = "2.2.0" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1018 | dependencies = [ 1019 | "rustls-pki-types", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "rustls-pki-types" 1024 | version = "1.11.0" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 1027 | dependencies = [ 1028 | "web-time", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "rustls-webpki" 1033 | version = "0.103.1" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" 1036 | dependencies = [ 1037 | "ring", 1038 | "rustls-pki-types", 1039 | "untrusted", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "rustversion" 1044 | version = "1.0.20" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1047 | 1048 | [[package]] 1049 | name = "ryu" 1050 | version = "1.0.20" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1053 | 1054 | [[package]] 1055 | name = "serde" 1056 | version = "1.0.219" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1059 | dependencies = [ 1060 | "serde_derive", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "serde_derive" 1065 | version = "1.0.219" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1068 | dependencies = [ 1069 | "proc-macro2", 1070 | "quote", 1071 | "syn", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "serde_json" 1076 | version = "1.0.140" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1079 | dependencies = [ 1080 | "itoa", 1081 | "memchr", 1082 | "ryu", 1083 | "serde", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "serde_urlencoded" 1088 | version = "0.7.1" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1091 | dependencies = [ 1092 | "form_urlencoded", 1093 | "itoa", 1094 | "ryu", 1095 | "serde", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "shlex" 1100 | version = "1.3.0" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1103 | 1104 | [[package]] 1105 | name = "slab" 1106 | version = "0.4.9" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1109 | dependencies = [ 1110 | "autocfg", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "smallvec" 1115 | version = "1.14.0" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 1118 | 1119 | [[package]] 1120 | name = "socket2" 1121 | version = "0.5.9" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1124 | dependencies = [ 1125 | "libc", 1126 | "windows-sys 0.52.0", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "stable_deref_trait" 1131 | version = "1.2.0" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1134 | 1135 | [[package]] 1136 | name = "strsim" 1137 | version = "0.11.1" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1140 | 1141 | [[package]] 1142 | name = "subtle" 1143 | version = "2.6.1" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1146 | 1147 | [[package]] 1148 | name = "syn" 1149 | version = "2.0.100" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1152 | dependencies = [ 1153 | "proc-macro2", 1154 | "quote", 1155 | "unicode-ident", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "sync_wrapper" 1160 | version = "1.0.2" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1163 | dependencies = [ 1164 | "futures-core", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "synstructure" 1169 | version = "0.13.1" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1172 | dependencies = [ 1173 | "proc-macro2", 1174 | "quote", 1175 | "syn", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "thiserror" 1180 | version = "2.0.12" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1183 | dependencies = [ 1184 | "thiserror-impl", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "thiserror-impl" 1189 | version = "2.0.12" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1192 | dependencies = [ 1193 | "proc-macro2", 1194 | "quote", 1195 | "syn", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "tinystr" 1200 | version = "0.7.6" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1203 | dependencies = [ 1204 | "displaydoc", 1205 | "zerovec", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "tinyvec" 1210 | version = "1.9.0" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 1213 | dependencies = [ 1214 | "tinyvec_macros", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "tinyvec_macros" 1219 | version = "0.1.1" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1222 | 1223 | [[package]] 1224 | name = "tokio" 1225 | version = "1.44.2" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 1228 | dependencies = [ 1229 | "backtrace", 1230 | "bytes", 1231 | "libc", 1232 | "mio", 1233 | "pin-project-lite", 1234 | "socket2", 1235 | "windows-sys 0.52.0", 1236 | ] 1237 | 1238 | [[package]] 1239 | name = "tokio-rustls" 1240 | version = "0.26.2" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 1243 | dependencies = [ 1244 | "rustls", 1245 | "tokio", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "tower" 1250 | version = "0.5.2" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1253 | dependencies = [ 1254 | "futures-core", 1255 | "futures-util", 1256 | "pin-project-lite", 1257 | "sync_wrapper", 1258 | "tokio", 1259 | "tower-layer", 1260 | "tower-service", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "tower-layer" 1265 | version = "0.3.3" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1268 | 1269 | [[package]] 1270 | name = "tower-service" 1271 | version = "0.3.3" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1274 | 1275 | [[package]] 1276 | name = "tracing" 1277 | version = "0.1.41" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1280 | dependencies = [ 1281 | "pin-project-lite", 1282 | "tracing-core", 1283 | ] 1284 | 1285 | [[package]] 1286 | name = "tracing-core" 1287 | version = "0.1.33" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1290 | dependencies = [ 1291 | "once_cell", 1292 | ] 1293 | 1294 | [[package]] 1295 | name = "try-lock" 1296 | version = "0.2.5" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1299 | 1300 | [[package]] 1301 | name = "unicode-ident" 1302 | version = "1.0.18" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1305 | 1306 | [[package]] 1307 | name = "untrusted" 1308 | version = "0.9.0" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1311 | 1312 | [[package]] 1313 | name = "url" 1314 | version = "2.5.4" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1317 | dependencies = [ 1318 | "form_urlencoded", 1319 | "idna", 1320 | "percent-encoding", 1321 | ] 1322 | 1323 | [[package]] 1324 | name = "utf16_iter" 1325 | version = "1.0.5" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1328 | 1329 | [[package]] 1330 | name = "utf8_iter" 1331 | version = "1.0.4" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1334 | 1335 | [[package]] 1336 | name = "utf8parse" 1337 | version = "0.2.2" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1340 | 1341 | [[package]] 1342 | name = "want" 1343 | version = "0.3.1" 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" 1345 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1346 | dependencies = [ 1347 | "try-lock", 1348 | ] 1349 | 1350 | [[package]] 1351 | name = "wasi" 1352 | version = "0.11.0+wasi-snapshot-preview1" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1355 | 1356 | [[package]] 1357 | name = "wasi" 1358 | version = "0.14.2+wasi-0.2.4" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1361 | dependencies = [ 1362 | "wit-bindgen-rt", 1363 | ] 1364 | 1365 | [[package]] 1366 | name = "wasm-bindgen" 1367 | version = "0.2.100" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1370 | dependencies = [ 1371 | "cfg-if", 1372 | "once_cell", 1373 | "rustversion", 1374 | "wasm-bindgen-macro", 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "wasm-bindgen-backend" 1379 | version = "0.2.100" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1382 | dependencies = [ 1383 | "bumpalo", 1384 | "log", 1385 | "proc-macro2", 1386 | "quote", 1387 | "syn", 1388 | "wasm-bindgen-shared", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "wasm-bindgen-futures" 1393 | version = "0.4.50" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1396 | dependencies = [ 1397 | "cfg-if", 1398 | "js-sys", 1399 | "once_cell", 1400 | "wasm-bindgen", 1401 | "web-sys", 1402 | ] 1403 | 1404 | [[package]] 1405 | name = "wasm-bindgen-macro" 1406 | version = "0.2.100" 1407 | source = "registry+https://github.com/rust-lang/crates.io-index" 1408 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1409 | dependencies = [ 1410 | "quote", 1411 | "wasm-bindgen-macro-support", 1412 | ] 1413 | 1414 | [[package]] 1415 | name = "wasm-bindgen-macro-support" 1416 | version = "0.2.100" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1419 | dependencies = [ 1420 | "proc-macro2", 1421 | "quote", 1422 | "syn", 1423 | "wasm-bindgen-backend", 1424 | "wasm-bindgen-shared", 1425 | ] 1426 | 1427 | [[package]] 1428 | name = "wasm-bindgen-shared" 1429 | version = "0.2.100" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1432 | dependencies = [ 1433 | "unicode-ident", 1434 | ] 1435 | 1436 | [[package]] 1437 | name = "web-sys" 1438 | version = "0.3.77" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1441 | dependencies = [ 1442 | "js-sys", 1443 | "wasm-bindgen", 1444 | ] 1445 | 1446 | [[package]] 1447 | name = "web-time" 1448 | version = "1.1.0" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1451 | dependencies = [ 1452 | "js-sys", 1453 | "wasm-bindgen", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "webpki-roots" 1458 | version = "0.26.8" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" 1461 | dependencies = [ 1462 | "rustls-pki-types", 1463 | ] 1464 | 1465 | [[package]] 1466 | name = "windows-link" 1467 | version = "0.1.1" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 1470 | 1471 | [[package]] 1472 | name = "windows-registry" 1473 | version = "0.4.0" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" 1476 | dependencies = [ 1477 | "windows-result", 1478 | "windows-strings", 1479 | "windows-targets 0.53.0", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "windows-result" 1484 | version = "0.3.2" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 1487 | dependencies = [ 1488 | "windows-link", 1489 | ] 1490 | 1491 | [[package]] 1492 | name = "windows-strings" 1493 | version = "0.3.1" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 1496 | dependencies = [ 1497 | "windows-link", 1498 | ] 1499 | 1500 | [[package]] 1501 | name = "windows-sys" 1502 | version = "0.52.0" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1505 | dependencies = [ 1506 | "windows-targets 0.52.6", 1507 | ] 1508 | 1509 | [[package]] 1510 | name = "windows-sys" 1511 | version = "0.59.0" 1512 | source = "registry+https://github.com/rust-lang/crates.io-index" 1513 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1514 | dependencies = [ 1515 | "windows-targets 0.52.6", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "windows-targets" 1520 | version = "0.52.6" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1523 | dependencies = [ 1524 | "windows_aarch64_gnullvm 0.52.6", 1525 | "windows_aarch64_msvc 0.52.6", 1526 | "windows_i686_gnu 0.52.6", 1527 | "windows_i686_gnullvm 0.52.6", 1528 | "windows_i686_msvc 0.52.6", 1529 | "windows_x86_64_gnu 0.52.6", 1530 | "windows_x86_64_gnullvm 0.52.6", 1531 | "windows_x86_64_msvc 0.52.6", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "windows-targets" 1536 | version = "0.53.0" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 1539 | dependencies = [ 1540 | "windows_aarch64_gnullvm 0.53.0", 1541 | "windows_aarch64_msvc 0.53.0", 1542 | "windows_i686_gnu 0.53.0", 1543 | "windows_i686_gnullvm 0.53.0", 1544 | "windows_i686_msvc 0.53.0", 1545 | "windows_x86_64_gnu 0.53.0", 1546 | "windows_x86_64_gnullvm 0.53.0", 1547 | "windows_x86_64_msvc 0.53.0", 1548 | ] 1549 | 1550 | [[package]] 1551 | name = "windows_aarch64_gnullvm" 1552 | version = "0.52.6" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1555 | 1556 | [[package]] 1557 | name = "windows_aarch64_gnullvm" 1558 | version = "0.53.0" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1561 | 1562 | [[package]] 1563 | name = "windows_aarch64_msvc" 1564 | version = "0.52.6" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1567 | 1568 | [[package]] 1569 | name = "windows_aarch64_msvc" 1570 | version = "0.53.0" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1573 | 1574 | [[package]] 1575 | name = "windows_i686_gnu" 1576 | version = "0.52.6" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1579 | 1580 | [[package]] 1581 | name = "windows_i686_gnu" 1582 | version = "0.53.0" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1585 | 1586 | [[package]] 1587 | name = "windows_i686_gnullvm" 1588 | version = "0.52.6" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1591 | 1592 | [[package]] 1593 | name = "windows_i686_gnullvm" 1594 | version = "0.53.0" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1597 | 1598 | [[package]] 1599 | name = "windows_i686_msvc" 1600 | version = "0.52.6" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1603 | 1604 | [[package]] 1605 | name = "windows_i686_msvc" 1606 | version = "0.53.0" 1607 | source = "registry+https://github.com/rust-lang/crates.io-index" 1608 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1609 | 1610 | [[package]] 1611 | name = "windows_x86_64_gnu" 1612 | version = "0.52.6" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1615 | 1616 | [[package]] 1617 | name = "windows_x86_64_gnu" 1618 | version = "0.53.0" 1619 | source = "registry+https://github.com/rust-lang/crates.io-index" 1620 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1621 | 1622 | [[package]] 1623 | name = "windows_x86_64_gnullvm" 1624 | version = "0.52.6" 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" 1626 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1627 | 1628 | [[package]] 1629 | name = "windows_x86_64_gnullvm" 1630 | version = "0.53.0" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1633 | 1634 | [[package]] 1635 | name = "windows_x86_64_msvc" 1636 | version = "0.52.6" 1637 | source = "registry+https://github.com/rust-lang/crates.io-index" 1638 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1639 | 1640 | [[package]] 1641 | name = "windows_x86_64_msvc" 1642 | version = "0.53.0" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1645 | 1646 | [[package]] 1647 | name = "wit-bindgen-rt" 1648 | version = "0.39.0" 1649 | source = "registry+https://github.com/rust-lang/crates.io-index" 1650 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1651 | dependencies = [ 1652 | "bitflags", 1653 | ] 1654 | 1655 | [[package]] 1656 | name = "write16" 1657 | version = "1.0.0" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1660 | 1661 | [[package]] 1662 | name = "writeable" 1663 | version = "0.5.5" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1666 | 1667 | [[package]] 1668 | name = "yoke" 1669 | version = "0.7.5" 1670 | source = "registry+https://github.com/rust-lang/crates.io-index" 1671 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1672 | dependencies = [ 1673 | "serde", 1674 | "stable_deref_trait", 1675 | "yoke-derive", 1676 | "zerofrom", 1677 | ] 1678 | 1679 | [[package]] 1680 | name = "yoke-derive" 1681 | version = "0.7.5" 1682 | source = "registry+https://github.com/rust-lang/crates.io-index" 1683 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1684 | dependencies = [ 1685 | "proc-macro2", 1686 | "quote", 1687 | "syn", 1688 | "synstructure", 1689 | ] 1690 | 1691 | [[package]] 1692 | name = "zerocopy" 1693 | version = "0.8.24" 1694 | source = "registry+https://github.com/rust-lang/crates.io-index" 1695 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 1696 | dependencies = [ 1697 | "zerocopy-derive", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "zerocopy-derive" 1702 | version = "0.8.24" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 1705 | dependencies = [ 1706 | "proc-macro2", 1707 | "quote", 1708 | "syn", 1709 | ] 1710 | 1711 | [[package]] 1712 | name = "zerofrom" 1713 | version = "0.1.6" 1714 | source = "registry+https://github.com/rust-lang/crates.io-index" 1715 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1716 | dependencies = [ 1717 | "zerofrom-derive", 1718 | ] 1719 | 1720 | [[package]] 1721 | name = "zerofrom-derive" 1722 | version = "0.1.6" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1725 | dependencies = [ 1726 | "proc-macro2", 1727 | "quote", 1728 | "syn", 1729 | "synstructure", 1730 | ] 1731 | 1732 | [[package]] 1733 | name = "zeroize" 1734 | version = "1.8.1" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1737 | 1738 | [[package]] 1739 | name = "zerovec" 1740 | version = "0.10.4" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1743 | dependencies = [ 1744 | "yoke", 1745 | "zerofrom", 1746 | "zerovec-derive", 1747 | ] 1748 | 1749 | [[package]] 1750 | name = "zerovec-derive" 1751 | version = "0.10.3" 1752 | source = "registry+https://github.com/rust-lang/crates.io-index" 1753 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1754 | dependencies = [ 1755 | "proc-macro2", 1756 | "quote", 1757 | "syn", 1758 | ] 1759 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cfspeedtest" 3 | version = "1.3.3" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Unofficial CLI for speed.cloudflare.com" 7 | readme = "README.md" 8 | repository = "https://github.com/code-inflation/cfspeedtest/" 9 | keywords = ["cli", "speedtest", "speed-test", "cloudflare"] 10 | categories = ["command-line-utilities"] 11 | exclude = [".github/"] 12 | 13 | [dependencies] 14 | log = "0.4" 15 | env_logger = "0.11" 16 | regex = "1.10" 17 | reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] } 18 | clap = { version = "4.5.37", features = ["derive"] } 19 | serde = { version = "1.0.219", features = ["derive"] } 20 | csv = "1.3.0" 21 | serde_json = "1.0" 22 | indexmap = "2.9.0" 23 | clap_complete = "4.5" 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:slim-bullseye as builder 2 | WORKDIR /usr/src/cfspeedtest 3 | COPY Cargo.toml Cargo.lock ./ 4 | COPY src ./src 5 | RUN cargo install --path . 6 | 7 | FROM debian:bullseye-slim 8 | RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/* 9 | COPY --from=builder /usr/local/cargo/bin/cfspeedtest /usr/local/bin/cfspeedtest 10 | 11 | # tini will be PID 1 and handle signal forwarding and process reaping 12 | ENTRYPOINT ["/usr/bin/tini", "--", "cfspeedtest"] 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cfspeedtest - Unofficial CLI for [speed.cloudflare.com](https://speed.cloudflare.com) 2 | ![CI](https://github.com/code-inflation/cfspeedtest/actions/workflows/CI.yml/badge.svg) 3 | ![Release](https://github.com/code-inflation/cfspeedtest/actions/workflows/release.yaml/badge.svg) 4 | ![Crates.io Version](https://img.shields.io/crates/v/cfspeedtest) 5 | ![Crates.io Downloads](https://img.shields.io/crates/d/cfspeedtest?label=Crates.io%20downloads) 6 | 7 | 8 | ## Installation 9 | Install using `cargo`: 10 | ```sh 11 | cargo install cfspeedtest 12 | ``` 13 | 14 | Or download the latest binary release here: [cfspeedtest/releases/latest](https://github.com/code-inflation/cfspeedtest/releases/latest) 15 | 16 | Alternatively there is also a [docker image available on dockerhub](https://hub.docker.com/r/cybuerg/cfspeedtest) 17 | ```sh 18 | docker run cybuerg/cfspeedtest 19 | ``` 20 | 21 | ## Usage 22 | ``` 23 | > cfspeedtest --help 24 | Unofficial CLI for speed.cloudflare.com 25 | 26 | Usage: cfspeedtest [OPTIONS] 27 | 28 | Options: 29 | -n, --nr-tests 30 | Number of test runs per payload size [default: 10] 31 | --nr-latency-tests 32 | Number of latency tests to run [default: 25] 33 | -m, --max-payload-size 34 | The max payload size in bytes to use [100k, 1m, 10m, 25m or 100m] [default: 25MB] 35 | -o, --output-format 36 | Set the output format [csv, json or json-pretty] > This silences all other output to stdout [default: StdOut] 37 | -v, --verbose 38 | Enable verbose output i.e. print boxplots of the measurements 39 | --ipv4 [] 40 | Force IPv4 with provided source IPv4 address or the default IPv4 address bound to the main interface 41 | --ipv6 [] 42 | Force IPv6 with provided source IPv6 address or the default IPv6 address bound to the main interface 43 | -d, --disable-dynamic-max-payload-size 44 | Disables dynamically skipping tests with larger payload sizes if the tests for the previous payload size took longer than 5 seconds 45 | --download-only 46 | Test download speed only 47 | --upload-only 48 | Test upload speed only 49 | --generate-completion 50 | Generate shell completion script for the specified shell [possible values: bash, elvish, fish, powershell, zsh] 51 | -h, --help 52 | Print help 53 | -V, --version 54 | Print version 55 | ``` 56 | 57 | Example usage: 58 | [![asciicast](https://asciinema.org/a/Moun5mFB1sm1VFkkFljG9UGyz.svg)](https://asciinema.org/a/Moun5mFB1sm1VFkkFljG9UGyz) 59 | 60 | Example with json-pretty output: 61 | [![asciicast](https://asciinema.org/a/P6IUAADtaCq3bT18GbYVHmksA.svg)](https://asciinema.org/a/P6IUAADtaCq3bT18GbYVHmksA) 62 | 63 | ### Shell Completion 64 | 65 | `cfspeedtest` supports generating shell completion scripts. Use the `--generate-completion` flag followed by your shell name (e.g., `bash`, `zsh`, `fish`, `powershell`, `elvish`). 66 | 67 | Example for bash (add to `~/.bashrc` or similar): 68 | ```sh 69 | cfspeedtest --generate-completion bash > ~/.local/share/bash-completion/completions/cfspeedtest 70 | # Or, if you don't have a completions directory set up: 71 | # source <(cfspeedtest --generate-completion bash) 72 | ``` 73 | 74 | Example for zsh (add to `~/.zshrc` or similar): 75 | ```sh 76 | # Ensure your fpath includes a directory for completions, e.g., ~/.zfunc 77 | # mkdir -p ~/.zfunc 78 | # echo 'fpath=(~/.zfunc $fpath)' >> ~/.zshrc 79 | cfspeedtest --generate-completion zsh > ~/.zfunc/_cfspeedtest 80 | # You may need to run compinit: 81 | # autoload -U compinit && compinit 82 | ``` 83 | 84 | Example for fish: 85 | ```sh 86 | cfspeedtest --generate-completion fish > ~/.config/fish/completions/cfspeedtest.fish 87 | ``` 88 | 89 | 90 | ## Development 91 | 92 | ### Logging 93 | Set the log level using the `RUST_LOG` env var: 94 | ```sh 95 | RUST_LOG=debug cargo run 96 | ``` 97 | ### Release 98 | #### Using `cargo-release` 99 | Install `cargo-release`: 100 | ```sh 101 | cargo install cargo-release 102 | ``` 103 | Create the release (version bump levels are `[patch, minor, major]`): 104 | ```sh 105 | cargo release patch --execute 106 | ``` 107 | This will bump the `cfspeedtest` version in both `Cargo.toml` and `Cargo.lock` and run `cargo publish` to push the release on crates.io. Additionally a version git tag is created and pushed to `master` triggering the GH action that creates the binary releases. 108 | 109 | #### On GitHub 110 | Release builds are published automatically using github actions. They are triggered when a git tag in the format `v[0-9]+.*` is pushed. 111 | ```sh 112 | git tag v1.0.0 113 | git push origin v1.0.0 114 | ``` 115 | #### On crates.io 116 | 1. Update `cfspeedtest` version in `Cargo.toml` 117 | 2. `cargo publish --dry-run` 118 | 3. Verify contents using `cargo package --list` 119 | 4. Upload to crates.io `cargo publish` 120 | -------------------------------------------------------------------------------- /examples/download_test.rs: -------------------------------------------------------------------------------- 1 | use cfspeedtest::speedtest::test_download; 2 | use cfspeedtest::OutputFormat; 3 | 4 | fn main() { 5 | println!("Testing download speed with 10MB of payload"); 6 | 7 | let download_speed = test_download( 8 | &reqwest::blocking::Client::new(), 9 | 10_000_000, 10 | OutputFormat::None, // don't write to stdout while running the test 11 | ); 12 | 13 | println!("download speed in mbit: {download_speed}") 14 | } 15 | -------------------------------------------------------------------------------- /examples/latency_test.rs: -------------------------------------------------------------------------------- 1 | use cfspeedtest::speedtest::run_latency_test; 2 | use cfspeedtest::OutputFormat; 3 | 4 | fn main() { 5 | println!("Testing latency"); 6 | 7 | let (latency_results, avg_latency) = run_latency_test( 8 | &reqwest::blocking::Client::new(), 9 | 25, 10 | OutputFormat::None, // don't write to stdout while running the test 11 | ); 12 | 13 | println!("average latancy in ms: {avg_latency}"); 14 | 15 | println!("all latency test results"); 16 | for latency_result in latency_results { 17 | println!("latency in ms: {latency_result}"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/simple_speedtest.rs: -------------------------------------------------------------------------------- 1 | use cfspeedtest::speedtest::speed_test; 2 | use cfspeedtest::speedtest::PayloadSize; 3 | use cfspeedtest::OutputFormat; 4 | use cfspeedtest::SpeedTestCLIOptions; 5 | 6 | fn main() { 7 | // define speedtest options 8 | let options = SpeedTestCLIOptions { 9 | output_format: OutputFormat::None, // don't write to stdout 10 | ipv4: None, // don't force ipv4 usage 11 | ipv6: None, // don't force ipv6 usage 12 | verbose: false, 13 | upload_only: false, 14 | download_only: false, 15 | nr_tests: 5, 16 | nr_latency_tests: 20, 17 | max_payload_size: PayloadSize::M10, 18 | disable_dynamic_max_payload_size: false, 19 | completion: None, 20 | }; 21 | 22 | let measurements = speed_test(reqwest::blocking::Client::new(), options); 23 | measurements 24 | .iter() 25 | .for_each(|measurement| println!("{measurement}")); 26 | } 27 | -------------------------------------------------------------------------------- /src/boxplot.rs: -------------------------------------------------------------------------------- 1 | use log; 2 | use std::fmt::Write; 3 | 4 | const PLOT_WIDTH: usize = 80; 5 | 6 | fn generate_axis_labels(minima: f64, maxima: f64) -> String { 7 | let mut labels = String::new(); 8 | write!(labels, "{:<10.2}", minima).unwrap(); 9 | write!( 10 | labels, 11 | "{:^width$.2}", 12 | (minima + maxima) / 2.0, 13 | width = PLOT_WIDTH - 20 14 | ) 15 | .unwrap(); 16 | write!(labels, "{:>10.2}", maxima).unwrap(); 17 | labels 18 | } 19 | 20 | pub(crate) fn render_plot(minima: f64, q1: f64, median: f64, q3: f64, maxima: f64) -> String { 21 | let value_range = maxima - minima; 22 | let quartile_0 = q1 - minima; 23 | let quartile_1 = median - q1; 24 | let quartile_2 = q3 - median; 25 | let quartile_3 = maxima - q3; 26 | 27 | let scale_factor = PLOT_WIDTH as f64 / value_range; 28 | 29 | let mut plot = String::with_capacity(PLOT_WIDTH); 30 | plot.push('|'); 31 | plot.push_str("-".repeat((quartile_0 * scale_factor) as usize).as_str()); 32 | plot.push_str("=".repeat((quartile_1 * scale_factor) as usize).as_str()); 33 | plot.push(':'); 34 | plot.push_str("=".repeat((quartile_2 * scale_factor) as usize).as_str()); 35 | plot.push_str("-".repeat((quartile_3 * scale_factor) as usize).as_str()); 36 | plot.push('|'); 37 | 38 | let axis_labels = generate_axis_labels(minima, maxima); 39 | plot.push('\n'); 40 | plot.push_str(&axis_labels); 41 | 42 | log::debug!("fn input: {minima}, {q1}, {median}, {q3}, {maxima}"); 43 | log::debug!("quartiles: {quartile_0}, {quartile_1}, {quartile_2}, {quartile_3}"); 44 | log::debug!("value range: {value_range}"); 45 | log::debug!("len of the plot: {}", plot.len()); 46 | 47 | plot 48 | } 49 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod boxplot; 2 | pub mod measurements; 3 | pub mod progress; 4 | pub mod speedtest; 5 | use std::fmt; 6 | use std::fmt::Display; 7 | 8 | use clap::Parser; 9 | use clap_complete::Shell; 10 | use speedtest::PayloadSize; 11 | 12 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 13 | pub enum OutputFormat { 14 | Csv, 15 | Json, 16 | JsonPretty, 17 | StdOut, 18 | None, 19 | } 20 | 21 | impl Display for OutputFormat { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | write!(f, "{:?}", self) 24 | } 25 | } 26 | 27 | impl OutputFormat { 28 | pub fn from(output_format_string: String) -> Result { 29 | match output_format_string.to_lowercase().as_str() { 30 | "csv" => Ok(Self::Csv), 31 | "json" => Ok(Self::Json), 32 | "json_pretty" | "json-pretty" => Ok(Self::JsonPretty), 33 | "stdout" => Ok(Self::StdOut), 34 | _ => Err("Value needs to be one of csv, json or json-pretty".to_string()), 35 | } 36 | } 37 | } 38 | 39 | /// Unofficial CLI for speed.cloudflare.com 40 | #[derive(Parser, Debug)] 41 | #[command(author, version, about, long_about = None)] 42 | pub struct SpeedTestCLIOptions { 43 | /// Number of test runs per payload size. 44 | #[arg(value_parser = clap::value_parser!(u32).range(1..1000), short, long, default_value_t = 10)] 45 | pub nr_tests: u32, 46 | 47 | /// Number of latency tests to run 48 | #[arg(long, default_value_t = 25)] 49 | pub nr_latency_tests: u32, 50 | 51 | /// The max payload size in bytes to use [100k, 1m, 10m, 25m or 100m] 52 | #[arg(value_parser = parse_payload_size, short, long, default_value_t = PayloadSize::M25)] 53 | pub max_payload_size: PayloadSize, 54 | 55 | /// Set the output format [csv, json or json-pretty] > 56 | /// This silences all other output to stdout 57 | #[arg(value_parser = parse_output_format, short, long, default_value_t = OutputFormat::StdOut)] 58 | pub output_format: OutputFormat, 59 | 60 | /// Enable verbose output i.e. print boxplots of the measurements 61 | #[arg(short, long)] 62 | pub verbose: bool, 63 | 64 | /// Force IPv4 with provided source IPv4 address or the default IPv4 address bound to the main interface 65 | #[clap(long, value_name = "IPv4", num_args = 0..=1, default_missing_value = "0.0.0.0", conflicts_with = "ipv6")] 66 | pub ipv4: Option, 67 | 68 | /// Force IPv6 with provided source IPv6 address or the default IPv6 address bound to the main interface 69 | #[clap(long, value_name = "IPv6", num_args = 0..=1, default_missing_value = "::", conflicts_with = "ipv4")] 70 | pub ipv6: Option, 71 | 72 | /// Disables dynamically skipping tests with larger payload sizes if the tests for the previous payload 73 | /// size took longer than 5 seconds 74 | #[arg(short, long)] 75 | pub disable_dynamic_max_payload_size: bool, 76 | 77 | /// Test download speed only 78 | #[arg(long, conflicts_with = "upload_only")] 79 | pub download_only: bool, 80 | 81 | /// Test upload speed only 82 | #[arg(long, conflicts_with = "download_only")] 83 | pub upload_only: bool, 84 | 85 | /// Generate shell completion script for the specified shell 86 | #[arg(long = "generate-completion", value_enum)] 87 | pub completion: Option, 88 | } 89 | 90 | impl SpeedTestCLIOptions { 91 | /// Returns whether download tests should be performed 92 | pub fn should_download(&self) -> bool { 93 | self.download_only || !self.upload_only 94 | } 95 | 96 | /// Returns whether upload tests should be performed 97 | pub fn should_upload(&self) -> bool { 98 | self.upload_only || !self.download_only 99 | } 100 | } 101 | 102 | fn parse_payload_size(input_string: &str) -> Result { 103 | PayloadSize::from(input_string.to_string()) 104 | } 105 | 106 | fn parse_output_format(input_string: &str) -> Result { 107 | OutputFormat::from(input_string.to_string()) 108 | } 109 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use cfspeedtest::speedtest; 2 | use cfspeedtest::OutputFormat; 3 | use cfspeedtest::SpeedTestCLIOptions; 4 | use clap::{CommandFactory, Parser}; 5 | use clap_complete::generate; 6 | use std::io; 7 | use std::net::IpAddr; 8 | 9 | use speedtest::speed_test; 10 | 11 | fn print_completions(gen: G, cmd: &mut clap::Command) { 12 | generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); 13 | } 14 | 15 | fn main() { 16 | env_logger::init(); 17 | let options = SpeedTestCLIOptions::parse(); 18 | 19 | if let Some(generator) = options.completion { 20 | let mut cmd = SpeedTestCLIOptions::command(); 21 | eprintln!("Generating completion script for {generator}..."); 22 | print_completions(generator, &mut cmd); 23 | return; 24 | } 25 | 26 | if options.output_format == OutputFormat::StdOut { 27 | println!("Starting Cloudflare speed test"); 28 | } 29 | let client; 30 | if let Some(ref ip) = options.ipv4 { 31 | client = reqwest::blocking::Client::builder() 32 | .local_address(ip.parse::().expect("Invalid IPv4 address")) 33 | .build(); 34 | } else if let Some(ref ip) = options.ipv6 { 35 | client = reqwest::blocking::Client::builder() 36 | .local_address(ip.parse::().expect("Invalid IPv6 address")) 37 | .build(); 38 | } else { 39 | client = reqwest::blocking::Client::builder().build(); 40 | } 41 | speed_test( 42 | client.expect("Failed to initialize reqwest client"), 43 | options, 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/measurements.rs: -------------------------------------------------------------------------------- 1 | use crate::boxplot; 2 | use crate::speedtest::TestType; 3 | use crate::OutputFormat; 4 | use indexmap::IndexSet; 5 | use serde::Serialize; 6 | use std::{fmt::Display, io}; 7 | 8 | #[derive(Serialize)] 9 | struct StatMeasurement { 10 | test_type: TestType, 11 | payload_size: usize, 12 | min: f64, 13 | q1: f64, 14 | median: f64, 15 | q3: f64, 16 | max: f64, 17 | avg: f64, 18 | } 19 | 20 | #[derive(Serialize)] 21 | pub struct Measurement { 22 | pub test_type: TestType, 23 | pub payload_size: usize, 24 | pub mbit: f64, 25 | } 26 | 27 | impl Display for Measurement { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | write!( 30 | f, 31 | "{:?}: \t{}\t-> {}", 32 | self.test_type, 33 | format_bytes(self.payload_size), 34 | self.mbit, 35 | ) 36 | } 37 | } 38 | 39 | pub(crate) fn log_measurements( 40 | measurements: &[Measurement], 41 | payload_sizes: Vec, 42 | verbose: bool, 43 | output_format: OutputFormat, 44 | ) { 45 | if output_format == OutputFormat::StdOut { 46 | println!("\nSummary Statistics"); 47 | println!("Type Payload | min/max/avg in mbit/s"); 48 | } 49 | let mut stat_measurements: Vec = Vec::new(); 50 | measurements 51 | .iter() 52 | .map(|m| m.test_type) 53 | .collect::>() 54 | .iter() 55 | .for_each(|t| { 56 | stat_measurements.extend(log_measurements_by_test_type( 57 | measurements, 58 | payload_sizes.clone(), 59 | verbose, 60 | output_format, 61 | *t, 62 | )) 63 | }); 64 | match output_format { 65 | OutputFormat::Csv => { 66 | let mut wtr = csv::Writer::from_writer(io::stdout()); 67 | for measurement in &stat_measurements { 68 | wtr.serialize(measurement).unwrap(); 69 | } 70 | wtr.flush().unwrap(); 71 | } 72 | OutputFormat::Json => { 73 | serde_json::to_writer(io::stdout(), &stat_measurements).unwrap(); 74 | println!(); 75 | } 76 | OutputFormat::JsonPretty => { 77 | // json_pretty output test 78 | serde_json::to_writer_pretty(io::stdout(), &stat_measurements).unwrap(); 79 | println!(); 80 | } 81 | OutputFormat::StdOut => {} 82 | OutputFormat::None => {} 83 | } 84 | } 85 | 86 | fn log_measurements_by_test_type( 87 | measurements: &[Measurement], 88 | payload_sizes: Vec, 89 | verbose: bool, 90 | output_format: OutputFormat, 91 | test_type: TestType, 92 | ) -> Vec { 93 | let mut stat_measurements: Vec = Vec::new(); 94 | for payload_size in payload_sizes { 95 | let type_measurements: Vec = measurements 96 | .iter() 97 | .filter(|m| m.test_type == test_type) 98 | .filter(|m| m.payload_size == payload_size) 99 | .map(|m| m.mbit) 100 | .collect(); 101 | 102 | // check if there are any measurements for the current payload_size 103 | // skip stats calculation if there are no measurements 104 | if !type_measurements.is_empty() { 105 | let (min, q1, median, q3, max, avg) = calc_stats(type_measurements).unwrap(); 106 | 107 | let formatted_payload = format_bytes(payload_size); 108 | let fmt_test_type = format!("{:?}", test_type); 109 | stat_measurements.push(StatMeasurement { 110 | test_type, 111 | payload_size, 112 | min, 113 | q1, 114 | median, 115 | q3, 116 | max, 117 | avg, 118 | }); 119 | if output_format == OutputFormat::StdOut { 120 | println!( 121 | "{fmt_test_type:<9} {formatted_payload:<7}| min {min:<7.2} max {max:<7.2} avg {avg:<7.2}" 122 | ); 123 | if verbose { 124 | let plot = boxplot::render_plot(min, q1, median, q3, max); 125 | println!("{plot}\n"); 126 | } 127 | } 128 | } 129 | } 130 | 131 | stat_measurements 132 | } 133 | 134 | fn calc_stats(mbit_measurements: Vec) -> Option<(f64, f64, f64, f64, f64, f64)> { 135 | log::debug!("calc_stats for mbit_measurements {mbit_measurements:?}"); 136 | let length = mbit_measurements.len(); 137 | if length == 0 { 138 | return None; 139 | } 140 | 141 | let mut sorted_data = mbit_measurements.clone(); 142 | sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Less)); 143 | 144 | if length == 1 { 145 | return Some(( 146 | sorted_data[0], 147 | sorted_data[0], 148 | sorted_data[0], 149 | sorted_data[0], 150 | sorted_data[0], 151 | sorted_data[0], 152 | )); 153 | } 154 | 155 | if length < 4 { 156 | return Some(( 157 | *sorted_data.first().unwrap(), 158 | *sorted_data.first().unwrap(), 159 | median(&sorted_data), 160 | *sorted_data.last().unwrap(), 161 | *sorted_data.last().unwrap(), 162 | mbit_measurements.iter().sum::() / mbit_measurements.len() as f64, 163 | )); 164 | } 165 | 166 | let q1 = if length % 2 == 0 { 167 | median(&sorted_data[0..length / 2]) 168 | } else { 169 | median(&sorted_data[0..length.div_ceil(2)]) 170 | }; 171 | 172 | let q3 = if length % 2 == 0 { 173 | median(&sorted_data[length / 2..length]) 174 | } else { 175 | median(&sorted_data[length.div_ceil(2)..length]) 176 | }; 177 | 178 | Some(( 179 | *sorted_data.first().unwrap(), 180 | q1, 181 | median(&sorted_data), 182 | q3, 183 | *sorted_data.last().unwrap(), 184 | mbit_measurements.iter().sum::() / mbit_measurements.len() as f64, 185 | )) 186 | } 187 | 188 | fn median(data: &[f64]) -> f64 { 189 | let length = data.len(); 190 | if length % 2 == 0 { 191 | (data[length / 2 - 1] + data[length / 2]) / 2.0 192 | } else { 193 | data[length / 2] 194 | } 195 | } 196 | 197 | pub(crate) fn format_bytes(bytes: usize) -> String { 198 | match bytes { 199 | 1_000..=999_999 => format!("{}KB", bytes / 1_000), 200 | 1_000_000..=999_999_999 => format!("{}MB", bytes / 1_000_000), 201 | _ => format!("{bytes} bytes"), 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/progress.rs: -------------------------------------------------------------------------------- 1 | use std::io::stdout; 2 | use std::io::Write; 3 | 4 | pub fn print_progress(name: &str, curr: u32, max: u32) { 5 | const BAR_LEN: u32 = 30; 6 | let progress_line = ((curr as f32 / max as f32) * BAR_LEN as f32) as u32; 7 | let remaining_line = BAR_LEN - progress_line; 8 | print!( 9 | "\r{:<15} [{}{}]", 10 | name, 11 | (0..progress_line).map(|_| "=").collect::(), 12 | (0..remaining_line).map(|_| "-").collect::(), 13 | ); 14 | stdout().flush().expect("error printing progress bar"); 15 | } 16 | -------------------------------------------------------------------------------- /src/speedtest.rs: -------------------------------------------------------------------------------- 1 | use crate::measurements::format_bytes; 2 | use crate::measurements::log_measurements; 3 | use crate::measurements::Measurement; 4 | use crate::progress::print_progress; 5 | use crate::OutputFormat; 6 | use crate::SpeedTestCLIOptions; 7 | use log; 8 | use regex::Regex; 9 | use reqwest::{blocking::Client, StatusCode}; 10 | use serde::Serialize; 11 | use std::{ 12 | fmt::Display, 13 | time::{Duration, Instant}, 14 | }; 15 | 16 | const BASE_URL: &str = "https://speed.cloudflare.com"; 17 | const DOWNLOAD_URL: &str = "__down?bytes="; 18 | const UPLOAD_URL: &str = "__up"; 19 | 20 | #[derive(Clone, Copy, Debug, Hash, Serialize, Eq, PartialEq)] 21 | pub enum TestType { 22 | Download, 23 | Upload, 24 | } 25 | 26 | #[derive(Clone, Debug)] 27 | pub enum PayloadSize { 28 | K100 = 100_000, 29 | M1 = 1_000_000, 30 | M10 = 10_000_000, 31 | M25 = 25_000_000, 32 | M100 = 100_000_000, 33 | } 34 | 35 | impl Display for PayloadSize { 36 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 37 | write!(f, "{}", format_bytes(self.clone() as usize)) 38 | } 39 | } 40 | 41 | impl PayloadSize { 42 | pub fn from(payload_string: String) -> Result { 43 | match payload_string.to_lowercase().as_str() { 44 | "100_000" | "100000" | "100k" | "100kb" => Ok(Self::K100), 45 | "1_000_000" | "1000000" | "1m" | "1mb" => Ok(Self::M1), 46 | "10_000_000" | "10000000" | "10m" | "10mb" => Ok(Self::M10), 47 | "25_000_000" | "25000000" | "25m" | "25mb" => Ok(Self::M25), 48 | "100_000_000" | "100000000" | "100m" | "100mb" => Ok(Self::M100), 49 | _ => Err("Value needs to be one of 100k, 1m, 10m, 25m or 100m".to_string()), 50 | } 51 | } 52 | 53 | pub fn sizes_from_max(max_payload_size: PayloadSize) -> Vec { 54 | log::debug!("getting payload iterations for max_payload_size {max_payload_size:?}"); 55 | let payload_bytes: Vec = 56 | vec![100_000, 1_000_000, 10_000_000, 25_000_000, 100_000_000]; 57 | match max_payload_size { 58 | PayloadSize::K100 => payload_bytes[0..1].to_vec(), 59 | PayloadSize::M1 => payload_bytes[0..2].to_vec(), 60 | PayloadSize::M10 => payload_bytes[0..3].to_vec(), 61 | PayloadSize::M25 => payload_bytes[0..4].to_vec(), 62 | PayloadSize::M100 => payload_bytes[0..5].to_vec(), 63 | } 64 | } 65 | } 66 | 67 | pub struct Metadata { 68 | city: String, 69 | country: String, 70 | ip: String, 71 | asn: String, 72 | colo: String, 73 | } 74 | 75 | impl Display for Metadata { 76 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 77 | write!( 78 | f, 79 | "City: {}\nCountry: {}\nIp: {}\nAsn: {}\nColo: {}", 80 | self.city, self.country, self.ip, self.asn, self.colo 81 | ) 82 | } 83 | } 84 | 85 | pub fn speed_test(client: Client, options: SpeedTestCLIOptions) -> Vec { 86 | let metadata = fetch_metadata(&client); 87 | if options.output_format == OutputFormat::StdOut { 88 | println!("{metadata}"); 89 | } 90 | run_latency_test(&client, options.nr_latency_tests, options.output_format); 91 | let payload_sizes = PayloadSize::sizes_from_max(options.max_payload_size.clone()); 92 | let mut measurements = Vec::new(); 93 | 94 | if options.should_download() { 95 | measurements.extend(run_tests( 96 | &client, 97 | test_download, 98 | TestType::Download, 99 | payload_sizes.clone(), 100 | options.nr_tests, 101 | options.output_format, 102 | options.disable_dynamic_max_payload_size, 103 | )); 104 | } 105 | 106 | if options.should_upload() { 107 | measurements.extend(run_tests( 108 | &client, 109 | test_upload, 110 | TestType::Upload, 111 | payload_sizes.clone(), 112 | options.nr_tests, 113 | options.output_format, 114 | options.disable_dynamic_max_payload_size, 115 | )); 116 | } 117 | 118 | log_measurements( 119 | &measurements, 120 | payload_sizes, 121 | options.verbose, 122 | options.output_format, 123 | ); 124 | measurements 125 | } 126 | 127 | pub fn run_latency_test( 128 | client: &Client, 129 | nr_latency_tests: u32, 130 | output_format: OutputFormat, 131 | ) -> (Vec, f64) { 132 | let mut measurements: Vec = Vec::new(); 133 | for i in 0..=nr_latency_tests { 134 | if output_format == OutputFormat::StdOut { 135 | print_progress("latency test", i, nr_latency_tests); 136 | } 137 | let latency = test_latency(client); 138 | measurements.push(latency); 139 | } 140 | let avg_latency = measurements.iter().sum::() / measurements.len() as f64; 141 | 142 | if output_format == OutputFormat::StdOut { 143 | println!( 144 | "\nAvg GET request latency {avg_latency:.2} ms (RTT excluding server processing time)\n" 145 | ); 146 | } 147 | (measurements, avg_latency) 148 | } 149 | 150 | pub fn test_latency(client: &Client) -> f64 { 151 | let url = &format!("{}/{}{}", BASE_URL, DOWNLOAD_URL, 0); 152 | let req_builder = client.get(url); 153 | 154 | let start = Instant::now(); 155 | let response = req_builder.send().expect("failed to get response"); 156 | let _status_code = response.status(); 157 | let duration = start.elapsed().as_secs_f64() * 1_000.0; 158 | 159 | let re = Regex::new(r"cfRequestDuration;dur=([\d.]+)").unwrap(); 160 | let cf_req_duration: f64 = re 161 | .captures( 162 | response 163 | .headers() 164 | .get("Server-Timing") 165 | .expect("No Server-Timing in response header") 166 | .to_str() 167 | .unwrap(), 168 | ) 169 | .unwrap() 170 | .get(1) 171 | .unwrap() 172 | .as_str() 173 | .parse() 174 | .unwrap(); 175 | let mut req_latency = duration - cf_req_duration; 176 | if req_latency < 0.0 { 177 | // TODO investigate negative latency values 178 | req_latency = 0.0 179 | } 180 | req_latency 181 | } 182 | 183 | const TIME_THRESHOLD: Duration = Duration::from_secs(5); 184 | 185 | pub fn run_tests( 186 | client: &Client, 187 | test_fn: fn(&Client, usize, OutputFormat) -> f64, 188 | test_type: TestType, 189 | payload_sizes: Vec, 190 | nr_tests: u32, 191 | output_format: OutputFormat, 192 | disable_dynamic_max_payload_size: bool, 193 | ) -> Vec { 194 | let mut measurements: Vec = Vec::new(); 195 | for payload_size in payload_sizes { 196 | log::debug!("running tests for payload_size {payload_size}"); 197 | let start = Instant::now(); 198 | for i in 0..nr_tests { 199 | if output_format == OutputFormat::StdOut { 200 | print_progress( 201 | &format!("{:?} {:<5}", test_type, format_bytes(payload_size)), 202 | i, 203 | nr_tests, 204 | ); 205 | } 206 | let mbit = test_fn(client, payload_size, output_format); 207 | measurements.push(Measurement { 208 | test_type, 209 | payload_size, 210 | mbit, 211 | }); 212 | } 213 | if output_format == OutputFormat::StdOut { 214 | print_progress( 215 | &format!("{:?} {:<5}", test_type, format_bytes(payload_size)), 216 | nr_tests, 217 | nr_tests, 218 | ); 219 | println!() 220 | } 221 | let duration = start.elapsed(); 222 | 223 | // only check TIME_THRESHOLD if dynamic max payload sizing is not disabled 224 | if !disable_dynamic_max_payload_size && duration > TIME_THRESHOLD { 225 | log::info!("Exceeded threshold"); 226 | break; 227 | } 228 | } 229 | measurements 230 | } 231 | 232 | pub fn test_upload(client: &Client, payload_size_bytes: usize, output_format: OutputFormat) -> f64 { 233 | let url = &format!("{BASE_URL}/{UPLOAD_URL}"); 234 | let payload: Vec = vec![1; payload_size_bytes]; 235 | let req_builder = client.post(url).body(payload); 236 | let (status_code, mbits, duration) = { 237 | let start = Instant::now(); 238 | let response = req_builder.send().expect("failed to get response"); 239 | let status_code = response.status(); 240 | let duration = start.elapsed(); 241 | let mbits = (payload_size_bytes as f64 * 8.0 / 1_000_000.0) / duration.as_secs_f64(); 242 | (status_code, mbits, duration) 243 | }; 244 | if output_format == OutputFormat::StdOut { 245 | print_current_speed(mbits, duration, status_code, payload_size_bytes); 246 | } 247 | mbits 248 | } 249 | 250 | pub fn test_download( 251 | client: &Client, 252 | payload_size_bytes: usize, 253 | output_format: OutputFormat, 254 | ) -> f64 { 255 | let url = &format!("{BASE_URL}/{DOWNLOAD_URL}{payload_size_bytes}"); 256 | let req_builder = client.get(url); 257 | let (status_code, mbits, duration) = { 258 | let response = req_builder.send().expect("failed to get response"); 259 | let status_code = response.status(); 260 | let start = Instant::now(); 261 | let _res_bytes = response.bytes(); 262 | let duration = start.elapsed(); 263 | let mbits = (payload_size_bytes as f64 * 8.0 / 1_000_000.0) / duration.as_secs_f64(); 264 | (status_code, mbits, duration) 265 | }; 266 | if output_format == OutputFormat::StdOut { 267 | print_current_speed(mbits, duration, status_code, payload_size_bytes); 268 | } 269 | mbits 270 | } 271 | 272 | fn print_current_speed( 273 | mbits: f64, 274 | duration: Duration, 275 | status_code: StatusCode, 276 | payload_size_bytes: usize, 277 | ) { 278 | print!( 279 | " {:>6.2} mbit/s | {:>5} in {:>4}ms -> status: {} ", 280 | mbits, 281 | format_bytes(payload_size_bytes), 282 | duration.as_millis(), 283 | status_code 284 | ); 285 | } 286 | 287 | pub fn fetch_metadata(client: &Client) -> Metadata { 288 | let url = &format!("{}/{}{}", BASE_URL, DOWNLOAD_URL, 0); 289 | let headers = client 290 | .get(url) 291 | .send() 292 | .expect("failed to get response") 293 | .headers() 294 | .to_owned(); 295 | Metadata { 296 | city: extract_header_value(&headers, "cf-meta-city", "City N/A"), 297 | country: extract_header_value(&headers, "cf-meta-country", "Country N/A"), 298 | ip: extract_header_value(&headers, "cf-meta-ip", "IP N/A"), 299 | asn: extract_header_value(&headers, "cf-meta-asn", "ASN N/A"), 300 | colo: extract_header_value(&headers, "cf-meta-colo", "Colo N/A"), 301 | } 302 | } 303 | 304 | fn extract_header_value( 305 | headers: &reqwest::header::HeaderMap, 306 | header_name: &str, 307 | na_value: &str, 308 | ) -> String { 309 | headers 310 | .get(header_name) 311 | .and_then(|value| value.to_str().ok()) 312 | .unwrap_or(na_value) 313 | .to_owned() 314 | } 315 | --------------------------------------------------------------------------------