diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f7d1e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +dst/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..61b90d5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,848 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", + "humansize", + "num-traits", + "percent-encoding", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "inferium" +version = "0.1.0" +dependencies = [ + "proc", + "tokio", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.170" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.10", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc" +version = "0.1.0" + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot 0.12.3", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zmp24" +version = "0.1.0" +dependencies = [ + "askama", + "bincode", + "chrono", + "inferium", + "serde", + "serde_json", + "sled", + "tokio", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5d33f07 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "zmp24" +version = "0.1.0" +edition = "2021" + +[profile.release] +strip = true +lto = true + +[dependencies] +askama = "0.12.1" +bincode = "1.3.3" +chrono = "0.4.40" +inferium = { path = "./lib/inferium", features = ["async", "tokio-net", "tokio-unixsocks"] } +serde = { version = "1.0.218", features = ["derive"] } +serde_json = "1.0.140" +sled = "0.34.7" +tokio = { version = "1.43.0", features = ["full"] } + +[features] +dev = [] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1fa7253 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +.PHONY: build +build: \ + target/release/zmp24 + +ARCH := $(shell uname -m) + +MAIN_RS_SRCS := $(shell find src -type f -regex '^.*\.rs$$') Cargo.toml \ + client/dst/index.html client/dst/script.js client/dst/style.css \ + config.json + +SEARCH_REPLACE := lib/search_and_replace/target/release/search_and_replace + +.PHONY: run +run: $(MAIN_RS_SRCS) + DB_PATH="$$(jq -r .dev.db config.json)" \ + BIND_TO="$$(jq -r .dev.bind_to config.json)" \ + cargo run --features dev + +.PHONY: clean +clean: client_clean + cargo clean + cd lib/search_and_replace && cargo clean + rm -rf dst + +dst: + mkdir dst + +include client/client.mk +include image/image.mk + +target/release/zmp24: $(MAIN_RS_SRCS) + DB_PATH="$$(jq -r .prod.db config.json)" \ + BIND_TO="$$(jq -r .prod.sock_path config.json)" \ + cargo build --release + +target/$(ARCH)-unknown-linux-musl/release/zmp24: $(MAIN_RS_SRCS) + DB_PATH="$$(jq -r .prod.db config.json)" \ + BIND_TO="$$(jq -r .prod.sock_path config.json)" \ + cargo build --target $(ARCH)-unknown-linux-musl --release + +$(SEARCH_REPLACE): lib/search_and_replace/src/main.rs lib/search_and_replace/Cargo.toml + cd lib/search_and_replace && cargo build --release diff --git a/askama.toml b/askama.toml new file mode 100644 index 0000000..d305604 --- /dev/null +++ b/askama.toml @@ -0,0 +1,2 @@ +[general] +dirs = ["client/dst/"] diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..84b126b --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dst/ diff --git a/client/bun.lockb b/client/bun.lockb new file mode 100755 index 0000000..03769ee Binary files /dev/null and b/client/bun.lockb differ diff --git a/client/client.mk b/client/client.mk new file mode 100644 index 0000000..101ee82 --- /dev/null +++ b/client/client.mk @@ -0,0 +1,33 @@ +client/node_modules: + bun install --cwd client + +client/dst: + mkdir client/dst + +client/dst/%.html: \ + client/src/%.html \ + client/node_modules \ + client/dst \ + $(SEARCH_REPLACE) + cat $< | bun run --cwd client html-minifier \ + --collapse-inline-tag-whitespace \ + --collapse-boolean-attributes \ + --collapse-whitespace \ + --remove-attribute-quotes \ + --remove-comments \ + --remove-redundant-attributes | \ + $(SEARCH_REPLACE) \ + '##LT##' '<' \ + '##GT##' '>' \ + > $@ + +client/dst/%.js: client/src/%.ts client/node_modules client/dst + bun build $< --minify --outfile $@ + +client/dst/%.css: client/src/%.scss client/node_modules client/dst + cat $< | bun run --cwd client sass --stdin --style compressed > $@ + +.PHONY: client_clean +client_clean: + rm -rf client/dst + rm -rf client/node_modules diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..439bbe8 --- /dev/null +++ b/client/package.json @@ -0,0 +1,13 @@ +{ + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "html-minifier": "^4.0.0", + "sass": "^1.85.1" + } +} diff --git a/client/src/index.html b/client/src/index.html new file mode 100644 index 0000000..69dcdef --- /dev/null +++ b/client/src/index.html @@ -0,0 +1,31 @@ + + + + + + Test page + + + +

This is a test page

+

zmp24 rizzin 2b tru σ pookie fr gooner skibidi

+

This page was visited {{ visit_count }} times before you showed up.

+

