diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index bac22546..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: test -on: - merge_group: - pull_request: - push: - branches: - - main - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Setup Bats - uses: mig4/setup-bats@v1 - - - name: Checkout Source Code - uses: actions/checkout@v3 - - - name: Build for Release - uses: actions-rs/cargo@v1 - with: - command: build - args: --release --locked - - - name: Run E2E Tests - env: - BIN: ./target/release/http-server - run: | - bats tests/e2e - - # - name: Run Unit Tests - # run: cargo test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 20556c12..e471ea71 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ Thumbs.db # Tests # ######### dhat-heap.json + +# Web # +####### +dist diff --git a/Cargo.lock b/Cargo.lock index 449893c6..fc55de5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -33,16 +42,53 @@ dependencies = [ ] [[package]] -name = "anes" -version = "0.1.6" +name = "anstream" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] [[package]] name = "anyhow" @@ -51,43 +97,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] -name = "async-stream" -version = "0.3.5" +name = "async-recursion" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", + "proc-macro2", + "quote", + "syn 2.0.82", ] [[package]] -name = "async-stream-impl" -version = "0.3.5" +name = "async-trait" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.82", ] [[package]] -name = "async-trait" -version = "0.1.74" +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attribute-derive" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1ee502851995027b06f99f5ffbeffa1406b38d0b318a1ebfa469332c6cbafd" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn 2.0.82", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "3601467f634cfe36c4780ca9c75dea9a5b34529c1f2810676a337e7e0997f954" dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils 0.8.0", "proc-macro2", "quote", - "syn 2.0.29", + "quote-use", + "syn 2.0.82", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" @@ -99,7 +170,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide 0.5.1", + "miniz_oxide", "object", "rustc-demangle", ] @@ -112,9 +183,18 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64" -version = "0.21.2" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] [[package]] name = "bitflags" @@ -130,60 +210,45 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "block-buffer" -version = "0.7.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "block-padding", - "byte-tools", - "byteorder", "generic-array", ] -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - [[package]] name = "bumpalo" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" - -[[package]] -name = "byte-tools" -version = "0.3.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.1.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] -name = "cast" -version = "0.3.0" +name = "camino" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" [[package]] name = "cc" -version = "1.0.79" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -193,9 +258,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -208,9 +273,9 @@ dependencies = [ [[package]] name = "ciborium" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -219,15 +284,15 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", @@ -235,176 +300,204 @@ dependencies = [ [[package]] name = "clap" -version = "2.34.0" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ - "bitflags 1.3.2", - "textwrap", - "unicode-width", + "clap_builder", + "clap_derive", ] [[package]] -name = "clap" -version = "4.3.23" +name = "clap_builder" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03aef18ddf7d879c15ce20f04826ef8418101c7e528014c3eeea13321047dca3" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ - "clap_builder", + "anstream", + "anstyle", + "clap_lex", + "strsim", ] [[package]] -name = "clap_builder" -version = "4.3.23" +name = "clap_derive" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ce6fffb678c9b80a70b6b6de0aad31df727623a70fd9a842c30cd573e2fa98" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ - "anstyle", - "clap_lex", + "heck", + "proc-macro2", + "quote", + "syn 2.0.82", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] -name = "core-foundation" -version = "0.9.3" +name = "collection_literals" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "config" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ - "core-foundation-sys", - "libc", + "convert_case", + "lazy_static", + "nom", + "pathdiff", + "serde", + "toml", ] [[package]] -name = "core-foundation-sys" -version = "0.8.3" +name = "console_error_panic_hook" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] [[package]] -name = "crc32fast" -version = "1.3.2" +name = "const_format" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" dependencies = [ - "cfg-if", + "const_format_proc_macros", ] [[package]] -name = "criterion" -version = "0.5.1" +name = "const_format_proc_macros" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" dependencies = [ - "anes", - "cast", - "ciborium", - "clap 4.3.23", - "criterion-plot", - "futures", - "is-terminal", - "itertools", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "tokio", - "walkdir", + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] -name = "criterion-plot" -version = "0.5.0" +name = "convert_case" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" dependencies = [ - "cast", - "itertools", + "unicode-segmentation", ] [[package]] -name = "crossbeam-channel" -version = "0.5.4" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "cfg-if", - "crossbeam-utils", + "core-foundation-sys", + "libc", ] [[package]] -name = "crossbeam-deque" -version = "0.8.1" +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", + "libc", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.8" +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", + "generic-array", + "typenum", ] [[package]] -name = "crossbeam-utils" -version = "0.8.8" +name = "dashmap" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "lazy_static", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] -name = "dhat" -version = "0.2.4" +name = "derive-where" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42c9f82890824583b2cfc03d524616ff7119d27b74bd1e74799c122d509df288" +checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ - "backtrace", - "lazy_static", - "rustc-hash", - "serde", - "serde_json", - "thousands", + "proc-macro2", + "quote", + "syn 2.0.82", ] [[package]] name = "digest" -version = "0.8.1" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "generic-array", + "block-buffer", + "crypto-common", ] +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + [[package]] name = "either" -version = "1.6.1" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] [[package]] name = "equivalent" @@ -414,39 +507,84 @@ checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" [[package]] name = "errno" -version = "0.3.2" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ - "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "fastrand" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "file-explorer" +version = "0.0.0" dependencies = [ - "cc", - "libc", + "anyhow", + "async-trait", + "bytes", + "chrono", + "file-explorer-core", + "file-explorer-proto", + "file-explorer-ui", + "futures", + "http 1.1.0", + "http-body-util", + "http-server-plugin", + "humansize", + "hyper", + "mime_guess", + "multer", + "percent-encoding", + "rust-embed", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", ] [[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +name = "file-explorer-core" +version = "0.0.0" +dependencies = [ + "anyhow", + "chrono", + "mime_guess", + "tokio", +] [[package]] -name = "flate2" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +name = "file-explorer-proto" +version = "0.0.0" +dependencies = [ + "chrono", + "serde", +] + +[[package]] +name = "file-explorer-ui" +version = "0.0.0" dependencies = [ - "crc32fast", - "miniz_oxide 0.7.1", + "anyhow", + "chrono", + "file-explorer-proto", + "gloo", + "gloo-file", + "leptos", + "leptos_meta", + "leptos_router", + "reqwest", + "rust-embed", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", ] [[package]] @@ -455,11 +593,35 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -472,9 +634,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -482,15 +644,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -499,38 +661,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.82", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -546,11 +708,25 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.12.4" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", ] [[package]] @@ -560,60 +736,285 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" [[package]] -name = "half" -version = "1.8.2" +name = "gloo" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net 0.5.0", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils", + "gloo-worker", +] [[package]] -name = "handlebars" -version = "4.3.7" +name = "gloo-console" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c3372087601b532857d332f5957cbae686da52bb7810bf038c3e3c3cc2fa0d" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" dependencies = [ - "log", - "pest", - "pest_derive", + "gloo-utils", + "js-sys", "serde", - "serde_json", - "thiserror", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "hashbrown" -version = "0.14.0" +name = "gloo-dialogs" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" - -[[package]] -name = "heck" -version = "0.3.3" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" dependencies = [ - "unicode-segmentation", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "gloo-file" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" dependencies = [ - "libc", + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom", + "gloo-events", + "gloo-utils", + "serde", + "serde-wasm-bindgen", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 1.1.0", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode", + "futures", + "gloo-utils", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.82", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] [[package]] name = "http" -version = "0.2.11" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -631,45 +1032,63 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.4" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "http", + "futures-util", + "http 1.1.0", + "http-body", "pin-project-lite", ] [[package]] name = "http-server" -version = "0.8.9" +version = "1.0.0-draft+1" dependencies = [ "anyhow", - "async-stream", "async-trait", - "chrono", - "criterion", - "dhat", - "flate2", - "futures", - "handlebars", - "http", + "bytes", + "clap", + "http 1.1.0", "http-auth-basic", - "humansize", + "http-body-util", + "http-server-plugin", "hyper", - "hyper-rustls", - "lazy_static", + "hyper-util", + "libloading", "local-ip-address", - "mime_guess", - "percent-encoding", - "rustls", - "rustls-pemfile", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "http-server-plugin" +version = "1.0.0-draft+1" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "http 1.1.0", + "http-body-util", + "hyper", + "rustc_version", "serde", - "serde_json", - "structopt", - "termcolor", "tokio", - "tokio-rustls", "toml", ] @@ -681,9 +1100,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humansize" @@ -696,55 +1115,89 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.27" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", - "http", + "h2", + "http 1.1.0", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ - "http", + "futures-util", + "http 1.1.0", "hyper", - "log", + "hyper-util", "rustls", - "rustls-native-certs", + "rustls-pki-types", "tokio", "tokio-rustls", - "webpki-roots", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -757,61 +1210,281 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "2.0.0" +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leptos" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cbb3237c274dadf00dcc27db96c52601b40375117178fb24a991cda073624f0" +dependencies = [ + "cfg-if", + "leptos_config", + "leptos_dom", + "leptos_macro", + "leptos_reactive", + "leptos_server", + "server_fn", + "tracing", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_config" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ed778611380ddea47568ac6ad6ec5158d39b5bd59e6c4dcd24efc15dc3dc0d" +dependencies = [ + "config", + "regex", + "serde", + "thiserror", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8401c46c86c1f4c16dcb7881ed319fcdca9cda9b9e78a6088955cb423afcf119" +dependencies = [ + "async-recursion", + "cfg-if", + "drain_filter_polyfill", + "futures", + "getrandom", + "html-escape", + "indexmap", + "itertools", + "js-sys", + "leptos_reactive", + "once_cell", + "pad-adapter", + "paste", + "rustc-hash", + "serde", + "serde_json", + "server_fn", + "smallvec", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb53d4794240b684a2f4be224b84bee9e62d2abc498cf2bcd643cd565e01d96" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn 2.0.82", + "walkdir", +] + +[[package]] +name = "leptos_macro" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "4b13bc3db70715cd8218c4535a5af3ae3c0e5fea6f018531fc339377b36bc0e0" dependencies = [ - "equivalent", - "hashbrown", + "attribute-derive", + "cfg-if", + "convert_case", + "html-escape", + "itertools", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn 2.0.82", + "tracing", + "uuid", ] [[package]] -name = "is-terminal" -version = "0.4.9" +name = "leptos_meta" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "25acc2f63cf91932013e400a95bf6e35e5d3dbb44a7b7e25a8e3057d12005b3b" dependencies = [ - "hermit-abi 0.3.2", - "rustix", - "windows-sys 0.48.0", + "cfg-if", + "indexmap", + "leptos", + "tracing", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "itertools" -version = "0.10.3" +name = "leptos_reactive" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "e4161acbf80f59219d8d14182371f57302bc7ff81ee41aba8ba1ff7295727f23" dependencies = [ - "either", + "base64 0.22.1", + "cfg-if", + "futures", + "indexmap", + "js-sys", + "oco_ref", + "paste", + "pin-project", + "rustc-hash", + "self_cell", + "serde", + "serde-wasm-bindgen", + "serde_json", + "slotmap", + "thiserror", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "itoa" -version = "1.0.1" +name = "leptos_router" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "8d71dea7d42c0d29c40842750232d3425ed1cf10e313a1f898076d20871dad32" +dependencies = [ + "cfg-if", + "gloo-net 0.6.0", + "itertools", + "js-sys", + "lazy_static", + "leptos", + "linear-map", + "once_cell", + "percent-encoding", + "send_wrapper", + "serde", + "serde_json", + "serde_qs 0.13.0", + "thiserror", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] -name = "js-sys" -version = "0.3.59" +name = "leptos_server" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "4a97eb90a13f71500b831c7119ddd3bdd0d7ae0a6b0487cade4fddeed3b8c03f" dependencies = [ - "wasm-bindgen", + "inventory", + "lazy_static", + "leptos_macro", + "leptos_reactive", + "serde", + "server_fn", + "thiserror", + "tracing", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "libc" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] -name = "libc" -version = "0.2.147" +name = "libloading" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] [[package]] name = "libm" @@ -819,29 +1492,39 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +dependencies = [ + "serde", + "serde_test", +] + [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "local-ip-address" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136ef34e18462b17bf39a7826f8f3bbc223341f8e83822beb8b77db9a3d49696" +checksum = "3669cf5561f8d27e8fc84cc15e58350e70f557d4d65f70e3154e54cd2f8e1782" dependencies = [ "libc", "neli", "thiserror", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -849,77 +1532,133 @@ dependencies = [ [[package]] name = "log" -version = "0.4.16" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" -dependencies = [ - "cfg-if", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] -name = "maplit" -version = "1.0.2" +name = "manyhow" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +checksum = "f91ea592d76c0b6471965708ccff7e6a5d277f676b90ab31f4d3f3fc77fade64" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.82", +] [[package]] -name = "memchr" -version = "2.5.0" +name = "manyhow-macros" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "c64621e2c08f2576e4194ea8be11daf24ac01249a4f53cd8befcbb7077120ead" +dependencies = [ + "proc-macro-utils 0.8.0", + "proc-macro2", + "quote", +] [[package]] -name = "memoffset" -version = "0.6.5" +name = "matchers" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "autocfg", + "regex-automata 0.1.10", ] +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", ] [[package]] -name = "miniz_oxide" -version = "0.5.1" +name = "minicov" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" dependencies = [ - "adler", + "cc", + "walkdir", ] +[[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.7.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.8" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.1.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] @@ -944,26 +1683,36 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 1.0.96", + "syn 1.0.109", ] [[package]] -name = "num-traits" -version = "0.2.14" +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "autocfg", + "memchr", + "minimal-lexical", ] [[package]] -name = "num_cpus" -version = "1.13.1" +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ - "hermit-abi 0.1.19", - "libc", + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", ] [[package]] @@ -975,6 +1724,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "oco_ref" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51ebcefb2f0b9a5e0bea115532c8ae4215d1b01eff176d0f4ba4192895c2708" +dependencies = [ + "serde", + "thiserror", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -982,16 +1741,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] -name = "oorandom" -version = "11.1.3" +name = "openssl" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] [[package]] -name = "opaque-debug" -version = "0.2.3" +name = "openssl-macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] [[package]] name = "openssl-probe" @@ -999,11 +1772,35 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pad-adapter" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" + [[package]] name = "parking_lot" -version = "0.12.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1011,209 +1808,380 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.2" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys 0.34.0", + "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" + [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "pest" -version = "2.1.3" +name = "pin-project" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" dependencies = [ - "ucd-trie", + "pin-project-internal", ] [[package]] -name = "pest_derive" -version = "2.1.0" +name = "pin-project-internal" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" dependencies = [ - "pest", - "pest_generator", + "proc-macro2", + "quote", + "syn 2.0.82", ] [[package]] -name = "pest_generator" -version = "2.1.3" +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "prettyplease" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "910d41a655dac3b764f1ade94821093d3610248694320cd072303a8eedcf221d" +dependencies = [ + "proc-macro2", + "syn 2.0.82", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ - "pest", - "pest_meta", + "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.96", + "version_check", ] [[package]] -name = "pest_meta" -version = "2.1.3" +name = "proc-macro-error-attr" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "maplit", - "pest", - "sha-1", + "proc-macro2", + "quote", + "version_check", ] [[package]] -name = "pin-project-lite" -version = "0.2.8" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] [[package]] -name = "pin-utils" -version = "0.1.0" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] [[package]] -name = "plotters" -version = "0.3.1" +name = "proc-macro-utils" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", + "proc-macro2", + "quote", + "smallvec", ] [[package]] -name = "plotters-backend" -version = "0.3.2" +name = "proc-macro-utils" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] [[package]] -name = "plotters-svg" -version = "0.3.1" +name = "proc-macro2" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ - "plotters-backend", + "unicode-ident", ] [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "proc-macro2-diagnostics" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ - "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.96", + "syn 2.0.82", "version_check", + "yansi", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "quote" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", - "quote", - "version_check", ] [[package]] -name = "proc-macro2" -version = "1.0.63" +name = "quote-use" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" dependencies = [ - "unicode-ident", + "quote", + "quote-use-macros", ] [[package]] -name = "quote" -version = "1.0.29" +name = "quote-use-macros" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" dependencies = [ + "proc-macro-utils 0.10.0", "proc-macro2", + "quote", + "syn 2.0.82", ] [[package]] -name = "rayon" -version = "1.5.2" +name = "redox_syscall" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "autocfg", - "crossbeam-deque", - "either", - "rayon-core", + "bitflags 2.4.0", ] [[package]] -name = "rayon-core" -version = "1.9.2" +name = "regex" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", + "aho-corasick", + "memchr", + "regex-automata 0.3.7", + "regex-syntax 0.7.5", ] [[package]] -name = "redox_syscall" -version = "0.2.13" +name = "regex-automata" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "bitflags 1.3.2", + "regex-syntax 0.6.29", ] [[package]] -name = "regex" -version = "1.5.5" +name = "regex-automata" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" dependencies = [ - "regex-syntax", + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "reqwest" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 1.1.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] [[package]] name = "ring" -version = "0.16.20" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", + "getrandom", "libc", - "once_cell", "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.52.0", +] + +[[package]] +name = "rstml" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe542870b8f59dd45ad11d382e5339c9a1047cde059be136a7016095bbdefa77" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.82", + "syn_derive", + "thiserror", +] + +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.82", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2", + "walkdir", ] [[package]] @@ -1228,57 +2196,78 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" -version = "0.38.8" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.20.6" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ - "log", - "ring", - "sct", - "webpki", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "rustls-native-certs" -version = "0.6.2" +name = "rustls-pemfile" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework", + "rustls-pki-types", ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls-pki-types" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ - "base64 0.21.2", + "ring", + "rustls-pki-types", + "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -1291,35 +2280,30 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.19" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "lazy_static", - "winapi", + "windows-sys 0.59.0", ] [[package]] -name = "scopeguard" -version = "1.1.0" +name = "scoped-tls" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] -name = "sct" -version = "0.7.0" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.6.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -1330,66 +2314,209 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + [[package]] name = "serde" -version = "1.0.192" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.82", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_spanned" -version = "0.6.3" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] -name = "sha-1" -version = "0.8.2" +name = "serde_test" +version = "1.0.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" dependencies = [ - "block-buffer", + "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 = "server_fn" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fae7a3038a32e5a34ba32c6c45eb4852f8affaf8b794ebfcd4b1099e2d62ebe" +dependencies = [ + "bytes", + "ciborium", + "const_format", + "dashmap", + "futures", + "gloo-net 0.6.0", + "http 1.1.0", + "js-sys", + "once_cell", + "send_wrapper", + "serde", + "serde_json", + "serde_qs 0.12.0", + "server_fn_macro_default", + "thiserror", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaaf648c6967aef78177c0610478abb5a3455811f401f3c62d10ae9bd3901a1" +dependencies = [ + "const_format", + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.82", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2aa8119b558a17992e0ac1fd07f080099564f24532858811ce04f742542440" +dependencies = [ + "server_fn_macro", + "syn 2.0.82", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", "digest", - "fake-simd", - "opaque-debug", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[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.0" @@ -1405,228 +2532,384 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "smallvec" -version = "1.8.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.4.9" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] -name = "structopt" -version = "0.3.26" +name = "syn" +version = "2.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" dependencies = [ - "clap 2.34.0", - "lazy_static", - "structopt-derive", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "structopt-derive" -version = "0.4.18" +name = "syn_derive" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" dependencies = [ - "heck", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.96", + "syn 2.0.82", ] [[package]] -name = "syn" -version = "1.0.96" +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "futures-core", ] [[package]] -name = "syn" -version = "2.0.29" +name = "system-configuration" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "bitflags 2.4.0", + "core-foundation", + "system-configuration-sys", ] [[package]] -name = "termcolor" -version = "1.1.3" +name = "system-configuration-sys" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ - "winapi-util", + "core-foundation-sys", + "libc", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "tempfile" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ - "unicode-width", + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" dependencies = [ "proc-macro2", "quote", - "syn 1.0.96", + "syn 2.0.82", ] [[package]] -name = "thousands" -version = "0.2.0" +name = "thread_local" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] [[package]] -name = "tinytemplate" -version = "1.2.1" +name = "tinyvec" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ - "serde", - "serde_json", + "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.29.1" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.82", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", ] [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", "tokio", - "webpki", ] [[package]] name = "toml" -version = "0.7.6" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.22.22", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.12" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.20", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +dependencies = [ + "bitflags 2.4.0", + "bytes", + "http 1.1.0", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.34" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "tracing-core" -version = "0.1.26" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "lazy_static", + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1636,64 +2919,136 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] -name = "typenum" -version = "1.15.0" +name = "typed-builder" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77739c880e00693faef3d65ea3aad725f196da38b22fdc7ea6ded6e1ce4d3add" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] [[package]] -name = "ucd-trie" -version = "0.1.3" +name = "typenum" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + [[package]] name = "unicode-ident" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] -name = "unicode-width" -version = "0.1.9" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", - "winapi", "winapi-util", ] @@ -1715,34 +3070,47 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.96", + "syn 2.0.82", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1750,50 +3118,70 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 1.0.96", + "syn 2.0.82", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] -name = "web-sys" -version = "0.3.57" +name = "wasm-bindgen-test" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "d381749acb0943d357dcbd8f0b100640679883fcdeeef04def49daf8d33a5426" dependencies = [ + "console_error_panic_hook", "js-sys", + "minicov", + "scoped-tls", "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", ] [[package]] -name = "webpki" -version = "0.22.0" +name = "wasm-bindgen-test-macro" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0" dependencies = [ - "ring", - "untrusted", + "proc-macro2", + "quote", + "syn 2.0.82", +] + +[[package]] +name = "wasm-streams" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "webpki-roots" -version = "0.22.3" +name = "web-sys" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ - "webpki", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -1814,11 +3202,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -1828,128 +3216,158 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] -name = "windows-sys" -version = "0.34.0" +name = "windows-registry" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows_aarch64_msvc 0.34.0", - "windows_i686_gnu 0.34.0", - "windows_i686_msvc 0.34.0", - "windows_x86_64_gnu 0.34.0", - "windows_x86_64_msvc 0.34.0", + "windows-result", + "windows-strings", + "windows-targets", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-result" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.1" +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.48.0", + "windows-result", + "windows-targets", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.34.0" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +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_i686_gnu" -version = "0.34.0" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_i686_gnu" -version = "0.48.0" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_i686_msvc" -version = "0.34.0" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_msvc" -version = "0.48.0" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_x86_64_gnu" -version = "0.34.0" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.34.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" +name = "winnow" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] [[package]] name = "winnow" -version = "0.4.9" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] + +[[package]] +name = "xxhash-rust" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 17ad829c..6b2781e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,69 +1,57 @@ -[package] -name = "http-server" -version = "0.8.9" -authors = ["Esteban Borai "] -edition = "2021" -description = "Simple and configurable command-line HTTP server" -repository = "https://github.com/EstebanBorai/http-server" -categories = ["web-programming", "web-programming::http-server"] -keywords = ["configurable", "http", "server", "serve", "static"] -license = "MIT OR Apache-2.0" -readme = "README.md" +[workspace] +members = [ + "crates/file-explorer", + "crates/file-explorer-core", + "crates/file-explorer-proto", + "crates/file-explorer-ui", + "crates/http-server", + "crates/http-server-plugin", +] +default-members = ["crates/http-server"] +resolver = "1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -name = "http_server_lib" -path = "src/lib.rs" - -[[bin]] -name = "http-server" -path = "src/bin/main.rs" - -[[bench]] -name = "file_explorer" -harness = false - -[features] -dhat-profiling = ["dhat"] - -[dependencies] -anyhow = "1.0.75" -async-stream = "0.3.5" -async-trait = "0.1.74" -chrono = { version = "0.4.31", features = ["serde"] } -dhat = { version = "0.2.4", optional = true } -futures = "0.3.30" -flate2 = "1.0.28" -http = "0.2.11" -http-auth-basic = "0.3.3" -handlebars = "4.3.7" -hyper = { version = "0.14.27", features = ["http1", "server", "stream", "tcp"] } -hyper-rustls = { version = "0.23.0", features = ["webpki-roots"] } -local-ip-address = "0.6.1" -mime_guess = "2.0.4" -percent-encoding = "2.2.0" -rustls = "0.20.6" -rustls-pemfile = "1.0.4" -serde = { version = "1.0.192", features = ["derive"] } -serde_json = "1.0.108" -structopt = { version = "0.3.26", default-features = false } -termcolor = "1.1.3" -tokio = { version = "1.29.1", features = [ - "fs", - "rt-multi-thread", - "signal", - "macros", -] } -tokio-rustls = "0.23.4" -toml = "0.7.6" +[workspace.dependencies] +anyhow = "1.0" +async-trait = "0.1.83" +bytes = "1.7.1" +chrono = "0.4.38" +clap = "4.5.20" +futures = "0.3.31" +gloo = "0.11.0" +gloo-file = "0.3.0" humansize = "2.1.3" - -[dev-dependencies] -criterion = { version = "0.5.1", features = ["async_tokio", "html_reports"] } -hyper = { version = "0.14.27", features = ["client"] } -tokio = { version = "1.29.1", features = ["full"] } -lazy_static = "1.4.0" - -[profile.release] -debug = 1 +http = "1.1.0" +http-auth-basic = "0.3.3" +http-body-util = "0.1" +hyper = "1.4" +hyper-util = "0.1.9" +leptos = "0.6" +leptos_meta = "0.6" +leptos_router = "0.6" +leptos-use = "0.10" +libloading = "0.8.5" +local-ip-address = "0.6.3" +mime_guess = "2.0.5" +multer = "3.1.0" +percent-encoding = "2.3.1" +reqwest = "0.12.8" +rust-embed = "8.5.0" +rustc_version = "0.4.1" +serde = "1.0.210" +serde_json = "1.0.128" +tokio = "1.40" +tokio-util = "0.7.12" +toml = "0.8.19" +tower-http = "0.6.1" +tower = "0.5.1" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +web-sys = "0.3.72" + +# Workspace Crates +file-explorer-core = { path = "crates/file-explorer-core" } +file-explorer-proto = { path = "crates/file-explorer-proto" } +file-explorer-ui = { path = "crates/file-explorer-ui" } +http-server-plugin = { path = "crates/http-server-plugin" } diff --git a/Justfile b/Justfile index 25f3c4f0..e5ee366c 100644 --- a/Justfile +++ b/Justfile @@ -9,3 +9,18 @@ release: # Runs Bats E2E Tests e2e: release BIN=./target/release/http-server bats tests/e2e + +# Runs formatting tool against Leptos source +ui-fmt: + leptosfmt ./crates/web/src/**/*.rs + +# Runs File Explorer UI for Development +ui-dev: + cd ./crates/file-explorer-ui && trunk serve --config ./Trunk.toml + +# Builds File Explorer UI for Production +ui-build: + cd ./crates/file-explorer-ui && trunk build --release --locked --config ./Trunk.toml + +dev: ui-build + cargo b --all && cargo r diff --git a/README.md b/README.md index 6ec60412..9e86c379 100644 --- a/README.md +++ b/README.md @@ -34,314 +34,6 @@ Verify successful installation. http-server --help ``` -Expect the following output: - -``` -USAGE: - http-server [FLAGS] [OPTIONS] [root-dir] - -FLAGS: - --cors Enable Cross-Origin Resource Sharing allowing any origin - --graceful-shutdown Waits for all requests to fulfill before shutting down the server - --gzip Enable GZip compression for HTTP Responses - --help Prints help information - -l, --logger Prints HTTP request and response details to stdout - -q, --quiet Turns off stdout/stderr logging - --spa Route non-existent files to /index.html - --tls Enables HTTPS serving using TLS - -i, --index Route directories to index.html if present - -V, --version Prints version information - -OPTIONS: - -c, --config Path to TOML configuration file - -h, --host Host (IP) to bind the server [default: 127.0.0.1] - --password Specifies password for basic authentication - -p, --port Port to bind the server [default: 7878] - --proxy Proxy requests to the provided URL - --tls-cert Path to the TLS Certificate [default: cert.pem] - --tls-key Path to the TLS Key [default: key.rsa] - --tls-key-algorithm Algorithm used to generate certificate key [default: rsa] - --username Specifies username for basic authentication - -ARGS: - Directory to serve files from [default: ./] -``` - -> If you find this output is out of date, don't hesitate to open a [PR here][1]. - -## Configuration - -When running the server with no options or flags provided, a set of default -configurations will be used. You can always change this behavior by either -creating your own config with the [Configuration TOML](https://github.com/http-server-rs/http-server/blob/main/fixtures/config.toml) file -or by providing CLI arguments described in the [usage](#usage) section. - -| Name | Description | Default | -| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| Host | Address to bind the server | `127.0.0.1` | -| Port | Port to bind the server | `7878` | -| Root Directory | The directory to serve files from | `CWD` | -| File Explorer UI | A File Explorer UI for the directory configured as the _Root Directory_ | Enabled | -| Configuration File | Specifies a configuration file. [Example](https://github.com/http-server-rs/http-server/blob/main/fixtures/config.toml) | Disabled | -| HTTPS (TLS) | HTTPS Secure connection configuration. Refer to [TLS (HTTPS)](https://github.com/http-server-rs/http-server#tls-https) reference | Disabled | -| CORS | Cross-Origin-Resource-Sharing headers support. Refer to [CORS](https://github.com/http-server-rs/http-server#cross-origin-resource-sharing-cors) reference | Disabled | -| Compression | GZip compression for HTTP Response Bodies. Refer to [Compression](https://github.com/http-server-rs/http-server#compression) reference | Disabled | -| Quiet | Don't print server details when running. This doesn't include any logging capabilities. | Disabled | -| Index | Route directories to index.html if present | Disabled | -| SPA | Route non-existent files to /index.html | Disabled | -| Basic Authentication | Authorize requests using Basic Authentication. Refer to [Basic Authentication](https://github.com/http-server-rs/http-server#basic-authentication) | Disabled | -| Logger | Prints HTTP request and response details to stdout | Disabled | - -## Usage - -``` -http-server [FLAGS] [OPTIONS] [root-dir] -``` - -### Flags - -Flags are provided without any values. For example: - -``` -http-server --help -``` - -| Name | Short | Long | Description | -| ----------------------------- | ----- | --------------------- | --------------------------------------------------------------------- | -| Cross-Origin Resource Sharing | N/A | `--cors` | Enable Cross-Origin Resource Sharing allowing any origin | -| GZip Compression | N/A | `--gzip` | Enable GZip compression for responses | -| Graceful Shutdown | N/A | `--graceful-shutdown` | Wait for all requests to be fulfilled before shutting down the server | -| Help | N/A | `--help` | Print help information | -| Logger | `-l` | `--logger` | Print HTTP request and response details to stdout | -| Version | `-V` | `--version` | Print version information | -| Quiet | `-q` | `--quiet` | Don't print output to console | -| Index | `-i` | `--index` | Route directories to index.html if present | -| SPA | N/A | `--spa` | Route non-existent files to /index.html | - -### Options - -Options receive a value and support default values as well. - -``` -http-server --host 127.0.0.1 -``` - -| Name | Short | Long | Description | Default Value | -| ------------------ | ----- | --------------------- | ----------------------------------------------------------------------------------------------------------- | ------------- | -| Host | `-h` | `--host` | Address to bind the server | `127.0.0.1` | -| Port | `-p` | `--port` | Port to bind the server | `7878` | -| Configuration File | `-c` | `--config` | Configuration file. [Example](https://github.com/http-server-rs/http-server/blob/main/fixtures/config.toml) | N/A | -| TLS | N/A | `--tls` | Enable TLS for HTTPS connections. Requires a Certificate and Key. [Reference](#tls-reference) | N/A | -| TLS Certificate | N/A | `--tls-cert` | Path to TLS certificate file. **Depends on `--tls`** | `cert.pem` | -| TLS Key | N/A | `--tls-key` | Path to TLS key file. **Depends on `--tls`** | `key.rsa` | -| TLS Key Algorithm | N/A | `--tls-key-algorithm` | Algorithm used to generate certificate key. **Depends on `--tls`** | `rsa` | -| Username | N/A | `--username` | Username to validate using basic authentication | N/A | -| Password | N/A | `--password` | Password to validate using basic authentication. **Depends on `--username`** | N/A | -| Proxy | N/A | `--proxy` | Proxy requests to the provided URL | N/A | - -## Request Handlers - -This HTTP Proxy supports different _Request Handlers_ which determine how each -incoming HTTP request is handled. They can't be combined, you must -choose one based on your needs. - -- [File Server](#file-server-handler) _default_ -- [Proxy](#proxy-handler) - -### File Server Handler - -Serves files from the provided directory. Navigation is scoped to the -specified directory. If no directory is provided the CWD will be used. - -> This is the default behavior for the HTTP server. - -### Proxy Handler - -Proxies requests to the provided URL. The URL provided is used as the base URL -for incoming requests. - -## Reference - -The following are some relevant details on features supported by this HTTP Server -that may be of interest to the user. - -### Compression - -Even though compression is supported, by default the server will not compress any -HTTP response contents. -You must specify the compression configuration you want to use, in the -configuration file or on the command line. - -As of today the server only supports compression with the GZip algorithm, but -`brotli` support is also planned. - -The following MIME types are never compressed: - -- `application/gzip` -- `application/octet-stream` -- `application/wasm` -- `application/zip` -- `image/*` -- `video/*` - -#### The Configuration File's Compression Section - -As future support for other compression algorithms is planned, -the configuration file already supports compression settings. - -```toml -[compression] -gzip = true -``` - -#### The `--gzip` flag - -Provide the `--gzip` argument to the server when executing it. - -```bash -http-server --gzip -``` - -### TLS (HTTPS) - -The TLS solution supported for this HTTP Server is built with the [rustls](https://github.com/ctz/rustls) -crate along with [hyper-rustls](https://github.com/ctz/hyper-rustls). - -When running with TLS support you will need: - -- A certificate -- A matching RSA Private Key for the certificate - -A script to generate certificates and keys is available here [tls-cert.sh](./docs/tls-cert.sh). -This script relies on `openssl`, so make sure you have it installed on your system. - -Run `http-server` as follows: - -```sh -http-server --tls --tls-cert --tls-key --tls-key-algorithm pkcs8 -``` - -### Cross-Origin Resource Sharing (CORS) - -This HTTP Server supports CORS headers _out of the box_. -Based on the headers you want to provide in your HTTP Responses, two -different methods for CORS configuration are available. - -By providing the `--cors` option to `http-server`, CORS headers -will be appended to every HTTP Response, allowing any origin. - -For more complex configurations, like specifying an origin, a set of allowed -HTTP methods and more, you should specify the configuration via the configuration -TOML file. - -The following example shows all the available options. - -```toml -[cors] -allow_credentials = false -allow_headers = ["content-type", "authorization", "content-length"] -allow_methods = ["GET", "PATCH", "POST", "PUT", "DELETE"] -allow_origin = "example.com" -expose_headers = ["*", "authorization"] -max_age = 600 -request_headers = ["x-app-version"] -request_method = "GET" -``` - -### Basic Authentication - -Basic Authentication is supported to deny requests when credentials are invalid. -You must provide the allowed `username` and `password` either by using the CLI -options `--username` along with the desired username and `--password` along with -the desired password, or by specifying such values through the configuration -TOML file. - -```toml -[basic_auth] -username = "John" -password = "Appleseed" -``` - -### Proxy - -The HTTP Server is able to proxy requests to a specified URL. - -When using the proxy, the FileExplorer won't be available, as the proxy is -an alternate _Request Handler_. - -The config TOML file can be used to provide proxy configurations: - -```toml -[proxy] -url = "https://example.com" -``` - -## Roadmap - -The following roadmap list features to provide for the version `v1.0.0`. - -This roadmap is still open for suggestions. If you find that there's a missing -feature in this list, that you would like to work on or expect for the first -stable release, please contact the software editors by opening an issue or a -discussion. - -If you want to contribute to one of these, please make sure -there's an issue tracking the feature and ping me. Otherwise -open an issue to be assigned and track the progress there. - -- [x] Logging - - [x] Request/Response Logging - - [x] Service Config Logins -- [ ] File Explorer - - [x] Modified Date - - [x] File Size - - [ ] Breadcrumb Navigation - - [ ] File Upload - - [ ] Filtering - - [ ] Sorting - - [ ] Sort By: File Name - - [ ] Sort By: File Size - - [ ] Sort By: File Modified Date - - [x] Directories First - - [ ] Files First -- [x] HTTPS/TLS Serving - - [x] HTTPS/TLS Support -- [ ] Compression - - [x] `gzip/deflate` Compression - - [ ] `brotli` Compression -- [ ] CORS - - [x] Cross Origin Resource Sharing - - [x] Allow Credentials - - [x] Allow Headers - - [x] Allow Methods - - [x] Allow Origin - - [x] Expose Headers - - [x] Max Age - - [x] Request Headers - - [x] Request Methods - - [ ] Multiple Origins (#8) -- [ ] Cache Control - - [ ] `Last-Modified` and `ETag` - - [ ] Respond with 304 to `If-Modified-Since` -- [ ] Partial Request - - [ ] `Accept-Ranges` - - [ ] `Content-Range` - - [ ] `If-Range` - - [ ] `If-Match` - - [ ] `Range` -- [x] Standalone Builds - - [x] macOS - - [x] Linux - - [x] Windows -- [ ] Development Server - - [ ] Live Reload -- [x] Proxy - - [x] URL Configuration -- [x] Basic Authentication - - [x] Username - - [x] Password -- [x] Graceful Shutdown - ## Release In order to create a release you must push a Git tag as follows diff --git a/benches/file_explorer.rs b/benches/file_explorer.rs deleted file mode 100644 index 9170a461..00000000 --- a/benches/file_explorer.rs +++ /dev/null @@ -1,43 +0,0 @@ -use criterion::Criterion; -use criterion::{criterion_group, criterion_main}; -use hyper::client::HttpConnector; -use hyper::Client; -use lazy_static::lazy_static; -use tokio::runtime::Runtime; - -lazy_static! { - static ref HTTP_CLIENT: Client = Client::new(); -} - -async fn http_get(uri: &str) { - HTTP_CLIENT.get(uri.parse().unwrap()).await.unwrap(); -} - -fn get_root(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - - c.bench_function("get_root", |b| { - b.to_async(&rt).iter(|| http_get("http://127.0.0.1:7878")); - }); -} - -fn get_file(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - - c.bench_function("get_file", |b| { - b.to_async(&rt) - .iter(|| http_get("http://127.0.0.1:7878/docs/screenshot.png")); - }); -} - -fn not_found_file(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - - c.bench_function("not_found_file", |b| { - b.to_async(&rt) - .iter(|| http_get("http://127.0.0.1:7878/thisfiledoesntexists123")); - }); -} - -criterion_group!(benches, get_root, get_file, not_found_file); -criterion_main!(benches); diff --git a/config.toml b/config.toml new file mode 100644 index 00000000..51d4f8e5 --- /dev/null +++ b/config.toml @@ -0,0 +1,5 @@ +host = "127.0.0.1" +port = "7878" + +[file-explorer] +path = "./" diff --git a/crates/file-explorer-core/Cargo.toml b/crates/file-explorer-core/Cargo.toml new file mode 100644 index 00000000..97f158bc --- /dev/null +++ b/crates/file-explorer-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "file-explorer-core" +version = "0.0.0" +authors = ["Esteban Borai "] +edition = "2021" +publish = false + +[dependencies] +anyhow = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +mime_guess = { workspace = true } +tokio = { workspace = true, features = ["fs", "rt-multi-thread", "signal", "macros"] } diff --git a/crates/file-explorer-core/src/fs/directory.rs b/crates/file-explorer-core/src/fs/directory.rs new file mode 100644 index 00000000..795c98e5 --- /dev/null +++ b/crates/file-explorer-core/src/fs/directory.rs @@ -0,0 +1,14 @@ +use std::path::PathBuf; + +/// Representation of a OS ScopedFileSystem directory providing the path +/// (`PathBuf`) +#[derive(Debug)] +pub struct Directory { + pub path: PathBuf, +} + +impl Directory { + pub fn path(&self) -> PathBuf { + self.path.clone() + } +} diff --git a/crates/file-explorer-core/src/fs/file.rs b/crates/file-explorer-core/src/fs/file.rs new file mode 100644 index 00000000..ce229b14 --- /dev/null +++ b/crates/file-explorer-core/src/fs/file.rs @@ -0,0 +1,56 @@ +use anyhow::{Context, Result}; +use chrono::{DateTime, Local}; +use tokio::io::AsyncReadExt; + +use std::fs::Metadata; +use std::mem::MaybeUninit; +use std::path::PathBuf; + +use mime_guess::{from_path, Mime}; + +pub const FILE_BUFFER_SIZE: usize = 8 * 1024; + +pub type FileBuffer = Box<[MaybeUninit; FILE_BUFFER_SIZE]>; + +/// Wrapper around `tokio::fs::File` built from a OS ScopedFileSystem file +/// providing `std::fs::Metadata` and the path to such file +#[derive(Debug)] +pub struct File { + pub path: PathBuf, + pub file: tokio::fs::File, + pub metadata: Metadata, +} + +impl File { + pub fn new(path: PathBuf, file: tokio::fs::File, metadata: Metadata) -> Self { + File { + path, + file, + metadata, + } + } + + pub fn mime(&self) -> Mime { + from_path(self.path.clone()).first_or_octet_stream() + } + + pub fn size(&self) -> u64 { + self.metadata.len() + } + + pub fn last_modified(&self) -> Result> { + let modified = self + .metadata + .modified() + .context("Failed to read last modified time for file")?; + let modified: DateTime = modified.into(); + + Ok(modified) + } + + pub async fn bytes(&mut self) -> Result> { + let mut buf = Vec::with_capacity(self.size() as usize); + self.file.read_to_end(&mut buf).await?; + Ok(buf) + } +} diff --git a/crates/file-explorer-core/src/fs/mod.rs b/crates/file-explorer-core/src/fs/mod.rs new file mode 100644 index 00000000..63fc62c8 --- /dev/null +++ b/crates/file-explorer-core/src/fs/mod.rs @@ -0,0 +1,24 @@ +mod directory; +mod file; + +use std::path::PathBuf; + +use anyhow::Result; + +pub use self::directory::Directory; +pub use self::file::File; + +#[derive(Debug, Clone)] +pub struct FileSystem { + pub path: PathBuf, +} + +impl FileSystem { + /// Creates a new instance of `ScopedFileSystem` using the provided PathBuf + /// as the root directory to serve files from. + /// + /// Provided paths will resolve relartive to the provided `root` directory. + pub fn new(path: PathBuf) -> Result { + Ok(Self { path }) + } +} diff --git a/crates/file-explorer-core/src/lib.rs b/crates/file-explorer-core/src/lib.rs new file mode 100644 index 00000000..e007c8b9 --- /dev/null +++ b/crates/file-explorer-core/src/lib.rs @@ -0,0 +1,113 @@ +mod fs; + +use std::path::{Component, PathBuf}; + +use tokio::fs::OpenOptions; + +pub use self::fs::{Directory, File}; + +use anyhow::Result; + +/// Any OS filesystem entry recognized by [`FileExplorer`] is treated as a +/// `Entry` both `File` and `Directory` are possible values. +#[derive(Debug)] +pub enum Entry { + File(Box), + Directory(Directory), +} + +pub struct FileExplorer { + root: PathBuf, +} + +impl FileExplorer { + pub fn new(root: PathBuf) -> Self { + Self { root } + } + + /// Peeks on the provided `path` as a "subpath" for this [`FileExplorer`] instance. + pub async fn peek(&self, path: PathBuf) -> Result { + let relative_path = self.build_relative_path(path); + self.open(relative_path).await + } + + /// Joins the provided `path` with the `root` path of this [`FileExplorer`] instance. + fn build_relative_path(&self, path: PathBuf) -> PathBuf { + let mut root = self.root.clone(); + root.extend(&self.normalize_path(&path)); + root + } + + /// Normalizes a `Path` to be directory-traversal safe. + /// + /// ```ignore + /// docs/collegue/cs50/lectures/../code/voting_excecise + /// ``` + /// + /// Will be normalized to be: + /// + /// ```ignore + /// docs/collegue/cs50/code/voting_excecise + /// ``` + /// + /// # Reference + /// + /// - https://owasp.org/www-community/attacks/Path_Traversal + fn normalize_path(&self, path: &PathBuf) -> PathBuf { + path.components() + .fold(PathBuf::new(), |mut result, p| match p { + Component::ParentDir => { + result.pop(); + result + } + Component::Normal(os_string) => { + result.push(os_string); + result + } + _ => result, + }) + } + + #[cfg(not(target_os = "windows"))] + async fn open(&self, path: PathBuf) -> Result { + let mut open_options = OpenOptions::new(); + let entry_path: PathBuf = path.clone(); + let file = open_options.read(true).open(path).await?; + let metadata = file.metadata().await?; + + if metadata.is_dir() { + return Ok(Entry::Directory(Directory { path: entry_path })); + } + + Ok(Entry::File(Box::new(File::new(entry_path, file, metadata)))) + } + + #[cfg(target_os = "windows")] + async fn open(&self, path: PathBuf) -> Result { + /// The file is being opened or created for a backup or restore operation. + /// The system ensures that the calling process overrides file security + /// checks when the process has SE_BACKUP_NAME and SE_RESTORE_NAME privileges. + /// + /// For more information, see Changing Privileges in a Token. + /// You must set this flag to obtain a handle to a directory. + /// A directory handle can be passed to some functions instead of a file handle. + /// + /// Refer: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea + const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x02000000; + + let mut open_options = OpenOptions::new(); + let entry_path: PathBuf = path.clone(); + let file = open_options + .read(true) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .open(path) + .await?; + let metadata = file.metadata().await?; + + if metadata.is_dir() { + return Ok(Entry::Directory(Directory { path: entry_path })); + } + + Ok(Entry::File(Box::new(File::new(entry_path, file, metadata)))) + } +} diff --git a/crates/file-explorer-proto/Cargo.toml b/crates/file-explorer-proto/Cargo.toml new file mode 100644 index 00000000..35d5a72f --- /dev/null +++ b/crates/file-explorer-proto/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "file-explorer-proto" +version = "0.0.0" +authors = ["Esteban Borai "] +edition = "2021" +publish = false + +[dependencies] +chrono = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } diff --git a/src/addon/file_server/directory_entry.rs b/crates/file-explorer-proto/src/lib.rs similarity index 61% rename from src/addon/file_server/directory_entry.rs rename to crates/file-explorer-proto/src/lib.rs index 472e3045..67ba90f5 100644 --- a/src/addon/file_server/directory_entry.rs +++ b/crates/file-explorer-proto/src/lib.rs @@ -1,18 +1,31 @@ +use std::cmp::Ordering; + use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; -use std::cmp::{Ord, Ordering}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub enum EntryType { + Directory, + File, + Git, + Justfile, + Markdown, + Rust, + Toml, +} /// A Directory entry used to display a File Explorer's entry. /// This struct is directly related to the Handlebars template used /// to power the File Explorer's UI -#[derive(Debug, Eq, Serialize)] +#[derive(Clone, Debug, Deserialize, Eq, Serialize)] pub struct DirectoryEntry { - pub(crate) display_name: String, - pub(crate) is_dir: bool, - pub(crate) size_bytes: u64, - pub(crate) entry_path: String, - pub(crate) date_created: Option>, - pub(crate) date_modified: Option>, + pub display_name: String, + pub is_dir: bool, + pub size_bytes: u64, + pub entry_path: String, + pub entry_type: EntryType, + pub date_created: Option>, + pub date_modified: Option>, } impl Ord for DirectoryEntry { @@ -46,24 +59,24 @@ impl PartialEq for DirectoryEntry { } /// A Breadcrumb Item used to navigate to previous path components -#[derive(Clone, Debug, Serialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct BreadcrumbItem { - pub(crate) entry_name: String, - pub(crate) entry_link: String, + pub depth: u8, + pub entry_name: String, + pub entry_link: String, } /// The value passed to the Handlebars template engine. /// All references contained in File Explorer's UI are provided /// via the `DirectoryIndex` struct -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct DirectoryIndex { - /// Directory listing entry - pub(crate) entries: Vec, - pub(crate) breadcrumbs: Vec, - pub(crate) sort: Sort, + pub entries: Vec, + pub breadcrumbs: Vec, + pub sort: Sort, } -#[derive(Serialize, Debug, PartialEq, Deserialize)] +#[derive(Clone, Serialize, Debug, PartialEq, Deserialize)] pub enum Sort { Directory, Name, diff --git a/crates/file-explorer-ui/Cargo.toml b/crates/file-explorer-ui/Cargo.toml new file mode 100644 index 00000000..d25b4395 --- /dev/null +++ b/crates/file-explorer-ui/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "file-explorer-ui" +version = "0.0.0" +edition = "2021" +authors = ["Esteban Borai "] +publish = false +description = "File Explorer UI" + +[lib] +name = "file_explorer_ui" +path = "src/lib.rs" + +[[bin]] +name = "file-explorer-ui" +path = "src/bin/main.rs" + +[dependencies] +anyhow = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +gloo = { workspace = true } +gloo-file = { workspace = true } +leptos = { workspace = true, features = ["csr"] } +leptos_meta = { workspace = true, features = ["csr"] } +leptos_router = { workspace = true, features = ["csr"] } +reqwest = { workspace = true, features = ["json"] } +rust-embed = { workspace = true } +web-sys = { workspace = true, features = ["FileList", "HtmlInputElement"] } + +file-explorer-proto = { workspace = true } + +[dev-dependencies] +web-sys = { workspace = true } + +wasm-bindgen = "0.2" +wasm-bindgen-test = "0.3" diff --git a/crates/file-explorer-ui/Trunk.toml b/crates/file-explorer-ui/Trunk.toml new file mode 100644 index 00000000..565ac05e --- /dev/null +++ b/crates/file-explorer-ui/Trunk.toml @@ -0,0 +1,13 @@ +[build] +# The index HTML file to drive the bundling process. +target = "./public/index.html" + +[watch] +# Paths to watch. The `build.target`'s parent folder is watched by default. +watch = ["./src"] +# Paths to ignore. +ignore = [] + +[[proxy]] +backend = "http://127.0.0.1:3000/api/v1" +rewrite = "/api/v1" diff --git a/crates/file-explorer-ui/assets/.gitkeep b/crates/file-explorer-ui/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/crates/file-explorer-ui/public/index.html b/crates/file-explorer-ui/public/index.html new file mode 100644 index 00000000..bf50d4b7 --- /dev/null +++ b/crates/file-explorer-ui/public/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/crates/file-explorer-ui/public/styles.css b/crates/file-explorer-ui/public/styles.css new file mode 100644 index 00000000..b5c61c95 --- /dev/null +++ b/crates/file-explorer-ui/public/styles.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/crates/file-explorer-ui/src/api.rs b/crates/file-explorer-ui/src/api.rs new file mode 100644 index 00000000..4ac2bd21 --- /dev/null +++ b/crates/file-explorer-ui/src/api.rs @@ -0,0 +1,56 @@ +use anyhow::Result; +use gloo::utils::window; +use reqwest::{header::CONTENT_TYPE, Url}; +use web_sys::FormData; + +use file_explorer_proto::DirectoryIndex; + +pub struct FileDownload { + pub bytes: Vec, + pub mime: String, +} + +pub struct Api { + base_url: Url, +} + +impl Api { + pub fn new() -> Self { + let base_url = Url::parse(&window().location().href().unwrap()).unwrap(); + + Self { base_url } + } + + pub async fn peek(&self, path: &str) -> Result { + let path = path.strip_prefix("/").unwrap(); + let url = self.base_url.join(&format!("/api/v1/{path}"))?; + let index = reqwest::get(url).await?.json::().await?; + + Ok(index) + } + + pub async fn upload(&self, form_data: FormData) -> Result<()> { + let url = self.base_url.join("api/v1")?; + + gloo::net::http::Request::post(url.as_ref()) + .body(form_data)? + .send() + .await?; + + Ok(()) + } + + pub async fn download(&self, path: &String) -> Result { + let path = path.strip_prefix("/").unwrap(); + let url = self.base_url.join(&format!("/api/v1/{path}"))?; + let res = reqwest::get(url).await?; + let headers = res.headers(); + let mime = headers + .get(CONTENT_TYPE) + .map(|hv| hv.to_str().unwrap().to_string()) + .unwrap_or("application/octet-stream".to_string()); + let bytes = res.bytes().await?.to_vec(); + + Ok(FileDownload { bytes, mime }) + } +} diff --git a/crates/file-explorer-ui/src/bin/main.rs b/crates/file-explorer-ui/src/bin/main.rs new file mode 100644 index 00000000..1c5d3504 --- /dev/null +++ b/crates/file-explorer-ui/src/bin/main.rs @@ -0,0 +1,9 @@ +use leptos::{mount_to_body, view}; + +use file_explorer_ui::App; + +fn main() { + mount_to_body(|| { + view! { } + }) +} diff --git a/crates/file-explorer-ui/src/components/atoms/button.rs b/crates/file-explorer-ui/src/components/atoms/button.rs new file mode 100644 index 00000000..6b8fbdc4 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/button.rs @@ -0,0 +1,17 @@ +use leptos::{component, view, Children, IntoView}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum ButtonVariant { + Primary, + #[default] + Secondary, +} + +#[component] +pub fn Button(children: Children) -> impl IntoView { + view! { + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/icons/download.rs b/crates/file-explorer-ui/src/components/atoms/icons/download.rs new file mode 100644 index 00000000..3fe17f10 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/download.rs @@ -0,0 +1,10 @@ +use leptos::{component, view, IntoView, TextProp}; + +#[component] +pub fn Download(#[prop(into, optional)] class: TextProp) -> impl IntoView { + view! { + + + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/icons/file.rs b/crates/file-explorer-ui/src/components/atoms/icons/file.rs new file mode 100644 index 00000000..528bfa8c --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/file.rs @@ -0,0 +1,10 @@ +use leptos::{component, view, IntoView}; + +#[component] +pub fn File() -> impl IntoView { + view! { + + + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/icons/folder.rs b/crates/file-explorer-ui/src/components/atoms/icons/folder.rs new file mode 100644 index 00000000..a16cff48 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/folder.rs @@ -0,0 +1,10 @@ +use leptos::{component, view, IntoView}; + +#[component] +pub fn Folder() -> impl IntoView { + view! { + + + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/icons/git.rs b/crates/file-explorer-ui/src/components/atoms/icons/git.rs new file mode 100644 index 00000000..abb24830 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/git.rs @@ -0,0 +1,10 @@ +use leptos::{component, view, IntoView}; + +#[component] +pub fn Git() -> impl IntoView { + view! { + + + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/icons/house.rs b/crates/file-explorer-ui/src/components/atoms/icons/house.rs new file mode 100644 index 00000000..d44a3642 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/house.rs @@ -0,0 +1,10 @@ +use leptos::{component, view, IntoView, TextProp}; + +#[component] +pub fn House(#[prop(into, optional)] class: TextProp) -> impl IntoView { + view! { + + + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/icons/justfile.rs b/crates/file-explorer-ui/src/components/atoms/icons/justfile.rs new file mode 100644 index 00000000..fc756682 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/justfile.rs @@ -0,0 +1,10 @@ +use leptos::{component, view, IntoView}; + +#[component] +pub fn Justfile() -> impl IntoView { + view! { + + + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/icons/markdown.rs b/crates/file-explorer-ui/src/components/atoms/icons/markdown.rs new file mode 100644 index 00000000..1f599a29 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/markdown.rs @@ -0,0 +1,10 @@ +use leptos::{component, view, IntoView}; + +#[component] +pub fn Markdown() -> impl IntoView { + view! { + + + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/icons/mod.rs b/crates/file-explorer-ui/src/components/atoms/icons/mod.rs new file mode 100644 index 00000000..c4fe0d16 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/mod.rs @@ -0,0 +1,19 @@ +mod download; +mod file; +mod folder; +mod git; +mod house; +mod justfile; +mod markdown; +mod rust; +mod toml; + +pub use download::Download; +pub use file::File; +pub use folder::Folder; +pub use git::Git; +pub use house::House; +pub use justfile::Justfile; +pub use markdown::Markdown; +pub use rust::Rust; +pub use toml::Toml; diff --git a/crates/file-explorer-ui/src/components/atoms/icons/rust.rs b/crates/file-explorer-ui/src/components/atoms/icons/rust.rs new file mode 100644 index 00000000..ca56eafe --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/rust.rs @@ -0,0 +1,11 @@ +use leptos::{component, view, IntoView}; + +#[component] +pub fn Rust() -> impl IntoView { + view! { + + + + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/icons/toml.rs b/crates/file-explorer-ui/src/components/atoms/icons/toml.rs new file mode 100644 index 00000000..14cf7fb9 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/icons/toml.rs @@ -0,0 +1,10 @@ +use leptos::{component, view, IntoView}; + +#[component] +pub fn Toml() -> impl IntoView { + view! { + + + + } +} diff --git a/crates/file-explorer-ui/src/components/atoms/mod.rs b/crates/file-explorer-ui/src/components/atoms/mod.rs new file mode 100644 index 00000000..36207d34 --- /dev/null +++ b/crates/file-explorer-ui/src/components/atoms/mod.rs @@ -0,0 +1,2 @@ +pub mod button; +pub mod icons; diff --git a/crates/file-explorer-ui/src/components/mod.rs b/crates/file-explorer-ui/src/components/mod.rs new file mode 100644 index 00000000..8f818fdb --- /dev/null +++ b/crates/file-explorer-ui/src/components/mod.rs @@ -0,0 +1,5 @@ +pub mod atoms; +pub mod molecules; +pub mod organisms; +pub mod pages; +pub mod templates; diff --git a/crates/file-explorer-ui/src/components/molecules/file_upload.rs b/crates/file-explorer-ui/src/components/molecules/file_upload.rs new file mode 100644 index 00000000..7f8a2d06 --- /dev/null +++ b/crates/file-explorer-ui/src/components/molecules/file_upload.rs @@ -0,0 +1,67 @@ +use gloo::utils::window; +use leptos::logging::log; +use leptos::wasm_bindgen::JsCast; +use leptos::{component, create_action, create_node_ref, html, view, IntoView}; +use web_sys::{Event, FormData, HtmlInputElement}; + +use crate::api::Api; +use crate::components::atoms::button::Button; + +#[component] +pub fn FileUpload() -> impl IntoView { + let file_input_el = create_node_ref::(); + let upload_file = create_action(|file: &web_sys::File| { + let file = file.to_owned(); + + let form_data = FormData::new().unwrap(); + form_data.append_with_blob("file", &file).unwrap(); + + async move { + match Api::new().upload(form_data).await { + Ok(_) => { + log!("File uploaded successfully"); + window() + .alert_with_message("File uploaded successfully") + .unwrap(); + } + Err(e) => { + log!("Failed to upload file: {:?}", e); + window() + .alert_with_message("Failed to upload file") + .unwrap(); + } + } + } + }); + + let handle_button_click = { + let file_input_el = file_input_el; + move |_| { + file_input_el.get_untracked().unwrap().click(); + } + }; + + view! { +
+ + (); + let mb_file_list = el.files(); + + if let Some(file_list) = mb_file_list { + if let Some(file) = file_list.get(0) { + log!("File selected: {:?}", file); + upload_file.dispatch(file); + } else { + log!("No file selected"); + } + } + } + /> +
+ } +} diff --git a/crates/file-explorer-ui/src/components/molecules/mod.rs b/crates/file-explorer-ui/src/components/molecules/mod.rs new file mode 100644 index 00000000..8737079a --- /dev/null +++ b/crates/file-explorer-ui/src/components/molecules/mod.rs @@ -0,0 +1 @@ +pub mod file_upload; diff --git a/crates/file-explorer-ui/src/components/organisms/action_bar.rs b/crates/file-explorer-ui/src/components/organisms/action_bar.rs new file mode 100644 index 00000000..4641fed6 --- /dev/null +++ b/crates/file-explorer-ui/src/components/organisms/action_bar.rs @@ -0,0 +1,12 @@ +use leptos::{component, view, IntoView}; + +use crate::components::molecules::file_upload::FileUpload; + +#[component] +pub fn ActionBar() -> impl IntoView { + view! { +
+ +
+ } +} diff --git a/crates/file-explorer-ui/src/components/organisms/mod.rs b/crates/file-explorer-ui/src/components/organisms/mod.rs new file mode 100644 index 00000000..7e547cd1 --- /dev/null +++ b/crates/file-explorer-ui/src/components/organisms/mod.rs @@ -0,0 +1,2 @@ +pub mod action_bar; +pub mod navigation_bar; diff --git a/crates/file-explorer-ui/src/components/organisms/navigation_bar.rs b/crates/file-explorer-ui/src/components/organisms/navigation_bar.rs new file mode 100644 index 00000000..61d157e4 --- /dev/null +++ b/crates/file-explorer-ui/src/components/organisms/navigation_bar.rs @@ -0,0 +1,42 @@ +use leptos::{component, view, For, IntoView, Signal, SignalGet}; + +use file_explorer_proto::BreadcrumbItem; + +use crate::components::atoms::icons::House; + +#[component] +pub fn NavigationBar(#[prop(into)] breadcrumbs: Signal>) -> impl IntoView { + view! { +
+ +
+ } +} diff --git a/crates/file-explorer-ui/src/components/pages/explorer.rs b/crates/file-explorer-ui/src/components/pages/explorer.rs new file mode 100644 index 00000000..b239feda --- /dev/null +++ b/crates/file-explorer-ui/src/components/pages/explorer.rs @@ -0,0 +1,51 @@ +use gloo::utils::window; +use leptos::{ + component, create_memo, create_signal, spawn_local, view, IntoView, SignalGet, SignalSet, +}; + +use file_explorer_proto::DirectoryIndex; + +use crate::api::Api; +use crate::components::organisms::action_bar::ActionBar; +use crate::components::organisms::navigation_bar::NavigationBar; +use crate::components::templates::file_list::FileList; + +#[component] +pub fn Explorer() -> impl IntoView { + let (index_getter, index_setter) = create_signal::>(None); + let entries = create_memo(move |_| { + index_getter + .get() + .map(|index| index.entries.clone()) + .unwrap_or_default() + }); + let breadcrumbs = create_memo(move |_| { + index_getter + .get() + .map(|index| index.breadcrumbs.clone()) + .unwrap_or_default() + }); + + spawn_local(async move { + leptos::logging::warn!("Performing a request to the server"); + let Ok(pathname) = window().location().pathname() else { + leptos::logging::error!("Failed to get the pathname"); + return; + }; + + let Ok(index) = Api::new().peek(&pathname).await else { + leptos::logging::error!("Failed to fetch the directory index"); + return; + }; + + index_setter.set(Some(index)); + }); + + view! { +
+ + + +
+ } +} diff --git a/crates/file-explorer-ui/src/components/pages/mod.rs b/crates/file-explorer-ui/src/components/pages/mod.rs new file mode 100644 index 00000000..fcb0baea --- /dev/null +++ b/crates/file-explorer-ui/src/components/pages/mod.rs @@ -0,0 +1 @@ +pub mod explorer; diff --git a/crates/file-explorer-ui/src/components/templates/file_list/download_button.rs b/crates/file-explorer-ui/src/components/templates/file_list/download_button.rs new file mode 100644 index 00000000..45941bb3 --- /dev/null +++ b/crates/file-explorer-ui/src/components/templates/file_list/download_button.rs @@ -0,0 +1,48 @@ +use gloo_file::{Blob, ObjectUrl}; +use leptos::{component, create_node_ref, html::A, spawn_local, view, IntoView}; + +use crate::api::{Api, FileDownload}; +use crate::components::atoms::icons::Download; + +#[component] +pub fn DownloadButton( + #[prop(into)] entry_path: String, + #[prop(into)] download_name: String, +) -> impl IntoView { + let anchor_ref = create_node_ref::(); + let download_file = { + move |_: _| { + let entry_path = entry_path.clone(); + let download_name = download_name.clone(); + + spawn_local(async move { + let api = Api::new(); + match api.download(&entry_path).await { + Ok(FileDownload { bytes, mime }) => { + let blob = Blob::new_with_options(bytes.as_slice(), Some(&mime)); + let object_url = ObjectUrl::from(blob); + + if let Some(anchor_el) = anchor_ref.get_untracked() { + anchor_el.set_href(&object_url); + anchor_el.set_download(&download_name); + anchor_el.click(); + } + } + Err(err) => { + leptos::logging::error!("Failed to download file: {:?}", err); + } + } + }); + } + }; + + view! { + + + {name} + + } + .into_view() + } else { + let download_name = name.clone(); + + view! { + + {name} + + + } + .into_view() + } + } + }; + + view! { + + + + + + + {render_name()} + + + + {size} + + + {format_date_or_default(date_created)} + + + {format_date_or_default(date_modified)} + + + } +} diff --git a/crates/file-explorer-ui/src/components/templates/file_list/entry_icon.rs b/crates/file-explorer-ui/src/components/templates/file_list/entry_icon.rs new file mode 100644 index 00000000..571c6c92 --- /dev/null +++ b/crates/file-explorer-ui/src/components/templates/file_list/entry_icon.rs @@ -0,0 +1,38 @@ +use leptos::{component, view, IntoView}; + +use file_explorer_proto::EntryType; + +use crate::components::atoms::icons::{File, Folder, Git, Justfile, Markdown, Rust, Toml}; + +#[component] +pub fn EntryIcon(#[prop(into)] entry_type: EntryType) -> impl IntoView { + let icon = match entry_type { + EntryType::Directory => view! { + + }, + EntryType::Git => view! { + + }, + EntryType::Justfile => view! { + + }, + EntryType::Markdown => view! { + + }, + EntryType::Rust => view! { + + }, + EntryType::Toml => view! { + + }, + _ => view! { + + }, + }; + + view! { +
+ {icon} +
+ } +} diff --git a/crates/file-explorer-ui/src/components/templates/file_list/mod.rs b/crates/file-explorer-ui/src/components/templates/file_list/mod.rs new file mode 100644 index 00000000..3a0db79e --- /dev/null +++ b/crates/file-explorer-ui/src/components/templates/file_list/mod.rs @@ -0,0 +1,55 @@ +mod download_button; +mod entry; +mod entry_icon; + +use leptos::{component, view, For, IntoView, Signal, SignalGet}; + +use file_explorer_proto::DirectoryEntry; + +use self::entry::Entry; + +#[component] +pub fn FileList(#[prop(into)] entries: Signal>) -> impl IntoView { + view! { +
+ + + + + + + + + + + + } + } + /> + +
+ + "Name" + + "Size" + + "Created" + + "Modified" +
+
+ } +} diff --git a/crates/file-explorer-ui/src/components/templates/mod.rs b/crates/file-explorer-ui/src/components/templates/mod.rs new file mode 100644 index 00000000..e108de48 --- /dev/null +++ b/crates/file-explorer-ui/src/components/templates/mod.rs @@ -0,0 +1 @@ +pub mod file_list; diff --git a/crates/file-explorer-ui/src/lib.rs b/crates/file-explorer-ui/src/lib.rs new file mode 100644 index 00000000..86e87a51 --- /dev/null +++ b/crates/file-explorer-ui/src/lib.rs @@ -0,0 +1,21 @@ +mod api; +mod components; + +use leptos::{component, view, IntoView, SignalGet}; +use leptos_meta::provide_meta_context; +use rust_embed::Embed; + +use crate::components::pages::explorer::Explorer; + +#[derive(Embed)] +#[folder = "public/dist"] +pub struct Assets; + +#[component] +pub fn App() -> impl IntoView { + provide_meta_context(); + + view! { + + } +} diff --git a/crates/file-explorer-ui/tailwind.config.js b/crates/file-explorer-ui/tailwind.config.js new file mode 100644 index 00000000..3c1e6d82 --- /dev/null +++ b/crates/file-explorer-ui/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.rs'], + theme: { + extend: {} + }, + plugins: [] +}; diff --git a/crates/file-explorer/Cargo.toml b/crates/file-explorer/Cargo.toml new file mode 100644 index 00000000..729e400f --- /dev/null +++ b/crates/file-explorer/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "file-explorer" +version = "0.0.0" +authors = ["Esteban Borai "] +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +bytes = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +futures = { workspace = true } +http = { workspace = true } +http-body-util = { workspace = true } +humansize = { workspace = true } +hyper = { workspace = true } +mime_guess = { workspace = true } +multer = { workspace = true } +rust-embed = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +percent-encoding = { workspace = true } +tokio = { workspace = true, features = ["fs", "rt-multi-thread", "signal", "macros"] } +tokio-util = { workspace = true, features = ["io"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } + +http-server-plugin = { workspace = true } +file-explorer-core = { workspace = true } +file-explorer-proto = { workspace = true } +file-explorer-ui = { workspace = true } diff --git a/crates/file-explorer/src/lib.rs b/crates/file-explorer/src/lib.rs new file mode 100644 index 00000000..cf05394f --- /dev/null +++ b/crates/file-explorer/src/lib.rs @@ -0,0 +1,434 @@ +mod utils; + +use std::fs::read_dir; +use std::mem::MaybeUninit; +use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use async_trait::async_trait; +use http::request::Parts; +use http::HeaderValue; +use http_body_util::Full; +use hyper::body::Bytes; +use hyper::header::CONTENT_TYPE; +use hyper::{Method, Response, StatusCode, Uri}; +use multer::Multipart; +use percent_encoding::{percent_decode_str, utf8_percent_encode}; +use serde::Deserialize; +use tokio::io::AsyncWriteExt; +use tokio::runtime::Handle; + +use file_explorer_core::{Entry, FileExplorer}; +use file_explorer_proto::{BreadcrumbItem, DirectoryEntry, DirectoryIndex, EntryType, Sort}; +use file_explorer_ui::Assets; +use http_server_plugin::config::read_from_path; +use http_server_plugin::{export_plugin, Function, InvocationError, PluginRegistrar}; + +use self::utils::{decode_uri, encode_uri, PERCENT_ENCODE_SET}; + +const FILE_BUFFER_SIZE: usize = 8 * 1024; + +pub type FileBuffer = Box<[MaybeUninit; FILE_BUFFER_SIZE]>; + +export_plugin!(register); + +const PLUGIN_NAME: &str = "file-explorer"; + +#[allow(improper_ctypes_definitions)] +extern "C" fn register(config_path: PathBuf, rt: Arc, registrar: &mut dyn PluginRegistrar) { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .init(); + + let config: FileExplorerConfig = read_from_path(config_path, PLUGIN_NAME).unwrap(); + + registrar.register_function( + PLUGIN_NAME, + Arc::new(FileExplorerPlugin::new(rt, config.path)), + ); +} + +#[derive(Debug, Deserialize)] +struct FileExplorerConfig { + pub path: PathBuf, +} + +struct FileExplorerPlugin { + file_explorer: FileExplorer, + path: PathBuf, + rt: Arc, +} + +#[async_trait] +impl Function for FileExplorerPlugin { + async fn call( + &self, + parts: Parts, + body: Bytes, + ) -> Result>, InvocationError> { + self.rt + .block_on(async move { self.handle(parts, body).await }) + } +} + +impl FileExplorerPlugin { + fn new(rt: Arc, path: PathBuf) -> Self { + let file_explorer = FileExplorer::new(path.clone()); + + Self { + file_explorer, + path, + rt, + } + } + + async fn handle( + &self, + parts: Parts, + body: Bytes, + ) -> Result>, InvocationError> { + tracing::info!("Handling request: {:?}", parts); + + if parts.uri.path().starts_with("/api/v1") { + self.handle_api(parts, body).await + } else { + let path = parts.uri.path(); + let path = path.strip_prefix('/').unwrap_or(path); + + if let Some(file) = Assets::get(path) { + let content_type = mime_guess::from_path(path).first_or_octet_stream(); + let content_type = HeaderValue::from_str(content_type.as_ref()).unwrap(); + let body = Full::new(Bytes::from(file.data.to_vec())); + let mut response = Response::new(body); + let mut headers = response.headers().clone(); + + headers.append(CONTENT_TYPE, content_type); + *response.headers_mut() = headers; + + return Ok(response); + } + + let index = Assets::get("index.html").unwrap(); + let body = Full::new(Bytes::from(index.data.to_vec())); + let mut response = Response::new(body); + let mut headers = response.headers().clone(); + + headers.append(CONTENT_TYPE, "text/html".try_into().unwrap()); + *response.headers_mut() = headers; + + Ok(response) + } + } + + async fn handle_api( + &self, + parts: Parts, + body: Bytes, + ) -> Result>, InvocationError> { + let path = Self::parse_req_uri(parts.uri.clone()).unwrap(); + + match parts.method { + Method::GET => match self.file_explorer.peek(path).await { + Ok(entry) => match entry { + Entry::Directory(dir) => { + let directory_index = + self.marshall_directory_index(dir.path()).await.unwrap(); + let json = serde_json::to_string(&directory_index).unwrap(); + let body = Full::new(Bytes::from(json)); + let mut response = Response::new(body); + let mut headers = response.headers().clone(); + + headers.append(CONTENT_TYPE, "application/json".try_into().unwrap()); + *response.headers_mut() = headers; + + Ok(response) + } + Entry::File(mut file) => { + let body = Full::new(Bytes::from(file.bytes().await.unwrap())); + let mut response = Response::new(body); + let mut headers = response.headers().clone(); + + headers.append(CONTENT_TYPE, file.mime().to_string().try_into().unwrap()); + *response.headers_mut() = headers; + + Ok(response) + } + }, + Err(err) => { + let message = format!("Failed to resolve path: {}", err); + Ok(Response::new(Full::new(Bytes::from(message)))) + } + }, + Method::POST => { + self.handle_file_upload(parts, body).await?; + Ok(Response::new(Full::new(Bytes::from( + "POST method is not supported", + )))) + } + _ => Ok(Response::new(Full::new(Bytes::from("Unsupported method")))), + } + } + + async fn handle_file_upload( + &self, + parts: Parts, + body: Bytes, + ) -> Result>, InvocationError> { + // Extract the `multipart/form-data` boundary from the headers. + let boundary = parts + .headers + .get(CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .and_then(|ct| multer::parse_boundary(ct).ok()); + + // Send `BAD_REQUEST` status if the content-type is not multipart/form-data. + if boundary.is_none() { + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::from("BAD REQUEST")) + .unwrap()); + } + + // Process the multipart e.g. you can store them in files. + if let Err(err) = self.process_multipart(body, boundary.unwrap()).await { + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Full::from(format!("INTERNAL SERVER ERROR: {}", err))) + .unwrap()); + } + + Ok(Response::new(Full::from("Success"))) + } + + async fn process_multipart(&self, bytes: Bytes, boundary: String) -> multer::Result<()> { + let cursor = std::io::Cursor::new(bytes); + let bytes_stream = tokio_util::io::ReaderStream::new(cursor); + let mut multipart = Multipart::new(bytes_stream, boundary); + + // Iterate over the fields, `next_field` method will return the next field if + // available. + while let Some(mut field) = multipart.next_field().await? { + // Get the field name. + let name = field.name(); + + // Get the field's filename if provided in "Content-Disposition" header. + let file_name = field.file_name().to_owned().unwrap_or("default.png"); + + // Get the "Content-Type" header as `mime::Mime` type. + let content_type = field.content_type(); + + let mut file = tokio::fs::File::create(file_name).await.unwrap(); + + println!( + "\n\nName: {:?}, FileName: {:?}, Content-Type: {:?}\n\n", + name, file_name, content_type + ); + + // Process the field data chunks e.g. store them in a file. + let mut field_bytes_len = 0; + while let Some(field_chunk) = field.chunk().await? { + // Do something with field chunk. + field_bytes_len += field_chunk.len(); + file.write_all(&field_chunk).await.unwrap(); + } + + println!("Field Bytes Length: {:?}", field_bytes_len); + } + + Ok(()) + } + + fn parse_req_uri(uri: Uri) -> Result { + let parts: Vec<&str> = uri.path().split('/').collect(); + let path = &parts[3..].join("/"); + + Ok(decode_uri(path)) + } + + /// Encodes a `PathBuf` component using `PercentEncode` with UTF-8 charset. + /// + /// # Panics + /// + /// If the component's `OsStr` representation doesn't belong to valid UTF-8 + /// this function panics. + fn encode_component(comp: Component) -> String { + let component = comp + .as_os_str() + .to_str() + .expect("The provided OsStr doesn't belong to the UTF-8 charset."); + + utf8_percent_encode(component, PERCENT_ENCODE_SET).to_string() + } + + fn breadcrumbs_from_path(root_dir: &Path, path: &Path) -> Result> { + let root_dir_name = root_dir + .components() + .last() + .unwrap() + .as_os_str() + .to_str() + .expect("The first path component is not UTF-8 charset compliant."); + let stripped = path + .strip_prefix(root_dir)? + .components() + .map(Self::encode_component) + .collect::>(); + + let mut breadcrumbs = stripped + .iter() + .enumerate() + .map(|(idx, entry_name)| BreadcrumbItem { + depth: (idx + 1) as u8, + entry_name: percent_decode_str(entry_name) + .decode_utf8() + .expect("The path name is not UTF-8 compliant") + .to_string(), + entry_link: format!("/{}", stripped[0..=idx].join("/")), + }) + .collect::>(); + + breadcrumbs.insert( + 0, + BreadcrumbItem { + depth: 0, + entry_name: String::from(root_dir_name), + entry_link: String::from("/"), + }, + ); + + Ok(breadcrumbs) + } + + /// Creates entry's relative path. Used by Handlebars template engine to + /// provide navigation through `FileExplorer` + /// + /// If the root_dir is: `https-server/src` + /// The entry path is: `https-server/src/server/service/file_explorer.rs` + /// + /// Then the resulting path from this function is the absolute path to + /// the "entry path" in relation to the "root_dir" path. + /// + /// This happens because links should behave relative to the `/` path + /// which in this case is `http-server/src` instead of system's root path. + fn make_dir_entry_link(root_dir: &Path, entry_path: &Path) -> String { + let path = entry_path.strip_prefix(root_dir).unwrap(); + + encode_uri(path) + } + + /// Creates a `DirectoryIndex` with the provided `root_dir` and `path` + /// (HTTP Request URI) + fn index_directory(root_dir: PathBuf, path: PathBuf) -> Result { + let breadcrumbs = Self::breadcrumbs_from_path(&root_dir, &path)?; + let entries = read_dir(path).context("Unable to read directory")?; + let mut directory_entries: Vec = Vec::new(); + + for entry in entries { + let entry = entry.context("Unable to read entry")?; + let metadata = entry.metadata()?; + + let display_name = entry + .file_name() + .to_str() + .context("Unable to gather file name into a String")? + .to_string(); + + let date_created = if let Ok(time) = metadata.created() { + Some(time.into()) + } else { + None + }; + + let date_modified = if let Ok(time) = metadata.modified() { + Some(time.into()) + } else { + None + }; + + let entry_type = if metadata.file_type().is_dir() { + EntryType::Directory + } else if let Some(ext) = display_name.split(".").last() { + match ext.to_ascii_lowercase().as_str() { + "gitignore" | "gitkeep" => EntryType::Git, + "justfile" => EntryType::Justfile, + "md" => EntryType::Markdown, + "rs" => EntryType::Rust, + "toml" => EntryType::Toml, + _ => EntryType::File, + } + } else { + EntryType::File + }; + + directory_entries.push(DirectoryEntry { + is_dir: metadata.is_dir(), + size_bytes: metadata.len(), + entry_path: Self::make_dir_entry_link(&root_dir, &entry.path()), + display_name, + entry_type, + date_created, + date_modified, + }); + } + + // if let Some(query_params) = query_params { + // if let Some(sort_by) = query_params.sort_by { + // match sort_by { + // SortBy::Name => { + // directory_entries.sort_by_key(|entry| entry.display_name.clone()); + // } + // SortBy::Size => directory_entries.sort_by_key(|entry| entry.size_bytes), + // SortBy::DateCreated => { + // directory_entries.sort_by_key(|entry| entry.date_created) + // } + // SortBy::DateModified => { + // directory_entries.sort_by_key(|entry| entry.date_modified) + // } + // }; + + // let sort_enum = match sort_by { + // SortBy::Name => Sort::Name, + // SortBy::Size => Sort::Size, + // SortBy::DateCreated => Sort::DateCreated, + // SortBy::DateModified => Sort::DateModified, + // }; + + // return Ok(DirectoryIndex { + // entries: directory_entries, + // breadcrumbs, + // sort: sort_enum, + // }); + // } + // } + + directory_entries.sort(); + + Ok(DirectoryIndex { + entries: directory_entries, + breadcrumbs, + sort: Sort::Directory, + }) + } + + async fn marshall_directory_index(&self, path: PathBuf) -> Result { + Self::index_directory(self.path.clone(), path) + } + + // pub async fn make_http_file_response(file: Box) -> Result>> { + // Response::builder() + // .header(CONTENT_TYPE, file.mime().to_string()) + // .header( + // ETAG, + // format!( + // "W/\"{0:x}-{1:x}.{2:x}\"", + // file.size(), + // file.last_modified().unwrap().timestamp(), + // file.last_modified().unwrap().timestamp_subsec_nanos(), + // ), + // ) + // .header(LAST_MODIFIED, file.last_modified().unwrap().to_rfc2822()) + // .body(Full::new(Bytes::from(file.bytes()))) + // .context("Failed to build HTTP File Response") + // } +} diff --git a/src/utils/url_encode.rs b/crates/file-explorer/src/utils.rs similarity index 99% rename from src/utils/url_encode.rs rename to crates/file-explorer/src/utils.rs index e2c56724..872d1aea 100644 --- a/src/utils/url_encode.rs +++ b/crates/file-explorer/src/utils.rs @@ -1,6 +1,7 @@ -use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; use std::path::{Path, PathBuf}; +use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; + pub const PERCENT_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC .remove(b'-') .remove(b'_') diff --git a/crates/http-server-plugin/Cargo.toml b/crates/http-server-plugin/Cargo.toml new file mode 100644 index 00000000..e632453c --- /dev/null +++ b/crates/http-server-plugin/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "http-server-plugin" +version = "1.0.0-draft+1" +authors = ["Esteban Borai "] +edition = "2021" +description = "HTTP Server RS Plugin Crate" +repository = "https://github.com/http-server-rs/http-server" +categories = ["web-programming", "web-programming::http-server"] +keywords = ["sdk", "http", "server", "plugin", "dll"] +license = "MIT OR Apache-2.0" +readme = "../../README.md" + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +bytes = { workspace = true } +http = { workspace = true } +http-body-util = { workspace = true } +hyper = { workspace = true } +serde = { workspace = true } +tokio = { workspace = true, features = ["full"] } +toml = { workspace = true } + +[build-dependencies] +rustc_version = { workspace = true } diff --git a/crates/http-server-plugin/build.rs b/crates/http-server-plugin/build.rs new file mode 100644 index 00000000..a38022ff --- /dev/null +++ b/crates/http-server-plugin/build.rs @@ -0,0 +1,4 @@ +fn main() { + let version = rustc_version::version().unwrap(); + println!("cargo:rustc-env=RUSTC_VERSION={}", version); +} diff --git a/crates/http-server-plugin/src/config.rs b/crates/http-server-plugin/src/config.rs new file mode 100644 index 00000000..9f5e63ec --- /dev/null +++ b/crates/http-server-plugin/src/config.rs @@ -0,0 +1,18 @@ +use std::fs::read_to_string; +use std::path::PathBuf; + +use anyhow::{bail, Result}; +use serde::de::DeserializeOwned; +use toml::Table; + +pub fn read_from_path(path: PathBuf, key: &str) -> Result { + let config_str = read_to_string(&path)?; + let config_tbl: Table = toml::from_str(&config_str)?; + + if let Some(tbl) = config_tbl.get(key) { + let config: T = tbl.to_owned().try_into().unwrap(); + return Ok(config); + } + + bail!("Key not found") +} diff --git a/crates/http-server-plugin/src/lib.rs b/crates/http-server-plugin/src/lib.rs new file mode 100644 index 00000000..4ad85f31 --- /dev/null +++ b/crates/http-server-plugin/src/lib.rs @@ -0,0 +1,54 @@ +pub mod config; + +use std::path::PathBuf; +use std::sync::Arc; + +use async_trait::async_trait; +use http::request::Parts; +use http_body_util::Full; +use hyper::body::Bytes; +use hyper::Response; +use tokio::runtime::Handle; + +pub static CORE_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub static RUSTC_VERSION: &str = env!("RUSTC_VERSION"); + +#[async_trait] +pub trait Function: Send + Sync { + async fn call( + &self, + parts: Parts, + body: Bytes, + ) -> Result>, InvocationError>; +} + +#[derive(Debug)] +pub enum InvocationError { + InvalidArgumentCount { expected: usize, found: usize }, + Other { msg: String }, +} + +#[allow(improper_ctypes_definitions)] +pub struct PluginDeclaration { + pub rustc_version: &'static str, + pub core_version: &'static str, + pub register: + unsafe extern "C" fn(config_path: PathBuf, rt: Arc, &mut dyn PluginRegistrar), +} + +pub trait PluginRegistrar { + fn register_function(&mut self, name: &str, function: Arc); +} + +#[macro_export] +macro_rules! export_plugin { + ($register:expr) => { + #[doc(hidden)] + #[no_mangle] + pub static PLUGIN_DECLARATION: $crate::PluginDeclaration = $crate::PluginDeclaration { + rustc_version: $crate::RUSTC_VERSION, + core_version: $crate::CORE_VERSION, + register: $register, + }; + }; +} diff --git a/crates/http-server/Cargo.toml b/crates/http-server/Cargo.toml new file mode 100644 index 00000000..755d7047 --- /dev/null +++ b/crates/http-server/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "http-server" +version = "1.0.0-draft+1" +authors = ["Esteban Borai "] +edition = "2021" +description = "Simple and configurable command-line HTTP server" +repository = "https://github.com/http-server-rs/http-server" +categories = ["web-programming", "web-programming::http-server"] +keywords = ["configurable", "http", "server", "serve", "static"] +license = "MIT OR Apache-2.0" +readme = "README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +bytes = { workspace = true } +clap = { workspace = true, features = ["env", "derive", "std"] } +http = { workspace = true } +http-auth-basic = { workspace = true } +http-body-util = { workspace = true } +hyper = { workspace = true } +hyper-util = { workspace = true, features = ["full"] } +libloading = { workspace = true } +local-ip-address = { workspace = true } +tokio = { workspace = true, features = ["fs", "rt-multi-thread", "signal", "macros"] } +tower-http = { workspace = true, features = ["cors"] } +tower = { workspace = true, features = ["util"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } + +http-server-plugin = { workspace = true } diff --git a/crates/http-server/src/cli.rs b/crates/http-server/src/cli.rs new file mode 100644 index 00000000..fd05e0dd --- /dev/null +++ b/crates/http-server/src/cli.rs @@ -0,0 +1,15 @@ +use std::net::IpAddr; + +use clap::Parser; + +#[derive(Debug, Parser)] +#[command( + name = "http-server", + author = "Esteban Borai ", + about = "Simple and configurable command-line HTTP server\nSource: https://github.com/EstebanBorai/http-server", + next_line_help = true +)] +pub struct Cli { + pub host: IpAddr, + pub port: u16, +} diff --git a/crates/http-server/src/config.rs b/crates/http-server/src/config.rs new file mode 100644 index 00000000..c06d6cd6 --- /dev/null +++ b/crates/http-server/src/config.rs @@ -0,0 +1,6 @@ +use std::net::IpAddr; + +pub struct Config { + pub host: IpAddr, + pub port: u16, +} diff --git a/crates/http-server/src/main.rs b/crates/http-server/src/main.rs new file mode 100644 index 00000000..9b340e1d --- /dev/null +++ b/crates/http-server/src/main.rs @@ -0,0 +1,36 @@ +pub mod cli; +pub mod config; +pub mod plugin; +pub mod server; + +use std::{process::exit, sync::Arc}; + +use anyhow::Result; +use tokio::runtime::Builder; + +use self::server::Server; + +fn main() -> Result<()> { + let rt = Builder::new_multi_thread() + .enable_all() + .thread_name("http-server") + .build()?; + let rt = Arc::new(rt); + + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .init(); + + rt.block_on(async { + match Server::run(Arc::clone(&rt)).await { + Ok(_) => { + println!("Server exited successfuly"); + Ok(()) + } + Err(error) => { + eprint!("{:?}", error); + exit(1); + } + } + }) +} diff --git a/crates/http-server/src/plugin.rs b/crates/http-server/src/plugin.rs new file mode 100644 index 00000000..f601c287 --- /dev/null +++ b/crates/http-server/src/plugin.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; +use std::ffi::OsStr; +use std::io; +use std::path::PathBuf; +use std::sync::Arc; + +use async_trait::async_trait; +use http::request::Parts; +use http_body_util::Full; +use hyper::body::Bytes; +use hyper::Response; +use libloading::Library; +use tokio::runtime::Handle; +use tokio::sync::Mutex; + +use http_server_plugin::{ + Function, InvocationError, PluginDeclaration, CORE_VERSION, RUSTC_VERSION, +}; + +/// A proxy object which wraps a [`Function`] and makes sure it can't outlive +/// the library it came from. +#[derive(Clone)] +pub struct FunctionProxy { + function: Arc, + _lib: Arc, +} + +#[async_trait] +impl Function for FunctionProxy { + async fn call( + &self, + parts: Parts, + bytes: Bytes, + ) -> Result>, InvocationError> { + self.function.call(parts, bytes).await + } +} + +pub struct ExternalFunctions { + handle: Arc, + functions: Mutex>, + libraries: Mutex>>, +} + +impl Default for ExternalFunctions { + fn default() -> Self { + Self::new() + } +} + +impl ExternalFunctions { + pub fn new() -> ExternalFunctions { + let handle = Arc::new(Handle::current()); + + ExternalFunctions { + handle, + functions: Mutex::new(HashMap::default()), + libraries: Mutex::new(Vec::new()), + } + } + + /// Loads a plugin from the given path. + /// + /// # Safety + /// + /// This function is unsafe because it loads a shared library and calls + /// functions from it. + pub async unsafe fn load>( + &self, + rt_handle: Arc, + config_path: PathBuf, + library_path: P, + ) -> io::Result<()> { + let library = Arc::new(Library::new(library_path).unwrap()); + let decl = library + .get::<*mut PluginDeclaration>(b"PLUGIN_DECLARATION\0") + .unwrap() + .read(); + + if decl.rustc_version != RUSTC_VERSION || decl.core_version != CORE_VERSION { + return Err(io::Error::new(io::ErrorKind::Other, "Version mismatch")); + } + + let mut registrar = PluginRegistrar::new(Arc::clone(&library)); + + (decl.register)(config_path, Arc::clone(&rt_handle), &mut registrar); + + self.functions.lock().await.extend(registrar.functions); + self.libraries.lock().await.push(library); + + Ok(()) + } + + async fn get_function(&self, func: &str) -> Option { + self.functions.lock().await.get(func).cloned() + } + + pub async fn call( + &self, + func: &str, + parts: Parts, + bytes: Bytes, + ) -> Result>, InvocationError> { + let function_proxy = self.get_function(func).await.unwrap(); + let join_handle = self + .handle + .spawn(async move { function_proxy.call(parts, bytes).await }) + .await; + + join_handle.unwrap() + } +} + +struct PluginRegistrar { + functions: HashMap, + lib: Arc, +} + +impl PluginRegistrar { + fn new(lib: Arc) -> PluginRegistrar { + PluginRegistrar { + lib, + functions: HashMap::default(), + } + } +} + +impl http_server_plugin::PluginRegistrar for PluginRegistrar { + fn register_function(&mut self, name: &str, function: Arc) { + let proxy = FunctionProxy { + function, + _lib: Arc::clone(&self.lib), + }; + + self.functions.insert(name.to_string(), proxy); + } +} diff --git a/crates/http-server/src/server/mod.rs b/crates/http-server/src/server/mod.rs new file mode 100644 index 00000000..67866d2a --- /dev/null +++ b/crates/http-server/src/server/mod.rs @@ -0,0 +1,80 @@ +use std::{convert::Infallible, net::SocketAddr, path::PathBuf, str::FromStr, sync::Arc}; + +use anyhow::Result; +use http_body_util::{BodyExt, Full}; +use hyper::{ + body::{Bytes, Incoming}, + server::conn::http1, + Method, Request, Response, +}; +use hyper_util::{rt::TokioIo, service::TowerToHyperService}; +use tokio::net::TcpListener; +use tokio::runtime::Runtime; +use tower::ServiceBuilder; +use tower_http::cors::{Any, CorsLayer}; +use tracing::info; + +use crate::plugin::ExternalFunctions; + +pub struct Server {} + +impl Server { + pub async fn run(rt: Arc) -> Result<()> { + info!("Initializing server"); + + let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); + let listener = TcpListener::bind(addr).await?; + let functions = Arc::new(ExternalFunctions::new()); + let plugin_library = PathBuf::from_str("./target/debug/libfile_explorer.dylib").unwrap(); + let config = PathBuf::from_str("./config.toml").unwrap(); + let handle = Arc::new(rt.handle().to_owned()); + let local_ip = local_ip_address::local_ip(); + + unsafe { + functions + .load(Arc::clone(&handle), config, plugin_library) + .await + .expect("Function loading failed"); + } + + info!(%addr, "Server Listening"); + info!(?local_ip, "Local Network"); + + loop { + let (stream, _) = listener.accept().await?; + let io = TokioIo::new(stream); + let functions = Arc::clone(&functions); + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(Any); + + handle.spawn(async move { + let functions = Arc::clone(&functions); + let svc = tower::service_fn(|req: Request| async { + let (parts, body) = req.into_parts(); + let body = body.collect().await.unwrap().to_bytes(); + + match functions.call("file-explorer", parts, body).await { + Ok(res) => Ok::< + Response>, + Infallible, + >(res), + Err(err) => { + eprintln!("Error: {:?}", err); + Ok(Response::new(Full::new(Bytes::from( + "Internal Server Error", + )))) + } + } + }); + + let svc = ServiceBuilder::new().layer(cors).service(svc); + let svc = TowerToHyperService::new(svc); + + if let Err(err) = http1::Builder::new().serve_connection(io, svc).await { + eprintln!("server error: {}", err); + } + }); + } + } +} diff --git a/fixtures/config.toml b/fixtures/config.toml deleted file mode 100644 index 7aae8d70..00000000 --- a/fixtures/config.toml +++ /dev/null @@ -1,35 +0,0 @@ -# Server configuration is also provided by specifying a TOML file with the -# following values - -host = "127.0.0.1" -port = 7878 - -# quiet = false -# root_dir = "./" -# graceful_shutdown = false -# index = false -# spa = false - -# [tls] -# cert = "cert.pem" -# key = "key.pem" - -# [cors] -# allow_credentials = false -# allow_headers = ["content-type", "authorization", "content-length"] -# allow_methods = ["GET", "PATCH", "POST", "PUT", "DELETE"] -# allow_origin = "example.com" -# expose_headers = ["*", "authorization"] -# max_age = 600 -# request_headers = ["x-app-version"] -# request_method = "GET" - -# [compression] -# gzip = true - -# [basic_auth] -# username = "John" -# password = "Appleseed" - -# [proxy] -# url = "https://example.com" diff --git a/src/addon/compression/gzip.rs b/src/addon/compression/gzip.rs deleted file mode 100644 index cb26b075..00000000 --- a/src/addon/compression/gzip.rs +++ /dev/null @@ -1,223 +0,0 @@ -use anyhow::{Error, Result}; -use flate2::write::GzEncoder; -use http::{HeaderValue, Request, Response}; -use hyper::body::aggregate; -use hyper::body::Buf; -use hyper::Body; -use std::io::Write; -use std::sync::Arc; -use tokio::sync::Mutex; - -/// Content-Type values that should be ignored by the compression algorithm -const IGNORED_CONTENT_TYPE: [&str; 6] = [ - "application/gzip", - "application/octet-stream", - "application/wasm", - "application/zip", - "image", - "video", -]; - -pub async fn is_encoding_accepted(request: Arc>>) -> Result { - if let Some(accept_encoding) = request - .lock() - .await - .headers() - .get(http::header::ACCEPT_ENCODING) - { - let accept_encoding = accept_encoding.to_str()?; - - return Ok(accept_encoding - .split(", ") - .map(|accepted_encoding| accepted_encoding.trim()) - .any(|accepted_encoding| accepted_encoding == "gzip")); - } - - Ok(false) -} - -pub async fn is_compressable_content_type(response: Arc>>) -> Result { - if let Some(content_type) = response - .lock() - .await - .headers() - .get(http::header::CONTENT_TYPE) - { - let content_type = content_type.to_str()?; - - if IGNORED_CONTENT_TYPE.contains(&content_type) { - return Ok(false); - } - - return Ok(true); - } - - Ok(false) -} - -pub async fn should_compress( - request: Arc>>, - response: Arc>>, -) -> Result { - Ok(is_encoding_accepted(request).await? - && is_compressable_content_type(Arc::clone(&response)).await?) -} - -pub fn compress(bytes: &[u8]) -> Result> { - let buffer: Vec = Vec::with_capacity(bytes.len()); - let mut compressor: GzEncoder> = GzEncoder::new(buffer, flate2::Compression::default()); - - compressor.write_all(bytes)?; - - compressor.finish().map_err(Error::from) -} - -pub async fn compress_http_response( - request: Arc>>, - response: Arc>>, -) -> Result<()> { - if let Ok(compressable) = should_compress(Arc::clone(&request), Arc::clone(&response)).await { - if compressable { - let mut buffer: Vec = Vec::new(); - - { - let mut response = response.lock().await; - - if response.headers().get("Content-Encoding").is_some() { - // if the "Content-Encoding" HTTP header is present in the - // `Response`, skip compression process - return Ok(()); - } - - let body = response.body_mut(); - let mut buffer_cursor = aggregate(body).await.unwrap(); - - while buffer_cursor.has_remaining() { - buffer.push(buffer_cursor.get_u8()); - } - } - - let compressed = compress(&buffer)?; - let mut response = response.lock().await; - let response_headers = response.headers_mut(); - - response_headers.append( - http::header::CONTENT_ENCODING, - HeaderValue::from_str("gzip").unwrap(), - ); - - response_headers.remove(http::header::CONTENT_LENGTH); - - *response.body_mut() = Body::from(compressed); - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use http::response::Builder as HttpResponseBuilder; - use hyper::{Body, Request}; - use std::sync::Arc; - use tokio::sync::Mutex; - - use crate::server::middleware; - - #[allow(unused_imports)] - use super::*; - - #[allow(dead_code)] - fn make_gzip_request_response( - accept_encoding_gzip: bool, - ) -> (middleware::Request, middleware::Response) { - let file = std::include_bytes!("../../../assets/test_file.hbs"); - let request = if accept_encoding_gzip { - let mut req = Request::new(Body::empty()); - - req.headers_mut().append( - http::header::ACCEPT_ENCODING, - HeaderValue::from_str("gzip, deflate").unwrap(), - ); - - Arc::new(Mutex::new(req)) - } else { - Arc::new(Mutex::new(Request::new(Body::empty()))) - }; - let response_builder = - HttpResponseBuilder::new().header(http::header::CONTENT_TYPE, "text/html"); - let response_body = Body::from(file.to_vec()); - - let response = response_builder.body(response_body).unwrap(); - let response = Arc::new(Mutex::new(response)); - - (request, response) - } - - #[test] - fn gzip_compression_header() { - let raw = b"aabbaabbaabbaabb\n"; - let compressed = compress(raw).unwrap(); - let expect: [u8; 27] = [ - 31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 75, 76, 76, 74, 74, 68, 194, 92, 0, 169, 225, 127, - 69, 17, 0, 0, 0, - ]; - - assert_eq!(compressed, expect); - } - - #[tokio::test] - async fn content_encoding_gzip() { - let (request, response) = make_gzip_request_response(true); - - compress_http_response(request, Arc::clone(&response)) - .await - .unwrap(); - - let compressed_response = response.lock().await; - - assert_eq!( - compressed_response - .headers() - .get(http::header::CONTENT_ENCODING) - .unwrap(), - "gzip" - ); - } - - #[tokio::test] - async fn compresses_body() { - let (request, response) = make_gzip_request_response(true); - let mut body_buffer = Vec::new(); - let mut compressed_body_buffer: Vec = Vec::new(); - - { - let mut response = response.lock().await; - let body = response.body_mut(); - - let mut buffer_cursor = aggregate(body).await.unwrap(); - - while buffer_cursor.has_remaining() { - body_buffer.push(buffer_cursor.get_u8()); - } - } - - compress_http_response(request, Arc::clone(&response)) - .await - .unwrap(); - - { - let mut compressed_response = response.lock().await; - let compressed_body = compressed_response.body_mut(); - - let mut buffer_cursor = aggregate(compressed_body).await.unwrap(); - - while buffer_cursor.has_remaining() { - compressed_body_buffer.push(buffer_cursor.get_u8()); - } - } - - assert_eq!(body_buffer.len(), 6364); - assert_eq!(compressed_body_buffer.len(), 20); - } -} diff --git a/src/addon/compression/mod.rs b/src/addon/compression/mod.rs deleted file mode 100644 index abcab22b..00000000 --- a/src/addon/compression/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod gzip; diff --git a/src/addon/cors.rs b/src/addon/cors.rs deleted file mode 100644 index ea51e816..00000000 --- a/src/addon/cors.rs +++ /dev/null @@ -1,377 +0,0 @@ -use anyhow::{Error, Result}; -use hyper::header::{self, HeaderName, HeaderValue}; -use std::convert::TryFrom; - -use crate::config::cors::CorsConfig; - -/// CORS (Cross Origin Resource Sharing) configuration for the HTTP/S -/// server. -/// -/// `CorsConfig` holds the configuration for the CORS headers for a -/// HTTP/S server instance. The following headers are supported: -/// -/// Access-Control-Allow-Credentials header -/// Access-Control-Allow-Headers header -/// Access-Control-Allow-Methods header -/// Access-Control-Expose-Headers header -/// Access-Control-Max-Age header -/// Access-Control-Request-Headers header -/// Access-Control-Request-Method header -/// -/// Refer to CORS here: https://www.w3.org/wiki/CORS -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Cors { - /// The Access-Control-Allow-Credentials response header tells browsers - /// whether to expose the response to frontend JavaScript code when the - /// request's credentials mode (Request.credentials) is include. - /// - /// The only valid value for this header is true (case-sensitive). If you - /// don't need credentials, omit this header entirely (rather than setting - /// its value to false). - /// - /// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials - pub(crate) allow_credentials: bool, - /// The Access-Control-Allow-Headers response header is used in response to a - /// preflight request which includes the Access-Control-Request-Headers to - /// indicate which HTTP headers can be used during the actual request. - /// - /// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers - pub(crate) allow_headers: Option>, - /// The Access-Control-Allow-Methods response header specifies the method or - /// methods allowed when accessing the resource in response to a preflight - /// request. - /// - /// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods - pub(crate) allow_methods: Option>, - /// The Access-Control-Allow-Origin response header indicates whether the - /// response can be shared with requesting code from the given origin. - /// - /// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin - pub(crate) allow_origin: Option, - /// The Access-Control-Expose-Headers response header allows a server to - /// indicate which response headers should be made available to scripts - /// running in the browser, in response to a cross-origin request. - /// - /// Only the CORS-safelisted response headers are exposed by default. - /// For clients to be able to access other headers, the server must list them - /// using the Access-Control-Expose-Headers header. - /// - /// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers - pub(crate) expose_headers: Option>, - /// The Access-Control-Max-Age response header indicates how long the results - /// of a preflight request (that is the information contained in the - /// Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) - /// can be cached. - /// - /// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age - pub(crate) max_age: Option, - /// The Access-Control-Request-Headers request header is used by browsers - /// when issuing a preflight request, to let the server know which HTTP - /// headers the client might send when the actual request is made (such as - /// with setRequestHeader()). This browser side header will be answered by - /// the complementary server side header of Access-Control-Allow-Headers. - /// - /// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers - pub(crate) request_headers: Option>, - /// The Access-Control-Request-Method request header is used by browsers when - /// issuing a preflight request, to let the server know which HTTP method will - /// be used when the actual request is made. This header is necessary as the - /// preflight request is always an OPTIONS and doesn't use the same method as - /// the actual request. - pub(crate) request_method: Option, -} - -impl Cors { - pub fn builder() -> CorsBuilder { - CorsBuilder { - config: Cors::default(), - } - } - - pub fn make_http_headers(&self) -> Vec<(HeaderName, HeaderValue)> { - let cors = self.clone(); - let mut cors_headers: Vec<(HeaderName, HeaderValue)> = Vec::new(); - - if self.allow_credentials { - cors_headers.push(( - header::ACCESS_CONTROL_ALLOW_CREDENTIALS, - HeaderValue::from_str("true").unwrap(), - )); - } - - if let Some(allow_headers) = cors.allow_headers { - let allow_headers = allow_headers.join(", "); - - cors_headers.push(( - header::ACCESS_CONTROL_ALLOW_HEADERS, - HeaderValue::from_str(allow_headers.as_str()).unwrap(), - )); - } - - if let Some(allow_methods) = cors.allow_methods { - let allow_methods = allow_methods.join(", "); - - cors_headers.push(( - header::ACCESS_CONTROL_ALLOW_METHODS, - HeaderValue::from_str(allow_methods.as_str()).unwrap(), - )); - } - - if let Some(allow_origin) = cors.allow_origin { - cors_headers.push(( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - HeaderValue::from_str(allow_origin.as_str()).unwrap(), - )); - } - - if let Some(expose_headers) = cors.expose_headers { - let expose_headers = expose_headers.join(", "); - - cors_headers.push(( - header::ACCESS_CONTROL_EXPOSE_HEADERS, - HeaderValue::from_str(expose_headers.as_str()).unwrap(), - )); - } - - if let Some(max_age) = cors.max_age { - cors_headers.push(( - header::ACCESS_CONTROL_MAX_AGE, - HeaderValue::from_str(max_age.to_string().as_str()).unwrap(), - )); - } - - if let Some(request_headers) = cors.request_headers { - let request_headers = request_headers.join(", "); - - cors_headers.push(( - header::ACCESS_CONTROL_REQUEST_HEADERS, - HeaderValue::from_str(request_headers.as_str()).unwrap(), - )); - } - - if let Some(request_method) = cors.request_method { - cors_headers.push(( - header::ACCESS_CONTROL_REQUEST_METHOD, - HeaderValue::from_str(request_method.as_str()).unwrap(), - )); - } - - cors_headers - } -} - -/// CorsConfig Builder -pub struct CorsBuilder { - config: Cors, -} - -impl CorsBuilder { - pub fn allow_origin(mut self, origin: String) -> Self { - self.config.allow_origin = Some(origin); - self - } - - pub fn allow_methods(mut self, methods: Vec) -> Self { - self.config.allow_methods = Some(methods); - self - } - - pub fn allow_headers(mut self, headers: Vec) -> Self { - self.config.allow_headers = Some(headers); - self - } - - pub fn allow_credentials(mut self) -> Self { - self.config.allow_credentials = true; - self - } - - pub fn max_age(mut self, duration: u64) -> Self { - self.config.max_age = Some(duration); - self - } - - pub fn expose_headers(mut self, headers: Vec) -> Self { - self.config.expose_headers = Some(headers); - self - } - - pub fn request_headers(mut self, headers: Vec) -> Self { - self.config.request_headers = Some(headers); - self - } - - pub fn request_method(mut self, method: String) -> Self { - self.config.request_method = Some(method); - self - } - - pub fn build(self) -> Cors { - self.config - } -} - -impl TryFrom for Cors { - type Error = Error; - - fn try_from(value: CorsConfig) -> Result { - let mut builder = Cors::builder(); - - if value.allow_credentials { - builder = builder.allow_credentials(); - } - - if let Some(headers) = value.allow_headers { - builder = builder.allow_headers(headers); - } - - if let Some(methods) = value.allow_methods { - builder = builder.allow_methods(methods); - } - - if let Some(origin) = value.allow_origin { - builder = builder.allow_origin(origin); - } - - if let Some(max_age) = value.max_age { - builder = builder.max_age(max_age); - } - - if let Some(expose_headers) = value.expose_headers { - builder = builder.expose_headers(expose_headers); - } - - if let Some(request_headers) = value.request_headers { - builder = builder.request_headers(request_headers); - } - - if let Some(request_method) = value.request_method { - builder = builder.request_method(request_method); - } - - Ok(builder.build()) - } -} - -#[cfg(test)] -mod tests { - #[allow(unused_imports)] - use super::*; - - #[test] - fn creates_cors_config_with_builder() { - let cors_config = Cors::builder() - .allow_origin("http://example.com".to_string()) - .allow_methods(vec![ - "GET".to_string(), - "POST".to_string(), - "PUT".to_string(), - "DELETE".to_string(), - ]) - .allow_headers(vec![ - "Content-Type".to_string(), - "Origin".to_string(), - "Content-Length".to_string(), - ]) - .build(); - - assert_eq!( - cors_config.allow_origin, - Some(String::from("http://example.com")) - ); - assert_eq!( - cors_config.allow_methods, - Some(vec![ - String::from("GET"), - String::from("POST"), - String::from("PUT"), - String::from("DELETE"), - ]) - ); - assert_eq!( - cors_config.allow_headers, - Some(vec![ - String::from("Content-Type"), - String::from("Origin"), - String::from("Content-Length"), - ]) - ); - assert!(!cors_config.allow_credentials); - assert_eq!(cors_config.max_age, None); - assert_eq!(cors_config.expose_headers, None); - assert_eq!(cors_config.request_headers, None); - assert_eq!(cors_config.request_method, None); - } - - #[test] - fn creates_cors_config_which_allows_all_connections() { - let cors_config = CorsConfig::allow_all(); - - assert_eq!(cors_config.allow_origin, Some(String::from("*"))); - assert_eq!( - cors_config.allow_methods, - Some(vec![ - String::from("GET"), - String::from("POST"), - String::from("PUT"), - String::from("PATCH"), - String::from("DELETE"), - String::from("HEAD"), - ]) - ); - assert_eq!( - cors_config.allow_headers, - Some(vec![ - String::from("Origin"), - String::from("Content-Length"), - String::from("Content-Type"), - ]) - ); - assert!(!cors_config.allow_credentials); - assert_eq!(cors_config.max_age, Some(43200)); - assert_eq!(cors_config.expose_headers, None); - assert_eq!(cors_config.request_headers, None); - assert_eq!(cors_config.request_method, None); - } - - #[test] - fn creates_cors_config_from_file() { - let allow_headers = vec![ - "content-type".to_string(), - "content-length".to_string(), - "request-id".to_string(), - ]; - let allow_mehtods = vec!["GET".to_string(), "POST".to_string(), "PUT".to_string()]; - let allow_origin = String::from("github.com"); - let expose_headers = vec!["content-type".to_string(), "request-id".to_string()]; - let max_age = 5400; - let request_headers = vec![ - "content-type".to_string(), - "content-length".to_string(), - "authorization".to_string(), - ]; - let request_method = String::from("GET"); - let config = CorsConfig { - allow_credentials: true, - allow_headers: Some(allow_headers.clone()), - allow_methods: Some(allow_mehtods.clone()), - allow_origin: Some(allow_origin.clone()), - expose_headers: Some(expose_headers.clone()), - max_age: Some(max_age), - request_headers: Some(request_headers.clone()), - request_method: Some(request_method.clone()), - }; - let cors = Cors { - allow_credentials: true, - allow_headers: Some(allow_headers), - allow_methods: Some(allow_mehtods), - allow_origin: Some(allow_origin), - expose_headers: Some(expose_headers), - max_age: Some(max_age), - request_headers: Some(request_headers), - request_method: Some(request_method), - }; - - assert_eq!(cors, Cors::try_from(config).unwrap()); - } -} diff --git a/src/addon/file_server/file.rs b/src/addon/file_server/file.rs deleted file mode 100644 index 8d003f54..00000000 --- a/src/addon/file_server/file.rs +++ /dev/null @@ -1,106 +0,0 @@ -use anyhow::{Context, Result}; -use chrono::{DateTime, Local}; -use futures::Stream; -use hyper::body::Bytes; -use mime_guess::{from_path, Mime}; -use std::fs::Metadata; -use std::mem::MaybeUninit; -use std::path::PathBuf; -use std::pin::Pin; -use std::task::{self, Poll}; -use tokio::io::{AsyncRead, ReadBuf}; - -pub const FILE_BUFFER_SIZE: usize = 8 * 1024; - -pub type FileBuffer = Box<[MaybeUninit; FILE_BUFFER_SIZE]>; - -/// Wrapper around `tokio::fs::File` built from a OS ScopedFileSystem file -/// providing `std::fs::Metadata` and the path to such file -#[derive(Debug)] -pub struct File { - pub path: PathBuf, - pub file: tokio::fs::File, - pub metadata: Metadata, -} - -impl File { - pub fn new(path: PathBuf, file: tokio::fs::File, metadata: Metadata) -> Self { - File { - path, - file, - metadata, - } - } - - pub fn mime(&self) -> Mime { - from_path(self.path.clone()).first_or_octet_stream() - } - - pub fn size(&self) -> u64 { - self.metadata.len() - } - - pub fn last_modified(&self) -> Result> { - let modified = self - .metadata - .modified() - .context("Failed to read last modified time for file")?; - let modified: DateTime = modified.into(); - - Ok(modified) - } - - #[allow(dead_code)] - pub fn bytes(self) -> Vec { - let byte_stream = ByteStream { - file: self.file, - buffer: Box::new([MaybeUninit::uninit(); FILE_BUFFER_SIZE]), - }; - - byte_stream - .buffer - .iter() - .map(|muint| unsafe { muint.assume_init() }) - .collect::>() - } -} - -pub struct ByteStream { - file: tokio::fs::File, - buffer: FileBuffer, -} - -impl From for ByteStream { - fn from(file: File) -> Self { - ByteStream { - file: file.file, - buffer: Box::new([MaybeUninit::uninit(); FILE_BUFFER_SIZE]), - } - } -} - -impl Stream for ByteStream { - type Item = Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - let ByteStream { - ref mut file, - ref mut buffer, - } = *self; - let mut read_buffer = ReadBuf::uninit(&mut buffer[..]); - - match Pin::new(file).poll_read(cx, &mut read_buffer) { - Poll::Ready(Ok(())) => { - let filled = read_buffer.filled(); - - if filled.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::copy_from_slice(filled)))) - } - } - Poll::Ready(Err(error)) => Poll::Ready(Some(Err(error.into()))), - Poll::Pending => Poll::Pending, - } - } -} diff --git a/src/addon/file_server/http_utils.rs b/src/addon/file_server/http_utils.rs deleted file mode 100644 index 5c2c96a5..00000000 --- a/src/addon/file_server/http_utils.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::fmt::Display; -use std::mem::MaybeUninit; -use std::pin::Pin; -use std::task::{self, Poll}; - -use anyhow::{Context, Result}; -use chrono::{DateTime, Local, Utc}; -use futures::Stream; -use http::response::Builder as HttpResponseBuilder; -use hyper::body::Body; -use hyper::body::Bytes; -use tokio::io::{AsyncRead, ReadBuf}; - -use super::file::File; - -const FILE_BUFFER_SIZE: usize = 8 * 1024; - -pub type FileBuffer = Box<[MaybeUninit; FILE_BUFFER_SIZE]>; - -/// HTTP Response `Cache-Control` directive -/// -/// Allow dead code until we have support for cache control configuration -#[allow(dead_code)] - -pub enum CacheControlDirective { - /// Cache-Control: must-revalidate - MustRevalidate, - /// Cache-Control: no-cache - NoCache, - /// Cache-Control: no-store - NoStore, - /// Cache-Control: no-transform - NoTransform, - /// Cache-Control: public - Public, - /// Cache-Control: private - Private, - /// Cache-Control: proxy-revalidate - ProxyRavalidate, - /// Cache-Control: max-age= - MaxAge(u64), - /// Cache-Control: s-maxage= - SMaxAge(u64), -} - -impl Display for CacheControlDirective { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let string = match &self { - Self::MustRevalidate => String::from("must-revalidate"), - Self::NoCache => String::from("no-cache"), - Self::NoStore => String::from("no-store"), - Self::NoTransform => String::from("no-transform"), - Self::Public => String::from("public"), - Self::Private => String::from("private"), - Self::ProxyRavalidate => String::from("proxy-revalidate"), - Self::MaxAge(age) => format!("max-age={}", age), - Self::SMaxAge(age) => format!("s-maxage={}", age), - }; - - write!(f, "{string}") - } -} - -pub struct ResponseHeaders { - cache_control: String, - content_length: u64, - content_type: String, - etag: String, - last_modified: String, -} - -impl ResponseHeaders { - pub fn new( - file: &File, - cache_control_directive: CacheControlDirective, - ) -> Result { - let last_modified = file.last_modified()?; - - Ok(ResponseHeaders { - cache_control: cache_control_directive.to_string(), - content_length: ResponseHeaders::content_length(file), - content_type: ResponseHeaders::content_type(file), - etag: ResponseHeaders::etag(file, &last_modified), - last_modified: ResponseHeaders::last_modified(&last_modified), - }) - } - - fn content_length(file: &File) -> u64 { - file.size() - } - - fn content_type(file: &File) -> String { - file.mime().to_string() - } - - fn etag(file: &File, last_modified: &DateTime) -> String { - format!( - "W/\"{0:x}-{1:x}.{2:x}\"", - file.size(), - last_modified.timestamp(), - last_modified.timestamp_subsec_nanos(), - ) - } - - fn last_modified(last_modified: &DateTime) -> String { - format!( - "{} GMT", - last_modified - .with_timezone(&Utc) - .format("%a, %e %b %Y %H:%M:%S") - ) - } -} - -pub async fn make_http_file_response( - file: File, - cache_control_directive: CacheControlDirective, -) -> Result> { - let headers = ResponseHeaders::new(&file, cache_control_directive)?; - let builder = HttpResponseBuilder::new() - .header(http::header::CONTENT_LENGTH, headers.content_length) - .header(http::header::CACHE_CONTROL, headers.cache_control) - .header(http::header::CONTENT_TYPE, headers.content_type) - .header(http::header::ETAG, headers.etag) - .header(http::header::LAST_MODIFIED, headers.last_modified); - - let body = file_bytes_into_http_body(file).await; - let response = builder - .body(body) - .context("Failed to build HTTP File Response")?; - - Ok(response) -} - -pub async fn file_bytes_into_http_body(file: File) -> Body { - let byte_stream = ByteStream { - file: file.file, - buffer: Box::new([MaybeUninit::uninit(); FILE_BUFFER_SIZE]), - }; - - Body::wrap_stream(byte_stream) -} - -pub struct ByteStream { - file: tokio::fs::File, - buffer: FileBuffer, -} - -impl Stream for ByteStream { - type Item = Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - let ByteStream { - ref mut file, - ref mut buffer, - } = *self; - let mut read_buffer = ReadBuf::uninit(&mut buffer[..]); - - match Pin::new(file).poll_read(cx, &mut read_buffer) { - Poll::Ready(Ok(())) => { - let filled = read_buffer.filled(); - - if filled.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::copy_from_slice(filled)))) - } - } - Poll::Ready(Err(error)) => Poll::Ready(Some(Err(error.into()))), - Poll::Pending => Poll::Pending, - } - } -} diff --git a/src/addon/file_server/mod.rs b/src/addon/file_server/mod.rs deleted file mode 100644 index fdcf6ec2..00000000 --- a/src/addon/file_server/mod.rs +++ /dev/null @@ -1,466 +0,0 @@ -mod directory_entry; -mod file; -mod http_utils; -mod query_params; -mod scoped_file_system; - -use chrono::{DateTime, Local}; - -pub use file::File; -use humansize::{format_size, DECIMAL}; - -pub use scoped_file_system::{Entry, ScopedFileSystem}; - -use anyhow::{Context, Result}; -use handlebars::{handlebars_helper, Handlebars}; -use http::response::Builder as HttpResponseBuilder; -use http::{StatusCode, Uri}; -use hyper::{Body, Response}; -use percent_encoding::{percent_decode_str, utf8_percent_encode}; -use std::fs::read_dir; -use std::path::{Component, Path, PathBuf}; -use std::str::FromStr; -use std::sync::Arc; - -use crate::config::Config; -use crate::utils::url_encode::{decode_uri, encode_uri, PERCENT_ENCODE_SET}; - -use self::directory_entry::{BreadcrumbItem, DirectoryEntry, DirectoryIndex, Sort}; -use self::http_utils::{make_http_file_response, CacheControlDirective}; -use self::query_params::{QueryParams, SortBy}; - -/// Explorer's Handlebars template filename -const EXPLORER_TEMPLATE: &str = "explorer"; - -pub struct FileServer { - handlebars: Arc>, - scoped_file_system: ScopedFileSystem, - config: Arc, -} - -impl<'a> FileServer { - /// Creates a new instance of the `FileExplorer` with the provided `root_dir` - pub fn new(config: Arc) -> Self { - let handlebars = FileServer::make_handlebars_engine(); - let scoped_file_system = ScopedFileSystem::new(config.root_dir.clone()).unwrap(); - - FileServer { - handlebars, - scoped_file_system, - config, - } - } - - /// Creates a new `Handlebars` instance with templates registered - fn make_handlebars_engine() -> Arc> { - let mut handlebars = Handlebars::new(); - - let template = std::include_bytes!("./template/explorer.hbs"); - let template = std::str::from_utf8(template).unwrap(); - - handlebars - .register_template_string(EXPLORER_TEMPLATE, template) - .unwrap(); - - handlebars_helper!(date: |d: Option>| { - match d { - Some(d) => d.format("%Y/%m/%d %H:%M:%S").to_string(), - None => "-".to_owned(), - } - }); - handlebars.register_helper("date", Box::new(date)); - - handlebars_helper!(size: |bytes: u64| format_size(bytes, DECIMAL)); - handlebars.register_helper("size", Box::new(size)); - - handlebars_helper!(sort_name: |sort: Sort| sort == Sort::Name); - handlebars.register_helper("sort_name", Box::new(sort_name)); - - handlebars_helper!(sort_size: |sort: Sort| sort == Sort::Size); - handlebars.register_helper("sort_size", Box::new(sort_size)); - - handlebars_helper!(sort_date_created: |sort: Sort| sort == Sort::DateCreated); - handlebars.register_helper("sort_date_created", Box::new(sort_date_created)); - - handlebars_helper!(sort_date_modified: |sort: Sort| sort == Sort::DateModified); - handlebars.register_helper("sort_date_modified", Box::new(sort_date_modified)); - - Arc::new(handlebars) - } - - fn parse_path(req_uri: &str) -> Result<(PathBuf, Option)> { - let uri = Uri::from_str(req_uri)?; - let uri_parts = uri.into_parts(); - - if let Some(path_and_query) = uri_parts.path_and_query { - let path = path_and_query.path(); - let query_params = if let Some(query_str) = path_and_query.query() { - Some(QueryParams::from_str(query_str)?) - } else { - None - }; - - return Ok((decode_uri(path), query_params)); - } - - Ok((PathBuf::from_str("/")?, None)) - } - - /// Resolves a HTTP Request to a file or directory. - /// - /// If the method of the HTTP Request is not `GET`, then responds with - /// `Bad Request` - /// - /// If URI doesn't matches a path relative to `root_dir`, then responds - /// with `Bad Reuest` - /// - /// If the HTTP Request URI points to `/` (root), the default behavior - /// would be to respond with `Not Found` but in order to provide `root_dir` - /// indexing, the request is handled and renders `root_dir` directory - /// listing instead. - /// - /// If the HTTP Request doesn't match any file relative to `root_dir` then - /// responds with 'Not Found' - /// - /// If the HTTP Request matches a forbidden file (User doesn't have - /// permissions to read), responds with `Forbidden` - /// - /// If the matched path resolves a directory, responds with the directory - /// listing page - /// - /// If the matched path resolves to a file, attempts to render it if the - /// MIME type is supported, otherwise returns the binary (downloadable file) - pub async fn resolve(&self, req_path: String) -> Result> { - let (path, query_params) = FileServer::parse_path(req_path.as_str())?; - - match self.scoped_file_system.resolve(path).await { - Ok(entry) => match entry { - Entry::Directory(dir) => { - if self.config.index { - let mut filepath = dir.path(); - - filepath.push("index.html"); - if let Ok(file) = tokio::fs::File::open(&filepath).await { - return make_http_file_response( - File { - path: filepath, - metadata: file.metadata().await?, - file, - }, - CacheControlDirective::MaxAge(2500), - ) - .await; - } - } - - self.render_directory_index(dir.path(), query_params).await - } - Entry::File(file) => { - make_http_file_response(*file, CacheControlDirective::MaxAge(2500)).await - } - }, - Err(err) => { - if self.config.spa { - return make_http_file_response( - { - let mut path = self.config.root_dir.clone(); - path.push("index.html"); - - let file = tokio::fs::File::open(&path).await?; - - let metadata = file.metadata().await?; - - File { - path, - metadata, - file, - } - }, - CacheControlDirective::MaxAge(2500), - ) - .await; - } - - let status = match err.kind() { - std::io::ErrorKind::NotFound => hyper::StatusCode::NOT_FOUND, - std::io::ErrorKind::PermissionDenied => hyper::StatusCode::FORBIDDEN, - _ => hyper::StatusCode::BAD_REQUEST, - }; - - let code = match err.kind() { - std::io::ErrorKind::NotFound => "404", - std::io::ErrorKind::PermissionDenied => "403", - _ => "400", - }; - - let response = hyper::Response::builder() - .status(status) - .header(http::header::CONTENT_TYPE, "text/html") - .body(hyper::Body::from( - handlebars::Handlebars::new().render_template( - include_str!("./template/error.hbs"), - &serde_json::json!({"error": err.to_string(), "code": code}), - )?, - ))?; - - Ok(response) - } - } - } - - /// Indexes the directory by creating a `DirectoryIndex`. Such `DirectoryIndex` - /// is used to build the Handlebars "Explorer" template using the Handlebars - /// engine and builds an HTTP Response containing such file - async fn render_directory_index( - &self, - path: PathBuf, - query_params: Option, - ) -> Result> { - let directory_index = - FileServer::index_directory(self.config.root_dir.clone(), path, query_params)?; - let html = self - .handlebars - .render(EXPLORER_TEMPLATE, &directory_index) - .unwrap(); - - let body = Body::from(html); - - Ok(HttpResponseBuilder::new() - .header(http::header::CONTENT_TYPE, "text/html") - .status(StatusCode::OK) - .body(body) - .expect("Failed to build response")) - } - - /// Encodes a `PathBuf` component using `PercentEncode` with UTF-8 charset. - /// - /// # Panics - /// - /// If the component's `OsStr` representation doesn't belong to valid UTF-8 - /// this function panics. - fn encode_component(comp: Component) -> String { - let component = comp - .as_os_str() - .to_str() - .expect("The provided OsStr doesn't belong to the UTF-8 charset."); - - utf8_percent_encode(component, PERCENT_ENCODE_SET).to_string() - } - - fn breadcrumbs_from_path(root_dir: &Path, path: &Path) -> Result> { - let root_dir_name = root_dir - .components() - .last() - .unwrap() - .as_os_str() - .to_str() - .expect("The first path component is not UTF-8 charset compliant."); - let stripped = path - .strip_prefix(root_dir)? - .components() - .map(FileServer::encode_component) - .collect::>(); - - let mut breadcrumbs = stripped - .iter() - .enumerate() - .map(|(idx, entry_name)| BreadcrumbItem { - entry_name: percent_decode_str(entry_name) - .decode_utf8() - .expect("The path name is not UTF-8 compliant") - .to_string(), - entry_link: format!("/{}", stripped[0..=idx].join("/")), - }) - .collect::>(); - - breadcrumbs.insert( - 0, - BreadcrumbItem { - entry_name: String::from(root_dir_name), - entry_link: String::from("/"), - }, - ); - - Ok(breadcrumbs) - } - - /// Creates a `DirectoryIndex` with the provided `root_dir` and `path` - /// (HTTP Request URI) - fn index_directory( - root_dir: PathBuf, - path: PathBuf, - query_params: Option, - ) -> Result { - let breadcrumbs = FileServer::breadcrumbs_from_path(&root_dir, &path)?; - let entries = read_dir(path).context("Unable to read directory")?; - let mut directory_entries: Vec = Vec::new(); - - for entry in entries { - let entry = entry.context("Unable to read entry")?; - let metadata = entry.metadata()?; - let date_created = if let Ok(time) = metadata.created() { - Some(time.into()) - } else { - None - }; - let date_modified = if let Ok(time) = metadata.modified() { - Some(time.into()) - } else { - None - }; - - directory_entries.push(DirectoryEntry { - display_name: entry - .file_name() - .to_str() - .context("Unable to gather file name into a String")? - .to_string(), - is_dir: metadata.is_dir(), - size_bytes: metadata.len(), - entry_path: FileServer::make_dir_entry_link(&root_dir, &entry.path()), - date_created, - date_modified, - }); - } - - if let Some(query_params) = query_params { - if let Some(sort_by) = query_params.sort_by { - match sort_by { - SortBy::Name => { - directory_entries.sort_by_key(|entry| entry.display_name.clone()); - } - SortBy::Size => directory_entries.sort_by_key(|entry| entry.size_bytes), - SortBy::DateCreated => { - directory_entries.sort_by_key(|entry| entry.date_created) - } - SortBy::DateModified => { - directory_entries.sort_by_key(|entry| entry.date_modified) - } - }; - - let sort_enum = match sort_by { - SortBy::Name => Sort::Name, - SortBy::Size => Sort::Size, - SortBy::DateCreated => Sort::DateCreated, - SortBy::DateModified => Sort::DateModified, - }; - - return Ok(DirectoryIndex { - entries: directory_entries, - breadcrumbs, - sort: sort_enum, - }); - } - } - - directory_entries.sort(); - - Ok(DirectoryIndex { - entries: directory_entries, - breadcrumbs, - sort: Sort::Directory, - }) - } - - /// Creates entry's relative path. Used by Handlebars template engine to - /// provide navigation through `FileExplorer` - /// - /// If the root_dir is: `https-server/src` - /// The entry path is: `https-server/src/server/service/file_explorer.rs` - /// - /// Then the resulting path from this function is the absolute path to - /// the "entry path" in relation to the "root_dir" path. - /// - /// This happens because links should behave relative to the `/` path - /// which in this case is `http-server/src` instead of system's root path. - fn make_dir_entry_link(root_dir: &Path, entry_path: &Path) -> String { - let path = entry_path.strip_prefix(root_dir).unwrap(); - - encode_uri(path) - } -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - use std::str::FromStr; - use std::vec; - - use super::{BreadcrumbItem, FileServer}; - - #[test] - fn makes_dir_entry_link() { - let root_dir = PathBuf::from_str("/Users/bob/sources/http-server").unwrap(); - let entry_path = - PathBuf::from_str("/Users/bob/sources/http-server/src/server/service/testing stuff/filename with spaces.txt") - .unwrap(); - - assert_eq!( - "/src/server/service/testing%20stuff/filename%20with%20spaces.txt", - FileServer::make_dir_entry_link(&root_dir, &entry_path) - ); - } - - #[test] - fn parse_req_uri_path() { - let have = [ - "/index.html", - "/index.html?foo=1234", - "/foo/index.html?bar=baz", - "/foo/bar/baz.html?day=6&month=27&year=2021", - ]; - - let want = [ - "/index.html", - "/index.html", - "/foo/index.html", - "/foo/bar/baz.html", - ]; - - for (idx, req_uri) in have.iter().enumerate() { - let sanitized_path = FileServer::parse_path(req_uri).unwrap().0; - let wanted_path = PathBuf::from_str(want[idx]).unwrap(); - - assert_eq!(sanitized_path, wanted_path); - } - } - - #[test] - fn breadcrumbs_from_paths() { - let root_dir = PathBuf::from_str("/Users/bob/sources/http-server").unwrap(); - let entry_path = - PathBuf::from_str("/Users/bob/sources/http-server/src/server/service/testing stuff/filename with spaces.txt") - .unwrap(); - let breadcrumbs = FileServer::breadcrumbs_from_path(&root_dir, &entry_path).unwrap(); - let expect = vec![ - BreadcrumbItem { - entry_name: String::from("http-server"), - entry_link: String::from("/"), - }, - BreadcrumbItem { - entry_name: String::from("src"), - entry_link: String::from("/src"), - }, - BreadcrumbItem { - entry_name: String::from("server"), - entry_link: String::from("/src/server"), - }, - BreadcrumbItem { - entry_name: String::from("service"), - entry_link: String::from("/src/server/service"), - }, - BreadcrumbItem { - entry_name: String::from("testing stuff"), - entry_link: String::from("/src/server/service/testing%20stuff"), - }, - BreadcrumbItem { - entry_name: String::from("filename with spaces.txt"), - entry_link: String::from( - "/src/server/service/testing%20stuff/filename%20with%20spaces.txt", - ), - }, - ]; - - assert_eq!(breadcrumbs, expect); - } -} diff --git a/src/addon/file_server/query_params.rs b/src/addon/file_server/query_params.rs deleted file mode 100644 index c548b1a4..00000000 --- a/src/addon/file_server/query_params.rs +++ /dev/null @@ -1,78 +0,0 @@ -use anyhow::Error; -use serde::Serialize; -use std::str::FromStr; - -#[derive(Debug, Eq, PartialEq, Serialize)] -pub enum SortBy { - Name, - Size, - DateCreated, - DateModified, -} - -impl FromStr for SortBy { - type Err = Error; - - fn from_str(s: &str) -> Result { - let lower = s.to_ascii_lowercase(); - let lower = lower.as_str(); - - match lower { - "name" => Ok(Self::Name), - "size" => Ok(Self::Size), - "date_created" => Ok(Self::DateCreated), - "date_modified" => Ok(Self::DateModified), - _ => Err(Error::msg("Value doesnt correspond")), - } - } -} - -#[derive(Debug, Default, PartialEq, Eq)] -pub struct QueryParams { - pub(crate) sort_by: Option, -} - -impl FromStr for QueryParams { - type Err = Error; - - fn from_str(s: &str) -> Result { - let mut query_params = QueryParams::default(); - - for set in s.split('&') { - let mut it = set.split('=').take(2); - - if let (Some(key), Some(value)) = (it.next(), it.next()) { - match key { - "sort_by" => { - if let Ok(sort_value) = SortBy::from_str(value) { - query_params.sort_by = Some(sort_value); - } - } - _ => continue, - } - } - - continue; - } - - Ok(query_params) - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::{QueryParams, SortBy}; - - #[test] - fn builds_query_params_from_str() { - let uri_string = "sort_by=name"; - let have = QueryParams::from_str(uri_string).unwrap(); - let expect = QueryParams { - sort_by: Some(SortBy::Name), - }; - - assert_eq!(have, expect); - } -} diff --git a/src/addon/file_server/scoped_file_system.rs b/src/addon/file_server/scoped_file_system.rs deleted file mode 100644 index d1145148..00000000 --- a/src/addon/file_server/scoped_file_system.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! FileSystem wrapper to navigate safely around the children of a root -//! directory. -//! -//! The `ScopedFileSystem` provides read capabilities on a single directory -//! and its children, either files and directories will be accessed by this -//! `ScopedFileSystem` instance. -//! -//! The `Entry` is a wrapper on OS file system entries such as `File` and -//! `Directory`. Both `File` and `Directory` are primitive types for -//! `ScopedFileSystem` -use anyhow::Result; -use std::path::{Component, Path, PathBuf}; -use tokio::fs::OpenOptions; - -use super::file::File; - -/// The file is being opened or created for a backup or restore operation. -/// The system ensures that the calling process overrides file security -/// checks when the process has SE_BACKUP_NAME and SE_RESTORE_NAME privileges. -/// For more information, see Changing Privileges in a Token. -/// You must set this flag to obtain a handle to a directory. -/// A directory handle can be passed to some functions instead of a file handle. -/// -/// Refer: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea -#[cfg(target_os = "windows")] -const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x02000000; - -/// Representation of a OS ScopedFileSystem directory providing the path -/// (`PathBuf`) -#[derive(Debug)] -pub struct Directory { - path: PathBuf, -} - -impl Directory { - pub fn path(&self) -> PathBuf { - self.path.clone() - } -} - -/// Any OS filesystem entry recognized by `ScopedFileSystem` is treated as a -/// `Entry` both `File` and `Directory` are possible values with full support by -/// `ScopedFileSystem` -#[derive(Debug)] -pub enum Entry { - File(Box), - Directory(Directory), -} - -/// `ScopedFileSystem` to resolve and serve static files relative to an specific -/// file system directory -#[derive(Clone)] -pub struct ScopedFileSystem { - pub root: PathBuf, -} - -impl ScopedFileSystem { - /// Creates a new instance of `ScopedFileSystem` using the provided PathBuf - /// as the root directory to serve files from. - /// - /// Provided paths will resolve relartive to the provided `root` directory. - pub fn new(root: PathBuf) -> Result { - Ok(ScopedFileSystem { root }) - } - - /// Resolves the provided path against the root directory of this - /// `ScopedFileSystem` instance. - /// - /// A relative path is built using `build_relative_path` and then is opened - /// to retrieve a `Entry`. - pub async fn resolve(&self, path: PathBuf) -> std::io::Result { - let entry_path = self.build_relative_path(path); - - ScopedFileSystem::open(entry_path).await - } - - /// Builds a path relative to `ScopedFileSystem`'s `root` path with the - /// provided path. - fn build_relative_path(&self, path: PathBuf) -> PathBuf { - let mut root = self.root.clone(); - - root.extend(&ScopedFileSystem::normalize_path(&path)); - - root - } - - /// Normalizes paths - /// - /// ```ignore - /// docs/collegue/cs50/lectures/../code/voting_excecise - /// ``` - /// - /// Will be normalized to be: - /// - /// ```ignore - /// docs/collegue/cs50/code/voting_excecise - /// ``` - fn normalize_path(path: &Path) -> PathBuf { - path.components() - .fold(PathBuf::new(), |mut result, p| match p { - Component::ParentDir => { - result.pop(); - result - } - Component::Normal(os_string) => { - result.push(os_string); - result - } - _ => result, - }) - } - - #[cfg(not(target_os = "windows"))] - async fn open(path: PathBuf) -> std::io::Result { - let mut open_options = OpenOptions::new(); - let entry_path: PathBuf = path.clone(); - let file = open_options.read(true).open(path).await?; - let metadata = file.metadata().await?; - - if metadata.is_dir() { - return Ok(Entry::Directory(Directory { path: entry_path })); - } - - Ok(Entry::File(Box::new(File::new(entry_path, file, metadata)))) - } - - #[cfg(target_os = "windows")] - async fn open(path: PathBuf) -> std::io::Result { - let mut open_options = OpenOptions::new(); - let entry_path: PathBuf = path.clone(); - let file = open_options - .read(true) - .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) - .open(path) - .await?; - let metadata = file.metadata().await?; - - if metadata.is_dir() { - return Ok(Entry::Directory(Directory { path: entry_path })); - } - - Ok(Entry::File(Box::new(File::new(entry_path, file, metadata)))) - } -} - -#[cfg(test)] -mod tests { - #[allow(unused_imports)] - use super::*; - - #[test] - fn builds_a_relative_path_to_root_from_provided_path() { - let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap(); - let mut root_path = sfs.root.clone(); - let file_path = PathBuf::from(".github/ISSUE_TEMPLATE/feature-request.md"); - - root_path.push(file_path); - - let resolved_path = - sfs.build_relative_path(PathBuf::from(".github/ISSUE_TEMPLATE/feature-request.md")); - - assert_eq!(root_path, resolved_path); - } - - #[test] - fn builds_a_relative_path_to_root_from_forward_slash() { - let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap(); - let resolved_path = sfs.build_relative_path(PathBuf::from("/")); - - assert_eq!(sfs.root, resolved_path); - } - - #[test] - fn builds_a_relative_path_to_root_from_backwards() { - let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap(); - let resolved_path = sfs.build_relative_path(PathBuf::from("../../")); - - assert_eq!(sfs.root, resolved_path); - } - - #[test] - fn builds_an_invalid_path_if_an_arbitrary_path_is_provided() { - let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap(); - let resolved_path = - sfs.build_relative_path(PathBuf::from("unexistent_dir/confidential/recipe.txt")); - - assert_ne!(sfs.root, resolved_path); - } - - #[test] - fn normalizes_an_arbitrary_path() { - let arbitrary_path = PathBuf::from("docs/collegue/cs50/lectures/../code/voting_excecise"); - let normalized = ScopedFileSystem::normalize_path(&arbitrary_path); - - assert_eq!( - normalized.to_str().unwrap(), - "docs/collegue/cs50/code/voting_excecise" - ); - } - - #[tokio::test] - async fn resolves_a_file() { - let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap(); - let resolved_entry = sfs.resolve(PathBuf::from("assets/logo.svg")).await.unwrap(); - - if let Entry::File(file) = resolved_entry { - assert!(file.metadata.is_file()); - } else { - panic!("Found a directory instead of a file in the provied path"); - } - } - - #[tokio::test] - async fn detect_directory_paths() { - let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap(); - let resolved_entry = sfs.resolve(PathBuf::from("assets/")).await.unwrap(); - - assert!(matches!(resolved_entry, Entry::Directory(_))); - } - - #[tokio::test] - async fn detect_directory_paths_without_postfixed_slash() { - let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap(); - let resolved_entry = sfs.resolve(PathBuf::from("assets")).await.unwrap(); - - assert!(matches!(resolved_entry, Entry::Directory(_))); - } - - #[tokio::test] - async fn returns_error_if_file_doesnt_exists() { - let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap(); - let resolved_entry = sfs - .resolve(PathBuf::from("assets/unexistent_file.doc")) - .await; - - assert!(resolved_entry.is_err()); - } -} diff --git a/src/addon/file_server/template/error.hbs b/src/addon/file_server/template/error.hbs deleted file mode 100644 index e5e340ed..00000000 --- a/src/addon/file_server/template/error.hbs +++ /dev/null @@ -1,4 +0,0 @@ -
-

