aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock722
-rw-r--r--Cargo.toml14
-rw-r--r--LICENSE13
-rw-r--r--README.md12
-rw-r--r--res/400.html14
-rw-r--r--res/404.html14
-rw-r--r--res/500.html14
-rw-r--r--res/501.html14
-rw-r--r--res/listing.html35
-rw-r--r--res/listing_entry.html11
-rw-r--r--src/chunked_bufreader.rs48
-rw-r--r--src/http/flatten.rs37
-rw-r--r--src/http/mod.rs6
-rw-r--r--src/http/parser.rs523
-rw-r--r--src/http/rule.rs123
-rw-r--r--src/main.rs159
-rw-r--r--src/peekable_bufreader.rs47
-rw-r--r--src/response.rs55
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"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..456c488
--- /dev/null
+++ b/LICENSE
@@ -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(&current_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(&current_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(&current_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(&current_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())))
+ }
+}