From 45640f08ee59bd7a4e1568721713e139fe09090f Mon Sep 17 00:00:00 2001 From: alyssa Date: Thu, 22 Aug 2024 07:10:35 +0900 Subject: [PATCH] feat: improve dispatch security --- .github/workflows/dispatch.yml | 33 + Cargo.lock | 706 ++++++++++++++------- Cargo.toml | 4 +- PluralKit.API/packages.lock.json | 6 +- PluralKit.Bot/Commands/Api.cs | 39 +- PluralKit.Bot/packages.lock.json | 12 +- PluralKit.Core/CoreConfig.cs | 2 + PluralKit.Core/Dispatch/DispatchModels.cs | 16 +- PluralKit.Core/Dispatch/DispatchService.cs | 44 +- PluralKit.Core/Utils/Emojis.cs | 2 +- PluralKit.Core/packages.lock.json | 6 +- PluralKit.Tests/packages.lock.json | 12 +- docs/content/api/dispatch.md | 2 +- lib/libpk/Cargo.toml | 3 +- services/dispatch/Cargo.toml | 16 + services/dispatch/Dockerfile | 17 + services/dispatch/src/logger.rs | 52 ++ services/dispatch/src/main.rs | 190 ++++++ 18 files changed, 893 insertions(+), 269 deletions(-) create mode 100644 .github/workflows/dispatch.yml create mode 100644 services/dispatch/Cargo.toml create mode 100644 services/dispatch/Dockerfile create mode 100644 services/dispatch/src/logger.rs create mode 100644 services/dispatch/src/main.rs diff --git a/.github/workflows/dispatch.yml b/.github/workflows/dispatch.yml new file mode 100644 index 00000000..a6306642 --- /dev/null +++ b/.github/workflows/dispatch.yml @@ -0,0 +1,33 @@ +name: Build and push dispatch Docker image +on: + push: + paths: + - '.github/workflows/dispatch.yml' + - 'Cargo.lock' + - 'services/dispatch/' + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + packages: write + if: github.repository == 'PluralKit/PluralKit' + steps: + - uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.CR_PAT }} + - uses: actions/checkout@v2 + - run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" | sed 's|/|-|g' >> $GITHUB_ENV + - uses: docker/build-push-action@v2 + with: + # https://github.com/docker/build-push-action/issues/378 + context: . + push: true + file: services/dispatch/Dockerfile + tags: | + ghcr.io/pluralkit/dispatch:${{ github.sha }} + ghcr.io/pluralkit/dispatch:${{ env.BRANCH_NAME }} + cache-from: type=registry,ref=ghcr.io/pluralkit/pluralkit:${{ env.BRANCH_NAME }} + cache-to: type=inline diff --git a/Cargo.lock b/Cargo.lock index dbb2f567..76f6764f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.7.6" @@ -239,6 +254,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.1" @@ -251,6 +281,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -339,7 +375,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -447,6 +483,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "der" version = "0.7.9" @@ -458,15 +500,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - [[package]] name = "digest" version = "0.9.0" @@ -488,6 +521,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.5", + "hickory-client", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -509,6 +557,24 @@ dependencies = [ "serde", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-as-inner" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -595,9 +661,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -626,7 +692,7 @@ dependencies = [ "sha-1", "tokio", "tokio-stream", - "tokio-util 0.6.10", + "tokio-util", "tracing", "url", ] @@ -762,6 +828,12 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "hashbrown" version = "0.12.3" @@ -823,12 +895,61 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-client" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab9683b08d8f8957a857b0236455d80e1886eaa8c6178af556aa7871fb61b55" +dependencies = [ + "cfg-if", + "data-encoding", + "futures-channel", + "futures-util", + "hickory-proto", + "once_cell", + "radix_trie", + "rand", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "hickory-proto" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -856,17 +977,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "hostname" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -dependencies = [ - "libc", - "match_cfg", - "winapi", -] - [[package]] name = "http" version = "0.2.8" @@ -993,6 +1103,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.3.1", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.5" @@ -1038,9 +1166,19 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1155,9 +1293,7 @@ dependencies = [ "sqlx", "tokio", "tracing", - "tracing-gelf", "tracing-subscriber", - "twilight-model", ] [[package]] @@ -1211,12 +1347,6 @@ dependencies = [ "libc", ] -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "matchers" version = "0.1.0" @@ -1328,15 +1458,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "mio" -version = "0.8.5" +name = "miniz_oxide" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] @@ -1345,6 +1484,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nom" version = "7.1.3" @@ -1382,12 +1530,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-integer" version = "0.1.46" @@ -1428,6 +1570,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1440,15 +1591,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-multimap" version = "0.4.3" @@ -1536,9 +1678,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" @@ -1616,9 +1758,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1659,12 +1801,6 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1775,6 +1911,54 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quinn" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.5.7", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.7", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -1784,6 +1968,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -1899,6 +2093,48 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + [[package]] name = "reverse-proxy-service" version = "0.2.1" @@ -1913,6 +2149,21 @@ dependencies = [ "tower-service", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "ron" version = "0.7.1" @@ -1954,6 +2205,18 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustix" version = "0.38.34" @@ -1967,6 +2230,47 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.102.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.11" @@ -2000,16 +2304,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_derive" version = "1.0.203" @@ -2041,17 +2335,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_repr" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2451,6 +2734,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "tempfile" @@ -2502,36 +2788,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -2549,33 +2805,42 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.25.0" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", - "num_cpus", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.4.7", + "socket2 0.5.7", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.66", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", ] [[package]] @@ -2603,20 +2868,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-util" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6a3b08b64e6dfad376fa2432c7b1f01522e37a623c3050bc95db2d3ff21583" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - [[package]] name = "toml" version = "0.5.11" @@ -2725,35 +2976,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - -[[package]] -name = "tracing-gelf" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c0170f1bf67b749d4377c2da1d99d6e722600051ee53870cfb6f618611e29e" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "hostname", - "serde_json", - "thiserror", - "tokio", - "tokio-util 0.7.6", - "tracing-core", - "tracing-futures", - "tracing-subscriber", -] - [[package]] name = "tracing-log" version = "0.1.3" @@ -2765,6 +2987,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.16" @@ -2775,12 +3007,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -2789,17 +3024,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" -[[package]] -name = "twilight-model" -version = "0.15.4" -dependencies = [ - "bitflags 2.5.0", - "serde", - "serde-value", - "serde_repr", - "time", -] - [[package]] name = "typenum" version = "1.16.0" @@ -2852,13 +3076,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] -name = "url" -version = "2.3.1" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] @@ -2939,6 +3169,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.92" @@ -2978,6 +3220,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "whoami" version = "1.5.1" @@ -3040,22 +3291,37 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] -name = "windows-sys" -version = "0.42.0" +name = "windows-registry" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", ] [[package]] @@ -3082,7 +3348,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -3117,18 +3383,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -3145,9 +3411,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -3163,9 +3429,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -3181,15 +3447,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -3205,9 +3471,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -3223,9 +3489,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -3241,9 +3507,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -3259,9 +3525,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index 796fc84b..cd075886 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] members = [ "./lib/libpk", - "./services/api" + "./services/api", + "./services/dispatch" ] [workspace.dependencies] @@ -15,6 +16,7 @@ serde_json = "1.0.117" sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "chrono", "macros"] } tokio = { version = "1.25.0", features = ["full"] } tracing = "0.1.37" +tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } prost = "0.12" prost-types = "0.12" diff --git a/PluralKit.API/packages.lock.json b/PluralKit.API/packages.lock.json index 618878b9..be260d23 100644 --- a/PluralKit.API/packages.lock.json +++ b/PluralKit.API/packages.lock.json @@ -516,8 +516,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "4.1.5", - "contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==", + "resolved": "4.1.13", + "contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.6.0" } @@ -1637,7 +1637,7 @@ "Newtonsoft.Json": "[13.0.1, )", "NodaTime": "[3.0.3, )", "NodaTime.Serialization.JsonNet": "[3.0.0, )", - "Npgsql": "[4.1.5, )", + "Npgsql": "[4.1.13, )", "Npgsql.NodaTime": "[4.1.5, )", "Serilog": "[2.12.0, )", "Serilog.Extensions.Logging": "[3.0.1, )", diff --git a/PluralKit.Bot/Commands/Api.cs b/PluralKit.Bot/Commands/Api.cs index f4ac4caa..568250c4 100644 --- a/PluralKit.Bot/Commands/Api.cs +++ b/PluralKit.Bot/Commands/Api.cs @@ -143,25 +143,32 @@ public class Api if (_webhookRegex.IsMatch(newUrl)) throw new PKError("PluralKit does not currently support setting a Discord webhook URL as your system's webhook URL."); - try - { - await _dispatch.DoPostRequest(ctx.System.Id, newUrl, null, true); - } - catch (Exception e) - { - throw new PKError($"Could not verify that the new URL is working: {e.Message}"); - } - var newToken = StringUtils.GenerateToken(); + await ctx.Reply($"{Emojis.Warn} The following token is used to authenticate requests from PluralKit to you." + + " If it is exposed publicly, you **must** clear and re-set the webhook URL to get a new token." + + "\n\n**Please review the security requirements at before continuing.**" + + "\n\nWhen the server is correctly validating the token, click or reply 'yes' to continue." + ); + await ctx.PromptYesNo(newToken, "Continue", matchFlag: false); + + var status = await _dispatch.TestUrl(ctx.System.Uuid, newUrl, newToken); + if (status != "OK") + { + var message = status switch + { + "BadData" => "the webhook url is invalid", + "NoIPs" => "could not find any valid IP addresses for the provided domain", + "InvalidIP" => "could not find any valid IP addresses for the provided domain", + "FetchFailed" => "unable to reach server", + "TestFailed" => "server failed to validate the signing token", + _ => $"an unknown error occurred ({status})" + }; + throw new PKError($"Failed to validate the webhook url: {message}"); + } + await ctx.Repository.UpdateSystem(ctx.System.Id, new SystemPatch { WebhookUrl = newUrl, WebhookToken = newToken }); - await ctx.Reply($"{Emojis.Success} Successfully the new webhook URL for your system." - + $"\n\n{Emojis.Warn} The following token is used to authenticate requests from PluralKit to you." - + " If it leaks, you should clear and re-set the webhook URL to get a new token." - + "\ntodo: add link to docs or something" - ); - - await ctx.Reply(newToken); + await ctx.Reply($"{Emojis.Success} Successfully the new webhook URL for your system."); } } \ No newline at end of file diff --git a/PluralKit.Bot/packages.lock.json b/PluralKit.Bot/packages.lock.json index 078a19d1..3262ef22 100644 --- a/PluralKit.Bot/packages.lock.json +++ b/PluralKit.Bot/packages.lock.json @@ -42,9 +42,9 @@ }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.0.1, )", - "resolved": "3.0.1", - "contentHash": "o0v/J6SJwp3RFrzR29beGx0cK7xcMRgOyIuw8ZNLQyNnBhiyL/vIQKn7cfycthcWUPG3XezUjFwBWzkcUUDFbg==" + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" }, "App.Metrics": { "type": "Transitive", @@ -466,8 +466,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "4.1.5", - "contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==", + "resolved": "4.1.13", + "contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.6.0" } @@ -1556,7 +1556,7 @@ "Newtonsoft.Json": "[13.0.1, )", "NodaTime": "[3.0.3, )", "NodaTime.Serialization.JsonNet": "[3.0.0, )", - "Npgsql": "[4.1.5, )", + "Npgsql": "[4.1.13, )", "Npgsql.NodaTime": "[4.1.5, )", "Serilog": "[2.12.0, )", "Serilog.Extensions.Logging": "[3.0.1, )", diff --git a/PluralKit.Core/CoreConfig.cs b/PluralKit.Core/CoreConfig.cs index 29fa664b..cac76e3d 100644 --- a/PluralKit.Core/CoreConfig.cs +++ b/PluralKit.Core/CoreConfig.cs @@ -15,6 +15,8 @@ public class CoreConfig public string LogDir { get; set; } public string? ElasticUrl { get; set; } public string? SeqLogUrl { get; set; } + public string? DispatchProxyUrl { get; set; } + public string? DispatchProxyToken { get; set; } public LogEventLevel ConsoleLogLevel { get; set; } = LogEventLevel.Debug; public LogEventLevel ElasticLogLevel { get; set; } = LogEventLevel.Information; diff --git a/PluralKit.Core/Dispatch/DispatchModels.cs b/PluralKit.Core/Dispatch/DispatchModels.cs index 3d870a92..f6ff8d52 100644 --- a/PluralKit.Core/Dispatch/DispatchModels.cs +++ b/PluralKit.Core/Dispatch/DispatchModels.cs @@ -43,7 +43,7 @@ public struct UpdateDispatchData public static class DispatchExt { - public static StringContent GetPayloadBody(this UpdateDispatchData data) + public static string GetPayloadBody(this UpdateDispatchData data) { var o = new JObject(); @@ -53,7 +53,18 @@ public static class DispatchExt o.Add("id", data.EntityId); o.Add("data", data.EventData); - return new StringContent(JsonConvert.SerializeObject(o), Encoding.UTF8, "application/json"); + return JsonConvert.SerializeObject(o); + } + + public static string GetPingBody(string systemId, string token) + { + var o = new JObject(); + + o.Add("type", "PING"); + o.Add("signing_token", token); + o.Add("system_id", systemId); + + return JsonConvert.SerializeObject(o); } private static List _privateNetworks = new() @@ -71,6 +82,7 @@ public static class DispatchExt try { var uri = new Uri(url); + if (uri.Scheme != "https") return false; host = await Dns.GetHostEntryAsync(uri.DnsSafeHost); } catch (Exception) diff --git a/PluralKit.Core/Dispatch/DispatchService.cs b/PluralKit.Core/Dispatch/DispatchService.cs index 783587f1..f9f79169 100644 --- a/PluralKit.Core/Dispatch/DispatchService.cs +++ b/PluralKit.Core/Dispatch/DispatchService.cs @@ -1,5 +1,10 @@ using Autofac; +using System.Text; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + using Serilog; namespace PluralKit.Core; @@ -8,32 +13,55 @@ public class DispatchService { private readonly HttpClient _client = new(); private readonly ILogger _logger; + private readonly CoreConfig _cfg; private readonly ILifetimeScope _provider; public DispatchService(ILogger logger, ILifetimeScope provider, CoreConfig cfg) { _logger = logger; + _cfg = cfg; _provider = provider; } - public async Task DoPostRequest(SystemId system, string webhookUrl, HttpContent content, bool isVerify = false) + public async Task TestUrl(Guid systemUuid, string newUrl, string newToken) { - if (!await DispatchExt.ValidateUri(webhookUrl)) + if (_cfg.DispatchProxyUrl == null || _cfg.DispatchProxyToken == null) + throw new Exception("tried to dispatch without a proxy set!"); + + var o = new JObject(); + o.Add("auth", _cfg.DispatchProxyToken); + o.Add("url", newUrl); + o.Add("payload", DispatchExt.GetPingBody(systemUuid.ToString(), newToken)); + o.Add("test", DispatchExt.GetPingBody(systemUuid.ToString(), StringUtils.GenerateToken())); + + var body = new StringContent(JsonConvert.SerializeObject(o), Encoding.UTF8, "application/json"); + + var res = await _client.PostAsync(_cfg.DispatchProxyUrl, body); + return await res.Content.ReadAsStringAsync(); + } + + public async Task DoPostRequest(SystemId system, string webhookUrl, string content) + { + if (_cfg.DispatchProxyUrl == null || _cfg.DispatchProxyToken == null) { - _logger.Warning( - "Failed to dispatch webhook for system {SystemId}: URL is invalid or points to a private address", - system); + _logger.Warning("tried to dispatch without a proxy set!"); return; } + var o = new JObject(); + o.Add("auth", _cfg.DispatchProxyToken); + o.Add("url", webhookUrl); + o.Add("payload", content); + + var body = new StringContent(JsonConvert.SerializeObject(o), Encoding.UTF8, "application/json"); + try { - await _client.PostAsync(webhookUrl, content); + await _client.PostAsync(_cfg.DispatchProxyUrl, body); + // todo: do something with proxy errors } catch (HttpRequestException e) { - if (isVerify) - throw; _logger.Error(e, "Could not dispatch webhook request!"); } } diff --git a/PluralKit.Core/Utils/Emojis.cs b/PluralKit.Core/Utils/Emojis.cs index 3f8b34bb..1ce9aa3f 100644 --- a/PluralKit.Core/Utils/Emojis.cs +++ b/PluralKit.Core/Utils/Emojis.cs @@ -2,7 +2,7 @@ namespace PluralKit.Core; public static class Emojis { - public static readonly string Warn = "\u26A0"; + public static readonly string Warn = "\u26A0\uFE0F"; public static readonly string Success = "\u2705"; public static readonly string Error = "\u274C"; public static readonly string Note = "\U0001f4dd"; diff --git a/PluralKit.Core/packages.lock.json b/PluralKit.Core/packages.lock.json index 95224c57..90887586 100644 --- a/PluralKit.Core/packages.lock.json +++ b/PluralKit.Core/packages.lock.json @@ -183,9 +183,9 @@ }, "Npgsql": { "type": "Direct", - "requested": "[4.1.5, )", - "resolved": "4.1.5", - "contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==", + "requested": "[4.1.13, )", + "resolved": "4.1.13", + "contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.6.0" } diff --git a/PluralKit.Tests/packages.lock.json b/PluralKit.Tests/packages.lock.json index ea4c6ea5..03217513 100644 --- a/PluralKit.Tests/packages.lock.json +++ b/PluralKit.Tests/packages.lock.json @@ -592,8 +592,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "4.1.5", - "contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==", + "resolved": "4.1.13", + "contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.6.0" } @@ -891,8 +891,8 @@ }, "SixLabors.ImageSharp": { "type": "Transitive", - "resolved": "3.0.1", - "contentHash": "o0v/J6SJwp3RFrzR29beGx0cK7xcMRgOyIuw8ZNLQyNnBhiyL/vIQKn7cfycthcWUPG3XezUjFwBWzkcUUDFbg==" + "resolved": "3.1.5", + "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" }, "SqlKata": { "type": "Transitive", @@ -1810,7 +1810,7 @@ "Myriad": "[1.0.0, )", "PluralKit.Core": "[1.0.0, )", "Sentry": "[3.11.1, )", - "SixLabors.ImageSharp": "[3.0.1, )" + "SixLabors.ImageSharp": "[3.1.5, )" } }, "pluralkit.core": { @@ -1834,7 +1834,7 @@ "Newtonsoft.Json": "[13.0.1, )", "NodaTime": "[3.0.3, )", "NodaTime.Serialization.JsonNet": "[3.0.0, )", - "Npgsql": "[4.1.5, )", + "Npgsql": "[4.1.13, )", "Npgsql.NodaTime": "[4.1.5, )", "Serilog": "[2.12.0, )", "Serilog.Extensions.Logging": "[3.0.1, )", diff --git a/docs/content/api/dispatch.md b/docs/content/api/dispatch.md index c26a8a39..399e6af1 100644 --- a/docs/content/api/dispatch.md +++ b/docs/content/api/dispatch.md @@ -19,7 +19,7 @@ To get dispatch events from PluralKit, you must set up a *public* HTTP endpoint. For this reason, when you register a webhook URL, PluralKit generates a secret token, and then includes it with every event sent to you in the `signing_token` key. If you receive an event with an invalid `signing_token`, you **must** stop processing the request and **respond with a 401 status code**. -PluralKit will send invalid requests to your endpoint, with `PING` event type, once in a while to confirm that you are correctly validating requests. +PluralKit will send invalid requests to your endpoint, with `PING` event type, once in a while to confirm that you are correctly validating requests. If validation fails, or if requests to your endpoint are repeatedly unsuccessful, the endpoint will be removed. ## Dispatch Event Model diff --git a/lib/libpk/Cargo.toml b/lib/libpk/Cargo.toml index 83b0339e..eae2cef5 100644 --- a/lib/libpk/Cargo.toml +++ b/lib/libpk/Cargo.toml @@ -15,8 +15,7 @@ serde = { workspace = true } sqlx = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -tracing-gelf = "0.7.1" -tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +tracing-subscriber = { workspace = true} prost = { workspace = true } prost-types = { workspace = true } diff --git a/services/dispatch/Cargo.toml b/services/dispatch/Cargo.toml new file mode 100644 index 00000000..68e1bdd4 --- /dev/null +++ b/services/dispatch/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dispatch" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +hickory-client = "0.24.1" +reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls"] } diff --git a/services/dispatch/Dockerfile b/services/dispatch/Dockerfile new file mode 100644 index 00000000..04ee7849 --- /dev/null +++ b/services/dispatch/Dockerfile @@ -0,0 +1,17 @@ +FROM alpine:latest AS builder + +WORKDIR /build + +RUN apk add rustup build-base +RUN rustup-init --default-host x86_64-unknown-linux-musl --default-toolchain nightly-2024-08-20 --profile default -y + +ENV PATH=/root/.cargo/bin:$PATH +ENV RUSTFLAGS='-C link-arg=-s' + +COPY . . + +RUN cargo build --bin dispatch --release --target x86_64-unknown-linux-musl + +FROM alpine:latest +COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/dispatch /usr/local/bin/dispatch +ENTRYPOINT ["/usr/local/bin/dispatch"] diff --git a/services/dispatch/src/logger.rs b/services/dispatch/src/logger.rs new file mode 100644 index 00000000..aa65bc67 --- /dev/null +++ b/services/dispatch/src/logger.rs @@ -0,0 +1,52 @@ +use std::time::Instant; + +use axum::{extract::MatchedPath, extract::Request, middleware::Next, response::Response}; +use tracing::{info, span, warn, Instrument, Level}; + +// log any requests that take longer than 2 seconds +// todo: change as necessary +const MIN_LOG_TIME: u128 = 2_000; + +pub async fn logger(request: Request, next: Next) -> Response { + let method = request.method().clone(); + + let endpoint = request + .extensions() + .get::() + .cloned() + .map(|v| v.as_str().to_string()) + .unwrap_or("unknown".to_string()); + + let uri = request.uri().clone(); + + let request_id_span = span!( + Level::INFO, + "request", + method = method.as_str(), + endpoint = endpoint.clone(), + ); + + let start = Instant::now(); + let response = next.run(request).instrument(request_id_span).await; + let elapsed = start.elapsed().as_millis(); + + info!( + "{} handled request for {} {} in {}ms", + response.status(), + method, + uri.path(), + elapsed + ); + + if elapsed > MIN_LOG_TIME { + warn!( + "request to {} full path {} (endpoint {}) took a long time ({}ms)!", + method, + uri.path(), + endpoint, + elapsed + ) + } + + response +} diff --git a/services/dispatch/src/main.rs b/services/dispatch/src/main.rs new file mode 100644 index 00000000..3a5e182c --- /dev/null +++ b/services/dispatch/src/main.rs @@ -0,0 +1,190 @@ +#![feature(ip)] + +use hickory_client::{ + client::{AsyncClient, ClientHandle}, + rr::{DNSClass, Name, RData, RecordType}, + udp::UdpClientStream, +}; +use reqwest::{redirect::Policy, StatusCode}; +use std::{ + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + sync::Arc, + time::Duration, +}; +use tokio::{net::UdpSocket, sync::RwLock}; +use tracing::{debug, error, info}; +use tracing_subscriber::EnvFilter; + +use axum::{extract::State, http::Uri, routing::post, Json, Router}; + +mod logger; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .json() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + info!("hello world"); + + let address = std::env::var("DNS_UPSTREAM").unwrap().parse().unwrap(); + let stream = UdpClientStream::::with_timeout(address, Duration::from_secs(1)); + let (client, bg) = AsyncClient::connect(stream).await?; + tokio::spawn(bg); + + let app = Router::new() + .route("/", post(dispatch)) + .with_state(Arc::new(RwLock::new(DNSClient(client)))) + .layer(axum::middleware::from_fn(logger::logger)); + + let listener = tokio::net::TcpListener::bind("0.0.0.0:5000").await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +#[derive(Debug, serde::Deserialize)] +struct DispatchRequest { + auth: String, + url: String, + payload: String, + test: Option, +} + +#[derive(Debug)] +enum DispatchResponse { + OK, + BadData, + ResolveFailed, + NoIPs, + InvalidIP, + FetchFailed, + InvalidResponseCode(StatusCode), + TestFailed, +} + +impl std::fmt::Display for DispatchResponse { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +async fn dispatch( + // not entirely sure if this RwLock is the right way to do it + State(dns): State>>, + Json(req): Json, +) -> String { + // todo: fix + if req.auth != std::env::var("HTTP_AUTH_TOKEN").unwrap() { + return "".to_string(); + } + + let uri = match req.url.parse::() { + Ok(v) if v.scheme_str() == Some("https") && v.host().is_some() => v, + Err(error) => { + error!(?error, "failed to parse uri {}", req.url); + return DispatchResponse::BadData.to_string(); + } + _ => { + error!("uri {} is invalid", req.url); + return DispatchResponse::BadData.to_string(); + } + }; + let ips = { + let mut dns = dns.write().await; + match dns.resolve(uri.host().unwrap().to_string()).await { + Ok(v) => v, + Err(error) => { + error!(?error, "failed to resolve"); + return DispatchResponse::ResolveFailed.to_string(); + } + } + }; + if ips.iter().any(|ip| !ip.is_global()) { + return DispatchResponse::InvalidIP.to_string(); + } + + if ips.len() == 0 { + return DispatchResponse::NoIPs.to_string(); + } + + let ips: Vec = ips + .iter() + .map(|ip| SocketAddr::V4(SocketAddrV4::new(*ip, 443))) + .collect(); + + let client = reqwest::ClientBuilder::new() + .user_agent("PluralKit Dispatch (https://pluralkit.me/api/dispatch/)") + .redirect(Policy::none()) + .timeout(Duration::from_secs(10)) + .http1_only() + .use_rustls_tls() + .https_only(true) + .resolve_to_addrs(uri.host().unwrap(), &ips) + .build() + .unwrap(); + + let res = client + .post(req.url.clone()) + .header("content-type", "application/json") + .body(req.payload) + .send() + .await; + + match res { + Ok(res) if res.status() != 200 => { + return DispatchResponse::InvalidResponseCode(res.status()).to_string() + } + Err(error) => { + error!(?error, url = req.url.clone(), "failed to fetch"); + return DispatchResponse::FetchFailed.to_string(); + } + _ => {} + } + + if let Some(test) = req.test { + let test_res = client + .post(req.url.clone()) + .header("content-type", "application/json") + .body(test) + .send() + .await; + + match test_res { + Ok(res) if res.status() != 401 => return DispatchResponse::TestFailed.to_string(), + Err(error) => { + error!(?error, url = req.url.clone(), "failed to fetch"); + return DispatchResponse::FetchFailed.to_string(); + } + _ => {} + } + } + + DispatchResponse::OK.to_string() +} + +struct DNSClient(AsyncClient); + +impl DNSClient { + async fn resolve(&mut self, host: String) -> anyhow::Result> { + let resp = self + .0 + .query(Name::from_ascii(host)?, DNSClass::IN, RecordType::A) + .await?; + + debug!("got dns response: {resp:?}"); + + Ok(resp + .answers() + .iter() + .filter_map(|ans| { + if let Some(RData::A(val)) = ans.data() { + Some(val.0) + } else { + None + } + }) + .collect()) + } +}