{{code}}

-

{{error}}

-
\ No newline at end of file diff --git a/src/addon/file_server/template/explorer.hbs b/src/addon/file_server/template/explorer.hbs deleted file mode 100644 index 75c7939d..00000000 --- a/src/addon/file_server/template/explorer.hbs +++ /dev/null @@ -1,249 +0,0 @@ - - - - - File Explorer - - - - -
- -
- -
-
- - diff --git a/src/addon/logger.rs b/src/addon/logger.rs deleted file mode 100644 index cdddeeac..00000000 --- a/src/addon/logger.rs +++ /dev/null @@ -1,93 +0,0 @@ -use anyhow::Result; -use chrono::{DateTime, Utc}; -use http::header::USER_AGENT; -use http::Method; -use hyper::Body; -use std::io::Write; -use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; - -use crate::server::middleware::{Request, Response}; - -pub struct Logger { - buffer_writer: BufferWriter, -} - -impl Logger { - pub fn new() -> Self { - let buffer_writer = BufferWriter::stdout(ColorChoice::Always); - - Logger { buffer_writer } - } - - pub async fn log(&mut self, request: Request, response: Response) -> Result<()> { - let mut buffer = self.buffer_writer.buffer(); - let request = request.lock().await; - let response = response.lock().await; - - // UTC Time - let moment: DateTime = Utc::now(); - write!(&mut buffer, "[{:?}] \"", moment)?; - - // HTTP Request Method - let method = request.method(); - - match *method { - Method::GET => buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?, - Method::POST | Method::PUT | Method::PATCH => { - buffer.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))? - } - Method::DELETE => buffer.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?, - _ => buffer.set_color(ColorSpec::new().set_fg(Some(Color::Magenta)))?, - }; - - write!(&mut buffer, "{} ", method)?; - buffer.reset()?; - - // HTTP Request URI and Version - write!(&mut buffer, "{} {:?} ", request.uri(), request.version())?; - - // HTTP Response Status Code - match response.status().as_u16() { - 100..=199 => { - // Informational Responses - buffer.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?; - } - 200..=299 => { - // Successful responses - buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?; - } - 300..=399 => { - // Redirection messages - buffer.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?; - } - 400..=499 => { - // Client error responses - buffer.set_color(ColorSpec::new().set_fg(Some(Color::Rgb(255, 140, 0))))?; - } - 500..=599 => { - // Server error responses - buffer.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?; - } - _ => { - // Unknown response codes - buffer.set_color(ColorSpec::new().set_fg(Some(Color::Magenta)))?; - } - } - write!(&mut buffer, "{}", response.status())?; - buffer.reset()?; - - // HTTP Request User Agent - let user_agent = if let Some(value) = request.headers().get(USER_AGENT) { - value.to_str()? - } else { - "N/A" - }; - write!(&mut buffer, "\" \"{}\" ", user_agent)?; - - writeln!(&mut buffer)?; - self.buffer_writer.print(&buffer)?; - buffer.reset()?; - - Ok(()) - } -} diff --git a/src/addon/mod.rs b/src/addon/mod.rs deleted file mode 100644 index 9987880c..00000000 --- a/src/addon/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod compression; -pub mod cors; -pub mod file_server; -pub mod logger; -pub mod proxy; diff --git a/src/addon/proxy.rs b/src/addon/proxy.rs deleted file mode 100644 index fb841692..00000000 --- a/src/addon/proxy.rs +++ /dev/null @@ -1,280 +0,0 @@ -use std::str::FromStr; -use std::sync::Arc; - -use http::header::USER_AGENT; -use http::header::{HeaderName, HeaderValue}; -use hyper::client::HttpConnector; -use hyper::{Body, Client, Response, Uri}; -use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; - -use crate::server::middleware::Request; - -pub struct Proxy { - client: Client>, - upstream: Uri, -} - -impl Proxy { - pub fn new(upstream: &str) -> Self { - let https_connector = HttpsConnectorBuilder::new() - .with_webpki_roots() - .https_or_http() - .enable_http1() - .build(); - let client = Client::builder().build::<_, hyper::Body>(https_connector); - let upstream = Uri::from_str(upstream).unwrap(); - - Proxy { client, upstream } - } - - pub async fn handle(&self, request: Request) -> Response { - self.remove_hbh_headers(Arc::clone(&request)).await; - self.add_via_header(Arc::clone(&request)).await; - - let mut outogoing = self.map_incoming_request(Arc::clone(&request)).await; - let outgoing_headers = outogoing.headers_mut(); - - // Host must be the authority from the proxied URL - outgoing_headers.remove(http::header::HOST); - outgoing_headers.append( - http::header::HOST, - HeaderValue::from_str(self.upstream.authority().unwrap().as_str()).unwrap(), - ); - - let client = self.client.clone(); - - tokio::spawn(async move { client.request(outogoing).await.unwrap() }) - .await - .unwrap() - } - - /// Creates a `Via` HTTP header for the provided HTTP Request. - /// - /// The Via general header is added by proxies, both forward and reverse, and - /// can appear in the request or response headers. It is used for tracking - /// message forwards, avoiding request loops, and identifying the protocol - /// capabilities of senders along the request/response chain. - /// - /// Via: [ "/" ] [ ":" ] - /// - /// ## References - /// - /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Via - async fn add_via_header(&self, request: Request) { - let mut request = request.lock().await; - let via_header_str = format!("{:?} Rust http-server", request.version()); - let via_header = HeaderValue::from_str(&via_header_str).unwrap(); - - if let Some(current_via_header) = request.headers().get("via") { - let current_via_header = current_via_header.to_str().unwrap(); - - if current_via_header.contains(&via_header_str) { - return; - } - - let mut via_set = current_via_header.split(',').collect::>(); - - via_set.push(&via_header_str); - - let proxies_list = via_set.join(", "); - - request.headers_mut().remove(HeaderName::from_static("via")); - request.headers_mut().append( - HeaderName::from_static("via"), - HeaderValue::from_str(proxies_list.as_str()).unwrap(), - ); - return; - } - - request - .headers_mut() - .append(HeaderName::from_static("via"), via_header); - } - - /// Removes Hop-by-Hop headers which are only meaningful for a singles - /// transport-level connection and should not be stored by caches following - /// RFC2616. - /// - /// The following HTTP/1.1 headers are hop-by-hop headers: - /// - /// - Connection - /// - Keep-Alive - /// - Proxy-Authenticate - /// - Proxy-Authorization - /// - TE - /// - Trailers - /// - Transfer-Encoding - /// - Upgrade - /// - /// ## References - /// - /// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html - async fn remove_hbh_headers(&self, request: Request) { - use http::header::{ - CONNECTION, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, TE, TRAILER, TRANSFER_ENCODING, - UPGRADE, - }; - - let mut request = request.lock().await; - let headers = request.headers_mut(); - - headers.remove(CONNECTION); - headers.remove(HeaderName::from_static("keep-alive")); - headers.remove(PROXY_AUTHENTICATE); - headers.remove(PROXY_AUTHORIZATION); - headers.remove(TE); - headers.remove(TRAILER); - headers.remove(TRANSFER_ENCODING); - headers.remove(UPGRADE); - } - - /// Maps a _incoming_ HTTP request into a _outgoing_ HTTP request. - async fn map_incoming_request(&self, incoming: Request) -> hyper::Request { - let incoming = incoming.lock().await; - let upstream_uri = self.map_upstream_uri(incoming.uri()); - let mut request = hyper::Request::builder() - .uri(upstream_uri) - .method(incoming.method()) - .body(Body::empty()) - .unwrap(); - let headers = request.headers_mut(); - - *headers = incoming.headers().clone(); - - // TODO: Instead of append and removing it would be great to support - // some kind of `set` operation which adds if not present or replaces - // if present. - // - // Host must be the authority from the proxied URL - headers.remove(http::header::HOST); - headers.append( - http::header::HOST, - HeaderValue::from_str(self.upstream.authority().unwrap().as_str()).unwrap(), - ); - - // Specify Proxy as User Agent - headers.remove(USER_AGENT).unwrap(); - headers.append(USER_AGENT, HeaderValue::from_static("Rust http-server/1.0")); - - request - } - - fn map_upstream_uri(&self, incoming_uri: &Uri) -> Uri { - let scheme = self.upstream.scheme_str().unwrap(); - let authority = self.upstream.authority().unwrap().as_str(); - let path_and_query = if let Some(paq) = incoming_uri.path_and_query() { - paq.as_str() - } else { - "" - }; - - Uri::builder() - .scheme(scheme) - .authority(authority) - .path_and_query(path_and_query) - .build() - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use http::header::{HeaderName, HeaderValue}; - use http::header::{ - CONNECTION, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, TE, TRAILER, TRANSFER_ENCODING, - UPGRADE, - }; - use hyper::Body; - use std::sync::Arc; - use tokio::sync::Mutex; - - use super::Proxy; - - #[tokio::test] - async fn adds_via_header_if_not_present() { - let proxy = Proxy::new("https://example.com"); - let request = http::Request::new(Body::empty()); - let request = Arc::new(Mutex::new(request)); - - proxy.add_via_header(Arc::clone(&request)).await; - - let request = request.lock().await; - let headers = request.headers(); - - assert!(headers.get(&HeaderName::from_static("via")).is_some()); - - let via_header_value = headers.get(&HeaderName::from_static("via")).unwrap(); - let via_header_value = via_header_value.to_str().unwrap(); - - assert_eq!(via_header_value, "HTTP/1.1 Rust http-server"); - } - - #[tokio::test] - async fn appends_via_header_if_another_is_present() { - let proxy = Proxy::new("https://example.com"); - let mut request = http::Request::new(Body::empty()); - let headers = request.headers_mut(); - - headers.append( - &HeaderName::from_static("via"), - HeaderValue::from_str("HTTP/1.1 GoodProxy").unwrap(), - ); - - let request = Arc::new(Mutex::new(request)); - - proxy.add_via_header(Arc::clone(&request)).await; - - let request = request.lock().await; - let headers = request.headers(); - - assert!(headers.get(&HeaderName::from_static("via")).is_some()); - - let via_header_value = headers.get(&HeaderName::from_static("via")).unwrap(); - let via_header_value = via_header_value.to_str().unwrap(); - - assert_eq!( - via_header_value, - "HTTP/1.1 GoodProxy, HTTP/1.1 Rust http-server" - ); - } - - #[tokio::test] - async fn removes_hbh_headers() { - let proxy = Proxy::new("https://example.com"); - let mut request = http::Request::new(Body::empty()); - let headers = request.headers_mut(); - let headers_to_add = vec![ - (CONNECTION, HeaderValue::from_str("keep-alive").unwrap()), - ( - PROXY_AUTHENTICATE, - HeaderValue::from_static(r#"Basic realm="Access to the internal site""#), - ), - ( - PROXY_AUTHORIZATION, - HeaderValue::from_static("Basic YWxhZGRpbjpvcGVuc2VzYW1l"), - ), - (TE, HeaderValue::from_static("compress")), - (TRAILER, HeaderValue::from_static("Expires")), - (TRANSFER_ENCODING, HeaderValue::from_static("chunked")), - (UPGRADE, HeaderValue::from_static("example/1, foo/2")), - ]; - - for (name, value) in headers_to_add { - headers.append(name, value); - } - - let request = Arc::new(Mutex::new(request)); - - proxy.remove_hbh_headers(Arc::clone(&request)).await; - - let request = request.lock().await; - - assert!(!request.headers().contains_key(CONNECTION)); - assert!(!request.headers().contains_key(PROXY_AUTHENTICATE)); - assert!(!request.headers().contains_key(PROXY_AUTHORIZATION)); - assert!(!request.headers().contains_key(TE)); - assert!(!request.headers().contains_key(TRAILER)); - assert!(!request.headers().contains_key(TRANSFER_ENCODING)); - assert!(!request.headers().contains_key(UPGRADE)); - } -} diff --git a/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index fc195ab8..00000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,25 +0,0 @@ -use http_server_lib::make_server; -use std::process::exit; - -#[cfg(feature = "dhat-profiling")] -use dhat::{Dhat, DhatAlloc}; - -#[cfg(feature = "dhat-profiling")] -#[global_allocator] -static ALLOCATOR: DhatAlloc = DhatAlloc; - -#[tokio::main] -async fn main() { - #[cfg(feature = "dhat-profiling")] - let _dhat = Dhat::start_heap_profiling(); - - match make_server() { - Ok(server) => { - server.run().await; - } - Err(error) => { - eprint!("{:?}", error); - exit(1); - } - } -} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 7974f0bd..00000000 --- a/src/cli.rs +++ /dev/null @@ -1,329 +0,0 @@ -use std::net::IpAddr; -use std::path::PathBuf; -use std::str::FromStr; -use structopt::StructOpt; - -use crate::config::util::tls::PrivateKeyAlgorithm; - -#[derive(Debug, StructOpt, PartialEq, Eq)] -#[structopt( - name = "http-server", - author = "Esteban Borai ", - about = "Simple and configurable command-line HTTP server\nSource: https://github.com/EstebanBorai/http-server" -)] -pub struct Cli { - /// Path to TOML configuration file. - #[structopt(parse(from_os_str), short = "c", long = "config")] - pub config: Option, - /// Host (IP) to bind the server - #[structopt(short = "h", long = "host", default_value = "127.0.0.1")] - pub host: IpAddr, - /// Port to bind the server - #[structopt(short = "p", long = "port", default_value = "7878")] - pub port: u16, - /// Route directories to index.html if present - #[structopt(short = "i", long = "index")] - pub index: bool, - /// Route non-existent files to /index.html - #[structopt(long = "spa")] - pub spa: bool, - /// Directory to serve files from - #[structopt(parse(from_os_str), default_value = "./")] - pub root_dir: PathBuf, - /// Turns off stdout/stderr logging - #[structopt(short = "q", long = "quiet")] - pub quiet: bool, - /// Enables HTTPS serving using TLS - #[structopt(long = "tls")] - pub tls: bool, - /// Path to the TLS Certificate - #[structopt(long = "tls-cert", parse(from_os_str), default_value = "cert.pem")] - pub tls_cert: PathBuf, - /// Path to the TLS Key - #[structopt(long = "tls-key", parse(from_os_str), default_value = "key.rsa")] - pub tls_key: PathBuf, - /// Algorithm used to generate certificate key - #[structopt(long = "tls-key-algorithm", default_value = "rsa")] - pub tls_key_algorithm: PrivateKeyAlgorithm, - /// Enable Cross-Origin Resource Sharing allowing any origin - #[structopt(long = "cors")] - pub cors: bool, - /// Enable GZip compression for HTTP Responses - #[structopt(long = "gzip")] - pub gzip: bool, - /// Specifies username for basic authentication - #[structopt(long = "username")] - pub username: Option, - /// Specifies password for basic authentication - #[structopt(long = "password")] - pub password: Option, - /// Prints HTTP request and response details to stdout - #[structopt(short = "l", long = "logger")] - pub logger: bool, - /// Proxy requests to the provided URL - #[structopt(long = "proxy")] - pub proxy: Option, - /// Waits for all requests to fulfill before shutting down the server - #[structopt(long = "graceful-shutdown")] - pub graceful_shutdown: bool, -} - -impl Cli { - pub fn from_str_args(args: Vec<&str>) -> Self { - Cli::from_iter_safe(args).unwrap_or_else(|e| e.exit()) - } -} - -impl Default for Cli { - fn default() -> Self { - Cli { - config: None, - host: "127.0.0.1".parse().unwrap(), - port: 7878_u16, - index: false, - spa: false, - root_dir: PathBuf::from_str("./").unwrap(), - quiet: false, - tls: false, - tls_cert: PathBuf::from_str("cert.pem").unwrap(), - tls_key: PathBuf::from_str("key.rsa").unwrap(), - tls_key_algorithm: PrivateKeyAlgorithm::Rsa, - cors: false, - gzip: false, - username: None, - password: None, - logger: false, - proxy: None, - graceful_shutdown: false, - } - } -} - -#[cfg(test)] -mod tests { - #[allow(unused_imports)] - use super::*; - - #[test] - fn no_arguments() { - let from_args = Cli::from_str_args(vec![]); - let expect = Cli::default(); - - assert_eq!(from_args, expect); - } - - #[test] - fn with_host() { - let from_args = Cli::from_str_args(vec!["http-server", "--host", "0.0.0.0"]); - let expect = Cli { - host: "0.0.0.0".parse().unwrap(), - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_host_and_port() { - let from_args = Cli::from_str_args(vec![ - "http-server", - "--host", - "192.168.0.1", - "--port", - "54200", - ]); - let expect = Cli { - host: "192.168.0.1".parse().unwrap(), - port: 54200_u16, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_root_dir() { - let from_args = Cli::from_str_args(vec!["http-server", "~/User/sources/http-server"]); - let expect = Cli { - root_dir: PathBuf::from_str("~/User/sources/http-server").unwrap(), - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_quiet_long() { - let from_args = Cli::from_str_args(vec!["http-server", "--quiet"]); - let expect = Cli { - quiet: true, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_quiet_short() { - let from_args = Cli::from_str_args(vec!["http-server", "-q"]); - let expect = Cli { - quiet: true, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_spa() { - let from_args = Cli::from_str_args(vec!["http-server", "--spa"]); - let expect = Cli { - spa: true, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_index_long() { - let from_args = Cli::from_str_args(vec!["http-server", "--index"]); - let expect = Cli { - index: true, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_index_short() { - let from_args = Cli::from_str_args(vec!["http-server", "-i"]); - let expect = Cli { - index: true, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_tls_no_config() { - let from_args = Cli::from_str_args(vec!["http-server", "--tls"]); - let expect = Cli { - tls: true, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_tls_and_config() { - let from_args = Cli::from_str_args(vec![ - "http-server", - "--tls", - "--tls-cert", - "~/secrets/cert", - "--tls-key", - "~/secrets/key", - "--tls-key-algorithm", - "rsa", - ]); - let expect = Cli { - tls: true, - tls_cert: PathBuf::from_str("~/secrets/cert").unwrap(), - tls_key: PathBuf::from_str("~/secrets/key").unwrap(), - tls_key_algorithm: PrivateKeyAlgorithm::Rsa, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_cors() { - let from_args = Cli::from_str_args(vec!["http-server", "--cors"]); - let expect = Cli { - cors: true, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_gzip() { - let from_args = Cli::from_str_args(vec!["http-server", "--gzip"]); - let expect = Cli { - gzip: true, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_basic_auth() { - let from_args = Cli::from_str_args(vec![ - "http-server", - "--username", - "John", - "--password", - "Appleseed", - ]); - let expect = Cli { - username: Some(String::from("John")), - password: Some(String::from("Appleseed")), - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_username_but_not_password() { - let from_args = Cli::from_str_args(vec!["http-server", "--username", "John"]); - let expect = Cli { - username: Some(String::from("John")), - password: None, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_password_but_not_username() { - let from_args = Cli::from_str_args(vec!["http-server", "--password", "Appleseed"]); - let expect = Cli { - username: None, - password: Some(String::from("Appleseed")), - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_logger() { - let from_args = Cli::from_str_args(vec!["http-server", "--logger"]); - let expect = Cli { - logger: true, - ..Default::default() - }; - - assert_eq!(from_args, expect); - } - - #[test] - fn with_proxy() { - let from_args = Cli::from_str_args(vec!["http-server", "--proxy", "https://example.com"]); - let expect = Cli { - proxy: Some(String::from("https://example.com")), - ..Default::default() - }; - - assert_eq!(from_args, expect); - } -} diff --git a/src/config/basic_auth.rs b/src/config/basic_auth.rs deleted file mode 100644 index abd94184..00000000 --- a/src/config/basic_auth.rs +++ /dev/null @@ -1,13 +0,0 @@ -use serde::Deserialize; - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -pub struct BasicAuthConfig { - pub username: String, - pub password: String, -} - -impl BasicAuthConfig { - pub fn new(username: String, password: String) -> Self { - BasicAuthConfig { username, password } - } -} diff --git a/src/config/compression.rs b/src/config/compression.rs deleted file mode 100644 index 1e718b09..00000000 --- a/src/config/compression.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::Deserialize; - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)] -pub struct CompressionConfig { - pub gzip: bool, -} diff --git a/src/config/cors.rs b/src/config/cors.rs deleted file mode 100644 index 61452a16..00000000 --- a/src/config/cors.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde::Deserialize; - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -pub struct CorsConfig { - pub allow_credentials: bool, - pub allow_headers: Option>, - pub allow_methods: Option>, - pub allow_origin: Option, - pub expose_headers: Option>, - pub max_age: Option, - pub request_headers: Option>, - pub request_method: Option, -} - -impl CorsConfig { - pub fn allow_all() -> Self { - CorsConfig { - allow_origin: Some(String::from("*")), - allow_methods: Some(vec![ - "GET".to_string(), - "POST".to_string(), - "PUT".to_string(), - "PATCH".to_string(), - "DELETE".to_string(), - "HEAD".to_string(), - ]), - allow_headers: Some(vec![ - "Origin".to_string(), - "Content-Length".to_string(), - "Content-Type".to_string(), - ]), - allow_credentials: false, - max_age: Some(43200), - expose_headers: None, - request_headers: None, - request_method: None, - } - } -} diff --git a/src/config/file.rs b/src/config/file.rs deleted file mode 100644 index 659eca96..00000000 --- a/src/config/file.rs +++ /dev/null @@ -1,337 +0,0 @@ -use anyhow::{Error, Result}; -use serde::{Deserialize, Deserializer}; -use std::fs; -use std::net::IpAddr; -use std::path::PathBuf; -use std::str::FromStr; - -use super::basic_auth::BasicAuthConfig; -use super::compression::CompressionConfig; -use super::cors::CorsConfig; -use super::proxy::ProxyConfig; -use super::tls::TlsConfigFile; - -#[derive(Debug, Deserialize)] -pub struct ConfigFile { - pub host: IpAddr, - pub port: u16, - pub quiet: Option, - pub index: Option, - pub spa: Option, - #[serde(default = "current_working_dir")] - #[serde(deserialize_with = "canonicalize_some")] - pub root_dir: Option, - pub tls: Option, - pub cors: Option, - pub compression: Option, - pub basic_auth: Option, - pub logger: Option, - pub proxy: Option, - pub graceful_shutdown: Option, -} - -impl ConfigFile { - pub fn from_file(file_path: PathBuf) -> Result { - let file = fs::read_to_string(file_path)?; - let config = ConfigFile::parse_toml(file.as_str())?; - - Ok(config) - } - - fn parse_toml(content: &str) -> Result { - match toml::from_str(content) { - Ok(config) => Ok(config), - Err(err) => Err(Error::msg(format!( - "Failed to parse config from file. {}", - err - ))), - } - } -} - -fn canonicalize_some<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value: &str = Deserialize::deserialize(deserializer)?; - let path = PathBuf::from_str(value).unwrap(); - let canon = fs::canonicalize(path).unwrap(); - - Ok(Some(canon)) -} - -fn current_working_dir() -> Option { - std::env::current_dir().ok() -} - -#[cfg(test)] -mod tests { - use std::net::Ipv4Addr; - use std::str::FromStr; - - use crate::config::util::tls::PrivateKeyAlgorithm; - - use super::*; - - #[test] - fn parses_config_from_file() { - let file_contents = r#" - host = "192.168.0.1" - port = 7878 - quiet = true - root_dir = "./fixtures" - index = true - spa = true - "#; - let host = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)); - let port = 7878; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - let mut root_dir = std::env::current_dir().unwrap(); - - root_dir.push("./fixtures"); - - assert!(config.graceful_shutdown.is_none()); - assert!(config.logger.is_none()); - assert!(config.compression.is_none()); - assert_eq!(config.host, host); - assert_eq!(config.port, port); - assert_eq!(config.quiet, Some(true)); - assert_eq!(config.index, Some(true)); - assert_eq!(config.spa, Some(true)); - assert_eq!(config.root_dir, Some(root_dir)); - } - - #[test] - #[should_panic( - expected = "Failed to parse config from file. missing field `host` at line 1 column 1" - )] - fn checks_invalid_config_from_file() { - let file_contents = r#" - port = 7878 - "#; - ConfigFile::parse_toml(file_contents).unwrap(); - } - - #[test] - fn parses_config_with_tls_using_rsa() { - let file_contents = r#" - host = "192.168.0.1" - port = 7878 - quiet = false - - [tls] - cert = "cert_123.pem" - key = "key_123.pem" - key_algorithm = "rsa" - "#; - let host = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)); - let port = 7878; - let root_dir = Some(std::env::current_dir().unwrap()); - let tls = TlsConfigFile { - cert: PathBuf::from_str("cert_123.pem").unwrap(), - key: PathBuf::from_str("key_123.pem").unwrap(), - key_algorithm: PrivateKeyAlgorithm::Rsa, - }; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - - assert_eq!(config.host, host); - assert_eq!(config.port, port); - assert_eq!(config.root_dir, root_dir); - assert_eq!(config.tls.unwrap(), tls); - assert_eq!(config.quiet, Some(false)); - } - - #[test] - fn parses_config_with_tls_using_pkcs8() { - let file_contents = r#" - host = "192.168.0.1" - port = 7878 - - [tls] - cert = "cert_123.pem" - key = "key_123.pem" - key_algorithm = "pkcs8" - "#; - let host = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)); - let port = 7878; - let root_dir = Some(std::env::current_dir().unwrap()); - let tls = TlsConfigFile { - cert: PathBuf::from_str("cert_123.pem").unwrap(), - key: PathBuf::from_str("key_123.pem").unwrap(), - key_algorithm: PrivateKeyAlgorithm::Pkcs8, - }; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - - assert_eq!(config.host, host); - assert_eq!(config.port, port); - assert_eq!(config.root_dir, root_dir); - assert_eq!(config.tls.unwrap(), tls); - } - - #[test] - fn parses_basic_cors_config_from_file() { - let file_contents = r#" - host = "0.0.0.0" - port = 8080 - - [cors] - allow_credentials = true - allow_headers = ["content-type", "authorization", "content-length"] - allow_methods = ["GET", "PATCH", "POST", "PUT", "DELETE"] - allow_origin = "example.com" - "#; - let host = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); - let port = 8080; - let cors = CorsConfig { - allow_credentials: true, - allow_headers: Some(vec![ - "content-type".to_string(), - "authorization".to_string(), - "content-length".to_string(), - ]), - allow_methods: Some(vec![ - "GET".to_string(), - "PATCH".to_string(), - "POST".to_string(), - "PUT".to_string(), - "DELETE".to_string(), - ]), - allow_origin: Some(String::from("example.com")), - expose_headers: None, - max_age: None, - request_headers: None, - request_method: None, - }; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - let root_dir = Some(std::env::current_dir().unwrap()); - - assert_eq!(config.host, host); - assert_eq!(config.port, port); - assert_eq!(config.root_dir, root_dir); - assert_eq!(config.cors.unwrap(), cors); - } - - #[test] - fn parses_complex_cors_config_from_file() { - let file_contents = r#" - host = "0.0.0.0" - port = 8080 - - [cors] - allow_credentials = true - allow_headers = ["content-type", "authorization", "content-length"] - allow_methods = ["GET", "PATCH", "POST", "PUT", "DELETE"] - allow_origin = "example.com" - expose_headers = ["*", "authorization"] - max_age = 2800 - request_headers = ["x-app-version"] - request_method = "GET" - "#; - let host = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); - let port = 8080; - let root_dir = Some(std::env::current_dir().unwrap()); - let cors = CorsConfig { - allow_credentials: true, - allow_headers: Some(vec![ - "content-type".to_string(), - "authorization".to_string(), - "content-length".to_string(), - ]), - allow_methods: Some(vec![ - "GET".to_string(), - "PATCH".to_string(), - "POST".to_string(), - "PUT".to_string(), - "DELETE".to_string(), - ]), - allow_origin: Some(String::from("example.com")), - expose_headers: Some(vec!["*".to_string(), "authorization".to_string()]), - max_age: Some(2800), - request_headers: Some(vec!["x-app-version".to_string()]), - request_method: Some(String::from("GET")), - }; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - - assert_eq!(config.host, host); - assert_eq!(config.port, port); - assert_eq!(config.root_dir, root_dir); - assert_eq!(config.cors.unwrap(), cors); - } - - #[test] - fn parses_config_with_gzip_compression() { - let file_contents = r#" - host = "0.0.0.0" - port = 7878 - - [compression] - gzip = true - "#; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - - assert!(config.compression.unwrap().gzip); - } - - #[test] - fn parses_config_with_basic_auth() { - let file_contents = r#" - host = "0.0.0.0" - port = 7878 - - [basic_auth] - username = "johnappleseed" - password = "john::likes::apples!" - "#; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - - assert!(config.basic_auth.is_some()); - - let basic_auth = config.basic_auth.unwrap(); - - assert_eq!(basic_auth.username, String::from("johnappleseed")); - assert_eq!(basic_auth.password, String::from("john::likes::apples!")); - } - - #[test] - fn parses_config_with_logger() { - let file_contents = r#" - host = "0.0.0.0" - port = 7878 - logger = true - "#; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - - assert_eq!(config.logger, Some(true)); - } - - #[test] - fn parses_config_with_proxy() { - let file_contents = r#" - host = "0.0.0.0" - port = 7878 - - [proxy] - url = "https://example.com" - "#; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - - assert!(config.proxy.is_some()); - - let proxy = config.proxy.unwrap(); - - assert_eq!(proxy.url, "https://example.com"); - } - - #[test] - fn parse_config_with_graceful_shutdown() { - let file_contents = r#" - host = "0.0.0.0" - port = 7878 - graceful_shutdown = true - "#; - let config = ConfigFile::parse_toml(file_contents).unwrap(); - - assert!(config.graceful_shutdown.is_some()); - assert!(config.graceful_shutdown.unwrap()); - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs deleted file mode 100644 index eecc3852..00000000 --- a/src/config/mod.rs +++ /dev/null @@ -1,218 +0,0 @@ -pub mod basic_auth; -pub mod compression; -pub mod cors; -pub mod file; -pub mod proxy; -pub mod tls; -pub mod util; - -use anyhow::{Error, Result}; -use std::convert::TryFrom; -use std::env::current_dir; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::path::PathBuf; - -use crate::cli::Cli; - -use self::basic_auth::BasicAuthConfig; -use self::compression::CompressionConfig; -use self::cors::CorsConfig; -use self::file::ConfigFile; -use self::proxy::ProxyConfig; -use self::tls::TlsConfig; - -/// Server instance configuration used on initialization -pub struct Config { - pub address: SocketAddr, - pub host: IpAddr, - pub port: u16, - pub index: bool, - pub spa: bool, - pub root_dir: PathBuf, - pub quiet: bool, - pub tls: Option, - pub cors: Option, - pub compression: Option, - pub basic_auth: Option, - pub logger: Option, - pub proxy: Option, - pub graceful_shutdown: bool, -} - -impl Default for Config { - fn default() -> Self { - let host = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); - let port = 7878; - let address = SocketAddr::new(host, port); - let root_dir = current_dir().unwrap(); - - Self { - host, - port, - index: false, - spa: false, - address, - root_dir, - quiet: false, - tls: None, - cors: None, - compression: None, - basic_auth: None, - logger: None, - proxy: None, - graceful_shutdown: false, - } - } -} - -impl TryFrom for Config { - type Error = anyhow::Error; - - fn try_from(cli_arguments: Cli) -> Result { - let quiet = cli_arguments.quiet; - let root_dir = if cli_arguments.root_dir.to_str().unwrap() == "./" { - current_dir().unwrap() - } else { - let root_dir = cli_arguments.root_dir.to_str().unwrap(); - - cli_arguments - .root_dir - .canonicalize() - .unwrap_or_else(|_| panic!("Failed to find config on: {}", root_dir)) - }; - - let tls: Option = if cli_arguments.tls { - Some(TlsConfig::new( - cli_arguments.tls_cert, - cli_arguments.tls_key, - cli_arguments.tls_key_algorithm, - )?) - } else { - None - }; - - let cors: Option = if cli_arguments.cors { - // when CORS is specified from CLI the default - // configuration should allow any origin, method and - // headers - Some(CorsConfig::allow_all()) - } else { - None - }; - - let compression: Option = if cli_arguments.gzip { - Some(CompressionConfig { gzip: true }) - } else { - None - }; - - let basic_auth: Option = - if cli_arguments.username.is_some() && cli_arguments.password.is_some() { - Some(BasicAuthConfig::new( - cli_arguments.username.unwrap(), - cli_arguments.password.unwrap(), - )) - } else { - None - }; - - let logger = if cli_arguments.logger { - Some(true) - } else { - None - }; - - let proxy = if cli_arguments.proxy.is_some() { - let proxy_url = cli_arguments.proxy.unwrap(); - - Some(ProxyConfig::url(proxy_url)) - } else { - None - }; - - let spa = cli_arguments.spa; - let index = spa || cli_arguments.index; - - Ok(Config { - host: cli_arguments.host, - port: cli_arguments.port, - address: SocketAddr::new(cli_arguments.host, cli_arguments.port), - index, - spa, - root_dir, - quiet, - tls, - cors, - compression, - basic_auth, - logger, - proxy, - graceful_shutdown: cli_arguments.graceful_shutdown, - }) - } -} - -impl TryFrom for Config { - type Error = Error; - - fn try_from(file: ConfigFile) -> Result { - let root_dir = file.root_dir.unwrap_or_default(); - let quiet = file.quiet.unwrap_or(false); - let tls: Option = if let Some(https_config) = file.tls { - Some(TlsConfig::new( - https_config.cert, - https_config.key, - https_config.key_algorithm, - )?) - } else { - None - }; - - let spa = file.spa.unwrap_or(false); - let index = spa || file.index.unwrap_or(false); - - Ok(Config { - host: file.host, - port: file.port, - address: SocketAddr::new(file.host, file.port), - index, - spa, - quiet, - root_dir, - tls, - cors: file.cors, - compression: file.compression, - basic_auth: file.basic_auth, - logger: file.logger, - proxy: file.proxy, - graceful_shutdown: file.graceful_shutdown.unwrap_or(false), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn creates_default_config() { - let host = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); - let port = 7878; - let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7878); - let config = Config::default(); - - assert_eq!( - config.host, - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - "default host: {}", - host - ); - assert_eq!(config.port, 7878, "default port: {}", port); - assert_eq!( - config.address, address, - "default socket address: {}", - address - ); - assert!(!config.quiet, "quiet is off by default"); - } -} diff --git a/src/config/proxy.rs b/src/config/proxy.rs deleted file mode 100644 index b57b1c2f..00000000 --- a/src/config/proxy.rs +++ /dev/null @@ -1,16 +0,0 @@ -use serde::Deserialize; - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -pub struct ProxyConfig { - pub url: String, -} - -impl ProxyConfig { - pub fn new(url: String) -> Self { - ProxyConfig { url } - } - - pub fn url(url: String) -> Self { - ProxyConfig { url } - } -} diff --git a/src/config/tls.rs b/src/config/tls.rs deleted file mode 100644 index 8f900c22..00000000 --- a/src/config/tls.rs +++ /dev/null @@ -1,44 +0,0 @@ -use anyhow::Result; -use rustls::{Certificate, PrivateKey}; -use serde::Deserialize; -use std::path::PathBuf; - -use super::util::tls::{load_cert, load_private_key, PrivateKeyAlgorithm}; - -/// Configuration for TLS protocol serving with its certificate and private key -#[derive(Clone, Debug)] -pub struct TlsConfig { - cert: Vec, - key: PrivateKey, - #[allow(dead_code)] - key_algorithm: PrivateKeyAlgorithm, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -pub struct TlsConfigFile { - pub cert: PathBuf, - pub key: PathBuf, - pub key_algorithm: PrivateKeyAlgorithm, -} - -impl TlsConfig { - pub fn new( - cert_path: PathBuf, - key_path: PathBuf, - key_algorithm: PrivateKeyAlgorithm, - ) -> Result { - let cert = load_cert(&cert_path)?; - let key = load_private_key(&key_path, &key_algorithm)?; - - Ok(TlsConfig { - cert, - key, - key_algorithm, - }) - } - - /// Retrieve certificates and private key loaded on initialization - pub fn parts(&self) -> (Vec, PrivateKey) { - (self.cert.clone(), self.key.clone()) - } -} diff --git a/src/config/util/mod.rs b/src/config/util/mod.rs deleted file mode 100644 index dbdc4f3c..00000000 --- a/src/config/util/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod tls; diff --git a/src/config/util/tls.rs b/src/config/util/tls.rs deleted file mode 100644 index 6b9dd1e3..00000000 --- a/src/config/util/tls.rs +++ /dev/null @@ -1,61 +0,0 @@ -use anyhow::{Context, Error, Result}; -use rustls::{Certificate, PrivateKey}; -use rustls_pemfile::{pkcs8_private_keys, rsa_private_keys}; -use serde::Deserialize; -use std::fs::File; -use std::io::BufReader; -use std::path::Path; -use std::str::FromStr; - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -pub enum PrivateKeyAlgorithm { - #[serde(rename = "rsa")] - Rsa, - #[serde(rename = "pkcs8")] - Pkcs8, -} - -impl FromStr for PrivateKeyAlgorithm { - type Err = Error; - - fn from_str(s: &str) -> Result { - match s { - "rsa" => Ok(PrivateKeyAlgorithm::Rsa), - "pkcs8" => Ok(PrivateKeyAlgorithm::Pkcs8), - _ => anyhow::bail!("Invalid algorithm name provided for TLS key. {}", s), - } - } -} - -/// Load certificate on the provided `path` and retrieve it -/// as an instance of `Vec`. -pub fn load_cert(path: &Path) -> Result> { - let file = File::open(path).context(format!( - "Unable to find the TLS certificate on: {}", - path.to_str().unwrap() - ))?; - let mut buf_reader = BufReader::new(file); - let cert_bytes = &rustls_pemfile::certs(&mut buf_reader).unwrap()[0]; - - Ok(vec![Certificate(cert_bytes.to_vec())]) -} - -pub fn load_private_key(path: &Path, kind: &PrivateKeyAlgorithm) -> Result { - let file = File::open(path) - .with_context(|| format!("Unable to find the TLS keys on: {}", path.to_str().unwrap()))?; - let mut reader = BufReader::new(file); - let keys = match kind { - PrivateKeyAlgorithm::Rsa => rsa_private_keys(&mut reader).map_err(|_| { - let path = path.to_str().unwrap(); - - Error::msg(format!("Failed to read private (RSA) keys at {}", path)) - })?, - PrivateKeyAlgorithm::Pkcs8 => pkcs8_private_keys(&mut reader).map_err(|_| { - let path = path.to_str().unwrap(); - - Error::msg(format!("Failed to read private (PKCS8) keys at {}", path)) - })?, - }; - - Ok(PrivateKey(keys[0].clone())) -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index c33b90ad..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,34 +0,0 @@ -mod addon; -mod cli; -mod config; -mod server; -mod utils; - -use anyhow::{Context, Result}; -use std::convert::TryFrom; -use structopt::StructOpt; - -use crate::config::file::ConfigFile; -use crate::config::Config; -use crate::server::Server; - -fn resolve_config(cli_arguments: cli::Cli) -> Result { - if let Some(config_path) = cli_arguments.config { - let config_file = ConfigFile::from_file(config_path)?; - let config = Config::try_from(config_file)?; - - return Ok(config); - } - - // Otherwise configuration is build from CLI arguments - Config::try_from(cli_arguments) - .with_context(|| anyhow::Error::msg("Failed to parse arguments from stdin")) -} - -pub fn make_server() -> Result { - let cli_arguments = cli::Cli::from_args(); - let config = resolve_config(cli_arguments)?; - let server = Server::new(config); - - Ok(server) -} diff --git a/src/server/handler/file_server.rs b/src/server/handler/file_server.rs deleted file mode 100644 index a5d7a01c..00000000 --- a/src/server/handler/file_server.rs +++ /dev/null @@ -1,44 +0,0 @@ -use async_trait::async_trait; -use http::response::Builder as HttpResponseBuilder; -use http::StatusCode; -use hyper::{Body, Method, Request}; -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::addon::file_server::FileServer; - -use super::RequestHandler; - -pub struct FileServerHandler { - file_server: Arc, -} - -impl FileServerHandler { - pub fn new(file_server: FileServer) -> Self { - let file_server = Arc::new(file_server); - - FileServerHandler { file_server } - } -} - -#[async_trait] -impl RequestHandler for FileServerHandler { - async fn handle(&self, req: Arc>>) -> Arc>> { - let request_lock = req.lock().await; - let req_path = request_lock.uri().to_string(); - let req_method = request_lock.method(); - - if req_method == Method::GET { - let response = self.file_server.resolve(req_path).await.unwrap(); - - return Arc::new(Mutex::new(response)); - } - - Arc::new(Mutex::new( - HttpResponseBuilder::new() - .status(StatusCode::METHOD_NOT_ALLOWED) - .body(Body::empty()) - .expect("Unable to build response"), - )) - } -} diff --git a/src/server/handler/mod.rs b/src/server/handler/mod.rs deleted file mode 100644 index 88c4c6fb..00000000 --- a/src/server/handler/mod.rs +++ /dev/null @@ -1,68 +0,0 @@ -mod file_server; -mod proxy; - -use anyhow::Result; -use async_trait::async_trait; -use http::{Request, Response}; -use hyper::Body; -use std::convert::TryFrom; -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::addon::file_server::FileServer; -use crate::addon::proxy::Proxy; -use crate::Config; - -use super::middleware::Middleware; - -use self::file_server::FileServerHandler; -use self::proxy::ProxyHandler; - -/// A trait to implement on addons in order to handle incomming requests and -/// generate responses. -#[async_trait] -pub trait RequestHandler { - async fn handle(&self, req: Arc>>) -> Arc>>; -} - -#[derive(Clone)] -pub struct HttpHandler { - request_handler: Arc, - middleware: Arc, -} - -impl HttpHandler { - pub async fn handle_request(self, request: Request) -> Result> { - let handler = Arc::clone(&self.request_handler); - let middleware = Arc::clone(&self.middleware); - let response = middleware.handle(request, handler).await; - - Ok(response) - } -} - -impl From> for HttpHandler { - fn from(config: Arc) -> Self { - if let Some(proxy_config) = config.proxy.clone() { - let proxy = Proxy::new(&proxy_config.url); - let request_handler = Arc::new(ProxyHandler::new(proxy)); - let middleware = Middleware::try_from(config).unwrap(); - let middleware = Arc::new(middleware); - - return HttpHandler { - request_handler, - middleware, - }; - } - - let file_server = FileServer::new(config.clone()); - let request_handler = Arc::new(FileServerHandler::new(file_server)); - let middleware = Middleware::try_from(config).unwrap(); - let middleware = Arc::new(middleware); - - HttpHandler { - request_handler, - middleware, - } - } -} diff --git a/src/server/handler/proxy.rs b/src/server/handler/proxy.rs deleted file mode 100644 index 502ae4e2..00000000 --- a/src/server/handler/proxy.rs +++ /dev/null @@ -1,31 +0,0 @@ -use async_trait::async_trait; -use hyper::{Body, Request}; -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::addon::proxy::Proxy; - -use super::RequestHandler; - -pub struct ProxyHandler { - proxy: Arc, -} - -impl ProxyHandler { - pub fn new(proxy: Proxy) -> Self { - let proxy = Arc::new(proxy); - - ProxyHandler { proxy } - } -} - -#[async_trait] -impl RequestHandler for ProxyHandler { - async fn handle(&self, req: Arc>>) -> Arc>> { - let proxy = Arc::clone(&self.proxy); - let request = Arc::clone(&req); - let response = proxy.handle(request).await; - - Arc::new(Mutex::new(response)) - } -} diff --git a/src/server/https.rs b/src/server/https.rs deleted file mode 100644 index f069cf56..00000000 --- a/src/server/https.rs +++ /dev/null @@ -1,61 +0,0 @@ -use anyhow::Result; -use async_stream::stream; -use futures::TryFutureExt; -use hyper::server::accept::Accept; -use hyper::server::Builder; -use rustls::{Certificate, PrivateKey, ServerConfig}; -use std::io::Error; -use std::net::SocketAddr; -use std::sync::Arc; -use tokio::net::{TcpListener, TcpStream}; -use tokio_rustls::server::TlsStream; -use tokio_rustls::TlsAcceptor; - -pub struct Https { - cert: Vec, - key: PrivateKey, -} - -impl Https { - pub fn new(cert: Vec, key: PrivateKey) -> Self { - Https { cert, key } - } - - fn make_tls_cfg(&self) -> Result> { - let (certs, private_key) = (self.cert.clone(), self.key.clone()); - let config = ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(certs, private_key) - .map_err(anyhow::Error::new)?; - - Ok(Arc::new(config)) - } - - pub async fn make_server( - &self, - addr: SocketAddr, - ) -> Result, Error = Error>>> { - let tcp = TcpListener::bind(addr).await?; - let tls_cfg = self.make_tls_cfg()?; - let tls_acceptor = TlsAcceptor::from(tls_cfg); - - let incoming_tls_stream = stream! { - loop { - let (socket, _) = tcp.accept().await?; - let stream = tls_acceptor.accept(socket).map_err(|error| { - println!("HTTPS Error: {:?}", error); - - error - }); - - yield stream.await; - } - }; - - let acceptor = hyper::server::accept::from_stream(incoming_tls_stream); - let server = hyper::server::Server::builder(acceptor); - - Ok(server) - } -} diff --git a/src/server/middleware/basic_auth.rs b/src/server/middleware/basic_auth.rs deleted file mode 100644 index 0412fdec..00000000 --- a/src/server/middleware/basic_auth.rs +++ /dev/null @@ -1,50 +0,0 @@ -use http::{Request, StatusCode}; -use http_auth_basic::Credentials; -use hyper::Body; -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::config::basic_auth::BasicAuthConfig; -use crate::utils::error::make_http_error_response; - -use super::MiddlewareBefore; - -pub fn make_basic_auth_middleware(basic_auth_config: BasicAuthConfig) -> MiddlewareBefore { - let credentials = Arc::new(Credentials::new( - basic_auth_config.username.as_str(), - basic_auth_config.password.as_str(), - )); - - Box::new(move |request: Arc>>| { - let credentials = Arc::clone(&credentials); - - Box::pin(async move { - let request = request.lock().await; - - if let Some(auth_header_value) = request.headers().get(http::header::AUTHORIZATION) { - let auth_header_value = auth_header_value.to_str().map_err(|err| { - make_http_error_response(StatusCode::BAD_REQUEST, err.to_string().as_str()) - })?; - - let incoming_credentials = Credentials::from_header(auth_header_value.to_string()) - .map_err(|err| { - make_http_error_response(StatusCode::BAD_REQUEST, err.to_string().as_str()) - })?; - - if incoming_credentials == *credentials { - return Ok(()); - } - - return Err(make_http_error_response( - StatusCode::UNAUTHORIZED, - "Unauthorized", - )); - } - - Err(make_http_error_response( - StatusCode::UNAUTHORIZED, - "Unauthorized", - )) - }) - }) -} diff --git a/src/server/middleware/cors.rs b/src/server/middleware/cors.rs deleted file mode 100644 index 84d9059e..00000000 --- a/src/server/middleware/cors.rs +++ /dev/null @@ -1,48 +0,0 @@ -use http::{Request, Response}; -use hyper::Body; -use std::convert::TryFrom; -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::addon::cors::Cors; -use crate::config::cors::CorsConfig; - -use super::MiddlewareAfter; - -/// Creates a CORS middleware with the configuration provided and returns it. -/// The configured headers will be appended to every HTTP Response before -/// sending such response back to the client (After Middleware) -/// -/// CORS headers for every response are built on server initialization and -/// then are "appended" to Response headers on every response. -/// -/// # Panics -/// -/// Panics if a CORS config is not defined for the `Config` instance provided. -/// (`Config.cors` is `None`). -/// `make_cors_middlware` should only be called when a `CorsConfig` is defined. -/// -/// Also panics if any CORS header value is not a valid UTF-8 string -pub fn make_cors_middleware(cors_config: CorsConfig) -> MiddlewareAfter { - let cors = Cors::try_from(cors_config).unwrap(); - let cors_headers = cors.make_http_headers(); - - Box::new( - move |_: Arc>>, response: Arc>>| { - let cors_headers = cors_headers.clone(); - let response = Arc::clone(&response); - - Box::pin(async move { - let mut response = response.lock().await; - - let headers = response.headers_mut(); - - cors_headers.iter().for_each(|(header, value)| { - headers.append(header, value.to_owned()); - }); - - Ok(()) - }) - }, - ) -} diff --git a/src/server/middleware/gzip.rs b/src/server/middleware/gzip.rs deleted file mode 100644 index 6a779730..00000000 --- a/src/server/middleware/gzip.rs +++ /dev/null @@ -1,26 +0,0 @@ -use http::{Request, Response, StatusCode}; -use hyper::Body; -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::addon::compression::gzip::compress_http_response; -use crate::utils::error::make_http_error_response; - -use super::MiddlewareAfter; - -pub fn make_gzip_compression_middleware() -> MiddlewareAfter { - Box::new( - move |request: Arc>>, response: Arc>>| { - Box::pin(async move { - compress_http_response(request, response) - .await - .map_err(|err| { - make_http_error_response( - StatusCode::INTERNAL_SERVER_ERROR, - &err.to_string(), - ) - }) - }) - }, - ) -} diff --git a/src/server/middleware/logger.rs b/src/server/middleware/logger.rs deleted file mode 100644 index d0af1cfd..00000000 --- a/src/server/middleware/logger.rs +++ /dev/null @@ -1,28 +0,0 @@ -use http::{Request, Response}; -use hyper::Body; -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::addon::logger::Logger; - -use super::MiddlewareAfter; - -pub fn make_logger_middleware() -> MiddlewareAfter { - let logger = Arc::new(Mutex::new(Logger::new())); - - Box::new( - move |request: Arc>>, response: Arc>>| { - let logger = Arc::clone(&logger); - - Box::pin(async move { - let mut logger = logger.lock().await; - - if let Err(error) = logger.log(request, response).await { - eprintln!("{:#?}", error); - } - - Ok(()) - }) - }, - ) -} diff --git a/src/server/middleware/mod.rs b/src/server/middleware/mod.rs deleted file mode 100644 index 41172258..00000000 --- a/src/server/middleware/mod.rs +++ /dev/null @@ -1,131 +0,0 @@ -pub mod basic_auth; -pub mod cors; -pub mod gzip; -pub mod logger; - -use anyhow::Error; -use futures::Future; -use hyper::Body; -use std::convert::TryFrom; -use std::pin::Pin; -use std::sync::Arc; -use tokio::sync::Mutex; - -use super::handler::RequestHandler; -use crate::config::Config; - -use self::basic_auth::make_basic_auth_middleware; -use self::cors::make_cors_middleware; -use self::gzip::make_gzip_compression_middleware; -use self::logger::make_logger_middleware; - -/// Middleware HTTP Response which expands to a `Arc>>` -pub type Request = Arc>>; - -/// Middleware HTTP Response which expands to a `Arc>>` -pub type Response = Arc>>; - -/// Middleware chain `Result` which specifies the `Err` variant as a -/// HTTP response. -pub type Result = std::result::Result<(), http::Response>; - -/// Middleware chain to execute before the main handler digests the -/// HTTP request. No HTTP response is available at this point. -pub type MiddlewareBefore = - Box) -> Pin + Send + Sync>> + Send + Sync>; - -/// Middleware chain to execute after the main handler digests the -/// HTTP request. The HTTP response is created by the handler and -/// consumed by every middleware after chain. -pub type MiddlewareAfter = Box< - dyn Fn(Request, Response) -> Pin + Send + Sync>> - + Send - + Sync, ->; - -#[derive(Default)] -pub struct Middleware { - before: Vec, - after: Vec, -} - -impl Middleware { - /// Appends a middleware function to run before handling the - /// HTTP Request - #[allow(dead_code)] - pub fn before(&mut self, middleware: MiddlewareBefore) { - self.before.push(middleware); - } - - /// Appends a middleware function to run after handling the - /// HTTP Request. Thus, functions appended after will receive - /// the handler's HTTP Response instead - pub fn after(&mut self, middleware: MiddlewareAfter) { - self.after.push(middleware); - } - - /// Runs functions from the chain that must run before - /// executing the handler (applied to the HTTP Request). - /// Then performs the handler operations on the HTTP Request - /// and finally executes the functions on the "after" chain - /// with the HTTP Response from the handler - pub async fn handle( - &self, - request: http::Request, - handler: Arc, - ) -> http::Response { - let request = Arc::new(Mutex::new(request)); - - for fx in self.before.iter() { - if let Err(err) = fx(Arc::clone(&request)).await { - return err; - } - } - - let response = handler.handle(Arc::clone(&request)).await; - - for fx in self.after.iter() { - if let Err(err) = fx(Arc::clone(&request), Arc::clone(&response)).await { - return err; - } - } - - Arc::try_unwrap(response) - .expect("There's one or more reference/s being hold by a middleware chain.") - .into_inner() - } -} - -impl TryFrom> for Middleware { - type Error = Error; - - fn try_from(config: Arc) -> std::result::Result { - let mut middleware = Middleware::default(); - - if let Some(basic_auth_config) = config.basic_auth.clone() { - let basic_auth_middleware = make_basic_auth_middleware(basic_auth_config); - - middleware.before(basic_auth_middleware); - } - - if let Some(cors_config) = config.cors.clone() { - let cors_middleware = make_cors_middleware(cors_config); - - middleware.after(cors_middleware); - } - - if let Some(compression_config) = config.compression.clone() { - if compression_config.gzip { - middleware.after(make_gzip_compression_middleware()); - } - } - - if let Some(should_log) = config.logger { - if should_log { - middleware.after(make_logger_middleware()); - } - } - - Ok(middleware) - } -} diff --git a/src/server/mod.rs b/src/server/mod.rs deleted file mode 100644 index 5e0271f3..00000000 --- a/src/server/mod.rs +++ /dev/null @@ -1,158 +0,0 @@ -mod handler; -mod https; -mod service; - -pub mod middleware; - -use anyhow::Error; -use hyper::service::{make_service_fn, service_fn}; -use std::net::{Ipv4Addr, SocketAddr}; -use std::process::exit; -use std::str::FromStr; -use std::sync::Arc; - -use crate::config::tls::TlsConfig; -use crate::config::Config; - -pub struct Server { - config: Arc, -} - -impl Server { - pub fn new(config: Config) -> Server { - let config = Arc::new(config); - - Server { config } - } - - pub async fn run(self) { - let config = Arc::clone(&self.config); - let address = config.address; - let handler = handler::HttpHandler::from(Arc::clone(&config)); - let server = Arc::new(self); - let mut server_instances: Vec> = Vec::new(); - - if config.spa { - let mut index_html = config.root_dir.clone(); - index_html.push("index.html"); - - if !index_html.exists() { - eprintln!( - "SPA flag is enabled, but index.html in root does not exist. Quitting..." - ); - exit(1); - } - } - - if config.tls.is_some() { - let https_config = config.tls.clone().unwrap(); - let handler = handler.clone(); - let host = config.address.ip(); - let port = config.address.port().saturating_add(1); - let address = SocketAddr::new(host, port); - let server = Arc::clone(&server); - let task = tokio::spawn(async move { - let server = Arc::clone(&server); - - server.serve_https(address, handler, https_config).await; - }); - - server_instances.push(task); - } - - let server = Arc::clone(&server); - let task = tokio::spawn(async move { - let server = Arc::clone(&server); - - server.serve(address, handler).await; - }); - - server_instances.push(task); - - for server_task in server_instances { - server_task.await.unwrap(); - } - } - - pub async fn serve(&self, address: SocketAddr, handler: handler::HttpHandler) { - let server = hyper::Server::bind(&address).serve(make_service_fn(|_| { - // Move a clone of `handler` into the `service_fn`. - let handler = handler.clone(); - - async { - Ok::<_, Error>(service_fn(move |req| { - service::main_service(handler.to_owned(), req) - })) - } - })); - - if !self.config.quiet { - println!("Serving HTTP: http://{}", address); - - if self.config.address.ip() == Ipv4Addr::from_str("0.0.0.0").unwrap() { - if let Ok(ip) = local_ip_address::local_ip() { - println!("Local Network IP: http://{}:{}", ip, self.config.port); - } - } - } - - if self.config.graceful_shutdown { - let graceful = server.with_graceful_shutdown(crate::utils::signal::shutdown_signal()); - - if let Err(e) = graceful.await { - eprint!("Server Error: {}", e); - } - - return; - } - - if let Err(e) = server.await { - eprint!("Server Error: {}", e); - } - } - - pub async fn serve_https( - &self, - address: SocketAddr, - handler: handler::HttpHandler, - https_config: TlsConfig, - ) { - let (cert, key) = https_config.parts(); - let https_server_builder = https::Https::new(cert, key); - let server = https_server_builder.make_server(address).await.unwrap(); - let server = server.serve(make_service_fn(|_| { - // Move a clone of `handler` into the `service_fn`. - let handler = handler.clone(); - - async { - Ok::<_, Error>(service_fn(move |req| { - service::main_service(handler.to_owned(), req) - })) - } - })); - - if !self.config.quiet { - println!("Serving HTTPS: http://{}", address); - - if self.config.address.ip() == Ipv4Addr::from_str("0.0.0.0").unwrap() { - if let Ok(ip) = local_ip_address::local_ip() { - println!("Local Network IP: https://{}:{}", ip, self.config.port); - } - } - } - - if self.config.graceful_shutdown { - let graceful = server.with_graceful_shutdown(crate::utils::signal::shutdown_signal()); - - if let Err(e) = graceful.await { - eprint!("Server Error: {}", e); - } - - return; - } - - if let Err(e) = server.await { - eprint!("Server Error: {}", e); - } - } -} diff --git a/src/server/service.rs b/src/server/service.rs deleted file mode 100644 index b894069a..00000000 --- a/src/server/service.rs +++ /dev/null @@ -1,9 +0,0 @@ -use anyhow::Result; -use http::{Request, Response}; -use hyper::Body; - -use super::handler::HttpHandler; - -pub async fn main_service(handler: HttpHandler, req: Request) -> Result> { - handler.handle_request(req).await -} diff --git a/src/utils/error.rs b/src/utils/error.rs deleted file mode 100644 index 88424c0b..00000000 --- a/src/utils/error.rs +++ /dev/null @@ -1,22 +0,0 @@ -use http::{Response, StatusCode}; -use hyper::Body; -use serde::Serialize; - -#[derive(Debug, Serialize)] -struct ErrorResponseBody { - status_code: u16, - message: String, -} - -pub fn make_http_error_response(status: StatusCode, message: &str) -> Response { - Response::builder() - .status(status) - .body(Body::from( - serde_json::ser::to_string(&ErrorResponseBody { - status_code: status.as_u16(), - message: message.to_string(), - }) - .unwrap(), - )) - .unwrap() -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs deleted file mode 100644 index 46d14a32..00000000 --- a/src/utils/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod error; -pub mod signal; -pub mod url_encode; diff --git a/src/utils/signal.rs b/src/utils/signal.rs deleted file mode 100644 index c251ce4e..00000000 --- a/src/utils/signal.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub async fn shutdown_signal() { - tokio::signal::ctrl_c() - .await - .expect("Failed to hook Ctrl + C signal handler"); - println!("Received Ctrl + C Signal"); -} diff --git a/tests/basic_auth.rs b/tests/basic_auth.rs deleted file mode 100644 index 819cb0b8..00000000 --- a/tests/basic_auth.rs +++ /dev/null @@ -1,52 +0,0 @@ -#[cfg(test)] -mod tests { - use http::{HeaderValue, Request, Response, StatusCode}; - use http_auth_basic::Credentials; - use hyper::{Body, Client}; - - async fn http_get(url: &str) -> Response { - let client = Client::default(); - - client.get(url.parse().unwrap()).await.unwrap() - } - - async fn http_get_with_basic_auth(url: &str, username: &str, password: &str) -> Response { - let credentials = Credentials::new(username, password); - let mut request = Request::builder(); - request = request.uri(url); - - let headers = request.headers_mut().unwrap(); - - headers.insert( - http::header::AUTHORIZATION, - HeaderValue::from_str(credentials.as_http_header().as_str()).unwrap(), - ); - - let client = Client::default(); - client - .request(request.body(Body::empty()).unwrap()) - .await - .unwrap() - } - - #[tokio::test] - async fn basic_auth_resolves_request_successfuly() { - let response = http_get_with_basic_auth("http://0.0.0.0:7878", "john", "appleseed").await; - - assert_eq!(response.status(), StatusCode::OK); - } - - #[tokio::test] - async fn basic_auth_validates_wrong_credentials() { - let response = http_get_with_basic_auth("http://0.0.0.0:7878", "somebody", "else").await; - - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn basic_auth_resolves_request_unauthorized_when_header_is_missing() { - let response = http_get("http://0.0.0.0:7878").await; - - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } -} diff --git a/tests/cors.rs b/tests/cors.rs deleted file mode 100644 index 3d4ef70a..00000000 --- a/tests/cors.rs +++ /dev/null @@ -1,63 +0,0 @@ -#[cfg(test)] -mod tests { - use http::{Response, StatusCode}; - use hyper::{Body, Client}; - - async fn http_get(url: &str) -> Response { - let client = Client::default(); - - client.get(url.parse().unwrap()).await.unwrap() - } - - #[tokio::test] - async fn cors_get_request_to_root_responds_200() { - let response = http_get("http://0.0.0.0:7878").await; - let headers = response.headers(); - - assert_eq!(response.status(), StatusCode::OK); - assert!(headers - .get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN) - .is_some()); - assert!(headers - .get(http::header::ACCESS_CONTROL_ALLOW_METHODS) - .is_some()); - assert!(headers - .get(http::header::ACCESS_CONTROL_ALLOW_HEADERS) - .is_some()); - } - - #[tokio::test] - async fn cors_get_request_retrieve_file() { - let response = http_get("http://0.0.0.0:7878/docs/screenshot.png").await; - let headers = response.headers(); - - println!("{:#?}", response.headers()); - assert_eq!(response.status(), StatusCode::OK); - assert!(headers - .get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN) - .is_some()); - assert!(headers - .get(http::header::ACCESS_CONTROL_ALLOW_METHODS) - .is_some()); - assert!(headers - .get(http::header::ACCESS_CONTROL_ALLOW_HEADERS) - .is_some()); - } - - #[tokio::test] - async fn cors_get_request_file_not_found() { - let response = http_get("http://0.0.0.0:7878/xyz/abc.txt").await; - let headers = response.headers(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - assert!(headers - .get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN) - .is_some()); - assert!(headers - .get(http::header::ACCESS_CONTROL_ALLOW_METHODS) - .is_some()); - assert!(headers - .get(http::header::ACCESS_CONTROL_ALLOW_HEADERS) - .is_some()); - } -} diff --git a/tests/defacto.rs b/tests/defacto.rs deleted file mode 100644 index 82ddfeb7..00000000 --- a/tests/defacto.rs +++ /dev/null @@ -1,32 +0,0 @@ -#[cfg(test)] -mod tests { - use http::{Response, StatusCode}; - use hyper::{Body, Client}; - - async fn http_get(url: &str) -> Response { - let client = Client::default(); - - client.get(url.parse().unwrap()).await.unwrap() - } - - #[tokio::test] - async fn defacto_get_request_to_root_responds_200() { - let response = http_get("http://0.0.0.0:7878").await; - - assert_eq!(response.status(), StatusCode::OK); - } - - #[tokio::test] - async fn defacto_get_request_retrieve_file() { - let response = http_get("http://0.0.0.0:7878/docs/screenshot.png").await; - - assert_eq!(response.status(), StatusCode::OK); - } - - #[tokio::test] - async fn defacto_get_request_file_not_found() { - let response = http_get("http://0.0.0.0:7878/xyz/abc.txt").await; - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } -} diff --git a/tests/e2e/basic.bats b/tests/e2e/basic.bats deleted file mode 100644 index 7a7c7cce..00000000 --- a/tests/e2e/basic.bats +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bats - -load 'helpers/load' - -@test "does not panics on default run" { - run bash -c '$BIN' & - - sleep 1 - - pkill -9 http-server - - sleep 1 - - assert_success -} - -@test "teardowns on graceful shutdown" { - run bash -c '$BIN --graceful-shutdown' & - - sleep 1 - - pkill -SIGINT http-server - - sleep 1 - - assert_success -} diff --git a/tests/e2e/helpers/assert.bash b/tests/e2e/helpers/assert.bash deleted file mode 100644 index b5e24294..00000000 --- a/tests/e2e/helpers/assert.bash +++ /dev/null @@ -1,720 +0,0 @@ -# -# bats-assert - Common assertions for Bats -# -# Written in 2016 by Zoltan Tombol -# -# To the extent possible under law, the author(s) have dedicated all -# copyright and related and neighboring rights to this software to the -# public domain worldwide. This software is distributed without any -# warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication -# along with this software. If not, see -# . -# - -# -# assert.bash -# ----------- -# -# Assertions are functions that perform a test and output relevant -# information on failure to help debugging. They return 1 on failure -# and 0 otherwise. -# -# All output is formatted for readability using the functions of -# `output.bash' and sent to the standard error. -# - -# Fail and display the expression if it evaluates to false. -# -# NOTE: The expression must be a simple command. Compound commands, such -# as `[[', can be used only when executed with `bash -c'. -# -# Globals: -# none -# Arguments: -# $1 - expression -# Returns: -# 0 - expression evaluates to TRUE -# 1 - otherwise -# Outputs: -# STDERR - details, on failure -assert() { - if ! "$@"; then - batslib_print_kv_single 10 'expression' "$*" \ - | batslib_decorate 'assertion failed' \ - | fail - fi -} - -# Fail and display the expression if it evaluates to true. -# -# NOTE: The expression must be a simple command. Compound commands, such -# as `[[', can be used only when executed with `bash -c'. -# -# Globals: -# none -# Arguments: -# $1 - expression -# Returns: -# 0 - expression evaluates to FALSE -# 1 - otherwise -# Outputs: -# STDERR - details, on failure -refute() { - if "$@"; then - batslib_print_kv_single 10 'expression' "$*" \ - | batslib_decorate 'assertion succeeded, but it was expected to fail' \ - | fail - fi -} - -# Fail and display details if the expected and actual values do not -# equal. Details include both values. -# -# Globals: -# none -# Arguments: -# $1 - actual value -# $2 - expected value -# Returns: -# 0 - values equal -# 1 - otherwise -# Outputs: -# STDERR - details, on failure -assert_equal() { - if [[ $1 != "$2" ]]; then - batslib_print_kv_single_or_multi 8 \ - 'expected' "$2" \ - 'actual' "$1" \ - | batslib_decorate 'values do not equal' \ - | fail - fi -} - -# Fail and display details if `$status' is not 0. Details include -# `$status' and `$output'. -# -# Globals: -# status -# output -# Arguments: -# none -# Returns: -# 0 - `$status' is 0 -# 1 - otherwise -# Outputs: -# STDERR - details, on failure -assert_success() { - if (( status != 0 )); then - { local -ir width=6 - batslib_print_kv_single "$width" 'status' "$status" - batslib_print_kv_single_or_multi "$width" 'output' "$output" - } | batslib_decorate 'command failed' \ - | fail - fi -} - -# Fail and display details if `$status' is 0. Details include `$output'. -# -# Optionally, when the expected status is specified, fail when it does -# not equal `$status'. In this case, details include the expected and -# actual status, and `$output'. -# -# Globals: -# status -# output -# Arguments: -# $1 - [opt] expected status -# Returns: -# 0 - `$status' is not 0, or -# `$status' equals the expected status -# 1 - otherwise -# Outputs: -# STDERR - details, on failure -assert_failure() { - (( $# > 0 )) && local -r expected="$1" - if (( status == 0 )); then - batslib_print_kv_single_or_multi 6 'output' "$output" \ - | batslib_decorate 'command succeeded, but it was expected to fail' \ - | fail - elif (( $# > 0 )) && (( status != expected )); then - { local -ir width=8 - batslib_print_kv_single "$width" \ - 'expected' "$expected" \ - 'actual' "$status" - batslib_print_kv_single_or_multi "$width" \ - 'output' "$output" - } | batslib_decorate 'command failed as expected, but status differs' \ - | fail - fi -} - -# Fail and display details if `$output' does not match the expected -# output. The expected output can be specified either by the first -# parameter or on the standard input. -# -# By default, literal matching is performed. The assertion fails if the -# expected output does not equal `$output'. Details include both values. -# -# Option `--partial' enables partial matching. The assertion fails if -# the expected substring cannot be found in `$output'. -# -# Option `--regexp' enables regular expression matching. The assertion -# fails if the extended regular expression does not match `$output'. An -# invalid regular expression causes an error to be displayed. -# -# It is an error to use partial and regular expression matching -# simultaneously. -# -# Globals: -# output -# Options: -# -p, --partial - partial matching -# -e, --regexp - extended regular expression matching -# Arguments: -# $1 - [=STDIN] expected output -# Returns: -# 0 - expected matches the actual output -# 1 - otherwise -# Inputs: -# STDIN - [=$1] expected output -# Outputs: -# STDERR - details, on failure -# error message, on error -assert_output() { - local -i is_mode_partial=0 - local -i is_mode_regexp=0 - - # Handle options. - while (( $# > 0 )); do - case "$1" in - -p|--partial) is_mode_partial=1; shift ;; - -e|--regexp) is_mode_regexp=1; shift ;; - --) shift; break ;; - *) break ;; - esac - done - - if (( is_mode_partial )) && (( is_mode_regexp )); then - echo "\`--partial' and \`--regexp' are mutually exclusive" \ - | batslib_decorate 'ERROR: assert_output' \ - | fail - return $? - fi - - # Arguments. - local expected - (( $# == 0 )) && expected="$(cat -)" || expected="$1" - - # Matching. - if (( is_mode_regexp )); then - if [[ '' =~ $expected ]] || (( $? == 2 )); then - echo "Invalid extended regular expression: \`$expected'" \ - | batslib_decorate 'ERROR: assert_output' \ - | fail - return $? - fi - if ! [[ $output =~ $expected ]]; then - batslib_print_kv_single_or_multi 6 \ - 'regexp' "$expected" \ - 'output' "$output" \ - | batslib_decorate 'regular expression does not match output' \ - | fail - fi - elif (( is_mode_partial )); then - if [[ $output != *"$expected"* ]]; then - batslib_print_kv_single_or_multi 9 \ - 'substring' "$expected" \ - 'output' "$output" \ - | batslib_decorate 'output does not contain substring' \ - | fail - fi - else - if [[ $output != "$expected" ]]; then - batslib_print_kv_single_or_multi 8 \ - 'expected' "$expected" \ - 'actual' "$output" \ - | batslib_decorate 'output differs' \ - | fail - fi - fi -} - -# Fail and display details if `$output' matches the unexpected output. -# The unexpected output can be specified either by the first parameter -# or on the standard input. -# -# By default, literal matching is performed. The assertion fails if the -# unexpected output equals `$output'. Details include `$output'. -# -# Option `--partial' enables partial matching. The assertion fails if -# the unexpected substring is found in `$output'. The unexpected -# substring is added to details. -# -# Option `--regexp' enables regular expression matching. The assertion -# fails if the extended regular expression does matches `$output'. The -# regular expression is added to details. An invalid regular expression -# causes an error to be displayed. -# -# It is an error to use partial and regular expression matching -# simultaneously. -# -# Globals: -# output -# Options: -# -p, --partial - partial matching -# -e, --regexp - extended regular expression matching -# Arguments: -# $1 - [=STDIN] unexpected output -# Returns: -# 0 - unexpected matches the actual output -# 1 - otherwise -# Inputs: -# STDIN - [=$1] unexpected output -# Outputs: -# STDERR - details, on failure -# error message, on error -refute_output() { - local -i is_mode_partial=0 - local -i is_mode_regexp=0 - - # Handle options. - while (( $# > 0 )); do - case "$1" in - -p|--partial) is_mode_partial=1; shift ;; - -e|--regexp) is_mode_regexp=1; shift ;; - --) shift; break ;; - *) break ;; - esac - done - - if (( is_mode_partial )) && (( is_mode_regexp )); then - echo "\`--partial' and \`--regexp' are mutually exclusive" \ - | batslib_decorate 'ERROR: refute_output' \ - | fail - return $? - fi - - # Arguments. - local unexpected - (( $# == 0 )) && unexpected="$(cat -)" || unexpected="$1" - - if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then - echo "Invalid extended regular expression: \`$unexpected'" \ - | batslib_decorate 'ERROR: refute_output' \ - | fail - return $? - fi - - # Matching. - if (( is_mode_regexp )); then - if [[ $output =~ $unexpected ]] || (( $? == 0 )); then - batslib_print_kv_single_or_multi 6 \ - 'regexp' "$unexpected" \ - 'output' "$output" \ - | batslib_decorate 'regular expression should not match output' \ - | fail - fi - elif (( is_mode_partial )); then - if [[ $output == *"$unexpected"* ]]; then - batslib_print_kv_single_or_multi 9 \ - 'substring' "$unexpected" \ - 'output' "$output" \ - | batslib_decorate 'output should not contain substring' \ - | fail - fi - else - if [[ $output == "$unexpected" ]]; then - batslib_print_kv_single_or_multi 6 \ - 'output' "$output" \ - | batslib_decorate 'output equals, but it was expected to differ' \ - | fail - fi - fi -} - -# Fail and display details if the expected line is not found in the -# output (default) or in a specific line of it. -# -# By default, the entire output is searched for the expected line. The -# expected line is matched against every element of `${lines[@]}'. If no -# match is found, the assertion fails. Details include the expected line -# and `${lines[@]}'. -# -# When `--index ' is specified, only the -th line is matched. -# If the expected line does not match `${lines[]}', the assertion -# fails. Details include and the compared lines. -# -# By default, literal matching is performed. A literal match fails if -# the expected string does not equal the matched string. -# -# Option `--partial' enables partial matching. A partial match fails if -# the expected substring is not found in the target string. -# -# Option `--regexp' enables regular expression matching. A regular -# expression match fails if the extended regular expression does not -# match the target string. An invalid regular expression causes an error -# to be displayed. -# -# It is an error to use partial and regular expression matching -# simultaneously. -# -# Mandatory arguments to long options are mandatory for short options -# too. -# -# Globals: -# output -# lines -# Options: -# -n, --index - match the -th line -# -p, --partial - partial matching -# -e, --regexp - extended regular expression matching -# Arguments: -# $1 - expected line -# Returns: -# 0 - match found -# 1 - otherwise -# Outputs: -# STDERR - details, on failure -# error message, on error -# FIXME(ztombol): Display `${lines[@]}' instead of `$output'! -assert_line() { - local -i is_match_line=0 - local -i is_mode_partial=0 - local -i is_mode_regexp=0 - - # Handle options. - while (( $# > 0 )); do - case "$1" in - -n|--index) - if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then - echo "\`--index' requires an integer argument: \`$2'" \ - | batslib_decorate 'ERROR: assert_line' \ - | fail - return $? - fi - is_match_line=1 - local -ri idx="$2" - shift 2 - ;; - -p|--partial) is_mode_partial=1; shift ;; - -e|--regexp) is_mode_regexp=1; shift ;; - --) shift; break ;; - *) break ;; - esac - done - - if (( is_mode_partial )) && (( is_mode_regexp )); then - echo "\`--partial' and \`--regexp' are mutually exclusive" \ - | batslib_decorate 'ERROR: assert_line' \ - | fail - return $? - fi - - # Arguments. - local -r expected="$1" - - if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then - echo "Invalid extended regular expression: \`$expected'" \ - | batslib_decorate 'ERROR: assert_line' \ - | fail - return $? - fi - - # Matching. - if (( is_match_line )); then - # Specific line. - if (( is_mode_regexp )); then - if ! [[ ${lines[$idx]} =~ $expected ]]; then - batslib_print_kv_single 6 \ - 'index' "$idx" \ - 'regexp' "$expected" \ - 'line' "${lines[$idx]}" \ - | batslib_decorate 'regular expression does not match line' \ - | fail - fi - elif (( is_mode_partial )); then - if [[ ${lines[$idx]} != *"$expected"* ]]; then - batslib_print_kv_single 9 \ - 'index' "$idx" \ - 'substring' "$expected" \ - 'line' "${lines[$idx]}" \ - | batslib_decorate 'line does not contain substring' \ - | fail - fi - else - if [[ ${lines[$idx]} != "$expected" ]]; then - batslib_print_kv_single 8 \ - 'index' "$idx" \ - 'expected' "$expected" \ - 'actual' "${lines[$idx]}" \ - | batslib_decorate 'line differs' \ - | fail - fi - fi - else - # Contained in output. - if (( is_mode_regexp )); then - local -i idx - for (( idx = 0; idx < ${#lines[@]}; ++idx )); do - [[ ${lines[$idx]} =~ $expected ]] && return 0 - done - { local -ar single=( - 'regexp' "$expected" - ) - local -ar may_be_multi=( - 'output' "$output" - ) - local -ir width="$( batslib_get_max_single_line_key_width \ - "${single[@]}" "${may_be_multi[@]}" )" - batslib_print_kv_single "$width" "${single[@]}" - batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" - } | batslib_decorate 'no output line matches regular expression' \ - | fail - elif (( is_mode_partial )); then - local -i idx - for (( idx = 0; idx < ${#lines[@]}; ++idx )); do - [[ ${lines[$idx]} == *"$expected"* ]] && return 0 - done - { local -ar single=( - 'substring' "$expected" - ) - local -ar may_be_multi=( - 'output' "$output" - ) - local -ir width="$( batslib_get_max_single_line_key_width \ - "${single[@]}" "${may_be_multi[@]}" )" - batslib_print_kv_single "$width" "${single[@]}" - batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" - } | batslib_decorate 'no output line contains substring' \ - | fail - else - local -i idx - for (( idx = 0; idx < ${#lines[@]}; ++idx )); do - [[ ${lines[$idx]} == "$expected" ]] && return 0 - done - { local -ar single=( - 'line' "$expected" - ) - local -ar may_be_multi=( - 'output' "$output" - ) - local -ir width="$( batslib_get_max_single_line_key_width \ - "${single[@]}" "${may_be_multi[@]}" )" - batslib_print_kv_single "$width" "${single[@]}" - batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" - } | batslib_decorate 'output does not contain line' \ - | fail - fi - fi -} - -# Fail and display details if the unexpected line is found in the output -# (default) or in a specific line of it. -# -# By default, the entire output is searched for the unexpected line. The -# unexpected line is matched against every element of `${lines[@]}'. If -# a match is found, the assertion fails. Details include the unexpected -# line, the index of the first match and `${lines[@]}' with the matching -# line highlighted if `${lines[@]}' is longer than one line. -# -# When `--index ' is specified, only the -th line is matched. -# If the unexpected line matches `${lines[]}', the assertion fails. -# Details include and the unexpected line. -# -# By default, literal matching is performed. A literal match fails if -# the unexpected string does not equal the matched string. -# -# Option `--partial' enables partial matching. A partial match fails if -# the unexpected substring is found in the target string. When used with -# `--index ', the unexpected substring is also displayed on -# failure. -# -# Option `--regexp' enables regular expression matching. A regular -# expression match fails if the extended regular expression matches the -# target string. When used with `--index ', the regular expression -# is also displayed on failure. An invalid regular expression causes an -# error to be displayed. -# -# It is an error to use partial and regular expression matching -# simultaneously. -# -# Mandatory arguments to long options are mandatory for short options -# too. -# -# Globals: -# output -# lines -# Options: -# -n, --index - match the -th line -# -p, --partial - partial matching -# -e, --regexp - extended regular expression matching -# Arguments: -# $1 - unexpected line -# Returns: -# 0 - match not found -# 1 - otherwise -# Outputs: -# STDERR - details, on failure -# error message, on error -# FIXME(ztombol): Display `${lines[@]}' instead of `$output'! -refute_line() { - local -i is_match_line=0 - local -i is_mode_partial=0 - local -i is_mode_regexp=0 - - # Handle options. - while (( $# > 0 )); do - case "$1" in - -n|--index) - if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then - echo "\`--index' requires an integer argument: \`$2'" \ - | batslib_decorate 'ERROR: refute_line' \ - | fail - return $? - fi - is_match_line=1 - local -ri idx="$2" - shift 2 - ;; - -p|--partial) is_mode_partial=1; shift ;; - -e|--regexp) is_mode_regexp=1; shift ;; - --) shift; break ;; - *) break ;; - esac - done - - if (( is_mode_partial )) && (( is_mode_regexp )); then - echo "\`--partial' and \`--regexp' are mutually exclusive" \ - | batslib_decorate 'ERROR: refute_line' \ - | fail - return $? - fi - - # Arguments. - local -r unexpected="$1" - - if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then - echo "Invalid extended regular expression: \`$unexpected'" \ - | batslib_decorate 'ERROR: refute_line' \ - | fail - return $? - fi - - # Matching. - if (( is_match_line )); then - # Specific line. - if (( is_mode_regexp )); then - if [[ ${lines[$idx]} =~ $unexpected ]] || (( $? == 0 )); then - batslib_print_kv_single 6 \ - 'index' "$idx" \ - 'regexp' "$unexpected" \ - 'line' "${lines[$idx]}" \ - | batslib_decorate 'regular expression should not match line' \ - | fail - fi - elif (( is_mode_partial )); then - if [[ ${lines[$idx]} == *"$unexpected"* ]]; then - batslib_print_kv_single 9 \ - 'index' "$idx" \ - 'substring' "$unexpected" \ - 'line' "${lines[$idx]}" \ - | batslib_decorate 'line should not contain substring' \ - | fail - fi - else - if [[ ${lines[$idx]} == "$unexpected" ]]; then - batslib_print_kv_single 5 \ - 'index' "$idx" \ - 'line' "${lines[$idx]}" \ - | batslib_decorate 'line should differ' \ - | fail - fi - fi - else - # Line contained in output. - if (( is_mode_regexp )); then - local -i idx - for (( idx = 0; idx < ${#lines[@]}; ++idx )); do - if [[ ${lines[$idx]} =~ $unexpected ]]; then - { local -ar single=( - 'regexp' "$unexpected" - 'index' "$idx" - ) - local -a may_be_multi=( - 'output' "$output" - ) - local -ir width="$( batslib_get_max_single_line_key_width \ - "${single[@]}" "${may_be_multi[@]}" )" - batslib_print_kv_single "$width" "${single[@]}" - if batslib_is_single_line "${may_be_multi[1]}"; then - batslib_print_kv_single "$width" "${may_be_multi[@]}" - else - may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \ - | batslib_prefix \ - | batslib_mark '>' "$idx" )" - batslib_print_kv_multi "${may_be_multi[@]}" - fi - } | batslib_decorate 'no line should match the regular expression' \ - | fail - return $? - fi - done - elif (( is_mode_partial )); then - local -i idx - for (( idx = 0; idx < ${#lines[@]}; ++idx )); do - if [[ ${lines[$idx]} == *"$unexpected"* ]]; then - { local -ar single=( - 'substring' "$unexpected" - 'index' "$idx" - ) - local -a may_be_multi=( - 'output' "$output" - ) - local -ir width="$( batslib_get_max_single_line_key_width \ - "${single[@]}" "${may_be_multi[@]}" )" - batslib_print_kv_single "$width" "${single[@]}" - if batslib_is_single_line "${may_be_multi[1]}"; then - batslib_print_kv_single "$width" "${may_be_multi[@]}" - else - may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \ - | batslib_prefix \ - | batslib_mark '>' "$idx" )" - batslib_print_kv_multi "${may_be_multi[@]}" - fi - } | batslib_decorate 'no line should contain substring' \ - | fail - return $? - fi - done - else - local -i idx - for (( idx = 0; idx < ${#lines[@]}; ++idx )); do - if [[ ${lines[$idx]} == "$unexpected" ]]; then - { local -ar single=( - 'line' "$unexpected" - 'index' "$idx" - ) - local -a may_be_multi=( - 'output' "$output" - ) - local -ir width="$( batslib_get_max_single_line_key_width \ - "${single[@]}" "${may_be_multi[@]}" )" - batslib_print_kv_single "$width" "${single[@]}" - if batslib_is_single_line "${may_be_multi[1]}"; then - batslib_print_kv_single "$width" "${may_be_multi[@]}" - else - may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \ - | batslib_prefix \ - | batslib_mark '>' "$idx" )" - batslib_print_kv_multi "${may_be_multi[@]}" - fi - } | batslib_decorate 'line should not be in output' \ - | fail - return $? - fi - done - fi - fi -} \ No newline at end of file diff --git a/tests/e2e/helpers/error.bash b/tests/e2e/helpers/error.bash deleted file mode 100644 index e5d97912..00000000 --- a/tests/e2e/helpers/error.bash +++ /dev/null @@ -1,41 +0,0 @@ -# -# bats-support - Supporting library for Bats test helpers -# -# Written in 2016 by Zoltan Tombol -# -# To the extent possible under law, the author(s) have dedicated all -# copyright and related and neighboring rights to this software to the -# public domain worldwide. This software is distributed without any -# warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication -# along with this software. If not, see -# . -# - -# -# error.bash -# ---------- -# -# Functions implementing error reporting. Used by public helper -# functions or test suits directly. -# - -# Fail and display a message. When no parameters are specified, the -# message is read from the standard input. Other functions use this to -# report failure. -# -# Globals: -# none -# Arguments: -# $@ - [=STDIN] message -# Returns: -# 1 - always -# Inputs: -# STDIN - [=$@] message -# Outputs: -# STDERR - message -fail() { - (( $# == 0 )) && batslib_err || batslib_err "$@" - return 1 -} diff --git a/tests/e2e/helpers/lang.bash b/tests/e2e/helpers/lang.bash deleted file mode 100644 index c57e299c..00000000 --- a/tests/e2e/helpers/lang.bash +++ /dev/null @@ -1,73 +0,0 @@ -# -# bats-util - Various auxiliary functions for Bats -# -# Written in 2016 by Zoltan Tombol -# -# To the extent possible under law, the author(s) have dedicated all -# copyright and related and neighboring rights to this software to the -# public domain worldwide. This software is distributed without any -# warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication -# along with this software. If not, see -# . -# - -# -# lang.bash -# --------- -# -# Bash language and execution related functions. Used by public helper -# functions. -# - -# Check whether the calling function was called from a given function. -# -# By default, direct invocation is checked. The function succeeds if the -# calling function was called directly from the given function. In other -# words, if the given function is the next element on the call stack. -# -# When `--indirect' is specified, indirect invocation is checked. The -# function succeeds if the calling function was called from the given -# function with any number of intermediate calls. In other words, if the -# given function can be found somewhere on the call stack. -# -# Direct invocation is a form of indirect invocation with zero -# intermediate calls. -# -# Globals: -# FUNCNAME -# Options: -# -i, --indirect - check indirect invocation -# Arguments: -# $1 - calling function's name -# Returns: -# 0 - current function was called from the given function -# 1 - otherwise -batslib_is_caller() { - local -i is_mode_direct=1 - - # Handle options. - while (( $# > 0 )); do - case "$1" in - -i|--indirect) is_mode_direct=0; shift ;; - --) shift; break ;; - *) break ;; - esac - done - - # Arguments. - local -r func="$1" - - # Check call stack. - if (( is_mode_direct )); then - [[ $func == "${FUNCNAME[2]}" ]] && return 0 - else - local -i depth - for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do - [[ $func == "${FUNCNAME[$depth]}" ]] && return 0 - done - fi - - return 1 -} diff --git a/tests/e2e/helpers/load.bash b/tests/e2e/helpers/load.bash deleted file mode 100644 index 87ee01c8..00000000 --- a/tests/e2e/helpers/load.bash +++ /dev/null @@ -1,4 +0,0 @@ -source "$(dirname "${BASH_SOURCE[0]}")/assert.bash" -source "$(dirname "${BASH_SOURCE[0]}")/error.bash" -source "$(dirname "${BASH_SOURCE[0]}")/lang.bash" -source "$(dirname "${BASH_SOURCE[0]}")/output.bash" diff --git a/tests/e2e/helpers/output.bash b/tests/e2e/helpers/output.bash deleted file mode 100644 index c6cf6a6b..00000000 --- a/tests/e2e/helpers/output.bash +++ /dev/null @@ -1,279 +0,0 @@ -# -# bats-support - Supporting library for Bats test helpers -# -# Written in 2016 by Zoltan Tombol -# -# To the extent possible under law, the author(s) have dedicated all -# copyright and related and neighboring rights to this software to the -# public domain worldwide. This software is distributed without any -# warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication -# along with this software. If not, see -# . -# - -# -# output.bash -# ----------- -# -# Private functions implementing output formatting. Used by public -# helper functions. -# - -# Print a message to the standard error. When no parameters are -# specified, the message is read from the standard input. -# -# Globals: -# none -# Arguments: -# $@ - [=STDIN] message -# Returns: -# none -# Inputs: -# STDIN - [=$@] message -# Outputs: -# STDERR - message -batslib_err() { - { if (( $# > 0 )); then - echo "$@" - else - cat - - fi - } >&2 -} - -# Count the number of lines in the given string. -# -# TODO(ztombol): Fix tests and remove this note after #93 is resolved! -# NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not -# give the same result as `${#lines[@]}' when the output contains -# empty lines. -# See PR #93 (https://github.com/sstephenson/bats/pull/93). -# -# Globals: -# none -# Arguments: -# $1 - string -# Returns: -# none -# Outputs: -# STDOUT - number of lines -batslib_count_lines() { - local -i n_lines=0 - local line - while IFS='' read -r line || [[ -n $line ]]; do - (( ++n_lines )) - done < <(printf '%s' "$1") - echo "$n_lines" -} - -# Determine whether all strings are single-line. -# -# Globals: -# none -# Arguments: -# $@ - strings -# Returns: -# 0 - all strings are single-line -# 1 - otherwise -batslib_is_single_line() { - for string in "$@"; do - (( $(batslib_count_lines "$string") > 1 )) && return 1 - done - return 0 -} - -# Determine the length of the longest key that has a single-line value. -# -# This function is useful in determining the correct width of the key -# column in two-column format when some keys may have multi-line values -# and thus should be excluded. -# -# Globals: -# none -# Arguments: -# $odd - key -# $even - value of the previous key -# Returns: -# none -# Outputs: -# STDOUT - length of longest key -batslib_get_max_single_line_key_width() { - local -i max_len=-1 - while (( $# != 0 )); do - local -i key_len="${#1}" - batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len" - shift 2 - done - echo "$max_len" -} - -# Print key-value pairs in two-column format. -# -# Keys are displayed in the first column, and their corresponding values -# in the second. To evenly line up values, the key column is fixed-width -# and its width is specified with the first parameter (possibly computed -# using `batslib_get_max_single_line_key_width'). -# -# Globals: -# none -# Arguments: -# $1 - width of key column -# $even - key -# $odd - value of the previous key -# Returns: -# none -# Outputs: -# STDOUT - formatted key-value pairs -batslib_print_kv_single() { - local -ir col_width="$1"; shift - while (( $# != 0 )); do - printf '%-*s : %s\n' "$col_width" "$1" "$2" - shift 2 - done -} - -# Print key-value pairs in multi-line format. -# -# The key is displayed first with the number of lines of its -# corresponding value in parenthesis. Next, starting on the next line, -# the value is displayed. For better readability, it is recommended to -# indent values using `batslib_prefix'. -# -# Globals: -# none -# Arguments: -# $odd - key -# $even - value of the previous key -# Returns: -# none -# Outputs: -# STDOUT - formatted key-value pairs -batslib_print_kv_multi() { - while (( $# != 0 )); do - printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )" - printf '%s\n' "$2" - shift 2 - done -} - -# Print all key-value pairs in either two-column or multi-line format -# depending on whether all values are single-line. -# -# If all values are single-line, print all pairs in two-column format -# with the specified key column width (identical to using -# `batslib_print_kv_single'). -# -# Otherwise, print all pairs in multi-line format after indenting values -# with two spaces for readability (identical to using `batslib_prefix' -# and `batslib_print_kv_multi') -# -# Globals: -# none -# Arguments: -# $1 - width of key column (for two-column format) -# $even - key -# $odd - value of the previous key -# Returns: -# none -# Outputs: -# STDOUT - formatted key-value pairs -batslib_print_kv_single_or_multi() { - local -ir width="$1"; shift - local -a pairs=( "$@" ) - - local -a values=() - local -i i - for (( i=1; i < ${#pairs[@]}; i+=2 )); do - values+=( "${pairs[$i]}" ) - done - - if batslib_is_single_line "${values[@]}"; then - batslib_print_kv_single "$width" "${pairs[@]}" - else - local -i i - for (( i=1; i < ${#pairs[@]}; i+=2 )); do - pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )" - done - batslib_print_kv_multi "${pairs[@]}" - fi -} - -# Prefix each line read from the standard input with the given string. -# -# Globals: -# none -# Arguments: -# $1 - [= ] prefix string -# Returns: -# none -# Inputs: -# STDIN - lines -# Outputs: -# STDOUT - prefixed lines -batslib_prefix() { - local -r prefix="${1:- }" - local line - while IFS='' read -r line || [[ -n $line ]]; do - printf '%s%s\n' "$prefix" "$line" - done -} - -# Mark select lines of the text read from the standard input by -# overwriting their beginning with the given string. -# -# Usually the input is indented by a few spaces using `batslib_prefix' -# first. -# -# Globals: -# none -# Arguments: -# $1 - marking string -# $@ - indices (zero-based) of lines to mark -# Returns: -# none -# Inputs: -# STDIN - lines -# Outputs: -# STDOUT - lines after marking -batslib_mark() { - local -r symbol="$1"; shift - # Sort line numbers. - set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" ) - - local line - local -i idx=0 - while IFS='' read -r line || [[ -n $line ]]; do - if (( ${1:--1} == idx )); then - printf '%s\n' "${symbol}${line:${#symbol}}" - shift - else - printf '%s\n' "$line" - fi - (( ++idx )) - done -} - -# Enclose the input text in header and footer lines. -# -# The header contains the given string as title. The output is preceded -# and followed by an additional newline to make it stand out more. -# -# Globals: -# none -# Arguments: -# $1 - title -# Returns: -# none -# Inputs: -# STDIN - text -# Outputs: -# STDOUT - decorated text -batslib_decorate() { - echo - echo "-- $1 --" - cat - - echo '--' - echo -} diff --git a/tests/gzip.rs b/tests/gzip.rs deleted file mode 100644 index ca43ae19..00000000 --- a/tests/gzip.rs +++ /dev/null @@ -1,73 +0,0 @@ -#[cfg(test)] -mod tests { - use http::{Request, Response, StatusCode}; - use hyper::{Body, Client}; - - async fn http_get(url: &str, accept_encoding: Option<&str>) -> Response { - let mut request = Request::builder(); - request = request.uri(url); - - let headers = request.headers_mut().unwrap(); - - if let Some(accept_encoding) = accept_encoding { - headers.insert( - http::header::ACCEPT_ENCODING, - http::HeaderValue::from_str(accept_encoding).unwrap(), - ); - } - - let client = Client::default(); - client - .request(request.body(Body::empty()).unwrap()) - .await - .unwrap() - } - - #[tokio::test] - async fn gzip_get_request_to_root_responds_200() { - let response = http_get("http://0.0.0.0:7878", Some("gzip, brotli")).await; - - assert_eq!(response.status(), StatusCode::OK); - assert!(response - .headers() - .get(http::header::CONTENT_ENCODING) - .is_some()); - } - - #[tokio::test] - async fn gzip_get_request_retrieve_image_file_not_present() { - let response = http_get( - "http://0.0.0.0:7878/docs/screenshot.png", - Some("gzip, brotli"), - ) - .await; - - assert_eq!(response.status(), StatusCode::OK); - assert!(response - .headers() - .get(http::header::CONTENT_ENCODING) - .is_some()); - } - - #[tokio::test] - async fn gzip_get_request_file_not_found() { - let response = http_get("http://0.0.0.0:7878/docs/xyz/foo.txt", Some("gzip, brotli")).await; - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - assert!(response - .headers() - .get(http::header::CONTENT_ENCODING) - .is_none()); - } - - #[tokio::test] - async fn gzip_no_compression_if_no_accept_encoding_header_is_provided() { - let response = http_get("http://0.0.0.0:7878/docs/screenshot.png", None).await; - - assert_eq!(response.status(), StatusCode::OK); - assert!(response - .headers() - .get(http::header::CONTENT_ENCODING) - .is_none()); - } -} diff --git a/tests/mod.rs b/tests/mod.rs deleted file mode 100644 index fb9996a6..00000000 --- a/tests/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod basic_auth; -mod cors; -mod defacto;