+ Here is {{ visit_count }} dot{% if visit_count != 1 %}s{% endif %}: + + + {% if visit_count ##LT## 100 %} + {% for _ in 0..visit_count %} + . + {% endfor %} + {% else %} + That's too much dots to display. So f u. + {% endif %} +

+ + + diff --git a/client/src/script.ts b/client/src/script.ts new file mode 100644 index 0000000..eee6aac --- /dev/null +++ b/client/src/script.ts @@ -0,0 +1,16 @@ +type Sigma = string; +type ToMew = "Σ" | Sigma; + +async function skibidi(): Promise { + return "fr fr"; +} + +async function boomer(): Promise { + return new Promise(() => {}); +} + +async function alpha(): Promise { + console.log(await Promise.any([skibidi(), boomer()]) + " nocap"); +} + +alpha(); diff --git a/client/src/style.scss b/client/src/style.scss new file mode 100644 index 0000000..ba54a04 --- /dev/null +++ b/client/src/style.scss @@ -0,0 +1,10 @@ +:root { + color-scheme: light dark; +} + +$color-me-daddy: light-dark(black, white); + +body { + font-family: Arial, Helvetica, sans-serif; + color: $color-me-daddy; +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..400b2c6 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": false, + "noEmit": true, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..b191dfc --- /dev/null +++ b/config.json @@ -0,0 +1,16 @@ +{ + "prod": { + "sock_path": "/run/zmp24.sock", + "db": "/data/db" + }, + + "dev": { + "bind_to": "[::1]:8080", + "db": "/tmp/zmp24db" + }, + + "image": { + "name": "zmp24", + "version": "0.1" + } +} diff --git a/image/image.dockerfile b/image/image.dockerfile new file mode 100644 index 0000000..7908204 --- /dev/null +++ b/image/image.dockerfile @@ -0,0 +1,7 @@ +FROM docker.io/alpine:latest + +COPY ./zmp24 /usr/bin/zmp24 +COPY ./run.sh /run.sh +RUN apk update && apk add gcompat && mkdir /data + +CMD [ "/run.sh" ] diff --git a/image/image.mk b/image/image.mk new file mode 100644 index 0000000..e8c302b --- /dev/null +++ b/image/image.mk @@ -0,0 +1,17 @@ +.PHONY: image +image: dst/image/zmp24 dst/image/Containerfile dst/image/run.sh config.json + cd dst/image && podman build . -t \ + "$$(jq -r .image.name ../../config.json):$$(jq -r .image.version ../../config.json)" + +dst/image: dst + mkdir dst/image + +dst/image/Containerfile: dst/image image/image.dockerfile + ln -sf $$(pwd)/image/image.dockerfile dst/image/Containerfile + +dst/image/run.sh: dst/image image/run.sh + ln -f image/run.sh dst/image/run.sh + chmod +x dst/image/run.sh + +dst/image/zmp24: dst/image target/$(ARCH)-unknown-linux-musl/release/zmp24 + ln -f target/$(ARCH)-unknown-linux-musl/release/zmp24 dst/image/zmp24 diff --git a/image/run.sh b/image/run.sh new file mode 100755 index 0000000..0215ca9 --- /dev/null +++ b/image/run.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +_term() { + kill -TERM "$server_pid" 2>/dev/null +} + +trap _term SIGTERM +/usr/bin/zmp24 & +server_pid=$! +wait "$server_pid" diff --git a/lib/inferium/.gitignore b/lib/inferium/.gitignore deleted file mode 100644 index a5ff07f..0000000 --- a/lib/inferium/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -/target - - -# Added by cargo -# -# already existing elements were commented out - -#/target diff --git a/lib/inferium/benches/client.rs b/lib/inferium/benches/client.rs deleted file mode 100644 index 95ff914..0000000 --- a/lib/inferium/benches/client.rs +++ /dev/null @@ -1,60 +0,0 @@ -#![feature(test)] -extern crate test; -use std::collections::HashMap; - -use test::Bencher; - -extern crate inferium; -use inferium::h1::{SyncClient, Response, ResponseHead, ProtocolVariant}; -use inferium::{Status, HeaderValue}; -use inferium::TestSyncStream; - -fn parse_response_sync_inner() { - let src = "HTTP/1.1 200 OK\r\nserver: inferium\r\n\r\n".as_bytes().to_vec(); - let stream = TestSyncStream::<4>::new(&src); - let mut client = SyncClient::>::new(stream); - let target = Response::HeadersOnly(ResponseHead::new( - Status::Ok, - ProtocolVariant::HTTP1_1, - HashMap::from([ - ("server".into(), HeaderValue::new(vec!["inferium".to_string()])) - ]) - )); - assert_eq!(client.receive_response().unwrap(), target); -} - -fn parse_response_sync_inner_body() { - let mut src = "HTTP/1.1 200 OK\r\ncontent-length: 50\r\n\r\n".as_bytes().to_vec(); - src.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - let stream = TestSyncStream::<4>::new(&src); - let mut client = SyncClient::>::new(stream); - let target_head = ResponseHead::new( - Status::Ok, - ProtocolVariant::HTTP1_1, - HashMap::from([ - ("content-length".into(), HeaderValue::new(vec!["50".to_string()])) - ]) - ); - let Response::WithSizedBody((h, mut b)) = client.receive_response().unwrap() else { - panic!(); - }; - let b = b.recv_all().unwrap(); - assert_eq!(h, target_head); - assert_eq!(b, b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); -} - -#[bench] -fn parse_response_sync(b: &mut Bencher) { - b.bytes = 37; - b.iter(|| { - parse_response_sync_inner(); - }); -} - -#[bench] -fn parse_response_sync_with_body(b: &mut Bencher) { - b.bytes = 39 + 13; - b.iter(|| { - parse_response_sync_inner_body(); - }); -} diff --git a/lib/inferium/benches/status_code_parsing.rs b/lib/inferium/benches/status_code_parsing.rs deleted file mode 100644 index 5d0fc18..0000000 --- a/lib/inferium/benches/status_code_parsing.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![feature(test)] -extern crate test; -use test::Bencher; - -extern crate inferium; -use inferium::Status; - -#[bench] -fn baseline(b: &mut Bencher) { - fn status_test_from_slice(raw: &[u8]) -> Result { - match raw { - b"200" => Ok(Status::Ok), - b"404" => Ok(Status::NotFound), - _ => Err(()), - } - } - - b.iter(|| { - assert_eq!(status_test_from_slice(b"200".as_slice()), Ok(Status::Ok)); - }); -} - -#[bench] -fn valid_ok(b: &mut Bencher) { - b.iter(|| { - assert_eq!(Status::try_from(b"200".as_slice()), Ok(Status::Ok)); - }); - -} - -#[bench] -fn valid_internal_server_error(b: &mut Bencher) { - b.iter(|| { - assert_eq!(Status::try_from(b"500".as_slice()), Ok(Status::InternalServerError)); - }); -} - -#[bench] -fn invalid(b: &mut Bencher) { - b.iter(|| { - assert!(Status::try_from(b"690".as_slice()).is_err()); - }) -} diff --git a/lib/inferium/examples/going_async.rs b/lib/inferium/examples/going_async.rs deleted file mode 100644 index 946b6e6..0000000 --- a/lib/inferium/examples/going_async.rs +++ /dev/null @@ -1,94 +0,0 @@ -// This is an async port of `examples/simple_server.rs`. Please see that example first. -// -// Features `async` and `tokio-net` must be enabled for this example to compile. -// It is also possible to enable feature `full` (which will enable all the features). - -use std::{collections::HashMap, net::SocketAddr}; -use tokio::net::{TcpListener, TcpStream}; -use inferium::{ - h1::{ProtocolVariant, Request, ResponseHead, ServerSendError, AsyncServer}, - HeaderKey, Method, Status, TokioInet -}; - -#[tokio::main] -async fn main() { - let listener = TcpListener::bind("localhost:8080").await.unwrap(); - loop { - let (conn, addr) = listener.accept().await.unwrap(); - // Here we are creating a new asynchronous task for every client. - // This will fork off in an asynchronous manner and won't block our accept loop. - tokio::task::spawn(async move { - // We created this new async block, so we need to `.await` on this function to propagate - // the future from the function to the top of the spawned task (this async block). - handle_client(conn, addr).await; - }); - // You can now handle multiple clients at once... congratulations. - } -} - -async fn handle_client(conn: TcpStream, addr: SocketAddr) { - println!("connection from {addr:?}"); - let mut server_handler = AsyncServer::::new(TokioInet::new(conn)); - // When receiving or sending - we call the same functions with `.await` appended (in an async - // context). This will automatically poll the returned future from the top of the context. - // The polling is handled by tokio here - so we don't need to worry about it. - while let Ok(request) = server_handler.receive_request().await { - match handle_request(request, addr) { - Ok((h, b)) => if let Err(_) = send_response(h, b, &mut server_handler).await { break; }, - Err(()) => break, - } - }; - println!("ended connection for {addr:?}"); -} - -fn handle_request( - req: Request, addr: SocketAddr -) -> Result<(ResponseHead, &'static [u8]), ()> { - let Request::HeadersOnly(headers) = req else { - return Err(()); - }; - - println!("req from {addr:?}: {headers}"); - - const OK_RESPONSE: &[u8] = b" - - -

Hello, world!

-

Hello from inferium.

- -"; - const NOT_FOUND_RESPONSE: &[u8] = b" - - -

Not found

-

This page was not found

- -"; - - Ok(match (headers.method(), headers.uri().path()) { - (&Method::GET, "/") => (ResponseHead::new( - Status::Ok, - ProtocolVariant::HTTP1_0, - HashMap::from([ - (HeaderKey::SERVER, "inferium".parse().unwrap()), - (HeaderKey::CONTENT_LENGTH, OK_RESPONSE.len().into()), - ]) - ), OK_RESPONSE), - - _ => (ResponseHead::new( - Status::NotFound, - ProtocolVariant::HTTP1_0, - HashMap::from([ - (HeaderKey::SERVER, "inferium".parse().unwrap()), - (HeaderKey::CONTENT_LENGTH, NOT_FOUND_RESPONSE.len().into()), - ]) - ), NOT_FOUND_RESPONSE), - }) -} - -async fn send_response( - response: ResponseHead, body: &[u8], conn: &mut AsyncServer -)-> Result<(), ServerSendError> { - conn.send_response(&response).await?; - conn.send_body_bytes(body).await.map_err(|e| e.try_into().unwrap()) -} diff --git a/lib/inferium/examples/https_client.rs b/lib/inferium/examples/https_client.rs deleted file mode 100644 index 7d6224c..0000000 --- a/lib/inferium/examples/https_client.rs +++ /dev/null @@ -1,88 +0,0 @@ -// This is a port of a client from `examples/start_here.rs`. Please see that example first. -// Also... maybe brush up on some async tasks since we are going to need them here (there is an -// example on async with inferium in `examples/going_async.rs`). -// -// Features `async`, `tokio-net` and `webpki-roots` dependency must be enabled for this example to -// compile. We recommend enabling the `dev` feature when running this example. - -use std::{collections::HashMap, sync::Arc}; -use tokio::net::TcpStream; -use tokio_rustls::{ - rustls::{ - pki_types::ServerName, - ClientConfig, - RootCertStore - }, - TlsConnector, - TlsStream -}; -use inferium::{ - h1::{ - ProtocolVariant, - RequestHead, - Response, - AsyncClient - }, - HeaderKey, - Method, - TokioRustls -}; - -async fn run_tls_handshake(raw_stream: TcpStream) -> TlsStream { - let mut root_certs = RootCertStore::empty(); - root_certs.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let config = ClientConfig::builder() - .with_root_certificates(root_certs) - .with_no_client_auth(); - let connector = TlsConnector::from(Arc::new(config)); - let verify_server_name = ServerName::try_from("zumepro.cz").unwrap(); - TlsStream::Client(connector.connect(verify_server_name, raw_stream).await.unwrap()) -} - -#[tokio::main] -async fn main() { - let stream = TcpStream::connect("zumepro.cz:443").await.unwrap(); - let stream = run_tls_handshake(stream).await; - let conn = TokioRustls::new(stream); - let mut client = AsyncClient::::new(conn); - - let to_send = RequestHead::new( - Method::GET, "/".parse().unwrap(), ProtocolVariant::HTTP1_1, - HashMap::from([ - (HeaderKey::USER_AGENT, "Mozilla/5.0 (inferium)".parse().unwrap()), - (HeaderKey::HOST, "zumepro.cz".parse().unwrap()), - (HeaderKey::CONNECTION, "close".parse().unwrap()) - ]) - ); - println!("----------> Sending\n\n{to_send}\n"); - client.send_request(&to_send).await.unwrap(); - let response = client.receive_response().await.unwrap(); - - let (header, body) = match response { - Response::HeadersOnly(h) => (h, None), - Response::WithSizedBody((_, _)) => panic!(), - Response::WithChunkedBody((h, b)) => (h, Some(b)), - }; - - println!("----------< Received\n\n{header}\n"); - - if let Some(mut body) = body { - - // Since our zumepro server sends bodies chunked - we will need to handle it. - // This simple loop just collects all the chunks into the res vector. - let mut res = Vec::new(); - while let Some(mut chunk) = body.get_chunk_async().await.unwrap() { - // Now here is a difference between sync and async. - // - // For easy body manipulation and no redundant trait pollution, a body within an - // asynchronous stream can be sent/received using the same methods as a synchronous one, - // but with the suffix `_async`. - res.append(&mut chunk.recv_all_async().await.unwrap()); - } - - println!( - "----------< Body\n\n{:?}\n", - std::str::from_utf8(&res).unwrap() - ); - } -} diff --git a/lib/inferium/examples/simple_server.rs b/lib/inferium/examples/simple_server.rs deleted file mode 100644 index ddbb821..0000000 --- a/lib/inferium/examples/simple_server.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::{collections::HashMap, net::{SocketAddr, TcpListener}}; -use inferium::{ - h1::{ProtocolVariant, Request, ResponseHead, ServerSendError, SyncServer}, - HeaderKey, Method, Status, StdInet -}; - -fn main() { - let listener = TcpListener::bind("localhost:8080").unwrap(); - loop { - let (conn, addr) = listener.accept().unwrap(); - println!("connection from {addr:?}"); - let mut server_handler = SyncServer::::new(StdInet::new(conn)); - // We'll serve the client as long as it sends valid requests. - // Note that this will effectively block other clients. - while let Ok(request) = server_handler.receive_request() { - // This matching is here to provide a way of controlling the while loop. - match handle_request(request, addr) { - Ok((h, b)) => if let Err(_) = send_response(h, b, &mut server_handler) { break; }, - Err(()) => break, - } - }; - println!("ended connection for {addr:?}"); - } -} - -fn handle_request( - req: Request, addr: SocketAddr -) -> Result<(ResponseHead, &'static [u8]), ()> { - let Request::HeadersOnly(headers) = req else { - // We will not handle POST requests with bodies - so let's tell the client to f*ck off. - return Err(()); - }; - - println!("req from {addr:?}: {headers}"); - - const OK_RESPONSE: &[u8] = b" - - -

Hello, world!

-

Hello from inferium.

- -"; - const NOT_FOUND_RESPONSE: &[u8] = b" - - -

Not found

-

This page was not found

- -"; - - // The URI can contain both path and parameters - so we're just getting the path here. - Ok(match (headers.method(), headers.uri().path()) { - // The ok response with our index page - (&Method::GET, "/") => (ResponseHead::new( - Status::Ok, - ProtocolVariant::HTTP1_0, - HashMap::from([ - (HeaderKey::SERVER, "inferium".parse().unwrap()), - (HeaderKey::CONTENT_LENGTH, OK_RESPONSE.len().into()), - ]) - ), OK_RESPONSE), - - // The not found response with an example not found page - _ => (ResponseHead::new( - Status::NotFound, - ProtocolVariant::HTTP1_0, - HashMap::from([ - (HeaderKey::SERVER, "inferium".parse().unwrap()), - (HeaderKey::CONTENT_LENGTH, NOT_FOUND_RESPONSE.len().into()), - ]) - ), NOT_FOUND_RESPONSE), - }) -} - -fn send_response( - response: ResponseHead, body: &[u8], conn: &mut SyncServer -)-> Result<(), ServerSendError> { - conn.send_response(&response)?; - // The send body can fail on an I/O error or if the content-length header does not match the - // actual sent length in this scenario. But we know that we have the correct length so with - // `.try_into().unwrap()` we tell inferium to convert the error and panic on (not so much) - // possible body length discrepancy. - conn.send_body_bytes(body).map_err(|e| e.try_into().unwrap()) -} diff --git a/lib/inferium/examples/start_here.rs b/lib/inferium/examples/start_here.rs deleted file mode 100644 index 2ce5dd2..0000000 --- a/lib/inferium/examples/start_here.rs +++ /dev/null @@ -1,101 +0,0 @@ -// Hello, and welcome to inferium. A performance-oriented small HTTP library written in Rust that -// keeps you (the user) in charge. - -// Let's first import some necessary things. - -// In inferium - HashMaps are used to store uri parameters and headers. -use std::collections::HashMap; -// TcpStream is needed if we want to connect to the internet. -use std::net::TcpStream; - -use inferium::{ - // The h1 module contains all the protocol specific things for HTTP/1.(0/1). - h1::{ - // ProtocolVariant contains variants with the protocol versions supported in this module: - // - HTTP/1.1 - // - HTTP/1.0 - ProtocolVariant, - // RequestHead contains headers and the HTTP request headline (method, path, protocol). - RequestHead, - // Response here is a wrapper for a response that can have the following: - // - Headers only - // - Headers and body (with a known length or chunked) - // - // ! The body in the response is not yet collected. It is up to you if you wish to discard - // the connection or receive and collect the response body into some structure. - // - // The same things here go for the Request object which is nearly the same except that the - // headline contains protocol and status instead. - Response, - // Sync client is a stream wrapper that helps us keep track of the open connection and - // perform request/response operations. - // - // The server equivalent is SyncServer. - SyncClient - }, - // Header key contains various known header keys, but can also store arbitrary (unknown) header - // key in the OTHER variant. - HeaderKey, - Method, - // StdInet here is a stream wrapper that allows the TcpStream to be used by inferium. There is - // also a unix socket equivalent and some asynchronous io wrappers. - StdInet -}; - -fn main() { - // Let's first create a connection... nothing weird here. - let conn = StdInet::new(TcpStream::connect("zumepro.cz:80").unwrap()); - // And a client... - let mut client = SyncClient::::new(conn); - - // Now let's create a request to send - let to_send = RequestHead::new( - // The path here is parsed into an HTTP path object (which also supports parameters) - // I'm using HTTP/1.0 in this example as HTTP/1.1 automatically infers a compatibility with - // chunked encoding (which I'm not even trying to handle here). - Method::GET, "/".parse().unwrap(), ProtocolVariant::HTTP1_0, - HashMap::from([ - // All headers are HeaderKey - HeaderValue pairs. We can parse the header value into - // the desired object. - // - // Constructing arbitrary header key is supported using the OTHER variant - however - // it's not recommended as a violation of the HTTP protocol can happen. - // - // If you really want to construct an arbitrary header key - please carefully check - // that all of the symbols are valid. - (HeaderKey::USER_AGENT, "Mozilla/5.0 (inferium)".parse().unwrap()), - (HeaderKey::HOST, "zumepro.cz".parse().unwrap()), - ]) - ); - println!("----------> Sending\n\n{to_send}\n"); - // Let's send the request - this is pretty straightforward. - client.send_request(&to_send).unwrap(); - // As is receiving a response. - let response = client.receive_response().unwrap(); - - // Now (as we discussed earlier) - the response can have a body. - // In this example we'll try to handle a basic body with a known size. - let (header, body) = match response { - // Extracting the headers if no body is present. - Response::HeadersOnly(h) => (h, None), - // Extracting both the headers and the body if body is present. - Response::WithSizedBody((h, b)) => (h, Some(b)), - // We will not handle chunked responses in this example. - Response::WithChunkedBody((_, _)) => panic!(), - }; - - // inferium kindly provides a simple way to print the head of a request/response. - // It will be formatted pretty close to the actual protocol plaintext representation. - println!("----------< Received\n\n{header}\n"); - - // And finally... if we have a body, we'll print it. - if let Some(mut body) = body { - println!( - "----------< Body\n\n{:?}\n", - // A body is always returned in bytes. It's up to you to decode it however you see fit. - std::str::from_utf8(&mut body.recv_all().unwrap()).unwrap() - ); - } - - // And you're done. Come on... try to run it. -} diff --git a/lib/inferium/src/headers.rs b/lib/inferium/src/headers.rs index a19ad85..ad50873 100644 --- a/lib/inferium/src/headers.rs +++ b/lib/inferium/src/headers.rs @@ -158,6 +158,14 @@ impl HeaderValue { self.inner.push(val); } + /// Create a new unchecked value. + /// This is faster, but can cause protocol violation. + /// Please avoid putting unchecked content here. + #[inline] + pub fn new_unchecked(inner: String) -> Self { + Self { inner: vec![inner] } + } + /// Query the first entry for this header key. /// /// See the documentation of [`HeaderValue`] for more information. diff --git a/lib/inferium/src/lib.rs b/lib/inferium/src/lib.rs index b6c8930..31ef4db 100644 --- a/lib/inferium/src/lib.rs +++ b/lib/inferium/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + pub mod settings; mod io; diff --git a/lib/inferium/src/proto/h1/exports.rs b/lib/inferium/src/proto/h1/exports.rs index 6afb61c..3dbc4ba 100644 --- a/lib/inferium/src/proto/h1/exports.rs +++ b/lib/inferium/src/proto/h1/exports.rs @@ -322,6 +322,28 @@ pub enum Request<'a, T> { WithChunkedBody((RequestHead, Incoming<'a, ChunkedIn<'a, PrependableStream>>)), } +impl Response<'_, T> { + #[inline] + pub fn head(&self) -> &ResponseHead { + match self { + Self::HeadersOnly(h) => h, + Self::WithSizedBody((h, _)) => h, + Self::WithChunkedBody((h, _)) => h, + } + } +} + +impl Request<'_, T> { + #[inline] + pub fn head(&self) -> &RequestHead { + match self { + Self::HeadersOnly(h) => h, + Self::WithSizedBody((h, _)) => h, + Self::WithChunkedBody((h, _)) => h, + } + } +} + fn get_outgoing_req_content_length(head: &RequestHead) -> Result, ClientSendError> { let Some(l) = head.headers.get(&HeaderKey::CONTENT_LENGTH) else { return Ok(None); diff --git a/lib/inferium/proc/.gitignore b/lib/search_and_replace/.gitignore similarity index 100% rename from lib/inferium/proc/.gitignore rename to lib/search_and_replace/.gitignore diff --git a/lib/search_and_replace/Cargo.lock b/lib/search_and_replace/Cargo.lock new file mode 100644 index 0000000..81a53e1 --- /dev/null +++ b/lib/search_and_replace/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "search_and_replace" +version = "0.1.0" diff --git a/lib/search_and_replace/Cargo.toml b/lib/search_and_replace/Cargo.toml new file mode 100644 index 0000000..0076d58 --- /dev/null +++ b/lib/search_and_replace/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "search_and_replace" +version = "0.1.0" +edition = "2021" + +[profile.release] +strip = true +lto = true + +[dependencies] diff --git a/lib/search_and_replace/src/main.rs b/lib/search_and_replace/src/main.rs new file mode 100644 index 0000000..871df42 --- /dev/null +++ b/lib/search_and_replace/src/main.rs @@ -0,0 +1,386 @@ +use std::{env::args, io::Read}; + +#[derive(Debug, PartialEq)] +struct JumpTable { + jumps: Vec, +} + +impl JumpTable { + /// Returns the pointer for the next char (if this char is matched). + fn resolve_char( + pat: &Vec, + ptr: &usize, + cur_char: &char, + ) -> usize { + if pat.get(*ptr).unwrap() == cur_char { + return *ptr + 1; + } + if pat.get(0).unwrap() == cur_char { + return 1; + } + return 0; + } + + pub fn new(pat: &Vec) -> Self { + if pat.len() == 0 { + return Self { jumps: vec![] }; + } + let mut ptr = 0_usize; + let mut jumps = vec![0]; + for cur_char in pat.iter().skip(1) { + ptr = JumpTable::resolve_char(pat, &ptr, cur_char); + jumps.push(ptr); + } + + Self { jumps } + } + + pub fn search(&self, pat: &Vec, needle: &char, ptr: &usize) -> usize { + let mut ptr = *ptr; + while ptr != 0 { + if pat.get(ptr).unwrap() == needle { + break; + } + ptr = *self.jumps.get(ptr.saturating_sub(1)).unwrap(); + } + ptr + } +} + +#[derive(Debug, PartialEq)] +struct Pattern { + pat: Vec, + jt: JumpTable, + ptr: usize, +} + +impl Pattern { + pub fn new(pat: String) -> Result { + if pat.len() == 0 { + return Err(()); + } + let pat = pat.chars().collect::>(); + let jt = JumpTable::new(&pat); + Ok(Self { + pat, + jt, + ptr: 0, + }) + } + + pub fn search_and_update(&mut self, cur_char: &char) -> bool { + if self.pat.get(self.ptr).unwrap() == cur_char { + self.ptr += 1; + return if self.ptr == self.pat.len() { true } else { false }; + } + let found = self.jt.search(&self.pat, &cur_char, &self.ptr); + self.ptr = found; + false + } + + pub fn reset_ptr(&mut self) { + self.ptr = 0; + } + + pub fn len(&self) -> usize { + self.pat.len() + } +} + +#[derive(Debug, PartialEq, Eq)] +struct Match { + pub pat_id: usize, + pub start_pos: usize, + pub len: usize, +} + +impl Match { + pub fn new(pat_id: usize, start_pos: usize, len: usize) -> Self { + Self { pat_id, start_pos, len } + } +} + +/// [`MPSearch`] assumes that each [`Match`] will be replaced later. So this will match the first +/// pattern only and reset all the pointers (including the matched one) to zero. +#[derive(Debug)] +struct MPSearch { + pats: Vec, +} + +impl MPSearch { + /// The [`Pattern`]s will be searched in the order as they are in the [`Vec`]. + pub fn new(pats: Vec) -> Self { + Self { pats } + } + + /// Reset pointers of all patterns + fn reset_all_ptrs(&mut self) { + for pat in self.pats.iter_mut() { + pat.reset_ptr(); + } + } + + /// Returns true if ANY pattern ended a match at this pos + /// + /// This will also clear all ptrs on a match + fn search_at_pos(&mut self, cur_char: &char, pos: &usize) -> Option { + let mut cur_match = None; + for (idx, pat) in self.pats.iter_mut().enumerate() { + if pat.search_and_update(cur_char) { + cur_match = Some(Match::new(idx, pos.saturating_sub(pat.len() - 1), pat.len())); + break; + } + } + if let Some(_) = cur_match { + self.reset_all_ptrs(); + } + cur_match + } + + /// Perform a search on the haystack - returning the ordered [`Match`]es. + pub fn search(&mut self, haystack: &String) -> Vec { + let mut res = Vec::new(); + for (cur_idx, cur_char) in haystack.chars().enumerate() { + if let Some(cur_match) = self.search_at_pos(&cur_char, &cur_idx) { + res.push(cur_match); + } + } + res + } +} + +macro_rules! skip_n { + ($iter: ident, $n: expr) => { + for _ in 0..$n { + $iter.next().ok_or(())?; + } + } +} + +macro_rules! append_n { + ($iter: ident, $target: ident, $n: expr) => { + for _ in 0..$n { + $target.push($iter.next().ok_or(())?); + } + } +} + + +/// Replace the given [`Match`]es with the replacements. The replacements need to be in the order +/// of the search patterns as when [`MPSearch`] was constructed. +#[derive(Debug)] +struct MPReplace { + replacements: Vec, + matches: Vec, +} + +impl MPReplace { + /// The replacement must be given in the same order as the patterns were on the corresponding + /// [`MPSearch`] construction. + pub fn new(matches: Vec, replacements: Vec) -> Self { + Self { replacements, matches } + } + + /// # Errors + /// This can fail if the [`Match`]es are invalid relevant to the given target. + pub fn replace(&self, target: &String) -> Result { + let mut ptr = 0; + let mut iter = target.chars(); + let mut res = String::new(); + for cur_match in self.matches.iter() { + append_n!(iter, res, cur_match.start_pos - ptr); + skip_n!(iter, cur_match.len); + res.push_str(self.replacements.get(cur_match.pat_id).ok_or(())?); + ptr = cur_match.start_pos + cur_match.len; + } + append_n!(iter, res, target.chars().count() - ptr); + Ok(res) + } +} + +struct PatMatchArgs> { + inner: I, +} + +impl> PatMatchArgs { + pub fn new(inner: I, count: usize) -> Result { + if count % 2 != 0 { + return Err(()); + } + Ok(Self { inner }) + } +} + +impl> Iterator for PatMatchArgs { + type Item = (String, String); + + fn next(&mut self) -> Option { + Some((self.inner.next()?, self.inner.next()?)) + } +} + +fn main() { + let mut contents = Vec::new(); + std::io::stdin().lock().read_to_end(&mut contents).unwrap(); + let contents = std::str::from_utf8(&contents).unwrap().to_string(); + + let args = match PatMatchArgs::new(args().skip(1), args().len() - 1) { + Ok(val) => val, + Err(()) => { + eprintln!("the number of arguments after filepath must be divisible by two"); + std::process::exit(1); + } + }; + let mut pats = Vec::new(); + let mut replacements = Vec::new(); + for (cur_pat, cur_replacement) in args { + let Ok(cur_pat) = Pattern::new(cur_pat) else { + eprintln!("the patterns can't be empty"); + std::process::exit(1); + }; + pats.push(cur_pat); + replacements.push(cur_replacement); + } + + let matches = MPSearch::new(pats).search(&contents); + let replaced = MPReplace::new(matches, replacements) + .replace(&contents).unwrap(); + println!("{}", replaced); +} + +#[cfg(test)] +mod test { + use crate::{JumpTable, MPReplace, MPSearch, Match, Pattern}; + + #[test] + fn jumps_01() { + let src = String::from("thisthen").chars().collect::>(); + let target = vec![0_usize, 0, 0, 0, 1, 2, 0, 0]; + let jt = JumpTable::new(&src); + assert_eq!(jt.jumps, target); + } + + #[test] + fn jumps_02() { + let src = String::from("tthis").chars().collect::>(); + let target = vec![0, 1, 0, 0, 0]; + let jt = JumpTable::new(&src); + assert_eq!(jt.jumps, target); + } + + #[test] + fn jumps_03() { + let src = String::from("t").chars().collect::>(); + let target = vec![0]; + let jt = JumpTable::new(&src); + assert_eq!(jt.jumps, target); + } + + #[test] + fn jumps_04() { + let src = String::from("tt").chars().collect::>(); + let target = vec![0, 1]; + let jt = JumpTable::new(&src); + assert_eq!(jt.jumps, target); + } + + #[test] + fn jumps_05() { + let src = String::from("").chars().collect::>(); + let target = vec![]; + let jt = JumpTable::new(&src); + assert_eq!(jt.jumps, target); + } + + #[test] + fn search_01() { + let pat = String::from("tthis").chars().collect::>(); + let jt = JumpTable::new(&pat); + assert_eq!(jt.search(&pat, &'t', &1), 1); + } + + #[test] + fn search_02() { + let pat = String::from("testtesa").chars().collect::>(); + let jt = JumpTable::new(&pat); + assert_eq!(jt.search(&pat, &'t', &7), 3); + } + + #[test] + fn search_03() { + let pat = String::from("ahojahoj").chars().collect::>(); + let jt = JumpTable::new(&pat); + assert_eq!(jt.search(&pat, &'j', &7), 7); + } + + #[test] + fn search_04() { + let pat = String::from("ahojahoj").chars().collect::>(); + let jt = JumpTable::new(&pat); + assert_eq!(jt.search(&pat, &'j', &7), 7); + } + + #[test] + fn search_05() { + let pat = String::from("jojojojojojoja").chars().collect::>(); + let jt = JumpTable::new(&pat); + assert_eq!(jt.search(&pat, &'o', &13), 11); + } + + #[test] + fn search_and_update_01() { + let pat = String::from("test"); + let mut pat = Pattern::new(pat).unwrap(); + let haystack = String::from("thisisatest"); + for cur_haystack in haystack.chars().take(haystack.len() - 1) { + assert_eq!(pat.search_and_update(&cur_haystack), false); + println!("{:?}", pat); + } + println!("{:?}", pat); + assert_eq!(pat.search_and_update(&'t'), true); + } + + #[test] + fn empty_pattern() { + assert_eq!(Pattern::new("".chars().collect()), Err(())); + } + + #[test] + fn mpsearch_01() { + let mut mpsearch = MPSearch::new( + vec!["this", "is", "a", "test"] + .iter() + .map(|p| Pattern::new(p.to_string()).unwrap()) + .collect() + ); + println!("{:?}", mpsearch); + let haystack = String::from("this is a test"); + let target = vec![ + Match::new(0, 0, 4), + Match::new(1, 5, 2), + Match::new(2, 8, 1), + Match::new(3, 10, 4) + ]; + assert_eq!(mpsearch.search(&haystack), target); + } + + #[test] + fn mpreplace_01() { + let mut mpsearch = MPSearch::new( + vec!["this", "is", "a", "test"] + .iter() + .map(|p| Pattern::new(p.to_string()).unwrap()) + .collect() + ); + let haystack = String::from("this-is.a*test///"); + let matches = mpsearch.search(&haystack); + let mpreplace = MPReplace::new(matches, vec![ + "that".to_string(), + "isn't".to_string(), + "the".to_string(), + "final".to_string(), + ]); + let replaced = mpreplace.replace(&haystack).unwrap(); + assert_eq!(replaced, "that-isn't.the*final///"); + } +} diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..ade4854 --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,25 @@ +#[macro_export] +macro_rules! logger { + (@inner $fixed: literal, $msg: literal$(, $($arg: expr),*)?) => { + println!(concat!( + $fixed, $msg + ), Utc::now().to_rfc3339_opts(SecondsFormat::Secs, false)$(, $($arg),*)?); + }; + + (debug $msg: literal $(, $($arg: expr),*$(,)?)?) => { + #[cfg(feature = "dev")] + logger!(@inner "\x1b[38;5;244m[{}] \x1b[38;5;159mDEBUG \x1b[39m", $msg $(,$($arg),*)?); + }; + + (info $msg: literal $(, $($arg: expr),*$(,)?)?) => { + logger!(@inner "\x1b[38;5;244m[{}] \x1b[38;5;148mINFO \x1b[39m", $msg $(,$($arg),*)?); + }; + + (warn $msg: literal $(, $($arg: expr),*$(,)?)?) => { + logger!(@inner "\x1b[38;5;244m[{}] \x1b[38;5;214mWARNING \x1b[39m", $msg $(,$($arg),*)?); + }; + + (err $msg: literal $(, $($arg: expr),*$(,)?)?) => { + logger!(@inner "\x1b[38;5;244m[{}] \x1b[38;5;196mERROR \x1b[39m", $msg $(,$($arg),*)?); + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cc1fd0c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,152 @@ +use std::sync::Arc; +use chrono::{SecondsFormat, Utc}; +#[cfg(feature = "dev")] +use { + inferium::TokioInet, + tokio::net::TcpListener, + std::net::SocketAddr, +}; +#[cfg(not(feature = "dev"))] +use { + inferium::TokioUnix, + tokio::net::UnixListener, + tokio::net::unix::SocketAddr, +}; +use inferium::h1::AsyncServer; +use tokio::sync::mpsc::{self, Sender}; +use std::sync::atomic::AtomicU32; + +mod mime_types; +mod response_builder; +mod router; +mod logger; + +async fn client_handler( + #[cfg(feature = "dev")] + mut stream: AsyncServer, + #[cfg(not(feature = "dev"))] + mut stream: AsyncServer, + #[cfg(feature = "dev")] + addr: SocketAddr, + #[cfg(not(feature = "dev"))] + _addr: SocketAddr, + tx: Sender<()>, + state: Arc, +) { + logger!(debug "new connection from {:?}", addr); + loop { + // Receive request + let Ok(req) = stream.receive_request().await else { + logger!(debug "invalid request from {:?}", addr); + break; + }; + + // Handling logic + let res = match router::handle_request(req, &tx, &state).await { + Ok(v) => v, + #[cfg(feature = "dev")] + Err(e) => { logger!(err "internal server error: {:?}", e); break; } + #[cfg(not(feature = "dev"))] + Err(_) => { break; }, + }; + + // Send response + match stream.send_response(&res.0).await { + Ok(()) => {}, + #[cfg(feature = "dev")] + Err(e) => { logger!(debug "err when sending response {:?}", e); break; }, + #[cfg(not(feature = "dev"))] + Err(_) => { break; }, + } + let Some(body) = res.1 else { + continue; + }; + match stream.send_body_bytes(&body).await { + Ok(()) => {}, + #[cfg(feature = "dev")] + Err(e) => { logger!(debug "err when sending response body {:?}", e); break; }, + #[cfg(not(feature = "dev"))] + Err(_) => { break; }, + } + } + logger!(debug "end connection to {:?}", addr); +} + +async fn server_handler( + #[cfg(feature = "dev")] + listener: TcpListener, + #[cfg(not(feature = "dev"))] + listener: UnixListener, + tx: Sender<()>, + state: Arc, +) { + loop { + let Ok((conn, addr)) = listener.accept().await else { + continue; + }; + let tx = tx.clone(); + let state = state.clone(); + tokio::task::spawn(async move { + #[cfg(feature = "dev")] + client_handler(AsyncServer::::new(TokioInet::new(conn)), addr, tx, state).await; + #[cfg(not(feature = "dev"))] + client_handler(AsyncServer::::new(TokioUnix::new(conn)), addr, tx, state).await; + }); + } +} + +pub struct State { + visit_count: AtomicU32, + db: sled::Db, +} + +impl State { + pub fn new(db_path: &str) -> Self { + Self { + visit_count: 0.into(), + db: sled::open(db_path).expect("could not open the database") + } + } +} + +/// Is used to cleanup old (non-active) unix socket before binding. +/// +/// # Caution +/// This function WILL panic if any I/O operation fails. +#[cfg(not(feature = "dev"))] +#[inline(always)] +fn delete_if_exists(filepath: &str) { + if std::fs::exists(filepath) + .expect(&format!("could not get info for filepath {:?}", filepath)) { + std::fs::remove_file(filepath) + .expect(&format!("could not delete existing socket {:?}", filepath)); + } +} + +#[tokio::main(flavor = "multi_thread", worker_threads = 4)] +async fn main() { + const BIND_TO: &str = std::env!("BIND_TO"); + const DB_PATH: &str = std::env!("DB_PATH"); + + + logger!(debug "binding to {:?}", BIND_TO); + #[cfg(not(feature = "dev"))] + delete_if_exists(BIND_TO); + #[cfg(not(feature = "dev"))] + let listener = UnixListener::bind(BIND_TO) + .expect(&format!("could not bind to {:?}", BIND_TO)); + #[cfg(feature = "dev")] + let listener = TcpListener::bind(BIND_TO).await + .expect(&format!("could not bind to {:?}", BIND_TO)); + + let (tx, mut rx) = mpsc::channel::<()>(1); + let state = Arc::new(State::new(DB_PATH)); + + logger!(info "server running on {:?}", BIND_TO); + tokio::select!( + _ = server_handler(listener, tx, state) => {} + _ = rx.recv() => {}, + ); + + logger!(info "graceful shutdown"); +} diff --git a/src/mime_types.rs b/src/mime_types.rs new file mode 100644 index 0000000..0c51ecb --- /dev/null +++ b/src/mime_types.rs @@ -0,0 +1,20 @@ +macro_rules! mimes { +($($ident: ident = $value: literal),*$(,)?) => { + pub enum MimeType { + $($ident),* + } + + impl MimeType { + pub fn text(&self) -> &'static str { + match self { $(Self::$ident => $value),* } + } + } +}; +} + +mimes! { + Txt = "text/plain", + Html = "text/html", + Css = "text/css", + Js = "text/javascript", +} diff --git a/src/response_builder.rs b/src/response_builder.rs new file mode 100644 index 0000000..2537f47 --- /dev/null +++ b/src/response_builder.rs @@ -0,0 +1,8 @@ +#[macro_export] +macro_rules! response { +($proto: ident $status: ident, [$($hkey: ident : $hval: literal),*$(,)?]) => { + ResponseHead::new(Status::$status, ProtocolVariant::$proto, HashMap::from([ + $((HeaderKey::$hkey, HeaderValue::new_unchecked($hval))),* + ])) +}; +} diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..6258ed9 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,127 @@ +use std::{collections::HashMap, sync::{atomic::Ordering, Arc}}; +use askama::Template; +#[cfg(feature = "dev")] +use inferium::TokioInet; +#[cfg(not(feature = "dev"))] +use inferium::TokioUnix; +use inferium::{ + h1::{Request, ResponseHead, ProtocolVariant}, + Method, Status, HeaderKey, HeaderValue +}; +use tokio::sync::mpsc::Sender; +use chrono::{Utc, SecondsFormat}; +use crate::{logger, mime_types::MimeType, State}; + +macro_rules! response { +(@add_default_headers $($entry: tt),*$(,)?) => {[ + (HeaderKey::SERVER, HeaderValue::new_unchecked("inferium".to_string())), + ( + HeaderKey::OTHER("x-powered-by".to_string()), + HeaderValue::new_unchecked("zumepro".to_string()) + ), + $($entry),* +]}; + +(@log $head: ident, $status: ident) => { + logger!( + info "[{}] {} {:?} {}", + Status::$status, + $head.method(), + $head.uri().to_string(), + $head.proto(), + ); +}; + +( + $head: ident, + $proto: ident $status: ident, + [$($hkey: ident : $hval: expr),*$(,)?], + file $static_filename: literal $ctype: ident +) => {{ + response!(@log $head, $status); + const BODY: &[u8] = include_bytes!($static_filename); + (ResponseHead::new(Status::$status, ProtocolVariant::$proto, HashMap::from( + response!(@add_default_headers + (HeaderKey::CONTENT_LENGTH, BODY.len().into()), + (HeaderKey::CONTENT_TYPE, HeaderValue::new_unchecked(MimeType::$ctype.text().to_string())), + $((HeaderKey::$hkey, HeaderValue::new_unchecked($hval.to_string()))),* + ))), Some(BODY.to_vec())) +}}; + +( + $head: ident, + $proto: ident $status: ident, + [$($hkey: ident : $hval: expr),*$(,)?], + $body: expr +) => {{ + response!(@log $head, $status); + (ResponseHead::new(Status::$status, ProtocolVariant::$proto, HashMap::from( + response!(@add_default_headers + (HeaderKey::CONTENT_LENGTH, $body.len().into()), + $((HeaderKey::$hkey, HeaderValue::new_unchecked($hval.to_string()))),* + ))), Some($body)) +}}; + +($head: ident, $proto: ident $status: ident, [$($hkey: ident : $hval: expr),*$(,)?]) => {{ + response!(@log $head, $status); + (ResponseHead::new(Status::$status, ProtocolVariant::$proto, HashMap::from( + response!(@add_default_headers + $((HeaderKey::$hkey, HeaderValue::new_unchecked($hval.to_string()))),* + ))), None) +}}; +} + +#[derive(Debug)] +pub enum InternalServerError { + Templating(askama::Error), + Generic(String), +} + +impl From for InternalServerError { + fn from(value: askama::Error) -> Self { + Self::Templating(value) + } +} + +/// Generate a response for the given request +/// +/// # Returns +/// - Http head to send. +/// - Optionally a body (note that this must be preceeded by putting `content-length` header in the +/// response head). +pub async fn handle_request<'a>( + #[cfg(feature = "dev")] + req: Request<'_, TokioInet>, + #[cfg(not(feature = "dev"))] + req: Request<'_, TokioUnix>, + _tx: &'_ Sender<()>, + state: &Arc, +) -> Result<(ResponseHead, Option>), InternalServerError> { + let head = req.head(); + Ok(match (head.method(), head.uri().path()) { + + (&Method::GET, "/") => { + let counter = state.visit_count.fetch_add(1, Ordering::Relaxed); + let response_body = Homepage { visit_count: counter }.render()?.into_bytes(); + response!(head, HTTP1_1 Ok, [CONTENT_TYPE: MimeType::Html.text()], response_body) + }, + + (&Method::GET, "/style.css") => response!( + head, HTTP1_1 Ok, [], file "../client/dst/style.css" Css + ), + + (&Method::GET, "/script.js") => response!( + head, HTTP1_1 Ok, [], file "../client/dst/script.js" Js + ), + + _ => response!(head, HTTP1_1 NotFound, [ + CONTENT_TYPE: MimeType::Txt.text(), + ], b"Not Found. :(".to_vec()), + }) +} + +#[derive(Template)] +#[template(path = "index.html")] +struct Homepage { + visit_count: u32, +}