Skip to main content

ctchi/core/
app.rs

1use std::net::{TcpListener, TcpStream};
2use std::io::{Read, Write, BufReader, BufRead};
3use std::sync::Arc;
4use std::collections::HashMap;
5
6use super::routes::Routes;
7use super::http::{HttpMethod, Request};
8use super::thread_pool::{ThreadPool};
9use crate::core::config::get_configuration;
10
11struct RequestHandler;
12
13fn read_static(file_pth: &str) -> impl Fn(&str) -> String + '_ {
14    move |pref| -> String {
15        use std::fs;
16        let content = fs::read_to_string(format!("{}/{}", pref, file_pth))
17            .unwrap_or_else(|error| { error.to_string() });
18        content
19    }
20}
21
22impl RequestHandler {
23    fn handle_request(&self, stream: TcpStream, routes: Arc<Routes>) {
24        let mut reader = BufReader::new(stream);
25
26        let request = self.parse_request(&mut reader);
27
28        log::info!("Request: {:?} {}", request.method, request.url);
29        if !request.body.is_empty() {
30            log::info!("{}", request.body);
31        }
32
33        let config_reader = get_configuration();
34        let config = config_reader.inner.lock().unwrap();
35        let tmp_base_path = config.base_path.to_string();
36        let prefix = config.static_uri_pref.to_string();
37        drop(config);
38
39        let content = if request.url.starts_with(&prefix) {
40            read_static(&request.url)(tmp_base_path.as_str())
41        } else {
42            (routes.get_route(request.url.as_ref()).render_action)(request.url.as_ref())
43        };
44
45        let response = format!(
46            "HTTP/1.1 200 OK\r\n\r\n{}",
47            content
48        );
49        log::info!("Response: {}", response);
50
51        let mut reader_stream = reader.into_inner();
52        let result = reader_stream.write(response.as_bytes());
53        result.unwrap_or_else(|error| {
54            log::info!("{}", error);
55            0
56        });
57
58        reader_stream.flush().unwrap_or_else(|error| {
59            log::info!("{}", error);
60        });
61    }
62
63    /// Parse stream of bytes in Request object.
64    /// Gets URI, HTTP method, headers and body.
65    fn parse_request(&self, reader: &mut BufReader<TcpStream>) -> Request {
66        let mut lines = reader.by_ref().lines();
67
68        // get method line, it should be in every request, so unwrapping is safe
69        // fixme thread '<unnamed>' panicked at 'called `Option::unwrap()` on a `None` value',
70        // /Users/glotitude/.cargo/registry/src/github.com-1ecc6299db9ec823/ctchi-0.19.0/src/core/app.rs:69:40
71        // for some reason
72        let method_line = lines.next().unwrap().unwrap();
73        let method = method_line.split(" ").collect::<Vec<&str>>();
74        let http_method = HttpMethod::parse(method[0]);
75
76        let mut headers = HashMap::<String, String>::new();
77        for line in lines {
78            let l = line.unwrap();
79
80            // fixme we will miss body in this case
81            if l == String::from("") {
82                break;
83            }
84
85            let parts = l.split(":").collect::<Vec<&str>>();
86            if parts.len() == 2 {
87                headers.insert(parts[0].to_string(), parts[1].to_string());
88            }
89        };
90
91        let mut url = if method.len() > 1 {
92            method[1].to_string()
93        } else {
94            String::new()
95        };
96
97        if !url.ends_with("/")
98            && !url.ends_with(".css")
99            && !url.ends_with(".js")
100            && !url.ends_with(".jpg")
101        {
102            url = format!("{}/", url);
103        }
104
105        Request {
106            method: http_method,
107            url,
108            headers,
109            body: String::new(),
110        }
111    }
112}
113
114pub struct Ctchi {
115    routes: Routes,
116}
117
118impl Ctchi {
119    /// Create new application with specified routes.
120    ///
121    /// Configuration gets by `ctchi::core::ctchi::get_configuration()` singleton.
122    ///
123    /// # Arguments:
124    /// * `routes` - list of routes. `ctchi::core::ctchi::Routes`
125    ///
126    pub fn new(routes: Routes) -> Ctchi {
127        Ctchi {
128            routes
129        }
130    }
131
132    /// Start configured application. Now it will lister for specified ip:port
133    /// and respond for request if URI is in routes
134    ///
135    /// # Examples:
136    ///
137    /// ```rust,ignore
138    /// #![feature(concat_idents)]
139    /// #[macro_use]
140    /// extern crate ctchi;
141    ///
142    /// use ctchi::core::app::Ctchi;
143    /// use ctchi::core::routes::{Routes, Route};
144    ///
145    /// use ctchi_codegen::route;
146    ///
147    /// #[route("/")]
148    /// fn index() -> String {
149    ///     render!("index.html")
150    /// }
151    ///
152    /// fn main() {
153    ///     let mut routes = Routes::new();
154    ///     // add route to your controller
155    ///     routes.add_route(routes!(index)());
156    ///
157    ///     // create and run local server
158    ///     let server = Ctchi::new(routes);
159    ///     let server_result = match server.start() {
160    ///         Ok(()) => "Ctchi application server is successfully running!".to_string(),
161    ///         Err(err) => format!("Can't start server! Because '{}'", err)
162    ///     };
163    /// }
164    /// ```
165    pub fn start(self) -> std::io::Result<()> {
166        let config_reader = get_configuration();
167        let config = config_reader.inner.lock().unwrap();
168
169        let listener = TcpListener::bind(&config.bind_path)?;
170        drop(config);
171        let routes = Arc::new(self.routes);
172
173        let pool = ThreadPool::new(4);
174
175        for stream in listener.incoming() {
176            let stream = stream.unwrap();
177            let r = routes.clone();
178
179            pool.execute(|| {
180                let handler = RequestHandler {};
181
182                handler.handle_request(stream, r);
183            });
184        }
185        Ok(())
186    }
187}