├── .cargo └── audit.toml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── RELEASING.md ├── rust-toolchain.toml ├── rustfmt.toml └── src ├── config.rs ├── errors.rs ├── http_client.rs ├── influxdb.rs ├── main.rs ├── push ├── apns.rs ├── fcm.rs ├── hms.rs ├── mod.rs └── threema_gateway.rs └── server.rs /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | ignore = [] 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | schedule: 7 | - cron: '30 3 * * 4' 8 | 9 | name: CI 10 | 11 | env: 12 | RUST_VERSION: "1.86" 13 | 14 | jobs: 15 | 16 | test: 17 | name: run tests 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: dtolnay/rust-toolchain@v1 22 | with: 23 | toolchain: $RUST_VERSION 24 | - run: cargo build 25 | - run: cargo test --all-features 26 | 27 | clippy: 28 | name: run clippy lints 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: dtolnay/rust-toolchain@v1 33 | with: 34 | toolchain: $RUST_VERSION 35 | components: clippy 36 | - run: cargo clippy --all-features -- -D warnings 37 | 38 | fmt: 39 | name: run rustfmt 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: dtolnay/rust-toolchain@v1 44 | with: 45 | toolchain: nightly 46 | components: rustfmt 47 | - run: rm rust-toolchain.toml 48 | - run: cargo fmt --all --check 49 | 50 | audit: 51 | name: run cargo audit 52 | runs-on: ubuntu-latest 53 | container: dbrgn/cargo-audit:latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | - run: cargo audit 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.swp 3 | config.ini 4 | config.toml 5 | .idea/ 6 | *.p8 7 | data 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["rust-lang.rust-analyzer", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "rust-lang.rust-analyzer", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": false, 5 | "rewrap.wrappingColumn": 120, 6 | "rust-analyzer.check.command": "clippy", 7 | "[markdown]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This project follows semantic versioning. 4 | 5 | Possible log types: 6 | 7 | - `[added]` for new features. 8 | - `[changed]` for changes in existing functionality. 9 | - `[deprecated]` for once-stable features removed in upcoming releases. 10 | - `[removed]` for features removed in this release. 11 | - `[fixed]` for any bug fixes. 12 | - `[security]` to invite users to upgrade in case of vulnerabilities. 13 | 14 | ### Unreleased 15 | 16 | - [changed] Bump rust toolchain to 1.86 17 | 18 | ### [5.0.4][v5.0.4] (2025-05-16) 19 | 20 | - [security] Update dependencies and fix vulnerabilities 21 | 22 | ### [5.0.3][v5.0.3] (2025-01-27) 23 | 24 | - [changed] Update dependencies 25 | - [changed] Bump rust toolchain to 1.84 26 | 27 | ### [5.0.2][v5.0.2] (2024-08-14) 28 | 29 | - [changed] Update dependencies 30 | 31 | ### [5.0.1][v5.0.1] (2024-06-20) 32 | 33 | - [changed] Update dependencies 34 | 35 | ### [v5.0.0][v5.0.0] (2024-05-29) 36 | 37 | - [changed] Migrate from legacy FCM HTTP API to HTTP v1 38 | 39 | Note that the FCM config format has changed! See README for details. 40 | 41 | ### [v4.3.0][v4.3.0] (2024-05-17) 42 | 43 | - [changed] Updated dependencies 44 | - [changed] Migrated from `hyper` to `reqwest` (client) and `axum` (server) 45 | - [changed] Switch from `log` crate to `tracing` 46 | - [fixed] Prevent potential panic when handling APNs push requests 47 | 48 | ### [v4.2.3][v4.2.3] (2024-02-02) 49 | 50 | - [changed] Updated dependencies 51 | 52 | ### [v4.2.2][v4.2.2] (2023-11-14) 53 | 54 | - [changed] Updated dependencies 55 | 56 | ### [v4.2.1][v4.2.1] (2023-07-05) 57 | 58 | - [changed] Improved logging 59 | 60 | ### [v4.2.0][v4.2.0] (2023-06-27) 61 | 62 | - [added] Support for Threema Gateway push (#52) 63 | - [changed] Updated dependencies 64 | 65 | ### [v4.1.1][v4.1.1] (2022-03-31) 66 | 67 | - [added] Log APNs push type (#49) 68 | - [changed] Updated dependencies 69 | 70 | ### [v4.1.0][v4.1.0] (2022-03-17) 71 | 72 | - [added] APNs: Support non-silent push notifications as well (#46) 73 | - [changed] Updated dependencies 74 | 75 | ### [v4.0.0][v4.0.0] (2021-03-15) 76 | 77 | - [added] Support for HMS 78 | - [added] FCM: Support for connection reuse and TLS session resumption 79 | - [changed] The config file format was changed from INI to TOML and the default 80 | filename was changed from `config.ini` to `config.toml`. Since TOML is a 81 | superset of INI, the existing config should remain valid. But the change 82 | simplifies parsing and allows more data types (like lists and maps). 83 | 84 | ### [v3.4.0][v3.4.0] (2020-01-13) 85 | 86 | - [security] Updated dependencies, including a [security update in a transitive 87 | dependency][rustsec-2019-033] 88 | - [changed] Require at least Rust 1.36 to build (previous: 1.33) 89 | 90 | [rustsec-2019-033]: https://rustsec.org/advisories/RUSTSEC-2019-0033.html 91 | 92 | ### [v3.3.0][v3.3.0] (2019-08-05) 93 | 94 | - [security] Updated dependencies, including a [security update in a transitive 95 | dependency][memoffset-9] 96 | - [changed] Require at least Rust 1.33 to build (previous: 1.31) 97 | 98 | [memoffset-9]: https://github.com/Gilnaa/memoffset/issues/9 99 | 100 | ### [v3.2.1][v3.2.1] (2019-07-08) 101 | 102 | - [security] Updated dependencies, including a [security update in a transitive 103 | dependency][smallvec-148] (#29) 104 | 105 | [smallvec-148]: https://github.com/servo/rust-smallvec/issues/148 106 | 107 | ### [v3.2.0][v3.2.0] (2019-05-23) 108 | 109 | - [added] APNS: Apply `collapse_key` and `ttl` if specified (#24) 110 | - [fixed] APNs: Use timestamp based on TTL instead of the TTL itself (#25) 111 | - [changed] Refined error handling (#26) 112 | 113 | ### [v3.1.0][v3.1.0] (2019-04-25) 114 | 115 | - [added] Allow clients to override the FCM TTL (#19) 116 | - [added] Allow clients to override the FCM collapse key (#20) 117 | - [changed] Improve handling of FCM push errors (#18) 118 | 119 | ### [v3.0.0][v3.0.0] (2019-01-24) 120 | 121 | - [changed] Use new FCM API endpoint 122 | - [changed] Rename `[gcm]` section in config.ini to `[fcm]` 123 | - [changed] Rename `type=gcm` request key to `type=fcm` 124 | (the `gcm` version will still work but is deprecated) 125 | 126 | ### [v2.2.0][v2.2.0] (2018-12-17) 127 | 128 | - [changed] Switch to Rust 2018 edition 129 | - [changed] Require at least Rust 1.31 to build (previous: 1.30) 130 | - [changed] Updated dependencies 131 | - [changed] Increase log level for some logs 132 | - [changed] Apply clippy lint feedback 133 | 134 | [v2.2.0]: https://github.com/threema-ch/push-relay/compare/v2.1.1...v2.2.0 135 | [v3.0.0]: https://github.com/threema-ch/push-relay/compare/v2.2.0...v3.0.0 136 | [v3.1.0]: https://github.com/threema-ch/push-relay/compare/v3.0.0...v3.1.0 137 | [v3.2.0]: https://github.com/threema-ch/push-relay/compare/v3.1.0...v3.2.0 138 | [v3.2.1]: https://github.com/threema-ch/push-relay/compare/v3.2.0...v3.2.1 139 | [v3.3.0]: https://github.com/threema-ch/push-relay/compare/v3.2.1...v3.3.0 140 | [v3.4.0]: https://github.com/threema-ch/push-relay/compare/v3.3.0...v3.4.0 141 | [v4.0.0]: https://github.com/threema-ch/push-relay/compare/v3.4.0...v4.0.0 142 | [v4.1.0]: https://github.com/threema-ch/push-relay/compare/v4.0.0...v4.1.0 143 | [v4.1.1]: https://github.com/threema-ch/push-relay/compare/v4.1.0...v4.1.1 144 | [v4.2.0]: https://github.com/threema-ch/push-relay/compare/v4.1.1...v4.2.0 145 | [v4.2.1]: https://github.com/threema-ch/push-relay/compare/v4.2.0...v4.2.1 146 | [v4.2.2]: https://github.com/threema-ch/push-relay/compare/v4.2.1...v4.2.2 147 | [v4.2.3]: https://github.com/threema-ch/push-relay/compare/v4.2.2...v4.2.3 148 | [v4.3.0]: https://github.com/threema-ch/push-relay/compare/v4.2.3...v4.3.0 149 | [v5.0.0]: https://github.com/threema-ch/push-relay/compare/v4.3.0...v5.0.0 150 | [v5.0.1]: https://github.com/threema-ch/push-relay/compare/v5.0.0...v5.0.1 151 | [v5.0.2]: https://github.com/threema-ch/push-relay/compare/v5.0.1...v5.0.2 152 | [v5.0.3]: https://github.com/threema-ch/push-relay/compare/v5.0.2...v5.0.3 153 | [v5.0.3]: https://github.com/threema-ch/push-relay/compare/v5.0.3...v5.0.4 154 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "a2" 7 | version = "0.10.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f279fc8b1f1a64138f0f4b9cda9be488ae35bc2f8556c7ffe60730f1c07d005a" 10 | dependencies = [ 11 | "base64 0.21.7", 12 | "erased-serde", 13 | "http", 14 | "http-body-util", 15 | "hyper", 16 | "hyper-rustls 0.26.0", 17 | "hyper-util", 18 | "openssl", 19 | "parking_lot", 20 | "rustls 0.22.4", 21 | "rustls-pemfile", 22 | "serde", 23 | "serde_json", 24 | "thiserror 1.0.69", 25 | "tokio", 26 | "tracing", 27 | ] 28 | 29 | [[package]] 30 | name = "addr2line" 31 | version = "0.24.2" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 34 | dependencies = [ 35 | "gimli", 36 | ] 37 | 38 | [[package]] 39 | name = "adler2" 40 | version = "2.0.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 43 | 44 | [[package]] 45 | name = "aead" 46 | version = "0.5.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" 49 | dependencies = [ 50 | "crypto-common", 51 | "generic-array", 52 | ] 53 | 54 | [[package]] 55 | name = "aho-corasick" 56 | version = "1.1.3" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 59 | dependencies = [ 60 | "memchr", 61 | ] 62 | 63 | [[package]] 64 | name = "android-tzdata" 65 | version = "0.1.1" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 68 | 69 | [[package]] 70 | name = "android_system_properties" 71 | version = "0.1.5" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 74 | dependencies = [ 75 | "libc", 76 | ] 77 | 78 | [[package]] 79 | name = "anstream" 80 | version = "0.6.18" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 83 | dependencies = [ 84 | "anstyle", 85 | "anstyle-parse", 86 | "anstyle-query", 87 | "anstyle-wincon", 88 | "colorchoice", 89 | "is_terminal_polyfill", 90 | "utf8parse", 91 | ] 92 | 93 | [[package]] 94 | name = "anstyle" 95 | version = "1.0.10" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 98 | 99 | [[package]] 100 | name = "anstyle-parse" 101 | version = "0.2.6" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 104 | dependencies = [ 105 | "utf8parse", 106 | ] 107 | 108 | [[package]] 109 | name = "anstyle-query" 110 | version = "1.1.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 113 | dependencies = [ 114 | "windows-sys 0.59.0", 115 | ] 116 | 117 | [[package]] 118 | name = "anstyle-wincon" 119 | version = "3.0.7" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 122 | dependencies = [ 123 | "anstyle", 124 | "once_cell", 125 | "windows-sys 0.59.0", 126 | ] 127 | 128 | [[package]] 129 | name = "anyhow" 130 | version = "1.0.98" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 133 | 134 | [[package]] 135 | name = "argparse" 136 | version = "0.2.2" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" 139 | 140 | [[package]] 141 | name = "assert-json-diff" 142 | version = "2.0.2" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" 145 | dependencies = [ 146 | "serde", 147 | "serde_json", 148 | ] 149 | 150 | [[package]] 151 | name = "async-trait" 152 | version = "0.1.88" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 155 | dependencies = [ 156 | "proc-macro2", 157 | "quote", 158 | "syn", 159 | ] 160 | 161 | [[package]] 162 | name = "atomic-waker" 163 | version = "1.1.2" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 166 | 167 | [[package]] 168 | name = "autocfg" 169 | version = "1.4.0" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 172 | 173 | [[package]] 174 | name = "axum" 175 | version = "0.8.4" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" 178 | dependencies = [ 179 | "axum-core", 180 | "bytes", 181 | "futures-util", 182 | "http", 183 | "http-body", 184 | "http-body-util", 185 | "hyper", 186 | "hyper-util", 187 | "itoa", 188 | "matchit", 189 | "memchr", 190 | "mime", 191 | "percent-encoding", 192 | "pin-project-lite", 193 | "rustversion", 194 | "serde", 195 | "sync_wrapper", 196 | "tokio", 197 | "tower", 198 | "tower-layer", 199 | "tower-service", 200 | ] 201 | 202 | [[package]] 203 | name = "axum-core" 204 | version = "0.5.2" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" 207 | dependencies = [ 208 | "bytes", 209 | "futures-core", 210 | "http", 211 | "http-body", 212 | "http-body-util", 213 | "mime", 214 | "pin-project-lite", 215 | "rustversion", 216 | "sync_wrapper", 217 | "tower-layer", 218 | "tower-service", 219 | ] 220 | 221 | [[package]] 222 | name = "backtrace" 223 | version = "0.3.75" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 226 | dependencies = [ 227 | "addr2line", 228 | "cfg-if", 229 | "libc", 230 | "miniz_oxide", 231 | "object", 232 | "rustc-demangle", 233 | "windows-targets 0.52.6", 234 | ] 235 | 236 | [[package]] 237 | name = "base64" 238 | version = "0.21.7" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 241 | 242 | [[package]] 243 | name = "base64" 244 | version = "0.22.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 247 | 248 | [[package]] 249 | name = "bitflags" 250 | version = "2.9.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 253 | 254 | [[package]] 255 | name = "bumpalo" 256 | version = "3.17.0" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 259 | 260 | [[package]] 261 | name = "bytes" 262 | version = "1.10.1" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 265 | 266 | [[package]] 267 | name = "cc" 268 | version = "1.2.23" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" 271 | dependencies = [ 272 | "shlex", 273 | ] 274 | 275 | [[package]] 276 | name = "cfg-if" 277 | version = "1.0.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 280 | 281 | [[package]] 282 | name = "cfg_aliases" 283 | version = "0.2.1" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 286 | 287 | [[package]] 288 | name = "chrono" 289 | version = "0.4.41" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 292 | dependencies = [ 293 | "android-tzdata", 294 | "iana-time-zone", 295 | "js-sys", 296 | "num-traits", 297 | "wasm-bindgen", 298 | "windows-link", 299 | ] 300 | 301 | [[package]] 302 | name = "cipher" 303 | version = "0.4.4" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 306 | dependencies = [ 307 | "crypto-common", 308 | "inout", 309 | "zeroize", 310 | ] 311 | 312 | [[package]] 313 | name = "clap" 314 | version = "4.5.38" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 317 | dependencies = [ 318 | "clap_builder", 319 | "clap_derive", 320 | ] 321 | 322 | [[package]] 323 | name = "clap_builder" 324 | version = "4.5.38" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 327 | dependencies = [ 328 | "anstream", 329 | "anstyle", 330 | "clap_lex", 331 | "strsim", 332 | ] 333 | 334 | [[package]] 335 | name = "clap_derive" 336 | version = "4.5.32" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 339 | dependencies = [ 340 | "heck", 341 | "proc-macro2", 342 | "quote", 343 | "syn", 344 | ] 345 | 346 | [[package]] 347 | name = "clap_lex" 348 | version = "0.7.4" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 351 | 352 | [[package]] 353 | name = "colorchoice" 354 | version = "1.0.3" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 357 | 358 | [[package]] 359 | name = "core-foundation" 360 | version = "0.10.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" 363 | dependencies = [ 364 | "core-foundation-sys", 365 | "libc", 366 | ] 367 | 368 | [[package]] 369 | name = "core-foundation-sys" 370 | version = "0.8.7" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 373 | 374 | [[package]] 375 | name = "cpufeatures" 376 | version = "0.2.17" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 379 | dependencies = [ 380 | "libc", 381 | ] 382 | 383 | [[package]] 384 | name = "crypto-common" 385 | version = "0.1.6" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 388 | dependencies = [ 389 | "generic-array", 390 | "rand_core 0.6.4", 391 | "typenum", 392 | ] 393 | 394 | [[package]] 395 | name = "crypto_secretbox" 396 | version = "0.1.1" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" 399 | dependencies = [ 400 | "aead", 401 | "cipher", 402 | "generic-array", 403 | "poly1305", 404 | "salsa20", 405 | "subtle", 406 | "zeroize", 407 | ] 408 | 409 | [[package]] 410 | name = "curve25519-dalek" 411 | version = "4.1.3" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 414 | dependencies = [ 415 | "cfg-if", 416 | "cpufeatures", 417 | "curve25519-dalek-derive", 418 | "fiat-crypto", 419 | "rustc_version", 420 | "subtle", 421 | "zeroize", 422 | ] 423 | 424 | [[package]] 425 | name = "curve25519-dalek-derive" 426 | version = "0.1.1" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 429 | dependencies = [ 430 | "proc-macro2", 431 | "quote", 432 | "syn", 433 | ] 434 | 435 | [[package]] 436 | name = "data-encoding" 437 | version = "2.9.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 440 | 441 | [[package]] 442 | name = "deadpool" 443 | version = "0.10.0" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" 446 | dependencies = [ 447 | "async-trait", 448 | "deadpool-runtime", 449 | "num_cpus", 450 | "tokio", 451 | ] 452 | 453 | [[package]] 454 | name = "deadpool-runtime" 455 | version = "0.1.4" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" 458 | 459 | [[package]] 460 | name = "deranged" 461 | version = "0.4.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 464 | dependencies = [ 465 | "powerfmt", 466 | "serde", 467 | ] 468 | 469 | [[package]] 470 | name = "displaydoc" 471 | version = "0.2.5" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 474 | dependencies = [ 475 | "proc-macro2", 476 | "quote", 477 | "syn", 478 | ] 479 | 480 | [[package]] 481 | name = "equivalent" 482 | version = "1.0.2" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 485 | 486 | [[package]] 487 | name = "erased-serde" 488 | version = "0.3.31" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" 491 | dependencies = [ 492 | "serde", 493 | ] 494 | 495 | [[package]] 496 | name = "fiat-crypto" 497 | version = "0.2.9" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 500 | 501 | [[package]] 502 | name = "fnv" 503 | version = "1.0.7" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 506 | 507 | [[package]] 508 | name = "foreign-types" 509 | version = "0.3.2" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 512 | dependencies = [ 513 | "foreign-types-shared", 514 | ] 515 | 516 | [[package]] 517 | name = "foreign-types-shared" 518 | version = "0.1.1" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 521 | 522 | [[package]] 523 | name = "form_urlencoded" 524 | version = "1.2.1" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 527 | dependencies = [ 528 | "percent-encoding", 529 | ] 530 | 531 | [[package]] 532 | name = "futures" 533 | version = "0.3.31" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 536 | dependencies = [ 537 | "futures-channel", 538 | "futures-core", 539 | "futures-executor", 540 | "futures-io", 541 | "futures-sink", 542 | "futures-task", 543 | "futures-util", 544 | ] 545 | 546 | [[package]] 547 | name = "futures-channel" 548 | version = "0.3.31" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 551 | dependencies = [ 552 | "futures-core", 553 | "futures-sink", 554 | ] 555 | 556 | [[package]] 557 | name = "futures-core" 558 | version = "0.3.31" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 561 | 562 | [[package]] 563 | name = "futures-executor" 564 | version = "0.3.31" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 567 | dependencies = [ 568 | "futures-core", 569 | "futures-task", 570 | "futures-util", 571 | ] 572 | 573 | [[package]] 574 | name = "futures-io" 575 | version = "0.3.31" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 578 | 579 | [[package]] 580 | name = "futures-macro" 581 | version = "0.3.31" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 584 | dependencies = [ 585 | "proc-macro2", 586 | "quote", 587 | "syn", 588 | ] 589 | 590 | [[package]] 591 | name = "futures-sink" 592 | version = "0.3.31" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 595 | 596 | [[package]] 597 | name = "futures-task" 598 | version = "0.3.31" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 601 | 602 | [[package]] 603 | name = "futures-util" 604 | version = "0.3.31" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 607 | dependencies = [ 608 | "futures-channel", 609 | "futures-core", 610 | "futures-io", 611 | "futures-macro", 612 | "futures-sink", 613 | "futures-task", 614 | "memchr", 615 | "pin-project-lite", 616 | "pin-utils", 617 | "slab", 618 | ] 619 | 620 | [[package]] 621 | name = "generic-array" 622 | version = "0.14.7" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 625 | dependencies = [ 626 | "typenum", 627 | "version_check", 628 | "zeroize", 629 | ] 630 | 631 | [[package]] 632 | name = "getrandom" 633 | version = "0.2.16" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 636 | dependencies = [ 637 | "cfg-if", 638 | "js-sys", 639 | "libc", 640 | "wasi 0.11.0+wasi-snapshot-preview1", 641 | "wasm-bindgen", 642 | ] 643 | 644 | [[package]] 645 | name = "getrandom" 646 | version = "0.3.3" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 649 | dependencies = [ 650 | "cfg-if", 651 | "js-sys", 652 | "libc", 653 | "r-efi", 654 | "wasi 0.14.2+wasi-0.2.4", 655 | "wasm-bindgen", 656 | ] 657 | 658 | [[package]] 659 | name = "gimli" 660 | version = "0.31.1" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 663 | 664 | [[package]] 665 | name = "h2" 666 | version = "0.4.10" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" 669 | dependencies = [ 670 | "atomic-waker", 671 | "bytes", 672 | "fnv", 673 | "futures-core", 674 | "futures-sink", 675 | "http", 676 | "indexmap", 677 | "slab", 678 | "tokio", 679 | "tokio-util", 680 | "tracing", 681 | ] 682 | 683 | [[package]] 684 | name = "hashbrown" 685 | version = "0.15.3" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 688 | 689 | [[package]] 690 | name = "heck" 691 | version = "0.5.0" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 694 | 695 | [[package]] 696 | name = "hermit-abi" 697 | version = "0.3.9" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 700 | 701 | [[package]] 702 | name = "hostname" 703 | version = "0.4.1" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" 706 | dependencies = [ 707 | "cfg-if", 708 | "libc", 709 | "windows-link", 710 | ] 711 | 712 | [[package]] 713 | name = "http" 714 | version = "1.3.1" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 717 | dependencies = [ 718 | "bytes", 719 | "fnv", 720 | "itoa", 721 | ] 722 | 723 | [[package]] 724 | name = "http-body" 725 | version = "1.0.1" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 728 | dependencies = [ 729 | "bytes", 730 | "http", 731 | ] 732 | 733 | [[package]] 734 | name = "http-body-util" 735 | version = "0.1.3" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 738 | dependencies = [ 739 | "bytes", 740 | "futures-core", 741 | "http", 742 | "http-body", 743 | "pin-project-lite", 744 | ] 745 | 746 | [[package]] 747 | name = "httparse" 748 | version = "1.10.1" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 751 | 752 | [[package]] 753 | name = "httpdate" 754 | version = "1.0.3" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 757 | 758 | [[package]] 759 | name = "hyper" 760 | version = "1.6.0" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 763 | dependencies = [ 764 | "bytes", 765 | "futures-channel", 766 | "futures-util", 767 | "h2", 768 | "http", 769 | "http-body", 770 | "httparse", 771 | "httpdate", 772 | "itoa", 773 | "pin-project-lite", 774 | "smallvec", 775 | "tokio", 776 | "want", 777 | ] 778 | 779 | [[package]] 780 | name = "hyper-rustls" 781 | version = "0.26.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" 784 | dependencies = [ 785 | "futures-util", 786 | "http", 787 | "hyper", 788 | "hyper-util", 789 | "rustls 0.22.4", 790 | "rustls-pki-types", 791 | "tokio", 792 | "tokio-rustls 0.25.0", 793 | "tower-service", 794 | "webpki-roots 0.26.11", 795 | ] 796 | 797 | [[package]] 798 | name = "hyper-rustls" 799 | version = "0.27.5" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 802 | dependencies = [ 803 | "futures-util", 804 | "http", 805 | "hyper", 806 | "hyper-util", 807 | "rustls 0.23.27", 808 | "rustls-native-certs", 809 | "rustls-pki-types", 810 | "tokio", 811 | "tokio-rustls 0.26.2", 812 | "tower-service", 813 | ] 814 | 815 | [[package]] 816 | name = "hyper-util" 817 | version = "0.1.11" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" 820 | dependencies = [ 821 | "bytes", 822 | "futures-channel", 823 | "futures-util", 824 | "http", 825 | "http-body", 826 | "hyper", 827 | "libc", 828 | "pin-project-lite", 829 | "socket2", 830 | "tokio", 831 | "tower-service", 832 | "tracing", 833 | ] 834 | 835 | [[package]] 836 | name = "iana-time-zone" 837 | version = "0.1.63" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 840 | dependencies = [ 841 | "android_system_properties", 842 | "core-foundation-sys", 843 | "iana-time-zone-haiku", 844 | "js-sys", 845 | "log", 846 | "wasm-bindgen", 847 | "windows-core", 848 | ] 849 | 850 | [[package]] 851 | name = "iana-time-zone-haiku" 852 | version = "0.1.2" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 855 | dependencies = [ 856 | "cc", 857 | ] 858 | 859 | [[package]] 860 | name = "icu_collections" 861 | version = "2.0.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 864 | dependencies = [ 865 | "displaydoc", 866 | "potential_utf", 867 | "yoke", 868 | "zerofrom", 869 | "zerovec", 870 | ] 871 | 872 | [[package]] 873 | name = "icu_locale_core" 874 | version = "2.0.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 877 | dependencies = [ 878 | "displaydoc", 879 | "litemap", 880 | "tinystr", 881 | "writeable", 882 | "zerovec", 883 | ] 884 | 885 | [[package]] 886 | name = "icu_normalizer" 887 | version = "2.0.0" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 890 | dependencies = [ 891 | "displaydoc", 892 | "icu_collections", 893 | "icu_normalizer_data", 894 | "icu_properties", 895 | "icu_provider", 896 | "smallvec", 897 | "zerovec", 898 | ] 899 | 900 | [[package]] 901 | name = "icu_normalizer_data" 902 | version = "2.0.0" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 905 | 906 | [[package]] 907 | name = "icu_properties" 908 | version = "2.0.0" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" 911 | dependencies = [ 912 | "displaydoc", 913 | "icu_collections", 914 | "icu_locale_core", 915 | "icu_properties_data", 916 | "icu_provider", 917 | "potential_utf", 918 | "zerotrie", 919 | "zerovec", 920 | ] 921 | 922 | [[package]] 923 | name = "icu_properties_data" 924 | version = "2.0.0" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" 927 | 928 | [[package]] 929 | name = "icu_provider" 930 | version = "2.0.0" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 933 | dependencies = [ 934 | "displaydoc", 935 | "icu_locale_core", 936 | "stable_deref_trait", 937 | "tinystr", 938 | "writeable", 939 | "yoke", 940 | "zerofrom", 941 | "zerotrie", 942 | "zerovec", 943 | ] 944 | 945 | [[package]] 946 | name = "idna" 947 | version = "1.0.3" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 950 | dependencies = [ 951 | "idna_adapter", 952 | "smallvec", 953 | "utf8_iter", 954 | ] 955 | 956 | [[package]] 957 | name = "idna_adapter" 958 | version = "1.2.1" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 961 | dependencies = [ 962 | "icu_normalizer", 963 | "icu_properties", 964 | ] 965 | 966 | [[package]] 967 | name = "indexmap" 968 | version = "2.9.0" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 971 | dependencies = [ 972 | "equivalent", 973 | "hashbrown", 974 | ] 975 | 976 | [[package]] 977 | name = "inout" 978 | version = "0.1.4" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" 981 | dependencies = [ 982 | "generic-array", 983 | ] 984 | 985 | [[package]] 986 | name = "ipnet" 987 | version = "2.11.0" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 990 | 991 | [[package]] 992 | name = "is_terminal_polyfill" 993 | version = "1.70.1" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 996 | 997 | [[package]] 998 | name = "itoa" 999 | version = "1.0.15" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1002 | 1003 | [[package]] 1004 | name = "js-sys" 1005 | version = "0.3.77" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1008 | dependencies = [ 1009 | "once_cell", 1010 | "wasm-bindgen", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "lazy_static" 1015 | version = "1.5.0" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1018 | 1019 | [[package]] 1020 | name = "libc" 1021 | version = "0.2.172" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 1024 | 1025 | [[package]] 1026 | name = "litemap" 1027 | version = "0.8.0" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1030 | 1031 | [[package]] 1032 | name = "lock_api" 1033 | version = "0.4.12" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 1036 | dependencies = [ 1037 | "autocfg", 1038 | "scopeguard", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "log" 1043 | version = "0.4.27" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1046 | 1047 | [[package]] 1048 | name = "lru-slab" 1049 | version = "0.1.2" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 1052 | 1053 | [[package]] 1054 | name = "matchers" 1055 | version = "0.1.0" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 1058 | dependencies = [ 1059 | "regex-automata 0.1.10", 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "matchit" 1064 | version = "0.8.4" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 1067 | 1068 | [[package]] 1069 | name = "memchr" 1070 | version = "2.7.4" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1073 | 1074 | [[package]] 1075 | name = "mime" 1076 | version = "0.3.17" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1079 | 1080 | [[package]] 1081 | name = "miniz_oxide" 1082 | version = "0.8.8" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 1085 | dependencies = [ 1086 | "adler2", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "mio" 1091 | version = "1.0.3" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1094 | dependencies = [ 1095 | "libc", 1096 | "wasi 0.11.0+wasi-snapshot-preview1", 1097 | "windows-sys 0.52.0", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "nu-ansi-term" 1102 | version = "0.46.0" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1105 | dependencies = [ 1106 | "overload", 1107 | "winapi", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "num-conv" 1112 | version = "0.1.0" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1115 | 1116 | [[package]] 1117 | name = "num-traits" 1118 | version = "0.2.19" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1121 | dependencies = [ 1122 | "autocfg", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "num_cpus" 1127 | version = "1.16.0" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 1130 | dependencies = [ 1131 | "hermit-abi", 1132 | "libc", 1133 | ] 1134 | 1135 | [[package]] 1136 | name = "num_threads" 1137 | version = "0.1.7" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 1140 | dependencies = [ 1141 | "libc", 1142 | ] 1143 | 1144 | [[package]] 1145 | name = "object" 1146 | version = "0.36.7" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1149 | dependencies = [ 1150 | "memchr", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "once_cell" 1155 | version = "1.21.3" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1158 | 1159 | [[package]] 1160 | name = "opaque-debug" 1161 | version = "0.3.1" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" 1164 | 1165 | [[package]] 1166 | name = "openssl" 1167 | version = "0.10.72" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" 1170 | dependencies = [ 1171 | "bitflags", 1172 | "cfg-if", 1173 | "foreign-types", 1174 | "libc", 1175 | "once_cell", 1176 | "openssl-macros", 1177 | "openssl-sys", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "openssl-macros" 1182 | version = "0.1.1" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1185 | dependencies = [ 1186 | "proc-macro2", 1187 | "quote", 1188 | "syn", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "openssl-probe" 1193 | version = "0.1.6" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1196 | 1197 | [[package]] 1198 | name = "openssl-sys" 1199 | version = "0.9.108" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" 1202 | dependencies = [ 1203 | "cc", 1204 | "libc", 1205 | "pkg-config", 1206 | "vcpkg", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "overload" 1211 | version = "0.1.1" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1214 | 1215 | [[package]] 1216 | name = "parking_lot" 1217 | version = "0.12.3" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1220 | dependencies = [ 1221 | "lock_api", 1222 | "parking_lot_core", 1223 | ] 1224 | 1225 | [[package]] 1226 | name = "parking_lot_core" 1227 | version = "0.9.10" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1230 | dependencies = [ 1231 | "cfg-if", 1232 | "libc", 1233 | "redox_syscall", 1234 | "smallvec", 1235 | "windows-targets 0.52.6", 1236 | ] 1237 | 1238 | [[package]] 1239 | name = "percent-encoding" 1240 | version = "2.3.1" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1243 | 1244 | [[package]] 1245 | name = "pin-project-lite" 1246 | version = "0.2.16" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1249 | 1250 | [[package]] 1251 | name = "pin-utils" 1252 | version = "0.1.0" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1255 | 1256 | [[package]] 1257 | name = "pkg-config" 1258 | version = "0.3.32" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1261 | 1262 | [[package]] 1263 | name = "poly1305" 1264 | version = "0.8.0" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" 1267 | dependencies = [ 1268 | "cpufeatures", 1269 | "opaque-debug", 1270 | "universal-hash", 1271 | ] 1272 | 1273 | [[package]] 1274 | name = "potential_utf" 1275 | version = "0.1.2" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 1278 | dependencies = [ 1279 | "zerovec", 1280 | ] 1281 | 1282 | [[package]] 1283 | name = "powerfmt" 1284 | version = "0.2.0" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1287 | 1288 | [[package]] 1289 | name = "ppv-lite86" 1290 | version = "0.2.21" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1293 | dependencies = [ 1294 | "zerocopy", 1295 | ] 1296 | 1297 | [[package]] 1298 | name = "proc-macro2" 1299 | version = "1.0.95" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1302 | dependencies = [ 1303 | "unicode-ident", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "push-relay" 1308 | version = "5.0.4" 1309 | dependencies = [ 1310 | "a2", 1311 | "aead", 1312 | "anyhow", 1313 | "argparse", 1314 | "axum", 1315 | "base64 0.22.1", 1316 | "chrono", 1317 | "clap", 1318 | "crypto_secretbox", 1319 | "data-encoding", 1320 | "form_urlencoded", 1321 | "futures", 1322 | "hostname", 1323 | "openssl", 1324 | "rand 0.8.5", 1325 | "reqwest", 1326 | "salsa20", 1327 | "serde", 1328 | "serde_json", 1329 | "thiserror 2.0.12", 1330 | "tokio", 1331 | "toml", 1332 | "tower", 1333 | "tower-http", 1334 | "tracing", 1335 | "tracing-subscriber", 1336 | "wiremock", 1337 | "x25519-dalek", 1338 | "yup-oauth2", 1339 | "zeroize", 1340 | ] 1341 | 1342 | [[package]] 1343 | name = "quinn" 1344 | version = "0.11.8" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" 1347 | dependencies = [ 1348 | "bytes", 1349 | "cfg_aliases", 1350 | "pin-project-lite", 1351 | "quinn-proto", 1352 | "quinn-udp", 1353 | "rustc-hash", 1354 | "rustls 0.23.27", 1355 | "socket2", 1356 | "thiserror 2.0.12", 1357 | "tokio", 1358 | "tracing", 1359 | "web-time", 1360 | ] 1361 | 1362 | [[package]] 1363 | name = "quinn-proto" 1364 | version = "0.11.12" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" 1367 | dependencies = [ 1368 | "bytes", 1369 | "getrandom 0.3.3", 1370 | "lru-slab", 1371 | "rand 0.9.1", 1372 | "ring", 1373 | "rustc-hash", 1374 | "rustls 0.23.27", 1375 | "rustls-pki-types", 1376 | "slab", 1377 | "thiserror 2.0.12", 1378 | "tinyvec", 1379 | "tracing", 1380 | "web-time", 1381 | ] 1382 | 1383 | [[package]] 1384 | name = "quinn-udp" 1385 | version = "0.5.12" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" 1388 | dependencies = [ 1389 | "cfg_aliases", 1390 | "libc", 1391 | "once_cell", 1392 | "socket2", 1393 | "tracing", 1394 | "windows-sys 0.59.0", 1395 | ] 1396 | 1397 | [[package]] 1398 | name = "quote" 1399 | version = "1.0.40" 1400 | source = "registry+https://github.com/rust-lang/crates.io-index" 1401 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1402 | dependencies = [ 1403 | "proc-macro2", 1404 | ] 1405 | 1406 | [[package]] 1407 | name = "r-efi" 1408 | version = "5.2.0" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1411 | 1412 | [[package]] 1413 | name = "rand" 1414 | version = "0.8.5" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1417 | dependencies = [ 1418 | "libc", 1419 | "rand_chacha 0.3.1", 1420 | "rand_core 0.6.4", 1421 | ] 1422 | 1423 | [[package]] 1424 | name = "rand" 1425 | version = "0.9.1" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1428 | dependencies = [ 1429 | "rand_chacha 0.9.0", 1430 | "rand_core 0.9.3", 1431 | ] 1432 | 1433 | [[package]] 1434 | name = "rand_chacha" 1435 | version = "0.3.1" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1438 | dependencies = [ 1439 | "ppv-lite86", 1440 | "rand_core 0.6.4", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "rand_chacha" 1445 | version = "0.9.0" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1448 | dependencies = [ 1449 | "ppv-lite86", 1450 | "rand_core 0.9.3", 1451 | ] 1452 | 1453 | [[package]] 1454 | name = "rand_core" 1455 | version = "0.6.4" 1456 | source = "registry+https://github.com/rust-lang/crates.io-index" 1457 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1458 | dependencies = [ 1459 | "getrandom 0.2.16", 1460 | ] 1461 | 1462 | [[package]] 1463 | name = "rand_core" 1464 | version = "0.9.3" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1467 | dependencies = [ 1468 | "getrandom 0.3.3", 1469 | ] 1470 | 1471 | [[package]] 1472 | name = "redox_syscall" 1473 | version = "0.5.12" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 1476 | dependencies = [ 1477 | "bitflags", 1478 | ] 1479 | 1480 | [[package]] 1481 | name = "regex" 1482 | version = "1.11.1" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1485 | dependencies = [ 1486 | "aho-corasick", 1487 | "memchr", 1488 | "regex-automata 0.4.9", 1489 | "regex-syntax 0.8.5", 1490 | ] 1491 | 1492 | [[package]] 1493 | name = "regex-automata" 1494 | version = "0.1.10" 1495 | source = "registry+https://github.com/rust-lang/crates.io-index" 1496 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1497 | dependencies = [ 1498 | "regex-syntax 0.6.29", 1499 | ] 1500 | 1501 | [[package]] 1502 | name = "regex-automata" 1503 | version = "0.4.9" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1506 | dependencies = [ 1507 | "aho-corasick", 1508 | "memchr", 1509 | "regex-syntax 0.8.5", 1510 | ] 1511 | 1512 | [[package]] 1513 | name = "regex-syntax" 1514 | version = "0.6.29" 1515 | source = "registry+https://github.com/rust-lang/crates.io-index" 1516 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1517 | 1518 | [[package]] 1519 | name = "regex-syntax" 1520 | version = "0.8.5" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1523 | 1524 | [[package]] 1525 | name = "reqwest" 1526 | version = "0.12.15" 1527 | source = "registry+https://github.com/rust-lang/crates.io-index" 1528 | checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 1529 | dependencies = [ 1530 | "base64 0.22.1", 1531 | "bytes", 1532 | "futures-core", 1533 | "futures-util", 1534 | "h2", 1535 | "http", 1536 | "http-body", 1537 | "http-body-util", 1538 | "hyper", 1539 | "hyper-rustls 0.27.5", 1540 | "hyper-util", 1541 | "ipnet", 1542 | "js-sys", 1543 | "log", 1544 | "mime", 1545 | "once_cell", 1546 | "percent-encoding", 1547 | "pin-project-lite", 1548 | "quinn", 1549 | "rustls 0.23.27", 1550 | "rustls-native-certs", 1551 | "rustls-pemfile", 1552 | "rustls-pki-types", 1553 | "serde", 1554 | "serde_json", 1555 | "serde_urlencoded", 1556 | "sync_wrapper", 1557 | "tokio", 1558 | "tokio-rustls 0.26.2", 1559 | "tower", 1560 | "tower-service", 1561 | "url", 1562 | "wasm-bindgen", 1563 | "wasm-bindgen-futures", 1564 | "web-sys", 1565 | "windows-registry", 1566 | ] 1567 | 1568 | [[package]] 1569 | name = "ring" 1570 | version = "0.17.14" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1573 | dependencies = [ 1574 | "cc", 1575 | "cfg-if", 1576 | "getrandom 0.2.16", 1577 | "libc", 1578 | "untrusted", 1579 | "windows-sys 0.52.0", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "rustc-demangle" 1584 | version = "0.1.24" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1587 | 1588 | [[package]] 1589 | name = "rustc-hash" 1590 | version = "2.1.1" 1591 | source = "registry+https://github.com/rust-lang/crates.io-index" 1592 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1593 | 1594 | [[package]] 1595 | name = "rustc_version" 1596 | version = "0.4.1" 1597 | source = "registry+https://github.com/rust-lang/crates.io-index" 1598 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 1599 | dependencies = [ 1600 | "semver", 1601 | ] 1602 | 1603 | [[package]] 1604 | name = "rustls" 1605 | version = "0.22.4" 1606 | source = "registry+https://github.com/rust-lang/crates.io-index" 1607 | checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" 1608 | dependencies = [ 1609 | "log", 1610 | "ring", 1611 | "rustls-pki-types", 1612 | "rustls-webpki 0.102.8", 1613 | "subtle", 1614 | "zeroize", 1615 | ] 1616 | 1617 | [[package]] 1618 | name = "rustls" 1619 | version = "0.23.27" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" 1622 | dependencies = [ 1623 | "once_cell", 1624 | "ring", 1625 | "rustls-pki-types", 1626 | "rustls-webpki 0.103.3", 1627 | "subtle", 1628 | "zeroize", 1629 | ] 1630 | 1631 | [[package]] 1632 | name = "rustls-native-certs" 1633 | version = "0.8.1" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 1636 | dependencies = [ 1637 | "openssl-probe", 1638 | "rustls-pki-types", 1639 | "schannel", 1640 | "security-framework", 1641 | ] 1642 | 1643 | [[package]] 1644 | name = "rustls-pemfile" 1645 | version = "2.2.0" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1648 | dependencies = [ 1649 | "rustls-pki-types", 1650 | ] 1651 | 1652 | [[package]] 1653 | name = "rustls-pki-types" 1654 | version = "1.12.0" 1655 | source = "registry+https://github.com/rust-lang/crates.io-index" 1656 | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 1657 | dependencies = [ 1658 | "web-time", 1659 | "zeroize", 1660 | ] 1661 | 1662 | [[package]] 1663 | name = "rustls-webpki" 1664 | version = "0.102.8" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 1667 | dependencies = [ 1668 | "ring", 1669 | "rustls-pki-types", 1670 | "untrusted", 1671 | ] 1672 | 1673 | [[package]] 1674 | name = "rustls-webpki" 1675 | version = "0.103.3" 1676 | source = "registry+https://github.com/rust-lang/crates.io-index" 1677 | checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" 1678 | dependencies = [ 1679 | "ring", 1680 | "rustls-pki-types", 1681 | "untrusted", 1682 | ] 1683 | 1684 | [[package]] 1685 | name = "rustversion" 1686 | version = "1.0.20" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1689 | 1690 | [[package]] 1691 | name = "ryu" 1692 | version = "1.0.20" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1695 | 1696 | [[package]] 1697 | name = "salsa20" 1698 | version = "0.10.2" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" 1701 | dependencies = [ 1702 | "cipher", 1703 | ] 1704 | 1705 | [[package]] 1706 | name = "schannel" 1707 | version = "0.1.27" 1708 | source = "registry+https://github.com/rust-lang/crates.io-index" 1709 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1710 | dependencies = [ 1711 | "windows-sys 0.59.0", 1712 | ] 1713 | 1714 | [[package]] 1715 | name = "scopeguard" 1716 | version = "1.2.0" 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" 1718 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1719 | 1720 | [[package]] 1721 | name = "seahash" 1722 | version = "4.1.0" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" 1725 | 1726 | [[package]] 1727 | name = "security-framework" 1728 | version = "3.2.0" 1729 | source = "registry+https://github.com/rust-lang/crates.io-index" 1730 | checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" 1731 | dependencies = [ 1732 | "bitflags", 1733 | "core-foundation", 1734 | "core-foundation-sys", 1735 | "libc", 1736 | "security-framework-sys", 1737 | ] 1738 | 1739 | [[package]] 1740 | name = "security-framework-sys" 1741 | version = "2.14.0" 1742 | source = "registry+https://github.com/rust-lang/crates.io-index" 1743 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1744 | dependencies = [ 1745 | "core-foundation-sys", 1746 | "libc", 1747 | ] 1748 | 1749 | [[package]] 1750 | name = "semver" 1751 | version = "1.0.26" 1752 | source = "registry+https://github.com/rust-lang/crates.io-index" 1753 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 1754 | 1755 | [[package]] 1756 | name = "serde" 1757 | version = "1.0.219" 1758 | source = "registry+https://github.com/rust-lang/crates.io-index" 1759 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1760 | dependencies = [ 1761 | "serde_derive", 1762 | ] 1763 | 1764 | [[package]] 1765 | name = "serde_derive" 1766 | version = "1.0.219" 1767 | source = "registry+https://github.com/rust-lang/crates.io-index" 1768 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1769 | dependencies = [ 1770 | "proc-macro2", 1771 | "quote", 1772 | "syn", 1773 | ] 1774 | 1775 | [[package]] 1776 | name = "serde_json" 1777 | version = "1.0.140" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1780 | dependencies = [ 1781 | "itoa", 1782 | "memchr", 1783 | "ryu", 1784 | "serde", 1785 | ] 1786 | 1787 | [[package]] 1788 | name = "serde_spanned" 1789 | version = "0.6.8" 1790 | source = "registry+https://github.com/rust-lang/crates.io-index" 1791 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1792 | dependencies = [ 1793 | "serde", 1794 | ] 1795 | 1796 | [[package]] 1797 | name = "serde_urlencoded" 1798 | version = "0.7.1" 1799 | source = "registry+https://github.com/rust-lang/crates.io-index" 1800 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1801 | dependencies = [ 1802 | "form_urlencoded", 1803 | "itoa", 1804 | "ryu", 1805 | "serde", 1806 | ] 1807 | 1808 | [[package]] 1809 | name = "sharded-slab" 1810 | version = "0.1.7" 1811 | source = "registry+https://github.com/rust-lang/crates.io-index" 1812 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1813 | dependencies = [ 1814 | "lazy_static", 1815 | ] 1816 | 1817 | [[package]] 1818 | name = "shlex" 1819 | version = "1.3.0" 1820 | source = "registry+https://github.com/rust-lang/crates.io-index" 1821 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1822 | 1823 | [[package]] 1824 | name = "slab" 1825 | version = "0.4.9" 1826 | source = "registry+https://github.com/rust-lang/crates.io-index" 1827 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1828 | dependencies = [ 1829 | "autocfg", 1830 | ] 1831 | 1832 | [[package]] 1833 | name = "smallvec" 1834 | version = "1.15.0" 1835 | source = "registry+https://github.com/rust-lang/crates.io-index" 1836 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1837 | 1838 | [[package]] 1839 | name = "socket2" 1840 | version = "0.5.9" 1841 | source = "registry+https://github.com/rust-lang/crates.io-index" 1842 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1843 | dependencies = [ 1844 | "libc", 1845 | "windows-sys 0.52.0", 1846 | ] 1847 | 1848 | [[package]] 1849 | name = "stable_deref_trait" 1850 | version = "1.2.0" 1851 | source = "registry+https://github.com/rust-lang/crates.io-index" 1852 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1853 | 1854 | [[package]] 1855 | name = "strsim" 1856 | version = "0.11.1" 1857 | source = "registry+https://github.com/rust-lang/crates.io-index" 1858 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1859 | 1860 | [[package]] 1861 | name = "subtle" 1862 | version = "2.6.1" 1863 | source = "registry+https://github.com/rust-lang/crates.io-index" 1864 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1865 | 1866 | [[package]] 1867 | name = "syn" 1868 | version = "2.0.101" 1869 | source = "registry+https://github.com/rust-lang/crates.io-index" 1870 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 1871 | dependencies = [ 1872 | "proc-macro2", 1873 | "quote", 1874 | "unicode-ident", 1875 | ] 1876 | 1877 | [[package]] 1878 | name = "sync_wrapper" 1879 | version = "1.0.2" 1880 | source = "registry+https://github.com/rust-lang/crates.io-index" 1881 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1882 | dependencies = [ 1883 | "futures-core", 1884 | ] 1885 | 1886 | [[package]] 1887 | name = "synstructure" 1888 | version = "0.13.2" 1889 | source = "registry+https://github.com/rust-lang/crates.io-index" 1890 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1891 | dependencies = [ 1892 | "proc-macro2", 1893 | "quote", 1894 | "syn", 1895 | ] 1896 | 1897 | [[package]] 1898 | name = "thiserror" 1899 | version = "1.0.69" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1902 | dependencies = [ 1903 | "thiserror-impl 1.0.69", 1904 | ] 1905 | 1906 | [[package]] 1907 | name = "thiserror" 1908 | version = "2.0.12" 1909 | source = "registry+https://github.com/rust-lang/crates.io-index" 1910 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1911 | dependencies = [ 1912 | "thiserror-impl 2.0.12", 1913 | ] 1914 | 1915 | [[package]] 1916 | name = "thiserror-impl" 1917 | version = "1.0.69" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1920 | dependencies = [ 1921 | "proc-macro2", 1922 | "quote", 1923 | "syn", 1924 | ] 1925 | 1926 | [[package]] 1927 | name = "thiserror-impl" 1928 | version = "2.0.12" 1929 | source = "registry+https://github.com/rust-lang/crates.io-index" 1930 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1931 | dependencies = [ 1932 | "proc-macro2", 1933 | "quote", 1934 | "syn", 1935 | ] 1936 | 1937 | [[package]] 1938 | name = "thread_local" 1939 | version = "1.1.8" 1940 | source = "registry+https://github.com/rust-lang/crates.io-index" 1941 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1942 | dependencies = [ 1943 | "cfg-if", 1944 | "once_cell", 1945 | ] 1946 | 1947 | [[package]] 1948 | name = "time" 1949 | version = "0.3.41" 1950 | source = "registry+https://github.com/rust-lang/crates.io-index" 1951 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 1952 | dependencies = [ 1953 | "deranged", 1954 | "libc", 1955 | "num-conv", 1956 | "num_threads", 1957 | "powerfmt", 1958 | "serde", 1959 | "time-core", 1960 | "time-macros", 1961 | ] 1962 | 1963 | [[package]] 1964 | name = "time-core" 1965 | version = "0.1.4" 1966 | source = "registry+https://github.com/rust-lang/crates.io-index" 1967 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 1968 | 1969 | [[package]] 1970 | name = "time-macros" 1971 | version = "0.2.22" 1972 | source = "registry+https://github.com/rust-lang/crates.io-index" 1973 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 1974 | dependencies = [ 1975 | "num-conv", 1976 | "time-core", 1977 | ] 1978 | 1979 | [[package]] 1980 | name = "tinystr" 1981 | version = "0.8.1" 1982 | source = "registry+https://github.com/rust-lang/crates.io-index" 1983 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1984 | dependencies = [ 1985 | "displaydoc", 1986 | "zerovec", 1987 | ] 1988 | 1989 | [[package]] 1990 | name = "tinyvec" 1991 | version = "1.9.0" 1992 | source = "registry+https://github.com/rust-lang/crates.io-index" 1993 | checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 1994 | dependencies = [ 1995 | "tinyvec_macros", 1996 | ] 1997 | 1998 | [[package]] 1999 | name = "tinyvec_macros" 2000 | version = "0.1.1" 2001 | source = "registry+https://github.com/rust-lang/crates.io-index" 2002 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2003 | 2004 | [[package]] 2005 | name = "tokio" 2006 | version = "1.45.0" 2007 | source = "registry+https://github.com/rust-lang/crates.io-index" 2008 | checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" 2009 | dependencies = [ 2010 | "backtrace", 2011 | "bytes", 2012 | "libc", 2013 | "mio", 2014 | "pin-project-lite", 2015 | "socket2", 2016 | "tokio-macros", 2017 | "windows-sys 0.52.0", 2018 | ] 2019 | 2020 | [[package]] 2021 | name = "tokio-macros" 2022 | version = "2.5.0" 2023 | source = "registry+https://github.com/rust-lang/crates.io-index" 2024 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2025 | dependencies = [ 2026 | "proc-macro2", 2027 | "quote", 2028 | "syn", 2029 | ] 2030 | 2031 | [[package]] 2032 | name = "tokio-rustls" 2033 | version = "0.25.0" 2034 | source = "registry+https://github.com/rust-lang/crates.io-index" 2035 | checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" 2036 | dependencies = [ 2037 | "rustls 0.22.4", 2038 | "rustls-pki-types", 2039 | "tokio", 2040 | ] 2041 | 2042 | [[package]] 2043 | name = "tokio-rustls" 2044 | version = "0.26.2" 2045 | source = "registry+https://github.com/rust-lang/crates.io-index" 2046 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 2047 | dependencies = [ 2048 | "rustls 0.23.27", 2049 | "tokio", 2050 | ] 2051 | 2052 | [[package]] 2053 | name = "tokio-util" 2054 | version = "0.7.15" 2055 | source = "registry+https://github.com/rust-lang/crates.io-index" 2056 | checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 2057 | dependencies = [ 2058 | "bytes", 2059 | "futures-core", 2060 | "futures-sink", 2061 | "pin-project-lite", 2062 | "tokio", 2063 | ] 2064 | 2065 | [[package]] 2066 | name = "toml" 2067 | version = "0.8.22" 2068 | source = "registry+https://github.com/rust-lang/crates.io-index" 2069 | checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" 2070 | dependencies = [ 2071 | "serde", 2072 | "serde_spanned", 2073 | "toml_datetime", 2074 | "toml_edit", 2075 | ] 2076 | 2077 | [[package]] 2078 | name = "toml_datetime" 2079 | version = "0.6.9" 2080 | source = "registry+https://github.com/rust-lang/crates.io-index" 2081 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 2082 | dependencies = [ 2083 | "serde", 2084 | ] 2085 | 2086 | [[package]] 2087 | name = "toml_edit" 2088 | version = "0.22.26" 2089 | source = "registry+https://github.com/rust-lang/crates.io-index" 2090 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 2091 | dependencies = [ 2092 | "indexmap", 2093 | "serde", 2094 | "serde_spanned", 2095 | "toml_datetime", 2096 | "toml_write", 2097 | "winnow", 2098 | ] 2099 | 2100 | [[package]] 2101 | name = "toml_write" 2102 | version = "0.1.1" 2103 | source = "registry+https://github.com/rust-lang/crates.io-index" 2104 | checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" 2105 | 2106 | [[package]] 2107 | name = "tower" 2108 | version = "0.5.2" 2109 | source = "registry+https://github.com/rust-lang/crates.io-index" 2110 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2111 | dependencies = [ 2112 | "futures-core", 2113 | "futures-util", 2114 | "pin-project-lite", 2115 | "sync_wrapper", 2116 | "tokio", 2117 | "tower-layer", 2118 | "tower-service", 2119 | ] 2120 | 2121 | [[package]] 2122 | name = "tower-http" 2123 | version = "0.6.4" 2124 | source = "registry+https://github.com/rust-lang/crates.io-index" 2125 | checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" 2126 | dependencies = [ 2127 | "bitflags", 2128 | "bytes", 2129 | "http", 2130 | "http-body", 2131 | "pin-project-lite", 2132 | "tower-layer", 2133 | "tower-service", 2134 | "tracing", 2135 | ] 2136 | 2137 | [[package]] 2138 | name = "tower-layer" 2139 | version = "0.3.3" 2140 | source = "registry+https://github.com/rust-lang/crates.io-index" 2141 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2142 | 2143 | [[package]] 2144 | name = "tower-service" 2145 | version = "0.3.3" 2146 | source = "registry+https://github.com/rust-lang/crates.io-index" 2147 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2148 | 2149 | [[package]] 2150 | name = "tracing" 2151 | version = "0.1.41" 2152 | source = "registry+https://github.com/rust-lang/crates.io-index" 2153 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2154 | dependencies = [ 2155 | "pin-project-lite", 2156 | "tracing-attributes", 2157 | "tracing-core", 2158 | ] 2159 | 2160 | [[package]] 2161 | name = "tracing-attributes" 2162 | version = "0.1.28" 2163 | source = "registry+https://github.com/rust-lang/crates.io-index" 2164 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 2165 | dependencies = [ 2166 | "proc-macro2", 2167 | "quote", 2168 | "syn", 2169 | ] 2170 | 2171 | [[package]] 2172 | name = "tracing-core" 2173 | version = "0.1.33" 2174 | source = "registry+https://github.com/rust-lang/crates.io-index" 2175 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 2176 | dependencies = [ 2177 | "once_cell", 2178 | "valuable", 2179 | ] 2180 | 2181 | [[package]] 2182 | name = "tracing-log" 2183 | version = "0.2.0" 2184 | source = "registry+https://github.com/rust-lang/crates.io-index" 2185 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2186 | dependencies = [ 2187 | "log", 2188 | "once_cell", 2189 | "tracing-core", 2190 | ] 2191 | 2192 | [[package]] 2193 | name = "tracing-subscriber" 2194 | version = "0.3.19" 2195 | source = "registry+https://github.com/rust-lang/crates.io-index" 2196 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 2197 | dependencies = [ 2198 | "matchers", 2199 | "nu-ansi-term", 2200 | "once_cell", 2201 | "regex", 2202 | "sharded-slab", 2203 | "smallvec", 2204 | "thread_local", 2205 | "tracing", 2206 | "tracing-core", 2207 | "tracing-log", 2208 | ] 2209 | 2210 | [[package]] 2211 | name = "try-lock" 2212 | version = "0.2.5" 2213 | source = "registry+https://github.com/rust-lang/crates.io-index" 2214 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2215 | 2216 | [[package]] 2217 | name = "typenum" 2218 | version = "1.18.0" 2219 | source = "registry+https://github.com/rust-lang/crates.io-index" 2220 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 2221 | 2222 | [[package]] 2223 | name = "unicode-ident" 2224 | version = "1.0.18" 2225 | source = "registry+https://github.com/rust-lang/crates.io-index" 2226 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2227 | 2228 | [[package]] 2229 | name = "universal-hash" 2230 | version = "0.5.1" 2231 | source = "registry+https://github.com/rust-lang/crates.io-index" 2232 | checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" 2233 | dependencies = [ 2234 | "crypto-common", 2235 | "subtle", 2236 | ] 2237 | 2238 | [[package]] 2239 | name = "untrusted" 2240 | version = "0.9.0" 2241 | source = "registry+https://github.com/rust-lang/crates.io-index" 2242 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2243 | 2244 | [[package]] 2245 | name = "url" 2246 | version = "2.5.4" 2247 | source = "registry+https://github.com/rust-lang/crates.io-index" 2248 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 2249 | dependencies = [ 2250 | "form_urlencoded", 2251 | "idna", 2252 | "percent-encoding", 2253 | ] 2254 | 2255 | [[package]] 2256 | name = "utf8_iter" 2257 | version = "1.0.4" 2258 | source = "registry+https://github.com/rust-lang/crates.io-index" 2259 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2260 | 2261 | [[package]] 2262 | name = "utf8parse" 2263 | version = "0.2.2" 2264 | source = "registry+https://github.com/rust-lang/crates.io-index" 2265 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2266 | 2267 | [[package]] 2268 | name = "valuable" 2269 | version = "0.1.1" 2270 | source = "registry+https://github.com/rust-lang/crates.io-index" 2271 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 2272 | 2273 | [[package]] 2274 | name = "vcpkg" 2275 | version = "0.2.15" 2276 | source = "registry+https://github.com/rust-lang/crates.io-index" 2277 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2278 | 2279 | [[package]] 2280 | name = "version_check" 2281 | version = "0.9.5" 2282 | source = "registry+https://github.com/rust-lang/crates.io-index" 2283 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2284 | 2285 | [[package]] 2286 | name = "want" 2287 | version = "0.3.1" 2288 | source = "registry+https://github.com/rust-lang/crates.io-index" 2289 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2290 | dependencies = [ 2291 | "try-lock", 2292 | ] 2293 | 2294 | [[package]] 2295 | name = "wasi" 2296 | version = "0.11.0+wasi-snapshot-preview1" 2297 | source = "registry+https://github.com/rust-lang/crates.io-index" 2298 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2299 | 2300 | [[package]] 2301 | name = "wasi" 2302 | version = "0.14.2+wasi-0.2.4" 2303 | source = "registry+https://github.com/rust-lang/crates.io-index" 2304 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 2305 | dependencies = [ 2306 | "wit-bindgen-rt", 2307 | ] 2308 | 2309 | [[package]] 2310 | name = "wasm-bindgen" 2311 | version = "0.2.100" 2312 | source = "registry+https://github.com/rust-lang/crates.io-index" 2313 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 2314 | dependencies = [ 2315 | "cfg-if", 2316 | "once_cell", 2317 | "rustversion", 2318 | "wasm-bindgen-macro", 2319 | ] 2320 | 2321 | [[package]] 2322 | name = "wasm-bindgen-backend" 2323 | version = "0.2.100" 2324 | source = "registry+https://github.com/rust-lang/crates.io-index" 2325 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 2326 | dependencies = [ 2327 | "bumpalo", 2328 | "log", 2329 | "proc-macro2", 2330 | "quote", 2331 | "syn", 2332 | "wasm-bindgen-shared", 2333 | ] 2334 | 2335 | [[package]] 2336 | name = "wasm-bindgen-futures" 2337 | version = "0.4.50" 2338 | source = "registry+https://github.com/rust-lang/crates.io-index" 2339 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 2340 | dependencies = [ 2341 | "cfg-if", 2342 | "js-sys", 2343 | "once_cell", 2344 | "wasm-bindgen", 2345 | "web-sys", 2346 | ] 2347 | 2348 | [[package]] 2349 | name = "wasm-bindgen-macro" 2350 | version = "0.2.100" 2351 | source = "registry+https://github.com/rust-lang/crates.io-index" 2352 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 2353 | dependencies = [ 2354 | "quote", 2355 | "wasm-bindgen-macro-support", 2356 | ] 2357 | 2358 | [[package]] 2359 | name = "wasm-bindgen-macro-support" 2360 | version = "0.2.100" 2361 | source = "registry+https://github.com/rust-lang/crates.io-index" 2362 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 2363 | dependencies = [ 2364 | "proc-macro2", 2365 | "quote", 2366 | "syn", 2367 | "wasm-bindgen-backend", 2368 | "wasm-bindgen-shared", 2369 | ] 2370 | 2371 | [[package]] 2372 | name = "wasm-bindgen-shared" 2373 | version = "0.2.100" 2374 | source = "registry+https://github.com/rust-lang/crates.io-index" 2375 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 2376 | dependencies = [ 2377 | "unicode-ident", 2378 | ] 2379 | 2380 | [[package]] 2381 | name = "web-sys" 2382 | version = "0.3.77" 2383 | source = "registry+https://github.com/rust-lang/crates.io-index" 2384 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 2385 | dependencies = [ 2386 | "js-sys", 2387 | "wasm-bindgen", 2388 | ] 2389 | 2390 | [[package]] 2391 | name = "web-time" 2392 | version = "1.1.0" 2393 | source = "registry+https://github.com/rust-lang/crates.io-index" 2394 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2395 | dependencies = [ 2396 | "js-sys", 2397 | "wasm-bindgen", 2398 | ] 2399 | 2400 | [[package]] 2401 | name = "webpki-roots" 2402 | version = "0.26.11" 2403 | source = "registry+https://github.com/rust-lang/crates.io-index" 2404 | checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 2405 | dependencies = [ 2406 | "webpki-roots 1.0.0", 2407 | ] 2408 | 2409 | [[package]] 2410 | name = "webpki-roots" 2411 | version = "1.0.0" 2412 | source = "registry+https://github.com/rust-lang/crates.io-index" 2413 | checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" 2414 | dependencies = [ 2415 | "rustls-pki-types", 2416 | ] 2417 | 2418 | [[package]] 2419 | name = "winapi" 2420 | version = "0.3.9" 2421 | source = "registry+https://github.com/rust-lang/crates.io-index" 2422 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2423 | dependencies = [ 2424 | "winapi-i686-pc-windows-gnu", 2425 | "winapi-x86_64-pc-windows-gnu", 2426 | ] 2427 | 2428 | [[package]] 2429 | name = "winapi-i686-pc-windows-gnu" 2430 | version = "0.4.0" 2431 | source = "registry+https://github.com/rust-lang/crates.io-index" 2432 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2433 | 2434 | [[package]] 2435 | name = "winapi-x86_64-pc-windows-gnu" 2436 | version = "0.4.0" 2437 | source = "registry+https://github.com/rust-lang/crates.io-index" 2438 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2439 | 2440 | [[package]] 2441 | name = "windows-core" 2442 | version = "0.61.1" 2443 | source = "registry+https://github.com/rust-lang/crates.io-index" 2444 | checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" 2445 | dependencies = [ 2446 | "windows-implement", 2447 | "windows-interface", 2448 | "windows-link", 2449 | "windows-result", 2450 | "windows-strings 0.4.1", 2451 | ] 2452 | 2453 | [[package]] 2454 | name = "windows-implement" 2455 | version = "0.60.0" 2456 | source = "registry+https://github.com/rust-lang/crates.io-index" 2457 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 2458 | dependencies = [ 2459 | "proc-macro2", 2460 | "quote", 2461 | "syn", 2462 | ] 2463 | 2464 | [[package]] 2465 | name = "windows-interface" 2466 | version = "0.59.1" 2467 | source = "registry+https://github.com/rust-lang/crates.io-index" 2468 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 2469 | dependencies = [ 2470 | "proc-macro2", 2471 | "quote", 2472 | "syn", 2473 | ] 2474 | 2475 | [[package]] 2476 | name = "windows-link" 2477 | version = "0.1.1" 2478 | source = "registry+https://github.com/rust-lang/crates.io-index" 2479 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 2480 | 2481 | [[package]] 2482 | name = "windows-registry" 2483 | version = "0.4.0" 2484 | source = "registry+https://github.com/rust-lang/crates.io-index" 2485 | checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" 2486 | dependencies = [ 2487 | "windows-result", 2488 | "windows-strings 0.3.1", 2489 | "windows-targets 0.53.0", 2490 | ] 2491 | 2492 | [[package]] 2493 | name = "windows-result" 2494 | version = "0.3.3" 2495 | source = "registry+https://github.com/rust-lang/crates.io-index" 2496 | checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" 2497 | dependencies = [ 2498 | "windows-link", 2499 | ] 2500 | 2501 | [[package]] 2502 | name = "windows-strings" 2503 | version = "0.3.1" 2504 | source = "registry+https://github.com/rust-lang/crates.io-index" 2505 | checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 2506 | dependencies = [ 2507 | "windows-link", 2508 | ] 2509 | 2510 | [[package]] 2511 | name = "windows-strings" 2512 | version = "0.4.1" 2513 | source = "registry+https://github.com/rust-lang/crates.io-index" 2514 | checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" 2515 | dependencies = [ 2516 | "windows-link", 2517 | ] 2518 | 2519 | [[package]] 2520 | name = "windows-sys" 2521 | version = "0.52.0" 2522 | source = "registry+https://github.com/rust-lang/crates.io-index" 2523 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2524 | dependencies = [ 2525 | "windows-targets 0.52.6", 2526 | ] 2527 | 2528 | [[package]] 2529 | name = "windows-sys" 2530 | version = "0.59.0" 2531 | source = "registry+https://github.com/rust-lang/crates.io-index" 2532 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2533 | dependencies = [ 2534 | "windows-targets 0.52.6", 2535 | ] 2536 | 2537 | [[package]] 2538 | name = "windows-targets" 2539 | version = "0.52.6" 2540 | source = "registry+https://github.com/rust-lang/crates.io-index" 2541 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2542 | dependencies = [ 2543 | "windows_aarch64_gnullvm 0.52.6", 2544 | "windows_aarch64_msvc 0.52.6", 2545 | "windows_i686_gnu 0.52.6", 2546 | "windows_i686_gnullvm 0.52.6", 2547 | "windows_i686_msvc 0.52.6", 2548 | "windows_x86_64_gnu 0.52.6", 2549 | "windows_x86_64_gnullvm 0.52.6", 2550 | "windows_x86_64_msvc 0.52.6", 2551 | ] 2552 | 2553 | [[package]] 2554 | name = "windows-targets" 2555 | version = "0.53.0" 2556 | source = "registry+https://github.com/rust-lang/crates.io-index" 2557 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 2558 | dependencies = [ 2559 | "windows_aarch64_gnullvm 0.53.0", 2560 | "windows_aarch64_msvc 0.53.0", 2561 | "windows_i686_gnu 0.53.0", 2562 | "windows_i686_gnullvm 0.53.0", 2563 | "windows_i686_msvc 0.53.0", 2564 | "windows_x86_64_gnu 0.53.0", 2565 | "windows_x86_64_gnullvm 0.53.0", 2566 | "windows_x86_64_msvc 0.53.0", 2567 | ] 2568 | 2569 | [[package]] 2570 | name = "windows_aarch64_gnullvm" 2571 | version = "0.52.6" 2572 | source = "registry+https://github.com/rust-lang/crates.io-index" 2573 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2574 | 2575 | [[package]] 2576 | name = "windows_aarch64_gnullvm" 2577 | version = "0.53.0" 2578 | source = "registry+https://github.com/rust-lang/crates.io-index" 2579 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 2580 | 2581 | [[package]] 2582 | name = "windows_aarch64_msvc" 2583 | version = "0.52.6" 2584 | source = "registry+https://github.com/rust-lang/crates.io-index" 2585 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2586 | 2587 | [[package]] 2588 | name = "windows_aarch64_msvc" 2589 | version = "0.53.0" 2590 | source = "registry+https://github.com/rust-lang/crates.io-index" 2591 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 2592 | 2593 | [[package]] 2594 | name = "windows_i686_gnu" 2595 | version = "0.52.6" 2596 | source = "registry+https://github.com/rust-lang/crates.io-index" 2597 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2598 | 2599 | [[package]] 2600 | name = "windows_i686_gnu" 2601 | version = "0.53.0" 2602 | source = "registry+https://github.com/rust-lang/crates.io-index" 2603 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 2604 | 2605 | [[package]] 2606 | name = "windows_i686_gnullvm" 2607 | version = "0.52.6" 2608 | source = "registry+https://github.com/rust-lang/crates.io-index" 2609 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2610 | 2611 | [[package]] 2612 | name = "windows_i686_gnullvm" 2613 | version = "0.53.0" 2614 | source = "registry+https://github.com/rust-lang/crates.io-index" 2615 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 2616 | 2617 | [[package]] 2618 | name = "windows_i686_msvc" 2619 | version = "0.52.6" 2620 | source = "registry+https://github.com/rust-lang/crates.io-index" 2621 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2622 | 2623 | [[package]] 2624 | name = "windows_i686_msvc" 2625 | version = "0.53.0" 2626 | source = "registry+https://github.com/rust-lang/crates.io-index" 2627 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 2628 | 2629 | [[package]] 2630 | name = "windows_x86_64_gnu" 2631 | version = "0.52.6" 2632 | source = "registry+https://github.com/rust-lang/crates.io-index" 2633 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2634 | 2635 | [[package]] 2636 | name = "windows_x86_64_gnu" 2637 | version = "0.53.0" 2638 | source = "registry+https://github.com/rust-lang/crates.io-index" 2639 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 2640 | 2641 | [[package]] 2642 | name = "windows_x86_64_gnullvm" 2643 | version = "0.52.6" 2644 | source = "registry+https://github.com/rust-lang/crates.io-index" 2645 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2646 | 2647 | [[package]] 2648 | name = "windows_x86_64_gnullvm" 2649 | version = "0.53.0" 2650 | source = "registry+https://github.com/rust-lang/crates.io-index" 2651 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 2652 | 2653 | [[package]] 2654 | name = "windows_x86_64_msvc" 2655 | version = "0.52.6" 2656 | source = "registry+https://github.com/rust-lang/crates.io-index" 2657 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2658 | 2659 | [[package]] 2660 | name = "windows_x86_64_msvc" 2661 | version = "0.53.0" 2662 | source = "registry+https://github.com/rust-lang/crates.io-index" 2663 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 2664 | 2665 | [[package]] 2666 | name = "winnow" 2667 | version = "0.7.10" 2668 | source = "registry+https://github.com/rust-lang/crates.io-index" 2669 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 2670 | dependencies = [ 2671 | "memchr", 2672 | ] 2673 | 2674 | [[package]] 2675 | name = "wiremock" 2676 | version = "0.6.3" 2677 | source = "registry+https://github.com/rust-lang/crates.io-index" 2678 | checksum = "101681b74cd87b5899e87bcf5a64e83334dd313fcd3053ea72e6dba18928e301" 2679 | dependencies = [ 2680 | "assert-json-diff", 2681 | "async-trait", 2682 | "base64 0.22.1", 2683 | "deadpool", 2684 | "futures", 2685 | "http", 2686 | "http-body-util", 2687 | "hyper", 2688 | "hyper-util", 2689 | "log", 2690 | "once_cell", 2691 | "regex", 2692 | "serde", 2693 | "serde_json", 2694 | "tokio", 2695 | "url", 2696 | ] 2697 | 2698 | [[package]] 2699 | name = "wit-bindgen-rt" 2700 | version = "0.39.0" 2701 | source = "registry+https://github.com/rust-lang/crates.io-index" 2702 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2703 | dependencies = [ 2704 | "bitflags", 2705 | ] 2706 | 2707 | [[package]] 2708 | name = "writeable" 2709 | version = "0.6.1" 2710 | source = "registry+https://github.com/rust-lang/crates.io-index" 2711 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 2712 | 2713 | [[package]] 2714 | name = "x25519-dalek" 2715 | version = "2.0.1" 2716 | source = "registry+https://github.com/rust-lang/crates.io-index" 2717 | checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" 2718 | dependencies = [ 2719 | "curve25519-dalek", 2720 | "rand_core 0.6.4", 2721 | "serde", 2722 | "zeroize", 2723 | ] 2724 | 2725 | [[package]] 2726 | name = "yoke" 2727 | version = "0.8.0" 2728 | source = "registry+https://github.com/rust-lang/crates.io-index" 2729 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 2730 | dependencies = [ 2731 | "serde", 2732 | "stable_deref_trait", 2733 | "yoke-derive", 2734 | "zerofrom", 2735 | ] 2736 | 2737 | [[package]] 2738 | name = "yoke-derive" 2739 | version = "0.8.0" 2740 | source = "registry+https://github.com/rust-lang/crates.io-index" 2741 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 2742 | dependencies = [ 2743 | "proc-macro2", 2744 | "quote", 2745 | "syn", 2746 | "synstructure", 2747 | ] 2748 | 2749 | [[package]] 2750 | name = "yup-oauth2" 2751 | version = "11.0.0" 2752 | source = "registry+https://github.com/rust-lang/crates.io-index" 2753 | checksum = "4ed5f19242090128c5809f6535cc7b8d4e2c32433f6c6005800bbc20a644a7f0" 2754 | dependencies = [ 2755 | "anyhow", 2756 | "async-trait", 2757 | "base64 0.22.1", 2758 | "futures", 2759 | "http", 2760 | "http-body-util", 2761 | "hyper", 2762 | "hyper-rustls 0.27.5", 2763 | "hyper-util", 2764 | "log", 2765 | "percent-encoding", 2766 | "rustls 0.23.27", 2767 | "rustls-pemfile", 2768 | "seahash", 2769 | "serde", 2770 | "serde_json", 2771 | "time", 2772 | "tokio", 2773 | "url", 2774 | ] 2775 | 2776 | [[package]] 2777 | name = "zerocopy" 2778 | version = "0.8.25" 2779 | source = "registry+https://github.com/rust-lang/crates.io-index" 2780 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 2781 | dependencies = [ 2782 | "zerocopy-derive", 2783 | ] 2784 | 2785 | [[package]] 2786 | name = "zerocopy-derive" 2787 | version = "0.8.25" 2788 | source = "registry+https://github.com/rust-lang/crates.io-index" 2789 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 2790 | dependencies = [ 2791 | "proc-macro2", 2792 | "quote", 2793 | "syn", 2794 | ] 2795 | 2796 | [[package]] 2797 | name = "zerofrom" 2798 | version = "0.1.6" 2799 | source = "registry+https://github.com/rust-lang/crates.io-index" 2800 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2801 | dependencies = [ 2802 | "zerofrom-derive", 2803 | ] 2804 | 2805 | [[package]] 2806 | name = "zerofrom-derive" 2807 | version = "0.1.6" 2808 | source = "registry+https://github.com/rust-lang/crates.io-index" 2809 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2810 | dependencies = [ 2811 | "proc-macro2", 2812 | "quote", 2813 | "syn", 2814 | "synstructure", 2815 | ] 2816 | 2817 | [[package]] 2818 | name = "zeroize" 2819 | version = "1.8.1" 2820 | source = "registry+https://github.com/rust-lang/crates.io-index" 2821 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2822 | dependencies = [ 2823 | "zeroize_derive", 2824 | ] 2825 | 2826 | [[package]] 2827 | name = "zeroize_derive" 2828 | version = "1.4.2" 2829 | source = "registry+https://github.com/rust-lang/crates.io-index" 2830 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 2831 | dependencies = [ 2832 | "proc-macro2", 2833 | "quote", 2834 | "syn", 2835 | ] 2836 | 2837 | [[package]] 2838 | name = "zerotrie" 2839 | version = "0.2.2" 2840 | source = "registry+https://github.com/rust-lang/crates.io-index" 2841 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 2842 | dependencies = [ 2843 | "displaydoc", 2844 | "yoke", 2845 | "zerofrom", 2846 | ] 2847 | 2848 | [[package]] 2849 | name = "zerovec" 2850 | version = "0.11.2" 2851 | source = "registry+https://github.com/rust-lang/crates.io-index" 2852 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 2853 | dependencies = [ 2854 | "yoke", 2855 | "zerofrom", 2856 | "zerovec-derive", 2857 | ] 2858 | 2859 | [[package]] 2860 | name = "zerovec-derive" 2861 | version = "0.11.1" 2862 | source = "registry+https://github.com/rust-lang/crates.io-index" 2863 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 2864 | dependencies = [ 2865 | "proc-macro2", 2866 | "quote", 2867 | "syn", 2868 | ] 2869 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "push-relay" 3 | description = "This server accepts push requests via HTTP and notifies FCM/APNs push services." 4 | version = "5.0.4" 5 | authors = ["Threema GmbH"] 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/threema-ch/push-relay" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | a2 = { version = "0.10", features = ["tracing"] } 12 | aead = "0.5" 13 | anyhow = "1.0.81" 14 | axum = { version = "0.8.1", features = ["http1", "http2", "tokio"], default-features = false } 15 | base64 = "0.22.0" 16 | chrono = "0.4" 17 | clap = { version = "4.4.7", features = ["std", "derive", "suggestions", "color", "help"], default-features = false } 18 | crypto_secretbox = "0.1" 19 | data-encoding = "2.4" 20 | form_urlencoded = "1" 21 | futures = "0.3" 22 | hostname = "0.4.0" 23 | rand = "0.8.5" 24 | reqwest = { version = "0.12.3", features = ["rustls-tls-native-roots", "http2"], default-features = false } 25 | salsa20 = { version = "0.10", features = ["zeroize"] } 26 | serde = "1.0.27" 27 | serde_json = "1.0.10" 28 | thiserror = "2.0.11" 29 | toml = "0.8.6" 30 | tower = { version = "0.5.2", features = ["util"], default-features = false } 31 | tower-http = { version = "0.6.2", features = ["trace"] } 32 | tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"], default-features = false } 33 | tracing = { version = "0.1.40", default-features = false } 34 | tracing-subscriber = { version = "0.3.18", features = ["tracing-log", "env-filter"] } 35 | x25519-dalek = { version = "2.0.0", features = ["static_secrets", "zeroize"] } 36 | yup-oauth2 = "11.0.0" 37 | zeroize = "1.6" 38 | 39 | [dev-dependencies] 40 | argparse = "*" 41 | openssl = "*" 42 | wiremock = "0.6.0" 43 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2022 Threema GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Push Relay 2 | 3 | [![CI][ci-badge]][ci] 4 | [![License][license-badge]][license] 5 | 6 | This server accepts push requests via HTTP and relays those requests to the appropriate push backends. 7 | 8 | Supported backends: 9 | 10 | - Apple APNs 11 | - Google FCM 12 | - Huawei HMS 13 | - Threema Gateway 14 | 15 | ## Request Format 16 | 17 | - POST request to `/push` 18 | - Request body must use `application/x-www-form-urlencoded` encoding 19 | 20 | Request keys: 21 | 22 | - `type`: `apns`, `fcm`, `hms` or `threema-gateway` 23 | - `token`: The device push token (not provided when using Threema Gateway) 24 | - for FCM: The token itself as received from the OS 25 | - for iOS: The hex encoded token (without bundle id or encryption key appended) 26 | - `session`: SHA256 hash of public permanent key of the initiator 27 | - `version`: Threema Web protocol version 28 | - `affiliation` (optional): An identifier for affiliating consecutive pushes 29 | - `ttl` (optional): The lifespan of a push message, defaults to 90 seconds 30 | - `collapse_key`: (optional) A parameter identifying a group of push messages that can be 31 | collapsed. 32 | - `bundleid` (APNs only): The bundle id to use 33 | - `endpoint` (APNs only): Either `p` (production) or `s` (sandbox) 34 | - `appid` (HMS only): Can be used to differentiate between multiple configs 35 | - `identity` (Threema Gateway only): The Threema ID of the user. 36 | - `public_key` (Threema Gateway only): Public key associated to the Threema ID of the user. 37 | 38 | Examples: 39 | 40 | curl -X POST -H "Origin: https://localhost" localhost:3000/push \ 41 | -d "type=apns&token=asdf&session=123deadbeef&version=3&bundleid=com.example.app&endpoint=s" 42 | curl -X POST -H "Origin: https://localhost" localhost:3000/push \ 43 | -d "type=fcm&token=asdf&session=123deadbeef&version=3" 44 | curl -X POST -H "Origin: https://localhost" localhost:3000/push \ 45 | -d "type=hms&appid=123456&token=asdf&session=123deadbeef&version=3" 46 | curl -X POST -H "Origin: https://localhost" localhost:3000/push \ 47 | -d "type=threema-gateway&session=123deadbeef&version=3&identity=ECHOECHO&public_key=0000000000000000000000000000000000000000000000000000000000000000" 48 | 49 | Possible response codes: 50 | 51 | - `HTTP 204 (No Content)`: Request was processed successfully 52 | - `HTTP 400 (Bad Request)`: Invalid or missing POST parameters (including expired push tokens) 53 | - `HTTP 500 (Internal Server Error)`: Processing of push request failed on the Push Relay server 54 | - `HTTP 502 (Bad Gateway)`: Processing of push request failed on the APNs, FCM, HMS or Threema Gateway server 55 | 56 | ## Push Payload 57 | 58 | The payload format looks like this: 59 | 60 | - `wcs`: Webclient session (sha256 hash of the public permanent key of the 61 | initiator), `string` 62 | - `wca`: An optional identifier for affiliating consecutive pushes, `string` or `null` 63 | - `wct`: Unix epoch timestamp of the request in seconds, `i64` 64 | - `wcv`: Protocol version, `u16` 65 | 66 | ### APNs 67 | 68 | The APNs message contains a key "3mw" containing the payload data as specified 69 | above. 70 | 71 | ### FCM / HMS / Threema Gateway 72 | 73 | The FCM, HMS and Threema Gateway messages contain the payload data as specified above. 74 | 75 | ## Running 76 | 77 | You need the Rust compiler. First, create a `config.toml` file that looks like this: 78 | 79 | [fcm] 80 | service_account_key_base64 = "aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1kUXc0dzlXZ1hjUQo=" 81 | project_id = 12345654321 82 | max_retries = 6 83 | 84 | [apns] 85 | keyfile = "your-keyfile.p8" 86 | key_id = "AB123456XY" 87 | team_id = "CD987654YZ" 88 | 89 | To support HMS as well, you need to add one or more named HMS config sections. 90 | The name should correspond to the App ID (and currently matches the Client ID). 91 | 92 | [hms.app-id-1] 93 | client_id = "your-client-id" 94 | client_secret = "your-client-secret" 95 | 96 | [hms.app-id-2] 97 | client_id = "your-client-id" 98 | client_secret = "your-client-secret" 99 | 100 | To support Threema Gateway, the following config sections need to be added. 101 | Note: The apps only support messages sent from `*3MAPUSH`. 102 | 103 | [threema_gateway] 104 | base_url = "https://msgapi.threema.ch" 105 | identity = "*3MAPUSH" 106 | secret = "secret-for-*3MAPUSH" 107 | private_key_file = "private-key-file-for-*3MAPUSH" 108 | 109 | If you want to log the pushes to InfluxDB, add the following section: 110 | 111 | [influxdb] 112 | connection_string = "http://127.0.0.1:8086" 113 | user = "foo" 114 | pass = "bar" 115 | db = "baz" 116 | 117 | Then simply run 118 | 119 | export RUST_LOG=push_relay=debug,hyper=info,a2=info,tower=debug 120 | cargo run 121 | 122 | ...to build and start the server in debug mode. 123 | 124 | ## Deployment 125 | 126 | - Always create a build in release mode: `cargo build --release` 127 | - Use a reverse proxy with proper TLS termination (e.g. Nginx) 128 | - Set `RUST_LOG=info` env variable 129 | 130 | ## Testing 131 | 132 | To run tests: 133 | 134 | cargo test 135 | 136 | ## Linting 137 | 138 | To run lints: 139 | 140 | $ rustup component add clippy 141 | $ cargo clean && cargo clippy --all-targets 142 | 143 | ## License 144 | 145 | Licensed under either of 146 | 147 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 148 | http://www.apache.org/licenses/LICENSE-2.0) 149 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 150 | http://opensource.org/licenses/MIT) 151 | 152 | at your option. 153 | 154 | 155 | [ci]: https://github.com/threema-ch/push-relay/actions?query=workflow%3ACI 156 | [ci-badge]: https://img.shields.io/github/actions/workflow/status/threema-ch/push-relay/ci.yml?branch=master 157 | [license]: https://github.com/threema-ch/push-relay#license 158 | [license-badge]: https://img.shields.io/badge/License-Apache%202.0%20%2f%20MIT-blue.svg 159 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | Set variables: 4 | 5 | $ export VERSION=X.Y.Z 6 | $ export GPG_KEY=E7ADD9914E260E8B35DFB50665FDE935573ACDA6 7 | 8 | Update changelog: 9 | 10 | $ vim CHANGELOG.md 11 | 12 | Update version numbers: 13 | 14 | $ vim Cargo.toml 15 | $ cargo update -p push-relay 16 | 17 | Commit & tag: 18 | 19 | $ git commit -S${GPG_KEY} -m "Release v${VERSION}" 20 | $ git tag -s -u ${GPG_KEY} v${VERSION} -m "Version ${VERSION}" 21 | 22 | Push: 23 | 24 | $ git push && git push --tags 25 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.86" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration. 2 | 3 | use std::{collections::HashMap, fs::File, io::Read, path::Path}; 4 | 5 | use base64::{engine::general_purpose, Engine}; 6 | use serde::{de::Error as DeserializeError, Deserialize, Deserializer}; 7 | 8 | #[derive(Debug, Deserialize)] 9 | pub struct Config { 10 | pub fcm: FcmConfig, 11 | pub apns: ApnsConfig, 12 | pub hms: Option>, 13 | pub threema_gateway: Option, 14 | pub influxdb: Option, 15 | } 16 | 17 | #[derive(Debug, Deserialize)] 18 | pub struct FcmApplicationSecret(#[serde(deserialize_with = "deserialize_base64")] Vec); 19 | 20 | impl AsRef<[u8]> for FcmApplicationSecret { 21 | fn as_ref(&self) -> &[u8] { 22 | &self.0 23 | } 24 | } 25 | 26 | fn deserialize_base64<'de, D>(deserializer: D) -> Result, D::Error> 27 | where 28 | D: Deserializer<'de>, 29 | { 30 | String::deserialize(deserializer).and_then(|string| { 31 | general_purpose::STANDARD 32 | .decode(string) 33 | .map_err(|err: base64::DecodeError| DeserializeError::custom(err.to_string())) 34 | }) 35 | } 36 | 37 | impl>> From for FcmApplicationSecret { 38 | fn from(value: S) -> Self { 39 | let vec = value.into(); 40 | FcmApplicationSecret(vec) 41 | } 42 | } 43 | 44 | #[derive(Debug, Deserialize)] 45 | pub struct FcmConfig { 46 | #[serde(rename = "service_account_key_base64")] 47 | pub service_account_key: FcmApplicationSecret, 48 | pub project_id: u64, 49 | pub max_retries: u8, 50 | } 51 | 52 | #[derive(Debug, Deserialize)] 53 | pub struct ApnsConfig { 54 | pub keyfile: String, 55 | pub key_id: String, 56 | pub team_id: String, 57 | } 58 | 59 | #[derive(Debug, Clone, Deserialize)] 60 | pub struct HmsConfig { 61 | pub client_id: String, 62 | pub client_secret: String, 63 | pub high_priority: Option, 64 | } 65 | 66 | #[derive(Clone, Debug, Deserialize)] 67 | pub struct ThreemaGatewayConfig { 68 | pub base_url: String, 69 | pub identity: String, 70 | pub secret: String, 71 | pub private_key_file: String, 72 | } 73 | 74 | #[derive(Debug, Deserialize)] 75 | pub struct InfluxdbConfig { 76 | pub connection_string: String, 77 | pub user: String, 78 | pub pass: String, 79 | pub db: String, 80 | } 81 | 82 | impl Config { 83 | pub fn load(path: &Path) -> Result { 84 | let mut file = File::open(path).map_err(|e| e.to_string())?; 85 | let mut contents = String::new(); 86 | file.read_to_string(&mut contents) 87 | .map_err(|e| e.to_string())?; 88 | toml::from_str(&contents).map_err(|e| e.to_string()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use a2::error::Error as A2Error; 2 | use axum::response::{IntoResponse, Response}; 3 | use reqwest::{Error as ReqwestError, StatusCode}; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum InitError { 8 | #[error("APNs init error: {0}")] 9 | Apns(#[source] A2Error), 10 | 11 | #[error("Reqwest init error: {0}")] 12 | Reqwest(#[source] ReqwestError), 13 | 14 | #[error("FCM init error: {0}")] 15 | Fcm(#[source] anyhow::Error), 16 | 17 | #[error("I/O error: {reason}: {source}")] 18 | Io { 19 | reason: &'static str, 20 | source: std::io::Error, 21 | }, 22 | } 23 | 24 | #[derive(Error, Debug)] 25 | // RemoteError 26 | pub enum SendPushError { 27 | /// The request could not be sent 28 | #[error("Push message could not be sent: {0}")] 29 | SendError(#[source] reqwest::Error), 30 | 31 | /// Caused by remote server. Retrying might help. 32 | #[error("Push message could not be processed: {0}")] 33 | RemoteServer(String), 34 | 35 | /// Caused by client (e.g. bad push token). Retrying would probably not help. 36 | #[error("Push message could not be processed: {0}")] 37 | RemoteClient(String), 38 | 39 | /// Server authentication error. Retrying might help. 40 | #[error("Authentication error: {0}")] 41 | RemoteAuth(String), 42 | 43 | #[error("Unspecified internal error: {0}")] 44 | Internal(String), 45 | } 46 | 47 | #[derive(Error, Debug)] 48 | pub enum InfluxdbError { 49 | #[error("HTTP error: {0}")] 50 | Http(String), 51 | 52 | #[error("Database not found")] 53 | DatabaseNotFound, 54 | 55 | #[error("Other: {0}")] 56 | Other(String), 57 | } 58 | 59 | /// Request handling error that is converted into an error response. 60 | /// 61 | /// Currently all error variants result in a "HTTP 400 Bad Request" response. 62 | #[derive(Error, Debug)] 63 | pub enum ServiceError { 64 | #[error("Missing content type")] 65 | MissingContentType, 66 | #[error("Invalid content type: {0}")] 67 | InvalidContentType(String), 68 | #[error("Missing parameters")] 69 | MissingParams, 70 | #[error("Invalid parameters")] 71 | InvalidParams, 72 | } 73 | 74 | // Tell axum how to convert `ServiceError` into a response. 75 | impl IntoResponse for ServiceError { 76 | fn into_response(self) -> Response { 77 | (StatusCode::BAD_REQUEST, self.to_string()).into_response() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/http_client.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use reqwest::{Client, Error}; 4 | 5 | /// Create a HTTP 1 client instance. 6 | /// 7 | /// Parameter: Timeout for idle sockets being kept-alive 8 | pub fn make_client(pool_idle_timeout_seconds: u64) -> Result { 9 | Client::builder() 10 | .pool_idle_timeout(Duration::from_secs(pool_idle_timeout_seconds)) 11 | .http1_only() 12 | .use_rustls_tls() 13 | .https_only(!cfg!(test)) 14 | .tls_built_in_native_certs(true) 15 | .build() 16 | } 17 | -------------------------------------------------------------------------------- /src/influxdb.rs: -------------------------------------------------------------------------------- 1 | use std::str::from_utf8; 2 | 3 | use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; 4 | use reqwest::{ 5 | header::{AUTHORIZATION, CONTENT_TYPE}, 6 | Body, Client, StatusCode, 7 | }; 8 | 9 | use crate::{errors::InfluxdbError, http_client::make_client}; 10 | 11 | /// InfluxDB client. 12 | #[derive(Debug)] 13 | pub struct Influxdb { 14 | connection_string: String, 15 | authorization: String, 16 | db: String, 17 | client: Client, 18 | hostname: String, 19 | } 20 | 21 | type InfluxdbResult = Result<(), InfluxdbError>; 22 | 23 | impl Influxdb { 24 | /// Create a new InfluxDB connection. 25 | pub fn new( 26 | connection_string: String, 27 | user: &str, 28 | pass: &str, 29 | db: String, 30 | ) -> Result { 31 | // Initialize HTTP client 32 | let client = 33 | make_client(90).map_err(|e| format!("Failed to initialize http client: {e}"))?; 34 | 35 | // Determine hostname 36 | let hostname = hostname::get().ok().map_or_else( 37 | || "unknown".to_string(), 38 | |os_string| os_string.to_string_lossy().into_owned(), 39 | ); 40 | 41 | // Determine authorization header 42 | let authorization = Self::get_authorization_header(user, pass); 43 | 44 | Ok(Self { 45 | connection_string, 46 | authorization, 47 | db, 48 | client, 49 | hostname, 50 | }) 51 | } 52 | 53 | fn get_authorization_header(user: &str, pass: &str) -> String { 54 | let bytes = format!("{}:{}", user, pass).into_bytes(); 55 | format!("Basic {}", BASE64.encode(bytes)) 56 | } 57 | 58 | /// Create the database. 59 | pub async fn create_db(&self) -> InfluxdbResult { 60 | debug!("Creating InfluxDB database \"{}\"", self.db); 61 | 62 | // Prepare body 63 | let body: Body = format!("q=CREATE%20DATABASE%20{}", self.db).into(); 64 | 65 | // Send request 66 | let response = self 67 | .client 68 | .post(format!("{}/query", &self.connection_string)) 69 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 70 | .header(AUTHORIZATION, &self.authorization) 71 | .body(body) 72 | .send() 73 | .await 74 | .map_err(|e| InfluxdbError::Http(format!("Request failed: {}", e)))?; 75 | 76 | // Handle response status codes 77 | match response.status() { 78 | StatusCode::OK => Ok(()), 79 | StatusCode::BAD_REQUEST => { 80 | let body = response 81 | .bytes() 82 | .await 83 | .ok() 84 | .and_then(|body| from_utf8(&body).ok().map(|s| s.to_string())) 85 | .unwrap_or_else(|| "[invalid utf8 body]".to_string()); 86 | Err(InfluxdbError::Other(body)) 87 | } 88 | status => Err(InfluxdbError::Http(format!( 89 | "Unexpected status code: {}", 90 | status 91 | ))), 92 | } 93 | } 94 | 95 | async fn log(&self, body: impl Into) -> InfluxdbResult { 96 | let body: Body = body.into(); 97 | 98 | // Send request 99 | let response = self 100 | .client 101 | .post(format!("{}/write?db={}", &self.connection_string, &self.db)) 102 | .header(AUTHORIZATION, &self.authorization) 103 | .body(body) 104 | .send() 105 | .await 106 | .map_err(|e| InfluxdbError::Http(format!("Request failed: {}", e)))?; 107 | 108 | // Handle response status codes 109 | match response.status() { 110 | StatusCode::NO_CONTENT => Ok(()), 111 | StatusCode::NOT_FOUND => Err(InfluxdbError::DatabaseNotFound), 112 | status => Err(InfluxdbError::Http(format!( 113 | "Unexpected status code: {}", 114 | status 115 | ))), 116 | } 117 | } 118 | 119 | /// Log the starting of the push relay server. 120 | pub async fn log_started(&self) -> InfluxdbResult { 121 | debug!("Logging \"started\" event to InfluxDB"); 122 | self.log(format!("started,host={} value=1", self.hostname)) 123 | .await 124 | } 125 | 126 | /// Log a push (either successful or failed) to InfluxDB. 127 | pub async fn log_push(&self, push_type: &str, version: u16, success: bool) -> InfluxdbResult { 128 | let success_str = if success { "true" } else { "false" }; 129 | let push_type = push_type.to_ascii_lowercase(); 130 | debug!( 131 | "Logging \"push\" event (success = \"{success_str}\", {push_type}, v{version}) to InfluxDB" 132 | ); 133 | let hostname = self.hostname.as_str(); 134 | self.log(format!( 135 | "push,host={hostname},type={push_type},version={version},success={success_str} value=1" 136 | )) 137 | .await 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! # FCM/APNs/HMS Push Relay 2 | //! 3 | //! This server accepts push requests via HTTPS and notifies the push 4 | //! service. 5 | //! 6 | //! Supported service: 7 | //! 8 | //! - Google FCM 9 | //! - Apple APNs 10 | //! - Huawei HMS 11 | 12 | #![deny(clippy::all)] 13 | #![allow(clippy::too_many_arguments)] 14 | #![allow(clippy::manual_unwrap_or)] 15 | 16 | #[macro_use] 17 | extern crate tracing; 18 | 19 | mod config; 20 | mod errors; 21 | mod http_client; 22 | mod influxdb; 23 | mod push; 24 | mod server; 25 | 26 | use std::{fs::File, io::Read, net::SocketAddr, path::PathBuf, process}; 27 | 28 | use clap::Parser; 29 | use data_encoding::HEXLOWER_PERMISSIVE; 30 | use zeroize::{ZeroizeOnDrop, Zeroizing}; 31 | 32 | use config::Config; 33 | 34 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 35 | 36 | #[derive(Clone, ZeroizeOnDrop)] 37 | pub struct ThreemaGatewayPrivateKey([u8; 32]); 38 | 39 | #[derive(Parser, Debug)] 40 | #[command(version, about, long_about = None)] 41 | struct Args { 42 | /// The ip/port to listen on 43 | #[arg(short, long, default_value = "127.0.0.1:3000")] 44 | listen: SocketAddr, 45 | 46 | /// Path to a config file 47 | #[arg(short, long, default_value = "config.toml")] 48 | config: PathBuf, 49 | } 50 | 51 | #[tokio::main(flavor = "multi_thread")] 52 | async fn main() { 53 | if let Err(e) = tracing_subscriber::fmt::try_init() { 54 | eprintln!("Could not init tracing_subscriber: {e}"); 55 | process::exit(1); 56 | }; 57 | 58 | let args = Args::parse(); 59 | 60 | // Load config file 61 | let config = Config::load(&args.config).unwrap_or_else(|e| { 62 | error!("Could not load config file {:?}: {}", args.config, e); 63 | process::exit(2); 64 | }); 65 | 66 | // Determine HMS credentials 67 | info!("Found FCM config"); 68 | info!("Found APNs config"); 69 | match config.hms { 70 | None => { 71 | warn!("No HMS credentials found in config, HMS pushes cannot be handled"); 72 | } 73 | Some(ref map) if map.is_empty() => { 74 | warn!("No HMS credentials found in config, HMS pushes cannot be handled"); 75 | } 76 | Some(ref map) => { 77 | let keys = map.keys().collect::>(); 78 | info!("Found {} HMS config(s): {:?}", map.len(), keys); 79 | } 80 | } 81 | 82 | // Determine Threema Gateway credentials 83 | let threema_gateway_private_key = get_gateway_key(&config); 84 | 85 | // Open and read APNs keyfile 86 | let mut apns_keyfile = File::open(&config.apns.keyfile).unwrap_or_else(|e| { 87 | error!( 88 | "Invalid APNs 'keyfile' path: Could not open '{}': {}", 89 | config.apns.keyfile, e 90 | ); 91 | process::exit(3); 92 | }); 93 | let mut apns_api_key = Vec::new(); 94 | apns_keyfile 95 | .read_to_end(&mut apns_api_key) 96 | .unwrap_or_else(|e| { 97 | error!( 98 | "Invalid 'keyfile': Could not read '{}': {}", 99 | config.apns.keyfile, e 100 | ); 101 | process::exit(3); 102 | }); 103 | 104 | info!("Starting Push Relay Server {} on {}", VERSION, &args.listen); 105 | 106 | if let Err(e) = server::serve( 107 | config, 108 | &apns_api_key, 109 | threema_gateway_private_key, 110 | args.listen, 111 | ) 112 | .await 113 | { 114 | error!("Server error: {}", e); 115 | process::exit(3); 116 | } 117 | } 118 | 119 | fn get_gateway_key(config: &Config) -> Option { 120 | match config.threema_gateway { 121 | None => { 122 | warn!( 123 | "No Threema Gateway credentials found in config, Threema pushes cannot be handled" 124 | ); 125 | None 126 | } 127 | Some(ref threema_gateway_config) => { 128 | info!( 129 | "Found Threema Gateway config: {}", 130 | &threema_gateway_config.identity 131 | ); 132 | 133 | // Open and read private key 134 | let mut private_key = Zeroizing::new(Vec::new()); 135 | File::open(&threema_gateway_config.private_key_file) 136 | .unwrap_or_else(|e| { 137 | error!( 138 | "Invalid Threema Gateway 'private_key_file' path: Could not open '{}': {}", 139 | threema_gateway_config.private_key_file, e 140 | ); 141 | process::exit(3); 142 | }) 143 | .read_to_end(&mut private_key) 144 | .unwrap_or_else(|e| { 145 | error!( 146 | "Invalid Threema Gateway 'private_key_file': Could not read '{}': {}", 147 | threema_gateway_config.private_key_file, e 148 | ); 149 | process::exit(3); 150 | }); 151 | 152 | // Strip `private:` prefix and new-line suffix 153 | let private_key = private_key.strip_prefix(b"private:").unwrap_or_else(|| { 154 | error!( 155 | "Invalid Threema Gateway 'private_key_file': Private key not prefixed with 'private:'", 156 | ); 157 | process::exit(3); 158 | }); 159 | let private_key = private_key.strip_suffix(b"\n").unwrap_or(private_key); 160 | 161 | // Decode private key 162 | let private_key = Zeroizing::new(HEXLOWER_PERMISSIVE 163 | .decode(private_key) 164 | .unwrap_or_else(|e| { 165 | error!( 166 | "Invalid Threema Gateway 'private_key_file': Could not hex decode private key: {}", 167 | e 168 | ); 169 | process::exit(3); 170 | })); 171 | let private_key_length = private_key.len(); 172 | let private_key = ThreemaGatewayPrivateKey(<[u8; 32]>::try_from(private_key.as_ref()).unwrap_or_else(|_| { 173 | error!( 174 | "Invalid Threema Gateway 'private_key_file': Could not decode private key, invalid length: {}", 175 | private_key_length 176 | ); 177 | process::exit(3); 178 | })); 179 | Some(private_key) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/push/apns.rs: -------------------------------------------------------------------------------- 1 | //! Code related to the sending of APNs push notifications. 2 | 3 | use std::{ 4 | collections::BTreeMap, 5 | convert::Into, 6 | io::Read, 7 | time::{Duration, SystemTime}, 8 | }; 9 | 10 | use a2::{ 11 | client::{Client, Endpoint}, 12 | error::Error as A2Error, 13 | request::{ 14 | notification::{ 15 | DefaultNotificationBuilder, NotificationBuilder, NotificationOptions, Priority, 16 | }, 17 | payload::{APSAlert, APSSound, Payload, APS}, 18 | }, 19 | response::ErrorReason, 20 | ClientConfig, CollapseId, PushType, 21 | }; 22 | 23 | use crate::{ 24 | errors::{InitError, SendPushError}, 25 | push::{ApnsToken, ThreemaPayload}, 26 | }; 27 | 28 | const PAYLOAD_KEY: &str = "3mw"; 29 | 30 | /// Create a new APNs client instance. 31 | pub fn create_client( 32 | endpoint: Endpoint, 33 | api_key: impl Read, 34 | team_id: S, 35 | key_id: S, 36 | ) -> Result 37 | where 38 | S: Into, 39 | { 40 | let config = ClientConfig::new(endpoint); 41 | Client::token(api_key, key_id, team_id, config).map_err(InitError::Apns) 42 | } 43 | 44 | /// Send an APNs push notification. 45 | pub async fn send_push( 46 | client: &Client, 47 | push_token: &ApnsToken, 48 | bundle_id: &str, 49 | version: u16, 50 | session: &str, 51 | affiliation: Option<&str>, 52 | collapse_id: Option>, 53 | ttl: u32, 54 | ) -> Result<(), SendPushError> { 55 | // Note: This will swallow any errors when converting to a timestamp 56 | let expiration: Option = match ttl { 57 | 0 => Some(0), 58 | ttl => { 59 | let now = SystemTime::now() 60 | .duration_since(SystemTime::UNIX_EPOCH) 61 | .expect("Could not retrieve UNIX timestamp"); 62 | now.checked_add(Duration::from_secs(u64::from(ttl))) 63 | .map(|expiration| expiration.as_secs()) 64 | } 65 | }; 66 | 67 | // CHeck if it is a voip push 68 | let is_voip = bundle_id.ends_with(".voip"); 69 | 70 | // Determine type of notification 71 | let apns_push_type = Some(if is_voip { 72 | PushType::Voip 73 | } else { 74 | PushType::Alert 75 | }); 76 | 77 | // Notification options 78 | let options = NotificationOptions { 79 | apns_id: None, 80 | apns_expiration: expiration, 81 | apns_priority: Some(Priority::High), 82 | apns_topic: Some(bundle_id), 83 | apns_collapse_id: collapse_id, 84 | apns_push_type, 85 | }; 86 | 87 | // Notification payload 88 | let mut payload = if is_voip { 89 | // This is a voip push, so use notification without body but `content-available` set to 1 to allow device wakeup 90 | // even though push is empty (silent notifications) 91 | DefaultNotificationBuilder::new() 92 | .set_content_available() 93 | .build(&push_token.0, options) 94 | } else { 95 | // Regular push, build notification ourselves for full control 96 | Payload { 97 | options, 98 | device_token: &push_token.0, 99 | aps: APS { 100 | alert: Some(APSAlert::Body("Threema Web Wakeup")), 101 | badge: None, 102 | sound: Some(APSSound::Sound("default")), 103 | content_available: None, 104 | category: None, 105 | mutable_content: Some(1), 106 | url_args: None, 107 | }, 108 | data: BTreeMap::new(), 109 | } 110 | }; 111 | 112 | let data = ThreemaPayload::new(session, affiliation, version, false); 113 | payload.add_custom_data(PAYLOAD_KEY, &data).map_err(|e| { 114 | SendPushError::Internal(format!("Could not add custom data to APNs payload: {}", e)) 115 | })?; 116 | trace!("Sending payload: {:#?}", payload); 117 | 118 | match client.send(payload).await { 119 | Ok(response) => { 120 | debug!("Success details: {:?}", response); 121 | Ok(()) 122 | } 123 | Err(e) => { 124 | if let A2Error::ResponseError(ref resp) = e { 125 | if let Some(ref body) = resp.error { 126 | trace!("Response body: {:?}", body); 127 | match body.reason { 128 | // Invalid device token 129 | ErrorReason::BadDeviceToken | 130 | ErrorReason::Unregistered | 131 | // Invalid expiration date (invalid TTL) 132 | ErrorReason::BadExpirationDate | 133 | // Invalid topic (bundle id) 134 | ErrorReason::BadTopic | 135 | ErrorReason::DeviceTokenNotForTopic | 136 | ErrorReason::TopicDisallowed => { 137 | return Err(SendPushError::RemoteClient( 138 | format!("Push was unsuccessful: {}", e))); 139 | }, 140 | 141 | // Below errors should never happen 142 | ErrorReason::BadCollapseId | 143 | ErrorReason::BadMessageId | 144 | ErrorReason::BadPriority | 145 | ErrorReason::DuplicateHeaders | 146 | ErrorReason::Forbidden | 147 | ErrorReason::IdleTimeout | 148 | ErrorReason::MissingDeviceToken | 149 | ErrorReason::MissingTopic | 150 | ErrorReason::PayloadEmpty | 151 | ErrorReason::BadCertificate | 152 | ErrorReason::BadCertificateEnvironment | 153 | ErrorReason::ExpiredProviderToken | 154 | ErrorReason::InvalidProviderToken | 155 | ErrorReason::MissingProviderToken | 156 | ErrorReason::BadPath | 157 | ErrorReason::MethodNotAllowed | 158 | ErrorReason::PayloadTooLarge | 159 | ErrorReason::TooManyProviderTokenUpdates => { 160 | error!("Unexpected APNs error response: {}", e); 161 | }, 162 | 163 | // APNs server errors 164 | ErrorReason::TooManyRequests | 165 | ErrorReason::InternalServerError | 166 | ErrorReason::ServiceUnavailable | 167 | ErrorReason::Shutdown => {} 168 | }; 169 | } 170 | } 171 | 172 | // Treat all other errors as server errors 173 | Err(SendPushError::RemoteServer(format!( 174 | "Push was unsuccessful: {}", 175 | e 176 | ))) 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/push/fcm.rs: -------------------------------------------------------------------------------- 1 | //! Code related to the sending of FCM push notifications. 2 | 3 | use std::{borrow::Cow, sync::Arc, time::Duration}; 4 | 5 | use anyhow::Context; 6 | use futures::{future::BoxFuture, Future, FutureExt}; 7 | use rand::Rng; 8 | use reqwest::{ 9 | header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, RETRY_AFTER}, 10 | tls, Client as HttpClient, StatusCode, 11 | }; 12 | use serde::{Deserialize, Serialize as DeriveSerialize, Serialize, Serializer}; 13 | 14 | use yup_oauth2::{authenticator::DefaultAuthenticator, AccessToken, ServiceAccountAuthenticator}; 15 | 16 | use crate::{ 17 | config::{self, FcmApplicationSecret}, 18 | errors::SendPushError, 19 | }; 20 | 21 | pub const FCM_ENDPOINT: &str = "https://fcm.googleapis.com"; 22 | const FCM_TIMEOUT_SECS: u64 = 10; 23 | const DEFAULT_RETRY_AFTER_MILLIS: u64 = 60 * 1000; 24 | 25 | pub fn get_push_retry_calculator() -> &'static impl CalculatePushSleep { 26 | #[cfg(test)] 27 | { 28 | static MOCK_CALC: test::PushSleepSimulator = test::PushSleepSimulator; 29 | &MOCK_CALC 30 | } 31 | #[cfg(not(test))] 32 | { 33 | static CALC: PushSleepCalculator = PushSleepCalculator; 34 | &CALC 35 | } 36 | } 37 | 38 | /// FCM push priority. 39 | #[derive(Debug, DeriveSerialize, Default)] 40 | #[serde(rename_all = "UPPERCASE")] 41 | #[allow(dead_code)] 42 | pub enum Priority { 43 | #[default] 44 | High, 45 | Normal, 46 | } 47 | 48 | /// FCM push response. 49 | #[derive(Debug, Deserialize)] 50 | pub struct MessageResponse { 51 | name: String, 52 | } 53 | 54 | #[derive(Debug, Deserialize, Serialize)] 55 | pub struct ErrorResponse { 56 | error: FcmError, 57 | } 58 | 59 | #[derive(Debug, Deserialize, Serialize)] 60 | struct FcmError { 61 | code: u16, 62 | details: Option>, 63 | message: Option, 64 | status: Option, 65 | } 66 | 67 | #[derive(Debug, Deserialize, Serialize)] 68 | struct ErrorDetails { 69 | #[serde(rename = "@type")] 70 | error_type: Option, 71 | #[serde(rename = "fieldViolations")] 72 | violations: Option>, 73 | } 74 | 75 | #[derive(Debug, Deserialize, Serialize)] 76 | struct ErrorViolations { 77 | description: Option, 78 | field: Option, 79 | } 80 | 81 | #[derive(Debug, Clone)] 82 | pub struct FcmEndpointConfig { 83 | /// Number of retries that will be made if a request fails in a recoverable way 84 | max_retries: u8, 85 | /// Full URL where the FCM requests will be sent to 86 | endpoint: String, 87 | } 88 | 89 | fn get_fcm_uri(project_id: u64, fcm_authority: impl AsRef) -> String { 90 | let base_url = fcm_authority.as_ref(); 91 | format!("{base_url}/v1/projects/{project_id}/messages:send") 92 | } 93 | 94 | impl FcmEndpointConfig { 95 | fn new(config: &config::FcmConfig, fcm_authority: impl AsRef) -> Self { 96 | let endpoint = get_fcm_uri(config.project_id, fcm_authority); 97 | Self { 98 | max_retries: config.max_retries, 99 | endpoint, 100 | } 101 | } 102 | } 103 | 104 | #[derive(Clone)] 105 | pub struct FcmState 106 | where 107 | R: RequestOauthToken, 108 | { 109 | config: FcmEndpointConfig, 110 | client: HttpClient, 111 | token_obtainer: R, 112 | } 113 | 114 | fn create_fcm_http_client() -> anyhow::Result { 115 | #[allow(unused_mut)] 116 | let mut builder = HttpClient::builder() 117 | .no_gzip() 118 | .no_brotli() 119 | .no_deflate() 120 | .no_proxy() 121 | // https://firebase.google.com/docs/cloud-messaging/scale-fcm#timeouts 122 | .timeout(Duration::from_secs(FCM_TIMEOUT_SECS)) 123 | .pool_idle_timeout(None) 124 | .min_tls_version(tls::Version::TLS_1_2); 125 | 126 | #[cfg(not(test))] 127 | { 128 | builder = builder.http2_prior_knowledge().https_only(true); 129 | } 130 | 131 | builder.build().context("Could not build fcm http client") 132 | } 133 | 134 | impl FcmState 135 | where 136 | T: RequestOauthToken, 137 | { 138 | pub async fn new( 139 | config: &config::FcmConfig, 140 | fcm_authority: Option, 141 | token_obtainer: T, 142 | ) -> anyhow::Result { 143 | // Create FCM HTTP client 144 | let client = create_fcm_http_client()?; 145 | 146 | let fcm_authority = fcm_authority 147 | .map(Cow::Owned) 148 | .unwrap_or_else(|| Cow::Borrowed(FCM_ENDPOINT)); 149 | 150 | Ok(Self { 151 | config: FcmEndpointConfig::new(config, fcm_authority), 152 | client, 153 | token_obtainer, 154 | }) 155 | } 156 | } 157 | 158 | pub trait OauthToken: Send { 159 | fn token(&self) -> Option<&'_ str>; 160 | } 161 | 162 | pub trait RequestOauthToken: Sized + Send + Sync + Clone { 163 | async fn new(application_secret: &FcmApplicationSecret) -> anyhow::Result; 164 | fn request_token( 165 | &self, 166 | ) -> impl Future> + std::marker::Send; 167 | } 168 | 169 | #[derive(Clone)] 170 | pub struct HttpOauthTokenObtainer { 171 | oauth_authenticator: DefaultAuthenticator, 172 | } 173 | 174 | struct FcmAccessToken { 175 | access_token: AccessToken, 176 | } 177 | 178 | impl OauthToken for FcmAccessToken { 179 | fn token(&self) -> Option<&'_ str> { 180 | self.access_token.token() 181 | } 182 | } 183 | 184 | impl RequestOauthToken for HttpOauthTokenObtainer { 185 | async fn request_token(&self) -> Result { 186 | const SCOPES: [&str; 1] = ["https://www.googleapis.com/auth/firebase.messaging"]; 187 | 188 | let access_token = self.oauth_authenticator.token(&SCOPES).await.map_err(|e| { 189 | SendPushError::RemoteAuth(format!("Could not retrieve bearer token: {e}")) 190 | })?; 191 | Ok(FcmAccessToken { access_token }) 192 | } 193 | 194 | async fn new(application_secret: &FcmApplicationSecret) -> anyhow::Result { 195 | let service_account_key = yup_oauth2::parse_service_account_key(application_secret) 196 | .map_err(|e| SendPushError::Internal(format!("Could not read fcm json secret: {e}"))) 197 | .context("Failed to read application secret")?; 198 | let oauth_authenticator = ServiceAccountAuthenticator::builder(service_account_key) 199 | .build() 200 | .await 201 | .map_err(|e| { 202 | SendPushError::Internal(format!("Could not initialize OAuth 2.0 client: {e}")) 203 | }) 204 | .context("Could not build oauth authenticator")?; 205 | Ok(Self { 206 | oauth_authenticator, 207 | }) 208 | } 209 | } 210 | 211 | /// Trait for implementing the time that is waited between subsequent push attempts 212 | pub trait CalculatePushSleep: Sized + Send + Sync { 213 | /// Calculate the number of milliseconds that are waited until a retry of the push send is attempted. 214 | /// 215 | /// Exponential backoff with jittering will be applied to calculate the sleep duration. See 216 | /// for further information. 217 | fn calculate_retry_sleep_millis(&self, try_counter: u8) -> u64; 218 | } 219 | 220 | #[derive(Debug, Clone, Copy)] 221 | pub struct AndroidTtlSeconds(u32); 222 | 223 | impl Default for AndroidTtlSeconds { 224 | fn default() -> Self { 225 | // Two weeks in seconds 226 | Self(2 * 7 * 24 * 3600) 227 | } 228 | } 229 | 230 | impl AndroidTtlSeconds { 231 | pub fn new(ttl: u32) -> Self { 232 | Self(ttl) 233 | } 234 | } 235 | 236 | #[derive(Debug, DeriveSerialize)] 237 | pub struct HttpV1Payload<'a, S: Serialize + Send> { 238 | /// See [`Message`] for docs 239 | message: Message<'a, S>, 240 | } 241 | 242 | impl<'a, S> HttpV1Payload<'a, S> 243 | where 244 | S: Serialize + Send, 245 | { 246 | pub fn new( 247 | android_ttl: AndroidTtlSeconds, 248 | registration_id: &'a str, 249 | payload: &'a S, 250 | collapse_key: Option<&'a str>, 251 | ) -> Self { 252 | Self { 253 | message: Message::new(android_ttl, registration_id, payload, collapse_key), 254 | } 255 | } 256 | } 257 | 258 | /// JSON specification of Message payload: 259 | #[derive(Debug, DeriveSerialize)] 260 | struct Message<'a, S: Serialize> { 261 | #[serde(rename = "token")] 262 | registration_id: &'a str, 263 | data: &'a S, 264 | #[serde(rename = "android")] 265 | android_config: AndroidConfig<'a>, 266 | } 267 | 268 | impl<'a, S> Message<'a, S> 269 | where 270 | S: Serialize, 271 | { 272 | fn new( 273 | android_ttl: AndroidTtlSeconds, 274 | registration_id: &'a str, 275 | data: &'a S, 276 | collapse_key: Option<&'a str>, 277 | ) -> Self { 278 | Self { 279 | registration_id, 280 | data, 281 | android_config: AndroidConfig::new(android_ttl, collapse_key), 282 | } 283 | } 284 | } 285 | 286 | #[derive(Debug, Serialize, Default)] 287 | struct AndroidConfig<'a> { 288 | #[serde(skip_serializing_if = "Option::is_none")] 289 | collapse_key: Option<&'a str>, 290 | priority: Priority, 291 | #[serde(serialize_with = "serialize_android_ttl")] 292 | ttl: AndroidTtlSeconds, 293 | } 294 | 295 | fn serialize_android_ttl(ttl: &AndroidTtlSeconds, serializer: S) -> Result 296 | where 297 | S: Serializer, 298 | { 299 | serializer.serialize_str(&format!("{}s", ttl.0)) 300 | } 301 | 302 | impl<'a> AndroidConfig<'a> { 303 | fn new(ttl: AndroidTtlSeconds, collapse_key: Option<&'a str>) -> Self { 304 | Self { 305 | ttl, 306 | collapse_key, 307 | ..Default::default() 308 | } 309 | } 310 | } 311 | 312 | /// Send a FCM push notification. 313 | pub fn send_push<'a>( 314 | state: Arc>, 315 | retry_calculator: &'static impl CalculatePushSleep, 316 | http_payload: HttpV1Payload<'a, impl Serialize + Send + Sync>, 317 | try_counter: u8, 318 | ) -> BoxFuture<'a, Result> { 319 | async move { _send_push(state, retry_calculator, http_payload, try_counter).await }.boxed() 320 | } 321 | 322 | pub struct PushSleepCalculator; 323 | 324 | impl CalculatePushSleep for PushSleepCalculator { 325 | fn calculate_retry_sleep_millis(&self, try_counter: u8) -> u64 { 326 | // 2 to the power of `try_counter_` will be used as base value for exponential backoff. `+-8%` of deviation 327 | // will be added or subtracted so that the retries are not scheduled at the same time. 328 | let sleep_millis = 2u64.pow(try_counter.into()) * 1000; 329 | let deviation = sleep_millis / 100 * 8; 330 | rand::thread_rng().gen_range((sleep_millis - deviation)..(sleep_millis + deviation)) 331 | } 332 | } 333 | 334 | fn can_push_be_retried(code: StatusCode) -> bool { 335 | let http_code = code.as_u16(); 336 | http_code == 429 || http_code >= 500 337 | } 338 | 339 | /// # Note 340 | /// Don't call this directly, call [`send_push`] instead! 341 | async fn _send_push( 342 | state: Arc>, 343 | retry_calculator: &'static impl CalculatePushSleep, 344 | http_payload: HttpV1Payload<'_, impl Serialize + Send + Sync>, 345 | try_counter: u8, 346 | ) -> Result { 347 | if try_counter != 0 { 348 | debug!("Retry #{}", try_counter); 349 | } 350 | 351 | let payload_string = serde_json::ser::to_string_pretty(&http_payload) 352 | .map_err(|e| SendPushError::Internal(format!("Could not encode JSON payload: {e}")))?; 353 | 354 | let response = { 355 | // Acquire token 356 | let access_token = state.token_obtainer.request_token().await?; 357 | let access_token_str = access_token.token().ok_or_else(|| { 358 | SendPushError::RemoteAuth("No bearer token present after retrieving it".to_string()) 359 | })?; 360 | 361 | // Send request 362 | state 363 | .client 364 | .post(&state.config.endpoint) 365 | .header(AUTHORIZATION, format!("Bearer {}", access_token_str)) 366 | .header(CONTENT_TYPE, "application/json") 367 | .header(CONTENT_LENGTH, payload_string.len().to_string()) 368 | .body(payload_string) 369 | .send() 370 | .await 371 | .map_err(SendPushError::SendError)? 372 | }; 373 | 374 | // Get retry-after header if it is present 375 | let retry_after_secs = response.headers().get(RETRY_AFTER).map(|value| { 376 | value 377 | .to_str() 378 | .context("No ascii string") 379 | .and_then(|a| str::parse::(a).context("No u64")) 380 | }); 381 | 382 | // Transform response into body 383 | let status = response.status(); 384 | let body = response 385 | .bytes() 386 | .await 387 | .map_err(|e| SendPushError::Internal(format!("Could not read FCM response body: {e}")))?; 388 | 389 | // Check status code 390 | let status_code = status.as_u16(); 391 | // Error reference: 392 | match status { 393 | // https://firebase.google.com/docs/cloud-messaging/manage-tokens#detect-invalid-token-responses-from-the-fcm-backend 394 | StatusCode::BAD_REQUEST | StatusCode::NOT_FOUND => { 395 | return Err(SendPushError::RemoteClient(format!( 396 | "Token or payload is invalid: HTTP {status_code}" 397 | ))); 398 | } 399 | StatusCode::UNAUTHORIZED | StatusCode::PAYMENT_REQUIRED => { 400 | return Err(SendPushError::RemoteClient(format!( 401 | "Unrecoverable error code received: HTTP {status_code}" 402 | ))) 403 | } 404 | status if can_push_be_retried(status) => { 405 | if try_counter >= state.config.max_retries { 406 | return Err(SendPushError::RemoteServer(format!( 407 | "Max push retry count has been reached. Last HTTP status: {status_code}" 408 | ))); 409 | } 410 | 411 | let sleep_time_millis = match retry_after_secs { 412 | Some(Ok(secs)) => secs * 1000, 413 | Some(Err(e)) => { 414 | info!("Could not parse \"retry-after\": {}", e); 415 | DEFAULT_RETRY_AFTER_MILLIS 416 | } 417 | None => retry_calculator.calculate_retry_sleep_millis(try_counter), 418 | }; 419 | 420 | tokio::time::sleep(Duration::from_millis(sleep_time_millis)).await; 421 | debug!("Retrying to send push after {} ms", sleep_time_millis); 422 | 423 | return send_push(state, retry_calculator, http_payload, try_counter + 1).await; 424 | } 425 | // Catch all error codes that cannot be retried 426 | _ if status_code >= 300 => { 427 | return Err(SendPushError::RemoteServer(format!( 428 | "Unknown http error code: HTTP {status_code}" 429 | ))) 430 | } 431 | _ => trace!("HTTP status code: {}", status_code), 432 | } 433 | 434 | // Decode UTF8 bytes 435 | let json_body = std::str::from_utf8(&body).map_err(|_| { 436 | SendPushError::Internal("Could not decode response JSON: Invalid UTF-8".into()) 437 | })?; 438 | 439 | // Parse JSON 440 | let data: MessageResponse = serde_json::de::from_str(json_body) 441 | .map_err(|e| SendPushError::Internal(format!("Could not decode response JSON: {e}")))?; 442 | 443 | debug!("Sent push message: {}", data.name); 444 | 445 | Ok(status_code) 446 | } 447 | 448 | #[cfg(test)] 449 | pub mod test { 450 | use super::*; 451 | 452 | pub fn get_fcm_test_path(config: &config::FcmConfig) -> String { 453 | get_fcm_uri(config.project_id, "") 454 | } 455 | 456 | #[derive(Clone)] 457 | pub struct MockAccessTokenObtainer; 458 | pub struct MockAccessToken; 459 | 460 | impl OauthToken for MockAccessToken { 461 | fn token(&self) -> Option<&'_ str> { 462 | Some("fake-access-token") 463 | } 464 | } 465 | 466 | impl RequestOauthToken for MockAccessTokenObtainer { 467 | async fn new(_: &FcmApplicationSecret) -> anyhow::Result { 468 | Ok(MockAccessTokenObtainer) 469 | } 470 | 471 | async fn request_token(&self) -> Result { 472 | Ok(MockAccessToken) 473 | } 474 | } 475 | 476 | pub struct PushSleepSimulator; 477 | 478 | impl CalculatePushSleep for PushSleepSimulator { 479 | fn calculate_retry_sleep_millis(&self, _try_counter: u8) -> u64 { 480 | 0 481 | } 482 | } 483 | 484 | #[test] 485 | fn test_priority_serialization() { 486 | assert_eq!(serde_json::to_string(&Priority::High).unwrap(), "\"HIGH\""); 487 | assert_eq!( 488 | serde_json::to_string(&Priority::Normal).unwrap(), 489 | "\"NORMAL\"" 490 | ); 491 | } 492 | 493 | pub fn get_fcm_error( 494 | code: StatusCode, 495 | message: &str, 496 | status_code_uppercase: &str, 497 | ) -> ErrorResponse { 498 | ErrorResponse { 499 | error: FcmError { 500 | code: code.as_u16(), 501 | details: Some(vec![ErrorDetails { 502 | error_type: Some("type.googleapis.com/google.rpc.SomeErrorType".to_owned()), 503 | violations: Some(vec![ErrorViolations { 504 | description: Some("Description of the violation".to_owned()), 505 | field: Some("field-that-violated".to_string()), 506 | }]), 507 | }]), 508 | message: Some(message.to_owned()), 509 | status: Some(status_code_uppercase.to_owned()), 510 | }, 511 | } 512 | } 513 | 514 | #[test] 515 | fn test_calculate_retry_sleep_millis() { 516 | let calc = PushSleepCalculator; 517 | let seconds = calc.calculate_retry_sleep_millis(0) as f64 / 1000.0; 518 | let rounded = seconds.round() as u64; 519 | assert_eq!(rounded, 1); 520 | 521 | let seconds = calc.calculate_retry_sleep_millis(5) as f64 / 1000.0; 522 | let rounded = seconds.round() as u64; 523 | assert!((29..=35).contains(&rounded)); 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /src/push/hms.rs: -------------------------------------------------------------------------------- 1 | //! Code related to the sending of HMS push notifications. 2 | //! 3 | //! ## Authentication 4 | //! 5 | //! We are using OAuth 2.0-based authentication with the "Client Credentials" mode. 6 | //! 7 | //! Docs: https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/open-platform-oauth-0000001053629189 8 | //! 9 | //! ## Message Sending 10 | //! 11 | //! Docs: https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/android-server-dev-0000001050040110 12 | //! 13 | //! Payload format: https://developer.huawei.com/consumer/en/doc/development/HMSCore-References-V5/https-send-api-0000001050986197-V5#EN-US_TOPIC_0000001124288117__section13271045101216 14 | 15 | use std::{ 16 | borrow::Cow, 17 | fmt, 18 | str::from_utf8, 19 | sync::Arc, 20 | time::{Duration, Instant}, 21 | }; 22 | 23 | use reqwest::{ 24 | header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE}, 25 | Client, StatusCode, 26 | }; 27 | use serde::{Deserialize, Serialize}; 28 | use serde_json as json; 29 | use tokio::sync::Mutex; 30 | 31 | use crate::{ 32 | config::HmsConfig, 33 | errors::SendPushError, 34 | push::{HmsToken, ThreemaPayload}, 35 | }; 36 | 37 | pub struct HmsEndpointConfig { 38 | login_endpoint: Cow<'static, str>, 39 | push_endpoint: Cow<'static, str>, 40 | } 41 | 42 | type SharedHmsConfig = Arc; 43 | 44 | impl HmsEndpointConfig { 45 | pub fn new_shared() -> SharedHmsConfig { 46 | let login_endpoint = Cow::Borrowed("https://oauth-login.cloud.huawei.com"); 47 | let push_endpoint = Cow::Borrowed("https://push-api.cloud.huawei.com"); 48 | Arc::new(Self { 49 | login_endpoint, 50 | push_endpoint, 51 | }) 52 | } 53 | fn hms_endpoint(&self, endpoint_type: EndpointType) -> &str { 54 | match endpoint_type { 55 | EndpointType::Login => self.login_endpoint.as_ref(), 56 | EndpointType::Push => self.push_endpoint.as_ref(), 57 | } 58 | } 59 | 60 | fn login_url(&self) -> String { 61 | format!("{}/oauth2/v3/token", self.hms_endpoint(EndpointType::Login)) 62 | } 63 | 64 | fn hms_push_url(&self, app_id: &str) -> String { 65 | format!( 66 | "{}/v1/{}/messages:send", 67 | self.hms_endpoint(EndpointType::Push), 68 | app_id 69 | ) 70 | } 71 | } 72 | 73 | enum EndpointType { 74 | Login, 75 | Push, 76 | } 77 | 78 | /// HMS push urgency. 79 | #[derive(Debug, Serialize)] 80 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 81 | pub enum Urgency { 82 | High, 83 | Normal, 84 | } 85 | 86 | /// HMS push category. 87 | /// 88 | /// Note: To be able to use these categories, you need to apply for special 89 | /// permission. 90 | #[derive(Debug, Serialize)] 91 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 92 | pub enum Category { 93 | //PlayVoice, 94 | Voip, 95 | } 96 | 97 | #[derive(Debug, Serialize)] 98 | pub struct AndroidConfig { 99 | /// The urgency. 100 | urgency: Urgency, 101 | /// The push category. 102 | #[serde(skip_serializing_if = "Option::is_none")] 103 | category: Option, 104 | /// Time to live in seconds. 105 | ttl: String, 106 | } 107 | 108 | #[derive(Debug, Serialize)] 109 | pub struct Message<'a> { 110 | /// The push payload. 111 | data: String, 112 | /// Android message push control. 113 | android: AndroidConfig, 114 | /// Push token(s) of the recipient(s). 115 | token: &'a [&'a str], 116 | } 117 | 118 | /// HMS request body. 119 | #[derive(Debug, Serialize)] 120 | struct Payload<'a> { 121 | /// The message. 122 | message: Message<'a>, 123 | } 124 | 125 | /// HMS auth response. 126 | #[derive(Debug, Deserialize)] 127 | struct AuthResponse { 128 | access_token: String, 129 | expires_in: i32, 130 | token_type: String, 131 | } 132 | 133 | /// HMS auth response. 134 | #[derive(Debug, Deserialize)] 135 | #[serde(rename_all = "camelCase")] 136 | struct PushResponse { 137 | code: String, 138 | #[allow(dead_code)] 139 | msg: String, 140 | #[allow(dead_code)] 141 | request_id: String, 142 | } 143 | 144 | /// HMS service result code. 145 | #[derive(Debug)] 146 | enum HmsCode { 147 | Success, // 80000000 148 | SomeInvalidTokens, // 80100000 149 | InvalidParameters, // 80100001 150 | InvalidTokenCount, // 80100002 151 | IncorrectMessageStructure, // 80100003 152 | InvalidTtl, // 80100004 153 | InvalidCollapseKey, // 80100013 154 | TooManyTopicMessages, // 80100017 155 | AuthenticationError, // 80200001 156 | AuthorizationExpired, // 80200003 157 | PermissionDenied, // 80300002 158 | InvalidTokens, // 80300007 159 | MessageTooLarge, // 80300008 160 | TooManyTokens, // 80300010 161 | HighPriorityPermissionMissing, // 80300011 162 | InternalError, // 81000001 163 | Other(String), 164 | } 165 | 166 | impl fmt::Display for HmsCode { 167 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 168 | const PREFIX: &str = "HMS push failed"; 169 | match &self { 170 | &Self::Other(reason) => write!(f, "{} with unspecified code: {}", PREFIX, reason), 171 | _ => write!(f, "{}: {:?}", PREFIX, &self), 172 | } 173 | } 174 | } 175 | 176 | impl From<&str> for HmsCode { 177 | fn from(val: &str) -> Self { 178 | match val { 179 | "80000000" => Self::Success, 180 | 181 | "80100000" => Self::SomeInvalidTokens, 182 | "80100001" => Self::InvalidParameters, 183 | "80100002" => Self::InvalidTokenCount, 184 | "80100003" => Self::IncorrectMessageStructure, 185 | "80100004" => Self::InvalidTtl, 186 | "80100013" => Self::InvalidCollapseKey, 187 | "80100017" => Self::TooManyTopicMessages, 188 | 189 | "80200001" => Self::AuthenticationError, 190 | "80200003" => Self::AuthorizationExpired, 191 | 192 | "80300002" => Self::PermissionDenied, 193 | "80300007" => Self::InvalidTokens, 194 | "80300008" => Self::MessageTooLarge, 195 | "80300010" => Self::TooManyTokens, 196 | "80300011" => Self::HighPriorityPermissionMissing, 197 | 198 | "81000001" => Self::InternalError, 199 | 200 | _ => Self::Other(val.to_string()), 201 | } 202 | } 203 | } 204 | 205 | /// HMS OAuth2 credentials. 206 | #[derive(Debug, Clone, PartialEq)] 207 | pub struct HmsCredentials { 208 | /// The OAuth2 access token. 209 | access_token: String, 210 | 211 | /// Expiration of this access token. 212 | /// 213 | /// Note: We may set this to a time earlier than the actual token 214 | /// expiration. 215 | expiration: Instant, 216 | } 217 | 218 | impl HmsCredentials { 219 | /// Return true if the credentials are expired. 220 | pub fn expired(&self) -> bool { 221 | self.expiration <= Instant::now() 222 | } 223 | } 224 | 225 | impl From for HmsCredentials { 226 | fn from(resp: AuthResponse) -> Self { 227 | // Renew 180 seconds before expiration timestamp 228 | let expires_in = i32::max(resp.expires_in - 180, 0) as u64; 229 | Self { 230 | access_token: resp.access_token, 231 | expiration: Instant::now() + Duration::from_secs(expires_in), 232 | } 233 | } 234 | } 235 | 236 | /// The context object that holds state and authentication information. 237 | #[derive(Debug)] 238 | pub struct HmsContext { 239 | /// The HTTP client used to connect to HMS. 240 | client: Client, 241 | 242 | /// The long-term credentials used to request temporary OAuth credentials. 243 | config: HmsConfig, 244 | 245 | /// The short-term credentials with a mutex, for exclusive access and 246 | /// interior mutability. 247 | credentials: Mutex>, 248 | } 249 | 250 | impl HmsContext { 251 | pub fn new(client: Client, config: HmsConfig) -> Self { 252 | Self { 253 | client, 254 | config, 255 | credentials: Mutex::new(None), 256 | } 257 | } 258 | 259 | /// Request new OAuth2 credentials from the Huawei server. 260 | async fn request_new_credentials( 261 | &self, 262 | config: &SharedHmsConfig, 263 | ) -> Result { 264 | debug!("Requesting OAuth2 credentials"); 265 | 266 | // Prepare request 267 | let body: String = form_urlencoded::Serializer::new(String::new()) 268 | .append_pair("grant_type", "client_credentials") 269 | .append_pair("client_id", &self.config.client_id) 270 | .append_pair("client_secret", &self.config.client_secret) 271 | .finish(); 272 | 273 | // Send request 274 | let response = self 275 | .client 276 | .post(config.login_url()) 277 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 278 | .header(CONTENT_LENGTH, &*body.len().to_string()) 279 | .body(body) 280 | .send() 281 | .await 282 | .map_err(|e| SendPushError::RemoteAuth(e.to_string()))?; 283 | 284 | // Extract status 285 | let status = response.status(); 286 | 287 | // Fetch body 288 | let body_bytes = response.bytes().await.map_err(|e| { 289 | SendPushError::RemoteAuth(format!("Could not read HMS auth response body: {e}")) 290 | })?; 291 | 292 | // Validate status code 293 | if status != StatusCode::OK { 294 | match from_utf8(&body_bytes) { 295 | Ok(body) => warn!("OAuth2 response: HTTP {}: {}", status, body), 296 | Err(_) => warn!("OAuth2 response: HTTP {} (invalid UTF8 body)", status), 297 | } 298 | return Err(SendPushError::RemoteAuth(format!( 299 | "Could not request HMS credentials: HTTP {}", 300 | status 301 | ))); 302 | } 303 | trace!("OAuth2 response: HTTP {}", status); 304 | 305 | // Decode UTF8 bytes 306 | let json_body = from_utf8(&body_bytes).map_err(|_| { 307 | SendPushError::RemoteAuth("Could not decode response JSON: Invalid UTF-8".into()) 308 | })?; 309 | 310 | // Parse JSON 311 | let data: AuthResponse = json::from_str(json_body).map_err(|e| { 312 | SendPushError::RemoteAuth(format!( 313 | "Could not decode response JSON: `{}` (Reason: {})", 314 | json_body, e 315 | )) 316 | })?; 317 | 318 | // Validate type 319 | if data.token_type != "Bearer" { 320 | warn!( 321 | "Returned OAuth2 token is of type '{}', not 'Bearer'", 322 | data.token_type 323 | ); 324 | } 325 | 326 | Ok(data.into()) 327 | } 328 | 329 | /// Return a copy of the HMS credentials. 330 | /// 331 | /// If there are no credentials so far, fetch and store them. 332 | /// If the credentials are outdated, refresh them. 333 | /// Otherwise, just return a copy directly. 334 | pub async fn get_active_credentials( 335 | &self, 336 | config: &SharedHmsConfig, 337 | ) -> Result { 338 | // Lock mutex 339 | let mut credentials = self.credentials.lock().await; 340 | 341 | match *credentials { 342 | // No credentials found, fetch initial credentials 343 | None => { 344 | let new_credentials = self.request_new_credentials(config).await?; 345 | *credentials = Some(new_credentials.clone()); 346 | info!("Fetched initial OAuth credentials"); 347 | Ok(new_credentials) 348 | } 349 | 350 | // Valid credentials found 351 | Some(ref credentials) if !credentials.expired() => { 352 | debug!( 353 | "Credentials are still valid, expiration in {} seconds", 354 | (credentials.expiration - Instant::now()).as_secs() 355 | ); 356 | Ok(credentials.clone()) 357 | } 358 | 359 | // Credentials must be renewed 360 | Some(_) => { 361 | let new_credentials = self.request_new_credentials(config).await?; 362 | *credentials = Some(new_credentials.clone()); 363 | info!("Refreshed OAuth credentials"); 364 | Ok(new_credentials) 365 | } 366 | } 367 | } 368 | 369 | /// Clear credentials 370 | pub async fn clear_credentials(&self) { 371 | info!("Clearing credentials"); 372 | let mut credentials = self.credentials.lock().await; 373 | *credentials = None; 374 | } 375 | } 376 | 377 | /// Send a HMS push notification. 378 | pub async fn send_push( 379 | context: &HmsContext, 380 | config: &SharedHmsConfig, 381 | push_token: &HmsToken, 382 | version: u16, 383 | session: &str, 384 | affiliation: Option<&str>, 385 | ttl: u32, 386 | ) -> Result<(), SendPushError> { 387 | let threema_payload = ThreemaPayload::new(session, affiliation, version, false); 388 | let high_priority = context.config.high_priority.unwrap_or(false); 389 | let payload = Payload { 390 | message: Message { 391 | data: json::to_string(&threema_payload).expect("Could not encode JSON threema payload"), 392 | android: AndroidConfig { 393 | urgency: if high_priority { 394 | Urgency::High 395 | } else { 396 | Urgency::Normal 397 | }, 398 | category: if high_priority { 399 | Some(Category::Voip) 400 | } else { 401 | None 402 | }, 403 | ttl: format!("{}s", ttl), 404 | }, 405 | token: &[&push_token.0], 406 | }, 407 | }; 408 | trace!("Sending payload: {:#?}", payload); 409 | 410 | // Encode payload 411 | let payload_string = json::to_string(&payload).expect("Could not encode JSON payload"); 412 | debug!("Payload: {}", payload_string); 413 | 414 | // Get or refresh credentials 415 | let credentials = context.get_active_credentials(config).await?; 416 | 417 | // Send request 418 | let response = context 419 | .client 420 | .post(config.hms_push_url(&context.config.client_id)) 421 | .header(CONTENT_TYPE, "application/json; charset=UTF-8") 422 | .header(CONTENT_LENGTH, &*payload_string.len().to_string()) 423 | .header( 424 | AUTHORIZATION, 425 | &format!("Bearer {}", credentials.access_token), 426 | ) 427 | .body(payload_string) 428 | .send() 429 | .await 430 | .map_err(SendPushError::SendError)?; 431 | 432 | // Extract status 433 | let status = response.status(); 434 | 435 | // Fetch body 436 | let body_bytes = response.bytes().await.map_err(|e| { 437 | SendPushError::RemoteServer(format!("Could not read HMS auth response body: {}", e)) 438 | })?; 439 | 440 | // Decode UTF8 bytes 441 | let body = match from_utf8(&body_bytes) { 442 | Ok(string) => string, 443 | Err(_) => "[Non-UTF8 Body]", // This will fail to parse as JSON, but it's helpful for error logging 444 | }; 445 | 446 | // Validate status code 447 | match status { 448 | StatusCode::OK => { 449 | trace!("HMS push request returned HTTP 200: {}", body); 450 | } 451 | StatusCode::BAD_REQUEST => { 452 | return Err(SendPushError::RemoteClient(format!( 453 | "Bad request: {}", 454 | body 455 | ))); 456 | } 457 | StatusCode::INTERNAL_SERVER_ERROR | StatusCode::BAD_GATEWAY => { 458 | return Err(SendPushError::RemoteServer(format!( 459 | "HMS server error: {}", 460 | body 461 | ))); 462 | } 463 | StatusCode::SERVICE_UNAVAILABLE => { 464 | return Err(SendPushError::RemoteServer(format!( 465 | "HMS quota reached: {}", 466 | body 467 | ))); 468 | } 469 | _other => { 470 | return Err(SendPushError::Internal(format!( 471 | "Unexpected status code: HTTP {}: {}", 472 | status, body 473 | ))); 474 | } 475 | } 476 | 477 | // Parse JSON 478 | let data: PushResponse = json::from_str(body).map_err(|e| { 479 | SendPushError::Internal(format!( 480 | "Could not decode response JSON: `{}` (Reason: {})", 481 | body, e 482 | )) 483 | })?; 484 | 485 | // Validate HMS code 486 | let code = HmsCode::from(&*data.code); 487 | match code { 488 | // Success 489 | HmsCode::Success => Ok(()), 490 | 491 | // Client errors 492 | HmsCode::SomeInvalidTokens | HmsCode::InvalidTokens => Err(SendPushError::RemoteClient( 493 | "Invalid push token(s)".to_string(), 494 | )), 495 | 496 | // Potentially temporary errors 497 | HmsCode::InternalError => Err(SendPushError::RemoteServer( 498 | "HMS internal server error".to_string(), 499 | )), 500 | 501 | // Auth errors 502 | HmsCode::AuthenticationError | HmsCode::AuthorizationExpired => { 503 | // Clear credentials, since token may be invalid 504 | context.clear_credentials().await; 505 | Err(SendPushError::RemoteServer(format!( 506 | "Authentication error: {:?}", 507 | code 508 | ))) 509 | } 510 | 511 | // Other errors 512 | other => Err(SendPushError::Internal(format!("{}", other))), 513 | } 514 | } 515 | 516 | #[cfg(test)] 517 | mod tests { 518 | use super::*; 519 | 520 | use crate::http_client; 521 | 522 | impl HmsEndpointConfig { 523 | pub fn stub_with(endpoint: Option) -> SharedHmsConfig { 524 | let endpoint = endpoint.unwrap_or_else(|| "invalid-hms.endpoint".to_owned()); 525 | Arc::new(Self { 526 | login_endpoint: Cow::Owned(endpoint.clone()), 527 | push_endpoint: Cow::Owned(endpoint), 528 | }) 529 | } 530 | } 531 | 532 | mod context { 533 | use wiremock::{ 534 | matchers::{body_string, method}, 535 | Mock, MockServer, ResponseTemplate, 536 | }; 537 | 538 | use super::*; 539 | 540 | #[tokio::test] 541 | async fn get_credentials() { 542 | const CLIENT_ID: &str = "klient"; 543 | const CLIENT_SECRET: &str = "sehr-sekur"; 544 | 545 | // Set up context 546 | let client = http_client::make_client(10).unwrap(); 547 | let context = HmsContext::new( 548 | client, 549 | HmsConfig { 550 | client_id: CLIENT_ID.into(), 551 | client_secret: CLIENT_SECRET.into(), 552 | high_priority: None, 553 | }, 554 | ); 555 | 556 | let mock_server = MockServer::start().await; 557 | 558 | let config = HmsEndpointConfig::stub_with(Some(mock_server.uri())); 559 | 560 | Mock::given(method("POST")) 561 | .and(body_string(format!( 562 | "grant_type=client_credentials&client_id={}&client_secret={}", 563 | CLIENT_ID, CLIENT_SECRET 564 | ))) 565 | .respond_with(ResponseTemplate::new(200).set_body_string( 566 | r#"{ 567 | "access_token": "akssess", 568 | "expires_in": 3600, 569 | "token_type": "Bearer" 570 | }"#, 571 | )) 572 | .expect(2) 573 | .mount(&mock_server) 574 | .await; 575 | 576 | // No credentials yet 577 | assert!(context.credentials.lock().await.is_none()); 578 | 579 | // Get new credentials 580 | let credentials = context.get_active_credentials(&config).await.unwrap(); 581 | assert!(context.credentials.lock().await.is_some()); 582 | assert_eq!(credentials.access_token, "akssess"); 583 | let remaining_validity = (credentials.expiration - Instant::now()).as_secs(); 584 | assert!(remaining_validity <= (3600 - 180)); 585 | assert!(remaining_validity > (3600 - 180 - 10)); 586 | 587 | // Get cached credentials 588 | let credentials2 = context.get_active_credentials(&config).await.unwrap(); 589 | assert_eq!(credentials, credentials2); 590 | 591 | // Refresh credentials 592 | context 593 | .credentials 594 | .lock() 595 | .await 596 | .as_mut() 597 | .unwrap() 598 | .expiration = Instant::now() - Duration::from_secs(3); 599 | let credentials3 = context.get_active_credentials(&config).await.unwrap(); 600 | let remaining_validity = (credentials3.expiration - Instant::now()).as_secs(); 601 | assert!(remaining_validity > (3600 - 180 - 10)); 602 | } 603 | } 604 | } 605 | -------------------------------------------------------------------------------- /src/push/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apns; 2 | pub mod fcm; 3 | pub mod hms; 4 | pub mod threema_gateway; 5 | 6 | use chrono::Utc; 7 | use serde::{Serialize, Serializer}; 8 | 9 | /// A FCM token. 10 | #[derive(Debug, PartialEq, Eq, Clone)] 11 | pub struct FcmToken(pub String); 12 | 13 | /// An APNs device token. 14 | #[derive(Debug, PartialEq, Eq, Clone)] 15 | pub struct ApnsToken(pub String); 16 | 17 | /// A HMS token. 18 | #[derive(Debug, PartialEq, Eq, Clone)] 19 | pub struct HmsToken(pub String); 20 | 21 | // A Threema Gateway token. 22 | #[derive(Debug, PartialEq, Eq, Clone)] 23 | pub struct ThreemaGatewayToken(pub String); 24 | 25 | /// The possible push token types. 26 | #[derive(Debug, PartialEq, Eq, Clone)] 27 | pub enum PushToken { 28 | Fcm(FcmToken), 29 | Apns(ApnsToken), 30 | Hms { 31 | token: HmsToken, 32 | app_id: String, 33 | }, 34 | ThreemaGateway { 35 | identity: String, 36 | public_key: [u8; 32], 37 | }, 38 | } 39 | 40 | impl PushToken { 41 | pub fn abbrev(&self) -> &'static str { 42 | match *self { 43 | PushToken::Fcm(_) => "FCM", 44 | PushToken::Apns(_) => "APNs", 45 | PushToken::Hms { .. } => "HMS", 46 | PushToken::ThreemaGateway { .. } => "ThreemaGateway", 47 | } 48 | } 49 | } 50 | 51 | impl AsRef for FcmToken { 52 | fn as_ref(&self) -> &str { 53 | self.0.as_str() 54 | } 55 | } 56 | 57 | /// Payload sent to end device inside the push notification. 58 | #[derive(Debug, Serialize)] 59 | pub(super) struct ThreemaPayload<'a> { 60 | /// Session id (public key of the initiator) 61 | #[serde(rename = "wcs")] 62 | session_id: &'a str, 63 | /// Affiliation id 64 | #[serde(rename = "wca", skip_serializing_if = "Option::is_none")] 65 | affiliation_id: Option<&'a str>, 66 | #[serde(flatten)] 67 | platform_specific_data: PlatformSpecificData, 68 | } 69 | 70 | #[derive(Debug, Serialize)] 71 | #[serde(untagged)] 72 | /// Data that does not have the same type on different platforms, although the fields are the same 73 | enum PlatformSpecificData { 74 | /// Used for FCM only. Has to be key/value payload of type string: 75 | /// 76 | AsString { 77 | /// Timestamp 78 | #[serde(rename = "wct", serialize_with = "serialize_as_str")] 79 | timestamp: i64, 80 | /// Version 81 | #[serde(rename = "wcv", serialize_with = "serialize_as_str")] 82 | version: u16, 83 | }, 84 | /// Used for all platforms but FCM 85 | AsNumber { 86 | /// Timestamp 87 | #[serde(rename = "wct")] 88 | timestamp: i64, 89 | /// Version 90 | #[serde(rename = "wcv")] 91 | version: u16, 92 | }, 93 | } 94 | 95 | fn serialize_as_str(n: &impl ToString, serializer: S) -> Result 96 | where 97 | S: Serializer, 98 | { 99 | serializer.serialize_str(&n.to_string()) 100 | } 101 | 102 | impl<'a> ThreemaPayload<'a> { 103 | pub fn new( 104 | session_id: &'a str, 105 | affiliation_id: Option<&'a str>, 106 | version: u16, 107 | is_fcm_payload: bool, 108 | ) -> Self { 109 | let timestamp = Utc::now().timestamp(); 110 | ThreemaPayload { 111 | session_id, 112 | affiliation_id, 113 | platform_specific_data: if is_fcm_payload { 114 | PlatformSpecificData::AsString { timestamp, version } 115 | } else { 116 | PlatformSpecificData::AsNumber { timestamp, version } 117 | }, 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/push/threema_gateway.rs: -------------------------------------------------------------------------------- 1 | //! Code related to the sending of Threema Gateway push notifications. 2 | 3 | use std::str; 4 | 5 | use aead::AeadInPlace; 6 | use crypto_secretbox::{ 7 | aead::{AeadCore, KeyInit, OsRng}, 8 | XSalsa20Poly1305, 9 | }; 10 | use data_encoding::HEXLOWER; 11 | use reqwest::{ 12 | header::{ACCEPT, CONTENT_TYPE}, 13 | Client, StatusCode, 14 | }; 15 | use serde_json as json; 16 | use x25519_dalek::StaticSecret; 17 | 18 | use crate::{ 19 | errors::SendPushError, 20 | push::{threema_gateway::x25519::SharedSecretHSalsa20, ThreemaPayload}, 21 | ThreemaGatewayPrivateKey, 22 | }; 23 | 24 | mod x25519 { 25 | use aead::generic_array::GenericArray; 26 | use salsa20::hsalsa; 27 | use x25519_dalek::SharedSecret; 28 | use zeroize::{Zeroize, ZeroizeOnDrop}; 29 | 30 | #[derive(Zeroize, ZeroizeOnDrop)] 31 | pub struct SharedSecretHSalsa20([u8; 32]); 32 | 33 | impl SharedSecretHSalsa20 { 34 | /// View this shared secret key as a byte array. 35 | #[inline] 36 | pub fn as_bytes(&self) -> &[u8; 32] { 37 | &self.0 38 | } 39 | } 40 | 41 | impl From for SharedSecretHSalsa20 { 42 | fn from(secret: SharedSecret) -> Self { 43 | // Use HSalsa20 to create a uniformly random key from the shared secret 44 | Self( 45 | hsalsa::( 46 | GenericArray::from_slice(secret.as_bytes()), 47 | &GenericArray::default(), 48 | ) 49 | .into(), 50 | ) 51 | } 52 | } 53 | } 54 | 55 | /// Send a Threema Gateway push notification. 56 | pub async fn send_push( 57 | client: &Client, 58 | base_url: &str, 59 | secret: &str, 60 | from_identity: &str, 61 | private_key: ThreemaGatewayPrivateKey, 62 | to_identity: &str, 63 | public_key: [u8; 32], 64 | version: u16, 65 | session: &str, 66 | affiliation: Option<&str>, 67 | ) -> Result<(), SendPushError> { 68 | let payload = ThreemaPayload::new(session, affiliation, version, false); 69 | trace!("Sending payload: {:#?}", payload); 70 | 71 | // Encode and encrypt 72 | let (nonce, message) = { 73 | let private_key = StaticSecret::from(private_key.0); 74 | let shared_secret = 75 | SharedSecretHSalsa20::from(private_key.diffie_hellman(&public_key.into())); 76 | let cipher = XSalsa20Poly1305::new(shared_secret.as_bytes().into()); 77 | let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng); 78 | let mut message: Vec = [ 79 | // E2E message type 80 | &[0xfe], 81 | // Content 82 | json::to_vec(&payload) 83 | .expect("Could not encode JSON payload") 84 | .as_slice(), 85 | // No additional padding because it's kinda obvious what's being sent here by looking at the sender's 86 | // identity. 87 | &[0x01], 88 | ] 89 | .concat(); 90 | cipher 91 | .encrypt_in_place(&nonce, b"", &mut message) 92 | .map_err(|_| SendPushError::Internal("Encryption failed".into()))?; 93 | (nonce, message) 94 | }; 95 | 96 | // URL-encode (sigh) 97 | let body = form_urlencoded::Serializer::new(String::new()) 98 | .append_pair("secret", secret) 99 | .append_pair("from", from_identity) 100 | .append_pair("to", to_identity) 101 | .append_pair("noPush", "1") 102 | .append_pair("noDeliveryReceipts", "1") 103 | .append_pair("nonce", HEXLOWER.encode(nonce.as_slice()).as_str()) 104 | .append_pair("box", HEXLOWER.encode(&message).as_str()) 105 | .finish(); 106 | 107 | // Send request 108 | let response = client 109 | .post(format!("{}/send_e2e", base_url)) 110 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 111 | .header(ACCEPT, "application/json") 112 | .body(body) 113 | .send() 114 | .await 115 | .map_err(SendPushError::SendError)?; 116 | 117 | // Check status code 118 | match response.status() { 119 | StatusCode::OK => Ok(()), 120 | StatusCode::BAD_REQUEST => Err(SendPushError::RemoteServer( 121 | "Receiver identity invalid".into(), 122 | )), 123 | StatusCode::UNAUTHORIZED => Err(SendPushError::RemoteServer( 124 | "Unauthorized. Is the API secret correct?".into(), 125 | )), 126 | StatusCode::PAYMENT_REQUIRED => Err(SendPushError::RemoteServer("Out of credits".into())), 127 | StatusCode::PAYLOAD_TOO_LARGE => { 128 | Err(SendPushError::RemoteServer("Message too long".into())) 129 | } 130 | status => Err(SendPushError::Internal(format!( 131 | "Unknown error: Status {}", 132 | status 133 | ))), 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap, convert::Into, net::SocketAddr, sync::Arc}; 2 | 3 | use a2::{ 4 | client::{Client as ApnsClient, Endpoint}, 5 | CollapseId, 6 | }; 7 | use axum::{ 8 | body::Body, 9 | extract::State, 10 | http::{Request, StatusCode}, 11 | response::Response, 12 | routing::post, 13 | Router, 14 | }; 15 | use data_encoding::HEXLOWER_PERMISSIVE; 16 | use futures::future::{BoxFuture, FutureExt}; 17 | use reqwest::{header::CONTENT_TYPE, Client as HttpClient}; 18 | use tokio::net::TcpListener; 19 | use tower::ServiceBuilder; 20 | use tower_http::trace::{self, TraceLayer}; 21 | use tracing::Level; 22 | 23 | use crate::{ 24 | config::{Config, ThreemaGatewayConfig}, 25 | errors::{InfluxdbError, InitError, SendPushError, ServiceError}, 26 | http_client, 27 | influxdb::Influxdb, 28 | push::{ 29 | apns, fcm, 30 | fcm::{AndroidTtlSeconds, FcmState, HttpOauthTokenObtainer, RequestOauthToken}, 31 | hms::{self, HmsContext, HmsEndpointConfig}, 32 | threema_gateway, ApnsToken, FcmToken, HmsToken, PushToken, ThreemaPayload, 33 | }, 34 | ThreemaGatewayPrivateKey, 35 | }; 36 | 37 | static COLLAPSE_KEY_PREFIX: &str = "relay"; 38 | static TTL_DEFAULT: u32 = 90; 39 | static PUSH_PATH: &str = "/push"; 40 | 41 | #[derive(Clone)] 42 | struct AppState 43 | where 44 | R: fcm::RequestOauthToken, 45 | { 46 | fcm_state: Arc>, 47 | apns_client_prod: ApnsClient, 48 | apns_client_sbox: ApnsClient, 49 | hms_contexts: Arc>, 50 | hms_config: Arc, 51 | threema_gateway_client: HttpClient, 52 | threema_gateway_config: Option, 53 | threema_gateway_private_key: Option, 54 | influxdb: Option>, 55 | } 56 | 57 | /// Start the server and run infinitely. 58 | pub async fn serve( 59 | config: Config, 60 | apns_api_key: &[u8], 61 | threema_gateway_private_key: Option, 62 | listen_on: SocketAddr, 63 | ) -> Result<(), InitError> { 64 | // Destructure config 65 | let Config { 66 | fcm, 67 | apns, 68 | hms, 69 | threema_gateway, 70 | influxdb, 71 | } = config; 72 | 73 | // Convert missing hms config to empty HashMap 74 | let hms = hms.unwrap_or_default(); 75 | 76 | let token_obtainer = HttpOauthTokenObtainer::new(&fcm.service_account_key) 77 | .await 78 | .map_err(InitError::Fcm)?; 79 | 80 | let fcm_state = FcmState::new(&fcm, None, token_obtainer) 81 | .await 82 | .map_err(InitError::Fcm)?; 83 | 84 | // Create APNs clients 85 | let apns_client_prod = apns::create_client( 86 | Endpoint::Production, 87 | apns_api_key, 88 | apns.team_id.clone(), 89 | apns.key_id.clone(), 90 | )?; 91 | let apns_client_sbox = 92 | apns::create_client(Endpoint::Sandbox, apns_api_key, apns.team_id, apns.key_id)?; 93 | 94 | // Create a shared HMS HTTP client 95 | let hms_client = http_client::make_client(90).map_err(InitError::Reqwest)?; 96 | 97 | // Create a HMS context for every config entry 98 | let hms_contexts = Arc::new( 99 | hms.iter() 100 | .map(|(k, v)| { 101 | ( 102 | k.to_string(), 103 | HmsContext::new(hms_client.clone(), v.clone()), 104 | ) 105 | }) 106 | .collect::>(), 107 | ); 108 | 109 | // Create Threema Gateway HTTP client 110 | let threema_gateway_client = http_client::make_client(90).map_err(InitError::Reqwest)?; 111 | 112 | // Create InfluxDB client 113 | let influxdb = influxdb.map(|c| { 114 | Arc::new( 115 | Influxdb::new(c.connection_string, &c.user, &c.pass, c.db) 116 | .expect("Failed to create Influxdb instance"), 117 | ) 118 | }); 119 | 120 | // Initialize InfluxDB 121 | if let Some(ref db) = influxdb { 122 | fn log_started(db: &Influxdb) -> BoxFuture<'_, ()> { 123 | async move { 124 | if let Err(e) = db.log_started().await { 125 | match e { 126 | InfluxdbError::DatabaseNotFound => { 127 | warn!("InfluxDB database does not yet exist. Create it..."); 128 | match db.create_db().await { 129 | Ok(_) => log_started(db).await, 130 | Err(e) => error!("Could not create InfluxDB database: {}", e), 131 | } 132 | } 133 | other => error!("Could not log starting event to InfluxDB: {}", other), 134 | } 135 | }; 136 | } 137 | .boxed() 138 | } 139 | debug!("Sending stats to InfluxDB"); 140 | log_started(db).await; 141 | } else { 142 | debug!("Not using InfluxDB logging"); 143 | }; 144 | 145 | let state = AppState { 146 | fcm_state: Arc::new(fcm_state), 147 | apns_client_prod: apns_client_prod.clone(), 148 | apns_client_sbox: apns_client_sbox.clone(), 149 | hms_contexts: hms_contexts.clone(), 150 | hms_config: HmsEndpointConfig::new_shared(), 151 | threema_gateway_client: threema_gateway_client.clone(), 152 | threema_gateway_private_key: threema_gateway_private_key.clone(), 153 | threema_gateway_config: threema_gateway.clone(), 154 | influxdb: influxdb.clone(), 155 | }; 156 | 157 | let app = get_router::(state); 158 | 159 | let listener = TcpListener::bind(listen_on) 160 | .await 161 | .map_err(|source| InitError::Io { 162 | reason: "Failed to bind to address", 163 | source, 164 | })?; 165 | 166 | axum::serve( 167 | listener, 168 | app.into_make_service_with_connect_info::(), 169 | ) 170 | .await 171 | .map_err(|e| InitError::Io { 172 | reason: "Failed to serve app", 173 | source: e, 174 | }) 175 | } 176 | 177 | fn get_router(state: AppState) -> Router { 178 | axum::Router::new() 179 | .route(PUSH_PATH, post(handle_push_request)) 180 | .layer( 181 | ServiceBuilder::new().layer( 182 | TraceLayer::new_for_http() 183 | .make_span_with(|req: &Request<_>| { 184 | let maybe_port = req 185 | .extensions() 186 | .get::>() 187 | .map(|ci| ci.0.port()); 188 | const SPAN_NAME: &str = "handle_push"; 189 | if let Some(port) = maybe_port { 190 | info_span!(SPAN_NAME, "type" = tracing::field::Empty, "port" = port) 191 | } else { 192 | info_span!(SPAN_NAME, "type" = tracing::field::Empty) 193 | } 194 | }) 195 | .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), 196 | ), 197 | ) 198 | .with_state(state) 199 | } 200 | 201 | /// Main push handling entry point. 202 | /// 203 | /// Handle a request, return a response. 204 | async fn handle_push_request( 205 | State(state): State>, 206 | headers: axum::http::HeaderMap, 207 | body: axum::body::Bytes, 208 | ) -> Result { 209 | // Verify content type 210 | let content_type = headers.get(CONTENT_TYPE).and_then(|h| h.to_str().ok()); 211 | match content_type { 212 | Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => {} 213 | Some(ct) => { 214 | warn!("Bad request, invalid content type: {}", ct); 215 | return Err(ServiceError::InvalidContentType(ct.to_owned())); 216 | } 217 | None => { 218 | warn!("Bad request, missing content type"); 219 | return Err(ServiceError::MissingContentType); 220 | } 221 | } 222 | 223 | let parsed = form_urlencoded::parse(&body).collect::>(); 224 | trace!("Request params: {:?}", parsed); 225 | 226 | // Validate parameters 227 | if parsed.is_empty() { 228 | return Err(ServiceError::MissingParams); 229 | } 230 | 231 | /// Iterate over parameters and find first matching key. 232 | /// Return an option. 233 | macro_rules! find { 234 | ($name:expr) => { 235 | parsed 236 | .iter() 237 | .find(|&&(ref k, _)| k == $name) 238 | .map(|&(_, ref v)| v) 239 | }; 240 | } 241 | 242 | /// Iterate over parameters and find first matching key. 243 | /// If the key is not found, then return a HTTP 400 response. 244 | macro_rules! find_or_bad_request { 245 | ($name:expr) => { 246 | match find!($name) { 247 | Some(v) => v, 248 | None => { 249 | warn!("Missing request parameter: {}", $name); 250 | return Err(ServiceError::MissingParams); 251 | } 252 | } 253 | }; 254 | } 255 | 256 | /// Iterate over parameters and find first matching key. 257 | /// If the key is not found, return a default. 258 | macro_rules! find_or_default { 259 | ($name:expr, $default:expr) => { 260 | match find!($name) { 261 | Some(v) => v, 262 | None => $default, 263 | } 264 | }; 265 | } 266 | 267 | let push_type = find_or_default!("type", "fcm"); 268 | { 269 | let span = tracing::Span::current(); 270 | span.record("type", push_type); 271 | } 272 | 273 | // Get parameters 274 | let push_token = match push_type { 275 | "gcm" | "fcm" => PushToken::Fcm(FcmToken(find_or_bad_request!("token").to_string())), 276 | "apns" => PushToken::Apns(ApnsToken(find_or_bad_request!("token").to_string())), 277 | "hms" => PushToken::Hms { 278 | token: HmsToken(find_or_bad_request!("token").to_string()), 279 | app_id: find_or_bad_request!("appid").to_string(), 280 | }, 281 | "threema-gateway" => { 282 | let identity = find_or_bad_request!("identity").to_string(); 283 | if identity.len() != 8 || identity.starts_with('*') { 284 | warn!("Got push request with invalid identity: {}", identity); 285 | return Err(ServiceError::InvalidParams); 286 | } 287 | let public_key_hex = find_or_bad_request!("public_key"); 288 | if public_key_hex.len() != 64 { 289 | warn!( 290 | "Got push request with invalid public key length: {}", 291 | public_key_hex.len() 292 | ); 293 | return Err(ServiceError::InvalidParams); 294 | } 295 | let Ok(public_key) = HEXLOWER_PERMISSIVE.decode(public_key_hex.as_bytes()) else { 296 | warn!( 297 | "Got push request with invalid public key: {}", 298 | public_key_hex 299 | ); 300 | return Err(ServiceError::InvalidParams); 301 | }; 302 | let Ok(public_key) = public_key.try_into() else { 303 | warn!( 304 | "Got push request with invalid public key: {}", 305 | public_key_hex 306 | ); 307 | return Err(ServiceError::InvalidParams); 308 | }; 309 | PushToken::ThreemaGateway { 310 | identity, 311 | public_key, 312 | } 313 | } 314 | other => { 315 | warn!("Got push request with invalid token type: {}", other); 316 | return Err(ServiceError::InvalidParams); 317 | } 318 | }; 319 | let session_public_key = find_or_bad_request!("session"); 320 | let version_string = find_or_bad_request!("version"); 321 | let version: u16 = match version_string.trim().parse::() { 322 | Ok(parsed) => parsed, 323 | Err(e) => { 324 | warn!("Got push request with invalid version param: {:?}", e); 325 | return Err(ServiceError::InvalidParams); 326 | } 327 | }; 328 | let affiliation = find!("affiliation").map(Cow::as_ref); 329 | let ttl_string = find!("ttl").map(|ttl_str| ttl_str.trim().parse()); 330 | let ttl: u32 = match ttl_string { 331 | // Parsing as u32 succeeded 332 | Some(Ok(val)) => val, 333 | // Parsing as u32 failed 334 | Some(Err(_)) => return Err(ServiceError::InvalidParams), 335 | // No TTL value was specified 336 | None => TTL_DEFAULT, 337 | }; 338 | let collapse_key: Option = 339 | find!("collapse_key").map(|key| format!("{}.{}", COLLAPSE_KEY_PREFIX, key)); 340 | 341 | #[allow(clippy::match_wildcard_for_single_variants)] 342 | let (bundle_id, endpoint, collapse_id) = match push_token { 343 | PushToken::Apns(_) => { 344 | let bundle_id = Some(find_or_bad_request!("bundleid")); 345 | let endpoint_str = find_or_bad_request!("endpoint"); 346 | let endpoint = Some(match endpoint_str.as_ref() { 347 | "p" => Endpoint::Production, 348 | "s" => Endpoint::Sandbox, 349 | _ => return Err(ServiceError::InvalidParams), 350 | }); 351 | let collapse_id = match collapse_key.as_deref().map(CollapseId::new) { 352 | Some(Ok(id)) => Some(id), 353 | Some(Err(_)) => return Err(ServiceError::InvalidParams), 354 | None => None, 355 | }; 356 | (bundle_id, endpoint, collapse_id) 357 | } 358 | _ => (None, None, None), 359 | }; 360 | 361 | // Send push notification 362 | let variant = match bundle_id { 363 | Some(bid) if bid.ends_with(".voip") => "/s", 364 | Some(_bid) => "/n", 365 | None => "", 366 | }; 367 | info!( 368 | "Sending push message to {}{} for session {} [v{}]", 369 | push_token.abbrev(), 370 | variant, 371 | session_public_key, 372 | version 373 | ); 374 | let push_result = match push_token { 375 | PushToken::Fcm(ref token) => { 376 | let retry_calc = fcm::get_push_retry_calculator(); 377 | let payload = ThreemaPayload::new(session_public_key, affiliation, version, true); 378 | let http_payload = fcm::HttpV1Payload::new( 379 | AndroidTtlSeconds::new(ttl), 380 | token.as_ref(), 381 | &payload, 382 | collapse_key.as_deref(), 383 | ); 384 | fcm::send_push(state.fcm_state.clone(), retry_calc, http_payload, 0) 385 | .await 386 | .map(|_| {}) 387 | } 388 | PushToken::Apns(ref token) => { 389 | let client = match endpoint.unwrap() { 390 | Endpoint::Production => { 391 | debug!("Using production endpoint"); 392 | &state.apns_client_prod 393 | } 394 | Endpoint::Sandbox => { 395 | debug!("Using sandbox endpoint"); 396 | &state.apns_client_sbox 397 | } 398 | }; 399 | apns::send_push( 400 | client, 401 | token, 402 | bundle_id.expect("bundle_id is None"), 403 | version, 404 | session_public_key, 405 | affiliation, 406 | collapse_id, 407 | ttl, 408 | ) 409 | .await 410 | } 411 | PushToken::Hms { 412 | ref token, 413 | ref app_id, 414 | } => match state.hms_contexts.get(app_id) { 415 | // We found a context for this App ID 416 | Some(context) => { 417 | hms::send_push( 418 | context, 419 | &state.hms_config, 420 | token, 421 | version, 422 | session_public_key, 423 | affiliation, 424 | ttl, 425 | ) 426 | .await 427 | } 428 | // No config found for this App ID 429 | None => Err(SendPushError::RemoteClient(format!( 430 | "Unknown HMS App ID: {}", 431 | app_id 432 | ))), 433 | }, 434 | PushToken::ThreemaGateway { 435 | ref identity, 436 | ref public_key, 437 | } => { 438 | if let (Some(threema_gateway_config), Some(threema_gateway_private_key)) = ( 439 | state.threema_gateway_config, 440 | state.threema_gateway_private_key, 441 | ) { 442 | threema_gateway::send_push( 443 | &state.threema_gateway_client, 444 | &threema_gateway_config.base_url, 445 | &threema_gateway_config.secret, 446 | &threema_gateway_config.identity, 447 | threema_gateway_private_key, 448 | identity, 449 | *public_key, 450 | version, 451 | session_public_key, 452 | affiliation, 453 | ) 454 | .await 455 | } else { 456 | // No config found for Threema Gateway 457 | Err(SendPushError::RemoteClient( 458 | "Cannot send Threema Gateway Push, not configured".into(), 459 | )) 460 | } 461 | } 462 | }; 463 | 464 | // Log to InfluxDB 465 | if let Some(influxdb) = state.influxdb { 466 | let log_result = influxdb 467 | .log_push(push_token.abbrev(), version, push_result.is_ok()) 468 | .await; 469 | if let Err(e) = log_result { 470 | warn!("Could not submit stats to InfluxDB: {}", e); 471 | } 472 | } 473 | 474 | // Handle result 475 | match push_result { 476 | Ok(()) => { 477 | debug!("Success!"); 478 | Ok(Response::builder() 479 | .status(StatusCode::NO_CONTENT) 480 | .header(CONTENT_TYPE, "text/plain") 481 | .body(Body::empty()) 482 | .unwrap()) 483 | } 484 | Err(e) => Ok(Response::builder() 485 | .status({ 486 | info!("{e}"); 487 | match e { 488 | SendPushError::RemoteServer(_) => StatusCode::BAD_GATEWAY, 489 | SendPushError::SendError(_) | SendPushError::RemoteClient(_) => { 490 | StatusCode::BAD_REQUEST 491 | } 492 | SendPushError::Internal(_) | SendPushError::RemoteAuth(_) => { 493 | StatusCode::INTERNAL_SERVER_ERROR 494 | } 495 | } 496 | }) 497 | .header(CONTENT_TYPE, "text/plain") 498 | .body(Body::from("Push not successful")) 499 | .unwrap()), 500 | } 501 | } 502 | 503 | #[cfg(test)] 504 | mod tests { 505 | use axum::http::{Request, Response}; 506 | use futures::StreamExt; 507 | use openssl::{ 508 | ec::{EcGroup, EcKey}, 509 | nid::Nid, 510 | }; 511 | use tower::util::ServiceExt; 512 | use wiremock::{ 513 | matchers::{body_partial_json, method, path}, 514 | Mock, MockServer, ResponseTemplate, 515 | }; 516 | 517 | use crate::{config::FcmConfig, server::tests::fcm::test::get_fcm_test_path}; 518 | 519 | use self::fcm::{test::MockAccessTokenObtainer, RequestOauthToken}; 520 | 521 | use super::*; 522 | 523 | async fn get_body(res: Response) -> String { 524 | let mut full_body = Vec::new(); 525 | let mut body = res.into_body().into_data_stream(); 526 | while let Some(chunk) = body.next().await { 527 | full_body.extend_from_slice(&chunk.unwrap()); 528 | } 529 | ::std::str::from_utf8(&full_body).unwrap().to_string() 530 | } 531 | 532 | fn get_apns_test_key() -> Vec { 533 | let curve: Nid = Nid::SECP128R1; 534 | let group = EcGroup::from_curve_name(curve).unwrap(); 535 | let key = EcKey::generate(&group).unwrap(); 536 | key.private_key_to_pem().unwrap() 537 | } 538 | 539 | fn get_test_max_retries() -> u8 { 540 | 6 541 | } 542 | 543 | fn get_test_fcm_config() -> FcmConfig { 544 | FcmConfig { 545 | service_account_key: b"yolo".into(), 546 | project_id: 12345678, 547 | max_retries: get_test_max_retries(), 548 | } 549 | } 550 | 551 | fn get_mock_fcm_response() -> &'static str { 552 | "{\"name\":\"mock-response\"}" 553 | } 554 | 555 | async fn get_test_state( 556 | fcm_config: &FcmConfig, 557 | fcm_endpoint: Option, 558 | ) -> AppState { 559 | let api_key = get_apns_test_key(); 560 | let apns_client_prod = apns::create_client( 561 | Endpoint::Production, 562 | api_key.as_slice(), 563 | "team_id", 564 | "key_id", 565 | ) 566 | .unwrap(); 567 | let apns_client_sbox = 568 | apns::create_client(Endpoint::Sandbox, api_key.as_slice(), "team_id", "key_id") 569 | .unwrap(); 570 | let threema_gateway_client = http_client::make_client(10).expect("threema_gateway_client"); 571 | 572 | let access_tokan_obtainer = 573 | fcm::test::MockAccessTokenObtainer::new(&fcm_config.service_account_key) 574 | .await 575 | .expect("MockAccessTokenObtainer"); 576 | 577 | let fcm_state = FcmState::new(fcm_config, fcm_endpoint, access_tokan_obtainer) 578 | .await 579 | .unwrap(); 580 | 581 | AppState { 582 | fcm_state: Arc::new(fcm_state), 583 | apns_client_prod, 584 | apns_client_sbox, 585 | hms_contexts: Arc::new(HashMap::new()), 586 | hms_config: HmsEndpointConfig::stub_with(None), 587 | threema_gateway_client, 588 | threema_gateway_config: None, 589 | threema_gateway_private_key: None, 590 | influxdb: None, 591 | } 592 | } 593 | 594 | async fn get_test_app(fcm_endpoint: Option) -> (Router, FcmConfig) { 595 | let fcm_config = get_test_fcm_config(); 596 | let state = get_test_state(&fcm_config, fcm_endpoint).await; 597 | let router = get_router(state); 598 | (router, fcm_config) 599 | } 600 | 601 | /// Handle invalid paths 602 | #[tokio::test] 603 | async fn test_invalid_path() { 604 | let (app, _) = get_test_app(None).await; 605 | 606 | let req = Request::builder().uri("/").body(Body::empty()).unwrap(); 607 | 608 | let response = app.oneshot(req).await.unwrap(); 609 | 610 | assert_eq!(response.status(), StatusCode::NOT_FOUND); 611 | } 612 | 613 | /// Handle invalid methods 614 | #[tokio::test] 615 | async fn test_invalid_method() { 616 | let (app, _) = get_test_app(None).await; 617 | 618 | let req = Request::builder() 619 | .method("GET") 620 | .uri(PUSH_PATH) 621 | .body(Body::empty()) 622 | .unwrap(); 623 | 624 | let response = app.oneshot(req).await.unwrap(); 625 | 626 | assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); 627 | } 628 | 629 | /// Handle invalid request content type 630 | #[tokio::test] 631 | async fn test_invalid_contenttype() { 632 | let (app, _) = get_test_app(None).await; 633 | 634 | let req = Request::post(PUSH_PATH) 635 | .header(CONTENT_TYPE, "text/plain") 636 | .body(Body::empty()) 637 | .unwrap(); 638 | 639 | let resp = app.oneshot(req).await.unwrap(); 640 | 641 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 642 | let body = get_body(resp).await; 643 | assert_eq!(&body, "Invalid content type: text/plain"); 644 | } 645 | 646 | /// Handle missing request content type 647 | #[tokio::test] 648 | async fn test_missing_contenttype() { 649 | let (app, _) = get_test_app(None).await; 650 | 651 | let req = Request::post(PUSH_PATH).body(Body::empty()).unwrap(); 652 | let resp = app.oneshot(req).await.unwrap(); 653 | 654 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 655 | let body = get_body(resp).await; 656 | assert_eq!(&body, "Missing content type"); 657 | } 658 | 659 | /// A request without parameters should result in a HTTP 400 response. 660 | #[tokio::test] 661 | async fn test_no_params() { 662 | let (app, _) = get_test_app(None).await; 663 | 664 | let req = Request::post(PUSH_PATH) 665 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 666 | .body(Body::empty()) 667 | .unwrap(); 668 | let resp = app.oneshot(req).await.unwrap(); 669 | 670 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 671 | let body = get_body(resp).await; 672 | assert_eq!(&body, "Missing parameters"); 673 | } 674 | 675 | /// A request with missing parameters should result in a HTTP 400 response. 676 | #[tokio::test] 677 | async fn test_missing_params() { 678 | let (app, _) = get_test_app(None).await; 679 | 680 | let req = Request::post(PUSH_PATH) 681 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 682 | .body("token=1234".to_string()) 683 | .unwrap(); 684 | let resp = app.oneshot(req).await.unwrap(); 685 | 686 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 687 | let body = get_body(resp).await; 688 | assert_eq!(&body, "Missing parameters"); 689 | } 690 | 691 | /// A request with missing parameters should result in a HTTP 400 response. 692 | #[tokio::test] 693 | async fn test_missing_params_apns() { 694 | let (app, _) = get_test_app(None).await; 695 | 696 | let req = Request::post(PUSH_PATH) 697 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 698 | .body("type=apns&token=1234&session=123deadbeef&version=3".to_string()) 699 | .unwrap(); 700 | let resp = app.oneshot(req).await.unwrap(); 701 | 702 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 703 | let body = get_body(resp).await; 704 | assert_eq!(&body, "Missing parameters"); 705 | } 706 | 707 | /// A request with bad parameters should result in a HTTP 400 response. 708 | #[tokio::test] 709 | async fn test_bad_endpoint() { 710 | let (app, _) = get_test_app(None).await; 711 | 712 | let req = Request::post(PUSH_PATH) 713 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 714 | .body( 715 | "type=apns&token=1234&session=123deadbeef&version=3&bundleid=jklö&endpoint=q" 716 | .to_string(), 717 | ) 718 | .unwrap(); 719 | let resp = app.oneshot(req).await.unwrap(); 720 | 721 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 722 | let body = get_body(resp).await; 723 | assert_eq!(&body, "Invalid parameters"); 724 | } 725 | 726 | /// A request with missing parameters should result in a HTTP 400 response. 727 | #[tokio::test] 728 | async fn test_bad_token_type() { 729 | let (app, _) = get_test_app(None).await; 730 | 731 | let req = Request::post(PUSH_PATH) 732 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 733 | .body("type=abc&token=aassddff&session=deadbeef&version=1".to_string()) 734 | .unwrap(); 735 | let resp = app.oneshot(req).await.unwrap(); 736 | 737 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 738 | let body = get_body(resp).await; 739 | assert_eq!(&body, "Invalid parameters"); 740 | } 741 | 742 | /// A request with invalid TTL parameter should result in a HTTP 400 response. 743 | #[tokio::test] 744 | async fn test_invalid_ttl() { 745 | let (app, _) = get_test_app(None).await; 746 | 747 | let req = Request::post(PUSH_PATH) 748 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 749 | .body( 750 | "type=fcm&token=aassddff&session=deadbeef&version=1&ttl=9999999999999999" 751 | .to_string(), 752 | ) 753 | .unwrap(); 754 | let resp = app.oneshot(req).await.unwrap(); 755 | 756 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 757 | let body = get_body(resp).await; 758 | assert_eq!(&body, "Invalid parameters"); 759 | } 760 | 761 | #[tokio::test] 762 | #[allow(clippy::useless_format)] 763 | async fn test_fcm_ok() { 764 | let to = "aassddff"; 765 | let session = "deadbeef"; 766 | let ttl = 120; 767 | let version = 3; 768 | let collapse_key = "another_collapse_key"; 769 | 770 | let mock_server = MockServer::start().await; 771 | 772 | let expected_body = serde_json::json!({ 773 | "message": { 774 | "token": to, 775 | "data": { 776 | "wcs": session, 777 | "wcv": version.to_string() 778 | }, 779 | "android": { 780 | "collapse_key": format!("relay.{collapse_key}"), 781 | "priority": "HIGH", 782 | "ttl": format!("{}s", ttl) 783 | } 784 | } 785 | }); 786 | 787 | let (app, fcm_config) = get_test_app(Some(mock_server.uri())).await; 788 | 789 | Mock::given(method("POST")) 790 | .and(path(get_fcm_test_path(&fcm_config))) 791 | .and(body_partial_json(expected_body)) 792 | .respond_with(ResponseTemplate::new(200).set_body_string(get_mock_fcm_response())) 793 | .expect(1) 794 | .mount(&mock_server) 795 | .await; 796 | 797 | let req = Request::post(PUSH_PATH) 798 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 799 | .body(format!( 800 | "type=fcm&token={to}&session={session}&version={version}&ttl={ttl}&collapse_key={collapse_key}", 801 | )) 802 | .unwrap(); 803 | let resp = app.oneshot(req).await.unwrap(); 804 | 805 | // Validate response 806 | assert_eq!(resp.status(), StatusCode::NO_CONTENT); 807 | assert_eq!( 808 | resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), 809 | "text/plain", 810 | ); 811 | } 812 | 813 | #[tokio::test] 814 | #[allow(clippy::useless_format)] 815 | async fn test_fcm_invalid_response() { 816 | let to = "aassddff"; 817 | let session = "deadbeef"; 818 | let version = 1; 819 | let collapse_key = "some_collapse_key"; 820 | let affiliation_id = "some_affiliation_id"; 821 | 822 | let mock_server = MockServer::start().await; 823 | 824 | let expected_body = serde_json::json!({ 825 | "message": { 826 | "token": to, 827 | "data": { 828 | "wcs": session, 829 | "wcv": version.to_string(), 830 | "wca": affiliation_id 831 | }, 832 | "android": { 833 | "collapse_key": format!("relay.{collapse_key}"), 834 | "priority": "HIGH", 835 | "ttl": "90s" 836 | } 837 | } 838 | }); 839 | 840 | let (app, fcm_config) = get_test_app(Some(mock_server.uri())).await; 841 | 842 | Mock::given(method("POST")) 843 | .and(path(get_fcm_test_path(&fcm_config))) 844 | .and(body_partial_json(expected_body)) 845 | .respond_with(ResponseTemplate::new(200).set_body_string("invalid body of response")) 846 | .expect(1) 847 | .mount(&mock_server) 848 | .await; 849 | 850 | let req = Request::post(PUSH_PATH) 851 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 852 | .body(format!( 853 | "type=fcm&token={to}&session={session}&version={version}&collapse_key={collapse_key}&affiliation={affiliation_id}", 854 | )) 855 | .unwrap(); 856 | let resp = app.oneshot(req).await.unwrap(); 857 | 858 | // Validate response 859 | assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); 860 | assert_eq!( 861 | resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), 862 | "text/plain", 863 | ); 864 | } 865 | 866 | async fn test_fcm_process_error( 867 | msg: &str, 868 | status_code: StatusCode, 869 | expected_http_count: Option, 870 | expected_status_code: StatusCode, 871 | ) { 872 | let mock_server = MockServer::start().await; 873 | 874 | let (app, fcm_config) = get_test_app(Some(mock_server.uri())).await; 875 | 876 | let error_body = fcm::test::get_fcm_error( 877 | status_code, 878 | &format!("Description of the error {}", msg), 879 | msg, 880 | ); 881 | 882 | Mock::given(method("POST")) 883 | .and(path(get_fcm_test_path(&fcm_config))) 884 | .respond_with(ResponseTemplate::new(status_code.as_u16()).set_body_json(error_body)) 885 | .expect(expected_http_count.unwrap_or(1)) 886 | .mount(&mock_server) 887 | .await; 888 | 889 | let req = Request::post(PUSH_PATH) 890 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded") 891 | .body("type=fcm&token=aassddff&session=deadbeef&version=1".to_string()) 892 | .unwrap(); 893 | let resp = app.oneshot(req).await.unwrap(); 894 | 895 | assert_eq!(resp.status(), expected_status_code); 896 | assert_eq!( 897 | resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), 898 | "text/plain", 899 | ); 900 | let body = get_body(resp).await; 901 | assert_eq!(&body, "Push not successful"); 902 | } 903 | 904 | #[tokio::test] 905 | async fn test_fcm_invalid() { 906 | test_fcm_process_error( 907 | "INVALID_ARGUMENT", 908 | StatusCode::BAD_REQUEST, 909 | None, 910 | StatusCode::BAD_REQUEST, 911 | ) 912 | .await; 913 | } 914 | 915 | #[tokio::test] 916 | async fn test_fcm_unregistered() { 917 | test_fcm_process_error( 918 | "UNREGISTERED", 919 | StatusCode::NOT_FOUND, 920 | None, 921 | StatusCode::BAD_REQUEST, 922 | ) 923 | .await; 924 | } 925 | 926 | #[tokio::test] 927 | async fn test_fcm_sender_id_mismatch() { 928 | test_fcm_process_error( 929 | "SENDER_ID_MISMATCH", 930 | StatusCode::FORBIDDEN, 931 | None, 932 | StatusCode::BAD_GATEWAY, 933 | ) 934 | .await; 935 | } 936 | 937 | #[tokio::test] 938 | async fn test_fcm_unavailable() { 939 | test_fcm_process_error( 940 | "UNAVAILABLE", 941 | StatusCode::SERVICE_UNAVAILABLE, 942 | Some((get_test_max_retries() + 1).into()), 943 | StatusCode::BAD_GATEWAY, 944 | ) 945 | .await; 946 | } 947 | 948 | #[tokio::test] 949 | async fn test_fcm_internal_server_error() { 950 | test_fcm_process_error( 951 | "INTERNAL", 952 | StatusCode::INTERNAL_SERVER_ERROR, 953 | Some((get_test_max_retries() + 1).into()), 954 | StatusCode::BAD_GATEWAY, 955 | ) 956 | .await; 957 | } 958 | 959 | #[tokio::test] 960 | async fn test_fcm_unknown_error() { 961 | test_fcm_process_error( 962 | "YourBicycleWasStolen", 963 | StatusCode::IM_A_TEAPOT, 964 | None, 965 | StatusCode::BAD_GATEWAY, 966 | ) 967 | .await; 968 | } 969 | } 970 | --------------------------------------------------------------------------------