commit 29b0ecfc17a77dc3fd691dc15239cc148435a0d1 Author: MeexReay Date: Mon Jun 3 17:57:01 2024 +0300 init diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4fe08d2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,479 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ezhttp" +version = "0.1.0" +dependencies = [ + "serde_json", + "tokio", + "urlencoding", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "proc-macro2" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d2ff50d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ezhttp" +version = "0.1.0" +edition = "2021" + +description = "lightweight http server" +license-file = "LICENSE" +readme = "README.md" +keywords = ["http", "server", "site"] + +[dependencies] +urlencoding = "2.1.3" +serde_json = "1.0" +tokio = { version = "1", features = ["full"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07b7a81 --- /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 + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d749187 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# EzHttp +Easy http server on rust + +Example: +```rust + +use ezhttp::{Headers, HttpResponse, HttpRequest, HttpServer}; +use tokio::{runtime::Runtime, net::TcpListener}; + +struct EzSite { + index_page: String +} + +impl HttpServer for EzSite { + async fn on_request(&mut self, req: &HttpRequest) -> Option { + println!("{} > {} {}", req.addr, req.method, req.page); + + if req.page == "/" { + Some( + HttpResponse::from_str( + Headers::from(vec![ + ("Content-Type", "text/html") + ]), + "200 OK".to_string(), + &self.index_page + ) + ) + } else { + None // just shutdown socket + } + } + + async fn on_start(&mut self, host: &str, listener: &TcpListener) { + println!("Http server started on {}", host); + } + + async fn on_close(&mut self) { + println!("Http server closed"); + } +} + +impl EzSite { + fn new(index_page: &str) -> Self { + EzSite { + index_page: index_page.to_string() + } + } +} + +fn main() { + let site = EzSite::new("Hello World!"); + let host = "localhost:8080"; + + Runtime::new().unwrap().block_on(async move { + ezhttp::start_server(site, host).await.unwrap(); + }); +} + +``` \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6c5bc92 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,697 @@ +use serde_json::Value; +use std::{ + boxed::Box, error::Error, net::{IpAddr, SocketAddr, ToSocketAddrs}, ptr::read, sync::Arc +}; +use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; +use tokio::{ + io::AsyncBufReadExt, + net::{TcpListener, TcpStream}, + sync::Mutex, +}; +use urlencoding::{decode, encode}; + +#[derive(Clone, Debug)] +pub struct Headers { + entries: Vec<(String, String)>, +} + +impl Headers { + pub fn from_entries(entries: Vec<(String, String)>) -> Self { + Headers { entries: entries } + } + + pub fn from(entries: Vec<(&str, &str)>) -> Self { + Headers { + entries: entries + .iter() + .map(|v| (v.0.to_string(), v.1.to_string())) + .collect(), + } + } + + pub fn new() -> Self { + Headers { + entries: Vec::new(), + } + } + + pub fn contains_value(self, value: String) -> bool { + for (k, v) in self.entries { + if v == value { + return true; + } + } + return false; + } + + pub fn contains_key(self, key: String) -> bool { + for (k, v) in self.entries { + if k == key.to_lowercase() { + return true; + } + } + return false; + } + + pub fn get(self, key: String) -> Option { + for (k, v) in self.entries { + if k == key.to_lowercase() { + return Some(v); + } + } + return None; + } + + pub fn put(&mut self, key: String, value: String) { + for t in self.entries.iter_mut() { + if t.0 == key.to_lowercase() { + t.1 = value; + return; + } + } + self.entries.push((key.to_lowercase(), value)); + } + + pub fn remove(&mut self, key: String) { + for (i, t) in self.entries.iter_mut().enumerate() { + if t.0 == key.to_lowercase() { + self.entries.remove(i); + return; + } + } + } + + pub fn keys(self) -> Vec { + let mut keys = Vec::new(); + for (k, v) in self.entries { + keys.push(k.to_lowercase()); + } + keys + } + + pub fn values(self) -> Vec { + let mut values = Vec::new(); + for (k, v) in self.entries { + values.push(v); + } + values + } + + pub fn entries(self) -> Vec<(String, String)> { + return self.entries; + } + + pub fn len(self) -> usize { + return self.entries.len(); + } + + pub fn clear(&mut self) { + self.entries.clear(); + } +} + +impl std::fmt::Display for Headers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} + +#[derive(Debug, Clone)] +pub struct HttpRequest { + pub page: String, + pub method: String, + pub addr: String, + pub headers: Headers, + pub params: Value, + pub data: Vec, +} + +impl std::fmt::Display for HttpRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} + +#[derive(Debug, Clone)] +pub struct HttpResponse { + pub headers: Headers, + pub status_code: String, + pub data: Vec, +} + +impl std::fmt::Display for HttpResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} + +#[derive(Debug)] +pub enum HttpError { + ReadLineEof, + ReadLineUnknown, + InvalidHeaders, + InvalidQuery, + InvalidContentSize, + InvalidContent, + JsonParseError, + WriteHeadError, + WriteBodyError, + InvalidStatus, +} + +impl std::fmt::Display for HttpError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} + +impl Error for HttpError {} + +async fn read_line(data: &mut BufReader<&mut TcpStream>) -> Result { + let mut buf = String::new(); + let mut buf = match data.read_line(&mut buf).await { + Ok(i) => { + if i == 0 { + return Err(HttpError::ReadLineEof); + } + buf + } + Err(_) => return Err(HttpError::ReadLineUnknown), + }; + Ok(buf.to_string()) +} + +async fn read_line_crlf(data: &mut BufReader<&mut TcpStream>) -> Result { + match read_line(data).await { + Ok(i) => Ok(i[..i.len() - 2].to_string()), + Err(e) => Err(e) + } +} + +async fn read_line_lf(data: &mut BufReader<&mut TcpStream>) -> Result { + match read_line(data).await { + Ok(i) => Ok(i[..i.len() - 1].to_string()), + Err(e) => Err(e) + } +} + +fn rem_first(value: &str) -> &str { + let mut chars = value.chars(); + chars.next(); + chars.as_str() +} + +fn split(text: String, delimiter: &str, times: usize) -> Vec { + match times { + 0 => text.split(delimiter).map(|v| v.to_string()).collect(), + 1 => { + let mut v: Vec = Vec::new(); + match text.split_once(delimiter) { + Some(i) => { + v.push(i.0.to_string()); + v.push(i.1.to_string()); + } + None => { + v.push(text); + } + } + v + } + _ => text + .splitn(times, delimiter) + .map(|v| v.to_string()) + .collect(), + } +} + +impl HttpRequest { + pub fn new(page: &str, method: &str, params: Value, headers: Headers, data: Vec) -> Self { + HttpRequest { + page: page.to_string(), + method: method.to_string(), + addr: String::new(), + params: params, + headers: headers, + data: data, + } + } + + pub async fn read(mut data: BufReader<&mut TcpStream>, addr: &SocketAddr) -> Result { + let octets = match addr.ip() { + IpAddr::V4(ip) => ip.octets(), + _ => [127, 0, 0, 1], + }; + + let ip_str = octets[0].to_string().as_str().to_owned() + + "." + + octets[1].to_string().as_str() + + "." + + octets[2].to_string().as_str() + + "." + + octets[3].to_string().as_str(); + + let mut status = split( + match read_line_crlf(&mut data).await { + Ok(i) => i, + Err(e) => return Err(e), + }, + " ", + 3, + ); + + let method = status[0].clone(); + let (page, query) = match status[1].split_once("?") { + Some(i) => (i.0.to_string(), Some(i.1)), + None => (status[1].clone(), None), + }; + + let mut headers = Headers::new(); + + loop { + let text = match read_line_crlf(&mut data).await { + Ok(i) => i, + Err(_) => return Err(HttpError::InvalidHeaders), + }; + + if text.len() == 0 { + break; + } + + let (key, value) = match text.split_once(": ") { + Some(i) => i, + None => return Err(HttpError::InvalidHeaders), + }; + + headers.put(key.to_lowercase(), value.to_string()); + } + + let mut params = serde_json::Map::new(); + + if let Some(i) = query { + for ele in i.split("&") { + let (k, v) = match ele.split_once("=") { + Some(i) => i, + None => return Err(HttpError::InvalidQuery), + }; + + params.insert( + match decode(k) { + Ok(i) => i.to_string(), + Err(_) => return Err(HttpError::InvalidQuery), + }, + match decode(v) { + Ok(i) => Value::String(i.to_string()), + Err(_) => return Err(HttpError::InvalidQuery), + }, + ); + } + } + + let mut reqdata: Vec = Vec::new(); + + if let Some(content_size) = headers.clone().get("content-length".to_string()) { + let content_size: usize = match content_size.parse() { + Ok(i) => i, + Err(_) => return Err(HttpError::InvalidContentSize), + }; + + if content_size > reqdata.len() { + let mut buf: Vec = Vec::new(); + buf.resize(content_size - reqdata.len(), 0); + + match data.read_exact(&mut buf).await { + Ok(i) => i, + Err(_) => return Err(HttpError::InvalidContent), + }; + + reqdata.append(&mut buf); + } + } + + if let Some(content_type) = headers.clone().get("content-type".to_string()) { + let mut body = match String::from_utf8(reqdata.clone()) { + Ok(i) => i, + Err(_) => return Err(HttpError::InvalidContent), + }; + + match content_type.as_str() { + "application/json" => { + let val: Value = match serde_json::from_str(&body) { + Ok(i) => i, + Err(_) => return Err(HttpError::JsonParseError), + }; + + if let Value::Object(mut dict) = val { + params.append(&mut dict); + } + } + "multipart/form-data" => { + let boundary = "--".to_string() + + &content_type[(content_type.find("boundary=").unwrap() + 9)..] + + "\r\n"; + for part in body.split(boundary.as_str()) { + let lines: Vec<&str> = part.split("\r\n").collect(); + if lines.len() >= 3 { + if lines[0].starts_with("Content-Disposition: form-data; name=\"") { + let name: &str = + &lines[0]["Content-Disposition: form-data; name=\"".len()..]; + let name: &str = &name[..name.len() - 1]; + params + .insert(name.to_string(), Value::String(lines[2].to_string())); + } + } + } + } + "application/x-www-form-urlencoded" => { + if body.starts_with("?") { + body = rem_first(body.as_str()).to_string() + } + + for ele in body.split("&") { + let (k, v) = match ele.split_once("=") { + Some(i) => i, + None => return Err(HttpError::InvalidQuery), + }; + + params.insert( + match decode(k) { + Ok(i) => i.to_string(), + Err(_) => return Err(HttpError::InvalidQuery), + }, + match decode(v) { + Ok(i) => Value::String(i.to_string()), + Err(_) => return Err(HttpError::InvalidQuery), + }, + ); + } + } + _ => {} + } + } + + Ok(HttpRequest { + page: page, + method: method, + addr: ip_str.to_string(), + params: Value::Object(params), + headers: headers, + data: reqdata.clone(), + }) + } + + pub async fn read_with_rrs(mut data: BufReader<&mut TcpStream>, addr: &SocketAddr) -> Result { + let addr = match read_line_lf(&mut data).await { + Ok(i) => i, + Err(e) => { return Err(e); } + }.to_socket_addrs().unwrap().collect::>()[0]; + HttpRequest::read(data, &addr).await + } + + pub fn params_to_page(&mut self) { + let mut query = String::new(); + + let mut i: bool = !self.page.contains("?"); + + if let Value::Object(obj) = self.params.clone() { + for (k, v) in obj { + query.push_str(if i { "?" } else { "&" }); + query.push_str(encode(k.as_str()).to_string().as_str()); + query.push_str("="); + query.push_str(encode(v.as_str().unwrap()).to_string().as_str()); + i = false; + } + } + + self.page += query.as_str(); + } + + pub async fn write(self, data: &mut TcpStream) -> Result<(), HttpError> { + let mut head: String = String::new(); + head.push_str(&self.method); + head.push_str(" "); + head.push_str(&self.page); + head.push_str(" HTTP/1.1"); + head.push_str("\r\n"); + + for (k, v) in self.headers.entries { + head.push_str(&k); + head.push_str(": "); + head.push_str(&v); + head.push_str("\r\n"); + } + + head.push_str("\r\n"); + + match data.write_all(head.as_bytes()).await { + Ok(i) => i, + Err(_) => return Err(HttpError::WriteHeadError), + }; + + if !self.data.is_empty() { + match data.write_all(&self.data).await { + Ok(i) => i, + Err(_) => return Err(HttpError::WriteBodyError), + }; + } + + Ok(()) + } +} + +impl HttpResponse { + pub fn new(headers: Headers, status_code: String, data: Vec) -> Self { + HttpResponse { + headers: headers, + data: data, + status_code: status_code, + } + } + + pub fn from_str(headers: Headers, status_code: String, data: &str) -> Self { + HttpResponse { + headers: headers, + data: data.to_string().into_bytes(), + status_code: status_code, + } + } + + pub fn get_text(self) -> String { + match String::from_utf8(self.data) { + Ok(i) => i, + Err(_) => String::new(), + } + } + + pub fn get_json(self) -> Value { + match serde_json::from_str(self.get_text().as_str()) { + Ok(i) => i, + Err(_) => Value::Null, + } + } + + pub async fn read(dread: &mut TcpStream) -> Result { + let mut data = BufReader::new(dread); + + let status = match read_line_crlf(&mut data).await { + Ok(i) => i, + Err(e) => { + return Err(e); + } + }; + + let (_, status_code) = match status.split_once(" ") { + Some(i) => i, + None => return Err(HttpError::InvalidStatus), + }; + + let mut headers = Headers::new(); + + loop { + let text = match read_line_crlf(&mut data).await { + Ok(i) => i, + Err(_) => return Err(HttpError::InvalidHeaders), + }; + + if text.len() == 0 { + break; + } + + let (key, value) = match text.split_once(": ") { + Some(i) => i, + None => return Err(HttpError::InvalidHeaders), + }; + + headers.put(key.to_lowercase(), value.to_string()); + } + + let mut reqdata: Vec = Vec::new(); + + if let Some(content_size) = headers.clone().get("content-length".to_string()) { + let content_size: usize = match content_size.parse() { + Ok(i) => i, + Err(_) => return Err(HttpError::InvalidContentSize), + }; + + if content_size > reqdata.len() { + let mut buf: Vec = Vec::new(); + buf.resize(content_size - reqdata.len(), 0); + + match data.read_exact(&mut buf).await { + Ok(i) => i, + Err(_) => return Err(HttpError::InvalidContent), + }; + + reqdata.append(&mut buf); + } + } else { + loop { + let mut buf: Vec = vec![0; 1024 * 32]; + + let mut buf_len: usize = match data.read(&mut buf).await { + Ok(i) => i, + Err(e) => { + break; + } + }; + + if buf_len == 0 { + break; + } + + buf.truncate(buf_len); + + reqdata.append(&mut buf); + } + } + + Ok(HttpResponse { + headers: headers, + status_code: status_code.to_string(), + data: reqdata, + }) + } + + pub async fn write(self, data: &mut TcpStream) -> Result<(), &str> { + let mut head: String = String::new(); + head.push_str("HTTP/1.1 "); + head.push_str(&self.status_code); + head.push_str("\r\n"); + + for (k, v) in self.headers.entries { + head.push_str(&k); + head.push_str(": "); + head.push_str(&v); + head.push_str("\r\n"); + } + + head.push_str("\r\n"); + + match data.write_all(head.as_bytes()).await { + Ok(i) => i, + Err(_) => return Err("write head error"), + }; + + match data.write_all(&self.data).await { + Ok(i) => i, + Err(_) => return Err("write body error"), + }; + + Ok(()) + } +} + +pub trait HttpServer: Sync { + async fn on_start(&mut self, host: &str, listener: &TcpListener); + async fn on_close(&mut self); + fn on_request(&mut self, req: &HttpRequest) -> impl std::future::Future> + std::marker::Send; +} + +pub async fn handle_connection( + server: Arc>, + mut sock: TcpStream, + addr: std::net::SocketAddr, +) where + S: HttpServer, +{ + let addr = sock.peer_addr().unwrap(); + + let req = match HttpRequest::read(BufReader::new(&mut sock), &addr).await { + Ok(i) => i, + Err(_) => { + return; + } + }; + let resp = match server.lock().await.on_request(&req).await { + Some(i) => i, + None => { + return; + } + }; + resp.write(&mut sock).await.unwrap(); +} + +pub async fn start_server(mut server: impl HttpServer + Send + 'static, host: &str) -> Result<(), Box> { + let server = Arc::new(Mutex::new(server)); + let listener = TcpListener::bind(host).await?; + + server.lock().await.on_start(host, &listener).await; + + loop { + let (sock, addr) = match listener.accept().await { + Ok(i) => i, + Err(_) => { break; } + }; + + let now_server = Arc::clone(&server); + tokio::spawn(handle_connection(now_server, sock, addr)); + } + + server.lock().await.on_close().await; + + Ok(()) +} + + +// http rrs +pub async fn start_server_rrs(mut server: impl HttpServer + Send + 'static, host: &str) -> Result<(), Box> { + let server = Arc::new(Mutex::new(server)); + let listener = TcpListener::bind(host).await?; + + server.lock().await.on_start(host, &listener).await; + + loop { + let (sock, addr) = match listener.accept().await { + Ok(i) => i, + Err(_) => { break; } + }; + + let now_server = Arc::clone(&server); + tokio::spawn(handle_connection_rrs(now_server, sock, addr)); + } + + server.lock().await.on_close().await; + + Ok(()) +} + + +// http rrs +async fn handle_connection_rrs( + server: Arc>, + mut sock: TcpStream, + addr: std::net::SocketAddr, +) { + let req = match HttpRequest::read_with_rrs(BufReader::new(&mut sock), &addr).await { + Ok(i) => i, + Err(_) => { + return; + } + }; + let resp = match server.lock().await.on_request(&req).await { + Some(i) => i, + None => { + return; + } + }; + resp.write(&mut sock).await.unwrap(); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0e21af8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,51 @@ +use ezhttp::{Headers, HttpResponse, HttpRequest, HttpServer}; +use tokio::{runtime::Runtime, net::TcpListener}; + +struct EzSite { + index_page: String +} + +impl HttpServer for EzSite { + async fn on_request(&mut self, req: &HttpRequest) -> Option { + println!("{} > {} {}", req.addr, req.method, req.page); + + if req.page == "/" { + Some( + HttpResponse::from_str( + Headers::from(vec![ + ("Content-Type", "text/html") + ]), + "200 OK".to_string(), + &self.index_page + ) + ) + } else { + None // just shutdown socket + } + } + + async fn on_start(&mut self, host: &str, listener: &TcpListener) { + println!("Http server started on {}", host); + } + + async fn on_close(&mut self) { + println!("Http server closed"); + } +} + +impl EzSite { + fn new(index_page: &str) -> Self { + EzSite { + index_page: index_page.to_string() + } + } +} + +fn main() { + let site = EzSite::new("Hello World!"); + let host = "localhost:8080"; + + Runtime::new().unwrap().block_on(async move { + ezhttp::start_server(site, host).await.unwrap(); + }); +} \ No newline at end of file