From a02bf39f27e0c1a4fca2c92a69eb50574cdb71f7 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 17 Sep 2024 13:46:13 +0200 Subject: [PATCH 1/7] Implement initial boilerplate The general architecture is a collection of four threads. Three threads that run their dedicated Tokio runtime and handle the axum server, send outgoing notifications and run scheduled API requests respectively. The fourth thread runs the Lua VM. Channels exist between the threads to send messages and allow the Lua script to orchestrate and configure everything. --- .gitignore | 2 + Cargo.lock | 1393 +++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 8 + README.md | 4 +- src/main.rs | 88 ++- src/types.rs | 104 ++++ src/worker/api.rs | 100 +++ src/worker/lua.rs | 87 +++ src/worker/sender.rs | 71 +++ src/worker/server.rs | 76 +++ 10 files changed, 1921 insertions(+), 12 deletions(-) create mode 100644 src/types.rs create mode 100644 src/worker/api.rs create mode 100644 src/worker/lua.rs create mode 100644 src/worker/sender.rs create mode 100644 src/worker/server.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..1d00310 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +.envrc +test.lua diff --git a/Cargo.lock b/Cargo.lock index aba6586..401708f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,84 @@ dependencies = [ "memchr", ] +[[package]] +name = "async-trait" +version = "0.1.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -41,6 +119,40 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + [[package]] name = "cc" version = "1.1.16" @@ -83,6 +195,62 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388979d208a049ffdfb22fa33b9c81942215b940910bccfe258caeb25d125cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "eyre" version = "0.6.12" @@ -93,18 +261,305 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indenter" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -117,6 +572,12 @@ version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "log" version = "0.4.22" @@ -132,12 +593,24 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -147,6 +620,96 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mlua" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7" +dependencies = [ + "bstr", + "erased-serde", + "mlua-sys", + "mlua_derive", + "num-traits", + "once_cell", + "rustc-hash", + "serde", + "serde-value", +] + +[[package]] +name = "mlua-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab7a5b4756b8177a2dfa8e0bbcde63bd4000afbc4ab20cbb68d114a25470f29" +dependencies = [ + "cc", + "cfg-if", + "pkg-config", +] + +[[package]] +name = "mlua_derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09697a6cec88e7f58a02c7ab5c18c611c6907c8654613df9cc0192658a4fb859" +dependencies = [ + "itertools", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.77", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ntfy-collector" +version = "0.1.0" +dependencies = [ + "axum", + "color-eyre", + "mlua", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "tokio", + "tokio-stream", + "tracing", + "tracing-error", + "tracing-subscriber", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -157,6 +720,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.32.2" @@ -172,6 +744,59 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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 = "overload" version = "0.1.1" @@ -184,12 +809,74 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -253,13 +940,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] -name = "rust-template" -version = "0.1.0" +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ - "color-eyre", - "tracing", - "tracing-error", - "tracing-subscriber", + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[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", + "untrusted", + "windows-sys 0.52.0", ] [[package]] @@ -268,6 +1003,184 @@ 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.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +dependencies = [ + "once_cell", + "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", + "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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +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.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "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.77", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -283,12 +1196,53 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.77" @@ -300,6 +1254,55 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -310,12 +1313,128 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[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]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -329,7 +1448,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.77", ] [[package]] @@ -381,18 +1500,160 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", + "percent-encoding", +] + [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.77", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -414,3 +1675,121 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[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", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 9e2e16c..526226c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,15 @@ edition = "2021" license = "EUPL-1.2" [dependencies] +axum = "0.7.5" color-eyre = "0.6.3" +mlua = { version = "0.9.9", features = ["luajit", "macros", "serialize"] } +reqwest = { version = "0.12.7", features = ["json"] } +serde = { version = "1.0.209", features = ["derive"] } +serde_json = "1.0.128" +serde_repr = "0.1.19" +tokio = { version = "1.40.0", features = ["rt", "sync"] } +tokio-stream = "0.1.16" tracing = "0.1.40" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/README.md b/README.md index 179d43b..fbbea4b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# Rust Project Template +# Ntfy Collector -A simple boilerplate for Rust projects +A daemon to collect notifications from various places and forward them to Ntfy. diff --git a/src/main.rs b/src/main.rs index 16d9595..e0e1a4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,53 @@ +use std::fs; +use std::thread::{self, JoinHandle}; + +use color_eyre::eyre::{eyre, Context as _}; use color_eyre::Result; use tracing_error::ErrorLayer; -use tracing_subscriber::{fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter}; +use tracing_subscriber::layer::SubscriberExt as _; +use tracing_subscriber::util::SubscriberInitExt as _; +use tracing_subscriber::{fmt, EnvFilter}; +pub(crate) mod types; +mod worker { + mod api; + mod lua; + mod sender; + mod server; + + pub use api::worker as api; + pub use lua::worker as lua; + pub use sender::worker as sender; + pub use server::worker as server; +} + +fn spawn(name: &'static str, task: F) -> Result>> +where + F: FnOnce() -> Result + Send + 'static, + T: Send + 'static, +{ + thread::Builder::new() + .name(name.to_string()) + .spawn(move || { + let res = task(); + + if let Err(err) = &res { + tracing::error!("Thread '{}' errored: {:?}", name, err); + } + + res + }) + .wrap_err_with(|| format!("Failed to create thread '{}'", name)) +} + +// TODO: Don't put the entire app into a Tokio runtime. +// Instead, run single-threaded runtimes off of those threads where I need +// Tokio. +// - Axum server +// - API query scheduler +// - Ntfy sender +// TODO: Find a way to communicate between them, in a way that works for both the non-Tokio +// Lua thread and the various other threads. fn main() -> Result<()> { color_eyre::install()?; tracing_subscriber::registry() @@ -10,7 +56,43 @@ fn main() -> Result<()> { .with(ErrorLayer::new(fmt::format::Pretty::default())) .init(); - println!("Hello, world!"); + // TODO: Create a separate, fixed thread to run Lua on + // TODO: Create a channel to send events to Lua + // TODO: Create another thread that periodically checks + // service APIs. The Lua thread should be able to send + // configurations here + // TODO: Create another thread that handles sending data to + // Ntfy. Most importantly the Lua thread needs to send events here. + // Could be consolidated with the service API thread, since it's all HTTP requests. - Ok(()) + let config_path = std::env::var("CONFIG_PATH").wrap_err("Missing variable 'CONFIG_PATH'")?; + + let (ntfy_tx, ntfy_rx) = tokio::sync::mpsc::unbounded_channel(); + let (event_tx, event_rx) = std::sync::mpsc::channel(); + // A channel that lets other threads, mostly the Lua code, register + // a task with the API fetcher. + let (api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + + { + let code = fs::read_to_string(&config_path) + .wrap_err_with(|| format!("Failed to read config from '{}'", config_path))?; + + spawn("lua", move || worker::lua(code, event_rx, api_tx, ntfy_tx))?; + } + + { + let event_tx = event_tx.clone(); + spawn("api", move || worker::api(api_rx, event_tx))?; + } + + { + let event_tx = event_tx.clone(); + spawn("sender", move || worker::sender(event_tx, ntfy_rx))?; + } + + let server_worker = spawn("server", move || worker::server(event_tx))?; + server_worker + .join() + .map_err(|err| eyre!("Thread 'server' panicked: {:?}", err)) + .and_then(|res| res) } diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..71e4ff5 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,104 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize)] +pub(crate) struct WebhookEvent { + pub topic: String, + pub query: HashMap, + pub body: serde_json::Value, +} + +#[derive(Clone, Debug, Serialize)] +pub(crate) struct ApiEvent { + pub id: String, + pub body: serde_json::Value, + pub status: u16, +} + +#[derive(Clone, Debug, Serialize)] +pub(crate) struct ErrorEvent { + pub id: String, + pub message: String, +} + +#[derive(Clone, Debug)] +pub(crate) enum Event { + Webhook(WebhookEvent), + Api(ApiEvent), + Error(ErrorEvent), +} + +impl Event { + pub fn error(id: String, message: impl ToString) -> Self { + Self::Error(ErrorEvent { + id, + message: message.to_string(), + }) + } +} + +#[derive(Clone, Debug, serde_repr::Deserialize_repr, serde_repr::Serialize_repr)] +#[repr(u8)] +pub(crate) enum NtfyPriority { + Min = 1, + Low = 2, + Default = 3, + High = 4, + Max = 5, +} + +impl Default for NtfyPriority { + fn default() -> Self { + Self::Default + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct NtfyAction { + pub action: String, + pub label: String, + pub url: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct NtfyMessage { + pub topic: String, + pub message: String, + pub title: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, + #[serde(default)] + pub priority: NtfyPriority, + #[serde(default)] + pub click: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub actions: Vec, + #[serde(default)] + pub markdown: bool, + #[serde(default)] + pub icon: Option, + #[serde(default)] + pub delay: Option, +} + +#[derive(Clone, Debug)] +pub(crate) enum Message { + Ntfy(NtfyMessage), +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) enum Method { + Get, + Post, +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct ApiTask { + pub id: String, + pub interval: u64, + pub method: Method, + pub url: String, + pub body: serde_json::Value, + pub query: HashMap, +} diff --git a/src/worker/api.rs b/src/worker/api.rs new file mode 100644 index 0000000..2afdcfb --- /dev/null +++ b/src/worker/api.rs @@ -0,0 +1,100 @@ +use std::sync::mpsc::Sender; +use std::sync::Arc; +use std::time::Duration; + +use color_eyre::eyre::Context as _; +use color_eyre::Result; +use reqwest::Client; +use tokio::runtime; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::Mutex; +use tokio_stream::wrappers::IntervalStream; +use tokio_stream::{StreamExt as _, StreamMap}; + +use crate::types::{ApiEvent, ApiTask, Event, Method}; + +async fn perform_request(client: &Client, task: &ApiTask) -> Result { + let req = match task.method { + Method::Get => client.get(&task.url), + Method::Post => { + let body = serde_json::to_vec(&task.body) + .expect("Type `serde_json::Value` should always be serializable"); + client.post(&task.url).body(body) + } + }; + let res = req.query(&task.query).send().await?; + let status = res.status().as_u16(); + + let body: serde_json::Value = res.json().await?; + + let event = ApiEvent { + id: task.id.clone(), + body, + status, + }; + Ok(Event::Api(event)) +} + +#[tracing::instrument(skip_all)] +pub async fn run(mut api_rx: UnboundedReceiver, event_tx: Sender) -> Result<()> { + let tasks = Arc::new(Mutex::new(StreamMap::new())); + + let client = Client::builder() + .build() + .wrap_err("Failed to build HTTP client")?; + + tokio::spawn({ + let tasks = tasks.clone(); + async move { + while let Some(task) = api_rx.recv().await { + tracing::trace!("Received new API task: {:?}", task); + + let id = task.id.clone(); + let interval = tokio::time::interval(Duration::from_secs(task.interval)); + + let task = Arc::new(task); + let mut tasks = tasks.lock().await; + tasks.insert(id, IntervalStream::new(interval).map(move |_| task.clone())); + } + tracing::error!("API task channel closed"); + } + }); + + loop { + // We need to guarantee that the lock on `tasks` is released as soon as possible. + // To ensure that it isn't held for the entirety of the `sleep`, the indirection + // via the extra `bool` value is used, so that the lock can be dropped immediately, + // and during the `sleep` new streams can be created. + let wait = { + let mut tasks = tasks.lock().await; + match tasks.next().await { + Some((_, task)) => { + let event = perform_request(&client, &task) + .await + .wrap_err("Failed to perform API request")?; + event_tx.send(event).wrap_err("Failed to send event")?; + + false + } + None => true, + } + }; + + if wait { + // Ideally we would be able to wait explicitly for the first stream + // to be registered. But as a workaround, we have to idle wait. + tokio::time::sleep(Duration::from_millis(500)).await; + } + } +} + +#[tracing::instrument("api::worker", skip_all)] +pub fn worker(api_rx: UnboundedReceiver, event_tx: Sender) -> Result<()> { + let rt = runtime::Builder::new_current_thread() + .thread_name("api") + .enable_io() + .enable_time() + .build()?; + + rt.block_on(run(api_rx, event_tx)) +} diff --git a/src/worker/lua.rs b/src/worker/lua.rs new file mode 100644 index 0000000..9911f3e --- /dev/null +++ b/src/worker/lua.rs @@ -0,0 +1,87 @@ +use std::sync::mpsc::Receiver; + +use color_eyre::Result; +use mlua::{Function, Lua, LuaSerdeExt}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::types::{ApiTask, Event, Message}; + +#[tracing::instrument(skip_all)] +pub fn worker( + config: String, + event_rx: Receiver, + api_tx: UnboundedSender, + ntfy_tx: UnboundedSender, +) -> Result<()> { + let lua = Lua::new(); + let globals = lua.globals(); + + let config = lua.load(config).set_name("config"); + + lua.scope(|scope| { + let ntfy_fn = scope.create_function_mut(|_, data: mlua::Value| { + let data = lua.from_value(data)?; + tracing::trace!("Sending Ntfy message: {:?}", data); + + match ntfy_tx.send(Message::Ntfy(data)) { + Ok(_) => Ok((true, mlua::Value::Nil)), + Err(_) => { + let msg = lua.create_string("Failed to send message")?; + Ok((false, mlua::Value::String(msg))) + } + } + })?; + + let set_api_task_fn = scope.create_function_mut(|_, data: mlua::Value| { + let task = lua.from_value(data)?; + tracing::trace!("Sending task request: {:?}", task); + + match api_tx.send(task) { + Ok(_) => Ok((true, mlua::Value::Nil)), + Err(_) => { + let msg = lua.create_string("Failed to trigger task")?; + Ok((false, mlua::Value::String(msg))) + } + } + })?; + + globals.set("ntfy", ntfy_fn)?; + globals.set("api_task", set_api_task_fn)?; + + config.exec()?; + + let event_fn: Function = match globals.get("on_event") { + Ok(f) => f, + Err(err) => match err { + mlua::Error::FromLuaConversionError { from, to: _, message: _ } => { + let err = mlua::Error::runtime(format!("Global function 'on_event' not defined properly. Got value of type '{}'", from)); + return Err(err); + } + err => return Err(err), + }, + }; + + // Main blocking loop. As long as we can receive events, this scope will stay active. + while let Ok(event) = event_rx.recv() { + tracing::trace!("Received event: {:?}", event); + match event { + Event::Webhook(data) => { + let data = lua.to_value(&data)?; + event_fn.call::<_, ()>(("webhook", data))? + } + Event::Api(data) => { + let data = lua.to_value(&data)?; + event_fn.call::<_, ()>(("api", data))? + } + Event::Error(data) => { + let data = lua.to_value(&data)?; + event_fn.call::<_, ()>(("error", data))? + } + } + } + + Ok(()) + })?; + + Ok(()) +} diff --git a/src/worker/sender.rs b/src/worker/sender.rs new file mode 100644 index 0000000..79a18be --- /dev/null +++ b/src/worker/sender.rs @@ -0,0 +1,71 @@ +use std::sync::mpsc::Sender; + +use color_eyre::eyre::{self, Context as _}; +use color_eyre::Result; +use reqwest::{header, Client}; +use tokio::runtime; +use tokio::sync::mpsc::UnboundedReceiver; + +use crate::types::{Event, Message, NtfyMessage}; + +static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + +#[tracing::instrument] +async fn send_ntfy(client: &Client, url: &String, data: &NtfyMessage) -> Result<()> { + let body = serde_json::to_string(data)?; + tracing::trace!("JSON: {}", body); + let res = client.post(url).body(body).send().await?; + if res.status().as_u16() >= 400 { + let body = res.text().await?; + eyre::bail!("Ntfy server returned error: {}", body); + } + + Ok(()) +} + +#[tracing::instrument(skip_all)] +pub async fn run(event_tx: Sender, mut ntfy_rx: UnboundedReceiver) -> Result<()> { + let ntfy_url = std::env::var("NTFY_URL").wrap_err("Missing env var 'NTFY_URL'")?; + let ntfy_token = std::env::var("NTFY_TOKEN").wrap_err("Missing env var 'NTFY_TOKEN'")?; + + let ntfy_client = Client::builder() + .default_headers({ + let mut headers = header::HeaderMap::new(); + + let auth = format!("Bearer {}", ntfy_token); + let mut auth = header::HeaderValue::from_str(&auth)?; + auth.set_sensitive(true); + + headers.insert(header::AUTHORIZATION, auth); + + headers + }) + .user_agent(APP_USER_AGENT) + .build()?; + + while let Some(message) = ntfy_rx.recv().await { + tracing::trace!("Received notification: {:?}", message); + + match message { + Message::Ntfy(data) => { + if let Err(err) = send_ntfy(&ntfy_client, &ntfy_url, &data).await { + tracing::error!("Failed to send to Ntfy: {:?}", err); + event_tx.send(Event::error(data.topic, err))?; + } + } + } + } + + tracing::debug!("Stopped receiving messages"); + Ok(()) +} + +#[tracing::instrument(skip_all)] +pub fn worker(event_tx: Sender, ntfy_rx: UnboundedReceiver) -> Result<()> { + let rt = runtime::Builder::new_current_thread() + .thread_name("sender") + .enable_io() + .build()?; + + rt.block_on(run(event_tx, ntfy_rx)) +} diff --git a/src/worker/server.rs b/src/worker/server.rs new file mode 100644 index 0000000..ede720d --- /dev/null +++ b/src/worker/server.rs @@ -0,0 +1,76 @@ +use std::collections::HashMap; +use std::sync::mpsc::Sender; +use std::sync::Arc; + +use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::routing::get; +use axum::{Json, Router}; +use color_eyre::eyre::Context as _; +use color_eyre::Result; +use tokio::net::TcpListener; +use tokio::runtime; + +use crate::types::{Event, WebhookEvent}; + +struct AppState { + event_tx: Sender, +} + +pub fn worker(event_tx: Sender) -> Result<()> { + let rt = runtime::Builder::new_current_thread() + .thread_name("server") + .enable_io() + .build()?; + + rt.block_on(task(event_tx)) +} + +async fn task(event_tx: Sender) -> Result<()> { + let state = AppState { event_tx }; + let shared_state = Arc::new(state); + + let app = Router::new() + .route( + "/webhook/:topic", + get(webhook_handler).post(webhook_handler), + ) + .with_state(shared_state); + + let listener = TcpListener::bind("0.0.0.0:3000") + .await + .wrap_err("Failed to bind to TCP socket")?; + tracing::info!("Listening on \"0.0.0.0:3000\""); + + axum::serve(listener, app) + .await + .wrap_err("Failed to start server") +} + +async fn webhook_handler( + State(state): State>, + Path(topic): Path, + Query(query): Query>, + body: Option>, +) -> impl IntoResponse { + let event = Event::Webhook(WebhookEvent { + topic, + query, + body: body + .map(|Json(body)| body) + .unwrap_or(serde_json::Value::Null), + }); + tracing::debug!("Received webhook event: {:?}", &event); + + if state.event_tx.send(event).is_err() { + tracing::error!("Failed to trigger webhook event"); + + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to trigger webhook event", + ) + } else { + (StatusCode::NO_CONTENT, "") + } +} From d2cb39f9a24cf3b25743619084652f60f83e13ff Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 18 Sep 2024 11:31:10 +0200 Subject: [PATCH 2/7] Rework API tasks GitHub expects a 'Last-Modified' header, and honoring an 'X-Poll-Interval' header for their notifications endpoint. Other services might also have certain limitations that require customizing every API query individually. Since that's not possible if API tasks are configured once and run off of an interval, this reworks them so that the config needs to trigger every query individually. A `delay` parameter allows re-creating the same intervals that were possible before. This also moves the configuration for Ntfy to the Lua file. --- .gitignore | 2 +- Cargo.lock | 12 ----- Cargo.toml | 1 - src/main.rs | 3 ++ src/types.rs | 73 +++++++++++++++++++++++++++++- src/worker/api.rs | 105 +++++++++++++++++++++++++------------------ src/worker/lua.rs | 4 +- src/worker/sender.rs | 52 ++++++++++----------- 8 files changed, 166 insertions(+), 86 deletions(-) diff --git a/.gitignore b/.gitignore index 1d00310..c19d4d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target .envrc -test.lua +*.lua diff --git a/Cargo.lock b/Cargo.lock index 401708f..96fac98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -704,7 +704,6 @@ dependencies = [ "serde_json", "serde_repr", "tokio", - "tokio-stream", "tracing", "tracing-error", "tracing-subscriber", @@ -1376,17 +1375,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.12" diff --git a/Cargo.toml b/Cargo.toml index 526226c..2c2b303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ serde = { version = "1.0.209", features = ["derive"] } serde_json = "1.0.128" serde_repr = "0.1.19" tokio = { version = "1.40.0", features = ["rt", "sync"] } -tokio-stream = "0.1.16" tracing = "0.1.40" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/src/main.rs b/src/main.rs index e0e1a4e..3e1a403 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,9 @@ mod worker { pub use server::worker as server; } +pub(crate) static APP_USER_AGENT: &str = + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + fn spawn(name: &'static str, task: F) -> Result>> where F: FnOnce() -> Result + Send + 'static, diff --git a/src/types.rs b/src/types.rs index 71e4ff5..57de440 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,42 @@ use std::collections::HashMap; +use mlua::IntoLua; use serde::{Deserialize, Serialize}; +/// Converts a `serde_json::Value` to a `mlua::Value`. +fn json_to_lua(value: serde_json::Value, lua: &mlua::Lua) -> mlua::Result> { + match value { + serde_json::Value::Null => Ok(mlua::Value::Nil), + serde_json::Value::Bool(value) => Ok(mlua::Value::Boolean(value)), + serde_json::Value::Number(value) => match value.as_f64() { + Some(number) => Ok(mlua::Value::Number(number)), + None => Err(mlua::Error::ToLuaConversionError { + from: "serde_json::Value::Number", + to: "Number", + message: Some("Number cannot be represented by a floating-point type".into()), + }), + }, + serde_json::Value::String(value) => lua.create_string(value).map(mlua::Value::String), + serde_json::Value::Array(value) => { + let tbl = lua.create_table_with_capacity(value.len(), 0)?; + for v in value { + let v = json_to_lua(v, lua)?; + tbl.push(v)?; + } + Ok(mlua::Value::Table(tbl)) + } + serde_json::Value::Object(value) => { + let tbl = lua.create_table_with_capacity(0, value.len())?; + for (k, v) in value { + let k = lua.create_string(k)?; + let v = json_to_lua(v, lua)?; + tbl.set(k, v)?; + } + Ok(mlua::Value::Table(tbl)) + } + } +} + #[derive(Clone, Debug, Serialize)] pub(crate) struct WebhookEvent { pub topic: String, @@ -13,9 +48,32 @@ pub(crate) struct WebhookEvent { pub(crate) struct ApiEvent { pub id: String, pub body: serde_json::Value, + pub headers: HashMap>, pub status: u16, } +impl<'lua> IntoLua<'lua> for ApiEvent { + fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { + let headers = lua.create_table_with_capacity(0, self.headers.len())?; + for (k, v) in self.headers { + let k = lua.create_string(k)?; + let v = lua.create_string(v)?; + headers.set(k, v)?; + } + + let body = json_to_lua(self.body, lua)?; + let tbl = lua.create_table_with_capacity(0, 4)?; + + lua.create_string(self.id) + .and_then(|id| tbl.set("id", id))?; + tbl.set("status", self.status)?; + tbl.set("headers", headers)?; + tbl.set("body", body)?; + + Ok(mlua::Value::Table(tbl)) + } +} + #[derive(Clone, Debug, Serialize)] pub(crate) struct ErrorEvent { pub id: String, @@ -63,8 +121,15 @@ pub(crate) struct NtfyAction { #[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct NtfyMessage { + // Needs to be deserialized from Lua, but should not be sent to Ntfy + #[serde(skip_serializing)] + pub url: String, + // Needs to be deserialized from Lua, but should not be sent to Ntfy + #[serde(skip_serializing)] + pub token: String, pub topic: String, pub message: String, + #[serde(default)] pub title: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tags: Vec, @@ -89,16 +154,22 @@ pub(crate) enum Message { #[derive(Clone, Debug, Deserialize)] pub(crate) enum Method { + #[serde(alias = "GET", alias = "get")] Get, + #[serde(alias = "POST", alias = "post")] Post, } #[derive(Clone, Debug, Deserialize)] pub(crate) struct ApiTask { pub id: String, - pub interval: u64, + #[serde(default)] + pub delay: u64, pub method: Method, pub url: String, pub body: serde_json::Value, + #[serde(default)] pub query: HashMap, + #[serde(default)] + pub headers: HashMap, } diff --git a/src/worker/api.rs b/src/worker/api.rs index 2afdcfb..593b849 100644 --- a/src/worker/api.rs +++ b/src/worker/api.rs @@ -1,17 +1,25 @@ -use std::sync::mpsc::Sender; -use std::sync::Arc; +use std::ascii::AsciiExt; use std::time::Duration; +use std::{collections::HashMap, sync::mpsc::Sender}; use color_eyre::eyre::Context as _; use color_eyre::Result; -use reqwest::Client; +use reqwest::{header, Client}; use tokio::runtime; use tokio::sync::mpsc::UnboundedReceiver; -use tokio::sync::Mutex; -use tokio_stream::wrappers::IntervalStream; -use tokio_stream::{StreamExt as _, StreamMap}; use crate::types::{ApiEvent, ApiTask, Event, Method}; +use crate::APP_USER_AGENT; + +fn header_map_to_hashmap(headers: &header::HeaderMap) -> HashMap> { + let mut map = HashMap::with_capacity(headers.len()); + + for (k, v) in headers { + map.insert(k.to_string(), v.as_bytes().to_vec()); + } + + map +} async fn perform_request(client: &Client, task: &ApiTask) -> Result { let req = match task.method { @@ -22,70 +30,81 @@ async fn perform_request(client: &Client, task: &ApiTask) -> Result { client.post(&task.url).body(body) } }; - let res = req.query(&task.query).send().await?; - let status = res.status().as_u16(); - let body: serde_json::Value = res.json().await?; + let res = req + .query(&task.query) + .headers({ + let mut headers = header::HeaderMap::new(); + + for (k, v) in &task.headers { + // Non-ASCII characters aren't supported anyways for header name, so no need to map + // those. + let k = header::HeaderName::from_lowercase(k.to_ascii_lowercase().as_bytes()) + .wrap_err_with(|| format!("Invalid header name '{}'", k))?; + let v = header::HeaderValue::from_str(v) + .wrap_err_with(|| format!("Invalid header value '{}'", v))?; + + headers.insert(k, v); + } + + headers + }) + .send() + .await?; + let status = res.status(); + let headers = header_map_to_hashmap(res.headers()); + + let body: serde_json::Value = if let Ok(text) = res.text().await { + tracing::trace!("Response body: {}", text); + serde_json::from_str(&text).unwrap_or(serde_json::Value::String(text)) + } else { + tracing::trace!("Response body: NULL"); + serde_json::Value::Null + }; let event = ApiEvent { id: task.id.clone(), + headers, body, - status, + status: status.as_u16(), }; Ok(Event::Api(event)) } #[tracing::instrument(skip_all)] pub async fn run(mut api_rx: UnboundedReceiver, event_tx: Sender) -> Result<()> { - let tasks = Arc::new(Mutex::new(StreamMap::new())); + let (task_tx, mut task_rx) = tokio::sync::mpsc::channel(64); let client = Client::builder() + .user_agent(APP_USER_AGENT) .build() .wrap_err("Failed to build HTTP client")?; tokio::spawn({ - let tasks = tasks.clone(); async move { while let Some(task) = api_rx.recv().await { tracing::trace!("Received new API task: {:?}", task); - let id = task.id.clone(); - let interval = tokio::time::interval(Duration::from_secs(task.interval)); - - let task = Arc::new(task); - let mut tasks = tasks.lock().await; - tasks.insert(id, IntervalStream::new(interval).map(move |_| task.clone())); + let _ = task_tx + .send(async move { + tokio::time::sleep(Duration::from_secs(task.delay)).await; + task + }) + .await; } tracing::error!("API task channel closed"); } }); - loop { - // We need to guarantee that the lock on `tasks` is released as soon as possible. - // To ensure that it isn't held for the entirety of the `sleep`, the indirection - // via the extra `bool` value is used, so that the lock can be dropped immediately, - // and during the `sleep` new streams can be created. - let wait = { - let mut tasks = tasks.lock().await; - match tasks.next().await { - Some((_, task)) => { - let event = perform_request(&client, &task) - .await - .wrap_err("Failed to perform API request")?; - event_tx.send(event).wrap_err("Failed to send event")?; - - false - } - None => true, - } - }; - - if wait { - // Ideally we would be able to wait explicitly for the first stream - // to be registered. But as a workaround, we have to idle wait. - tokio::time::sleep(Duration::from_millis(500)).await; - } + while let Some(task) = task_rx.recv().await { + let task = task.await; + let event = perform_request(&client, &task) + .await + .wrap_err("Failed to perform API request")?; + event_tx.send(event).wrap_err("Failed to send event")?; } + + Ok(()) } #[tracing::instrument("api::worker", skip_all)] diff --git a/src/worker/lua.rs b/src/worker/lua.rs index 9911f3e..003327a 100644 --- a/src/worker/lua.rs +++ b/src/worker/lua.rs @@ -1,7 +1,7 @@ use std::sync::mpsc::Receiver; use color_eyre::Result; -use mlua::{Function, Lua, LuaSerdeExt}; +use mlua::{Function, IntoLua as _, Lua, LuaSerdeExt}; use tokio::sync::mpsc::UnboundedSender; use crate::types::{ApiTask, Event, Message}; @@ -70,7 +70,7 @@ pub fn worker( event_fn.call::<_, ()>(("webhook", data))? } Event::Api(data) => { - let data = lua.to_value(&data)?; + let data = data.into_lua(&lua)?; event_fn.call::<_, ()>(("api", data))? } Event::Error(data) => { diff --git a/src/worker/sender.rs b/src/worker/sender.rs index 79a18be..9288760 100644 --- a/src/worker/sender.rs +++ b/src/worker/sender.rs @@ -1,22 +1,38 @@ use std::sync::mpsc::Sender; -use color_eyre::eyre::{self, Context as _}; +use color_eyre::eyre; use color_eyre::Result; -use reqwest::{header, Client}; +use reqwest::header; +use reqwest::Client; use tokio::runtime; use tokio::sync::mpsc::UnboundedReceiver; use crate::types::{Event, Message, NtfyMessage}; - -static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); +use crate::APP_USER_AGENT; #[tracing::instrument] -async fn send_ntfy(client: &Client, url: &String, data: &NtfyMessage) -> Result<()> { +async fn send_ntfy(client: &Client, data: &NtfyMessage) -> Result<()> { let body = serde_json::to_string(data)?; - tracing::trace!("JSON: {}", body); - let res = client.post(url).body(body).send().await?; + + let res = client + .post(&data.url) + .headers({ + let mut headers = header::HeaderMap::new(); + + let auth = format!("Bearer {}", data.token); + let mut auth = header::HeaderValue::from_str(&auth)?; + auth.set_sensitive(true); + + headers.insert(header::AUTHORIZATION, auth); + + headers + }) + .body(body) + .send() + .await?; + if res.status().as_u16() >= 400 { - let body = res.text().await?; + let body = res.text().await.unwrap_or_default(); eyre::bail!("Ntfy server returned error: {}", body); } @@ -25,30 +41,14 @@ async fn send_ntfy(client: &Client, url: &String, data: &NtfyMessage) -> Result< #[tracing::instrument(skip_all)] pub async fn run(event_tx: Sender, mut ntfy_rx: UnboundedReceiver) -> Result<()> { - let ntfy_url = std::env::var("NTFY_URL").wrap_err("Missing env var 'NTFY_URL'")?; - let ntfy_token = std::env::var("NTFY_TOKEN").wrap_err("Missing env var 'NTFY_TOKEN'")?; - - let ntfy_client = Client::builder() - .default_headers({ - let mut headers = header::HeaderMap::new(); - - let auth = format!("Bearer {}", ntfy_token); - let mut auth = header::HeaderValue::from_str(&auth)?; - auth.set_sensitive(true); - - headers.insert(header::AUTHORIZATION, auth); - - headers - }) - .user_agent(APP_USER_AGENT) - .build()?; + let ntfy_client = Client::builder().user_agent(APP_USER_AGENT).build()?; while let Some(message) = ntfy_rx.recv().await { tracing::trace!("Received notification: {:?}", message); match message { Message::Ntfy(data) => { - if let Err(err) = send_ntfy(&ntfy_client, &ntfy_url, &data).await { + if let Err(err) = send_ntfy(&ntfy_client, &data).await { tracing::error!("Failed to send to Ntfy: {:?}", err); event_tx.send(Event::error(data.topic, err))?; } From 23d27389d33450e3d362affe4f067b8ff8ddaa25 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 19 Sep 2024 13:52:36 +0200 Subject: [PATCH 3/7] Expose logging functions to Lua --- .gitignore | 2 +- src/types.rs | 8 ++++++++ src/worker/lua.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c19d4d2..5656c7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target .envrc -*.lua +lua/ diff --git a/src/types.rs b/src/types.rs index 57de440..82002f0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -160,13 +160,21 @@ pub(crate) enum Method { Post, } +impl Default for Method { + fn default() -> Self { + Self::Get + } +} + #[derive(Clone, Debug, Deserialize)] pub(crate) struct ApiTask { pub id: String, #[serde(default)] pub delay: u64, + #[serde(default)] pub method: Method, pub url: String, + #[serde(default)] pub body: serde_json::Value, #[serde(default)] pub query: HashMap, diff --git a/src/worker/lua.rs b/src/worker/lua.rs index 003327a..bfc1e88 100644 --- a/src/worker/lua.rs +++ b/src/worker/lua.rs @@ -19,6 +19,34 @@ pub fn worker( let config = lua.load(config).set_name("config"); lua.scope(|scope| { + let log_trace_fn = scope.create_function(|_, s: String| { + tracing::trace!(name: "lua", "{}", s); + Ok(()) + })?; + let log_debug_fn = scope.create_function(|_, s: String| { + tracing::debug!(name: "lua", "{}", s); + Ok(()) + })?; + let log_info_fn = scope.create_function(|_, s: String| { + tracing::info!(name: "lua", "{}", s); + Ok(()) + })?; + let log_warn_fn = scope.create_function(|_, s: String| { + tracing::warn!(name: "lua", "{}", s); + Ok(()) + })?; + let log_error_fn = scope.create_function(|_, s: String| { + tracing::error!(name: "lua", "{}", s); + Ok(()) + })?; + + let log_tbl = lua.create_table_with_capacity(0, 5)?; + log_tbl.set("trace", log_trace_fn)?; + log_tbl.set("debug", log_debug_fn)?; + log_tbl.set("info", log_info_fn)?; + log_tbl.set("warn", log_warn_fn)?; + log_tbl.set("error", log_error_fn)?; + let ntfy_fn = scope.create_function_mut(|_, data: mlua::Value| { let data = lua.from_value(data)?; tracing::trace!("Sending Ntfy message: {:?}", data); @@ -45,6 +73,7 @@ pub fn worker( } })?; + globals.set("log", log_tbl)?; globals.set("ntfy", ntfy_fn)?; globals.set("api_task", set_api_task_fn)?; From e9796333bb52336d36357ab8195641128accc3f2 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 20 Sep 2024 10:26:41 +0200 Subject: [PATCH 4/7] Reduce logging noise --- src/main.rs | 2 +- src/worker/api.rs | 12 +++++++----- src/worker/lua.rs | 14 ++++++++------ src/worker/sender.rs | 3 +-- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3e1a403..0485687 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,7 +55,7 @@ fn main() -> Result<()> { color_eyre::install()?; tracing_subscriber::registry() .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())) - .with(fmt::layer().pretty()) + .with(fmt::layer().compact()) .with(ErrorLayer::new(fmt::format::Pretty::default())) .init(); diff --git a/src/worker/api.rs b/src/worker/api.rs index 593b849..623484a 100644 --- a/src/worker/api.rs +++ b/src/worker/api.rs @@ -1,12 +1,13 @@ -use std::ascii::AsciiExt; +use std::collections::HashMap; +use std::sync::mpsc::Sender; use std::time::Duration; -use std::{collections::HashMap, sync::mpsc::Sender}; use color_eyre::eyre::Context as _; use color_eyre::Result; use reqwest::{header, Client}; use tokio::runtime; use tokio::sync::mpsc::UnboundedReceiver; +use tracing::Instrument as _; use crate::types::{ApiEvent, ApiTask, Event, Method}; use crate::APP_USER_AGENT; @@ -21,6 +22,7 @@ fn header_map_to_hashmap(headers: &header::HeaderMap) -> HashMap map } +#[tracing::instrument] async fn perform_request(client: &Client, task: &ApiTask) -> Result { let req = match task.method { Method::Get => client.get(&task.url), @@ -55,10 +57,8 @@ async fn perform_request(client: &Client, task: &ApiTask) -> Result { let headers = header_map_to_hashmap(res.headers()); let body: serde_json::Value = if let Ok(text) = res.text().await { - tracing::trace!("Response body: {}", text); serde_json::from_str(&text).unwrap_or(serde_json::Value::String(text)) } else { - tracing::trace!("Response body: NULL"); serde_json::Value::Null }; @@ -81,9 +81,10 @@ pub async fn run(mut api_rx: UnboundedReceiver, event_tx: Sender .wrap_err("Failed to build HTTP client")?; tokio::spawn({ + let span = tracing::info_span!("api receiver"); async move { while let Some(task) = api_rx.recv().await { - tracing::trace!("Received new API task: {:?}", task); + tracing::trace!(id = task.id, "Received new API task"); let _ = task_tx .send(async move { @@ -94,6 +95,7 @@ pub async fn run(mut api_rx: UnboundedReceiver, event_tx: Sender } tracing::error!("API task channel closed"); } + .instrument(span) }); while let Some(task) = task_rx.recv().await { diff --git a/src/worker/lua.rs b/src/worker/lua.rs index bfc1e88..6ef1ad7 100644 --- a/src/worker/lua.rs +++ b/src/worker/lua.rs @@ -4,7 +4,7 @@ use color_eyre::Result; use mlua::{Function, IntoLua as _, Lua, LuaSerdeExt}; use tokio::sync::mpsc::UnboundedSender; -use crate::types::{ApiTask, Event, Message}; +use crate::types::{ApiTask, Event, Message, NtfyMessage}; #[tracing::instrument(skip_all)] pub fn worker( @@ -48,8 +48,8 @@ pub fn worker( log_tbl.set("error", log_error_fn)?; let ntfy_fn = scope.create_function_mut(|_, data: mlua::Value| { - let data = lua.from_value(data)?; - tracing::trace!("Sending Ntfy message: {:?}", data); + let data: NtfyMessage = lua.from_value(data)?; + tracing::trace!(topic = data.topic, title = data.title, msg = data.message,"Sending Ntfy message"); match ntfy_tx.send(Message::Ntfy(data)) { Ok(_) => Ok((true, mlua::Value::Nil)), @@ -61,8 +61,8 @@ pub fn worker( })?; let set_api_task_fn = scope.create_function_mut(|_, data: mlua::Value| { - let task = lua.from_value(data)?; - tracing::trace!("Sending task request: {:?}", task); + let task: ApiTask = lua.from_value(data)?; + tracing::trace!("Sending task request: id = {}, delay = {}", task.id, task.delay); match api_tx.send(task) { Ok(_) => Ok((true, mlua::Value::Nil)), @@ -92,17 +92,19 @@ pub fn worker( // Main blocking loop. As long as we can receive events, this scope will stay active. while let Ok(event) = event_rx.recv() { - tracing::trace!("Received event: {:?}", event); match event { Event::Webhook(data) => { + tracing::trace!(id = data.topic, "Received webhook event"); let data = lua.to_value(&data)?; event_fn.call::<_, ()>(("webhook", data))? } Event::Api(data) => { + tracing::trace!(id = data.id, status = data.status, "Received api event"); let data = data.into_lua(&lua)?; event_fn.call::<_, ()>(("api", data))? } Event::Error(data) => { + tracing::trace!(id = data.id, message = data.message, "Received error event"); let data = lua.to_value(&data)?; event_fn.call::<_, ()>(("error", data))? } diff --git a/src/worker/sender.rs b/src/worker/sender.rs index 9288760..891ff49 100644 --- a/src/worker/sender.rs +++ b/src/worker/sender.rs @@ -44,10 +44,9 @@ pub async fn run(event_tx: Sender, mut ntfy_rx: UnboundedReceiver { + tracing::trace!(topic = data.topic, "Sending Ntfy notification"); if let Err(err) = send_ntfy(&ntfy_client, &data).await { tracing::error!("Failed to send to Ntfy: {:?}", err); event_tx.send(Event::error(data.topic, err))?; From f5c64b788f2cd34febdacbf63f151e3fa4b8e6aa Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 20 Sep 2024 10:28:08 +0200 Subject: [PATCH 5/7] Use Rustls for TLS Slim Docker images like Alpine or Distroless don't ship OpenSSL by default, and rather than installing that, Rustls can be linked statically. --- Cargo.lock | 456 ++++++++++++++++++++++------------------------------- Cargo.toml | 4 +- 2 files changed, 190 insertions(+), 270 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96fac98..9f2d01c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,12 +37,6 @@ dependencies = [ "syn 2.0.77", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.3.0" @@ -147,6 +141,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.7.1" @@ -195,43 +195,12 @@ dependencies = [ "tracing-error", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -[[package]] -name = "encoding_rs" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - [[package]] name = "erased-serde" version = "0.4.3" @@ -261,33 +230,12 @@ dependencies = [ "once_cell", ] -[[package]] -name = "fastrand" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" - [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -312,12 +260,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - [[package]] name = "futures-task" version = "0.3.30" @@ -353,37 +295,21 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -[[package]] -name = "h2" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "1.1.0" @@ -439,7 +365,6 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", "http", "http-body", "httparse", @@ -466,22 +391,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", + "webpki-roots", ] [[package]] @@ -520,16 +430,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" -[[package]] -name = "indexmap" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "ipnet" version = "2.10.0" @@ -584,6 +484,25 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lua-src" +version = "547.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.5.10+f725e44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a0fa0df28e21f785c48d9c0f0be355cf40658badb667284207dbb4d1e574a9" +dependencies = [ + "cc", + "which", +] + [[package]] name = "matchers" version = "0.1.0" @@ -657,6 +576,8 @@ checksum = "3ab7a5b4756b8177a2dfa8e0bbcde63bd4000afbc4ab20cbb68d114a25470f29" dependencies = [ "cc", "cfg-if", + "lua-src", + "luajit-src", "pkg-config", ] @@ -675,23 +596,6 @@ dependencies = [ "syn 2.0.77", ] -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ntfy-collector" version = "0.1.0" @@ -743,50 +647,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "openssl" -version = "0.10.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "ordered-float" version = "2.10.1" @@ -852,6 +712,15 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -885,6 +754,54 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -894,6 +811,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.10.6" @@ -946,38 +893,37 @@ checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-core", "futures-util", - "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", - "system-configuration", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "windows-registry", ] @@ -1028,6 +974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1073,38 +1020,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "schannel" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "serde" version = "1.0.209" @@ -1269,37 +1184,23 @@ dependencies = [ ] [[package]] -name = "system-configuration" -version = "0.6.1" +name = "thiserror" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", + "thiserror-impl", ] [[package]] -name = "system-configuration-sys" -version = "0.6.0" +name = "thiserror-impl" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" -dependencies = [ - "cfg-if", - "fastrand", - "once_cell", - "rustix", - "windows-sys 0.59.0", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -1354,16 +1255,6 @@ dependencies = [ "syn 2.0.77", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.0" @@ -1375,19 +1266,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-util" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - [[package]] name = "tower" version = "0.4.13" @@ -1538,12 +1416,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -1642,6 +1514,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1776,6 +1669,33 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "zeroize" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index 2c2b303..2610947 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,8 @@ license = "EUPL-1.2" [dependencies] axum = "0.7.5" color-eyre = "0.6.3" -mlua = { version = "0.9.9", features = ["luajit", "macros", "serialize"] } -reqwest = { version = "0.12.7", features = ["json"] } +mlua = { version = "0.9.9", features = ["luajit", "macros", "serialize", "vendored"] } +reqwest = { version = "0.12.7", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0.209", features = ["derive"] } serde_json = "1.0.128" serde_repr = "0.1.19" From c105ac80cd33b550f560c7384344655af7cbbb82 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 20 Sep 2024 10:31:19 +0200 Subject: [PATCH 6/7] Add Dockerfile --- .dockerignore | 1 + Cargo.toml | 4 ++++ Dockerfile | 27 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.toml b/Cargo.toml index 2610947..9d7f73e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,7 @@ tokio = { version = "1.40.0", features = ["rt", "sync"] } tracing = "0.1.40" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } + +[profile.release] +strip = true +lto = true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..214f567 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM rust:1.81.0-slim AS build + +WORKDIR /src + +RUN set -ex; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + make \ + libluajit-5.1-dev \ + ; \ + rm -rf /var/lib/apt/lists/*; + +COPY . /src +RUN --mount=type=cache,id=cargo-registry,target=/cargo/registry \ + --mount=type=cache,id=cargo-target,target=/src/target \ + cargo build --release --locked && cp /src/target/release/ntfy-collector /src/; + +FROM gcr.io/distroless/cc-debian12 AS final + +WORKDIR /ntfy-collector +ENV CONFIG_PATH=/ntfy-collector/lua/config.lua +ENV LUA_PATH=/ntfy-collector/lua/?.lua;/ntfy-collector/lua/?/init.lua + +COPY --from=build /src/ntfy-collector /usr/bin/ntfy-collector +COPY ./lua ./lua + +CMD ["/usr/bin/ntfy-collector"] From 18efbb095780b95922145b3fc626a0a1a7c6e035 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 24 Sep 2024 11:48:15 +0200 Subject: [PATCH 7/7] Catch failing worker threads Previously if one of the threads other than the server failed, it would log the error but continue to run in a broken state. So instead this makes sure than if any thread finishes the whole application stops. Since all worker threads should always wait indefinitely for more work, this should only happen if one of them bails on an error. --- src/main.rs | 91 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0485687..095627e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use std::fs; +use std::sync::mpsc::Sender; use std::thread::{self, JoinHandle}; -use color_eyre::eyre::{eyre, Context as _}; +use color_eyre::eyre::{bail, eyre, Context as _}; use color_eyre::Result; use tracing_error::ErrorLayer; use tracing_subscriber::layer::SubscriberExt as _; @@ -24,7 +25,11 @@ mod worker { pub(crate) static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); -fn spawn(name: &'static str, task: F) -> Result>> +fn spawn( + name: &'static str, + close_tx: Sender<&'static str>, + task: F, +) -> Result>> where F: FnOnce() -> Result + Send + 'static, T: Send + 'static, @@ -38,19 +43,13 @@ where tracing::error!("Thread '{}' errored: {:?}", name, err); } + let _ = close_tx.send(name); + res }) .wrap_err_with(|| format!("Failed to create thread '{}'", name)) } -// TODO: Don't put the entire app into a Tokio runtime. -// Instead, run single-threaded runtimes off of those threads where I need -// Tokio. -// - Axum server -// - API query scheduler -// - Ntfy sender -// TODO: Find a way to communicate between them, in a way that works for both the non-Tokio -// Lua thread and the various other threads. fn main() -> Result<()> { color_eyre::install()?; tracing_subscriber::registry() @@ -59,43 +58,67 @@ fn main() -> Result<()> { .with(ErrorLayer::new(fmt::format::Pretty::default())) .init(); - // TODO: Create a separate, fixed thread to run Lua on - // TODO: Create a channel to send events to Lua - // TODO: Create another thread that periodically checks - // service APIs. The Lua thread should be able to send - // configurations here - // TODO: Create another thread that handles sending data to - // Ntfy. Most importantly the Lua thread needs to send events here. - // Could be consolidated with the service API thread, since it's all HTTP requests. - let config_path = std::env::var("CONFIG_PATH").wrap_err("Missing variable 'CONFIG_PATH'")?; - let (ntfy_tx, ntfy_rx) = tokio::sync::mpsc::unbounded_channel(); + // A channel send to each thread to signal that any of them finished. + // Since all workers are supposed to be running indefinitely, waiting for + // events from the outside, they can only stop because of an error. + // But to be able + let (close_tx, close_rx) = std::sync::mpsc::channel(); + + let (sender_tx, sender_rx) = tokio::sync::mpsc::unbounded_channel(); let (event_tx, event_rx) = std::sync::mpsc::channel(); // A channel that lets other threads, mostly the Lua code, register // a task with the API fetcher. let (api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); - { + let lua_thread = { let code = fs::read_to_string(&config_path) .wrap_err_with(|| format!("Failed to read config from '{}'", config_path))?; - spawn("lua", move || worker::lua(code, event_rx, api_tx, ntfy_tx))?; - } + spawn("lua", close_tx.clone(), move || { + worker::lua(code, event_rx, api_tx, sender_tx) + })? + }; - { + let api_thread = { let event_tx = event_tx.clone(); - spawn("api", move || worker::api(api_rx, event_tx))?; - } + spawn("api", close_tx.clone(), move || { + worker::api(api_rx, event_tx) + })? + }; - { + let sender_thread = { let event_tx = event_tx.clone(); - spawn("sender", move || worker::sender(event_tx, ntfy_rx))?; - } + spawn("sender", close_tx.clone(), move || { + worker::sender(event_tx, sender_rx) + })? + }; - let server_worker = spawn("server", move || worker::server(event_tx))?; - server_worker - .join() - .map_err(|err| eyre!("Thread 'server' panicked: {:?}", err)) - .and_then(|res| res) + let server_thread = spawn("server", close_tx, move || worker::server(event_tx))?; + + match close_rx.recv() { + Ok(name) => match name { + "api" => api_thread + .join() + .map_err(|err| eyre!("Thread 'api' panicked: {:?}", err)) + .and_then(|res| res), + "lua" => lua_thread + .join() + .map_err(|err| eyre!("Thread 'lua' panicked: {:?}", err)) + .and_then(|res| res), + "sender" => sender_thread + .join() + .map_err(|err| eyre!("Thread 'sender' panicked: {:?}", err)) + .and_then(|res| res), + "server" => server_thread + .join() + .map_err(|err| eyre!("Thread 'server' panicked: {:?}", err)) + .and_then(|res| res), + _ => bail!("Unknown thread '{}'", name), + }, + Err(_) => unreachable!( + "Any thread given this channel will send a closing notification before dropping it" + ), + } }