diff --git a/.gitignore b/.gitignore index b60de5b..4e5103b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ **/target +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9879e30..7a41547 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,26 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[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 = "async-io" version = "1.12.0" @@ -42,18 +62,119 @@ dependencies = [ "syn", ] +[[package]] +name = "async-trait" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8582122b8edba2af43eaf6b80dbfd33f421b5a0eb3a3113d21bc096ac5b44faf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f958c80c248b34b9a877a643811be8dbca03ca5ba827f2b63baf3a81e5fc4e" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bson" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8746d07211bb12a7c34d995539b4a2acd4e0b0e757de98ce2ab99bcf17443fad" +dependencies = [ + "ahash", + "base64 0.13.1", + "hex", + "indexmap", + "lazy_static", + "rand", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + [[package]] name = "bumpalo" version = "3.12.0" @@ -98,7 +219,7 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.16", "serde", "serde_json", ] @@ -115,6 +236,28 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "concurrent-queue" version = "2.1.0" @@ -124,6 +267,38 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-channel" version = "0.5.7" @@ -156,6 +331,154 @@ dependencies = [ "cfg-if", ] +[[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 = "cxx" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.0", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "enum-as-inner" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "errno" version = "0.2.8" @@ -201,12 +524,63 @@ dependencies = [ "instant", ] +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +[[package]] +name = "futures-executor" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.26" @@ -239,6 +613,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" + [[package]] name = "futures-task" version = "0.3.26" @@ -251,14 +631,28 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" 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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -276,6 +670,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.2.6" @@ -285,6 +691,156 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.12" @@ -304,6 +860,24 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "ipconfig" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +dependencies = [ + "socket2", + "widestring", + "winapi", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" + [[package]] name = "itoa" version = "1.0.5" @@ -331,6 +905,21 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -356,6 +945,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "mach" version = "0.3.2" @@ -365,6 +963,33 @@ dependencies = [ "libc", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest", +] + [[package]] name = "memchr" version = "2.5.0" @@ -380,6 +1005,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "mio" version = "0.8.6" @@ -408,7 +1049,7 @@ dependencies = [ "once_cell", "parking_lot", "quanta", - "rustc_version", + "rustc_version 0.4.0", "scheduled-thread-pool", "skeptic", "smallvec", @@ -418,6 +1059,53 @@ dependencies = [ "uuid", ] +[[package]] +name = "mongodb" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37fe10c1485a0cd603468e284a1a8535b4ecf46808f5f7de3639a1e1252dbf8" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bitflags", + "bson", + "chrono", + "derivative", + "derive_more", + "futures-core", + "futures-executor", + "futures-io", + "futures-util", + "hex", + "hmac", + "lazy_static", + "md-5", + "pbkdf2", + "percent-encoding", + "rand", + "rustc_version_runtime", + "rustls", + "rustls-pemfile", + "serde", + "serde_bytes", + "serde_with", + "sha-1", + "sha2", + "socket2", + "stringprep", + "strsim", + "take_mut", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-util", + "trust-dns-proto", + "trust-dns-resolver", + "typed-builder", + "uuid", + "webpki-roots", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -428,6 +1116,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.15.0" @@ -479,6 +1186,41 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -505,6 +1247,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.51" @@ -541,6 +1289,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.23" @@ -550,6 +1304,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 = "raw-cpuid" version = "10.7.0" @@ -568,13 +1352,57 @@ dependencies = [ "bitflags", ] +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver", + "semver 1.0.16", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31b7153270ebf48bf91c65ae5b0c00e749c4cfad505f66530ac74950249582f" +dependencies = [ + "rustc_version 0.2.3", + "semver 0.9.0", ] [[package]] @@ -591,6 +1419,33 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.0", +] + +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + [[package]] name = "ryu" version = "1.0.12" @@ -621,6 +1476,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5e082f6ea090deaf0e6dd04b68360fd5cddb152af6ce8927c9d25db299f98c" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.16" @@ -630,6 +1510,12 @@ dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.152" @@ -639,6 +1525,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.152" @@ -652,15 +1547,81 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" dependencies = [ + "indexmap", "itoa", "ryu", "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0969fff533976baadd92e08b1d102c5a3d8a8049eadfd69d4d1e3c5b2ed189" +dependencies = [ + "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 = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -719,6 +1680,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.109" @@ -730,12 +1719,24 @@ 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 = "tagptr" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + [[package]] name = "tempfile" version = "3.4.0" @@ -749,6 +1750,15 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.38" @@ -779,6 +1789,48 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +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.25.0" @@ -810,6 +1862,102 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-util" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "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-cookies" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40f38d941a2ffd8402b36e02ae407637a9caceb693aaf2edc910437db0f36984" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.37" @@ -817,6 +1965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -874,6 +2023,74 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1ee9bd9239c339d714d657fac840c6d2a4f9c45f4f9ec7b0975113458be78db" +[[package]] +name = "trust-dns-proto" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "log", + "rand", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typed-builder" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicase" version = "2.6.0" @@ -883,12 +2100,50 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" + [[package]] name = "unicode-ident" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna 0.3.0", + "percent-encoding", +] + [[package]] name = "uuid" version = "1.3.0" @@ -896,6 +2151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ "getrandom", + "serde", ] [[package]] @@ -927,6 +2183,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" @@ -1003,6 +2269,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "wepoll-ffi" version = "0.1.2" @@ -1012,6 +2297,12 @@ dependencies = [ "cc", ] +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + [[package]] name = "winapi" version = "0.3.9" @@ -1124,13 +2415,33 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "wrapper" version = "0.1.0" dependencies = [ "async-recursion", + "axum", + "bytes", + "dotenv", + "futures", "moka", + "mongodb", + "rand", + "serde", + "serde_json", "tokio", + "tower", + "tower-cookies", + "tower-http", "tracing", "tracing-subscriber", ] diff --git a/Cargo.toml b/Cargo.toml index a8c6cd9..c114ff5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,34 @@ version = "0.1.0" edition = "2021" [dependencies] +# Blazingly fast runtime tokio = { version = "1", features = ["full"] } -async-recursion = "1" -tracing = "0.1.37" tracing-subscriber = "0.3.16" -moka = { version = "0.10.0", features = ["future"] } \ No newline at end of file +tracing = "0.1.37" + +# Allow recursion inside tokio async +async-recursion = "1" + +# DNS Caching Layer +moka = { version = "0.10.0", features = ["future"] } + +# Mongodb +mongodb = { version = "2.4", features = ["tokio-sync"] } +futures = "0.3.26" + +# Convert values to json for Mongodb +serde = { version = "1.0", features = ["derive"] } + +# Reading env vars from .env +dotenv = "0.15.0" + +# For the meme records +rand = "0.8.5" + +# For the http web frontend +axum = "0.6.4" +tower-http = { version = "0.4.0", features = ["fs"] } +tower-cookies = "0.9.0" +tower = "0.4.13" +bytes = "1.4.0" +serde_json = "1" \ No newline at end of file diff --git a/public/css/home.css b/public/css/home.css new file mode 100644 index 0000000..2548a28 --- /dev/null +++ b/public/css/home.css @@ -0,0 +1,40 @@ +span { + margin-top: 5rem; + margin-bottom: 1rem; + width: 45rem; + font-size: 2em; +} + +#new { + display: flex; + justify-content: center; + width: 100%; + padding-top: 2rem; + padding-bottom: 1rem; + border-bottom: solid 1px var(--gray); +} + +#new input, .block { + border-radius: 1rem 0 0 1rem; + width: 40rem; +} + +.block { + width: 33em; +} + +#new button { + border-radius: 0 1rem 1rem 0; +} + +.domain { + margin-top: 2rem; +} + +.domain .delete { + border-radius: 0 1rem 1rem 0; +} + +.domain .edit { + border-radius: 0; +} \ No newline at end of file diff --git a/public/css/login.css b/public/css/login.css new file mode 100644 index 0000000..2be7c13 --- /dev/null +++ b/public/css/login.css @@ -0,0 +1,18 @@ +#login { + margin-top: 20em; +} + +#logo { + font-size: 6em; + font-weight: 750; + font-family: bold; + margin-bottom: 2rem; +} + +form { + width: 30rem; +} + +form input { + width: 100%; +} \ No newline at end of file diff --git a/public/css/main.css b/public/css/main.css new file mode 100644 index 0000000..971e2bb --- /dev/null +++ b/public/css/main.css @@ -0,0 +1,119 @@ +:root { + --dark: #222428; + --dark-alternate: #2b2e36; + --header: #1e1e22; + + --accent: #8849f5; + --accent-alternate: #6829d5; + --gray: #2f2f3f; + --main: #ffffff; + --main-alternate: #cccccc; +} + +* { + padding: 0; + margin: 0; +} + +@font-face { + font-family: main; + src: url("../fonts/helvetica.ttf") format("truetype"); + font-display: swap; +} + +@font-face { + font-family: bold; + src: url("../fonts/overpass-bold.otf") format("opentype"); + font-display: swap; +} + +@font-face { + font-family: bold-italic; + src: url("../fonts/overpass-bold-italic.otf") format("opentype"); + font-display: swap; +} + +html { + background-color: var(--dark); + font-family: main; + color: var(--main); + width: 100%; + height: 100%; +} + +body { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.accent { + color: var(--accent); +} + +.fill { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +input, button, .block { + all: unset; + display: inline-block; + font: main; + background-color: var(--dark-alternate); + font-size: 1rem; + padding: 1rem; + border-radius: 1rem; + margin-bottom: 20px; +} + +button { + background-color: var(--accent); + width: 5em; + text-align: center; +} + +button:hover { + cursor: pointer; + background-color: var(--accent-alternate); +} + +.delete { + background-color: #f54842; +} + +.delete:hover { + cursor: pointer; + background-color: #d52822; +} + +form { + display: flex; + flex-direction: column; +} + +#header { + width: calc(100% - 4rem); + background-color: var(--header); + border-bottom: solid 1px var(--gray); + padding: 1rem; + padding-left: 3rem; +} + +#logo { + font-size: 2em; + font-weight: 500; + font-family: bold; +} + +#title { + font-size: 2em; + font-weight: 300; + font-family: sans-serif; + padding-left: 1em; +} \ No newline at end of file diff --git a/public/css/record.css b/public/css/record.css new file mode 100644 index 0000000..3dc257a --- /dev/null +++ b/public/css/record.css @@ -0,0 +1,67 @@ +#buttons { + margin-top: 2rem; + width: 50rem; +} + +#buttons button { + margin: 0; + margin-right: 2rem; + border-radius: 10px; + width: auto; + padding: .75rem 1rem; +} + +.record { + width: 50rem; + background-color: var(--header); + padding: 1rem; + margin-top: 2rem; +} + +.header { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.header span { + font-family: bold; +} + +.header button { + margin: 0; + margin-left: 2rem; + padding: .5rem 1rem; + width: auto; + border-radius: 5px; +} + +.type { + margin-right: 1rem; + background-color: var(--accent); + padding: .25rem .5rem; + border-radius: 5px; +} + +.domain { + color: var(--main-alternate); + flex-grow: 1; +} + +.properties { + display: flex; + flex-direction: column; +} + +.poperty { + display: flex; + flex-direction: row; + border-bottom: solid 1px var(--gray); + margin-top: 1rem; +} + +.key { + font-family: bold; + width: 5rem; +} + diff --git a/public/domain.html b/public/domain.html new file mode 100644 index 0000000..ba22eaf --- /dev/null +++ b/public/domain.html @@ -0,0 +1,21 @@ + + + + + + Wrapper - Records + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/fonts/helvetica-bold.ttf b/public/fonts/helvetica-bold.ttf new file mode 100644 index 0000000..332b66c Binary files /dev/null and b/public/fonts/helvetica-bold.ttf differ diff --git a/public/fonts/helvetica.ttf b/public/fonts/helvetica.ttf new file mode 100644 index 0000000..718f22d Binary files /dev/null and b/public/fonts/helvetica.ttf differ diff --git a/public/fonts/overpass-bold-italic.otf b/public/fonts/overpass-bold-italic.otf new file mode 100644 index 0000000..f4929ae Binary files /dev/null and b/public/fonts/overpass-bold-italic.otf differ diff --git a/public/fonts/overpass-bold.otf b/public/fonts/overpass-bold.otf new file mode 100644 index 0000000..962a5d7 Binary files /dev/null and b/public/fonts/overpass-bold.otf differ diff --git a/public/home.html b/public/home.html new file mode 100644 index 0000000..9efb21e --- /dev/null +++ b/public/home.html @@ -0,0 +1,21 @@ + + + + + + Wrapper - Domains + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/js/api.js b/public/js/api.js new file mode 100644 index 0000000..7a29d65 --- /dev/null +++ b/public/js/api.js @@ -0,0 +1,51 @@ +const endpoint = '/api' + +const request = async (url, method, body) => { + + let response; + + if (method == 'GET') { + response = await fetch(endpoint + url, { + method, + headers: { + 'Content-Type': 'application/json' + } + }); + } else { + response = await fetch(endpoint + url, { + method, + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json' + } + }); + } + + if (response.status == 401) { + location.href = '/login' + } + const contentType = response.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + const json = await response.json() + return { status: response.status, msg: json.msg, json } + } else { + const msg = await response.text(); + return { status: response.status, msg } + } +} + +export const login = async (user, pass) => { + return await request('/login', 'POST', {user, pass}) +} + +export const domains = async () => { + return await request('/domains', 'GET') +} + +export const del_domain = async (domain) => { + return await request('/domains', 'DELETE', {domain}) +} + +export const records = async (domain) => { + return await request(`/records?domain=${domain}`, 'GET') +} \ No newline at end of file diff --git a/public/js/components.js b/public/js/components.js new file mode 100644 index 0000000..00def5f --- /dev/null +++ b/public/js/components.js @@ -0,0 +1,12 @@ +import { div, parse, span } from './main.js'; + +export function header(title) { + return div({id: 'header'}, + span({id: 'logo', class: 'accent'}, + parse("Wrapper") + ), + span({id: 'title'}, + parse(title) + ), + ) +} \ No newline at end of file diff --git a/public/js/domain.js b/public/js/domain.js new file mode 100644 index 0000000..36af422 --- /dev/null +++ b/public/js/domain.js @@ -0,0 +1,95 @@ +import { del_domain, domains, records } from './api.js' +import { header } from './components.js' +import { body, parse, div, input, button, span, is_domain } from './main.js'; + +function render(domain, records) { + + let divs = [] + for (const record of records) { + divs.push(gen_record(record)) + } + + document.body.replaceWith( + body({}, + header(domain), + div({id: 'buttons'}, + button({onclick: (event) => { + location.href = '/home' + }}, parse("Home")), + button({}, parse("New Record")), + ), + ...divs + ) + ) +} + +function gen_record(record) { + let domain = record.domain + let prefix = record.prefix + + if (prefix.length > 0) { + prefix = prefix + '.' + } + + let type = Object.keys(record.record)[0] + let data = record.record[type] + + let divs = [] + for (const key in data) { + let disp_key; + if (key == 'ttl') { + disp_key = 'TTL' + } else { + disp_key = upper(key) + } + divs.push( + div({class: 'poperty'}, + div({class: 'key'}, parse(disp_key)), + div({class: 'value'}, parse(data[key])), + ) + ) + } + + return div({class: 'record'}, + div({class: 'header'}, + span({class: 'type'}, parse(type)), + span({class: 'prefix'}, parse(prefix)), + span({class: 'domain'}, parse(domain)), + button({}, parse("Edit")), + button({class: 'delete'}, parse("Delete")) + ), + div({class: 'properties'}, + ...divs + ) + ) +} + +function upper(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +async function init() { + + const params = new Proxy(new URLSearchParams(window.location.search), { + get: (searchParams, prop) => searchParams.get(prop), + }); + + let domain = params.domain; + + if (!is_domain(domain)) { + location.href = '/home' + return + } + + let res = await records(domain); + + if (res.status !== 200) { + alert(res.msg) + return + } + + render(domain, res.json) + +} + +init() \ No newline at end of file diff --git a/public/js/home.js b/public/js/home.js new file mode 100644 index 0000000..f615632 --- /dev/null +++ b/public/js/home.js @@ -0,0 +1,91 @@ +import { del_domain, domains } from './api.js' +import { header } from './components.js' +import { body, parse, div, input, button, span, is_domain } from './main.js'; + +function render(domains) { + document.body.replaceWith( + body({}, + header('domains'), + div({id: 'new'}, + input({ + type: 'text', + name: 'domain', + id: 'domain', + placeholder: 'Type domain name to create new records', + autocomplete: "off", + }), + button({onclick: () => { + let domain = document.getElementById('domain').value + + if (!is_domain(domain)) { + alert("Invalid domain") + return + } + + location.href = '/domain?domain='+domain + }}, + parse("Create") + ) + ), + ...domain(domains) + ) + ) +} + +function domain(domains) { + let divs = [] + for (const domain of domains) { + divs.push( + div({class: 'domain'}, + div({class: 'block'}, + parse(domain) + ), + button({class: 'edit', onclick: (event) => { + console.log(event.target.parentElement) + let domain = event + .target + .parentElement + .getElementsByClassName('block')[0] + .innerText + + if (!is_domain(domain)) { + alert("Invalid domain") + return + } + + location.href = '/domain?domain='+domain + }}, + parse("Edit") + ), + button({class: 'delete', onclick: async () => { + let res = await del_domain(domain) + + if (res.status != 204) { + alert(res.msg) + return + } + + location.reload() + }}, + parse("Delete") + ) + ) + ) + } + return divs +} + +async function init() { + + let res = await domains(); + + if (res.status !== 200) { + alert(res.msg) + return + } + + render(res.json) + +} + +init() \ No newline at end of file diff --git a/public/js/login.js b/public/js/login.js new file mode 100644 index 0000000..3bd64ad --- /dev/null +++ b/public/js/login.js @@ -0,0 +1,44 @@ +import { body, div, form, input, p, parse, span} from './main.js' +import { login } from './api.js' + +function render() { + document.body.replaceWith( + body({}, + div({id: 'login', class: 'fill'}, + span({id: 'logo'}, + span({class: 'accent'}, parse('Wrapper')) + ), + form({autocomplete: "off"}, + input({ + type: 'text', + name: 'user', + id: 'user', + placeholder: 'Username', + autofocus: 1 + }), + input({ + type: 'password', + name: 'pass', + id: 'pass', + placeholder: 'Password', + onkeydown: async (event) => { + if (event.key == 'Enter') { + event.preventDefault() + let user = document.getElementById('user').value + let pass = document.getElementById('pass').value + + let res = await login(user, pass) + + if (res.status === 200) { + location.href = '/home' + } + } + } + }) + ) + ) + ) + ) +} + +render() diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..615b3d6 --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,136 @@ +function createElement(name, attrs, ...children) { + const el = document.createElement(name); + + for (const attr in attrs) { + if(attr.startsWith("on")) { + el[attr] = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]) + } + } + + for (const child of children) { + if (child == null) { + continue + } + el.appendChild(child) + } + + return el +} + +export function createElementNS(name, attrs, ...children) { + var svgns = "http://www.w3.org/2000/svg"; + var el = document.createElementNS(svgns, name); + + for (const attr in attrs) { + if(attr.startsWith("on")) { + el[attr] = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]) + } + } + + for (const child of children) { + if (child == null) { + continue + } + el.appendChild(child) + } + + return el +} + +export function p(attrs, ...children) { + return createElement("p", attrs, ...children) +} + +export function span(attrs, ...children) { + return createElement("span", attrs, ...children) +} + +export function div(attrs, ...children) { + return createElement("div", attrs, ...children) +} + +export function a(attrs, ...children) { + return createElement("a", attrs, ...children) +} + +export function i(attrs, ...children) { + return createElement("i", attrs, ...children) +} + +export function form(attrs, ...children) { + return createElement("form", attrs, ...children) +} + +export function img(alt, attrs, ...children) { + attrs['onerror'] = (event) => event.target.remove() + attrs['alt'] = alt + return createElement("img", attrs, ...children) +} + +export function input(attrs, ...children) { + return createElement("input", attrs, ...children) +} + +export function button(attrs, ...children) { + return createElement("button", attrs, ...children) +} + +export function path(attrs, ...children) { + return createElementNS("path", attrs, ...children) +} + +export function svg(attrs, ...children) { + return createElementNS("svg", attrs, ...children) +} + +export function body(attrs, ...children) { + return createElement("body", attrs, ...children) +} + +export function textarea(attrs, ...children) { + return createElement("textarea", attrs, ...children) +} + +export function parse(input) { + const pattern = /^[ a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*$/; + + input = input + ''; + + if (!pattern.test(input)) { + return null; + } + + const sanitized = input.replace(//g, '>'); + return document.createRange().createContextualFragment(sanitized); +} + +export function is_domain(domain) { + domain = domain.toLowerCase() + + const pattern = /^[a-z0-9_\-.]*$/; + if (!pattern.test(domain)) { + return false + } + + let parts = domain.split('.').reverse() + for (const part of parts) { + if (part.length < 1) { + return false + } + } + + if (parts.length < 2 || parts[0].length < 2) { + return false + } + + const tld_pattern = /^[a-z]*$/; + if (!tld_pattern.test(parts[0])) { + return false + } + + return true +} \ No newline at end of file diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000..b03324b --- /dev/null +++ b/public/login.html @@ -0,0 +1,21 @@ + + + + + + Wrapper - Login + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..b1b4ec3 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,9 @@ +User-agent: Googlebot +Disallow: /api + +User-agent: Googlebot +User-agent: AdsBot-Google +Disallow: /api + +User-agent: * +Disallow: /api \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 9350adf..547e853 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,35 +1,57 @@ -use std::net::IpAddr; +use std::{env, net::IpAddr, str::FromStr, fmt::Display}; #[derive(Clone)] pub struct Config { - fallback: IpAddr, - port: u16, + pub dns_fallback: IpAddr, + pub dns_port: u16, + pub dns_cache_size: u64, + + pub db_host: String, + pub db_port: u16, + pub db_user: String, + pub db_pass: String, + + pub web_user: String, + pub web_pass: String, + pub web_port: u16, } impl Config { pub fn new() -> Self { - let fallback = "9.9.9.9" - .parse::() - .expect("Failed to create default ns fallback"); + let dns_port = Self::get_var::("WRAPPER_DNS_PORT", 53); + let dns_fallback = Self::get_var::("WRAPPER_FALLBACK_DNS", [9, 9, 9, 9].into()); + let dns_cache_size = Self::get_var::("WRAPPER_CACHE_SIZE", 1000); + + let db_host = Self::get_var::("WRAPPER_DB_HOST", String::from("localhost")); + let db_port = Self::get_var::("WRAPPER_DB_PORT", 27017); + let db_user = Self::get_var::("WRAPPER_DB_USER", String::from("root")); + let db_pass = Self::get_var::("WRAPPER_DB_PASS", String::from("")); + + let web_user = Self::get_var::("WRAPPER_WEB_USER", String::from("admin")); + let web_pass = Self::get_var::("WRAPPER_WEB_PASS", String::from("wrapper")); + let web_port = Self::get_var::("WRAPPER_WEB_PORT", 80); + Self { - fallback, - port: 2000, + dns_fallback, + dns_port, + dns_cache_size, + + db_host, + db_port, + db_user, + db_pass, + + web_user, + web_pass, + web_port, } } - pub fn get_fallback_ns(&self) -> &IpAddr { - &self.fallback - } - - pub fn get_port(&self) -> u16 { - self.port - } - - pub fn set_fallback_ns(&mut self, addr: &IpAddr) { - self.fallback = *addr; - } - - pub fn set_port(&mut self, port: u16) { - self.port = port; + fn get_var(name: &str, default: T) -> T + where + T: FromStr + Display, + { + let env = env::var(name).unwrap_or(format!("{default}")); + env.parse::().unwrap_or(default) } } diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..0d81dc3 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,146 @@ +use futures::TryStreamExt; +use mongodb::{ + bson::doc, + options::{ClientOptions, Credential, ServerAddress}, + Client, +}; +use serde::{Deserialize, Serialize}; +use tracing::info; + +use crate::{ + config::Config, + dns::packet::{query::QueryType, record::DnsRecord}, +}; + +use crate::Result; + +#[derive(Clone)] +pub struct Database { + client: Client, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StoredRecord { + record: DnsRecord, + domain: String, + prefix: String, +} + +impl StoredRecord { + fn get_domain_parts(domain: &str) -> (String, String) { + let parts: Vec<&str> = domain.split(".").collect(); + let len = parts.len(); + if len == 1 { + (String::new(), String::from(parts[0])) + } else if len == 2 { + (String::new(), String::from(parts.join("."))) + } else { + ( + String::from(parts[0..len - 2].join(".")), + String::from(parts[len - 2..len].join(".")), + ) + } + } +} + +impl From for StoredRecord { + fn from(record: DnsRecord) -> Self { + let (prefix, domain) = Self::get_domain_parts(&record.get_domain()); + Self { + record, + domain, + prefix, + } + } +} + +impl Into for StoredRecord { + fn into(self) -> DnsRecord { + self.record + } +} + +impl Database { + pub async fn new(config: Config) -> Result { + let options = ClientOptions::builder() + .hosts(vec![ServerAddress::Tcp { + host: config.db_host, + port: Some(config.db_port), + }]) + .credential( + Credential::builder() + .username(config.db_user) + .password(config.db_pass) + .build(), + ) + .max_pool_size(100) + .app_name(String::from("wrapper")) + .build(); + + let client = Client::with_options(options)?; + + client + .database("wrapper") + .run_command(doc! {"ping": 1}, None) + .await?; + + info!("Connection to mongodb successfully"); + + Ok(Database { client }) + } + + pub async fn get_records(&self, domain: &str, qtype: QueryType) -> Result> { + let (prefix, domain) = StoredRecord::get_domain_parts(domain); + Ok(self + .get_domain(&domain) + .await? + .into_iter() + .filter(|r| r.prefix == prefix) + .filter(|r| { + let rqtype = r.record.get_qtype(); + if qtype == QueryType::A { + return rqtype == QueryType::A || rqtype == QueryType::AR; + } else if qtype == QueryType::AAAA { + return rqtype == QueryType::AAAA || rqtype == QueryType::AAAAR; + } else { + r.record.get_qtype() == qtype + } + }) + .map(|r| r.into()) + .collect()) + } + + pub async fn get_domain(&self, domain: &str) -> Result> { + let db = self.client.database("wrapper"); + let col = db.collection::(domain); + + let filter = doc! { "domain": domain }; + let mut cursor = col.find(filter, None).await?; + + let mut records = Vec::new(); + while let Some(record) = cursor.try_next().await? { + records.push(record); + } + + Ok(records) + } + + pub async fn add_record(&self, record: DnsRecord) -> Result<()> { + let record = StoredRecord::from(record); + let db = self.client.database("wrapper"); + let col = db.collection::(&record.domain); + col.insert_one(record, None).await?; + Ok(()) + } + + pub async fn get_domains(&self) -> Result> { + let db = self.client.database("wrapper"); + Ok(db.list_collection_names(None).await?) + } + + pub async fn delete_domain(&self, domain: String) -> Result<()> { + let db = self.client.database("wrapper"); + let col = db.collection::(&domain); + Ok(col.drop(None).await?) + } +} diff --git a/src/server/binding.rs b/src/dns/binding.rs similarity index 95% rename from src/server/binding.rs rename to src/dns/binding.rs index 1c69651..4c7e15f 100644 --- a/src/server/binding.rs +++ b/src/dns/binding.rs @@ -3,7 +3,8 @@ use std::{ sync::Arc, }; -use crate::packet::{buffer::PacketBuffer, Packet, Result}; +use super::packet::{buffer::PacketBuffer, Packet}; +use crate::Result; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream, UdpSocket}, @@ -140,11 +141,4 @@ impl Connection { } } } - - // fn pb(buf: &[u8]) { - // for i in 0..buf.len() { - // print!("{:02X?} ", buf[i]); - // } - // println!(""); - // } } diff --git a/src/server/mod.rs b/src/dns/mod.rs similarity index 72% rename from src/server/mod.rs rename to src/dns/mod.rs index 25076ef..6f1e59e 100644 --- a/src/server/mod.rs +++ b/src/dns/mod.rs @@ -1,3 +1,4 @@ mod binding; +pub mod packet; mod resolver; pub mod server; diff --git a/src/packet/buffer.rs b/src/dns/packet/buffer.rs similarity index 86% rename from src/packet/buffer.rs rename to src/dns/packet/buffer.rs index 4ecc605..058156e 100644 --- a/src/packet/buffer.rs +++ b/src/dns/packet/buffer.rs @@ -1,4 +1,4 @@ -use super::Result; +use crate::Result; pub struct PacketBuffer { pub buf: Vec, @@ -9,19 +9,9 @@ pub struct PacketBuffer { impl PacketBuffer { pub fn new(buf: Vec) -> Self { Self { + size: buf.len(), buf, pos: 0, - size: 0, - } - } - - fn check(&mut self, pos: usize) { - if self.size < pos { - self.size = pos; - } - - if self.buf.len() <= self.size { - self.buf.resize(self.size + 1, 0x00); } } @@ -42,32 +32,25 @@ impl PacketBuffer { } pub fn read(&mut self) -> Result { - // if self.pos >= 512 { - // error!("Tried to read past end of buffer"); - // return Err("End of buffer".into()); - // } - self.check(self.pos); + if self.pos >= self.size { + return Err("Tried to read past end of buffer".into()); + } let res = self.buf[self.pos]; self.pos += 1; - Ok(res) } pub fn get(&mut self, pos: usize) -> Result { - // if pos >= 512 { - // error!("Tried to read past end of buffer"); - // return Err("End of buffer".into()); - // } - self.check(pos); + if pos >= self.size { + return Err("Tried to read past end of buffer".into()); + } Ok(self.buf[pos]) } pub fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> { - // if start + len >= 512 { - // error!("Tried to read past end of buffer"); - // return Err("End of buffer".into()); - // } - self.check(start + len); + if start + len >= self.size { + return Err("Tried to read past end of buffer".into()); + } Ok(&self.buf[start..start + len]) } @@ -169,7 +152,13 @@ impl PacketBuffer { } pub fn write(&mut self, val: u8) -> Result<()> { - self.check(self.pos); + if self.size < self.pos { + self.size = self.pos; + } + + if self.buf.len() <= self.size { + self.buf.resize(self.size + 1, 0x00); + } self.buf[self.pos] = val; self.pos += 1; @@ -208,7 +197,9 @@ impl PacketBuffer { } } - self.write_u8(0)?; + if !qname.is_empty() { + self.write_u8(0)?; + } Ok(()) } diff --git a/src/packet/header.rs b/src/dns/packet/header.rs similarity index 97% rename from src/packet/header.rs rename to src/dns/packet/header.rs index a75f6ba..2355ecb 100644 --- a/src/packet/header.rs +++ b/src/dns/packet/header.rs @@ -1,4 +1,5 @@ -use super::{buffer::PacketBuffer, result::ResultCode, Result}; +use super::{buffer::PacketBuffer, result::ResultCode}; +use crate::Result; #[derive(Clone, Debug)] pub struct DnsHeader { diff --git a/src/packet/mod.rs b/src/dns/packet/mod.rs similarity index 97% rename from src/packet/mod.rs rename to src/dns/packet/mod.rs index 0b7cb7b..9873b94 100644 --- a/src/packet/mod.rs +++ b/src/dns/packet/mod.rs @@ -4,9 +4,7 @@ use self::{ buffer::PacketBuffer, header::DnsHeader, query::QueryType, question::DnsQuestion, record::DnsRecord, }; - -type Error = Box; -pub type Result = std::result::Result; +use crate::Result; pub mod buffer; pub mod header; diff --git a/src/packet/query.rs b/src/dns/packet/query.rs similarity index 54% rename from src/packet/query.rs rename to src/dns/packet/query.rs index cae6f09..732b9b2 100644 --- a/src/packet/query.rs +++ b/src/dns/packet/query.rs @@ -12,6 +12,8 @@ pub enum QueryType { SRV, // 33 OPT, // 41 CAA, // 257 + AR, // 1000 + AAAAR, // 1001 } impl QueryType { @@ -29,6 +31,8 @@ impl QueryType { Self::SRV => 33, Self::OPT => 41, Self::CAA => 257, + Self::AR => 1000, + Self::AAAAR => 1001, } } @@ -45,7 +49,30 @@ impl QueryType { 33 => Self::SRV, 41 => Self::OPT, 257 => Self::CAA, + 1000 => Self::AR, + 1001 => Self::AAAAR, _ => Self::UNKNOWN(num), } } + + pub fn allowed_actions(&self) -> (bool, bool) { + // 0. duplicates allowed + // 1. allowed to be created by database + match self { + QueryType::UNKNOWN(_) => (false, false), + QueryType::A => (true, true), + QueryType::NS => (false, true), + QueryType::CNAME => (false, true), + QueryType::SOA => (false, false), + QueryType::PTR => (false, true), + QueryType::MX => (false, true), + QueryType::TXT => (true, true), + QueryType::AAAA => (true, true), + QueryType::SRV => (false, true), + QueryType::OPT => (false, false), + QueryType::CAA => (false, true), + QueryType::AR => (false, true), + QueryType::AAAAR => (false, true), + } + } } diff --git a/src/packet/question.rs b/src/dns/packet/question.rs similarity index 100% rename from src/packet/question.rs rename to src/dns/packet/question.rs diff --git a/src/packet/record.rs b/src/dns/packet/record.rs similarity index 83% rename from src/packet/record.rs rename to src/dns/packet/record.rs index c29dd8f..88008f0 100644 --- a/src/packet/record.rs +++ b/src/dns/packet/record.rs @@ -1,11 +1,12 @@ use std::net::{Ipv4Addr, Ipv6Addr}; +use rand::RngCore; +use serde::{Deserialize, Serialize}; use tracing::{trace, warn}; use super::{buffer::PacketBuffer, query::QueryType, Result}; -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub enum DnsRecord { UNKNOWN { domain: String, @@ -76,10 +77,17 @@ pub enum DnsRecord { value: String, ttl: u32, }, // 257 + AR { + domain: String, + ttl: u32, + }, + AAAAR { + domain: String, + ttl: u32, + }, } impl DnsRecord { - pub fn read(buffer: &mut PacketBuffer) -> Result { let mut domain = String::new(); buffer.read_qname(&mut domain)?; @@ -90,10 +98,10 @@ impl DnsRecord { let ttl = buffer.read_u32()?; let data_len = buffer.read_u16()?; - let header_pos = buffer.pos(); - trace!("Reading DNS Record TYPE: {:?}", qtype); + let header_pos = buffer.pos(); + match qtype { QueryType::A => { let raw_addr = buffer.read_u32()?; @@ -471,6 +479,29 @@ impl DnsRecord { let size = buffer.pos() - (pos + 2); buffer.set_u16(pos, size as u16)?; } + Self::AR { ref domain, ttl } => { + buffer.write_qname(domain)?; + buffer.write_u16(QueryType::A.to_num())?; + buffer.write_u16(1)?; + buffer.write_u32(ttl)?; + buffer.write_u16(4)?; + + let mut rand = rand::thread_rng(); + buffer.write_u32(rand.next_u32())?; + } + Self::AAAAR { ref domain, ttl } => { + buffer.write_qname(domain)?; + buffer.write_u16(QueryType::A.to_num())?; + buffer.write_u16(1)?; + buffer.write_u32(ttl)?; + buffer.write_u16(4)?; + + let mut rand = rand::thread_rng(); + buffer.write_u32(rand.next_u32())?; + buffer.write_u32(rand.next_u32())?; + buffer.write_u32(rand.next_u32())?; + buffer.write_u32(rand.next_u32())?; + } Self::UNKNOWN { .. } => { warn!("Skipping record: {self:?}"); } @@ -479,20 +510,35 @@ impl DnsRecord { Ok(buffer.pos() - start_pos) } + pub fn get_domain(&self) -> String { + self.get_shared_domain().0 + } + + pub fn get_qtype(&self) -> QueryType { + self.get_shared_domain().1 + } + pub fn get_ttl(&self) -> u32 { - match *self { - DnsRecord::UNKNOWN { .. } => 0, - DnsRecord::AAAA { ttl, .. } => ttl, - DnsRecord::A { ttl, .. } => ttl, - DnsRecord::NS { ttl, .. } => ttl, - DnsRecord::CNAME { ttl, .. } => ttl, - DnsRecord::SOA { ttl, .. } => ttl, - DnsRecord::PTR { ttl, .. } => ttl, - DnsRecord::MX { ttl, .. } => ttl, - DnsRecord::TXT { ttl, .. } => ttl, - DnsRecord::SRV { ttl, .. } => ttl, - DnsRecord::CAA { ttl, .. } => ttl, + self.get_shared_domain().2 + } + + fn get_shared_domain(&self) -> (String, QueryType, u32) { + match self { + DnsRecord::UNKNOWN { + domain, ttl, qtype, .. + } => (domain.clone(), QueryType::UNKNOWN(*qtype), *ttl), + DnsRecord::AAAA { domain, ttl, .. } => (domain.clone(), QueryType::AAAA, *ttl), + DnsRecord::A { domain, ttl, .. } => (domain.clone(), QueryType::A, *ttl), + DnsRecord::NS { domain, ttl, .. } => (domain.clone(), QueryType::NS, *ttl), + DnsRecord::CNAME { domain, ttl, .. } => (domain.clone(), QueryType::CNAME, *ttl), + DnsRecord::SOA { domain, ttl, .. } => (domain.clone(), QueryType::SOA, *ttl), + DnsRecord::PTR { domain, ttl, .. } => (domain.clone(), QueryType::PTR, *ttl), + DnsRecord::MX { domain, ttl, .. } => (domain.clone(), QueryType::MX, *ttl), + DnsRecord::TXT { domain, ttl, .. } => (domain.clone(), QueryType::TXT, *ttl), + DnsRecord::SRV { domain, ttl, .. } => (domain.clone(), QueryType::SRV, *ttl), + DnsRecord::CAA { domain, ttl, .. } => (domain.clone(), QueryType::CAA, *ttl), + DnsRecord::AR { domain, ttl, .. } => (domain.clone(), QueryType::AR, *ttl), + DnsRecord::AAAAR { domain, ttl, .. } => (domain.clone(), QueryType::AAAAR, *ttl), } } - } diff --git a/src/packet/result.rs b/src/dns/packet/result.rs similarity index 100% rename from src/packet/result.rs rename to src/dns/packet/result.rs diff --git a/src/server/resolver.rs b/src/dns/resolver.rs similarity index 57% rename from src/server/resolver.rs rename to src/dns/resolver.rs index 464620c..18b5bba 100644 --- a/src/server/resolver.rs +++ b/src/dns/resolver.rs @@ -1,11 +1,7 @@ use super::binding::Connection; -use crate::{ - config::Config, - packet::{ - query::QueryType, question::DnsQuestion, result::ResultCode, Packet, - Result, - }, get_time, -}; +use super::packet::{query::QueryType, question::DnsQuestion, result::ResultCode, Packet}; +use crate::Result; +use crate::{config::Config, database::Database, get_time}; use async_recursion::async_recursion; use moka::future::Cache; use std::{net::IpAddr, sync::Arc, time::Duration}; @@ -15,6 +11,7 @@ pub struct Resolver { request_id: u16, connection: Connection, config: Arc, + database: Arc, cache: Cache, } @@ -23,18 +20,59 @@ impl Resolver { request_id: u16, connection: Connection, config: Arc, + database: Arc, cache: Cache, ) -> Self { Self { request_id, connection, config, + database, cache, } } - async fn lookup_cache(&mut self, qname: &str, qtype: QueryType) -> Option { - let question = DnsQuestion::new(qname.to_string(), qtype); + async fn lookup_database(&self, question: &DnsQuestion) -> Option { + let records = match self + .database + .get_records(&question.name, question.qtype) + .await + { + Ok(record) => record, + Err(err) => { + error!("{err}"); + return None; + } + }; + + if records.is_empty() { + return None; + } + + let mut packet = Packet::new(); + + packet.header.id = self.request_id; + packet.header.questions = 1; + packet.header.answers = records.len() as u16; + packet.header.recursion_desired = true; + packet + .questions + .push(DnsQuestion::new(question.name.to_string(), question.qtype)); + + for record in records { + packet.answers.push(record); + } + + trace!( + "Found stored value for {:?} {}", + question.qtype, + question.name + ); + + Some(packet) + } + + async fn lookup_cache(&self, question: &DnsQuestion) -> Option { let Some((packet, date)) = self.cache.get(&question) else { return None }; @@ -46,16 +84,20 @@ impl Resolver { let ttl = answer.get_ttl(); if diff > ttl { self.cache.invalidate(&question).await; - return None + return None; } } - trace!("Found cached value for {qtype:?} {qname}"); + trace!( + "Found cached value for {:?} {}", + question.qtype, + question.name + ); Some(packet) } - async fn lookup(&mut self, qname: &str, qtype: QueryType, server: (IpAddr, u16)) -> Packet { + async fn lookup_fallback(&self, question: &DnsQuestion, server: (IpAddr, u16)) -> Packet { let mut packet = Packet::new(); packet.header.id = self.request_id; @@ -63,7 +105,7 @@ impl Resolver { packet.header.recursion_desired = true; packet .questions - .push(DnsQuestion::new(qname.to_string(), qtype)); + .push(DnsQuestion::new(question.name.to_string(), question.qtype)); let packet = match self.connection.request_packet(packet, server).await { Ok(packet) => packet, @@ -78,28 +120,47 @@ impl Resolver { packet } + async fn lookup(&self, question: &DnsQuestion, server: (IpAddr, u16)) -> Packet { + if let Some(packet) = self.lookup_cache(question).await { + return packet; + }; + + if let Some(packet) = self.lookup_database(question).await { + return packet; + }; + + trace!( + "Attempting lookup of {:?} {} with ns {}", + question.qtype, + question.name, + server.0 + ); + + self.lookup_fallback(question, server).await + } + #[async_recursion] async fn recursive_lookup(&mut self, qname: &str, qtype: QueryType) -> Packet { let question = DnsQuestion::new(qname.to_string(), qtype); - let mut ns = self.config.get_fallback_ns().clone(); - - if let Some(packet) = self.lookup_cache(qname, qtype).await { return packet } + let mut ns = self.config.dns_fallback.clone(); loop { - trace!("Attempting lookup of {qtype:?} {qname} with ns {ns}"); - let ns_copy = ns; let server = (ns_copy, 53); - let response = self.lookup(qname, qtype, server).await; + let response = self.lookup(&question, server).await; if !response.answers.is_empty() && response.header.rescode == ResultCode::NOERROR { - self.cache.insert(question, (response.clone(), get_time())).await; + self.cache + .insert(question, (response.clone(), get_time())) + .await; return response; } if response.header.rescode == ResultCode::NXDOMAIN { - self.cache.insert(question, (response.clone(), get_time())).await; + self.cache + .insert(question, (response.clone(), get_time())) + .await; return response; } @@ -111,9 +172,11 @@ impl Resolver { let new_ns_name = match response.get_unresolved_ns(qname) { Some(x) => x, None => { - self.cache.insert(question, (response.clone(), get_time())).await; - return response - }, + self.cache + .insert(question, (response.clone(), get_time())) + .await; + return response; + } }; let recursive_response = self.recursive_lookup(new_ns_name, QueryType::A).await; @@ -121,7 +184,9 @@ impl Resolver { if let Some(new_ns) = recursive_response.get_random_a() { ns = new_ns; } else { - self.cache.insert(question, (response.clone(), get_time())).await; + self.cache + .insert(question, (response.clone(), get_time())) + .await; return response; } } diff --git a/src/dns/server.rs b/src/dns/server.rs new file mode 100644 index 0000000..65d15df --- /dev/null +++ b/src/dns/server.rs @@ -0,0 +1,85 @@ +use super::{ + binding::Binding, + packet::{question::DnsQuestion, Packet}, + resolver::Resolver, +}; +use crate::{config::Config, database::Database, Result}; +use moka::future::Cache; +use std::{net::SocketAddr, sync::Arc, time::Duration}; +use tokio::task::JoinHandle; +use tracing::{error, info}; + +pub struct DnsServer { + addr: SocketAddr, + config: Arc, + database: Arc, + cache: Cache, +} + +impl DnsServer { + pub async fn new(config: Config, database: Database) -> Result { + let addr = format!("[::]:{}", config.dns_port).parse::()?; + let cache = Cache::builder() + .time_to_live(Duration::from_secs(60 * 60)) + .max_capacity(config.dns_cache_size) + .build(); + + info!("Created DNS cache with size of {}", config.dns_cache_size); + + Ok(Self { + addr, + config: Arc::new(config), + database: Arc::new(database), + cache, + }) + } + + pub async fn run(&self) -> Result<(JoinHandle<()>, JoinHandle<()>)> { + let tcp = Binding::tcp(self.addr).await?; + let tcp_handle = self.listen(tcp); + + let udp = Binding::udp(self.addr).await?; + let udp_handle = self.listen(udp); + + info!( + "Fallback DNS Server is set to: {:?}", + self.config.dns_fallback + ); + info!( + "Listening for TCP and UDP traffic on [::]:{}", + self.config.dns_port + ); + + Ok((udp_handle, tcp_handle)) + } + + fn listen(&self, mut binding: Binding) -> JoinHandle<()> { + let config = self.config.clone(); + let database = self.database.clone(); + let cache = self.cache.clone(); + tokio::spawn(async move { + let mut id = 0; + loop { + let Ok(connection) = binding.connect().await else { continue }; + info!("Received request on {}", binding.name()); + + let resolver = Resolver::new( + id, + connection, + config.clone(), + database.clone(), + cache.clone(), + ); + + let name = binding.name().to_string(); + tokio::spawn(async move { + if let Err(err) = resolver.handle_query().await { + error!("{} request {} failed: {:?}", name, id, err); + }; + }); + + id += 1; + } + }) + } +} diff --git a/src/main.rs b/src/main.rs index c891d50..679e87b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,34 @@ -use std::{time::{UNIX_EPOCH, SystemTime}, env, net::IpAddr}; +use std::time::{SystemTime, UNIX_EPOCH}; use config::Config; -use server::server::Server; -use tracing::metadata::LevelFilter; +use database::Database; +use dotenv::dotenv; +use dns::server::DnsServer; +use tracing::{error, metadata::LevelFilter}; use tracing_subscriber::{ filter::filter_fn, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, }; +use web::WebServer; mod config; -mod packet; -mod server; +mod database; +mod dns; +mod web; + +type Error = Box; +pub type Result = std::result::Result; #[tokio::main] async fn main() { + if let Err(err) = run().await { + error!("{err}") + }; +} + +async fn run() -> Result<()> { + dotenv().ok(); + tracing_subscriber::registry() .with( tracing_subscriber::fmt::layer() @@ -24,19 +39,20 @@ async fn main() { ) .init(); - let mut config = Config::new(); + let config = Config::new(); + let database = Database::new(config.clone()).await?; - if let Ok(port) = env::var("PORT").unwrap_or(String::new()).parse::() { - config.set_port(port); - } + let dns_server = DnsServer::new(config.clone(), database.clone()).await?; + let (udp, tcp) = dns_server.run().await?; - if let Ok(fallback) = env::var("FALLBACK_DNS").unwrap_or(String::new()).parse::() { - config.set_fallback_ns(&fallback); - } + let web_server = WebServer::new(config, database).await?; + let web = web_server.run().await?; - let server = Server::new(config).await.expect("Failed to bind server"); + tokio::join!(udp).0?; + tokio::join!(tcp).0?; + tokio::join!(web).0?; - server.run().await.unwrap(); + Ok(()) } pub fn get_time() -> u64 { diff --git a/src/server/server.rs b/src/server/server.rs deleted file mode 100644 index e006bb1..0000000 --- a/src/server/server.rs +++ /dev/null @@ -1,73 +0,0 @@ -use moka::future::Cache; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; -use tokio::task::JoinHandle; -use tracing::{error, info}; - -use crate::config::Config; -use crate::packet::question::DnsQuestion; -use crate::packet::{Result, Packet}; - -use super::binding::Binding; -use super::resolver::Resolver; - -pub struct Server { - addr: SocketAddr, - config: Arc, - cache: Cache, -} - -impl Server { - pub async fn new(config: Config) -> Result { - let addr = format!("[::]:{}", config.get_port()).parse::()?; - let cache = Cache::builder() - .time_to_live(Duration::from_secs(60 * 60)) - .max_capacity(1_000) - .build(); - Ok(Self { - addr, - config: Arc::new(config), - cache, - }) - } - - pub async fn run(&self) -> Result<()> { - let tcp = Binding::tcp(self.addr).await?; - let tcp_handle = self.listen(tcp); - - let udp = Binding::udp(self.addr).await?; - let udp_handle = self.listen(udp); - - info!("Fallback DNS Server is set to: {:?}", self.config.get_fallback_ns()); - info!("Listening for TCP and UDP traffic on [::]:{}", self.config.get_port()); - - tokio::join!(tcp_handle) - .0 - .expect("Failed to join tcp thread"); - tokio::join!(udp_handle) - .0 - .expect("Failed to join udp thread"); - Ok(()) - } - - fn listen(&self, mut binding: Binding) -> JoinHandle<()> { - let config = self.config.clone(); - let cache = self.cache.clone(); - tokio::spawn(async move { - let mut id = 0; - loop { - let Ok(connection) = binding.connect().await else { continue }; - info!("Received request on {}", binding.name()); - - let resolver = Resolver::new(id, connection, config.clone(), cache.clone()); - - if let Err(err) = resolver.handle_query().await { - error!("{} request {} failed: {:?}", binding.name(), id, err); - }; - - id += 1; - } - }) - } -} diff --git a/src/web/api.rs b/src/web/api.rs new file mode 100644 index 0000000..1fddb5f --- /dev/null +++ b/src/web/api.rs @@ -0,0 +1,156 @@ +use std::net::IpAddr; + +use axum::{ + extract::Query, + response::Response, + routing::{get, post, put, delete}, + Extension, Router, +}; +use moka::future::Cache; +use rand::distributions::{Alphanumeric, DistString}; +use serde::Deserialize; +use tower_cookies::{Cookie, Cookies}; + +use crate::{config::Config, database::Database, dns::packet::record::DnsRecord}; + +use super::{ + extract::{Authorized, Body, RequestIp}, + http::{json, text}, +}; + +pub fn router() -> Router { + Router::new() + .route("/login", post(login)) + .route("/domains", get(list_domains)) + .route("/domains", delete(delete_domain)) + .route("/records", get(get_domain)) + .route("/records", put(add_record)) +} + +async fn list_domains(_: Authorized, Extension(database): Extension) -> Response { + let domains = match database.get_domains().await { + Ok(domains) => domains, + Err(err) => return text(500, &format!("{err}")), + }; + + let Ok(domains) = serde_json::to_string(&domains) else { + return text(500, "Failed to fetch domains") + }; + + json(200, &domains) +} + +#[derive(Deserialize)] +struct DomainRequest { + domain: String, +} + +async fn get_domain( + _: Authorized, + Extension(database): Extension, + Query(query): Query, +) -> Response { + let records = match database.get_domain(&query.domain).await { + Ok(records) => records, + Err(err) => return text(500, &format!("{err}")), + }; + + let Ok(records) = serde_json::to_string(&records) else { + return text(500, "Failed to fetch records") + }; + + json(200, &records) +} + +async fn delete_domain( + _: Authorized, + Extension(database): Extension, + Body(body): Body, +) -> Response { + + let Ok(request) = serde_json::from_str::(&body) else { + return text(400, "Missing request parameters") + }; + + let Ok(domains) = database.get_domains().await else { + return text(500, "Failed to delete domain") + }; + + if !domains.contains(&request.domain) { + return text(400, "Domain does not exist") + } + + if database.delete_domain(request.domain).await.is_err() { + return text(500, "Failed to delete domain") + }; + + return text(204, "Successfully deleted domain") +} + +async fn add_record( + _: Authorized, + Extension(database): Extension, + Body(body): Body, +) -> Response { + let Ok(record) = serde_json::from_str::(&body) else { + return text(400, "Invalid DNS record") + }; + + let allowed = record.get_qtype().allowed_actions(); + if !allowed.1 { + return text(400, "Not allowed to create record") + } + + let Ok(records) = database.get_records(&record.get_domain(), record.get_qtype()).await else { + return text(500, "Failed to complete record check"); + }; + + if !records.is_empty() && !allowed.0 { + return text(400, "Not allowed to create duplicate record") + }; + + if records.contains(&record) { + return text(400, "Not allowed to create duplicate record") + } + + if let Err(err) = database.add_record(record).await { + return text(500, &format!("{err}")); + } + + return text(201, "Added record to database successfully"); +} + +#[derive(Deserialize)] +struct LoginRequest { + user: String, + pass: String, +} + +async fn login( + Extension(config): Extension, + Extension(cache): Extension>, + RequestIp(ip): RequestIp, + cookies: Cookies, + Body(body): Body, +) -> Response { + let Ok(request) = serde_json::from_str::(&body) else { + return text(400, "Missing request parameters") + }; + + if request.user != config.web_user || request.pass != config.web_pass { + return text(400, "Invalid credentials"); + }; + + let token = Alphanumeric.sample_string(&mut rand::thread_rng(), 128); + + cache.insert(token.clone(), ip).await; + + let mut cookie = Cookie::new("auth", token); + cookie.set_secure(true); + cookie.set_http_only(true); + cookie.set_path("/"); + + cookies.add(cookie); + + text(200, "Successfully logged in") +} diff --git a/src/web/extract.rs b/src/web/extract.rs new file mode 100644 index 0000000..4b6cd7c --- /dev/null +++ b/src/web/extract.rs @@ -0,0 +1,139 @@ +use std::{ + io::Read, + net::{IpAddr, SocketAddr}, +}; + +use axum::{ + async_trait, + body::HttpBody, + extract::{ConnectInfo, FromRequest, FromRequestParts}, + http::{request::Parts, Request}, + response::Response, + BoxError, +}; +use bytes::Bytes; +use moka::future::Cache; +use tower_cookies::Cookies; + +use super::http::text; + +pub struct Authorized; + +#[async_trait] +impl FromRequestParts for Authorized +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let Ok(Some(cookies)) = Option::::from_request_parts(parts, state).await else { + return Err(text(403, "No cookies provided")) + }; + + let Some(token) = cookies.get("auth") else { + return Err(text(403, "No auth token provided")) + }; + + let auth_ip: IpAddr; + { + let Some(cache) = parts.extensions.get::>() else { + return Err(text(500, "Failed to load auth store")) + }; + + let Some(ip) = cache.get(token.value()) else { + return Err(text(401, "Unauthorized")) + }; + + auth_ip = ip + } + + let Ok(Some(RequestIp(ip))) = Option::::from_request_parts(parts, state).await else { + return Err(text(403, "You have no ip")) + }; + + if auth_ip != ip { + return Err(text(403, "Auth token does not match current ip")); + } + + Ok(Self) + } +} + +pub struct RequestIp(pub IpAddr); + +#[async_trait] +impl FromRequestParts for RequestIp +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let headers = &parts.headers; + + let forwardedfor = headers + .get("x-forwarded-for") + .and_then(|h| h.to_str().ok()) + .and_then(|h| { + h.split(',') + .rev() + .find_map(|s| s.trim().parse::().ok()) + }); + + if let Some(forwardedfor) = forwardedfor { + return Ok(Self(forwardedfor)); + } + + let realip = headers + .get("x-real-ip") + .and_then(|hv| hv.to_str().ok()) + .and_then(|s| s.parse::().ok()); + + if let Some(realip) = realip { + return Ok(Self(realip)); + } + + let realip = headers + .get("x-real-ip") + .and_then(|hv| hv.to_str().ok()) + .and_then(|s| s.parse::().ok()); + + if let Some(realip) = realip { + return Ok(Self(realip)); + } + + let info = parts.extensions.get::>(); + + if let Some(info) = info { + return Ok(Self(info.0.ip())); + } + + Err(text(403, "You have no ip")) + } +} + +pub struct Body(pub String); + +#[async_trait] +impl FromRequest for Body +where + B: HttpBody + Sync + Send + 'static, + B::Data: Send, + B::Error: Into, + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request(req: Request, state: &S) -> Result { + let Ok(bytes) = Bytes::from_request(req, state).await else { + return Err(text(413, "Payload too large")); + }; + + let Ok(body) = String::from_utf8(bytes.bytes().flatten().collect()) else { + return Err(text(400, "Invalid utf8 body")) + }; + + Ok(Self(body)) + } +} diff --git a/src/web/file.rs b/src/web/file.rs new file mode 100644 index 0000000..73ecdc9 --- /dev/null +++ b/src/web/file.rs @@ -0,0 +1,31 @@ +use axum::{extract::Path, response::Response}; + +use super::http::serve; + +pub async fn js(Path(path): Path) -> Response { + let path = format!("/js/{path}"); + serve(&path).await +} + +pub async fn css(Path(path): Path) -> Response { + let path = format!("/css/{path}"); + serve(&path).await +} + +pub async fn fonts(Path(path): Path) -> Response { + let path = format!("/fonts/{path}"); + serve(&path).await +} + +pub async fn image(Path(path): Path) -> Response { + let path = format!("/image/{path}"); + serve(&path).await +} + +pub async fn favicon() -> Response { + serve("/favicon.ico").await +} + +pub async fn robots() -> Response { + serve("/robots.txt").await +} diff --git a/src/web/http.rs b/src/web/http.rs new file mode 100644 index 0000000..7ab1b11 --- /dev/null +++ b/src/web/http.rs @@ -0,0 +1,50 @@ +use axum::{ + body::Body, + http::{header::HeaderName, HeaderValue, Request, StatusCode}, + response::{IntoResponse, Response}, +}; +use std::str; +use tower::ServiceExt; +use tower_http::services::ServeFile; + +pub fn text(code: u16, msg: &str) -> Response { + (status_code(code), msg.to_owned()).into_response() +} + +pub fn json(code: u16, json: &str) -> Response { + let mut res = (status_code(code), json.to_owned()).into_response(); + res.headers_mut().insert( + HeaderName::from_static("content-type"), + HeaderValue::from_static("application/json"), + ); + res +} + +pub async fn serve(path: &str) -> Response { + if !path.chars().any(|c| c == '.') { + return text(403, "Invalid file path"); + } + + let path = format!("public{path}"); + let file = ServeFile::new(path); + + let Ok(mut res) = file.oneshot(Request::new(Body::empty())).await else { + tracing::error!("Error while fetching file"); + return text(500, "Error when fetching file") + }; + + if res.status() != StatusCode::OK { + return text(404, "File not found"); + } + + res.headers_mut().insert( + HeaderName::from_static("cache-control"), + HeaderValue::from_static("max-age=300"), + ); + + res.into_response() +} + +fn status_code(code: u16) -> StatusCode { + StatusCode::from_u16(code).map_or(StatusCode::OK, |code| code) +} diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100644 index 0000000..530a3f9 --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,82 @@ +use std::net::{IpAddr, SocketAddr, TcpListener}; +use std::time::Duration; + +use axum::routing::get; +use axum::{Extension, Router}; +use moka::future::Cache; +use tokio::task::JoinHandle; +use tower_cookies::CookieManagerLayer; +use tracing::{error, info}; + +use crate::config::Config; +use crate::database::Database; +use crate::Result; + +mod api; +mod extract; +mod file; +mod http; +mod pages; + +pub struct WebServer { + config: Config, + database: Database, + addr: SocketAddr, +} + +impl WebServer { + pub async fn new(config: Config, database: Database) -> Result { + let addr = format!("[::]:{}", config.web_port).parse::()?; + Ok(Self { + config, + database, + addr, + }) + } + + pub async fn run(&self) -> Result> { + let config = self.config.clone(); + let database = self.database.clone(); + let listener = TcpListener::bind(self.addr)?; + + info!( + "Listening for HTTP traffic on [::]:{}", + self.config.web_port + ); + + let app = Self::router(config, database); + let server = axum::Server::from_tcp(listener)?; + + let web_handle = tokio::spawn(async move { + if let Err(err) = server + .serve(app.into_make_service_with_connect_info::()) + .await + { + error!("{err}"); + } + }); + + Ok(web_handle) + } + + fn router(config: Config, database: Database) -> Router { + let cache: Cache = Cache::builder() + .time_to_live(Duration::from_secs(60 * 15)) + .max_capacity(config.dns_cache_size) + .build(); + + Router::new() + .nest("/", pages::router()) + .nest("/api", api::router()) + .layer(Extension(config)) + .layer(Extension(cache)) + .layer(Extension(database)) + .layer(CookieManagerLayer::new()) + .route("/js/*path", get(file::js)) + .route("/css/*path", get(file::css)) + .route("/fonts/*path", get(file::fonts)) + .route("/image/*path", get(file::image)) + .route("/favicon.ico", get(file::favicon)) + .route("/robots.txt", get(file::robots)) + } +} diff --git a/src/web/pages.rs b/src/web/pages.rs new file mode 100644 index 0000000..a8605ef --- /dev/null +++ b/src/web/pages.rs @@ -0,0 +1,31 @@ +use axum::{response::Response, routing::get, Router}; + +use super::{extract::Authorized, http::serve}; + +pub fn router() -> Router { + Router::new() + .route("/", get(root)) + .route("/login", get(login)) + .route("/home", get(home)) + .route("/domain", get(domain)) +} + +async fn root(user: Option) -> Response { + if user.is_some() { + home().await + } else { + login().await + } +} + +async fn login() -> Response { + serve("/login.html").await +} + +async fn home() -> Response { + serve("/home.html").await +} + +async fn domain() -> Response { + serve("/domain.html").await +}