├── .dockerignore ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── README.md ├── rustfmt.toml ├── src ├── lib.rs ├── main.rs ├── metrics.rs ├── models.rs ├── ratelimiter.rs ├── ratelimiter │ ├── local.rs │ ├── redis.rs │ └── scripts │ │ ├── claim.lua │ │ └── release.lua ├── route.rs ├── runtime.rs └── runtime │ ├── client.rs │ ├── config.rs │ └── metrics.rs └── tests └── handles_request.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.gitignore 3 | /.github 4 | /.vscode 5 | /.dockerignore 6 | /target 7 | /Dockerfile 8 | /README.md 9 | /rustfmt.toml 10 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | release: 9 | types: [ published ] 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macos-latest] 20 | steps: 21 | - uses: actions/checkout@v2 22 | - run: rustup update stable 23 | - name: Build 24 | run: cargo build --release --verbose 25 | - name: Upload artifacts 26 | uses: actions/upload-artifact@v2 27 | with: 28 | name: proxy-${{ runner.OS }} 29 | path: target/release/proxy* 30 | 31 | publish: 32 | runs-on: ubuntu-latest 33 | if: github.event_name != 'pull_request' 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Publish to Docker 37 | uses: jerray/publish-docker-action@master 38 | with: 39 | username: ${{ secrets.DOCKER_USERNAME }} 40 | password: ${{ secrets.DOCKER_PASSWORD }} 41 | repository: spectacles/proxy 42 | auto_tag: true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /proxy.toml 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.12.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "anyhow" 25 | version = "1.0.63" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "a26fa4d7e3f2eebadf743988fc8aec9fa9a9e82611acafd77c1462ed6262440a" 28 | 29 | [[package]] 30 | name = "assert-json-diff" 31 | version = "1.1.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "4259cbe96513d2f1073027a259fc2ca917feb3026a5a8d984e3628e490255cc0" 34 | dependencies = [ 35 | "extend", 36 | "serde", 37 | "serde_json", 38 | ] 39 | 40 | [[package]] 41 | name = "async-trait" 42 | version = "0.1.57" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" 45 | dependencies = [ 46 | "proc-macro2", 47 | "quote", 48 | "syn", 49 | ] 50 | 51 | [[package]] 52 | name = "atty" 53 | version = "0.2.14" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 56 | dependencies = [ 57 | "hermit-abi", 58 | "libc", 59 | "winapi", 60 | ] 61 | 62 | [[package]] 63 | name = "autocfg" 64 | version = "0.1.8" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" 67 | dependencies = [ 68 | "autocfg 1.1.0", 69 | ] 70 | 71 | [[package]] 72 | name = "autocfg" 73 | version = "1.1.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 76 | 77 | [[package]] 78 | name = "base64" 79 | version = "0.13.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 82 | 83 | [[package]] 84 | name = "bitflags" 85 | version = "1.3.2" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 88 | 89 | [[package]] 90 | name = "block-buffer" 91 | version = "0.10.2" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" 94 | dependencies = [ 95 | "generic-array", 96 | ] 97 | 98 | [[package]] 99 | name = "buf_redux" 100 | version = "0.8.4" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" 103 | dependencies = [ 104 | "memchr", 105 | "safemem", 106 | ] 107 | 108 | [[package]] 109 | name = "bumpalo" 110 | version = "3.11.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" 113 | 114 | [[package]] 115 | name = "byteorder" 116 | version = "1.4.3" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 119 | 120 | [[package]] 121 | name = "bytes" 122 | version = "1.2.1" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 125 | dependencies = [ 126 | "serde", 127 | ] 128 | 129 | [[package]] 130 | name = "cc" 131 | version = "1.0.73" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 134 | 135 | [[package]] 136 | name = "cfg-if" 137 | version = "1.0.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 140 | 141 | [[package]] 142 | name = "cloudabi" 143 | version = "0.0.3" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 146 | dependencies = [ 147 | "bitflags", 148 | ] 149 | 150 | [[package]] 151 | name = "colored" 152 | version = "1.9.3" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" 155 | dependencies = [ 156 | "atty", 157 | "lazy_static", 158 | "winapi", 159 | ] 160 | 161 | [[package]] 162 | name = "cpufeatures" 163 | version = "0.2.4" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813" 166 | dependencies = [ 167 | "libc", 168 | ] 169 | 170 | [[package]] 171 | name = "crypto-common" 172 | version = "0.1.6" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 175 | dependencies = [ 176 | "generic-array", 177 | "typenum", 178 | ] 179 | 180 | [[package]] 181 | name = "deadpool" 182 | version = "0.9.5" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" 185 | dependencies = [ 186 | "async-trait", 187 | "deadpool-runtime", 188 | "num_cpus", 189 | "retain_mut", 190 | "tokio", 191 | ] 192 | 193 | [[package]] 194 | name = "deadpool-runtime" 195 | version = "0.1.2" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" 198 | 199 | [[package]] 200 | name = "difference" 201 | version = "2.0.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 204 | 205 | [[package]] 206 | name = "digest" 207 | version = "0.10.3" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" 210 | dependencies = [ 211 | "block-buffer", 212 | "crypto-common", 213 | ] 214 | 215 | [[package]] 216 | name = "dtoa" 217 | version = "0.4.8" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" 220 | 221 | [[package]] 222 | name = "either" 223 | version = "1.8.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" 226 | 227 | [[package]] 228 | name = "encoding_rs" 229 | version = "0.8.31" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" 232 | dependencies = [ 233 | "cfg-if", 234 | ] 235 | 236 | [[package]] 237 | name = "env_logger" 238 | version = "0.7.1" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 241 | dependencies = [ 242 | "atty", 243 | "humantime 1.3.0", 244 | "log", 245 | "regex", 246 | "termcolor", 247 | ] 248 | 249 | [[package]] 250 | name = "extend" 251 | version = "0.1.2" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "f47da3a72ec598d9c8937a7ebca8962a5c7a1f28444e38c2b33c771ba3f55f05" 254 | dependencies = [ 255 | "proc-macro-error", 256 | "proc-macro2", 257 | "quote", 258 | "syn", 259 | ] 260 | 261 | [[package]] 262 | name = "fastrand" 263 | version = "1.8.0" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 266 | dependencies = [ 267 | "instant", 268 | ] 269 | 270 | [[package]] 271 | name = "fnv" 272 | version = "1.0.7" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 275 | 276 | [[package]] 277 | name = "form_urlencoded" 278 | version = "1.0.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 281 | dependencies = [ 282 | "matches", 283 | "percent-encoding", 284 | ] 285 | 286 | [[package]] 287 | name = "fuchsia-cprng" 288 | version = "0.1.1" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 291 | 292 | [[package]] 293 | name = "futures" 294 | version = "0.3.24" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" 297 | dependencies = [ 298 | "futures-channel", 299 | "futures-core", 300 | "futures-executor", 301 | "futures-io", 302 | "futures-sink", 303 | "futures-task", 304 | "futures-util", 305 | ] 306 | 307 | [[package]] 308 | name = "futures-channel" 309 | version = "0.3.24" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" 312 | dependencies = [ 313 | "futures-core", 314 | "futures-sink", 315 | ] 316 | 317 | [[package]] 318 | name = "futures-core" 319 | version = "0.3.24" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" 322 | 323 | [[package]] 324 | name = "futures-executor" 325 | version = "0.3.24" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" 328 | dependencies = [ 329 | "futures-core", 330 | "futures-task", 331 | "futures-util", 332 | ] 333 | 334 | [[package]] 335 | name = "futures-io" 336 | version = "0.3.24" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" 339 | 340 | [[package]] 341 | name = "futures-macro" 342 | version = "0.3.24" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" 345 | dependencies = [ 346 | "proc-macro2", 347 | "quote", 348 | "syn", 349 | ] 350 | 351 | [[package]] 352 | name = "futures-sink" 353 | version = "0.3.24" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" 356 | 357 | [[package]] 358 | name = "futures-task" 359 | version = "0.3.24" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" 362 | 363 | [[package]] 364 | name = "futures-util" 365 | version = "0.3.24" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" 368 | dependencies = [ 369 | "futures-channel", 370 | "futures-core", 371 | "futures-io", 372 | "futures-macro", 373 | "futures-sink", 374 | "futures-task", 375 | "memchr", 376 | "pin-project-lite", 377 | "pin-utils", 378 | "slab", 379 | ] 380 | 381 | [[package]] 382 | name = "generic-array" 383 | version = "0.14.6" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 386 | dependencies = [ 387 | "typenum", 388 | "version_check", 389 | ] 390 | 391 | [[package]] 392 | name = "getrandom" 393 | version = "0.1.16" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 396 | dependencies = [ 397 | "cfg-if", 398 | "libc", 399 | "wasi 0.9.0+wasi-snapshot-preview1", 400 | ] 401 | 402 | [[package]] 403 | name = "getrandom" 404 | version = "0.2.7" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 407 | dependencies = [ 408 | "cfg-if", 409 | "libc", 410 | "wasi 0.11.0+wasi-snapshot-preview1", 411 | ] 412 | 413 | [[package]] 414 | name = "h2" 415 | version = "0.3.14" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" 418 | dependencies = [ 419 | "bytes", 420 | "fnv", 421 | "futures-core", 422 | "futures-sink", 423 | "futures-util", 424 | "http", 425 | "indexmap", 426 | "slab", 427 | "tokio", 428 | "tokio-util", 429 | "tracing", 430 | ] 431 | 432 | [[package]] 433 | name = "hashbrown" 434 | version = "0.12.3" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 437 | 438 | [[package]] 439 | name = "headers" 440 | version = "0.3.7" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" 443 | dependencies = [ 444 | "base64", 445 | "bitflags", 446 | "bytes", 447 | "headers-core", 448 | "http", 449 | "httpdate", 450 | "mime", 451 | "sha-1", 452 | ] 453 | 454 | [[package]] 455 | name = "headers-core" 456 | version = "0.2.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 459 | dependencies = [ 460 | "http", 461 | ] 462 | 463 | [[package]] 464 | name = "hermit-abi" 465 | version = "0.1.19" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 468 | dependencies = [ 469 | "libc", 470 | ] 471 | 472 | [[package]] 473 | name = "http" 474 | version = "0.2.8" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 477 | dependencies = [ 478 | "bytes", 479 | "fnv", 480 | "itoa 1.0.3", 481 | ] 482 | 483 | [[package]] 484 | name = "http-body" 485 | version = "0.4.5" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 488 | dependencies = [ 489 | "bytes", 490 | "http", 491 | "pin-project-lite", 492 | ] 493 | 494 | [[package]] 495 | name = "httparse" 496 | version = "1.8.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 499 | 500 | [[package]] 501 | name = "httpdate" 502 | version = "1.0.2" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 505 | 506 | [[package]] 507 | name = "humantime" 508 | version = "1.3.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 511 | dependencies = [ 512 | "quick-error", 513 | ] 514 | 515 | [[package]] 516 | name = "humantime" 517 | version = "2.1.0" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 520 | 521 | [[package]] 522 | name = "humantime-serde" 523 | version = "1.1.1" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" 526 | dependencies = [ 527 | "humantime 2.1.0", 528 | "serde", 529 | ] 530 | 531 | [[package]] 532 | name = "hyper" 533 | version = "0.14.20" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" 536 | dependencies = [ 537 | "bytes", 538 | "futures-channel", 539 | "futures-core", 540 | "futures-util", 541 | "h2", 542 | "http", 543 | "http-body", 544 | "httparse", 545 | "httpdate", 546 | "itoa 1.0.3", 547 | "pin-project-lite", 548 | "socket2", 549 | "tokio", 550 | "tower-service", 551 | "tracing", 552 | "want", 553 | ] 554 | 555 | [[package]] 556 | name = "hyper-rustls" 557 | version = "0.23.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" 560 | dependencies = [ 561 | "http", 562 | "hyper", 563 | "rustls", 564 | "tokio", 565 | "tokio-rustls", 566 | ] 567 | 568 | [[package]] 569 | name = "idna" 570 | version = "0.2.3" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 573 | dependencies = [ 574 | "matches", 575 | "unicode-bidi", 576 | "unicode-normalization", 577 | ] 578 | 579 | [[package]] 580 | name = "indexmap" 581 | version = "1.9.1" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 584 | dependencies = [ 585 | "autocfg 1.1.0", 586 | "hashbrown", 587 | ] 588 | 589 | [[package]] 590 | name = "instant" 591 | version = "0.1.12" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 594 | dependencies = [ 595 | "cfg-if", 596 | ] 597 | 598 | [[package]] 599 | name = "ipnet" 600 | version = "2.5.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" 603 | 604 | [[package]] 605 | name = "itertools" 606 | version = "0.10.3" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 609 | dependencies = [ 610 | "either", 611 | ] 612 | 613 | [[package]] 614 | name = "itoa" 615 | version = "0.4.8" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 618 | 619 | [[package]] 620 | name = "itoa" 621 | version = "1.0.3" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" 624 | 625 | [[package]] 626 | name = "js-sys" 627 | version = "0.3.59" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" 630 | dependencies = [ 631 | "wasm-bindgen", 632 | ] 633 | 634 | [[package]] 635 | name = "lazy_static" 636 | version = "1.4.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 639 | 640 | [[package]] 641 | name = "libc" 642 | version = "0.2.132" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" 645 | 646 | [[package]] 647 | name = "lock_api" 648 | version = "0.4.8" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" 651 | dependencies = [ 652 | "autocfg 1.1.0", 653 | "scopeguard", 654 | ] 655 | 656 | [[package]] 657 | name = "log" 658 | version = "0.4.17" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 661 | dependencies = [ 662 | "cfg-if", 663 | ] 664 | 665 | [[package]] 666 | name = "matchers" 667 | version = "0.1.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 670 | dependencies = [ 671 | "regex-automata", 672 | ] 673 | 674 | [[package]] 675 | name = "matches" 676 | version = "0.1.9" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 679 | 680 | [[package]] 681 | name = "memchr" 682 | version = "2.5.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 685 | 686 | [[package]] 687 | name = "mime" 688 | version = "0.3.16" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 691 | 692 | [[package]] 693 | name = "mime_guess" 694 | version = "2.0.4" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 697 | dependencies = [ 698 | "mime", 699 | "unicase", 700 | ] 701 | 702 | [[package]] 703 | name = "minimal-lexical" 704 | version = "0.2.1" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 707 | 708 | [[package]] 709 | name = "mio" 710 | version = "0.8.4" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" 713 | dependencies = [ 714 | "libc", 715 | "log", 716 | "wasi 0.11.0+wasi-snapshot-preview1", 717 | "windows-sys", 718 | ] 719 | 720 | [[package]] 721 | name = "mockito" 722 | version = "0.27.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "3a634720d366bcbce30fb05871a35da229cef101ad0b2ea4e46cf5abf031a273" 725 | dependencies = [ 726 | "assert-json-diff", 727 | "colored", 728 | "difference", 729 | "httparse", 730 | "lazy_static", 731 | "log", 732 | "rand 0.7.3", 733 | "regex", 734 | "serde_json", 735 | "serde_urlencoded 0.6.1", 736 | ] 737 | 738 | [[package]] 739 | name = "multipart" 740 | version = "0.18.0" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" 743 | dependencies = [ 744 | "buf_redux", 745 | "httparse", 746 | "log", 747 | "mime", 748 | "mime_guess", 749 | "quick-error", 750 | "rand 0.8.5", 751 | "safemem", 752 | "tempfile", 753 | "twoway", 754 | ] 755 | 756 | [[package]] 757 | name = "nanoid" 758 | version = "0.3.0" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "a6226bc4e142124cb44e309a37a04cd9bb10e740d8642855441d3b14808f635e" 761 | dependencies = [ 762 | "rand 0.6.5", 763 | ] 764 | 765 | [[package]] 766 | name = "nom" 767 | version = "7.1.1" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 770 | dependencies = [ 771 | "memchr", 772 | "minimal-lexical", 773 | ] 774 | 775 | [[package]] 776 | name = "num-traits" 777 | version = "0.2.15" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 780 | dependencies = [ 781 | "autocfg 1.1.0", 782 | ] 783 | 784 | [[package]] 785 | name = "num_cpus" 786 | version = "1.13.1" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 789 | dependencies = [ 790 | "hermit-abi", 791 | "libc", 792 | ] 793 | 794 | [[package]] 795 | name = "once_cell" 796 | version = "1.13.1" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" 799 | 800 | [[package]] 801 | name = "parking_lot" 802 | version = "0.11.2" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 805 | dependencies = [ 806 | "instant", 807 | "lock_api", 808 | "parking_lot_core", 809 | ] 810 | 811 | [[package]] 812 | name = "parking_lot_core" 813 | version = "0.8.5" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 816 | dependencies = [ 817 | "cfg-if", 818 | "instant", 819 | "libc", 820 | "redox_syscall", 821 | "smallvec", 822 | "winapi", 823 | ] 824 | 825 | [[package]] 826 | name = "paste" 827 | version = "1.0.9" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" 830 | 831 | [[package]] 832 | name = "percent-encoding" 833 | version = "2.1.0" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 836 | 837 | [[package]] 838 | name = "pin-project" 839 | version = "1.0.12" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" 842 | dependencies = [ 843 | "pin-project-internal", 844 | ] 845 | 846 | [[package]] 847 | name = "pin-project-internal" 848 | version = "1.0.12" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" 851 | dependencies = [ 852 | "proc-macro2", 853 | "quote", 854 | "syn", 855 | ] 856 | 857 | [[package]] 858 | name = "pin-project-lite" 859 | version = "0.2.9" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 862 | 863 | [[package]] 864 | name = "pin-utils" 865 | version = "0.1.0" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 868 | 869 | [[package]] 870 | name = "ppv-lite86" 871 | version = "0.2.16" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 874 | 875 | [[package]] 876 | name = "proc-macro-error" 877 | version = "1.0.4" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 880 | dependencies = [ 881 | "proc-macro-error-attr", 882 | "proc-macro2", 883 | "quote", 884 | "syn", 885 | "version_check", 886 | ] 887 | 888 | [[package]] 889 | name = "proc-macro-error-attr" 890 | version = "1.0.4" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 893 | dependencies = [ 894 | "proc-macro2", 895 | "quote", 896 | "version_check", 897 | ] 898 | 899 | [[package]] 900 | name = "proc-macro2" 901 | version = "1.0.43" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" 904 | dependencies = [ 905 | "unicode-ident", 906 | ] 907 | 908 | [[package]] 909 | name = "prometheus" 910 | version = "0.11.0" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "c8425533e7122f0c3cc7a37e6244b16ad3a2cc32ae7ac6276e2a75da0d9c200d" 913 | dependencies = [ 914 | "cfg-if", 915 | "fnv", 916 | "lazy_static", 917 | "parking_lot", 918 | "protobuf", 919 | "regex", 920 | "thiserror", 921 | ] 922 | 923 | [[package]] 924 | name = "protobuf" 925 | version = "2.27.1" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" 928 | 929 | [[package]] 930 | name = "quick-error" 931 | version = "1.2.3" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 934 | 935 | [[package]] 936 | name = "quote" 937 | version = "1.0.21" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 940 | dependencies = [ 941 | "proc-macro2", 942 | ] 943 | 944 | [[package]] 945 | name = "rand" 946 | version = "0.6.5" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" 949 | dependencies = [ 950 | "autocfg 0.1.8", 951 | "libc", 952 | "rand_chacha 0.1.1", 953 | "rand_core 0.4.2", 954 | "rand_hc 0.1.0", 955 | "rand_isaac", 956 | "rand_jitter", 957 | "rand_os", 958 | "rand_pcg", 959 | "rand_xorshift", 960 | "winapi", 961 | ] 962 | 963 | [[package]] 964 | name = "rand" 965 | version = "0.7.3" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 968 | dependencies = [ 969 | "getrandom 0.1.16", 970 | "libc", 971 | "rand_chacha 0.2.2", 972 | "rand_core 0.5.1", 973 | "rand_hc 0.2.0", 974 | ] 975 | 976 | [[package]] 977 | name = "rand" 978 | version = "0.8.5" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 981 | dependencies = [ 982 | "libc", 983 | "rand_chacha 0.3.1", 984 | "rand_core 0.6.3", 985 | ] 986 | 987 | [[package]] 988 | name = "rand_chacha" 989 | version = "0.1.1" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" 992 | dependencies = [ 993 | "autocfg 0.1.8", 994 | "rand_core 0.3.1", 995 | ] 996 | 997 | [[package]] 998 | name = "rand_chacha" 999 | version = "0.2.2" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 1002 | dependencies = [ 1003 | "ppv-lite86", 1004 | "rand_core 0.5.1", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "rand_chacha" 1009 | version = "0.3.1" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1012 | dependencies = [ 1013 | "ppv-lite86", 1014 | "rand_core 0.6.3", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "rand_core" 1019 | version = "0.3.1" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 1022 | dependencies = [ 1023 | "rand_core 0.4.2", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "rand_core" 1028 | version = "0.4.2" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 1031 | 1032 | [[package]] 1033 | name = "rand_core" 1034 | version = "0.5.1" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 1037 | dependencies = [ 1038 | "getrandom 0.1.16", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "rand_core" 1043 | version = "0.6.3" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 1046 | dependencies = [ 1047 | "getrandom 0.2.7", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "rand_hc" 1052 | version = "0.1.0" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" 1055 | dependencies = [ 1056 | "rand_core 0.3.1", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "rand_hc" 1061 | version = "0.2.0" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 1064 | dependencies = [ 1065 | "rand_core 0.5.1", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "rand_isaac" 1070 | version = "0.1.1" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" 1073 | dependencies = [ 1074 | "rand_core 0.3.1", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "rand_jitter" 1079 | version = "0.1.4" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" 1082 | dependencies = [ 1083 | "libc", 1084 | "rand_core 0.4.2", 1085 | "winapi", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "rand_os" 1090 | version = "0.1.3" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 1093 | dependencies = [ 1094 | "cloudabi", 1095 | "fuchsia-cprng", 1096 | "libc", 1097 | "rand_core 0.4.2", 1098 | "rdrand", 1099 | "winapi", 1100 | ] 1101 | 1102 | [[package]] 1103 | name = "rand_pcg" 1104 | version = "0.1.2" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" 1107 | dependencies = [ 1108 | "autocfg 0.1.8", 1109 | "rand_core 0.4.2", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "rand_xorshift" 1114 | version = "0.1.1" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" 1117 | dependencies = [ 1118 | "rand_core 0.3.1", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "rdrand" 1123 | version = "0.4.0" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 1126 | dependencies = [ 1127 | "rand_core 0.3.1", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "redox_syscall" 1132 | version = "0.2.16" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1135 | dependencies = [ 1136 | "bitflags", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "redust" 1141 | version = "0.3.0" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "38eae551d21bb844f4dd935e8ac68e66d5d6d3e0162dc7657e3797247a5f9a5c" 1144 | dependencies = [ 1145 | "async-trait", 1146 | "bytes", 1147 | "deadpool", 1148 | "futures", 1149 | "pin-project-lite", 1150 | "redust-resp 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 1151 | "serde", 1152 | "serde_bytes", 1153 | "tokio", 1154 | "tokio-util", 1155 | "tracing", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "redust" 1160 | version = "0.3.0" 1161 | source = "git+https://github.com/appellation/redust#45cba775b935211c01a8758e30834aa97442e9ea" 1162 | dependencies = [ 1163 | "async-trait", 1164 | "bytes", 1165 | "deadpool", 1166 | "futures", 1167 | "pin-project-lite", 1168 | "redust-resp 0.2.2 (git+https://github.com/appellation/redust)", 1169 | "serde", 1170 | "serde_bytes", 1171 | "tokio", 1172 | "tokio-util", 1173 | "tracing", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "redust-resp" 1178 | version = "0.2.2" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "bbdc631f8d6bf4c8329e621e6b2fcc262633a9587552120fc764cc15b2fe1dc6" 1181 | dependencies = [ 1182 | "bytes", 1183 | "itertools", 1184 | "nom", 1185 | "serde", 1186 | "serde_bytes", 1187 | "thiserror", 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "redust-resp" 1192 | version = "0.2.2" 1193 | source = "git+https://github.com/appellation/redust#45cba775b935211c01a8758e30834aa97442e9ea" 1194 | dependencies = [ 1195 | "bytes", 1196 | "itertools", 1197 | "nom", 1198 | "serde", 1199 | "serde_bytes", 1200 | "thiserror", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "regex" 1205 | version = "1.6.0" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 1208 | dependencies = [ 1209 | "aho-corasick", 1210 | "memchr", 1211 | "regex-syntax", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "regex-automata" 1216 | version = "0.1.10" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1219 | dependencies = [ 1220 | "regex-syntax", 1221 | ] 1222 | 1223 | [[package]] 1224 | name = "regex-syntax" 1225 | version = "0.6.27" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 1228 | 1229 | [[package]] 1230 | name = "remove_dir_all" 1231 | version = "0.5.3" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1234 | dependencies = [ 1235 | "winapi", 1236 | ] 1237 | 1238 | [[package]] 1239 | name = "reqwest" 1240 | version = "0.11.11" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" 1243 | dependencies = [ 1244 | "base64", 1245 | "bytes", 1246 | "encoding_rs", 1247 | "futures-core", 1248 | "futures-util", 1249 | "h2", 1250 | "http", 1251 | "http-body", 1252 | "hyper", 1253 | "hyper-rustls", 1254 | "ipnet", 1255 | "js-sys", 1256 | "lazy_static", 1257 | "log", 1258 | "mime", 1259 | "percent-encoding", 1260 | "pin-project-lite", 1261 | "rustls", 1262 | "rustls-pemfile 1.0.1", 1263 | "serde", 1264 | "serde_json", 1265 | "serde_urlencoded 0.7.1", 1266 | "tokio", 1267 | "tokio-rustls", 1268 | "tower-service", 1269 | "url", 1270 | "wasm-bindgen", 1271 | "wasm-bindgen-futures", 1272 | "web-sys", 1273 | "webpki-roots", 1274 | "winreg", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "retain_mut" 1279 | version = "0.1.9" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" 1282 | 1283 | [[package]] 1284 | name = "ring" 1285 | version = "0.16.20" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 1288 | dependencies = [ 1289 | "cc", 1290 | "libc", 1291 | "once_cell", 1292 | "spin", 1293 | "untrusted", 1294 | "web-sys", 1295 | "winapi", 1296 | ] 1297 | 1298 | [[package]] 1299 | name = "rmp" 1300 | version = "0.8.11" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" 1303 | dependencies = [ 1304 | "byteorder", 1305 | "num-traits", 1306 | "paste", 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "rmp-serde" 1311 | version = "0.14.4" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "4ce7d70c926fe472aed493b902010bccc17fa9f7284145cb8772fd22fdb052d8" 1314 | dependencies = [ 1315 | "byteorder", 1316 | "rmp", 1317 | "serde", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "rmp-serde" 1322 | version = "0.15.5" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "723ecff9ad04f4ad92fe1c8ca6c20d2196d9286e9c60727c4cb5511629260e9d" 1325 | dependencies = [ 1326 | "byteorder", 1327 | "rmp", 1328 | "serde", 1329 | ] 1330 | 1331 | [[package]] 1332 | name = "rustacles-brokers" 1333 | version = "0.2.0" 1334 | source = "git+https://github.com/spec-tacles/rustacles?rev=73bfe08#73bfe081b85ce19eefbfebdb3b06c2a6a9903d91" 1335 | dependencies = [ 1336 | "async-trait", 1337 | "bytes", 1338 | "env_logger", 1339 | "futures", 1340 | "nanoid", 1341 | "pin-project", 1342 | "redust 0.3.0 (git+https://github.com/appellation/redust)", 1343 | "rmp-serde 0.15.5", 1344 | "serde", 1345 | "serde_bytes", 1346 | "thiserror", 1347 | "tokio", 1348 | "tokio-stream", 1349 | "tracing", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "rustls" 1354 | version = "0.20.6" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" 1357 | dependencies = [ 1358 | "log", 1359 | "ring", 1360 | "sct", 1361 | "webpki", 1362 | ] 1363 | 1364 | [[package]] 1365 | name = "rustls-pemfile" 1366 | version = "0.2.1" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" 1369 | dependencies = [ 1370 | "base64", 1371 | ] 1372 | 1373 | [[package]] 1374 | name = "rustls-pemfile" 1375 | version = "1.0.1" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" 1378 | dependencies = [ 1379 | "base64", 1380 | ] 1381 | 1382 | [[package]] 1383 | name = "ryu" 1384 | version = "1.0.11" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 1387 | 1388 | [[package]] 1389 | name = "safemem" 1390 | version = "0.3.3" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 1393 | 1394 | [[package]] 1395 | name = "scoped-tls" 1396 | version = "1.0.0" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" 1399 | 1400 | [[package]] 1401 | name = "scopeguard" 1402 | version = "1.1.0" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1405 | 1406 | [[package]] 1407 | name = "sct" 1408 | version = "0.7.0" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 1411 | dependencies = [ 1412 | "ring", 1413 | "untrusted", 1414 | ] 1415 | 1416 | [[package]] 1417 | name = "serde" 1418 | version = "1.0.144" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" 1421 | dependencies = [ 1422 | "serde_derive", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "serde_bytes" 1427 | version = "0.11.7" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" 1430 | dependencies = [ 1431 | "serde", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "serde_derive" 1436 | version = "1.0.144" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" 1439 | dependencies = [ 1440 | "proc-macro2", 1441 | "quote", 1442 | "syn", 1443 | ] 1444 | 1445 | [[package]] 1446 | name = "serde_json" 1447 | version = "1.0.85" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" 1450 | dependencies = [ 1451 | "itoa 1.0.3", 1452 | "ryu", 1453 | "serde", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "serde_repr" 1458 | version = "0.1.9" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" 1461 | dependencies = [ 1462 | "proc-macro2", 1463 | "quote", 1464 | "syn", 1465 | ] 1466 | 1467 | [[package]] 1468 | name = "serde_urlencoded" 1469 | version = "0.6.1" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" 1472 | dependencies = [ 1473 | "dtoa", 1474 | "itoa 0.4.8", 1475 | "serde", 1476 | "url", 1477 | ] 1478 | 1479 | [[package]] 1480 | name = "serde_urlencoded" 1481 | version = "0.7.1" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1484 | dependencies = [ 1485 | "form_urlencoded", 1486 | "itoa 1.0.3", 1487 | "ryu", 1488 | "serde", 1489 | ] 1490 | 1491 | [[package]] 1492 | name = "sha-1" 1493 | version = "0.10.0" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" 1496 | dependencies = [ 1497 | "cfg-if", 1498 | "cpufeatures", 1499 | "digest", 1500 | ] 1501 | 1502 | [[package]] 1503 | name = "sharded-slab" 1504 | version = "0.1.4" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 1507 | dependencies = [ 1508 | "lazy_static", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "slab" 1513 | version = "0.4.7" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 1516 | dependencies = [ 1517 | "autocfg 1.1.0", 1518 | ] 1519 | 1520 | [[package]] 1521 | name = "smallvec" 1522 | version = "1.9.0" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 1525 | 1526 | [[package]] 1527 | name = "socket2" 1528 | version = "0.4.7" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 1531 | dependencies = [ 1532 | "libc", 1533 | "winapi", 1534 | ] 1535 | 1536 | [[package]] 1537 | name = "spectacles-proxy" 1538 | version = "0.1.0" 1539 | dependencies = [ 1540 | "anyhow", 1541 | "async-trait", 1542 | "bytes", 1543 | "futures", 1544 | "http", 1545 | "humantime 2.1.0", 1546 | "humantime-serde", 1547 | "lazy_static", 1548 | "mockito", 1549 | "prometheus", 1550 | "redust 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 1551 | "reqwest", 1552 | "rmp-serde 0.14.4", 1553 | "rustacles-brokers", 1554 | "serde", 1555 | "serde_repr", 1556 | "test-log", 1557 | "tokio", 1558 | "tokio-stream", 1559 | "toml", 1560 | "tracing", 1561 | "tracing-subscriber", 1562 | "uriparse", 1563 | "warp", 1564 | ] 1565 | 1566 | [[package]] 1567 | name = "spin" 1568 | version = "0.5.2" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1571 | 1572 | [[package]] 1573 | name = "syn" 1574 | version = "1.0.99" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" 1577 | dependencies = [ 1578 | "proc-macro2", 1579 | "quote", 1580 | "unicode-ident", 1581 | ] 1582 | 1583 | [[package]] 1584 | name = "tempfile" 1585 | version = "3.3.0" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 1588 | dependencies = [ 1589 | "cfg-if", 1590 | "fastrand", 1591 | "libc", 1592 | "redox_syscall", 1593 | "remove_dir_all", 1594 | "winapi", 1595 | ] 1596 | 1597 | [[package]] 1598 | name = "termcolor" 1599 | version = "1.1.3" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 1602 | dependencies = [ 1603 | "winapi-util", 1604 | ] 1605 | 1606 | [[package]] 1607 | name = "test-log" 1608 | version = "0.2.11" 1609 | source = "registry+https://github.com/rust-lang/crates.io-index" 1610 | checksum = "38f0c854faeb68a048f0f2dc410c5ddae3bf83854ef0e4977d58306a5edef50e" 1611 | dependencies = [ 1612 | "proc-macro2", 1613 | "quote", 1614 | "syn", 1615 | ] 1616 | 1617 | [[package]] 1618 | name = "thiserror" 1619 | version = "1.0.33" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57" 1622 | dependencies = [ 1623 | "thiserror-impl", 1624 | ] 1625 | 1626 | [[package]] 1627 | name = "thiserror-impl" 1628 | version = "1.0.33" 1629 | source = "registry+https://github.com/rust-lang/crates.io-index" 1630 | checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09" 1631 | dependencies = [ 1632 | "proc-macro2", 1633 | "quote", 1634 | "syn", 1635 | ] 1636 | 1637 | [[package]] 1638 | name = "thread_local" 1639 | version = "1.1.4" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 1642 | dependencies = [ 1643 | "once_cell", 1644 | ] 1645 | 1646 | [[package]] 1647 | name = "tinyvec" 1648 | version = "1.6.0" 1649 | source = "registry+https://github.com/rust-lang/crates.io-index" 1650 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1651 | dependencies = [ 1652 | "tinyvec_macros", 1653 | ] 1654 | 1655 | [[package]] 1656 | name = "tinyvec_macros" 1657 | version = "0.1.0" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1660 | 1661 | [[package]] 1662 | name = "tokio" 1663 | version = "1.20.1" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" 1666 | dependencies = [ 1667 | "autocfg 1.1.0", 1668 | "bytes", 1669 | "libc", 1670 | "memchr", 1671 | "mio", 1672 | "num_cpus", 1673 | "once_cell", 1674 | "pin-project-lite", 1675 | "socket2", 1676 | "tokio-macros", 1677 | "winapi", 1678 | ] 1679 | 1680 | [[package]] 1681 | name = "tokio-macros" 1682 | version = "1.8.0" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" 1685 | dependencies = [ 1686 | "proc-macro2", 1687 | "quote", 1688 | "syn", 1689 | ] 1690 | 1691 | [[package]] 1692 | name = "tokio-rustls" 1693 | version = "0.23.4" 1694 | source = "registry+https://github.com/rust-lang/crates.io-index" 1695 | checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" 1696 | dependencies = [ 1697 | "rustls", 1698 | "tokio", 1699 | "webpki", 1700 | ] 1701 | 1702 | [[package]] 1703 | name = "tokio-stream" 1704 | version = "0.1.9" 1705 | source = "registry+https://github.com/rust-lang/crates.io-index" 1706 | checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" 1707 | dependencies = [ 1708 | "futures-core", 1709 | "pin-project-lite", 1710 | "tokio", 1711 | "tokio-util", 1712 | ] 1713 | 1714 | [[package]] 1715 | name = "tokio-tungstenite" 1716 | version = "0.17.2" 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" 1718 | checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" 1719 | dependencies = [ 1720 | "futures-util", 1721 | "log", 1722 | "tokio", 1723 | "tungstenite", 1724 | ] 1725 | 1726 | [[package]] 1727 | name = "tokio-util" 1728 | version = "0.7.3" 1729 | source = "registry+https://github.com/rust-lang/crates.io-index" 1730 | checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" 1731 | dependencies = [ 1732 | "bytes", 1733 | "futures-core", 1734 | "futures-sink", 1735 | "pin-project-lite", 1736 | "tokio", 1737 | "tracing", 1738 | ] 1739 | 1740 | [[package]] 1741 | name = "toml" 1742 | version = "0.5.9" 1743 | source = "registry+https://github.com/rust-lang/crates.io-index" 1744 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" 1745 | dependencies = [ 1746 | "serde", 1747 | ] 1748 | 1749 | [[package]] 1750 | name = "tower-service" 1751 | version = "0.3.2" 1752 | source = "registry+https://github.com/rust-lang/crates.io-index" 1753 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1754 | 1755 | [[package]] 1756 | name = "tracing" 1757 | version = "0.1.36" 1758 | source = "registry+https://github.com/rust-lang/crates.io-index" 1759 | checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" 1760 | dependencies = [ 1761 | "cfg-if", 1762 | "log", 1763 | "pin-project-lite", 1764 | "tracing-attributes", 1765 | "tracing-core", 1766 | ] 1767 | 1768 | [[package]] 1769 | name = "tracing-attributes" 1770 | version = "0.1.22" 1771 | source = "registry+https://github.com/rust-lang/crates.io-index" 1772 | checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" 1773 | dependencies = [ 1774 | "proc-macro2", 1775 | "quote", 1776 | "syn", 1777 | ] 1778 | 1779 | [[package]] 1780 | name = "tracing-core" 1781 | version = "0.1.29" 1782 | source = "registry+https://github.com/rust-lang/crates.io-index" 1783 | checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" 1784 | dependencies = [ 1785 | "once_cell", 1786 | "valuable", 1787 | ] 1788 | 1789 | [[package]] 1790 | name = "tracing-log" 1791 | version = "0.1.3" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 1794 | dependencies = [ 1795 | "lazy_static", 1796 | "log", 1797 | "tracing-core", 1798 | ] 1799 | 1800 | [[package]] 1801 | name = "tracing-subscriber" 1802 | version = "0.3.15" 1803 | source = "registry+https://github.com/rust-lang/crates.io-index" 1804 | checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" 1805 | dependencies = [ 1806 | "ansi_term", 1807 | "matchers", 1808 | "once_cell", 1809 | "regex", 1810 | "sharded-slab", 1811 | "smallvec", 1812 | "thread_local", 1813 | "tracing", 1814 | "tracing-core", 1815 | "tracing-log", 1816 | ] 1817 | 1818 | [[package]] 1819 | name = "try-lock" 1820 | version = "0.2.3" 1821 | source = "registry+https://github.com/rust-lang/crates.io-index" 1822 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1823 | 1824 | [[package]] 1825 | name = "tungstenite" 1826 | version = "0.17.3" 1827 | source = "registry+https://github.com/rust-lang/crates.io-index" 1828 | checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" 1829 | dependencies = [ 1830 | "base64", 1831 | "byteorder", 1832 | "bytes", 1833 | "http", 1834 | "httparse", 1835 | "log", 1836 | "rand 0.8.5", 1837 | "sha-1", 1838 | "thiserror", 1839 | "url", 1840 | "utf-8", 1841 | ] 1842 | 1843 | [[package]] 1844 | name = "twoway" 1845 | version = "0.1.8" 1846 | source = "registry+https://github.com/rust-lang/crates.io-index" 1847 | checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" 1848 | dependencies = [ 1849 | "memchr", 1850 | ] 1851 | 1852 | [[package]] 1853 | name = "typenum" 1854 | version = "1.15.0" 1855 | source = "registry+https://github.com/rust-lang/crates.io-index" 1856 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 1857 | 1858 | [[package]] 1859 | name = "unicase" 1860 | version = "2.6.0" 1861 | source = "registry+https://github.com/rust-lang/crates.io-index" 1862 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1863 | dependencies = [ 1864 | "version_check", 1865 | ] 1866 | 1867 | [[package]] 1868 | name = "unicode-bidi" 1869 | version = "0.3.8" 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" 1871 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 1872 | 1873 | [[package]] 1874 | name = "unicode-ident" 1875 | version = "1.0.3" 1876 | source = "registry+https://github.com/rust-lang/crates.io-index" 1877 | checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" 1878 | 1879 | [[package]] 1880 | name = "unicode-normalization" 1881 | version = "0.1.21" 1882 | source = "registry+https://github.com/rust-lang/crates.io-index" 1883 | checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" 1884 | dependencies = [ 1885 | "tinyvec", 1886 | ] 1887 | 1888 | [[package]] 1889 | name = "untrusted" 1890 | version = "0.7.1" 1891 | source = "registry+https://github.com/rust-lang/crates.io-index" 1892 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1893 | 1894 | [[package]] 1895 | name = "uriparse" 1896 | version = "0.6.4" 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" 1898 | checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" 1899 | dependencies = [ 1900 | "fnv", 1901 | "lazy_static", 1902 | ] 1903 | 1904 | [[package]] 1905 | name = "url" 1906 | version = "2.2.2" 1907 | source = "registry+https://github.com/rust-lang/crates.io-index" 1908 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1909 | dependencies = [ 1910 | "form_urlencoded", 1911 | "idna", 1912 | "matches", 1913 | "percent-encoding", 1914 | ] 1915 | 1916 | [[package]] 1917 | name = "utf-8" 1918 | version = "0.7.6" 1919 | source = "registry+https://github.com/rust-lang/crates.io-index" 1920 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1921 | 1922 | [[package]] 1923 | name = "valuable" 1924 | version = "0.1.0" 1925 | source = "registry+https://github.com/rust-lang/crates.io-index" 1926 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1927 | 1928 | [[package]] 1929 | name = "version_check" 1930 | version = "0.9.4" 1931 | source = "registry+https://github.com/rust-lang/crates.io-index" 1932 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1933 | 1934 | [[package]] 1935 | name = "want" 1936 | version = "0.3.0" 1937 | source = "registry+https://github.com/rust-lang/crates.io-index" 1938 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1939 | dependencies = [ 1940 | "log", 1941 | "try-lock", 1942 | ] 1943 | 1944 | [[package]] 1945 | name = "warp" 1946 | version = "0.3.2" 1947 | source = "git+https://github.com/seanmonstar/warp.git#3cf17837c40461316a53f91468c30972cf7c7d1c" 1948 | dependencies = [ 1949 | "bytes", 1950 | "futures-channel", 1951 | "futures-util", 1952 | "headers", 1953 | "http", 1954 | "hyper", 1955 | "log", 1956 | "mime", 1957 | "mime_guess", 1958 | "multipart", 1959 | "percent-encoding", 1960 | "pin-project", 1961 | "rustls-pemfile 0.2.1", 1962 | "scoped-tls", 1963 | "serde", 1964 | "serde_json", 1965 | "serde_urlencoded 0.7.1", 1966 | "tokio", 1967 | "tokio-stream", 1968 | "tokio-tungstenite", 1969 | "tokio-util", 1970 | "tower-service", 1971 | "tracing", 1972 | ] 1973 | 1974 | [[package]] 1975 | name = "wasi" 1976 | version = "0.9.0+wasi-snapshot-preview1" 1977 | source = "registry+https://github.com/rust-lang/crates.io-index" 1978 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 1979 | 1980 | [[package]] 1981 | name = "wasi" 1982 | version = "0.11.0+wasi-snapshot-preview1" 1983 | source = "registry+https://github.com/rust-lang/crates.io-index" 1984 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1985 | 1986 | [[package]] 1987 | name = "wasm-bindgen" 1988 | version = "0.2.82" 1989 | source = "registry+https://github.com/rust-lang/crates.io-index" 1990 | checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" 1991 | dependencies = [ 1992 | "cfg-if", 1993 | "wasm-bindgen-macro", 1994 | ] 1995 | 1996 | [[package]] 1997 | name = "wasm-bindgen-backend" 1998 | version = "0.2.82" 1999 | source = "registry+https://github.com/rust-lang/crates.io-index" 2000 | checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" 2001 | dependencies = [ 2002 | "bumpalo", 2003 | "log", 2004 | "once_cell", 2005 | "proc-macro2", 2006 | "quote", 2007 | "syn", 2008 | "wasm-bindgen-shared", 2009 | ] 2010 | 2011 | [[package]] 2012 | name = "wasm-bindgen-futures" 2013 | version = "0.4.32" 2014 | source = "registry+https://github.com/rust-lang/crates.io-index" 2015 | checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" 2016 | dependencies = [ 2017 | "cfg-if", 2018 | "js-sys", 2019 | "wasm-bindgen", 2020 | "web-sys", 2021 | ] 2022 | 2023 | [[package]] 2024 | name = "wasm-bindgen-macro" 2025 | version = "0.2.82" 2026 | source = "registry+https://github.com/rust-lang/crates.io-index" 2027 | checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" 2028 | dependencies = [ 2029 | "quote", 2030 | "wasm-bindgen-macro-support", 2031 | ] 2032 | 2033 | [[package]] 2034 | name = "wasm-bindgen-macro-support" 2035 | version = "0.2.82" 2036 | source = "registry+https://github.com/rust-lang/crates.io-index" 2037 | checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" 2038 | dependencies = [ 2039 | "proc-macro2", 2040 | "quote", 2041 | "syn", 2042 | "wasm-bindgen-backend", 2043 | "wasm-bindgen-shared", 2044 | ] 2045 | 2046 | [[package]] 2047 | name = "wasm-bindgen-shared" 2048 | version = "0.2.82" 2049 | source = "registry+https://github.com/rust-lang/crates.io-index" 2050 | checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" 2051 | 2052 | [[package]] 2053 | name = "web-sys" 2054 | version = "0.3.59" 2055 | source = "registry+https://github.com/rust-lang/crates.io-index" 2056 | checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" 2057 | dependencies = [ 2058 | "js-sys", 2059 | "wasm-bindgen", 2060 | ] 2061 | 2062 | [[package]] 2063 | name = "webpki" 2064 | version = "0.22.0" 2065 | source = "registry+https://github.com/rust-lang/crates.io-index" 2066 | checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" 2067 | dependencies = [ 2068 | "ring", 2069 | "untrusted", 2070 | ] 2071 | 2072 | [[package]] 2073 | name = "webpki-roots" 2074 | version = "0.22.4" 2075 | source = "registry+https://github.com/rust-lang/crates.io-index" 2076 | checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" 2077 | dependencies = [ 2078 | "webpki", 2079 | ] 2080 | 2081 | [[package]] 2082 | name = "winapi" 2083 | version = "0.3.9" 2084 | source = "registry+https://github.com/rust-lang/crates.io-index" 2085 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2086 | dependencies = [ 2087 | "winapi-i686-pc-windows-gnu", 2088 | "winapi-x86_64-pc-windows-gnu", 2089 | ] 2090 | 2091 | [[package]] 2092 | name = "winapi-i686-pc-windows-gnu" 2093 | version = "0.4.0" 2094 | source = "registry+https://github.com/rust-lang/crates.io-index" 2095 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2096 | 2097 | [[package]] 2098 | name = "winapi-util" 2099 | version = "0.1.5" 2100 | source = "registry+https://github.com/rust-lang/crates.io-index" 2101 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 2102 | dependencies = [ 2103 | "winapi", 2104 | ] 2105 | 2106 | [[package]] 2107 | name = "winapi-x86_64-pc-windows-gnu" 2108 | version = "0.4.0" 2109 | source = "registry+https://github.com/rust-lang/crates.io-index" 2110 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2111 | 2112 | [[package]] 2113 | name = "windows-sys" 2114 | version = "0.36.1" 2115 | source = "registry+https://github.com/rust-lang/crates.io-index" 2116 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 2117 | dependencies = [ 2118 | "windows_aarch64_msvc", 2119 | "windows_i686_gnu", 2120 | "windows_i686_msvc", 2121 | "windows_x86_64_gnu", 2122 | "windows_x86_64_msvc", 2123 | ] 2124 | 2125 | [[package]] 2126 | name = "windows_aarch64_msvc" 2127 | version = "0.36.1" 2128 | source = "registry+https://github.com/rust-lang/crates.io-index" 2129 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 2130 | 2131 | [[package]] 2132 | name = "windows_i686_gnu" 2133 | version = "0.36.1" 2134 | source = "registry+https://github.com/rust-lang/crates.io-index" 2135 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 2136 | 2137 | [[package]] 2138 | name = "windows_i686_msvc" 2139 | version = "0.36.1" 2140 | source = "registry+https://github.com/rust-lang/crates.io-index" 2141 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 2142 | 2143 | [[package]] 2144 | name = "windows_x86_64_gnu" 2145 | version = "0.36.1" 2146 | source = "registry+https://github.com/rust-lang/crates.io-index" 2147 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 2148 | 2149 | [[package]] 2150 | name = "windows_x86_64_msvc" 2151 | version = "0.36.1" 2152 | source = "registry+https://github.com/rust-lang/crates.io-index" 2153 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 2154 | 2155 | [[package]] 2156 | name = "winreg" 2157 | version = "0.10.1" 2158 | source = "registry+https://github.com/rust-lang/crates.io-index" 2159 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 2160 | dependencies = [ 2161 | "winapi", 2162 | ] 2163 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spectacles-proxy" 3 | version = "0.1.0" 4 | authors = ["Will Nelson "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [[bin]] 10 | name = "proxy" 11 | path = "src/main.rs" 12 | 13 | [dependencies] 14 | anyhow = "1.0" 15 | async-trait = "0.1" 16 | bytes = { version = "1.0", features = ["serde"] } 17 | futures = "0.3" 18 | http = "0.2" 19 | humantime = "2.0" 20 | humantime-serde = "1.0" 21 | lazy_static = "1.4" 22 | prometheus = { version = "0.11", optional = true } 23 | redust = { version = "0.3", features = ["script", "model", "pool"] } 24 | rmp-serde = "0.14" 25 | serde = "1.0" 26 | serde_repr = "0.1" 27 | tokio-stream = "0.1" 28 | toml = "0.5" 29 | tracing = "0.1" 30 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 31 | uriparse = "0.6" 32 | 33 | [dependencies.rustacles-brokers] 34 | git = "https://github.com/spec-tacles/rustacles" 35 | rev = "73bfe08" 36 | features = ["redis-broker"] 37 | 38 | [dependencies.tokio] 39 | version = "1.0" 40 | features = ["rt-multi-thread", "time", "macros"] 41 | 42 | [dependencies.reqwest] 43 | version = "0.11" 44 | features = ["rustls-tls"] 45 | default-features = false 46 | 47 | [dependencies.warp] 48 | git = "https://github.com/seanmonstar/warp.git" 49 | optional = true 50 | 51 | [features] 52 | default = [] 53 | redis-ratelimiter = [] 54 | metrics = ["prometheus", "warp"] 55 | 56 | [dev-dependencies] 57 | mockito = "0.27" 58 | test-log = { version = "0.2", default-features = false, features = ["trace"] } 59 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:alpine AS build 2 | WORKDIR /usr/src/proxy 3 | RUN apk add openssl-dev musl-dev 4 | COPY . . 5 | RUN cargo install --path . --features redis-ratelimiter 6 | 7 | FROM scratch 8 | COPY --from=build /usr/local/cargo/bin/proxy /proxy 9 | CMD ["/proxy"] 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spectacles Proxy 2 | 3 | The Spectacles proxy is responsible for handling all of the data interactions with Discord. 4 | 5 | - [x] Ratelimited REST calls to Discord 6 | - [ ] Caching REST responses 7 | - [ ] Caching ingested data from the [gateway](https://github.com/spec-tacles/gateway) 8 | 9 | ## Usage 10 | 11 | The proxy communicates with other services using the [Spectacles spec](https://github.com/spec-tacles/spec) over Redis. 12 | 13 | - JS: [`brokers.js`](https://github.com/spec-tacles/brokers.js) 14 | - C#: [`Spectacles.NET`](https://github.com/spec-tacles/Spectacles.NET) 15 | - Go: [`spec-tacles/go`](https://github.com/spec-tacles/go) 16 | - Rust: [`rustacles`](https://github.com/spec-tacles/rustacles) 17 | 18 | The proxy can be configured with the following options. This file must be called `proxy.toml` and exist in the CWD. Alternatively, specify the values in the environment variables named adjacently. The following example contains the default values. 19 | 20 | ```toml 21 | timeout = "" # TIMEOUT 22 | 23 | [broker] 24 | group = "proxy" # BROKER_GROUP 25 | event = "REQUEST" # BROKER_EVENT 26 | 27 | [redis] 28 | url = "localhost:6379" # REDIS_URL 29 | pool_size = 32 # REDIS_POOL_SIZE 30 | 31 | [discord] 32 | api_version = 10 # DISCORD_API_VERSION 33 | 34 | [metrics] 35 | # addr = "0.0.0.0:3000" # METRICS_ADDR 36 | # path = "metrics" # METRICS_PATH 37 | ``` 38 | 39 | ### Timeout 40 | 41 | The timeout is a human-readable duration (e.g. 2min). It applies for the entire duration of the request, including time paused for ratelimiting. Once the timeout occurs, the proxy will attempt to stop the request; however, it's possible for the data to be sent to Discord and the timeout to occur during the response, meaning that your client will receive the error but the request will have succeeded. This is done to protect against indefinitely hung requests in case Discord doesn't respond. 42 | 43 | ### Request Format 44 | 45 | Requests can be made by publishing on the specified event to the specified group. The data must be serialized in MessagePack format. 46 | 47 | ```json 48 | { 49 | "method": "GET", 50 | "path": "/users/1234", 51 | "query": { 52 | "foo": "bar" 53 | }, 54 | "body": [], 55 | "headers": { 56 | "def": "uvw" 57 | } 58 | } 59 | ``` 60 | 61 | `query`, `body`, and `headers` are optional. Body must be binary data. 62 | 63 | ### Response Format 64 | 65 | The response is returned on the callback queue in the following MessagePack format. 66 | 67 | ```json 68 | { 69 | "status": 0, 70 | "body": ... 71 | } 72 | ``` 73 | 74 | #### Response Status 75 | 76 | **Status**|**Description** 77 | -----:|----- 78 | 0|Success 79 | 1|Unknown error 80 | 2|Invalid request format (non-JSON) 81 | 3|Invalid URL path 82 | 4|Invalid URL query 83 | 5|Invalid HTTP method 84 | 6|Invalid HTTP headers 85 | 7|Request failure 86 | 8|Request timeout 87 | 88 | #### Response Body 89 | 90 | For a successful call (status 0), the body will be an object representing the HTTP response: 91 | 92 | ```json 93 | { 94 | "status": 200, 95 | "headers": { 96 | "foo": "bar" 97 | }, 98 | "url": "https://discord.com/api/v6/users/4567", 99 | "body": [] 100 | } 101 | ``` 102 | 103 | `url` represents the full, final URL of the request. `body` is the binary response body from the server. 104 | 105 | For an unsuccessful status code (non-zero status), the body will be a string describing the error. 106 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "metrics")] 2 | pub mod metrics; 3 | pub mod models; 4 | pub mod ratelimiter; 5 | pub mod route; 6 | pub mod runtime; 7 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | #[cfg(not(feature = "redis-ratelimiter"))] 3 | use spectacles_proxy::ratelimiter::local::LocalRatelimiter; 4 | #[cfg(feature = "redis-ratelimiter")] 5 | use spectacles_proxy::ratelimiter::redis::RedisRatelimiter; 6 | #[cfg(feature = "metrics")] 7 | use spectacles_proxy::runtime::metrics::start_server; 8 | use spectacles_proxy::{ 9 | ratelimiter::Ratelimiter, 10 | runtime::{Client, Config}, 11 | }; 12 | use tokio::spawn; 13 | use tracing::info; 14 | use tracing_subscriber::EnvFilter; 15 | use uriparse::Scheme; 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<()> { 19 | tracing_subscriber::fmt() 20 | .with_env_filter(EnvFilter::from_default_env()) 21 | .init(); 22 | 23 | let config = Config::from_toml_file("proxy.toml") 24 | .unwrap_or_default() 25 | .with_env(); 26 | 27 | let broker = config.new_broker(); 28 | 29 | let ratelimiter = get_ratelimiter(&config); 30 | let client = Client { 31 | http: reqwest::Client::new(), 32 | ratelimiter, 33 | api_base: "discord.com".to_string(), 34 | api_scheme: Scheme::HTTPS, 35 | api_version: config.discord.api_version, 36 | timeout: config.timeout.map(|d| d.into()), 37 | }; 38 | 39 | #[cfg(feature = "metrics")] 40 | if let Some(ref config) = config.metrics { 41 | info!("Launching metrics server"); 42 | spawn(start_server(config.path.clone(), config.addr)); 43 | } 44 | 45 | let events = vec![config.broker.event.into()]; 46 | broker.ensure_events(events.iter()).await?; 47 | 48 | info!("Beginning normal message consumption"); 49 | client.consume_stream(broker.consume(events)).await?; 50 | 51 | Ok(()) 52 | } 53 | 54 | #[cfg(feature = "redis-ratelimiter")] 55 | fn get_ratelimiter(config: &Config) -> impl Ratelimiter + Clone { 56 | let manager = redust::pool::Manager::new(config.redis.url.clone()); 57 | let pool = redust::pool::Pool::builder(manager) 58 | .max_size(config.redis.pool_size) 59 | .build() 60 | .expect("Unable to connect to Redis"); 61 | 62 | RedisRatelimiter::new(pool.clone()) 63 | } 64 | 65 | #[cfg(not(feature = "redis-ratelimiter"))] 66 | fn get_ratelimiter(_config: &Config) -> impl Ratelimiter + Clone { 67 | LocalRatelimiter::default() 68 | } 69 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use prometheus::{register_histogram_vec, register_int_counter_vec, HistogramVec, IntCounterVec}; 3 | 4 | lazy_static! { 5 | pub static ref REQUESTS_TOTAL: IntCounterVec = register_int_counter_vec!( 6 | "proxy_requests_total", 7 | "Number of HTTP requests made", 8 | &["method", "path"] 9 | ) 10 | .unwrap(); 11 | pub static ref RESPONSES_TOTAL: IntCounterVec = register_int_counter_vec!( 12 | "proxy_responses_total", 13 | "Number of HTTP responses received", 14 | &["method", "path", "status"] 15 | ) 16 | .unwrap(); 17 | pub static ref REQUEST_LATENCY: HistogramVec = register_histogram_vec!( 18 | "proxy_request_latency", 19 | "Latency of HTTP requests (in seconds)", 20 | &["method", "path"] 21 | ) 22 | .unwrap(); 23 | pub static ref RATELIMIT_LATENCY: HistogramVec = register_histogram_vec!( 24 | "proxy_ratelimit_latency", 25 | "Latency of ratelimit checking, including wait time for any ratelimited requests.", 26 | &["method", "path"] 27 | ) 28 | .unwrap(); 29 | } 30 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bytes::Bytes; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_repr::*; 5 | use std::{ 6 | collections::HashMap, 7 | fmt::{self, Display, Formatter}, 8 | }; 9 | use tokio::time::{error::Elapsed, Duration}; 10 | 11 | #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] 12 | pub struct SerializableHttpRequest { 13 | pub method: String, 14 | pub path: String, 15 | pub query: Option>, 16 | pub body: Option, 17 | #[serde(default)] 18 | pub headers: HashMap, 19 | pub timeout: Option, 20 | } 21 | 22 | impl Display for SerializableHttpRequest { 23 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 24 | write!( 25 | f, 26 | "{} {} Query={:?} Headers={:?} BodyLen={:?} Timeout={:?}ms", 27 | self.method, 28 | self.path, 29 | self.query, 30 | self.headers, 31 | self.body.as_ref().map(|b| b.len()), 32 | self.timeout.map(|d| d.as_millis()) 33 | ) 34 | } 35 | } 36 | 37 | #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] 38 | pub struct SerializableHttpResponse { 39 | pub status: u16, 40 | pub headers: HashMap, 41 | pub url: String, 42 | pub body: Bytes, 43 | } 44 | 45 | impl Display for SerializableHttpResponse { 46 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 47 | write!( 48 | f, 49 | "{} {} Headers={:?} BodyLen={}", 50 | self.status, 51 | self.url, 52 | self.headers, 53 | self.body.len() 54 | ) 55 | } 56 | } 57 | 58 | #[repr(u8)] 59 | #[derive(Debug, Serialize_repr, Deserialize_repr, Eq, PartialEq)] 60 | pub enum ResponseStatus { 61 | Success, 62 | Unknown, 63 | InvalidRequestFormat, 64 | InvalidPath, 65 | InvalidQuery, 66 | InvalidMethod, 67 | InvalidHeaders, 68 | RequestFailure, 69 | RequestTimeout, 70 | } 71 | 72 | impl From<&(dyn std::error::Error + 'static)> for ResponseStatus { 73 | fn from(e: &(dyn std::error::Error + 'static)) -> Self { 74 | if e.is::() { 75 | ResponseStatus::InvalidRequestFormat 76 | } else if e.is::() { 77 | ResponseStatus::InvalidPath 78 | } else if e.is::() { 79 | ResponseStatus::InvalidQuery 80 | } else if e.is::() { 81 | ResponseStatus::InvalidMethod 82 | } else if e.is::() { 83 | ResponseStatus::InvalidHeaders 84 | } else if e.is::() { 85 | ResponseStatus::RequestFailure 86 | } else if e.is::() { 87 | ResponseStatus::RequestTimeout 88 | } else { 89 | ResponseStatus::Unknown 90 | } 91 | } 92 | } 93 | 94 | #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] 95 | pub struct RequestResponse { 96 | pub status: ResponseStatus, 97 | pub body: RequestResponseBody, 98 | } 99 | 100 | #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] 101 | #[serde(untagged)] 102 | pub enum RequestResponseBody { 103 | Ok(T), 104 | Err(String), 105 | } 106 | 107 | impl From> for RequestResponse { 108 | fn from(res: Result) -> Self { 109 | match res { 110 | Err(e) => { 111 | let e_ref: &(dyn std::error::Error) = e.as_ref(); 112 | Self { 113 | status: e_ref.into(), 114 | body: RequestResponseBody::Err(e.to_string()), 115 | } 116 | } 117 | Ok(t) => Self { 118 | status: ResponseStatus::Success, 119 | body: RequestResponseBody::Ok(t), 120 | }, 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/ratelimiter.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_trait::async_trait; 3 | use reqwest::{header::HeaderMap, Response}; 4 | use std::{ops::Deref, str::FromStr}; 5 | 6 | pub mod local; 7 | #[cfg(feature = "redis-ratelimiter")] 8 | pub mod redis; 9 | 10 | #[async_trait] 11 | pub trait Ratelimiter { 12 | async fn claim(&self, bucket: String) -> Result<()>; 13 | async fn release(&self, bucket: String, info: RatelimitInfo) -> Result<()>; 14 | } 15 | 16 | #[async_trait] 17 | impl Ratelimiter for T 18 | where 19 | T: Deref + Send + Sync, 20 | U: Ratelimiter + Send + Sync + 'static, 21 | { 22 | async fn claim(&self, bucket: String) -> Result<()> { 23 | Ratelimiter::claim(self.deref(), bucket).await 24 | } 25 | 26 | async fn release(&self, bucket: String, info: RatelimitInfo) -> Result<()> { 27 | Ratelimiter::release(self.deref(), bucket, info).await 28 | } 29 | } 30 | 31 | #[derive(Debug, Default, Eq, PartialEq)] 32 | pub struct RatelimitInfo { 33 | pub limit: Option, 34 | pub resets_in: Option, 35 | } 36 | 37 | fn get_header(headers: &HeaderMap, key: &str) -> Option { 38 | headers 39 | .get(key) 40 | .and_then(|value| Some(value.to_str().ok()?.parse().ok()?)) 41 | } 42 | 43 | impl<'a, E> From> for RatelimitInfo { 44 | fn from(r: std::result::Result<&'a Response, E>) -> Self { 45 | match r { 46 | Ok(r) => { 47 | let headers = r.headers(); 48 | Self { 49 | limit: get_header(headers, "x-ratelimit-limit"), 50 | resets_in: get_header(headers, "x-ratelimit-reset-after") 51 | .map(|r: f64| (r * 1000.) as u64), 52 | } 53 | } 54 | Err(_) => Self::default(), 55 | } 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod test { 61 | use super::{RatelimitInfo, Ratelimiter}; 62 | use anyhow::{anyhow, Result}; 63 | use futures::TryFutureExt; 64 | use std::{ 65 | sync::{ 66 | atomic::{AtomicBool, Ordering}, 67 | Arc, 68 | }, 69 | time::{Duration, SystemTime}, 70 | }; 71 | use tokio::{ 72 | time::{sleep, timeout}, 73 | try_join, 74 | }; 75 | 76 | async fn claim_timeout( 77 | client: Arc, 78 | bucket: &str, 79 | min_millis: u64, 80 | max_millis: u64, 81 | ) -> Result<()> { 82 | let min = Duration::from_millis(min_millis); 83 | let max = Duration::from_millis(max_millis); 84 | let start = SystemTime::now(); 85 | timeout(max, client.claim(bucket.to_string())).await??; 86 | 87 | let end = SystemTime::now(); 88 | if end < start + min { 89 | return Err(anyhow!( 90 | "failed to claim \"{}\" in more than {:?} (claimed in {:?})", 91 | bucket, 92 | min, 93 | end.duration_since(start)?, 94 | )); 95 | } 96 | 97 | Ok(()) 98 | } 99 | 100 | pub async fn claim_release(client: Arc) -> Result<()> { 101 | claim_timeout(client.clone(), "foo1", 0, 50).await?; 102 | 103 | let released = Arc::new(AtomicBool::new(false)); 104 | try_join!( 105 | async { 106 | claim_timeout(client.clone(), "foo1", 0, 100).await?; 107 | match released.load(Ordering::Relaxed) { 108 | true => Ok(()), 109 | false => Err(anyhow::anyhow!("claimed before lock was released")), 110 | } 111 | }, 112 | async { 113 | client 114 | .clone() 115 | .release("foo1".into(), RatelimitInfo::default()) 116 | .await?; 117 | released.store(true, Ordering::Relaxed); 118 | Ok(()) 119 | }, 120 | )?; 121 | 122 | Ok(()) 123 | } 124 | 125 | pub async fn claim_timeout_release(client: Arc) -> Result<()> { 126 | claim_timeout(client.clone(), "foo2", 0, 50).await?; 127 | 128 | let start = SystemTime::now(); 129 | client 130 | .clone() 131 | .release( 132 | "foo2".into(), 133 | RatelimitInfo { 134 | limit: None, 135 | resets_in: Some(5000), 136 | }, 137 | ) 138 | .await?; 139 | 140 | claim_timeout(client.clone(), "foo2", 0, 50).await?; 141 | 142 | let min = Duration::from_secs(5) - SystemTime::now().duration_since(start)?; 143 | let min = min.as_millis() as u64; 144 | claim_timeout(client, "foo2", min, min + 50).await?; 145 | Ok(()) 146 | } 147 | 148 | pub async fn claim_3x(client: Arc) -> Result<()> { 149 | let claims = claim_timeout(client.clone(), "foo3", 0, 50) 150 | .and_then(|_| claim_timeout(client.clone(), "foo3", 5000, 5050)) 151 | .and_then(|_| claim_timeout(client.clone(), "foo3", 5000, 5050)); 152 | 153 | try_join!(claims, async { 154 | for _ in 0..2 { 155 | sleep(Duration::from_secs(5)).await; 156 | client 157 | .clone() 158 | .release( 159 | "foo3".into(), 160 | RatelimitInfo { 161 | limit: None, 162 | resets_in: None, 163 | }, 164 | ) 165 | .await?; 166 | } 167 | 168 | Ok(()) 169 | })?; 170 | 171 | Ok(()) 172 | } 173 | 174 | pub async fn claim_limit_release(client: Arc) -> Result<()> { 175 | claim_timeout(client.clone(), "foo4", 0, 50).await?; 176 | client 177 | .clone() 178 | .release( 179 | "foo4".into(), 180 | RatelimitInfo { 181 | limit: Some(2), 182 | resets_in: None, 183 | }, 184 | ) 185 | .await?; 186 | 187 | for _ in 0..2 { 188 | claim_timeout(client.clone(), "foo4", 0, 50).await?; 189 | } 190 | 191 | let released = AtomicBool::new(false); 192 | try_join!( 193 | async { 194 | claim_timeout(client.clone(), "foo4", 5000, 5050).await?; 195 | match released.load(Ordering::Relaxed) { 196 | true => Ok(()), 197 | false => Err(anyhow::anyhow!("claimed before release")), 198 | } 199 | }, 200 | async { 201 | sleep(Duration::from_secs(5)).await; 202 | client 203 | .clone() 204 | .release( 205 | "foo4".into(), 206 | RatelimitInfo { 207 | limit: Some(2), 208 | resets_in: None, 209 | }, 210 | ) 211 | .await?; 212 | released.store(true, Ordering::Relaxed); 213 | Ok(()) 214 | } 215 | )?; 216 | 217 | Ok(()) 218 | } 219 | 220 | pub async fn claim_limit_timeout(client: Arc) -> Result<()> { 221 | claim_timeout(client.clone(), "foo5", 0, 50).await?; 222 | 223 | let start = SystemTime::now(); 224 | client 225 | .clone() 226 | .release( 227 | "foo5".into(), 228 | RatelimitInfo { 229 | limit: Some(2), 230 | resets_in: Some(5000), 231 | }, 232 | ) 233 | .await?; 234 | 235 | for _ in 0..2 { 236 | claim_timeout(client.clone(), "foo5", 0, 50).await?; 237 | } 238 | 239 | let min = Duration::from_secs(5) - SystemTime::now().duration_since(start)?; 240 | let min = min.as_millis() as u64; 241 | claim_timeout(client.clone(), "foo5", min, min + 50).await?; 242 | claim_timeout(client, "foo5", 0, 50).await?; 243 | 244 | Ok(()) 245 | } 246 | 247 | pub async fn claim_limit_release_timeout(client: Arc) -> Result<()> { 248 | claim_timeout(client.clone(), "foo6", 0, 50).await?; 249 | 250 | let start = SystemTime::now(); 251 | client 252 | .clone() 253 | .release( 254 | "foo6".into(), 255 | RatelimitInfo { 256 | limit: Some(2), 257 | resets_in: Some(5000), 258 | }, 259 | ) 260 | .await?; 261 | 262 | for _ in 0..2 { 263 | claim_timeout(client.clone(), "foo6", 0, 50).await?; 264 | } 265 | 266 | sleep(Duration::from_secs(1)).await; 267 | client 268 | .clone() 269 | .release( 270 | "foo6".into(), 271 | RatelimitInfo { 272 | limit: Some(2), 273 | resets_in: Some(4000), 274 | }, 275 | ) 276 | .await?; 277 | 278 | let min = Duration::from_secs(5) - SystemTime::now().duration_since(start)?; 279 | let min = min.as_millis() as u64; 280 | claim_timeout(client.clone(), "foo6", min, min + 50).await?; 281 | claim_timeout(client.clone(), "foo6", 0, 50).await?; 282 | 283 | Ok(()) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/ratelimiter/local.rs: -------------------------------------------------------------------------------- 1 | use super::{RatelimitInfo, Ratelimiter}; 2 | use anyhow::{anyhow, Result}; 3 | use async_trait::async_trait; 4 | use std::{ 5 | collections::HashMap, 6 | mem::drop, 7 | sync::{ 8 | atomic::{AtomicUsize, Ordering}, 9 | Arc, 10 | }, 11 | }; 12 | use tokio::{ 13 | select, spawn, 14 | sync::{ 15 | mpsc::{self, Sender}, 16 | Mutex, RwLock, Semaphore, 17 | }, 18 | time::{sleep, sleep_until, Duration, Instant}, 19 | }; 20 | use tracing::{debug, instrument}; 21 | 22 | #[derive(Debug)] 23 | struct Bucket { 24 | ready: Semaphore, 25 | new_timeout: Mutex>>, 26 | size: AtomicUsize, 27 | } 28 | 29 | impl Default for Bucket { 30 | fn default() -> Self { 31 | Self { 32 | ready: Semaphore::new(1), 33 | new_timeout: Default::default(), 34 | size: AtomicUsize::new(1), 35 | } 36 | } 37 | } 38 | 39 | #[derive(Debug, Default, Clone)] 40 | pub struct LocalRatelimiter { 41 | buckets: Arc>>>, 42 | } 43 | 44 | #[async_trait] 45 | impl Ratelimiter for LocalRatelimiter { 46 | #[instrument(level = "debug")] 47 | async fn claim(&self, bucket_name: String) -> Result<()> { 48 | let buckets = Arc::clone(&self.buckets); 49 | let mut claim = buckets.write().await; 50 | let bucket = Arc::clone(claim.entry(bucket_name.clone()).or_default()); 51 | drop(claim); 52 | 53 | bucket.ready.acquire().await?.forget(); 54 | 55 | debug!("Acquired lock for \"{}\"", &bucket_name); 56 | Ok(()) 57 | } 58 | 59 | #[instrument(level = "debug")] 60 | async fn release(&self, bucket_name: String, info: RatelimitInfo) -> Result<()> { 61 | let buckets = Arc::clone(&self.buckets); 62 | let now = Instant::now(); 63 | 64 | debug!("Releasing \"{}\"", &bucket_name); 65 | 66 | let bucket = Arc::clone( 67 | buckets 68 | .read() 69 | .await 70 | .get(&bucket_name) 71 | .ok_or(anyhow!("Attempted to release before claim"))?, 72 | ); 73 | 74 | let mut maybe_sender = bucket.new_timeout.lock().await; 75 | 76 | if let None = &*maybe_sender { 77 | debug!("No timeout: releasing \"{}\" immediately", &bucket_name); 78 | bucket.ready.add_permits(1); 79 | } 80 | 81 | if let Some(resets_in) = info.resets_in { 82 | let duration = Duration::from_millis(resets_in); 83 | 84 | debug!( 85 | "Received timeout of {:?} for \"{}\"", 86 | duration, &bucket_name 87 | ); 88 | 89 | match &mut *maybe_sender { 90 | Some(sender) => { 91 | debug!("Resetting expiration for \"{}\"", &bucket_name); 92 | sender.send(now + duration).await?; 93 | } 94 | None => { 95 | debug!("Creating new expiration for \"{}\"", &bucket_name); 96 | let mut delay = sleep(duration); 97 | let (sender, mut receiver) = mpsc::channel(1); 98 | let timeout_bucket = Arc::clone(&bucket); 99 | let bucket_name = bucket_name.clone(); 100 | spawn(async move { 101 | loop { 102 | select! { 103 | Some(new_instant) = receiver.recv() => { 104 | debug!("Updating timeout for \"{}\" to {:?}", &bucket_name, new_instant); 105 | delay = sleep_until(new_instant); 106 | }, 107 | _ = delay => { 108 | debug!("Releasing \"{}\" after timeout", &bucket_name); 109 | let size = timeout_bucket.size.load(Ordering::SeqCst); 110 | timeout_bucket.ready.add_permits(size); 111 | *timeout_bucket.new_timeout.lock().await = None; 112 | break; 113 | } 114 | } 115 | } 116 | }); 117 | *maybe_sender = Some(sender); 118 | } 119 | } 120 | } 121 | 122 | if let Some(size) = info.limit { 123 | let old_size = bucket.size.swap(size, Ordering::SeqCst); 124 | let diff = size - old_size; 125 | debug!( 126 | "New bucket size for \"{}\": {} (changing permits by {})", 127 | &bucket_name, size, diff 128 | ); 129 | bucket.ready.add_permits(diff); 130 | } 131 | 132 | Ok(()) 133 | } 134 | } 135 | 136 | #[cfg(test)] 137 | mod test { 138 | use std::sync::Arc; 139 | 140 | use anyhow::Result; 141 | use test_log::test; 142 | 143 | use super::{super::test, LocalRatelimiter}; 144 | 145 | fn get_client() -> Arc { 146 | Default::default() 147 | } 148 | 149 | #[test(tokio::test)] 150 | async fn claim_release() -> Result<()> { 151 | test::claim_release(get_client()).await 152 | } 153 | 154 | #[test(tokio::test)] 155 | async fn claim_timeout_release() -> Result<()> { 156 | test::claim_timeout_release(get_client()).await 157 | } 158 | 159 | #[test(tokio::test)] 160 | async fn claim_3x() -> Result<()> { 161 | test::claim_3x(get_client()).await 162 | } 163 | 164 | #[test(tokio::test)] 165 | async fn claim_limit_release() -> Result<()> { 166 | test::claim_limit_release(get_client()).await 167 | } 168 | 169 | #[test(tokio::test)] 170 | async fn claim_limit_timeout() -> Result<()> { 171 | test::claim_limit_timeout(get_client()).await 172 | } 173 | 174 | #[test(tokio::test)] 175 | async fn claim_limit_release_timeout() -> Result<()> { 176 | test::claim_limit_release_timeout(get_client()).await 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/ratelimiter/redis.rs: -------------------------------------------------------------------------------- 1 | use super::{RatelimitInfo, Ratelimiter}; 2 | use anyhow::Result; 3 | use async_trait::async_trait; 4 | use futures::TryStreamExt; 5 | use lazy_static::lazy_static; 6 | use redust::{model::pubsub, pool::Pool, resp::from_data, script::Script}; 7 | use std::{fmt::Debug, str::from_utf8, time::Duration}; 8 | use tokio::{net::ToSocketAddrs, time::sleep}; 9 | use tracing::{debug, instrument}; 10 | 11 | static NOTIFY_KEY: &'static str = "rest_ready"; 12 | 13 | lazy_static! { 14 | static ref CLAIM_SCRIPT: Script<2> = Script::new(include_bytes!("./scripts/claim.lua")); 15 | static ref RELEASE_SCRIPT: Script<3> = Script::new(include_bytes!("./scripts/release.lua")); 16 | } 17 | 18 | #[derive(Clone, Debug)] 19 | pub struct RedisRatelimiter 20 | where 21 | A: ToSocketAddrs + Clone + Send + Sync + Debug, 22 | { 23 | redis: Pool, 24 | } 25 | 26 | impl RedisRatelimiter 27 | where 28 | A: ToSocketAddrs + Clone + Send + Sync + Debug + 'static, 29 | { 30 | pub fn new(pool: Pool) -> Self { 31 | Self { redis: pool } 32 | } 33 | } 34 | 35 | #[async_trait] 36 | impl Ratelimiter for RedisRatelimiter 37 | where 38 | A: ToSocketAddrs + Clone + Send + Sync + Debug, 39 | { 40 | #[instrument(level = "debug")] 41 | async fn claim(&self, bucket: String) -> Result<()> { 42 | loop { 43 | let mut conn = self.redis.get().await?; 44 | let expiration = CLAIM_SCRIPT 45 | .exec(&mut conn) 46 | .keys([&bucket, &(bucket.to_string() + "_size")]) 47 | .invoke() 48 | .await?; 49 | let expiration = from_data::(expiration)?; 50 | 51 | debug!("Received expiration of {}ms for \"{}\"", expiration, bucket); 52 | 53 | if expiration.is_positive() { 54 | sleep(Duration::from_millis(expiration as u64)).await; 55 | continue; 56 | } 57 | 58 | if expiration == 0 { 59 | break; 60 | } 61 | 62 | conn.cmd(["SUBSCRIBE", NOTIFY_KEY]).await?; 63 | while let Some(data) = conn.try_next().await? { 64 | let res = from_data::(data)?; 65 | match res { 66 | pubsub::Response::Message(msg) if from_utf8(&msg.data) == Ok(&bucket) => { 67 | break; 68 | } 69 | _ => {} 70 | } 71 | } 72 | conn.cmd(["UNSUBSCRIBE", NOTIFY_KEY]).await?; 73 | } 74 | 75 | Ok(()) 76 | } 77 | 78 | #[instrument(level = "debug")] 79 | async fn release(&self, bucket: String, info: RatelimitInfo) -> Result<()> { 80 | let mut conn = self.redis.get().await?; 81 | RELEASE_SCRIPT 82 | .exec(&mut conn) 83 | .keys([bucket.as_str(), &(bucket.to_string() + "_size"), NOTIFY_KEY]) 84 | .args(&[ 85 | info.limit.unwrap_or(0).to_string(), 86 | info.resets_in.unwrap_or(0).to_string(), 87 | ]) 88 | .invoke() 89 | .await?; 90 | 91 | Ok(()) 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod test { 97 | use std::sync::{ 98 | atomic::{AtomicUsize, Ordering}, 99 | Arc, 100 | }; 101 | 102 | use anyhow::Result; 103 | use redust::pool::{deadpool::managed::HookFuture, Hook, Manager, Pool}; 104 | use test_log::test; 105 | 106 | use super::{super::test, RedisRatelimiter}; 107 | 108 | static NEXT_DB: AtomicUsize = AtomicUsize::new(0); 109 | 110 | async fn get_client() -> Result>> { 111 | let db = NEXT_DB.fetch_add(1, Ordering::Relaxed); 112 | dbg!(db); 113 | 114 | let manager = Manager::new("localhost:6379"); 115 | let pool = Pool::builder(manager) 116 | .post_create(Hook::async_fn( 117 | move |conn, _metrics| -> HookFuture { 118 | Box::pin(async move { 119 | conn.cmd(["SELECT", &db.to_string()]).await.unwrap(); 120 | conn.cmd(["FLUSHDB"]).await.unwrap(); 121 | Ok(()) 122 | }) 123 | }, 124 | )) 125 | .build()?; 126 | 127 | Ok(Arc::new(RedisRatelimiter::new(pool))) 128 | } 129 | 130 | #[test(tokio::test)] 131 | async fn claim_release() -> Result<()> { 132 | let client = get_client().await?; 133 | test::claim_release(client).await 134 | } 135 | 136 | #[test(tokio::test)] 137 | async fn claim_timeout_release() -> Result<()> { 138 | let client = get_client().await?; 139 | test::claim_timeout_release(client).await 140 | } 141 | 142 | #[test(tokio::test)] 143 | async fn claim_3x() -> Result<()> { 144 | let client = get_client().await?; 145 | test::claim_3x(client).await 146 | } 147 | 148 | #[test(tokio::test)] 149 | async fn claim_limit_release() -> Result<()> { 150 | let client = get_client().await?; 151 | test::claim_limit_release(client).await 152 | } 153 | 154 | #[test(tokio::test)] 155 | async fn claim_limit_timeout() -> Result<()> { 156 | let client = get_client().await?; 157 | test::claim_limit_timeout(client).await 158 | } 159 | 160 | #[test(tokio::test)] 161 | async fn claim_limit_release_timeout() -> Result<()> { 162 | let client = get_client().await?; 163 | test::claim_limit_release_timeout(client).await 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/ratelimiter/scripts/claim.lua: -------------------------------------------------------------------------------- 1 | local remaining = tonumber(redis.call("GET", KEYS[1])) 2 | 3 | if remaining == nil then 4 | local bucket_size = tonumber(redis.call("GET", KEYS[2])) 5 | 6 | if bucket_size == nil then 7 | bucket_size = 1 8 | end 9 | 10 | remaining = bucket_size 11 | redis.call("SET", KEYS[1], bucket_size) 12 | end 13 | 14 | if remaining <= 0 then 15 | local ttl = redis.call("PTTL", KEYS[1]) 16 | if ttl == nil then return -1 end 17 | return ttl 18 | end 19 | 20 | redis.call("DECR", KEYS[1]) 21 | return 0 22 | -------------------------------------------------------------------------------- /src/ratelimiter/scripts/release.lua: -------------------------------------------------------------------------------- 1 | local bucket_key = KEYS[1] 2 | local bucket_size_key = KEYS[2] 3 | local notify_key = KEYS[3] 4 | 5 | local new_bucket_size = tonumber(ARGV[1]) 6 | local expires_in = tonumber(ARGV[2]) 7 | 8 | if new_bucket_size > 0 then 9 | local original_bucket_size = tonumber(redis.call("GET", bucket_size_key)) 10 | if original_bucket_size == nil then 11 | -- if there was no bucket size, we assumed 1 12 | original_bucket_size = 1 13 | end 14 | 15 | local diff = new_bucket_size - original_bucket_size 16 | if diff ~= 0 then 17 | redis.call("INCRBY", bucket_key, diff) 18 | end 19 | 20 | redis.call("SET", bucket_size_key, new_bucket_size) 21 | end 22 | 23 | local ttl = redis.call("TTL", bucket_key) 24 | if ttl < 0 then -- key has no expire or doesn't exist 25 | redis.call("INCR", bucket_key) 26 | redis.call("PUBLISH", notify_key, bucket_key) 27 | end 28 | 29 | if expires_in > 0 then 30 | redis.call("PEXPIRE", bucket_key, expires_in) 31 | end 32 | -------------------------------------------------------------------------------- /src/route.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use std::convert::TryFrom; 3 | use uriparse::path::{Path, Segment}; 4 | 5 | pub fn make_route(path: &str) -> Result { 6 | let mut path = Path::try_from(path)?; 7 | if !path.is_absolute() { 8 | return Err(anyhow!("path is not absolute")); 9 | } 10 | 11 | let segments = path.segments_mut(); 12 | match segments[0].as_str() { 13 | "guilds" | "channels" | "webhooks" if segments.len() > 1 => { 14 | segments[1] = Segment::try_from(":id").unwrap(); 15 | Ok(path.into()) 16 | } 17 | _ => Ok(path.into()), 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod test { 23 | use super::make_route; 24 | 25 | #[test] 26 | fn makes_route() { 27 | assert_eq!(make_route("/foo/bar").unwrap(), "/foo/bar".to_string()); 28 | assert_eq!( 29 | make_route("/guilds/1234/roles").unwrap(), 30 | "/guilds/:id/roles".to_string() 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/runtime.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod config; 3 | #[cfg(feature = "metrics")] 4 | pub mod metrics; 5 | 6 | pub use client::Client; 7 | pub use config::Config; 8 | -------------------------------------------------------------------------------- /src/runtime/client.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "metrics")] 2 | use crate::metrics::{RATELIMIT_LATENCY, REQUESTS_TOTAL, REQUEST_LATENCY, RESPONSES_TOTAL}; 3 | use crate::{ 4 | models::{RequestResponse, SerializableHttpRequest, SerializableHttpResponse}, 5 | ratelimiter::Ratelimiter, 6 | route::make_route, 7 | }; 8 | use anyhow::{Context, Result}; 9 | use futures::{TryStream, TryStreamExt}; 10 | use http::Method; 11 | use reqwest::Request; 12 | use rustacles_brokers::{common::Message, redis::message}; 13 | use std::{convert::TryInto, fmt::Debug, str::FromStr, time::SystemTime}; 14 | use tokio::{ 15 | net::ToSocketAddrs, 16 | spawn, 17 | time::{self, timeout_at, Duration, Instant}, 18 | }; 19 | use tracing::{info, instrument, warn}; 20 | use uriparse::{Path, Query, Scheme, URIBuilder}; 21 | 22 | #[cfg(feature = "metrics")] 23 | use super::metrics::LatencyTracker; 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct Client { 27 | pub http: reqwest::Client, 28 | pub ratelimiter: R, 29 | pub api_scheme: Scheme<'static>, 30 | pub api_version: u8, 31 | pub api_base: String, 32 | pub timeout: Option, 33 | } 34 | 35 | impl Client 36 | where 37 | R: Ratelimiter + Clone + Sync + Send + 'static, 38 | { 39 | fn create_request(&self, data: &SerializableHttpRequest) -> Result { 40 | let path_str = format!( 41 | "/api/v{}/{}", 42 | self.api_version, 43 | data.path.strip_prefix('/').unwrap_or_default() 44 | ); 45 | let mut path: Path = path_str.as_str().try_into()?; 46 | path.normalize(false); 47 | 48 | let mut builder = URIBuilder::new(); 49 | builder 50 | .scheme(self.api_scheme.clone()) 51 | .authority(Some( 52 | (&*self.api_base) 53 | .try_into() 54 | .expect("Invalid authority configuration"), 55 | )) 56 | .path(path); 57 | 58 | if let Some(query) = &data.query { 59 | let maybe_qs = 60 | query 61 | .iter() 62 | .map(|(k, v)| format!("{}={}", k, v)) 63 | .reduce(|mut acc, pair| { 64 | acc.push('&'); 65 | acc.push_str(&pair); 66 | acc 67 | }); 68 | 69 | if let Some(qs) = maybe_qs { 70 | let mut query: Query = qs.as_str().try_into()?; 71 | query.normalize(); 72 | builder.query(Some(query.into_owned())); 73 | } 74 | } 75 | 76 | let url = builder.build()?; 77 | 78 | let mut req_builder = self 79 | .http 80 | .request(Method::from_str(&data.method)?, &url.to_string()) 81 | .headers((&data.headers).try_into()?); 82 | 83 | if let Some(body) = data.body.clone() { 84 | req_builder = req_builder.body(body); 85 | } 86 | 87 | Ok(req_builder 88 | .build() 89 | .context("Unable to build HTTP request")?) 90 | } 91 | 92 | #[instrument(level = "trace", skip(self), ret)] 93 | async fn claim(&self, data: &SerializableHttpRequest) -> Result<(Request, String)> { 94 | let req = self.create_request(data)?; 95 | let bucket = make_route(req.url().path())?; 96 | self.ratelimiter.claim(bucket.clone()).await?; 97 | 98 | Ok((req, bucket)) 99 | } 100 | 101 | #[instrument(level = "debug", skip(self))] 102 | async fn do_request( 103 | &self, 104 | message: &message::Message, 105 | data: &SerializableHttpRequest, 106 | ) -> Result 107 | where 108 | A: ToSocketAddrs + Clone + Send + Sync + Debug, 109 | { 110 | #[cfg(feature = "metrics")] 111 | let req_labels: [&str; 2] = [&data.method, &data.path]; 112 | 113 | let claim = { 114 | #[cfg(feature = "metrics")] 115 | let _ = LatencyTracker::new(&RATELIMIT_LATENCY, &req_labels); 116 | self.claim(data).await 117 | }; 118 | 119 | message.ack().await?; 120 | let (req, bucket) = claim?; 121 | 122 | #[cfg(feature = "metrics")] 123 | REQUESTS_TOTAL 124 | .get_metric_with_label_values(&req_labels)? 125 | .inc(); 126 | 127 | let res = { 128 | #[cfg(feature = "metrics")] 129 | let _ = LatencyTracker::new(&REQUEST_LATENCY, &req_labels); 130 | self.http.execute(req).await 131 | }; 132 | 133 | self.ratelimiter 134 | .release(bucket, res.as_ref().into()) 135 | .await?; 136 | let res = res?; 137 | 138 | #[cfg(feature = "metrics")] 139 | { 140 | let status = res.status(); 141 | let res_labels = [&data.method, &data.path, status.as_str()]; 142 | RESPONSES_TOTAL 143 | .get_metric_with_label_values(&res_labels)? 144 | .inc(); 145 | } 146 | 147 | Ok(SerializableHttpResponse { 148 | status: res.status().as_u16(), 149 | headers: res 150 | .headers() 151 | .into_iter() 152 | .map(|(name, value)| { 153 | ( 154 | name.as_str().to_string(), 155 | value.to_str().unwrap().to_string(), 156 | ) 157 | }) 158 | .collect(), 159 | url: res.url().to_string(), 160 | body: res.bytes().await?, 161 | }) 162 | } 163 | 164 | pub async fn consume_stream( 165 | &self, 166 | mut stream: impl TryStream< 167 | Ok = message::Message, 168 | Error = rustacles_brokers::error::Error, 169 | > + Unpin, 170 | ) -> Result<()> 171 | where 172 | A: 'static + ToSocketAddrs + Clone + Send + Sync + Debug, 173 | { 174 | while let Some(message) = stream.try_next().await? { 175 | let client = self.clone(); 176 | match message.timeout_at { 177 | Some(timeout) => { 178 | let duration = timeout.duration_since(SystemTime::now()).expect("duration"); 179 | let instant = Instant::now() + duration; 180 | spawn(async move { 181 | timeout_at(instant, client.handle_message(message)).await; 182 | }); 183 | } 184 | None => { 185 | spawn(async move { 186 | client.handle_message(message).await; 187 | }); 188 | } 189 | } 190 | } 191 | 192 | Ok(()) 193 | } 194 | 195 | #[instrument(level = "debug", skip(self))] 196 | pub async fn handle_message( 197 | &self, 198 | message: message::Message, 199 | ) -> Result<()> 200 | where 201 | A: ToSocketAddrs + Clone + Send + Sync + Debug, 202 | { 203 | message.ack().await?; 204 | 205 | let data = match message.data { 206 | Some(ref data) => data, 207 | None => { 208 | warn!("Message missing data"); 209 | return Ok(()); 210 | } 211 | }; 212 | info!("--> REQ({}): {}", message.id, data); 213 | 214 | let timeout = data.timeout; 215 | let req = self.do_request(&message, &data); 216 | 217 | let body = if let Some(min_timeout) = self.timeout.min(timeout) { 218 | time::timeout(min_timeout, req).await? 219 | } else { 220 | req.await 221 | }; 222 | 223 | match &body { 224 | Ok(res) => info!("<-- RES({}): {}", message.id, res), 225 | Err(e) => warn!("<-- ERR({}): {:?}", message.id, e), 226 | } 227 | 228 | let body = RequestResponse::::from(body); 229 | 230 | message 231 | .reply(&body) 232 | .await 233 | .expect("Unable to respond to query"); 234 | 235 | Ok(()) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/runtime/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use humantime::parse_duration; 3 | use rustacles_brokers::redis::{ 4 | redust::pool::{Manager, Pool}, 5 | RedisBroker, 6 | }; 7 | use serde::Deserialize; 8 | use std::{env, net::SocketAddr, time::Duration}; 9 | 10 | #[derive(Debug, Default, Deserialize)] 11 | pub struct Config { 12 | #[serde(default)] 13 | pub redis: RedisConfig, 14 | #[serde(default)] 15 | pub discord: DiscordConfig, 16 | #[serde(with = "humantime_serde")] 17 | pub timeout: Option, 18 | pub metrics: Option, 19 | #[serde(default)] 20 | pub broker: BrokerConfig, 21 | } 22 | 23 | impl Config { 24 | pub fn from_toml_file(file: &str) -> Result { 25 | Ok(toml::from_slice(&std::fs::read(file)?)?) 26 | } 27 | 28 | pub fn with_env(mut self) -> Self { 29 | for (k, v) in env::vars() { 30 | match k.as_str() { 31 | "BROKER_GROUP" => self.broker.group = v, 32 | "BROKER_EVENT" => self.broker.event = v, 33 | "REDIS_URL" => self.redis.url = v, 34 | "REDIS_POOL_SIZE" => { 35 | self.redis.pool_size = v.parse().expect("valid REDIS_POOL_SIZE (usize)") 36 | } 37 | "TIMEOUT" => self.timeout = parse_duration(&v).ok(), 38 | "DISCORD_API_VERSION" => { 39 | self.discord.api_version = v.parse().expect("valid DISCORD_API_VERSION (u8)") 40 | } 41 | "METRICS_ADDR" => { 42 | self.metrics.get_or_insert(MetricsConfig::default()).addr = 43 | v.parse().expect("valid METRICS_ADDR (SocketAddr)") 44 | } 45 | "METRICS_PATH" => { 46 | self.metrics.get_or_insert(MetricsConfig::default()).path = v; 47 | } 48 | _ => {} 49 | } 50 | } 51 | 52 | self 53 | } 54 | 55 | pub fn new_broker(&self) -> RedisBroker { 56 | let manager = Manager::new(self.redis.url.clone()); 57 | let pool = Pool::builder(manager) 58 | .max_size(self.redis.pool_size) 59 | .build() 60 | .unwrap(); 61 | 62 | RedisBroker::new(self.broker.group.clone(), pool) 63 | } 64 | } 65 | 66 | #[derive(Debug, Deserialize)] 67 | pub struct RedisConfig { 68 | #[serde(default = "RedisConfig::default_url")] 69 | pub url: String, 70 | #[serde(default = "RedisConfig::default_pool_size")] 71 | pub pool_size: usize, 72 | } 73 | 74 | impl RedisConfig { 75 | fn default_url() -> String { 76 | "localhost:6379".into() 77 | } 78 | 79 | fn default_pool_size() -> usize { 80 | 32 81 | } 82 | } 83 | 84 | impl Default for RedisConfig { 85 | fn default() -> Self { 86 | Self { 87 | url: Self::default_url(), 88 | pool_size: Self::default_pool_size(), 89 | } 90 | } 91 | } 92 | 93 | #[derive(Debug, Deserialize)] 94 | pub struct DiscordConfig { 95 | #[serde(default = "DiscordConfig::default_api_version")] 96 | pub api_version: u8, 97 | } 98 | 99 | impl DiscordConfig { 100 | fn default_api_version() -> u8 { 101 | return 10; 102 | } 103 | } 104 | 105 | impl Default for DiscordConfig { 106 | fn default() -> Self { 107 | Self { 108 | api_version: Self::default_api_version(), 109 | } 110 | } 111 | } 112 | 113 | #[derive(Debug, Deserialize)] 114 | pub struct MetricsConfig { 115 | #[serde(default = "MetricsConfig::default_addr")] 116 | pub addr: SocketAddr, 117 | #[serde(default = "MetricsConfig::default_path")] 118 | pub path: String, 119 | } 120 | 121 | impl MetricsConfig { 122 | fn default_addr() -> SocketAddr { 123 | ([0, 0, 0, 0], 3000).into() 124 | } 125 | 126 | fn default_path() -> String { 127 | "metrics".to_owned() 128 | } 129 | } 130 | 131 | impl Default for MetricsConfig { 132 | fn default() -> Self { 133 | Self { 134 | addr: Self::default_addr(), 135 | path: Self::default_path(), 136 | } 137 | } 138 | } 139 | 140 | #[derive(Debug, Deserialize)] 141 | pub struct BrokerConfig { 142 | #[serde(default = "BrokerConfig::default_group")] 143 | pub group: String, 144 | #[serde(default = "BrokerConfig::default_event")] 145 | pub event: String, 146 | } 147 | 148 | impl BrokerConfig { 149 | fn default_group() -> String { 150 | "proxy".to_string() 151 | } 152 | 153 | fn default_event() -> String { 154 | "REQUEST".to_string() 155 | } 156 | } 157 | 158 | impl Default for BrokerConfig { 159 | fn default() -> Self { 160 | Self { 161 | group: Self::default_group(), 162 | event: Self::default_event(), 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/runtime/metrics.rs: -------------------------------------------------------------------------------- 1 | use std::{net::SocketAddr, time::Instant}; 2 | 3 | use lazy_static::lazy_static; 4 | use prometheus::{Encoder, HistogramVec, TextEncoder}; 5 | use warp::Filter; 6 | 7 | lazy_static! { 8 | static ref TEXT_ENCODER: TextEncoder = TextEncoder::new(); 9 | } 10 | 11 | pub async fn start_server(path: String, addr: impl Into) { 12 | let route = warp::path(path).and(warp::get()).map(|| { 13 | let metrics = prometheus::gather(); 14 | let mut out = vec![]; 15 | TEXT_ENCODER 16 | .encode(&metrics, &mut out) 17 | .expect("unable to encode response"); 18 | String::from_utf8(out).expect("valid UTF-8 metrics") 19 | }); 20 | 21 | warp::serve(route).run(addr).await; 22 | } 23 | 24 | pub struct LatencyTracker<'vec, 'labels> { 25 | vec: &'vec HistogramVec, 26 | labels: &'labels [&'labels str], 27 | start: Instant, 28 | } 29 | 30 | impl<'vec, 'labels> LatencyTracker<'vec, 'labels> { 31 | pub fn new(vec: &'vec HistogramVec, labels: &'labels [&'labels str]) -> Self { 32 | Self { 33 | vec, 34 | labels, 35 | start: Instant::now(), 36 | } 37 | } 38 | } 39 | 40 | impl<'vec, 'labels> Drop for LatencyTracker<'vec, 'labels> { 41 | fn drop(&mut self) { 42 | let latency = Instant::now().duration_since(self.start); 43 | self.vec 44 | .get_metric_with_label_values(self.labels) 45 | .unwrap() 46 | .observe(latency.as_secs_f64()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/handles_request.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bytes::Bytes; 3 | use futures::TryStreamExt; 4 | use mockito::mock; 5 | use rustacles_brokers::common::Rpc; 6 | use rustacles_brokers::redis::redust::pool::{Manager, Pool}; 7 | use rustacles_brokers::redis::RedisBroker; 8 | use spectacles_proxy::ratelimiter::local::LocalRatelimiter; 9 | use spectacles_proxy::{ 10 | models::{ 11 | RequestResponse, RequestResponseBody, ResponseStatus, SerializableHttpRequest, 12 | SerializableHttpResponse, 13 | }, 14 | runtime::{Client, Config}, 15 | }; 16 | use std::sync::Arc; 17 | use test_log::test; 18 | use tokio::{ 19 | spawn, 20 | time::{timeout, Duration}, 21 | }; 22 | 23 | #[test(tokio::test)] 24 | async fn handles_request() -> Result<()> { 25 | let config = dbg!(Config::default().with_env()); 26 | let manager = Manager::new(config.redis.url.clone()); 27 | let pool = Pool::builder(manager) 28 | .max_size(config.redis.pool_size) 29 | .build() 30 | .expect("pool should be built"); 31 | let broker = RedisBroker::new(config.broker.group.clone(), pool.clone()); 32 | 33 | let rpc_broker = RedisBroker::new(config.broker.group, pool); 34 | 35 | let ratelimiter = LocalRatelimiter::default(); 36 | let mock_addr = mockito::server_address(); 37 | 38 | let client = Client { 39 | api_base: mock_addr.to_string(), 40 | api_scheme: uriparse::Scheme::HTTP, 41 | api_version: 6, 42 | http: reqwest::Client::new(), 43 | ratelimiter: Arc::new(ratelimiter), 44 | timeout: None, 45 | }; 46 | 47 | let mock = mock("GET", "/api/v6/foo/bar") 48 | .with_body(rmp_serde::to_vec(&["hello world"])?) 49 | .create(); 50 | 51 | let events = vec![Bytes::from(config.broker.event.clone())]; 52 | broker.ensure_events(events.iter()).await?; 53 | spawn(async move { 54 | let mut consumer = broker.consume(events); 55 | while let Some(message) = consumer.try_next().await.expect("Next message") { 56 | client 57 | .handle_message(message) 58 | .await 59 | .expect("Unable to handle message"); 60 | } 61 | }); 62 | 63 | let payload = SerializableHttpRequest { 64 | method: "GET".into(), 65 | path: "/foo/bar".into(), 66 | query: None, 67 | body: None, 68 | headers: Default::default(), 69 | timeout: None, 70 | }; 71 | 72 | let rpc = timeout( 73 | Duration::from_secs(5), 74 | rpc_broker.call(config.broker.event.as_str(), &payload, None), 75 | ) 76 | .await??; 77 | 78 | let response = rpc 79 | .response::>() 80 | .await? 81 | .unwrap(); 82 | mock.assert(); 83 | 84 | assert_eq!(response.status, ResponseStatus::Success); 85 | assert_eq!( 86 | response.body, 87 | RequestResponseBody::Ok(SerializableHttpResponse { 88 | status: 200, 89 | headers: vec![ 90 | ("connection".to_string(), "close".to_string()), 91 | ("content-length".to_string(), "13".to_string()) 92 | ] 93 | .into_iter() 94 | .collect(), 95 | url: format!("http://{}/api/v6/foo/bar", mock_addr), 96 | body: rmp_serde::to_vec(&["hello world"])?.into(), 97 | }) 98 | ); 99 | 100 | Ok(()) 101 | } 102 | --------------------------------------------------------------------------------