├── .gitignore ├── Cargo.toml ├── README.md ├── boot ├── Cargo.toml └── src │ └── main.rs ├── config ├── dispatch.json ├── mime.json ├── mime.types └── status.json ├── core ├── Cargo.toml └── src │ ├── lib.rs │ └── thread.rs ├── http ├── Cargo.toml └── src │ ├── config.rs │ ├── httprequest.rs │ ├── httpresponse.rs │ ├── lib.rs │ ├── mimetype.rs │ └── status.rs ├── public ├── 404.html ├── index.html └── index.js ├── router ├── Cargo.toml └── src │ ├── handler │ ├── api.rs │ ├── mod.rs │ └── staticres.rs │ ├── lib.rs │ ├── parser.rs │ └── proxy.rs └── utils ├── Cargo.toml └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode 3 | Cargo.lock 4 | .idea 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = ["http", "boot", "router", "utils", "core"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rhttp-server 2 | -------------------------------------------------------------------------------- /boot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boot" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | futures = "0.3.19" 10 | once_cell = "1.9.0" 11 | 12 | http = { path = "../http" } 13 | router = { path = "../router" } 14 | core = { path = "../core" } 15 | 16 | [dependencies.async-std] 17 | version = "1.6" 18 | features = ["attributes"] -------------------------------------------------------------------------------- /boot/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::net::{TcpListener, TcpStream}; 3 | 4 | use http::{self, config, httprequest::HttpRequest}; 5 | use router::Router; 6 | use core as self_core; 7 | use self_core::thread::ThreadPool; 8 | 9 | 10 | fn main() { 11 | config::init(); 12 | println!("finished to init config"); 13 | let bind_addr = "localhost:9090"; 14 | let server_socket = TcpListener::bind(bind_addr).unwrap(); 15 | // default 10 threads in thread pool; 16 | let pool = ThreadPool::new(10); 17 | 18 | for conn_wrapper in server_socket.incoming() { 19 | match conn_wrapper { 20 | Ok(stream) => { 21 | pool.execute(Box::new(|| { 22 | handle_connection(stream); 23 | })) 24 | } 25 | Err(e) => eprintln!( 26 | "failed to process incoming connection from remote. {:?}", 27 | e.kind() 28 | ), 29 | }; 30 | } 31 | println!("rhttp-server started in {}", bind_addr); 32 | } 33 | 34 | fn handle_connection(mut stream: TcpStream) { 35 | let request = HttpRequest::from(&mut stream); 36 | println!("request is: {:?}", request.resource); 37 | let resp = Router::route(&request); 38 | let resp_str: String = resp.into(); 39 | stream.write(resp_str.as_bytes() as &[u8]).unwrap(); 40 | stream.flush().unwrap(); 41 | } -------------------------------------------------------------------------------- /config/dispatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "/index": { 3 | "target": "/index", 4 | "lbs": "random|loop", 5 | "backend_servers": [ 6 | "127.0.0.1:8082", 7 | "127.0.0.1:8083" 8 | ], 9 | "headers": [ 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /config/mime.json: -------------------------------------------------------------------------------- 1 | { 2 | "text/html": "html htm shtml", 3 | "text/css": "css", 4 | "text/xml": "xml", 5 | "image/gif": "gif", 6 | "image/jpeg": "jpeg jpg", 7 | "application/javascript": "js wasm", 8 | "application/atom+xml": "atom", 9 | "application/rss+xml": "rss", 10 | "text/mathml": "mml", 11 | "text/plain": "txt", 12 | "text/vnd.sun.j2me.app-descriptor": "jad", 13 | "text/vnd.wap.wml": "wml", 14 | "text/x-component": "htc", 15 | "image/png": "png", 16 | "image/svg+xml": "svg svgz", 17 | "image/tiff": "tif tiff", 18 | "image/vnd.wap.wbmp": "wbmp", 19 | "image/webp": "webp", 20 | "image/x-icon": "ico", 21 | "image/x-jng": "jng", 22 | "image/x-ms-bmp": "bmp", 23 | "font/woff": "woff", 24 | "font/woff2": "woff2", 25 | "application/java-archive": "jar war ear", 26 | "application/json": "json", 27 | "application/mac-binhex40": "hqx", 28 | "application/msword": "doc", 29 | "application/pdf": "pdf", 30 | "application/postscript": "ps eps ai", 31 | "application/rtf": "rtf", 32 | "application/vnd.apple.mpegurl": "m3u8", 33 | "application/vnd.google-earth.kml+xml": "kml", 34 | "application/vnd.google-earth.kmz": "kmz", 35 | "application/vnd.ms-excel": "xls", 36 | "application/vnd.ms-fontobject": "eot", 37 | "application/vnd.ms-powerpoint": "ppt", 38 | "application/vnd.oasis.opendocument.graphics": "odg", 39 | "application/vnd.oasis.opendocument.presentation": "odp", 40 | "application/vnd.oasis.opendocument.spreadsheet": "ods", 41 | "application/vnd.oasis.opendocument.text": "odt", 42 | "application/vnd.wap.wmlc": "wmlc", 43 | "application/x-7z-compressed": "7z", 44 | "application/x-cocoa": "cco", 45 | "application/x-java-archive-diff": "jardiff", 46 | "application/x-java-jnlp-file": "jnlp", 47 | "application/x-makeself": "run", 48 | "application/x-perl": "pl pm", 49 | "application/x-pilot": "prc pdb", 50 | "application/x-rar-compressed": "rar", 51 | "application/x-redhat-package-manager": "rpm", 52 | "application/x-sea": "sea", 53 | "application/x-shockwave-flash": "swf", 54 | "application/x-stuffit": "sit", 55 | "application/x-tcl": "tcl tk", 56 | "application/x-x509-ca-cert": "der pem crt", 57 | "application/x-xpinstall": "xpi", 58 | "application/xhtml+xml": "xhtml", 59 | "application/xspf+xml": "xspf", 60 | "application/zip": "zip", 61 | "application/octet-stream": "bin exe dll", 62 | "application/octet-stream": "deb", 63 | "application/octet-stream": "dmg", 64 | "application/octet-stream": "iso img", 65 | "application/octet-stream": "msi msp msm", 66 | "audio/midi": "mid midi kar", 67 | "audio/mpeg": "mp3", 68 | "audio/ogg": "ogg", 69 | "audio/x-m4a": "m4a", 70 | "audio/x-realaudio": "ra", 71 | "video/3gpp": "3gpp 3gp", 72 | "video/mp2t": "ts", 73 | "video/mp4": "mp4", 74 | "video/mpeg": "mpeg mpg", 75 | "video/quicktime": "mov", 76 | "video/webm": "webm", 77 | "video/x-flv": "flv", 78 | "video/x-m4v": "m4v", 79 | "video/x-mng": "mng", 80 | "video/x-ms-asf": "asx asf", 81 | "video/x-ms-wmv": "wmv", 82 | "video/x-msvideo": "avi" 83 | } -------------------------------------------------------------------------------- /config/mime.types: -------------------------------------------------------------------------------- 1 | { 2 | "text/html" "html htm shtml", 3 | "text/css" "css", 4 | "text/xml" "xml", 5 | "image/gif" "gif", 6 | "image/jpeg" "jpeg jpg", 7 | "application/javascript" "js wasm", 8 | "application/atom+xml" "atom", 9 | "application/rss+xml" "rss", 10 | "text/mathml" "mml", 11 | "text/plain" "txt", 12 | "text/vnd.sun.j2me.app-descriptor" "jad", 13 | "text/vnd.wap.wml" "wml", 14 | "text/x-component" "htc", 15 | "image/png" "png", 16 | "image/svg+xml" "svg svgz", 17 | "image/tiff" "tif tiff", 18 | "image/vnd.wap.wbmp" "wbmp", 19 | "image/webp" "webp", 20 | "image/x-icon" "ico", 21 | "image/x-jng" "jng", 22 | "image/x-ms-bmp" "bmp", 23 | "font/woff" "woff", 24 | "font/woff2" "woff2", 25 | "application/java-archive" "jar war ear", 26 | "application/json" "json", 27 | "application/mac-binhex40" "hqx", 28 | "application/msword" "doc", 29 | "application/pdf" "pdf", 30 | "application/postscript" "ps eps ai", 31 | "application/rtf" "rtf", 32 | "application/vnd.apple.mpegurl" "m3u8", 33 | "application/vnd.google-earth.kml+xml" "kml", 34 | "application/vnd.google-earth.kmz" "kmz", 35 | "application/vnd.ms-excel" "xls", 36 | "application/vnd.ms-fontobject" "eot", 37 | "application/vnd.ms-powerpoint" "ppt", 38 | "application/vnd.oasis.opendocument.graphics" "odg", 39 | "application/vnd.oasis.opendocument.presentation" "odp", 40 | "application/vnd.oasis.opendocument.spreadsheet" "ods", 41 | "application/vnd.oasis.opendocument.text" "odt", 42 | "application/vnd.wap.wmlc" "wmlc", 43 | "application/x-7z-compressed" "7z", 44 | "application/x-cocoa" "cco", 45 | "application/x-java-archive-diff" "jardiff", 46 | "application/x-java-jnlp-file" "jnlp", 47 | "application/x-makeself" "run", 48 | "application/x-perl" "pl pm", 49 | "application/x-pilot" "prc pdb", 50 | "application/x-rar-compressed" "rar", 51 | "application/x-redhat-package-manager" "rpm", 52 | "application/x-sea" "sea", 53 | "application/x-shockwave-flash" "swf", 54 | "application/x-stuffit" "sit", 55 | "application/x-tcl" "tcl tk", 56 | "application/x-x509-ca-cert" "der pem crt", 57 | "application/x-xpinstall" "xpi", 58 | "application/xhtml+xml" "xhtml", 59 | "application/xspf+xml" "xspf", 60 | "application/zip" "zip", 61 | "application/octet-stream" "bin exe dll", 62 | "application/octet-stream" "deb", 63 | "application/octet-stream" "dmg", 64 | "application/octet-stream" "iso img", 65 | "application/octet-stream" "msi msp msm", 66 | "audio/midi" "mid midi kar", 67 | "audio/mpeg" "mp3", 68 | "audio/ogg" "ogg", 69 | "audio/x-m4a" "m4a", 70 | "audio/x-realaudio" "ra", 71 | "video/3gpp" "3gpp 3gp", 72 | "video/mp2t" "ts", 73 | "video/mp4" "mp4", 74 | "video/mpeg" "mpeg mpg", 75 | "video/quicktime" "mov", 76 | "video/webm" "webm", 77 | "video/x-flv" "flv", 78 | "video/x-m4v" "m4v", 79 | "video/x-mng" "mng", 80 | "video/x-ms-asf" "asx asf", 81 | "video/x-ms-wmv" "wmv", 82 | "video/x-msvideo" "avi" 83 | } -------------------------------------------------------------------------------- /config/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "100": "Continue", 3 | "101": "Switching Protocols", 4 | "200": "OK", 5 | "201": "Created", 6 | "202": "Accepted", 7 | "203": "Non-Authoritative Information", 8 | "204": "No Content", 9 | "205": "Reset Content", 10 | "206": "Partial Content", 11 | "300": "Multiple Choices", 12 | "301": "Moved Permanently", 13 | "302": "Found", 14 | "303": "See Other", 15 | "304": "Not Modified", 16 | "305": "Use Proxy", 17 | "306": "Unused", 18 | "307": "Temporary Redirect", 19 | "400": "Bad Request", 20 | "401": "Unauthorized", 21 | "402": "Payment Required", 22 | "403": "Forbidden", 23 | "404": "Not Found", 24 | "405": "Method Not Allowed", 25 | "406": "Not Acceptable", 26 | "407": "Proxy Authentication Required", 27 | "408": "Request Time-out", 28 | "409": "Conflict", 29 | "410": "Gone", 30 | "411": "Length Required", 31 | "412": "Precondition Failed", 32 | "413": "Request Entity Too Large", 33 | "414": "Request-URI Too Large", 34 | "415": "Unsupported Media Type", 35 | "416": "Requested range not satisfiable", 36 | "417": "Expectation Failed", 37 | "500": "Internal Server Error", 38 | "501": "Not Implemented", 39 | "502": "Bad Gateway", 40 | "503": "Service Unavailable", 41 | "504": "Gateway Time-out", 42 | "505": "HTTP Version not supported" 43 | } -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | [dependencies.async-std] 11 | version = "1.6" 12 | features = ["attributes"] -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod thread; -------------------------------------------------------------------------------- /core/src/thread.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::{Arc, mpsc, Mutex}, 3 | future::Future, 4 | sync::mpsc::{Receiver, Sender}, 5 | thread, 6 | thread::JoinHandle, 7 | }; 8 | 9 | type Task = Box; 10 | 11 | pub struct ThreadPool { 12 | workers: Vec, 13 | task_sender: Sender, 14 | } 15 | 16 | impl ThreadPool { 17 | pub fn new(size: usize) -> ThreadPool { 18 | if size > 512 { 19 | panic!("#size must between 1 and 2024"); 20 | } 21 | 22 | let (task_sender, task_receiver) = mpsc::channel(); 23 | let task_receiver_arc = Arc::new(Mutex::new(task_receiver)); 24 | 25 | let mut workers = Vec::with_capacity(size); 26 | for i in 0..size { 27 | workers.push(Worker::new(i, Arc::clone(&task_receiver_arc))); 28 | } 29 | ThreadPool { 30 | workers, 31 | task_sender, 32 | } 33 | } 34 | 35 | pub fn execute(&self, task: Task) { 36 | self.task_sender.send(task).unwrap(); 37 | } 38 | } 39 | 40 | struct Worker { 41 | id: usize, 42 | thread_handler: JoinHandle<()>, 43 | } 44 | 45 | impl Worker { 46 | fn new(id: usize, task_receiver: Arc>>) -> Worker { 47 | let thread_handler = thread::spawn(move || loop { 48 | let task = task_receiver.lock().unwrap().recv().unwrap(); 49 | task(); 50 | } 51 | ); 52 | Worker { 53 | id, 54 | thread_handler, 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /http/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | once_cell = "1.9.0" 10 | serde_json = "1.0.73" 11 | utils = { path = "../utils" } 12 | #futures-lite = "1.12.0" 13 | futures = "0.3.19" 14 | 15 | [dependencies.async-std] 16 | version = "1.6" 17 | features = ["attributes"] 18 | -------------------------------------------------------------------------------- /http/src/config.rs: -------------------------------------------------------------------------------- 1 | pub fn init() { 2 | crate::mimetype::init_default_mime_type(); 3 | crate::status::init_default_http_status(); 4 | } 5 | -------------------------------------------------------------------------------- /http/src/httprequest.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | }; 4 | use std::io::{BufRead, BufReader, Read}; 5 | use std::net::TcpStream; 6 | 7 | #[derive(Debug, PartialEq, Clone)] 8 | pub enum Method { 9 | GET, 10 | POST, 11 | PUT, 12 | DELETE, 13 | UNKNOWN, 14 | // 暂时先支持这么多. 15 | } 16 | 17 | impl Into for Method { 18 | fn into(self) -> String { 19 | match self { 20 | Method::GET => "GET", 21 | Method::POST => "POST", 22 | Method::PUT => "PUT", 23 | Method::DELETE => "DELETE", 24 | _ => "GET", 25 | }.into() 26 | } 27 | } 28 | 29 | impl From<&str> for Method { 30 | fn from(method: &str) -> Self { 31 | let lowercase_method = method.to_lowercase(); 32 | let res = lowercase_method.as_str(); 33 | match res { 34 | "get" => Method::GET, 35 | "post" => Method::POST, 36 | "put" => Method::PUT, 37 | "delete" => Method::DELETE, 38 | _ => Method::UNKNOWN, 39 | } 40 | } 41 | } 42 | 43 | #[derive(Debug, PartialEq, Clone)] 44 | pub enum Version { 45 | HTTP1_1, 46 | HTTP2_0, 47 | UNKNOWN, 48 | } 49 | 50 | impl Into for Version { 51 | fn into(self) -> String { 52 | match self { 53 | Version::HTTP1_1 => "HTTP/1.1", 54 | Version::HTTP2_0 => "HTTP/2.0", 55 | _ => "HTTP/1.1", 56 | }.into() 57 | } 58 | } 59 | 60 | impl From<&str> for Version { 61 | fn from(ver: &str) -> Self { 62 | let ver_in_lowercase = ver.to_lowercase(); 63 | match ver_in_lowercase.as_str() { 64 | "http/1.1" => Version::HTTP1_1, 65 | "http/2.0" => Version::HTTP2_0, 66 | _ => Version::UNKNOWN, 67 | } 68 | } 69 | } 70 | 71 | #[derive(Debug, PartialEq, Clone)] 72 | pub enum Resource { 73 | Path(String), 74 | } 75 | 76 | #[derive(Debug, PartialEq, Clone)] 77 | pub struct HttpRequest { 78 | pub method: Method, 79 | pub version: Version, 80 | pub resource: Resource, 81 | pub headers: Option>, 82 | pub msg_body: Option, 83 | } 84 | 85 | impl Default for HttpRequest { 86 | fn default() -> Self { 87 | Self { 88 | method: Method::GET, 89 | version: Version::HTTP1_1, 90 | resource: Resource::Path(String::from("/")), 91 | headers: None, 92 | msg_body: None, 93 | } 94 | } 95 | } 96 | 97 | 98 | impl Into for HttpRequest { 99 | fn into(self) -> String { 100 | let serialized_headers = match &self.headers { 101 | None => "".into(), 102 | Some(headers) => { 103 | let serialized_headers = headers.iter().map(|(k, v)| { 104 | let mut item = String::from(""); 105 | item.push_str(k); 106 | item.push_str(": "); 107 | item.push_str(v); 108 | item 109 | }).collect::>(); 110 | serialized_headers.join("\r\n") 111 | } 112 | }; 113 | let serialzied_body = &self.msg_body.unwrap_or(String::from("")); 114 | let Resource::Path(path) = &self.resource; 115 | let method: String = self.method.into(); 116 | let version: String = self.version.into(); 117 | format!("{} {} {}\r\n{}\r\n\r\n{}", method, path, version, serialized_headers, serialzied_body) 118 | } 119 | } 120 | 121 | impl HttpRequest { 122 | pub fn from(stream: &mut TcpStream) -> Self { 123 | let mut reader = BufReader::new(stream); 124 | let mut request = HttpRequest::default(); 125 | let mut headers = HashMap::::new(); 126 | let mut content_len = 0; 127 | let mut is_req_line = true; 128 | loop { 129 | let mut line = String::from(""); 130 | reader.read_line(&mut line).unwrap(); 131 | if is_req_line { 132 | if line.is_empty() && is_req_line { 133 | // if the request line is empty, skip and return default HttpRequest; 134 | return HttpRequest::default(); 135 | } 136 | // parse and set http request info. 137 | let (method, resource, version) = process_req_line(line.as_str()); 138 | request.method = method; 139 | request.resource = resource; 140 | request.version = version; 141 | is_req_line = false; 142 | } else if line.contains(":") { // process headers; 143 | let (key, value) = process_request_header(line.as_str()); 144 | headers.insert(key.clone(), value.clone().trim().to_string()); 145 | if key == "Content-Length" { 146 | content_len = value.trim().parse::().unwrap(); 147 | } 148 | } else if line == String::from("\r\n") { 149 | // the split line between headers and body; 150 | break; 151 | } 152 | } 153 | request.headers = Some(headers); 154 | if content_len > 0 { 155 | let mut buf = vec![0 as u8; content_len]; 156 | let buf_slice = buf.as_mut_slice(); 157 | // 读取请求体,注意,这里不能在使用stream进行读取,否则会一直卡在这里,要继续用reader进行读取. 158 | // BufReader::read(&mut reader, buf_slice).await.unwrap(); 159 | reader.read(buf_slice).unwrap(); 160 | request.msg_body = Some(String::from_utf8_lossy(buf_slice).to_string()); 161 | } 162 | request 163 | } 164 | } 165 | 166 | impl From for HttpRequest { 167 | fn from(diagram: String) -> Self { 168 | let mut request = HttpRequest::default(); 169 | let mut headers = HashMap::::new(); 170 | for line in diagram.lines() { 171 | if line.contains("HTTP") { 172 | // process request line. 173 | println!("request line is: {}", line); 174 | let (method, resource, version) = process_req_line(line); 175 | request.method = method; 176 | request.resource = resource; 177 | request.version = version; 178 | } else if line.contains(":") { 179 | // process request header; 180 | let (key, value) = process_request_header(line); 181 | headers.insert(key, value); 182 | } else if line.is_empty() { 183 | // skip the line before msg body. 184 | } else { 185 | // process msg body; 186 | request.msg_body = Some(line.to_string()); 187 | } 188 | } 189 | request.headers = Some(headers); 190 | request 191 | } 192 | } 193 | 194 | fn process_request_header(line: &str) -> (String, String) { 195 | let mut seg_iter = line.split(":"); 196 | ( 197 | seg_iter.next().unwrap().into(), 198 | seg_iter.next().unwrap().into(), 199 | ) 200 | } 201 | 202 | fn process_req_line(line: &str) -> (Method, Resource, Version) { 203 | let mut segments = line.split_whitespace(); 204 | ( 205 | segments.next().unwrap().into(), 206 | Resource::Path(segments.next().unwrap().to_string()), 207 | segments.next().unwrap().into(), 208 | ) 209 | } 210 | 211 | #[cfg(test)] 212 | mod method_testsuite { 213 | use super::*; 214 | 215 | #[test] 216 | fn test_method_match() { 217 | let m: Method = "GET".into(); 218 | assert_eq!(Method::GET, m); 219 | let m: Method = "posT".into(); 220 | assert_eq!(Method::POST, m); 221 | } 222 | } 223 | 224 | #[cfg(test)] 225 | mod version_testsuite { 226 | use super::*; 227 | 228 | #[test] 229 | fn test_version_match() { 230 | let v: Version = "HTTP/1.1".into(); 231 | assert_eq!(v, Version::HTTP1_1); 232 | 233 | let v: Version = "Http/2.0".into(); 234 | assert_eq!(v, Version::HTTP2_0); 235 | 236 | let v: Version = "HTTP/1.3".into(); 237 | assert_eq!(v, Version::UNKNOWN); 238 | } 239 | } 240 | 241 | #[cfg(test)] 242 | mod http_request_testsuite { 243 | use super::*; 244 | 245 | #[test] 246 | fn test_request_parse() { 247 | let req = 248 | "GET /index.js HTTP/1.1\r\nHost: localhost\r\nContent-Type: text/html\r\n\r\nxxxx"; 249 | let actual_request: HttpRequest = String::from(req).into(); 250 | let expected_request = HttpRequest { 251 | method: Method::GET, 252 | resource: Resource::Path("/index.js".to_string()), 253 | version: Version::HTTP1_1, 254 | headers: { 255 | let mut h = HashMap::::new(); 256 | h.insert("Host".to_string(), " localhost".to_string()); 257 | h.insert("Content-Type".to_string(), " text/html".to_string()); 258 | Some(h) 259 | }, 260 | msg_body: Some(String::from("xxxx")), 261 | }; 262 | assert_eq!(expected_request, actual_request); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /http/src/httpresponse.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use super::mimetype::GLOBAL_MIME_CFG; 4 | use super::status::Status; 5 | 6 | #[derive(Debug, PartialEq, Clone)] 7 | pub struct HttpResponse { 8 | pub version: String, 9 | pub status_code: String, 10 | pub status_text: String, 11 | pub headers: Option>, 12 | pub resp_body: Option, 13 | } 14 | 15 | impl Default for HttpResponse { 16 | fn default() -> Self { 17 | Self { 18 | version: String::from("HTTP/1.1"), 19 | status_code: String::from("200"), 20 | status_text: "OK".to_string(), 21 | headers: Some(HashMap::new()), 22 | resp_body: None, 23 | } 24 | } 25 | } 26 | 27 | impl HttpResponse { 28 | pub fn set_status(&mut self, status: Status) { 29 | let Status(status_code, status_text) = status; 30 | self.status_code = status_code; 31 | self.status_text = status_text; 32 | } 33 | 34 | pub fn set_headers(&mut self, headers: HashMap) { 35 | self.headers = Some(headers); 36 | } 37 | 38 | pub fn set_body(&mut self, body: String) { 39 | self.resp_body = Some(body); 40 | } 41 | 42 | pub fn add_header(&mut self, key: String, value: String) { 43 | let headers = self.headers.as_mut().unwrap(); 44 | headers.insert(key, value); 45 | } 46 | 47 | pub fn new( 48 | version: String, 49 | status_code: String, 50 | headers: Option>, 51 | resp_body: Option, 52 | ) -> Self { 53 | let mut response = HttpResponse::default(); 54 | response.version = version.to_string(); 55 | if status_code != "200" { 56 | response.status_code = status_code.to_string(); 57 | response.status_text = { 58 | let code = status_code; 59 | let mime_config = &GLOBAL_MIME_CFG.get(); 60 | let mut status_text = String::from(""); 61 | if let Some(config) = mime_config { 62 | let desc = match config.get(&*code.to_string()) { 63 | Some(status_desc) => status_desc, 64 | None => "Unknown Status", 65 | }; 66 | status_text.push_str(desc); 67 | }; 68 | status_text 69 | }; 70 | } 71 | if let Some(_) = headers { 72 | response.headers = headers; 73 | } 74 | 75 | if let Some(_) = resp_body { 76 | response.resp_body = resp_body; 77 | } 78 | response 79 | } 80 | 81 | fn get_serialized_headers(&self) -> String { 82 | let mut result = String::from(""); 83 | match &self.headers { 84 | Some(headers) => { 85 | let mut keys = headers.keys().collect::>(); 86 | keys.sort(); 87 | keys.iter() 88 | .for_each(|&k| result = format!("{}{}: {}\r\n", result, k, headers[k])); 89 | let content_len = if let Some(body) = &self.resp_body { 90 | body.len() 91 | } else { 92 | 0 93 | }; 94 | result = format!("{}{}: {}\r\n", result, "Content-Length", content_len); 95 | } 96 | None => {} 97 | } 98 | result 99 | } 100 | 101 | fn get_serialized_body(&self) -> String { 102 | match &self.resp_body { 103 | Some(body) => body.to_string(), 104 | None => String::from(""), 105 | } 106 | } 107 | } 108 | 109 | impl<'a> Into for HttpResponse { 110 | fn into(self) -> String { 111 | format!( 112 | "{} {} {}\r\n{}\r\n{}", 113 | &self.version, 114 | &self.status_code, 115 | &self.status_text, 116 | &self.get_serialized_headers(), 117 | &self.get_serialized_body(), 118 | ) 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod http_response_testsuite { 124 | use super::*; 125 | 126 | #[test] 127 | fn test_response_tostring() { 128 | let expected_resp_string = "HTTP/1.1 200 OK\r\n\ 129 | Content-Type: text/html\r\n\ 130 | Cookie: name=linhuadong\r\n\ 131 | Host: localhost:8080\r\n\ 132 | Content-Length: 4\r\n\ 133 | \r\n\ 134 | yyyy"; 135 | let mut headers = HashMap::::new(); 136 | headers.insert("Content-Type".into(), "text/html".into()); 137 | headers.insert("Cookie".into(), "name=linhuadong".into()); 138 | headers.insert("Host".into(), "localhost:8080".into()); 139 | 140 | let resp_body = String::from("yyyy"); 141 | 142 | let response = HttpResponse::new( 143 | "HTTP/1.1".to_string(), 144 | "200".to_string(), 145 | Some(headers), 146 | Some(resp_body), 147 | ); 148 | let actual_resp_string: String = response.into(); 149 | assert_eq!(expected_resp_string, actual_resp_string); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /http/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod httprequest; 2 | pub mod httpresponse; 3 | pub mod config; 4 | pub mod mimetype; 5 | pub mod status; -------------------------------------------------------------------------------- /http/src/mimetype.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use once_cell::sync::OnceCell; 4 | 5 | pub static GLOBAL_MIME_CFG: OnceCell> = OnceCell::new(); 6 | 7 | pub struct MimeType(pub String, pub String); 8 | 9 | pub static OCTECT_STREAM: &'static str = "application/octet-stream"; 10 | 11 | pub(crate) fn init_default_mime_type() { 12 | let mut entries = HashMap::::new(); 13 | let content_wrapper = utils::read_file_to_string_rel_to_runtime_dir("config/mime.json"); 14 | if let Ok(mime_cfg_json) = content_wrapper { 15 | let json: HashMap = serde_json::from_str(mime_cfg_json.as_str()).unwrap(); 16 | for (k, v) in json.iter() { 17 | v.split_whitespace() 18 | .filter(|&tp| !tp.is_empty()) 19 | .for_each(|tp| { 20 | entries.insert(tp.to_string(), k.to_string()); 21 | }) 22 | } 23 | } 24 | println!("begin loading mime config"); 25 | GLOBAL_MIME_CFG.set(entries).unwrap(); 26 | } -------------------------------------------------------------------------------- /http/src/status.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::OnceCell; 2 | use std::collections::HashMap; 3 | use serde_json::Result; 4 | 5 | pub(crate) fn init_default_http_status() { 6 | let mut status_config = HashMap::::new(); 7 | if let Ok(status_cfg_json) = utils::read_file_to_string_rel_to_runtime_dir("config/status.json") { 8 | let kvs: Result> = 9 | serde_json::from_str(status_cfg_json.as_str()); 10 | match kvs { 11 | Ok(kvs) => { 12 | kvs.iter().for_each(|(k, v)| { 13 | status_config.insert(k.clone(), Status(k.clone(), v.clone())); 14 | }); 15 | } 16 | Err(_) => { 17 | eprintln!("failed to load status.json"); 18 | } 19 | } 20 | } 21 | GLOBAL_STATUSES.set(status_config).unwrap(); 22 | } 23 | 24 | #[derive(Clone, Debug, PartialEq)] 25 | pub struct Status(pub String, pub String); 26 | 27 | impl Status {} 28 | 29 | pub static GLOBAL_STATUSES: OnceCell> = OnceCell::new(); -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 |

Oops! The Page Is Missing!!

3 | 4 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 |

this is page title.

3 |

this is a p tag

4 |
    5 |
  • first
  • 6 |
  • second
  • 7 |
  • thrid
  • 8 |
9 | 10 | -------------------------------------------------------------------------------- /public/index.js: -------------------------------------------------------------------------------- 1 | console.log('this is a js file'); -------------------------------------------------------------------------------- /router/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "router" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | http = { path = "../http" } 10 | url = "2.2.2" 11 | 12 | [dependencies.async-std] 13 | version = "1.6" 14 | features = ["attributes"] -------------------------------------------------------------------------------- /router/src/handler/api.rs: -------------------------------------------------------------------------------- 1 | use http::{httprequest::HttpRequest, httpresponse::HttpResponse}; 2 | 3 | use super::HttpReqHandler; 4 | 5 | #[derive(Default)] 6 | pub struct ApiHandler {} 7 | 8 | impl HttpReqHandler for ApiHandler { 9 | fn handle(&self, _request: &HttpRequest) -> HttpResponse { 10 | todo!("to implement") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /router/src/handler/mod.rs: -------------------------------------------------------------------------------- 1 | use http::{httprequest::HttpRequest, httpresponse::HttpResponse}; 2 | 3 | pub mod api; 4 | pub mod staticres; 5 | 6 | pub trait HttpReqHandler { 7 | fn handle(&self, request: &HttpRequest) -> HttpResponse; 8 | } 9 | -------------------------------------------------------------------------------- /router/src/handler/staticres.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs}; 2 | 3 | use http::{ 4 | httprequest::{HttpRequest, Resource}, 5 | httpresponse::HttpResponse, 6 | }; 7 | 8 | use crate::STATIC_RES; 9 | 10 | use super::HttpReqHandler; 11 | use http::mimetype; 12 | 13 | #[derive(Default)] 14 | pub struct StaticResHandler {} 15 | 16 | impl HttpReqHandler for StaticResHandler { 17 | fn handle(&self, request: &HttpRequest) -> HttpResponse { 18 | let mut resp = HttpResponse::default(); 19 | let Resource::Path(ref path) = request.resource; 20 | let real_path = &path[STATIC_RES.len()..]; 21 | let mut runtime_dir = env::current_dir().unwrap(); 22 | runtime_dir.push("public"); 23 | real_path 24 | .split("/") 25 | .into_iter() 26 | .for_each(|seg| runtime_dir.push(seg)); 27 | let res_content = fs::read_to_string(runtime_dir.to_str().unwrap()); 28 | if let Ok(content) = res_content { 29 | resp.resp_body = Some(content); 30 | } else { 31 | let mut path_buf = env::current_dir().unwrap(); 32 | path_buf.push("public/404.html"); 33 | let not_found_page_path = path_buf.to_str().unwrap(); 34 | resp.resp_body = Some(fs::read_to_string(not_found_page_path).unwrap()); 35 | resp.add_header("Content-Type".into(), "text/html".into()); 36 | let statuses = http::status::GLOBAL_STATUSES.get().unwrap(); 37 | let status = statuses.get("404").unwrap(); 38 | resp.set_status(status.clone()); 39 | return resp; 40 | }; 41 | 42 | let content_type = if let Some(res_name) = path.split("/").last() { 43 | match res_name.split(".").last() { 44 | Some(ext) => mimetype::GLOBAL_MIME_CFG.get().map(|entries| { 45 | if let Some(tp) = entries.get(ext) { 46 | tp.clone() 47 | } else { 48 | mimetype::OCTECT_STREAM.into() 49 | } 50 | }), 51 | None => Some(mimetype::OCTECT_STREAM.into()), 52 | } 53 | .unwrap() 54 | } else { 55 | mimetype::OCTECT_STREAM.into() 56 | }; 57 | resp.add_header("Content-Type".into(), content_type); 58 | resp 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /router/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::net::TcpStream; 3 | use handler::api::ApiHandler; 4 | use http::{ 5 | httprequest::{HttpRequest, Method, Resource}, 6 | httpresponse::HttpResponse, 7 | }; 8 | 9 | use crate::handler::{HttpReqHandler, staticres::StaticResHandler}; 10 | 11 | mod handler; 12 | mod parser; 13 | mod proxy; 14 | 15 | const STATIC_RES: &str = "/staticres"; 16 | 17 | pub struct Router {} 18 | 19 | impl Router { 20 | pub fn route(request: &HttpRequest) -> HttpResponse { 21 | let mut resp = HttpResponse::default(); 22 | let Resource::Path(path) = &request.resource; 23 | if path.starts_with(STATIC_RES) { 24 | let handler_wrapper = get_request_handler(request); 25 | if let Some(handler) = handler_wrapper { 26 | resp = handler.handle(request); 27 | return resp; 28 | } 29 | } 30 | 31 | // url API 测试begin 32 | let proxy_path = parser::parse(&"http://127.0.0.1:8080/user/100?name=linhuadong".into()); 33 | let url_parsed = url::Url::parse(&proxy_path).unwrap(); 34 | let schema = url_parsed.scheme(); 35 | let domain = url_parsed.host_str().unwrap_or("localhost"); 36 | let path = url_parsed.path(); 37 | let query = url_parsed.query().unwrap_or(""); 38 | let port = url_parsed.port().unwrap(); 39 | println!("schema: {}, domain: {}, port: {}, path: {}, query: {}", schema, domain, port, path, query); 40 | // url API 测试end 41 | 42 | let mut proxy_req = request.clone(); 43 | let proxy_addr = format!("{}:{}", domain, port); 44 | proxy_req.resource = Resource::Path(String::from(path)); 45 | let mut client = TcpStream::connect(proxy_addr).unwrap(); 46 | let req_str: String = proxy_req.into(); 47 | client.write_all(req_str.as_bytes()).unwrap(); 48 | client.flush().unwrap(); 49 | // 处理接口返回结果. 50 | match request.method { 51 | Method::GET => { 52 | resp.set_body("this is api response.".into()); 53 | } 54 | _ => { 55 | resp.set_body("hello world".into()); 56 | } 57 | } 58 | resp 59 | } 60 | } 61 | 62 | const STATIC_RES_HANDLER: StaticResHandler = StaticResHandler {}; 63 | const API_RES_HANDLER: ApiHandler = ApiHandler {}; 64 | 65 | fn get_request_handler(req: &HttpRequest) -> Option> { 66 | match req.method { 67 | Method::GET => Some(Box::new(&STATIC_RES_HANDLER)), 68 | _ => Some(Box::new(&API_RES_HANDLER)), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /router/src/parser.rs: -------------------------------------------------------------------------------- 1 | pub fn parse(proxy_path: &String) -> String { 2 | // mock; 3 | String::from(proxy_path) 4 | } -------------------------------------------------------------------------------- /router/src/proxy.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScathonLin/rhttp-server/e2f1e1e3a947cd3a0150e698b92697c37be60831/router/src/proxy.rs -------------------------------------------------------------------------------- /utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "utils" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs}; 2 | use std::io::Result; 3 | 4 | pub fn read_file_to_string_rel_to_runtime_dir(file_path: &str) -> Result { 5 | let mut runtime_dir = env::current_dir().unwrap(); 6 | runtime_dir.push(file_path); 7 | return fs::read_to_string(runtime_dir.to_str().unwrap()); 8 | } --------------------------------------------------------------------------------