diff --git a/Cargo.lock b/Cargo.lock index 00c04a4..ff495d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -503,7 +503,10 @@ version = "0.1.0" dependencies = [ "actix-rt", "actix-web", + "encoding_rs", + "futures", "mysql", + "percent-encoding", "serde", "yaml-rust", ] diff --git a/Cargo.toml b/Cargo.toml index 4d02645..e3bfbb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,7 @@ yaml-rust = "0.4.3" mysql = "18.2.0" actix-web = "2.0.0" actix-rt = "1.1.1" -serde = "1.0.110" \ No newline at end of file +serde = "1.0.110" +futures = "0.3.5" +encoding_rs = "0.8.23" +percent-encoding = "2.1.0" \ No newline at end of file diff --git a/src/controllers/server.rs b/src/controllers/server.rs index 5ed04c5..12bbc25 100644 --- a/src/controllers/server.rs +++ b/src/controllers/server.rs @@ -1,17 +1,119 @@ -use actix_web::{App, HttpResponse, HttpServer, web, http}; +use actix_web::{App, HttpResponse, HttpServer, web, http, FromRequest, HttpRequest, HttpMessage}; -use crate::controllers::routes::{get_routes, Route}; +use crate::controllers::routes::{get_routes, Route, RequestResult}; use crate::data::config::Config; use crate::data::http_error::HttpError; use crate::controllers::routes::Method::{GET, POST}; -use crate::data::http_request_handler::HttpRequestHandler; +use crate::data::http_request_handler::{HttpRequestHandler, RequestValue}; +use actix_web::dev::{PayloadStream, Payload, Decompress}; +use actix_web::web::{BytesMut}; +use futures::future::{ok, LocalBoxFuture, err}; +use futures::{FutureExt, StreamExt, TryFutureExt}; +use actix_web::error::{PayloadError, ErrorBadRequest}; +use std::error::Error; +use encoding_rs::UTF_8; +use std::collections::HashMap; + /// Main server functions /// /// @author Pierre Hubert +/// Custom request value +struct CustomRequest { + req: web::HttpRequest, + body: HashMap, +} + +/// Process in our way incoming requests +impl FromRequest for CustomRequest { + type Error = actix_web::Error; + type Future = LocalBoxFuture<'static, Result>; + type Config = (); + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let req = req.clone(); + let payload = payload.take(); + + async move { + let mut body_args = HashMap::new(); + + // Process "application/x-www-form-urlencoded" requests + if req.content_type().eq("application/x-www-form-urlencoded") { + + // Maximum size of the request + let limit = 16384; + + // Ready body + let mut body = BytesMut::with_capacity(8192); + let mut stream = Decompress::from_headers(payload, req.headers()); + while let Some(item) = stream.next().await { + let chunk = item?; + + + if body.len() + chunk.len() > limit { + return Err(actix_web::error::ErrorBadRequest("Overflow - too long")); + } else { + body.extend_from_slice(&chunk); + } + } + let body = body.freeze(); + + + // Decode body as a string + let encoding = req.encoding()?; + + let body_str = if encoding == UTF_8 { + String::from_utf8_lossy(body.as_ref()).to_string() + } else { + encoding + .decode_without_bom_handling_and_without_replacement(&body) + .map(|s| s.into_owned()) + .ok_or_else(|| ErrorBadRequest("Can not decode body"))? + }; + + // Parse body arguments (following the pattern key1=value1&key2=value2) + if body_str.len() > 0 { + for v in body_str.split("&") { + if v.len() == 0 { + continue; + } + + let args: Vec<&str> = v.split("=").collect(); + + if args.len() != 2 { + return Err(actix_web::error::ErrorBadRequest(format!("{} is invalid!", args[0]))); + } + + // Add the value to the body + body_args.insert( + args[0].to_string(), + RequestValue::string(args[1].to_string()) + ); + } + } + } + + Ok(CustomRequest { + req: req.clone(), + body: body_args, + }) + }.boxed_local() + } +} + +/// Process a "simple request" aka not a WebSocket request +fn process_simple_route(route: &Route, req: &mut HttpRequestHandler) -> RequestResult { + + // Validate client token + req.check_client_token()?; + + (route.func)(req) +} + /// Process an incoming request -async fn process_request(req: web::HttpRequest) -> HttpResponse { +async fn process_request(custom_req: CustomRequest) -> HttpResponse { + let req = &custom_req.req; let routes = get_routes(); // We search the appropriate route for the request @@ -40,9 +142,9 @@ async fn process_request(req: web::HttpRequest) -> HttpResponse { let route = route.unwrap(); // Execute the request - let mut request = HttpRequestHandler::new(req); + let mut request = HttpRequestHandler::new(custom_req.req, custom_req.body); - match (route.func)(&mut request) { + match process_simple_route(route, &mut request) { // Set default error response if required Err(e) => { @@ -57,10 +159,6 @@ async fn process_request(req: web::HttpRequest) -> HttpResponse { request.success("Success").unwrap() } } - - // I use this to be quiet with IntelliJ - #[allow(unreachable_patterns)] - _ => {println!("Unexpected case (server.rs)!")} } request.response() diff --git a/src/data/error.rs b/src/data/error.rs index db31387..65fb93c 100644 --- a/src/data/error.rs +++ b/src/data/error.rs @@ -6,6 +6,10 @@ use std::error; /// /// @author Pierre Hubert + +/// Simple result type +pub type ResultExecError = Result; + #[derive(Debug, Clone)] pub struct ExecError(pub String); @@ -17,6 +21,10 @@ impl ExecError { pub fn boxed_new(msg: &str) -> Box { Box::new(ExecError(msg.to_string())) } + + pub fn boxed_string(msg: String) -> Box { + Box::new(ExecError(msg)) + } } impl fmt::Display for ExecError { diff --git a/src/data/http_request_handler.rs b/src/data/http_request_handler.rs index b5bf79b..872843b 100644 --- a/src/data/http_request_handler.rs +++ b/src/data/http_request_handler.rs @@ -4,11 +4,27 @@ use crate::data::http_error::HttpError; use std::convert::TryFrom; use std::error::Error; use serde::Serialize; +use crate::data::error::{ResultExecError, ExecError}; +use std::collections::HashMap; /// Http request handler /// /// @author Pierre Hubert +/// Single request body value +pub struct RequestValue { + pub string: Option +} + +impl RequestValue { + /// Build a string value + pub fn string(s: String) -> RequestValue { + RequestValue { + string: Some(s) + } + } +} + #[derive(Serialize)] struct SuccessMessage { success: String @@ -16,16 +32,17 @@ struct SuccessMessage { pub struct HttpRequestHandler { request: web::HttpRequest, - response: Option + body: HashMap, + response: Option, } impl HttpRequestHandler { - /// Construct a new request handler - pub fn new(req: HttpRequest) -> HttpRequestHandler { + pub fn new(req: HttpRequest, body: HashMap) -> HttpRequestHandler { HttpRequestHandler { request: req, - response: None + body, + response: None, } } @@ -53,4 +70,34 @@ impl HttpRequestHandler { HttpError::internal_error("Internal server error."))); Err(Box::try_from(actix_web::error::ErrorInternalServerError(error)).unwrap()) } + + + /// Check login tokens + pub fn check_client_token(&mut self) -> ResultExecError<()> { + + println!("me = {}", self.post_string("me")?); + + Ok(()) + } + + /// Check if a POST parameter was present in the request or not + pub fn has_post_parameter(&self, name: &str) -> bool { + self.body.contains_key(name) + } + + /// Get a post parameter + pub fn post_parameter(&self, name: &str) -> Result<&RequestValue, ExecError> { + self.body.get(name) + .ok_or(ExecError(format!("POST parameter {} not found in request!", name))) + } + + /// Get a post string + pub fn post_string(&self, name: &str) -> Result { + let param = self.post_parameter(name)?; + + match ¶m.string { + None => Err(ExecError(format!("{} is not a string!", name))), + Some(s) => Ok(s.to_string()) + } + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 0a02af1..612f66d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,7 @@ +use comunic_server::controllers::server; use comunic_server::data::config::{conf, Config}; use comunic_server::helpers::database; -use comunic_server::controllers::server; -use comunic_server::helpers::database::QueryInfo; -#[derive(Debug)] -struct User { - id : i64, - name: String, -} #[actix_rt::main] async fn main() -> std::io::Result<()> { @@ -18,16 +12,6 @@ async fn main() -> std::io::Result<()> { // Connect to the database database::connect(&conf().database).expect("Could not connect to database!"); - let mut query = QueryInfo::new("user"); - query.cond("age", "190"); - //query.cond("id", "1"); - let res = database::query_row(query, |res| Ok(User { - id: res.get_int64("id")?, - name: res.get_str("name")?, - })).unwrap(); - - println!("{:#?}", res); - // Start the server server::start_server(conf()).await }