diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..63b4a96 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1763 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "api_key" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e5f39ee86cb09ff3715e83cb45547afff203bbd9dd77acb02453d958cf90d62" +dependencies = [ + "base-x", + "base32", + "rand", + "regex", + "uuid", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[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 = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 0.2.12", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nexus" +version = "0.1.0" +dependencies = [ + "api_key", + "chrono", + "env_logger", + "futures", + "log", + "os_path", + "regex", + "serde", + "serde_json", + "serde_jsonc", + "simple-error", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "url", + "uuid", + "warp", +] + +[[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.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" + +[[package]] +name = "os_path" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360a6ecb129f544ba5ae18776ca8779cf3cf979c8133e9eefe9464ea74741f6b" +dependencies = [ + "regex", + "serde", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +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 0.2.15", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_jsonc" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a58154381df481a41b7536101c0daccdaf2426f244334074c4c77b89b6253a7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simple-error" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2accd2c41a0e920d2abd91b2badcfa1da784662f54fbc47e0e3a51f1e2e1cf" + +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +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 = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +dependencies = [ + "getrandom 0.3.1", +] + +[[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 = "warp" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http 0.2.12", + "hyper", + "log", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + +[[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 = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..80fac23 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "nexus" +version = "0.1.0" +edition = "2024" + +[dependencies] +api_key = "0.1.0" +chrono = "0.4.40" +env_logger = "0.11.7" +futures = "0.3.31" +log = "0.4.26" +os_path = "0.8.0" +regex = "1.11.1" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +serde_jsonc = "1.0.108" +simple-error = "0.3.1" +thiserror = "2.0.12" +tokio = { version = "1.44.0", features = ["macros", "rt-multi-thread", "signal"] } +tokio-util = "0.7.13" +url = "2.5.4" +uuid = { version = "1.15.1", features = ["v4"] } +warp = "0.3.7" diff --git a/src/arbiter/mod.rs b/src/arbiter/mod.rs new file mode 100644 index 0000000..fad0352 --- /dev/null +++ b/src/arbiter/mod.rs @@ -0,0 +1,89 @@ +pub mod models; + +use std::sync::Arc; +use log::{debug, info}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use tokio_util::sync::CancellationToken; +use url::Url; +use crate::utils::send_ipc_message; + +use crate::server::models::{IPCMessageWithId, CoreUserConfig, Store}; + +pub async fn arbiter_main( + ipc_tx: UnboundedSender, + mut ipc_rx: UnboundedReceiver, + store: Arc, + user_config: Arc, + cancellation_tokens: (CancellationToken, CancellationToken) +) { + info!("Starting Arbiter..."); + + let init_store = Arc::new(store.clone()); + let init_user = Arc::new(user_config.clone()); + let (init_from_tx, mut init_from_rx) = unbounded_channel::(); + cancellation_tokens.0.run_until_cancelled(async move { + let _ = send_ipc_message( + &init_store, + &init_user, + Arc::new(init_from_tx), + "clover://arbiter.clover.reboot-codes.com/status".to_string(), + "finished-init".to_string() + ).await; + }).await; + + let ipc_recv_token = cancellation_tokens.0.clone(); + let ipc_recv_handle = tokio::task::spawn(async move { + tokio::select! { + _ = ipc_recv_token.cancelled() => { + debug!("ipc_recv exited"); + }, + _ = async move { + while let Some(msg) = ipc_rx.recv().await { + let kind = Url::parse(&msg.kind.clone()).unwrap(); + + // Verify that we care about this event. + if kind.host().unwrap() == url::Host::Domain("arbiter.clover.reboot-codes.com") { + debug!("Processing: {}", msg.kind.clone()); + } + } + } => {} + } + }); + + let ipc_trans_token = cancellation_tokens.0.clone(); + let ipc_trans_tx = Arc::new(ipc_tx.clone()); + let ipc_trans_handle = tokio::task::spawn(async move { + tokio::select! { + _ = async move { + while let Some(msg) = init_from_rx.recv().await { + match ipc_trans_tx.send(msg) { + Ok(_) => {}, + Err(_) => { + debug!("Failed to send message to IPC bus!"); + } + } + } + } => {}, + _ = ipc_trans_token.cancelled() => { + debug!("ipc_trans exited"); + } + } + }); + + let cleanup_token = cancellation_tokens.0.clone(); + tokio::select! { + _ = cleanup_token.cancelled() => { + ipc_recv_handle.abort(); + ipc_trans_handle.abort(); + + info!("Cleaning up users..."); + // TODO: Clean up registered users when server is shutting down. + + std::mem::drop(store); + + cancellation_tokens.1.cancel(); + } + } + + info!("Arbiter has stopped!"); +} diff --git a/src/arbiter/models.rs b/src/arbiter/models.rs new file mode 100644 index 0000000..aedd5ab --- /dev/null +++ b/src/arbiter/models.rs @@ -0,0 +1,106 @@ +use std::{collections::HashMap, sync::Arc}; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; +use crate::server::models::Session; + +// TODO: Define defaults via `Default` trait impl. + +#[derive(Debug, Clone)] +pub struct User { + /// A vector of API keys associated with this user. + pub api_keys: Vec, + pub sessions: Arc>>, + pub user_type: String, + pub pretty_name: String +} + +impl User { + pub fn to_user_with_id(self, id: String) -> UserWithId { + UserWithId { + id, + api_keys: self.api_keys, + sessions: self.sessions, + user_type: self.user_type, + pretty_name: self.pretty_name + } + } +} + +#[derive(Debug, Clone)] +pub struct UserWithId { + pub id: String, + pub api_keys: Vec, + pub sessions: Arc>>, + pub user_type: String, + pub pretty_name: String +} + +impl Into for UserWithId { + fn into(self) -> User { + User { + api_keys: self.api_keys, + sessions: self.sessions, + user_type: self.user_type, + pretty_name: self.pretty_name + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ApiKey { + pub allowed_events_to: Vec, + pub allowed_events_from: Vec, + pub user_id: String, + pub echo: bool, +} + +impl ApiKey { + pub fn to_api_key_with_key(self, key: String) -> ApiKeyWithKey { + ApiKeyWithKey { + key, + allowed_events_to: self.allowed_events_to, + allowed_events_from: self.allowed_events_from, + user_id: self.user_id, + echo: self.echo, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ApiKeyWithKey { + pub key: String, + pub allowed_events_to: Vec, + pub allowed_events_from: Vec, + pub user_id: String, + pub echo: bool, +} + +impl Into for ApiKeyWithKey { + fn into(self) -> ApiKey { + ApiKey { + allowed_events_to: self.allowed_events_to, + allowed_events_from: self.allowed_events_from, + user_id: self.user_id, + echo: self.echo, + } + } +} + +impl Into for ApiKeyWithKey { + fn into(self) -> ApiKeyWithKeyWithoutUID { + ApiKeyWithKeyWithoutUID { + key: self.key, + allowed_events_to: self.allowed_events_to, + allowed_events_from: self.allowed_events_from, + echo: self.echo, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ApiKeyWithKeyWithoutUID { + pub key: String, + pub allowed_events_to: Vec, + pub allowed_events_from: Vec, + pub echo: bool, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..947e24d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,18 @@ +pub mod server; +pub mod arbiter; +mod utils; + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2252e54 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,6 @@ +use nexus::add; + +fn main() { + // TODO: Wrap embedable server in this binary so it can be used outside of any modules that use this library. + println!("Hallo welt! 2 + 2 = {}", add(2, 2)); +} diff --git a/src/server/listener.rs b/src/server/listener.rs new file mode 100644 index 0000000..a4526a6 --- /dev/null +++ b/src/server/listener.rs @@ -0,0 +1,511 @@ +use log::{debug, error, info, warn}; +use regex::Regex; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; +use url::Url; +use std::collections::HashMap; +use std::convert::Infallible; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use std::sync::Arc; +use std::time::SystemTime; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use warp::{Filter, http::StatusCode}; +use crate::arbiter::models::{ApiKeyWithKey, UserWithId}; +use crate::utils::{gen_cid_with_check, gen_ipc_message, iso8601}; +use crate::server::models::{Client, ClientWithId, CoreUserConfig, IPCMessageWithId, Session, Store}; +use crate::server::websockets::handle_ws_client; + +// example error response +#[derive(Serialize, Debug)] +struct ApiErrorResult { + detail: String, +} + +// errors thrown by handlers and custom filters, +// such as `ensure_authentication` filter +#[derive(Error, Debug)] +enum ApiErrors { + #[error("user not authorized")] + NotAuthorized(String), +} + +// ensure that warp`s Reject recognizes `ApiErrors` +impl warp::reject::Reject for ApiErrors {} + +// generic errors handler for all api errors +// ensures unified error structure +async fn handle_rejection(err: warp::reject::Rejection) -> std::result::Result { + let code; + let message; + + if err.is_not_found() { + code = StatusCode::NOT_FOUND; + message = "Not found"; + } else if let Some(_) = err.find::() { + code = StatusCode::BAD_REQUEST; + message = "Invalid Body"; + } else if let Some(e) = err.find::() { + match e { + ApiErrors::NotAuthorized(_error_message) => { + code = StatusCode::UNAUTHORIZED; + message = "Action not authorized"; + } + } + } else if let Some(_) = err.find::() { + code = StatusCode::METHOD_NOT_ALLOWED; + message = "Method not allowed"; + } else { + // We should have expected this... Just log and say its a 500 + error!("unhandled rejection: {:?}", err); + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "Internal server error"; + } + + let json = warp::reply::json(&ApiErrorResult { detail: message.into() }); + + Ok(warp::reply::with_status(json, code)) +} + +// middleware that looks for authorization header and validates it +async fn ensure_authentication(path: String, store: Arc>, auth_header: Option) -> Result<(UserWithId, ApiKeyWithKey, ClientWithId, Session), warp::reject::Rejection> { + let client_id = gen_cid_with_check(&store).await; + let mut client = ClientWithId { api_key: "".to_string(), user_id: "".to_string(), id: client_id.clone(), active: true }; + store.clients.lock().await.insert(client_id.clone(), client.clone().into()); + + info!("Client: {}, hit secure path: {}, attempting authentication...", client.id.clone(), path.clone()); + + match auth_header { + Some(header) => { + debug!("got auth header, verifying: {}", header); + let parts: Vec<&str> = header.split(" ").collect(); + let mut authenticated = false; + let mut api_key_str = "".to_string(); + + if parts.len() == 2 && parts[0] == "Token" { + api_key_str = parts[1].to_string(); + debug!("parsed key: {}", api_key_str.clone()); + for registered_api_key in store.clone().api_keys.lock().await.clone().into_iter() { + debug!("testing against: {}", registered_api_key.0.clone()); + if api_key_str == registered_api_key.0 { + authenticated = true; + break; + } + } + } + + if authenticated { + debug!("Running through client registration for api_key: {}", api_key_str.clone()); + let api_keys = store.clone().api_keys.clone(); + let api_keys_locked = api_keys.lock().await; + let api_key = api_keys_locked.get(&api_key_str.clone()).unwrap().clone().to_api_key_with_key(api_key_str.clone()); + + let user_id = api_key.clone().user_id; + debug!("Registering as client: {}", client_id.clone()); + client = ClientWithId { api_key: api_key_str.clone(), user_id: user_id.clone(), id: client_id.clone(), active: true }; + store.clients.lock().await.insert(client_id.clone(), client.clone().into()); + + let user = store.users.clone().lock().await.get(&user_id.clone()).unwrap().clone().to_user_with_id(user_id.clone()); + let session = Session { start_time: iso8601(&SystemTime::now()), end_time: "".to_string(), api_key: api_key.key.clone() }; + user.sessions.lock().await.insert(client_id.clone(), session.clone()); + + debug!("Registered: {}!", client_id.clone()); + + info!("Client: {}, authenticated as user: {}!", client_id.clone(), api_key.clone().user_id); + return Ok((user.clone(), api_key.clone(), client.clone(), session.clone())); + } else { + warn!("Client: {}, attempted to connect with an invalid api key, disconnecting...", client.id.clone()); + client = ClientWithId { api_key: client.api_key, user_id: client.user_id, id: client.id, active: false }; + store.clients.lock().await.insert(client_id.clone(), client.clone().into()); + return Err(warp::reject::custom(ApiErrors::NotAuthorized( + "api key not registered".to_string(), + ))); + } + }, + None => { + warn!("Client: {}, attempted to connect without an api key, disconnecting...", client.id.clone()); + client = ClientWithId { api_key: client.api_key, user_id: client.user_id, id: client.id, active: false }; + store.clients.lock().await.insert(client_id.clone(), client.clone().into()); + Err(warp::reject::custom(ApiErrors::NotAuthorized( + "no authorization header".to_string(), + ))) + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ServerHealth { + up_since: String +} + +async fn handle_ipc_send(sender: Arc>, msg: IPCMessageWithId, user_config: &Arc, store: &Store) { + let users_mutex = &store.users.to_owned(); + let users = users_mutex.lock().await; + let user_conf = users.get(&user_config.id.clone()).expect(&format!("ERROR: Core user not found: {}", user_config.id.clone())); + let keys_mutex = &store.api_keys.to_owned(); + let keys = keys_mutex.lock().await; + + let api_key_conf = keys.get(&user_config.api_key.clone()).expect(&format!("ERROR: Core user api_key not found: {}", user_config.api_key.clone())); + let mut event_sent = false; + + for allowed_event_regex in api_key_conf.allowed_events_from.clone() { + match Regex::new(&allowed_event_regex.clone()) { + Ok(regex) => { + if regex.is_match(&msg.kind.clone()) { + match sender.send(msg.clone()) { + Ok(_) => { + event_sent = true; + }, + Err(e) => { + error!("Core user: {}, IPC channel: {}, Failed to send message: {{ \"author\": \"{}\", \"kind\": \"{}\", \"message\": \"{}\" }}, due to:\n{}", user_config.id.clone(), user_conf.user_type.clone(), msg.author.clone(), msg.kind.clone(), msg.message.clone(), e); + } + }; + } + }, + Err(e) => { + error!("Core user: {}, api key's \"allowed events from\", regex: {}, is invalid! Regex Error: {}", user_config.id.clone(), allowed_event_regex.clone(), e); + } + } + } + + if !event_sent { + debug!("Core user: {}, event \"{}\" not sent.", user_config.id, msg.kind.clone()); + } +} + +pub async fn nexus_listener( + port: u16, + store: Arc, + mut arbiter_ipc: (&CoreUserConfig, UnboundedReceiver, UnboundedSender), + mut renderer_ipc: (&CoreUserConfig, UnboundedReceiver, UnboundedSender), + mut modman_ipc: (&CoreUserConfig, UnboundedReceiver, UnboundedSender), + mut inference_engine_ipc: (&CoreUserConfig, UnboundedReceiver, UnboundedSender), + mut appd_ipc: (&CoreUserConfig, UnboundedReceiver, UnboundedSender), + mut warehouse_ipc: (&CoreUserConfig, UnboundedReceiver, UnboundedSender), + cancellation_tokens: (CancellationToken, CancellationToken), + nexus_user_config: Arc +) { + info!("Starting nexus on port: {}...", port); + let clients_tx: Arc>>> = Arc::new(Mutex::new(HashMap::new())); + + let arbiter_cfg = arbiter_ipc.0; + let arbiter_tx = arbiter_ipc.2; + let renderer_cfg = renderer_ipc.0; + let renderer_tx = renderer_ipc.2; + let modman_cfg = modman_ipc.0; + let modman_tx = modman_ipc.2; + let inference_engine_cfg = inference_engine_ipc.0; + let inference_engine_tx = inference_engine_ipc.2; + let appd_cfg = appd_ipc.0; + let appd_tx = appd_ipc.2; + let warehouse_cfg = warehouse_ipc.0; + let warehouse_tx = warehouse_ipc.2; + for client in vec![ + (arbiter_cfg, arbiter_tx), + (renderer_cfg, renderer_tx), + (modman_cfg, modman_tx), + (inference_engine_cfg, inference_engine_tx), + (appd_cfg, appd_tx), + (warehouse_cfg, warehouse_tx) + ] { + let client_obj = Client { + api_key: client.0.api_key.clone(), + user_id: client.0.id.clone(), + active: true, + }; + let cid = gen_cid_with_check(&store).await; + + debug!("Registering internal client \"{}\":\n{:#?}", cid.clone(), client_obj.clone()); + + store.clients.lock().await.insert(cid.clone(), client_obj.clone()); + clients_tx.lock().await.insert(cid.clone(), client.1); + } + + let (from_client_tx, mut from_client_rx) = mpsc::unbounded_channel::(); + + let filter_to_clients_tx = Arc::new(clients_tx.clone()); + let to_clients_tx_filter = warp::any().map(move || filter_to_clients_tx.clone()); + + let filter_from_clients_tx = Arc::new(from_client_tx.clone()); + let from_clients_tx_filter = warp::any().map(move || filter_from_clients_tx.clone()); + + let filter_store = Arc::new(store.clone()); + let store_filter = warp::any().map(move || filter_store.clone()); + + let start_up_time = iso8601(&SystemTime::now()); + let health_check_path = warp::path("health-check") + .map(move || { + let current_health = ServerHealth{up_since: start_up_time.clone()}; + warp::reply::json(¤t_health) + }); + + let ws_path = warp::path("ws") + .and(warp::any().map(|| "/ws".to_string())) + .and(store_filter.clone()) + .and(warp::header::optional::("Authorization")) + .and_then(ensure_authentication) + .and(warp::ws()) + .and(store_filter.clone()) + .and(to_clients_tx_filter.clone()) + .and(from_clients_tx_filter.clone()) + .and_then(handle_ws_client); + + let routes = health_check_path + .or(ws_path) + .with(warp::cors().allow_any_origin()) + .recover(handle_rejection); + + // TODO: Add control REST API for start up and shut down. + + // TODO: Start creating GQL API endpoint. + + let server_port = Arc::new(port.clone()); + let http_token = cancellation_tokens.0.clone(); + let http_handle = tokio::task::spawn(async move { + let (_, server) = warp::serve(routes) + // TODO: Add option for listening address. + .try_bind_with_graceful_shutdown(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), *server_port), async move { + tokio::select! { + _ = http_token.cancelled() => {} + } + }).expect(""); + server.await; + info!("Server stopped."); + }); + + let ipc_dispatch_store = Arc::new(store.clone()); + let ipc_dispatch_clients_tx = Arc::new(clients_tx.clone()); + let ipc_dispatch_user_config = Arc::new(nexus_user_config.clone()); + let ipc_dispatch_token = cancellation_tokens.0.clone(); + let ipc_dispatch_handle = tokio::task::spawn(async move { + tokio::select! { + _ = ipc_dispatch_token.cancelled() => { + debug!("ipc_dispatch exited"); + }, + _ = async move { + while let Some(message) = from_client_rx.recv().await { + debug!("Got message type: {}, with data:\n {}", message.kind.clone(), message.message.clone()); + match Url::parse(&message.kind.clone()) { + Ok (_kind) => { + for client in ipc_dispatch_store.clients.lock().await.clone().into_iter() { + let client_id = Arc::new(client.0); + let mutex = &ipc_dispatch_clients_tx.to_owned(); + let client_senders = mutex.lock(); + let hash_map = &client_senders.await; + let mut message_sent = false; + + match hash_map.get(&client_id.to_string()) { + Some(client_sender) => { + if client.1.active { + match ipc_dispatch_store.clone().api_keys.lock().await.get(&client.1.api_key) { + Some(api_key) => { + for allowed_event_regex in &api_key.allowed_events_to { + match Regex::new(&allowed_event_regex) { + Ok(regex) => { + // TODO: Generate an internal CID in CoreUserConfig! + if regex.is_match(&allowed_event_regex) { // && !(message.author.clone().split("?client=").collect::>()[1] == *client_id.clone()) { + debug!("Sending event: \"{}\", to client: {}...", message.kind.clone(), client_id.clone()); + match client_sender.send(message.clone()) { + Ok(_) => { + message_sent = true; + }, + Err(e) => { + error!("Failed to send message to client: {}, due to:\n{}", client_id.clone(), e); + } + }; + + break; + } + }, + Err(e) => { + error!("Message: \"{}\", failed, allowed event regular expression for client: {}, errored with: {}", message.kind, client_id.clone(), e); + } + } + } + + let message_author = message.author.clone(); + let message_client_vec = message_author.split("?client=").collect::>(); + if message_client_vec.len() > 1 { + if (!message_sent) && api_key.echo && (message_client_vec[1] == *client_id.clone()) { + debug!("Echoing event: \"{}\", to client: {}...", message.kind.clone(), client_id.clone()); + match client_sender.send(message.clone()) { + Ok(_) => { + message_sent = true; + }, + Err(e) => { + error!("Failed to send message to client: {}, due to:\n{}", client_id.clone(), e); + } + + }; + } + } + }, + None => { + error!("DANGER! Client: {}, had API key removed from store without closing connection on removal, THIS IS BAD; please report this! Closing connection...", client_id.clone()); + + let kind = Url::parse("clover://nexus.clover.reboot-codes.com/clients/unauthorize") + .unwrap() + .query_pairs_mut() + .append_pair("id", &client_id.clone()) + .finish() + .to_string(); + + let generated_message = gen_ipc_message( + &ipc_dispatch_store.clone(), + &ipc_dispatch_user_config.clone(), + kind, + "api key removed from store".to_string() + ).await; + ipc_dispatch_store.messages.lock().await.insert(generated_message.id.clone(), generated_message.clone().into()); + + let _ = client_sender.send(generated_message.clone()); + } + } + } + }, + None => { + error!("Client: {}, does not exist in the client map!", client_id.clone()); + } + } + + if !message_sent { debug!("Message: \"{}\", not sent to client: {}", message.kind.clone(), client_id.clone()); } + } + }, + Err(e) => { + error!("Message: {{ \"id\": \"{}\", \"kind\": \"{}\", \"message\": \"{}\" }}, error parsing `kind`: {}", message.id.clone(), message.kind.clone(), message.message.clone(), e); + } + } + } + } => {} + } + }); + + // Internal IPC Handles + let from_warehouse_cfg = Arc::new(warehouse_cfg.clone()); + let from_warehouse_store = Arc::new(store.clone()); + let from_warehouse_tx = Arc::new(from_client_tx.clone()); + let from_warehouse_token = cancellation_tokens.0.clone(); + let from_warehouse_handle = tokio::task::spawn(async move { + tokio::select! { + _ = from_warehouse_token.cancelled() => { + debug!("from_warehouse exited"); + }, + _ = async move { + while let Some(msg) = warehouse_ipc.1.recv().await { + handle_ipc_send(from_warehouse_tx.clone(), msg, &from_warehouse_cfg.clone(), &from_warehouse_store.clone()).await; + } + } => {} + } + }); + + let from_arbiter_cfg = Arc::new(arbiter_cfg.clone()); + let from_arbiter_store = Arc::new(store.clone()); + let from_arbiter_tx = Arc::new(from_client_tx.clone()); + let from_arbiter_token = cancellation_tokens.0.clone(); + let from_arbiter_handle = tokio::task::spawn(async move { + tokio::select! { + _ = from_arbiter_token.cancelled() => { + debug!("from_arbiter exited"); + }, + _ = async move { + while let Some(msg) = arbiter_ipc.1.recv().await { + handle_ipc_send(from_arbiter_tx.clone(), msg, &from_arbiter_cfg.clone(), &from_arbiter_store.clone()).await; + } + } => {} + } + }); + + let from_renderer_cfg = Arc::new(renderer_cfg.clone()); + let from_renderer_store = Arc::new(store.clone()); + let from_renderer_tx = Arc::new(from_client_tx.clone()); + let from_renderer_token = cancellation_tokens.0.clone(); + let from_renderer_handle = tokio::task::spawn(async move { + tokio::select! { + _ = from_renderer_token.cancelled() => { + debug!("from_renderer exited"); + }, + _ = async move { + while let Some(msg) = renderer_ipc.1.recv().await { + handle_ipc_send(from_renderer_tx.clone(), msg, &from_renderer_cfg.clone(), &from_renderer_store.clone()).await; + } + } => {} + } + }); + + let from_modman_cfg = Arc::new(modman_cfg.clone()); + let from_modman_store = Arc::new(store.clone()); + let from_modman_tx = Arc::new(from_client_tx.clone()); + let from_modman_token = cancellation_tokens.0.clone(); + let from_modman_handle = tokio::task::spawn(async move { + tokio::select! { + _ = from_modman_token.cancelled() => { + debug!("from_modman exited"); + }, + _ = async move { + while let Some(msg) = modman_ipc.1.recv().await { + handle_ipc_send(from_modman_tx.clone(), msg, &from_modman_cfg.clone(), &from_modman_store.clone()).await; + } + } => {} + } + }); + + let from_inference_engine_cfg = Arc::new(inference_engine_cfg.clone()); + let from_inference_engine_store = Arc::new(store.clone()); + let from_inference_engine_tx = Arc::new(from_client_tx.clone()); + let from_inference_engine_token = cancellation_tokens.0.clone(); + let from_inference_engine_handle = tokio::task::spawn(async move { + tokio::select! { + _ = from_inference_engine_token.cancelled() => { + debug!("from_reference_engine exited"); + }, + _ = async move { + while let Some(msg) = inference_engine_ipc.1.recv().await { + handle_ipc_send(from_inference_engine_tx.clone(), msg, &from_inference_engine_cfg.clone(), &from_inference_engine_store.clone()).await; + } + } => {} + } + }); + + let from_appd_cfg = Arc::new(appd_cfg.clone()); + let from_appd_store = Arc::new(store.clone()); + let from_appd_tx = Arc::new(from_client_tx.clone()); + let from_appd_token = cancellation_tokens.0.clone(); + let from_appd_handle = tokio::task::spawn(async move { + tokio::select! { + _ = from_appd_token.cancelled() => { + debug!("from_appd exited"); + }, + _ = async move { + while let Some(msg) = appd_ipc.1.recv().await { + handle_ipc_send(from_appd_tx.clone(), msg, &from_appd_cfg.clone(), &from_appd_store.clone()).await; + } + } => {} + } + }); + + let cleanup_token = cancellation_tokens.0.clone(); + tokio::select! { + _ = cleanup_token.cancelled() => { + info!("Cleaning and saving store..."); + // TODO: Clean up registered sessions when server is shutting down. + + std::mem::drop(store); + + cancellation_tokens.1.cancel(); + } + } + + tokio::select! {_ = futures::future::join_all(vec![ + from_warehouse_handle, + http_handle, + ipc_dispatch_handle, + from_arbiter_handle, + from_renderer_handle, + from_modman_handle, + from_inference_engine_handle, + from_appd_handle + ]) => { + info!("nexus has stopped!"); + }} +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..b777955 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,5 @@ +pub mod listener; +pub mod models; +pub mod websockets; + +// TODO: Move all events to ones with data that has a set serde model! diff --git a/src/server/models.rs b/src/server/models.rs new file mode 100644 index 0000000..9641aa3 --- /dev/null +++ b/src/server/models.rs @@ -0,0 +1,302 @@ +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::Mutex; +use serde::{Deserialize, Serialize}; +use crate::{arbiter::models::{ApiKey, ApiKeyWithKeyWithoutUID, User}, utils::{gen_api_key_with_check, gen_uid_with_check}}; + +// TODO: Define defaults via `Default` trait impl. + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct IPCMessage { + pub author: String, + pub kind: String, + pub message: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct IPCMessageWithId { + pub author: String, + pub kind: String, + pub message: String, + pub id: String, +} + +impl Into for IPCMessageWithId { + fn into(self) -> IPCMessage { + IPCMessage { + author: self.author, + kind: self.kind, + message: self.message, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Client { + pub api_key: String, + pub user_id: String, + pub active: bool, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ClientWithId { + pub id: String, + pub api_key: String, + pub user_id: String, + pub active: bool, +} + +impl Into for ClientWithId { + fn into(self) -> Client { + Client { + api_key: self.api_key, + user_id: self.user_id, + active: self.active, + } + } +} + +#[derive(Debug, Clone)] +pub struct Session { + pub start_time: String, + pub end_time: String, + pub api_key: String, +} + +// TODO: Move User and API Key models to Arbiter. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CoreUserConfig { + pub id: String, + pub api_key: String +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserConfig { + pub user_type: String, + pub pretty_name: String, + pub id: String, + pub api_keys: Vec +} + +// TODO: Add serialization/deserialization functions... +// TODO: Add options for making certain models ephemeral or persistent. +#[derive(Debug, Clone)] +pub struct Store { + pub users: Arc>>, + pub api_keys: Arc>>, + pub clients: Arc>>, + pub messages: Arc>>, +} + +#[derive(Debug, Clone)] +pub struct CoreUserConfigs { + pub evtbuzz: CoreUserConfig, + pub arbiter: CoreUserConfig, + pub renderer: CoreUserConfig, + pub appd: CoreUserConfig, + pub modman: CoreUserConfig, + pub inference_engine: CoreUserConfig, + pub warehouse: CoreUserConfig +} + +impl Store { + pub fn new() -> Self { + Store { + users: Arc::new(Mutex::new(HashMap::new())), + api_keys: Arc::new(Mutex::new(HashMap::new())), + clients: Arc::new(Mutex::new(HashMap::new())), + messages: Arc::new(Mutex::new(HashMap::new())), + } + } + + // Create a new store with a set master user. + pub async fn new_configured_store() -> (Store, CoreUserConfig, CoreUserConfigs) { + let ret = Store::new(); + + let master_user_config = ret.clone().create_master_user().await; + let core_users_config = ret.clone().add_all_core_users().await; + + (ret, master_user_config, core_users_config) + } + + pub async fn add_user(self, user_config: UserConfig) { + let mut key_ids: Vec = vec![]; + for key_config in user_config.api_keys.iter() { key_ids.push(key_config.key.clone()); }; + + self.users.lock().await.insert(user_config.id.clone(), User { + pretty_name: user_config.pretty_name, + user_type: user_config.user_type, + api_keys: key_ids, + sessions: Arc::new(Mutex::new(HashMap::new())) + }); + + for key_config in user_config.api_keys.iter() { + self.api_keys.lock().await.insert(key_config.key.clone(), ApiKey { + allowed_events_to: key_config.allowed_events_to.clone(), + allowed_events_from: key_config.allowed_events_from.clone(), + user_id: user_config.id.clone(), + echo: key_config.echo.clone() + }); + }; + } + + pub async fn create_master_user(self) -> CoreUserConfig { + let master_user_id = gen_uid_with_check(&self).await; + let master_api_key = gen_api_key_with_check(&self).await; + + self.add_user(UserConfig { + id: master_user_id.clone(), + pretty_name: "Master User".to_string(), + user_type: "com.reboot-codes.clover.master".to_string(), + api_keys: vec![ApiKeyWithKeyWithoutUID { + allowed_events_to: vec![".*".to_string()], + allowed_events_from: vec![".*".to_string()], + key: master_api_key.clone(), + echo: true + }] + }).await; + + CoreUserConfig { + id: master_user_id.clone(), + api_key: master_api_key.clone() + } + } + + /// Adds all the core user accounts, returns their configurations. + pub async fn add_all_core_users(self) -> CoreUserConfigs { + // EvtBuzz + let evtbuzz_uid = gen_uid_with_check(&self).await; + let evtbuzz_key = gen_api_key_with_check(&self).await; + self.clone().add_user(UserConfig { + user_type: "com.reboot-codes.clover.evtbuzz".to_string(), + pretty_name: "EvtBuzz".to_string(), + id: evtbuzz_uid.clone(), + api_keys: vec![ApiKeyWithKeyWithoutUID { + allowed_events_to: vec![".*".to_string()], + allowed_events_from: vec![".*".to_string()], + key: evtbuzz_key.clone(), + echo: true + }] + }).await; + + // Arbiter + let arbiter_uid = gen_uid_with_check(&self).await; + let arbiter_key = gen_api_key_with_check(&self).await; + self.clone().add_user(UserConfig { + user_type: "com.reboot-codes.clover.arbiter".to_string(), + pretty_name: "Arbiter".to_string(), + id: arbiter_uid.clone(), + api_keys: vec![ApiKeyWithKeyWithoutUID { + allowed_events_to: vec![".*".to_string()], + allowed_events_from: vec![".*".to_string()], + key: arbiter_key.clone(), + echo: true + }] + }).await; + + // Renderer + let renderer_uid = gen_uid_with_check(&self).await; + let renderer_key = gen_api_key_with_check(&self).await; + self.clone().add_user(UserConfig { + user_type: "com.reboot-codes.clover.renderer".to_string(), + pretty_name: "Renderer".to_string(), + id: renderer_uid.clone(), + api_keys: vec![ApiKeyWithKeyWithoutUID { + allowed_events_to: vec![".*".to_string()], + allowed_events_from: vec![".*".to_string()], + key: renderer_key.clone(), + echo: true + }] + }).await; + + // AppD + let appd_uid = gen_uid_with_check(&self).await; + let appd_key = gen_api_key_with_check(&self).await; + self.clone().add_user(UserConfig { + user_type: "com.reboot-codes.clover.appd".to_string(), + pretty_name: "appd".to_string(), + id: appd_uid.clone(), + api_keys: vec![ApiKeyWithKeyWithoutUID { + allowed_events_to: vec![".*".to_string()], + allowed_events_from: vec![".*".to_string()], + key: appd_key.clone(), + echo: true + }] + }).await; + + // ModMan + let modman_uid = gen_uid_with_check(&self).await; + let modman_key = gen_api_key_with_check(&self).await; + self.clone().add_user(UserConfig { + user_type: "com.reboot-codes.clover.modman".to_string(), + pretty_name: "ModMan".to_string(), + id: modman_uid.clone(), + api_keys: vec![ApiKeyWithKeyWithoutUID { + allowed_events_to: vec![".*".to_string()], + allowed_events_from: vec![".*".to_string()], + key: modman_key.clone(), + echo: true + }] + }).await; + + // Inference Engine + let inference_engine_uid = gen_uid_with_check(&self).await; + let inference_engine_key = gen_api_key_with_check(&self).await; + self.clone().add_user(UserConfig { + user_type: "com.reboot-codes.clover.inference-engine".to_string(), + pretty_name: "Inference Engine".to_string(), + id: inference_engine_uid.clone(), + api_keys: vec![ApiKeyWithKeyWithoutUID { + allowed_events_to: vec![".*".to_string()], + allowed_events_from: vec![".*".to_string()], + key: inference_engine_key.clone(), + echo: true + }] + }).await; + + // Warehouse + let warehouse_uid = gen_uid_with_check(&self).await; + let warehouse_key = gen_api_key_with_check(&self).await; + self.clone().add_user(UserConfig { + user_type: "com.reboot-codes.clover.warehouse".to_string(), + pretty_name: "Warehouse".to_string(), + id: warehouse_uid.clone(), + api_keys: vec![ApiKeyWithKeyWithoutUID { + allowed_events_to: vec![".*".to_string()], + allowed_events_from: vec![".*".to_string()], + key: warehouse_key.clone(), + echo: true + }] + }).await; + + CoreUserConfigs { + evtbuzz: CoreUserConfig { + id: evtbuzz_uid.clone(), + api_key: evtbuzz_key.clone() + }, + arbiter: CoreUserConfig { + id: arbiter_uid.clone(), + api_key: arbiter_key.clone() + }, + renderer: CoreUserConfig { + id: renderer_uid.clone(), + api_key: renderer_key.clone() + }, + appd: CoreUserConfig { + id: appd_uid.clone(), + api_key: appd_key.clone() + }, + modman: CoreUserConfig { + id: modman_uid.clone(), + api_key: modman_key.clone() + }, + inference_engine: CoreUserConfig { + id: inference_engine_uid.clone(), + api_key: inference_engine_key.clone() + }, + warehouse: CoreUserConfig { + id: warehouse_uid.clone(), + api_key: warehouse_key.clone() + }, + } + } +} diff --git a/src/server/websockets.rs b/src/server/websockets.rs new file mode 100644 index 0000000..11bd2e2 --- /dev/null +++ b/src/server/websockets.rs @@ -0,0 +1,194 @@ +use log::{debug, error, info, warn}; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; +use url::Url; +use uuid::Uuid; +use std::collections::HashMap; +use tokio::sync::mpsc::{self, UnboundedSender}; +use std::sync::Arc; +use std::time::SystemTime; +use futures::{SinkExt, StreamExt}; +use warp::filters::ws::{Message, WebSocket}; +use crate::arbiter::models::{ApiKeyWithKey, UserWithId}; +use crate::utils::iso8601; +use crate::server::models::{Client, ClientWithId, IPCMessageWithId, Session, Store}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct WsIn { + pub kind: String, + pub message: String, +} + +pub async fn handle_ws_client(auth: (UserWithId, ApiKeyWithKey, ClientWithId, Session), ws: warp::ws::Ws, store: Arc>, to_clients_tx: Arc>>>>, from_clients_tx: Arc>) -> Result { + let user = auth.0.clone(); + let api_key = auth.1.clone(); + let client = auth.2.clone(); + let session = auth.3.clone(); + + info!("Upgrading client: {}, to websocket connection...", client.id.clone()); + + let ws_client = Arc::new(client.clone()); + Ok(ws.on_upgrade(move |websocket: WebSocket| async move { + info!("Upgraded client: {}, to websocket connection!", ws_client.id.clone()); + + let (mut sender, mut receiver) = websocket.split(); + let (to_client_tx, mut to_client_rx) = mpsc::unbounded_channel::(); + let mut deauthed = false; + + to_clients_tx.lock().await.insert(ws_client.id.clone(), to_client_tx); + + let recv_api_key = Arc::new(api_key.clone()); + let recv_user = Arc::new(user.clone()); + let recv_client = Arc::new(client.clone()); + let recv_store = Arc::new(store.clone()); + let recv_handle = tokio::task::spawn(async move { + while !deauthed && let Some(body) = receiver.next().await { + match body { + Ok(msg) => { + // Skip any non-Text messages... + let message = if let Ok(s) = msg.to_str() { + s + } else { + if msg.is_close() { + info!("Client: {}, disconnected!", recv_client.id.clone()); + (#[allow(unused_assignments)] + deauthed) = true; + break; + } + + return; + }; + + match serde_json::from_str::(message) { + Ok(msg) => { + let message_id = loop { + let message_id = Uuid::new_v4().to_string(); + match recv_store.messages.lock().await.get(&message_id.clone()) { + Some(_) => { + debug!("Message: {}, exists, retrying...", message_id.clone()); + }, + None => { + break message_id; + } + } + }; + debug!("Client: {}, send message: {{ \"id\": \"{}\", \"kind\": \"{}\", \"message\": \"{}\" }}...", ws_client.id.clone(), message_id.clone(), msg.kind.clone(), msg.message.clone()); + + let mut allowed_to_send = false; + for allowed_send_pattern in recv_api_key.allowed_events_from.clone() { + match Regex::new(&allowed_send_pattern) { + Ok(pattern) => { + if pattern.is_match(&msg.kind.clone()) { + allowed_to_send = true; + break; + } + }, + Err(e) => { + warn!("Allowed send from pattern: \"{}\" (for user: \"{}\"), is not valid, due to:\n{}", allowed_send_pattern.clone(), recv_user.id.clone(), e); + } + } + } + + if allowed_to_send { + let generated_message = IPCMessageWithId { id: message_id.clone(), author: format!("ws:{}?client={}", recv_api_key.user_id.clone(), ws_client.id.clone()), kind: msg.kind.clone(), message: msg.message.clone() }; + + recv_store.messages.lock().await.insert(message_id.clone(), generated_message.clone().into()); + + match from_clients_tx.send(generated_message.clone()) { + Ok(_) => { + debug!("Client: {}, successfully sent message: {{ \"id\": \"{}\", \"kind\": \"{}\", \"message\": \"{}\" }}, over to dispatch IPC thread!", recv_client.id.clone(), message_id.clone(), msg.kind.clone(), msg.message.clone()); + }, + Err(e) => { + error!("Client: {}, failed to send message: {{ \"id\": \"{}\", \"kind\": \"{}\", \"message\": \"{}\" }}, over to dispatch IPC thread, due to:\n {}", recv_client.id.clone(), message_id.clone(), msg.kind.clone(), msg.message.clone(), e); + } + }; + } else { + warn!("Client: {}, attempted to send message of {{ \"id\": \"{}\", \"kind\": \"{}\", \"message\": \"{}\" }} when unauthorized!", recv_client.id.clone(), message_id.clone(), msg.kind.clone(), msg.message.clone()); + // TODO: Send unauthorized warning. + } + }, + Err(e) => { + warn!("Client: {}, error reading message: \"{}\", due to:\n {}", recv_client.id.clone(), message, e); + // TODO: Send err? + } + }; + }, + Err(e) => { + error!("Error reading message on client: \"{}\", {}", ws_client.id.clone(), e); + // TODO: send error? + } + }; + } + + // Client closed connection. + (#[allow(unused_assignments)] + deauthed) = true; + }); + + let send_api_key = Arc::new(api_key.clone()); + let send_client = Arc::new(client.clone()); + let send_handle = tokio::task::spawn(async move { + while let Some(msg) = to_client_rx.recv().await { + if msg.kind == Url::parse("clover://evtbuzz.clover.reboot-codes.com/clients/unauthorize") + .unwrap() + .query_pairs_mut() + .append_pair("id", send_client.id.clone().as_str()) + .finish() + .to_string() { + match sender.close().await { + Ok(_) => {}, + Err(e) => { + error!("Client: {}, failed to close connection due to:\n {}", send_client.id.clone(), e); + } + }; + + (#[allow(unused_assignments)] + deauthed) = true; + break; + } else if deauthed { + break; + } else { + let msg_author = msg.author.clone(); + let msg_client_id = if msg.author.clone().split("?client=").collect::>().len() > 1 { + msg_author.split("?client=").collect::>()[1] + } else { + "" + }; + + if (msg_client_id != send_client.id.clone()) || send_api_key.echo.clone() { + let response = serde_json::to_string(&IPCMessageWithId { + id: msg.id.clone(), + author: msg.author.clone(), + kind: msg.kind.clone(), + message: msg.message.clone(), + }) + .unwrap(); + match sender.send(Message::text(response)).await { + Ok(_) => { + + }, + Err(err) => { + error!("Client: {}, error sending message: {{ \"id\": \"{}\", \"kind\": \"{}\", \"message\": \"{}\" }}, {}", send_client.id.clone(), msg.id.clone(), msg.kind.clone(), msg.message.clone(), err); + } + } + } + } + } + }); + + let clean_up_client = Arc::new(client.clone()); + let clean_up_store = Arc::new(store.clone()); + let clean_up_handle = tokio::task::spawn(async move { + while !deauthed {if deauthed { break; }} + + info!("Client: {}, disconnected, cleaning up...", clean_up_client.id.clone()); + debug!("Ending session for: {}...", clean_up_client.id.clone()); + clean_up_store.users.clone().lock().await.get(&user.id.clone()).unwrap().sessions.lock().await.insert(clean_up_client.id.clone(), Session { start_time: session.start_time.clone(), end_time: iso8601(&SystemTime::now()), api_key: api_key.key.clone() }); + debug!("Deactivating client: {}...", clean_up_client.id.clone()); + clean_up_store.clients.clone().lock().await.insert(clean_up_client.id.clone(), Client { api_key: api_key.key.clone(), user_id: user.id.clone(), active: false }); + }); + + futures::future::join_all(vec![recv_handle, send_handle, clean_up_handle]).await; + })) +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c752fd6 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,149 @@ +use std::{hash::{DefaultHasher, Hash, Hasher}, sync::Arc}; +use tokio::{fs, io::AsyncReadExt}; +use api_key::types::{ApiKeyResults, Default, StringGenerator}; +use chrono::prelude::{DateTime, Utc}; +use os_path::OsPath; +use simple_error::SimpleError; +use tokio::sync::mpsc::UnboundedSender; +use uuid::Uuid; +use crate::server::models::{CoreUserConfig, IPCMessageWithId, Store}; + +pub struct RecvSync(pub std::sync::mpsc::Receiver); + +unsafe impl Sync for RecvSync {} + +/// formats like "2001-07-08T00:34:60.026490+09:30" +pub fn iso8601(st: &std::time::SystemTime) -> String { + let dt: DateTime = st.clone().into(); + format!("{}", dt.format("%+")) +} + +#[allow(dead_code)] +pub fn calculate_hash(t: &T) -> u64 { + let mut s = DefaultHasher::new(); + t.hash(&mut s); + s.finish() +} + +/// Generates a new api key. Please use [`gen_api_key_with_check`] to ensure that its' unique! +pub fn gen_api_key() -> String { + let options = StringGenerator { + prefix: "CLOVER:".to_string(), + length: 50, + ..StringGenerator::default() + }; + let key: ApiKeyResults = api_key::string(options); + + match key { + ApiKeyResults::String(res) => res, + ApiKeyResults::StringArray(res_vec) => res_vec.join(""), + } +} + +/// Generates a new API key after checking that it is not currently in the Store. +pub async fn gen_api_key_with_check(store: &Store) -> String { + loop { + let api_key = gen_api_key(); + match store.api_keys.lock().await.get(&api_key.clone()) { + Some(_) => {}, + None => { + break api_key; + } + } + } +} + +/// Generates a new UID after checking that it is currently not in the Store. +pub async fn gen_uid_with_check(store: &Store) -> String { + loop { + let uid = Uuid::new_v4().to_string(); + match store.users.lock().await.get(&uid.clone()) { + Some(_) => {}, + None => { + break uid; + } + } + } +} + +pub async fn gen_message_id_with_check(store: &Store) -> String { + loop { + let message_id = Uuid::new_v4().to_string(); + match store.messages.lock().await.get(&message_id.clone()) { + Some(_) => {}, + None => { + break message_id; + } + } + } +} + +pub async fn gen_ipc_message(store: &Store, user_config: &CoreUserConfig, kind: String, message: String) -> IPCMessageWithId { + let message_id = gen_message_id_with_check(&store.clone()).await; + IPCMessageWithId { + id: message_id.clone(), + author: user_config.id.to_string(), + kind, + message + } +} + +pub async fn gen_cid_with_check(store: &Store) -> String { + loop { + let client_id = Uuid::new_v4().to_string(); + match store.clients.lock().await.get(&client_id.clone()) { + Some(_) => {}, + None => { + break client_id; + } + } + } +} + +pub async fn send_ipc_message( + store: &Store, + user_config: &CoreUserConfig, + ipc_tx: Arc>, + kind: String, + message: String +) -> Result<(), tokio::sync::mpsc::error::SendError> { + ipc_tx.send(gen_ipc_message(store, user_config, kind, message).await) +} + +pub async fn read_file(path: OsPath) -> Result { + let mut err = None; + let mut ret = None; + let mut contents = String::new(); + + if path.exists() { + match fs::File::open(path.to_path()).await { + Ok(mut file) => { + match file.read_to_string(&mut contents).await { + Ok(_) => { + ret = Some(contents); + }, + Err(e) => { + err = Some(SimpleError::from(e)); + } + } + }, + Err(e) => { + err = Some(SimpleError::from(e)) + } + } + } else { + err = Some(SimpleError::new("Path does not exist!")); + } + + match err { + Some(e) => { Err(e) }, + None => { + match ret { + Some(val) => { Ok(val) }, + None => { + Err(SimpleError::new("Impossible state, no error reported but return value is missing!")) + } + } + } + } +}