diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 722 | ||||
-rw-r--r-- | Cargo.toml | 14 | ||||
-rw-r--r-- | LICENSE | 13 | ||||
-rw-r--r-- | README.md | 12 | ||||
-rw-r--r-- | res/400.html | 14 | ||||
-rw-r--r-- | res/404.html | 14 | ||||
-rw-r--r-- | res/500.html | 14 | ||||
-rw-r--r-- | res/501.html | 14 | ||||
-rw-r--r-- | res/listing.html | 35 | ||||
-rw-r--r-- | res/listing_entry.html | 11 | ||||
-rw-r--r-- | src/chunked_bufreader.rs | 48 | ||||
-rw-r--r-- | src/http/flatten.rs | 37 | ||||
-rw-r--r-- | src/http/mod.rs | 6 | ||||
-rw-r--r-- | src/http/parser.rs | 523 | ||||
-rw-r--r-- | src/http/rule.rs | 123 | ||||
-rw-r--r-- | src/main.rs | 159 | ||||
-rw-r--r-- | src/peekable_bufreader.rs | 47 | ||||
-rw-r--r-- | src/response.rs | 55 |
19 files changed, 1862 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f2c988d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,722 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-mutex", + "blocking", + "futures-lite", + "num_cpus", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi", +] + +[[package]] +name = "async-lock" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f06685bad74e0570f5213741bea82158279a4103d988e57bfada11ad230341" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "blocking" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" + +[[package]] +name = "cache-padded" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" + +[[package]] +name = "cc" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "ctor" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "event-listener" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" + +[[package]] +name = "fastrand" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b705829d1e87f762c2df6da140b26af5839e1033aa84aa5f56bb688e4e1bdb" +dependencies = [ + "instant", +] + +[[package]] +name = "futures" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" + +[[package]] +name = "futures-executor" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" +dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" + +[[package]] +name = "futures-task" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" + +[[package]] +name = "futures-util" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" +dependencies = [ + "autocfg", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "gloo-timers" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "js-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", + "value-bag", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "pin-project" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "polling" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92341d779fa34ea8437ef4d82d440d5e1ce3f3ff7f824aa64424cd481f9a1f25" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "slab" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" + +[[package]] +name = "socket2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + +[[package]] +name = "tiny-serve" +version = "0.1.0" +dependencies = [ + "async-std", + "chrono", + "futures", + "lazy_static", + "pin-project", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd320e1520f94261153e96f7534476ad869c14022aee1e59af7c778075d840ae" +dependencies = [ + "ctor", + "version_check", +] + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" + +[[package]] +name = "web-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ae2c800 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tiny-serve" +version = "0.1.0" +authors = ["matthew"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = "0.3" +async-std = { version = "1.6", features = ["attributes"] } +lazy_static = "1.4" +pin-project = "1.0" +chrono = "0.4" @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..54071bf --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# tiny-serve +A small, fast, fully asynchronous, and concurrent HTTP/1.1 server for static content stored on the filesystem, written in Rust using `async_std` and `futures`. + +It uses a custom hand-written parser based on the relevant RFCs that sweeps over the message with a single byte of lookahead. The parser supports a reasonable subset of HTTP/1.1, but lacks some unnecessary grammar and such that are only used for optional, unimplemented HTTP/1.1 verbs. + +It accepts one command line argument, the port number to bind to: + +``` +tiny-serve 8080 +``` + +which defaults to port 8000 if not specified or invalid.
\ No newline at end of file diff --git a/res/400.html b/res/400.html new file mode 100644 index 0000000..db6465c --- /dev/null +++ b/res/400.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title>400 Bad Request</title> + <link href="data:," rel="icon"/> + </head> + <body> + <h1> + 400 Bad Request + </h1> + </body> +</html> diff --git a/res/404.html b/res/404.html new file mode 100644 index 0000000..ce779a4 --- /dev/null +++ b/res/404.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title>404 Not Found</title> + <link href="data:," rel="icon"/> + </head> + <body> + <h1> + 404 Not Found + </h1> + </body> +</html> diff --git a/res/500.html b/res/500.html new file mode 100644 index 0000000..84abbae --- /dev/null +++ b/res/500.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title>500 Internal Server Error</title> + <link href="data:," rel="icon"/> + </head> + <body> + <h1> + 500 Internal Server Error + </h1> + </body> +</html> diff --git a/res/501.html b/res/501.html new file mode 100644 index 0000000..64b1a64 --- /dev/null +++ b/res/501.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title>501 Not Implemented</title> + <link href="data:," rel="icon"/> + </head> + <body> + <h1> + 501 Not Implemented + </h1> + </body> +</html> diff --git a/res/listing.html b/res/listing.html new file mode 100644 index 0000000..51e88a2 --- /dev/null +++ b/res/listing.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title>/{}</title> + <link href="data:," rel="icon"/> + <style> + body {{ + font-family: sans-serif; + padding: 5px; + }} + + table {{ + border-collapse: collapse; + }} + + tr, td {{ + padding: 5px; + }} + + tr + tr {{ + border-top: 1px solid black; + }} + </style> + </head> + <body> + <h1> + /{} + </h1> + <table> + {} + </table> + </body> +</html> diff --git a/res/listing_entry.html b/res/listing_entry.html new file mode 100644 index 0000000..40183fd --- /dev/null +++ b/res/listing_entry.html @@ -0,0 +1,11 @@ +<tr> + <td> + <a href="/{}">{}</a> + </td> + <td> + {} + </td> + <td> + {} + </td> +</tr> diff --git a/src/chunked_bufreader.rs b/src/chunked_bufreader.rs new file mode 100644 index 0000000..02c2bb8 --- /dev/null +++ b/src/chunked_bufreader.rs @@ -0,0 +1,48 @@ +use std::pin::Pin; + +use async_std::prelude::*; +use async_std::io::BufReader; +use async_std::io::Read; +use futures::task::{Context, Poll}; +use pin_project::pin_project; + +const CHUNK_SIZE: usize = 4096; + +#[pin_project] +pub struct ChunkedBufReader<T> + where T: Read + Unpin { + #[pin] + reader: BufReader<T>, +} + +impl<T> ChunkedBufReader<T> + where T: Read + Unpin { + pub fn new(reader: BufReader<T>) -> Self { + Self { + reader, + } + } +} + +impl<T> Stream for ChunkedBufReader<T> + where T: Read + Unpin { + type Item = Vec<u8>; + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { + let this = self.project(); + // This is quite wasteful, but perfomance is fine and the only real optimization (other + // than some zero-copy shenanigans) would be to not initialize this vec, with unsafe{} + let mut chunk = vec![0; CHUNK_SIZE]; + match this.reader.poll_read(cx, &mut chunk[0..(CHUNK_SIZE - 1)]) { + Poll::Ready(Ok(size)) => { + if size == 0 { + Poll::Ready(None) + } else { + chunk.truncate(size); + Poll::Ready(Some(chunk)) + } + }, + Poll::Ready(Err(_)) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/src/http/flatten.rs b/src/http/flatten.rs new file mode 100644 index 0000000..51ead0c --- /dev/null +++ b/src/http/flatten.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use super::rule::{HTTPMessage, Method}; + +#[derive(Debug)] +pub enum Version { + Http0_9, + Http1_0, + Http1_1, + // HTTP/2 won't parse, anyway. +} + +#[derive(Debug)] +pub struct HTTPRequest { + pub method: Method, + pub version: Version, + pub requested_path: Vec<String>, + pub headers: HashMap<String, Vec<u8>>, +} + +pub fn flatten(message: HTTPMessage) -> Option<HTTPRequest> { + let method = message.request_line.method; + let version = match (message.request_line.http_version.major, message.request_line.http_version.minor) { + (0, 9) => Version::Http0_9, + (1, 0) => Version::Http1_0, + (1, 1) => Version::Http1_1, + _ => return None, + }; + let requested_path = message.request_line.request_target.absolute_path.segments.into_iter().map(|segment| segment.lexeme).collect(); + let headers = message.header_fields.into_iter().map(|field| (field.name.lexeme, field.value.content)).collect(); + Some(HTTPRequest{ + method, + version, + requested_path, + headers, + }) +} diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 0000000..d2fd5ac --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,6 @@ +mod parser; +mod rule; +mod flatten; + +pub use parser::Parser; +pub use rule::Method; diff --git a/src/http/parser.rs b/src/http/parser.rs new file mode 100644 index 0000000..259ddf2 --- /dev/null +++ b/src/http/parser.rs @@ -0,0 +1,523 @@ +use async_std::io::Read; + +use super::rule::{HTTPMessage, HeaderField, FieldName, FieldValue, FieldContent, RequestLine, Method, OriginForm, HTTPVersion, AbsolutePath, Query, Segment}; +use super::flatten::{flatten, HTTPRequest}; +use crate::peekable_bufreader::PeekableBufReader; + +const HTAB: u8 = 0x09; +const SPACE: u8 = 0x20; +const PERCENT: u8 = 0x25; +const SLASH: u8 = 0x2F; +const DOT: u8 = 0x2E; +const COLON: u8 = 0x3A; +const QUESTION_MARK: u8 = 0x3F; +const ATSIGN: u8 = 0x40; + +enum ErrorType { + Missing, + Malformed, +} + +pub struct Parser<T> + where T: Read + Unpin { + source: PeekableBufReader<T>, +} + +impl<T> Parser<T> + where T: Read + Unpin { + pub fn new(source: PeekableBufReader<T>) -> Self { + Self { + source, + } + } + + pub async fn parse(self) -> Option<HTTPRequest> { + flatten(self.http_message().await?) + } + + /* + * RFC 7230, Page 19 + */ + async fn http_message(mut self) -> Option<HTTPMessage> { + let start_line = self.start_line().await?; + let mut header_fields = Vec::new(); + loop { + if let Some(_) = self.consume_carriage_return().await { + break; + } + header_fields.push(self.header_field().await?); + self.consume_carriage_return().await?; + } + // GET Requests don't have a message body, and we only really deal with GET requests. + // There's no need to examine the headers and attempt to read a message body. + Some(HTTPMessage { + request_line: start_line, + header_fields, + }) + } + + /* + * RFC 7230, Page 23 + */ + async fn header_field(&mut self) -> Option<HeaderField> { + let name = self.field_name().await?; + self.consume_char(&COLON).await?; + self.consume_optional_whitespace().await; + let value = self.field_value().await?; + self.consume_optional_whitespace().await; + Some(HeaderField { + name, + value, + }) + } + + /* + * RFC 7230, Page 23 + */ + async fn field_name(&mut self) -> Option<FieldName> { + Some(FieldName { + lexeme: self.logical_token().await?, + }) + } + + /* + * RFC 7230, Page 23 + * obs-fold is deprecated except within message/http media. This isn't going to come up for us, + * so we deviate from the grammar slightly. + */ + async fn field_value(&mut self) -> Option<FieldValue> { + let mut content = Vec::new(); + loop { + // Look, no obs-fold! + if let Some(value) = self.field_content().await { + // Let's do some pre-emptive flattening here. + content.push(value.first_char); + if let Some(second_char) = value.second_char { + content.push(SPACE); + content.push(second_char); + } + } else { + break; + } + } + Some(FieldValue { + content, + }) + } + + /* + * RFC 7230, Page 23 + */ + async fn field_content(&mut self) -> Option<FieldContent> { + let first_char = self.field_vchar().await?; + let second_char = if let Some(_) = self.consume_required_whitespace().await { + Some(self.field_vchar().await?) + } else { + None + }; + Some(FieldContent{ + first_char, + second_char, + }) + } + + async fn field_vchar(&mut self) -> Option<u8> { + let next_char = self.source.peek().await?; + if Self::is_visible_char(next_char) || Self::is_obs_text_char(next_char) { + return self.source.next().await; + } + None + } + + /* + * RFC 7230, Page 21 + * This is a server, so the start-line is exclusively a request-line. + */ + async fn start_line(&mut self) -> Option<RequestLine> { + self.request_line().await + } + + /* + * RFC 7230, Page 21 + */ + async fn request_line(&mut self) -> Option<RequestLine> { + let method = self.method().await?; + self.consume_char(&SPACE).await?; + let request_target = self.request_target().await?; + self.consume_char(&SPACE).await?; + let http_version = self.http_version().await?; + self.consume_carriage_return().await?; + Some(RequestLine { + method, + request_target, + http_version, + }) + } + + /* + * RFC 7230, Page 41 + * We only serve some static content; therefore we only need support origin-form. + */ + async fn request_target(&mut self) -> Option<OriginForm> { + self.origin_form().await + } + + /* + * RFC 7230, Page 42 + */ + async fn origin_form(&mut self) -> Option<OriginForm> { + let absolute_path = self.absolute_path().await?; + let query = if let Some(_) = self.consume_char(&QUESTION_MARK).await { + Some(self.query().await?) + } else { + None + }; + Some(OriginForm { + absolute_path, + query, + }) + } + + /* + * RFC 7230, Page 16 + */ + async fn absolute_path(&mut self) -> Option<AbsolutePath> { + let mut segments = Vec::new(); + self.consume_char(&SLASH).await?; + segments.push(self.segment().await?); + loop { + if let None = self.consume_char(&SLASH).await { + break; + } + if let Some(segment) = self.segment().await { + segments.push(segment); + } else { + return None; + } + } + Some(AbsolutePath { + segments, + }) + } + + /* + * RFC 3986, Page 23 + */ + async fn segment(&mut self) -> Option<Segment> { + let mut segment = Vec::new(); + while self.source.peek().await.is_some() { + match self.consume_path_character().await { + Ok(character) => segment.push(character as char), + Err(ErrorType::Missing) => break, + Err(ErrorType::Malformed) => return None, + } + } + Some(Segment{ + lexeme: segment.into_iter().collect(), + }) + } + + /* + * RFC 3986, Page 50 + */ + async fn query(&mut self) -> Option<Query> { + let mut query = Vec::new(); + while self.source.peek().await.is_some() { + match self.consume_query_character().await { + Ok(character) => query.push(character as char), + Err(ErrorType::Missing) => break, + Err(ErrorType::Malformed) => return None, + } + } + Some(Query{ + lexeme: query.into_iter().collect(), + }) + } + + /* + * RFC 7230, Page 14 + */ + async fn http_version(&mut self) -> Option<HTTPVersion> { + self.consume_logical_token("HTTP").await?; + self.consume_char(&SLASH).await?; + let major = Self::ascii_digit_to_value(&self.consume_digit().await?); + self.consume_char(&DOT).await?; + let minor = Self::ascii_digit_to_value(&self.consume_digit().await?); + Some(HTTPVersion{ + major, + minor, + }) + } + + /* + * RFC 7230, Page 21 + */ + async fn method(&mut self) -> Option<Method> { + Method::from_string(&self.logical_token().await?) + } + + /* + * RFC 7230, Page 27 + */ + async fn logical_token(&mut self) -> Option<String> { + let mut logical_token = Vec::new(); + if !Self::is_logical_token_char(self.source.peek().await?) { + return None; + } + while self.source.peek().await.is_some() && Self::is_logical_token_char(self.source.peek().await.unwrap()) { + logical_token.push(self.source.next().await.unwrap() as char); + } + Some(logical_token.into_iter().collect()) + } + + async fn consume_char(&mut self, character: &u8) -> Option<u8> { + let next_char = self.source.peek().await?; + if *next_char == *character { + return self.source.next().await; + } + None + } + + async fn consume_logical_token(&mut self, value: &str) -> Option<String> { + let logical_token = self.logical_token().await?; + if logical_token == value { + return Some(logical_token); + } + None + } + + /* + * RFC 3986, Page 23 + */ + async fn consume_path_character(&mut self) -> Result<u8, ErrorType> { + match self.consume_unreserved_character().await { + Some(character) => return Ok(character), + _ => {} + } + match self.consume_sub_delim_character().await { + Some(character) => return Ok(character), + _ => {} + } + match self.consume_char(&COLON).await { + Some(character) => return Ok(character), + _ => {} + } + match self.consume_char(&ATSIGN).await { + Some(character) => return Ok(character), + _ => {} + } + self.consume_percent_encoded().await + } + + /* + * RFC 3986, Page 50 + */ + async fn consume_query_character(&mut self) -> Result<u8, ErrorType> { + match self.consume_unreserved_character().await { + Some(character) => return Ok(character), + _ => {} + } + match self.consume_sub_delim_character().await { + Some(character) => return Ok(character), + _ => {} + } + match self.consume_char(&COLON).await { + Some(character) => return Ok(character), + _ => {} + } + match self.consume_char(&ATSIGN).await { + Some(character) => return Ok(character), + _ => {} + } + match self.consume_char(&SLASH).await { + Some(character) => return Ok(character), + _ => {} + } + match self.consume_char(&QUESTION_MARK).await { + Some(character) => return Ok(character), + _ => {} + } + self.consume_percent_encoded().await + } + + /* + * RFC 5234, Page 5 + */ + async fn consume_carriage_return(&mut self) -> Option<()> { + self.consume_char(&0x0D).await?; + self.consume_char(&0x0A).await?; + Some(()) + } + + async fn consume_optional_whitespace(&mut self) { + loop { + if let None = self.consume_char(&SPACE).await { + if let None = self.consume_char(&HTAB).await { + break; + } + } + } + } + + async fn consume_required_whitespace(&mut self) -> Option<()> { + if let None = self.consume_char(&SPACE).await { + if let None = self.consume_char(&HTAB).await { + return None; + } + } + loop { + if let None = self.consume_char(&SPACE).await { + if let None = self.consume_char(&HTAB).await { + break; + } + } + } + Some(()) + } + + /* + * RFC 5234, Page 14 + */ + async fn consume_digit(&mut self) -> Option<u8> { + let next_char = self.source.peek().await?; + if Self::is_digit_char(next_char) { + return self.source.next().await + } + None + } + + async fn consume_unreserved_character(&mut self) -> Option<u8> { + let next_char = self.source.peek().await?; + if Self::is_unreserved_char(next_char) { + return self.source.next().await + } + None + } + + async fn consume_sub_delim_character(&mut self) -> Option<u8> { + let next_char = self.source.peek().await?; + if Self::is_sub_delim_char(next_char) { + return self.source.next().await + } + None + } + + async fn consume_percent_encoded(&mut self) -> Result<u8, ErrorType> { + self.consume_char(&PERCENT).await.ok_or(ErrorType::Missing)?; + let high_word = self.consume_hex_digit().await.ok_or(ErrorType::Malformed)?; + let low_word = self.consume_hex_digit().await.ok_or(ErrorType::Malformed)?; + Self::hex_digits_to_byte(high_word, low_word).ok_or(ErrorType::Malformed) + } + + async fn consume_hex_digit(&mut self) -> Option<u8> { + let next_char = self.source.peek().await?; + if Self::is_hex_digit_char(next_char) { + return self.source.next().await + } + None + } + + fn ascii_digit_to_value(character: &u8) -> u32 { + *character as u32 - 0x30 + } + + fn hex_digits_to_byte(high_word: u8, low_word: u8) -> Option<u8> { + u8::from_str_radix(&((high_word as char).to_string() + &(low_word as char).to_string()), 16).ok() + } + + /* + * RFC 7230, Page 27 + */ + fn is_logical_token_char(character: &u8) -> bool { + *character == 0x21 || // ! + *character == 0x23 || // # + *character == 0x24 || // $ + *character == 0x25 || // % + *character == 0x26 || // & + *character == 0x27 || // ' + *character == 0x2A || // * + *character == 0x2B || // + + *character == 0x2D || // - + *character == 0x2E || // . + *character == 0x5E || // ^ + *character == 0x5F || // _ + *character == 0x60 || // ` + *character == 0x7C || // | + *character == 0x7E || // ~ + Self::is_digit_char(character) || + Self::is_alpha_char(character) + } + + /* + * RFC 3986, Page 13 + */ + fn is_unreserved_char(character: &u8) -> bool { + Self::is_alpha_char(character) || + Self::is_digit_char(character) || + *character == 0x2D || // - + *character == 0x2E || // . + *character == 0x5F || // _ + *character == 0x7E // ~ + } + + /* + * RFC 3986, Page 13 + */ + fn is_sub_delim_char(character: &u8) -> bool { + *character == 0x21 || // ! + *character == 0x24 || // $ + *character == 0x26 || // & + *character == 0x27 || // ' + *character == 0x28 || // ( + *character == 0x29 || // ) + *character == 0x2A || // * + *character == 0x2B || // + + *character == 0x2C || // , + *character == 0x3B || // ; + *character == 0x3D // = + } + + /* + * RFC 5234, Page 13 + */ + fn is_alpha_char(character: &u8) -> bool { + (*character >= 0x41 && *character <= 0x5A) || (*character >= 0x61 && *character <= 0x7A) + } + + /* + * RFC 5234, Page 14 + */ + fn is_digit_char(character: &u8) -> bool { + *character >= 0x30 && *character <= 0x39 + } + + /* + * RFC 5234, Page 14 + */ + fn is_hex_digit_char(character: &u8) -> bool { + Self::is_digit_char(character) || + *character == 0x41 || // A + *character == 0x42 || // B + *character == 0x43 || // C + *character == 0x44 || // D + *character == 0x45 || // E + *character == 0x46 || // F + *character == 0x61 || // a + *character == 0x62 || // b + *character == 0x63 || // c + *character == 0x64 || // d + *character == 0x65 || // e + *character == 0x66 // f + } + + /* + * RFC 5234, Page 14 + */ + fn is_visible_char(character: &u8) -> bool { + *character >= 0x21 && *character <= 0x7E + } + + fn is_obs_text_char(character: &u8) -> bool { + *character >= 0x80 + } +} diff --git a/src/http/rule.rs b/src/http/rule.rs new file mode 100644 index 0000000..1672d99 --- /dev/null +++ b/src/http/rule.rs @@ -0,0 +1,123 @@ +use std::collections::HashMap; + +lazy_static! { + static ref METHODS: HashMap<&'static str, Method> = { + let mut m = HashMap::new(); + m.insert("GET", Method::GET); + m.insert("HEAD", Method::HEAD); + m.insert("POST", Method::POST); + m.insert("PUT", Method::PUT); + m.insert("DELETE", Method::DELETE); + m.insert("CONNECT", Method::CONNECT); + m.insert("OPTIONS", Method::OPTIONS); + m.insert("TRACE", Method::TRACE); + m + }; +} + +/* +* RFC 7230, Page 19 +*/ +#[derive(Debug)] +pub struct HTTPMessage { + pub request_line: RequestLine, + pub header_fields: Vec<HeaderField>, +} + +/* +* RFC 7230, Page 23 +*/ +#[derive(Debug)] +pub struct HeaderField { + pub name: FieldName, + pub value: FieldValue, +} + +/* +* RFC 7230, Page 23 +*/ +#[derive(Debug)] +pub struct FieldName { + pub lexeme: String, +} + +/* +* RFC 7230, Page 23 +*/ +#[derive(Debug)] +pub struct FieldValue { + pub content: Vec<u8>, +} + +/* +* RFC 7230, Page 23 +*/ +#[derive(Debug)] +pub struct FieldContent { + pub first_char: u8, + pub second_char: Option<u8>, +} + +/* +* RFC 7230, Page 21 +*/ +#[derive(Debug)] +pub struct RequestLine { + pub method: Method, + pub request_target: OriginForm, + pub http_version: HTTPVersion, +} + +/* +* RFC 7231, Page 22 +*/ +#[derive(Debug, Clone)] +pub enum Method { + GET, + HEAD, + POST, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, +} + +impl Method { + pub fn from_string(string: &str) -> Option<Method> { + METHODS.get(string).cloned() + } +} + +/* +* RFC 7230, Page 41 +*/ +#[derive(Debug)] +pub struct OriginForm { + pub absolute_path: AbsolutePath, + pub query: Option<Query>, +} + +/* +* RFC 7230, Page 14 +*/ +#[derive(Debug)] +pub struct HTTPVersion { + pub major: u32, + pub minor: u32, +} + +#[derive(Debug)] +pub struct AbsolutePath { + pub segments: Vec<Segment>, +} + +#[derive(Debug)] +pub struct Query { + pub lexeme: String, +} + +#[derive(Debug)] +pub struct Segment { + pub lexeme: String, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..36699af --- /dev/null +++ b/src/main.rs @@ -0,0 +1,159 @@ +#[macro_use] +extern crate lazy_static; + +mod http; +mod peekable_bufreader; +mod response; +mod chunked_bufreader; + +use std::env; +use std::path::PathBuf; +use std::path::Path; + +use futures::stream; +use futures::stream::StreamExt; + +use async_std::prelude::*; +use async_std::task; +use async_std::net::{TcpStream, TcpListener}; +use async_std::io::{BufReader, BufWriter}; +use async_std::fs::File; +use async_std::fs; + +use chrono::offset::Local; +use chrono::DateTime; + +use http::{Parser, Method}; +use peekable_bufreader::PeekableBufReader; +use chunked_bufreader::ChunkedBufReader; +use response::{Response, BadRequest, NotFound, NotImplemented, InternalServerError}; + +#[async_std::main] +async fn main() { + let port = env::args().skip(1).next().unwrap_or("8000".to_owned()).parse::<u16>().unwrap_or(8000); + let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await; + match listener { + Err(e) => { + eprintln!("Failed to bind TCP Listener: {}", e); + return; + }, + _ => {}, + } + let listener = listener.unwrap(); + listener + .incoming() + .for_each_concurrent(None, |stream| async move { + if let Ok(valid) = stream { + task::spawn(handle_connection(valid)); + } + }) + .await; +} + +async fn handle_connection(mut stream: TcpStream) { + let response = generate_response(&stream).await.response_bytes(); + let writer = BufWriter::new(&mut stream); + let mut writer = response.fold(writer, |mut writer, bytes| async move { + writer.write(&bytes).await.unwrap(); + writer + }).await; + writer.flush().await.unwrap(); + stream.flush().await.unwrap(); +} + +async fn generate_response(stream: &TcpStream) -> Box<dyn Response> { + let reader = PeekableBufReader::new(BufReader::new(stream)); + let request = match Parser::new(reader).parse().await { + Some(success) => success, + None => return Box::new(BadRequest{}), + }; + match request.method { + Method::GET => { + if request.requested_path.iter().filter(|segment| segment.contains("/")).count() != 0 { + return Box::new(BadRequest{}); + } else { + let path = match PathBuf::from("./".to_owned() + &request.requested_path.join("/")).canonicalize() { + Ok(canonical_path) => canonical_path, + Err(_) => return Box::new(NotFound{}), + }; + let current_dir = match env::current_dir() { + Ok(dir) => match dir.canonicalize() { + Ok(canonical_path) => canonical_path, + Err(_) => return Box::new(InternalServerError{}), + }, + Err(_) => return Box::new(InternalServerError{}), + }; + if !is_path_ancestor_of(¤t_dir, &path) { + return Box::new(NotFound{}); + } + let metadata = match fs::metadata(&path).await { + Ok(data) => data, + Err(_) => return Box::new(NotFound{}), + }; + if metadata.is_dir() { + let friendly_name = match path.strip_prefix(¤t_dir) { + Ok(result) => { + match result.to_str() { + Some(result) => result, + None => return Box::new(InternalServerError{}), + } + }, + Err(_) => return Box::new(InternalServerError{}), + }; + let mut listings = Vec::new(); + if path != current_dir { + listings.push(format!(include_str!("../res/listing_entry.html"), path.parent().unwrap().strip_prefix(¤t_dir).unwrap().to_str().unwrap(), "..", "-", "-")); + } + for file in path.read_dir().unwrap() { + let file_path = file.unwrap().path(); + let file_metadata = fs::metadata(&file_path).await.unwrap(); + let created_time = match file_metadata.created() { + Ok(birth_time) => { + let formatted_time: DateTime<Local> = birth_time.into(); + formatted_time.format("%d-%b-%Y %H:%M").to_string() + }, + Err(_) => "-".to_owned(), + }; + let file_size = if file_metadata.is_dir() { + "-".to_owned() + } else { + file_metadata.len().to_string() + }; + listings.push(format!(include_str!("../res/listing_entry.html"), + file_path.strip_prefix(¤t_dir).unwrap().to_str().unwrap(), + file_path.file_name().unwrap().to_str().unwrap(), + created_time, + file_size, + )) + } + return Box::new(response::Ok { + file_stream: Box::new(stream::iter(vec![Vec::from( + format!( + include_str!("../res/listing.html"), + friendly_name, + friendly_name, + listings.join("\n"), + ).as_bytes() + )].into_iter().map(|entry| entry.to_owned()))) + }); + } else { + return Box::new(response::Ok{ file_stream: Box::new(ChunkedBufReader::new(BufReader::new(File::open(&path).await.unwrap()))) }) + } + } + }, + _ => return Box::new(NotImplemented{}), + } +} + +fn is_path_ancestor_of(ancestor: &Path, child: &Path) -> bool { + let mut ancestors = child.ancestors(); + loop { + if let Some(parent) = ancestors.next() { + if parent == ancestor { + return true; + } + } else { + return false; + } + } +} diff --git a/src/peekable_bufreader.rs b/src/peekable_bufreader.rs new file mode 100644 index 0000000..ae02155 --- /dev/null +++ b/src/peekable_bufreader.rs @@ -0,0 +1,47 @@ +use async_std::prelude::*; +use async_std::io::BufReader; +use async_std::io::Read; + +pub struct PeekableBufReader<T> + where T: Read + Unpin { + reader: BufReader<T>, + buffer: [u8; 1], + peeked_last: bool, +} + +impl<T> PeekableBufReader<T> + where T: Read + Unpin { + pub fn new(reader: BufReader<T>) -> Self { + Self { + reader, + buffer: [0], + peeked_last: false, + } + } + + pub async fn next(&mut self) -> Option<u8> { + if self.peeked_last { + self.peeked_last = false; + Some(self.buffer[0]) + } else { + match self.reader.read(&mut self.buffer).await.ok()? { + 1 => Some(self.buffer[0]), + _ => None, + } + } + } + + pub async fn peek(&mut self) -> Option<&u8> { + if self.peeked_last { + Some(&self.buffer[0]) + } else { + match self.reader.read(&mut self.buffer).await.ok()? { + 1 => { + self.peeked_last = true; + Some(&self.buffer[0]) + }, + _ => None, + } + } + } +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..8817148 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,55 @@ +use futures::prelude::*; + +pub trait Response: Send + Sync { + fn response_bytes(self: Box<Self>) -> Box<dyn Stream<Item = Vec<u8>> + Unpin + Send>; +} + +pub struct Ok { + pub file_stream: Box<dyn Stream<Item = Vec<u8>> + Unpin + Send + Sync>, +} + +impl Response for Ok { + fn response_bytes(self: Box<Self>) -> Box<dyn Stream<Item = Vec<u8>> + Unpin + Send> { + Box::new(stream::iter(vec![Vec::from(b"HTTP/1.1 200 OK\r\n\r\n" as &[u8])].into_iter().map(|entry| entry.to_owned())).chain(self.file_stream)) + } +} + +pub struct BadRequest { + +} + +impl Response for BadRequest { + fn response_bytes(self: Box<Self>) -> Box<dyn Stream<Item = Vec<u8>> + Unpin + Send> { + Box::new(stream::iter(vec![Vec::from(b"HTTP/1.1 400 Bad Request\r\n\r\n" as &[u8]), include_bytes!("../res/400.html").to_vec()].into_iter().map(|entry| entry.to_owned()))) + } +} + +pub struct NotFound { + +} + +impl Response for NotFound { + fn response_bytes(self: Box<Self>) -> Box<dyn Stream<Item = Vec<u8>> + Unpin + Send> { + Box::new(stream::iter(vec![Vec::from(b"HTTP/1.1 404 Not Found\r\n\r\n" as &[u8]), include_bytes!("../res/404.html").to_vec()].into_iter().map(|entry| entry.to_owned()))) + } +} + +pub struct NotImplemented { + +} + +impl Response for NotImplemented { + fn response_bytes(self: Box<Self>) -> Box<dyn Stream<Item = Vec<u8>> + Unpin + Send> { + Box::new(stream::iter(vec![Vec::from(b"HTTP/1.1 501 Not Implemented\r\n\r\n" as &[u8]), include_bytes!("../res/501.html").to_vec()].into_iter().map(|entry| entry.to_owned()))) + } +} + +pub struct InternalServerError { + +} + +impl Response for InternalServerError { + fn response_bytes(self: Box<Self>) -> Box<dyn Stream<Item = Vec<u8>> + Unpin + Send> { + Box::new(stream::iter(vec![Vec::from(b"HTTP/1.1 500 InternalServerError\r\n\r\n" as &[u8]), include_bytes!("../res/500.html").to_vec()].into_iter().map(|entry| entry.to_owned()))) + } +} |