diff --git a/Cargo.lock b/Cargo.lock index a50d523..7a5f958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,6 +276,11 @@ dependencies = [ "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "doc-comment" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "dtoa" version = "0.4.4" @@ -645,6 +650,7 @@ version = "0.1.2" dependencies = [ "app_dirs 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "byte-unit 3.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", "chashmap 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -660,6 +666,7 @@ dependencies = [ "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "tree_magic 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "warp 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1385,6 +1392,25 @@ name = "smallvec" version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "snafu" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "snafu-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "spin" version = "0.5.2" @@ -1985,6 +2011,7 @@ dependencies = [ "checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" "checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +"checksum doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97" "checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" "checksum failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9" @@ -2113,6 +2140,8 @@ dependencies = [ "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" +"checksum snafu 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41207ca11f96a62cd34e6b7fdf73d322b25ae3848eb9d38302169724bb32cf27" +"checksum snafu-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4c5e338c8b0577457c9dda8e794b6ad7231c96e25b1b0dd5842d52249020c1c0" "checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" "checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" diff --git a/Cargo.toml b/Cargo.toml index bd9fb4f..6483a22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ chashmap = "*" plugin = "*" tree_magic = "*" byte-unit = "*" +snafu = "*" +bytes = "*" #hyper = "*" #hyper-openssl = "*" diff --git a/src/configuration.rs b/src/configuration.rs index 55df0ca..1ca7c04 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,4 +1,4 @@ -use std::net::Ipv4Addr; +use std::net::{Ipv4Addr, IpAddr}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Configuration { @@ -7,14 +7,15 @@ pub struct Configuration { pub persistence: Persistence, pub encryption: Encryption, pub webui: WebUI, - pub store: Store + pub store: Store, + pub http: Http } impl Configuration { pub fn default() -> Configuration { Configuration { default: Base { - bind_address: Ipv4Addr::LOCALHOST.to_string(), + bind_address: IpAddr::from(Ipv4Addr::LOCALHOST), port: 7021, // TODO: change after implementing SSL port_ssl: 7021, use_ssl: false, @@ -38,6 +39,9 @@ impl Configuration { store: Store { max_limit: 7340032 }, + http: Http { + request_size_limit: 8388608 + }, } } @@ -48,9 +52,9 @@ impl Configuration { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Base { - pub bind_address: String, - pub port: i32, - pub port_ssl: i32, + pub bind_address: IpAddr, + pub port: u16, + pub port_ssl: u16, pub use_ssl: bool, } @@ -80,7 +84,12 @@ pub struct WebUI { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Store { - pub max_limit: i32 + pub max_limit: u64 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Http { + pub request_size_limit: u64 } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/server.rs b/src/server.rs index 6beee06..d9fa218 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,292 +1,310 @@ -use std::io::Read; use std::sync::Arc; use std::sync::RwLock; -use byte_unit::Byte; -use hyper::header::*; -use jsonwebtoken::{decode, Validation}; -use nickel::{*, HttpRouter, Middleware, MiddlewareResult, Nickel, Options, Request, Response, StaticFilesHandler}; -use nickel::hyper::method::Method; -use nickel::status::StatusCode; +use bytes::Buf; +use jsonwebtoken::Validation; +use snafu::Snafu; +use warp::{self, filters, fs, http::StatusCode, path, Filter, Rejection, Reply}; -use crate::configuration::*; +use crate::configuration::{Claims, Configuration}; use crate::kvstore::KvStore; -use crate::logger::{Logger, LogLevel}; - -pub struct Server { - configuration: Configuration -} #[derive(Serialize, Deserialize)] -struct ErrorMessage { +struct JsonMessage { message: String, } -#[derive(Serialize, Deserialize)] -struct Claims { - sub: String, - iss: String, - iat: i64, - exp: i64, +pub struct Server { + configuration: Arc>, } -#[derive(Serialize, Deserialize)] -struct PatchValue { - operation: String -} +impl Server { + pub fn new() -> Server { + Server { + configuration: Arc::new(RwLock::new(Configuration::default())), + } + } -fn middleware_webui<'a>(_: &mut Request, mut res: Response<'a>) -> MiddlewareResult<'a> { - res.set(MediaType::Html); - res.send_file("webui/dist/index.html") -} + pub fn configure(&mut self, configuration: Configuration) { + *self.configuration.write().unwrap() = configuration; + } -fn middleware_logging<'a, D>(request: &mut Request, response: Response<'a, D>) -> MiddlewareResult<'a, D> { - crate::logger::print(&LogLevel::Information, format!("{} {}", request.origin.method, request.origin.uri).as_ref()); - response.next_middleware() -} + pub fn run(&self) { + let store = Arc::new(KvStore::new()); + let store = warp::any().map(move || store.clone()); -fn middleware_cors<'mw>(_req: &mut Request, mut res: Response<'mw>) -> MiddlewareResult<'mw> { - res.headers_mut().set_raw("Access-Control-Allow-Origin", vec![b"*".to_vec()]); - res.headers_mut().set_raw("Access-Control-Allow-Methods", vec![b"*".to_vec()]); - res.headers_mut().set_raw("Access-Control-Allow-Headers", vec![b"*".to_vec()]); //Origin, Authorization, X-Requested-With, Content-Type, Accept - res.next_middleware() -} + let config = self.configuration.clone(); + let config = warp::any().map(move || config.clone()); -struct KvStoreMiddleware { - http_verb: hyper::method::Method, - store: Arc>, - configuration: Configuration -} + let auth = warp::header::optional::("authorization") + .and(config.clone()) + .and_then(verify_auth) + .untuple_one(); -impl Middleware for KvStoreMiddleware { - fn invoke<'mw, 'conn>(&self, req: &mut Request<'mw, 'conn, D>, mut res: Response<'mw, D>) -> MiddlewareResult<'mw, D> { + let webui_enabled = config + .clone() + .and_then(|config: Arc>| { + if config.read().unwrap().webui.enabled { + Ok(()) + } else { + Err(warp::reject::not_found()) + } + }) + .untuple_one(); - // Get the request body and retrieve the KV store - let store = &*self.store.write().unwrap(); - let mut buffer = Vec::new(); - let body_size = req.origin.read_to_end(&mut buffer).unwrap(); + let configuration = self.configuration.read().unwrap(); - // Set the server response header - res.set(Server(format!("Lucid {}", crate_version!()))); + let api_kv = path!("api" / "kv") + .and(path::end()) + .and(auth) + .and(filters::body::content_length_limit( + configuration.http.request_size_limit, + )) + .and( + warp::get2() + .and(store.clone()) + .and(warp::query::()) + .and_then(get_key) + .or(warp::put2() + .and(store.clone()) + .and(config.clone()) + .and(warp::query::()) + .and(filters::body::content_length_limit( + configuration.store.max_limit, + )) + .and(warp::body::concat()) + .and_then(put_key)) + .or(warp::delete2() + .and(store.clone()) + .and(warp::query::()) + .and_then(delete_key)) + .or(warp::head() + .and(store.clone()) + .and(warp::query::()) + .and_then(find_key)) + .or(warp::patch() + .and(store.clone()) + .and(warp::query::()) + .and(filters::body::json()) + .and_then(patch_key)), + ); - // TODO: handle authentication disabled - match req.origin.headers.get::>() { - Some(header) => match decode::(&header.token, self.configuration.authentication.secret_key.as_ref(), &Validation::default()) { - Ok(_bearer) => match self.http_verb { - Method::Head => match req.param("key") { - Some(key) => match &store.get(key.to_string()) { - Some(_) => { - res.set(StatusCode::Ok); - res.send("") - }, - None => { - res.set(StatusCode::NotFound).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "The specified key does not exists.".to_string() }).unwrap()) - } - }, - None => { - res.set(StatusCode::BadRequest).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "Missing key parameter.".to_string() }).unwrap()) - } - }, - Method::Put => { - if body_size == 0 { - res.set(StatusCode::BadRequest).set(MediaType::Json); - return res.send(serde_json::to_string_pretty(&ErrorMessage { message: "Missing request body.".to_string() }).unwrap()); - } + let webui = fs::dir("assets") + .or(fs::dir("webui/dist")) + .and(webui_enabled); - match req.param("key") { - Some(key) => if buffer.len() < self.configuration.store.max_limit as usize { - match store.set(key.to_string(), buffer) { - None => { - res.set(StatusCode::Created).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "The specified key was successfully created.".to_string() }).unwrap()) - }, - Some(_) => { - res.set(StatusCode::Ok).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "The specified key was successfully updated.".to_string() }).unwrap()) - } - } - } else { - res.set(StatusCode::BadRequest).set(MediaType::Json); - let max_limit = Byte::from_bytes(self.configuration.store.max_limit as u128); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: format!("The maximum allowed value size is {}.", max_limit.get_appropriate_unit(true)) }).unwrap()) - }, - None => { - res.set(StatusCode::BadRequest).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "Missing key parameter.".to_string() }).unwrap()) - } - } - }, - Method::Get => match req.param("key") { - // TODO: check query string, for getting metadata + let robots = warp::path("robots.txt") + .and(path::end()) + .and(warp::get2().map(|| "User-agent: *\nDisallow: /")); - Some(key) => match store.get(key.to_string()) { - Some(value) => { - res.set(StatusCode::Ok).set(MediaType::Txt); - res.send(value) - }, - None => { - // TODO: found a better name / location - if req.param("key").unwrap() == "check-token" { - res.set(StatusCode::Ok).set(MediaType::Json); - return res.send(serde_json::to_string_pretty(&ErrorMessage { message: format!("Lucid {}", crate_version!()) }).unwrap()); - } - res.set(StatusCode::NotFound).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "The specified key does not exists.".to_string() }).unwrap()) - } - }, - None => { - res.set(StatusCode::BadRequest).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "Missing key parameter.".to_string() }).unwrap()) - } - }, - Method::Delete => match req.param("key") { - Some(key) => { - store.drop(key.to_string()); - res.set(StatusCode::Ok); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "The specified key and it's data was successfully deleted.".to_string() }).unwrap()) - }, - None => { - res.set(StatusCode::BadRequest).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "Missing key parameter.".to_string() }).unwrap()) - } - }, - Method::Patch => match req.param("key") { - Some(key) => { - // TODO: Operations to implement: LOCK, UNLOCK, INCREMENT, DECREMENT, EXPIRE + let routes = api_kv.or(webui).or(robots).recover(process_error); + warp::serve(routes).run(( + configuration.default.bind_address, + configuration.default.port, + )); + } +} - if let Ok(json_body) = std::str::from_utf8((*buffer).as_ref()) { - match serde_json::from_str::(json_body) { - Ok(patch_value) => { - match patch_value.operation.as_str() { - "lock" | "unlock" => { - let r = store.switch_lock(key.to_string(), true); - println!("{}", r); - } - _ => () - } - }, - Err(e) => { - println!("{}", e); - } - } - } - res.set(StatusCode::Ok); - res.send("") - }, - None => { - res.set(StatusCode::BadRequest).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "Missing key parameter.".to_string() }).unwrap()) - } - }, - _ => { - res.set(StatusCode::MethodNotAllowed).set(MediaType::Json); - res.send(serde_json::to_string_pretty(&ErrorMessage { message: "Method not allowed, maybe in the future :)".to_string() }).unwrap()) - } - }, - Err(_) => { - res.set(StatusCode::InternalServerError).set(MediaType::Json); - return res.send(serde_json::to_string_pretty(&ErrorMessage { message: "Unable to decrypt JWT token.".to_string() }).unwrap()); //, details: Some(e.to_string()) - } - }, - None => { - res.set(StatusCode::Unauthorized).set(MediaType::Json); - return res.send(serde_json::to_string_pretty(&ErrorMessage { message: "Missing JWT token.".to_string() }).unwrap()); - } +#[derive(Debug, Deserialize)] +struct GetKeyParameters { + key: Option, +} +fn get_key(store: Arc, parameters: GetKeyParameters) -> Result { + if let Some(key) = parameters.key { + if let Some(value) = store.get(key) { + Ok(value) + } else { + Err(warp::reject::custom(Error::KeyNotFound)) } + } else { + Err(warp::reject::custom(Error::MissingParameter { + parameter: "key".to_string(), + })) } } -impl Server -{ - pub fn new() -> Server - { - Server { - configuration: Configuration::default() +#[derive(Debug, Deserialize)] +struct PutKeyParameters { + key: Option, +} +fn put_key( + store: Arc, + config: Arc>, + parameters: PutKeyParameters, + body: filters::body::FullBody, +) -> Result { + if body.remaining() == 0 { + Err(warp::reject::custom(Error::MissingBody)) + } else if body.bytes().len() as u64 > config.read().unwrap().store.max_limit { + Err(warp::reject::custom(Error::ValueSizeLimit { + max_limit: config.read().unwrap().store.max_limit, + })) + } else { + if let Some(key) = parameters.key { + if let Some(_) = store.set(key, body.bytes().to_vec()) { + Ok(warp::reply::json(&JsonMessage { + message: "The specified key was successfully updated.".to_string(), + })) + } else { + Ok(warp::reply::json(&JsonMessage { + message: "The specified key was successfully created.".to_string(), + })) + } + } else { + Err(warp::reject::custom(Error::MissingParameter { + parameter: "key".to_string(), + })) } } +} - pub fn configure(&mut self, configuration: Configuration) { - self.configuration = configuration; +#[derive(Debug, Deserialize)] +struct DeleteKeyParameters { + key: Option, +} +fn delete_key( + store: Arc, + parameters: DeleteKeyParameters, +) -> Result { + if let Some(key) = parameters.key { + if let Some(_) = store.get(key.clone()) { + (*store).drop(key); + Ok(warp::reply::json(&JsonMessage { + message: "The specified key and it's data was successfully deleted".to_string(), + })) + } else { + Err(warp::reject::custom(Error::KeyNotFound)) + } + } else { + Err(warp::reject::custom(Error::MissingParameter { + parameter: "key".to_string(), + })) } +} - fn router_webui(&self) -> nickel::Router { - let mut router = Nickel::router(); - router.get("/", middleware_webui); - router.get("/api/ui/version", middleware!(format!("Lucid Version {}", crate_version!()))); - router +#[derive(Debug, Deserialize)] +struct HeadKeyParameters { + key: Option, +} +fn find_key(store: Arc, parameters: HeadKeyParameters) -> Result { + if let Some(key) = parameters.key { + if let Some(_) = store.get(key) { + Ok("") + } else { + Err(warp::reject::custom(Error::KeyNotFound)) + } + } else { + Err(warp::reject::custom(Error::MissingParameter { + parameter: "key".to_string(), + })) } +} - fn router_sse(&self) -> nickel::Router { - let mut router = Nickel::router(); - router.get("/sse/test", middleware! { |_request, mut response| - response.set(StatusCode::BadRequest).set(MediaType::Json); - "lol" - }); - router +#[derive(Debug, Deserialize)] +struct PatchKeyParameters { + key: Option, +} +#[derive(Debug, Deserialize)] +struct PatchValue { + operation: String, +} +fn patch_key( + store: Arc, + parameters: PatchKeyParameters, + patch_value: PatchValue, +) -> Result { + if let Some(key) = parameters.key { + if let Some(_) = store.get(key.clone()) { + match patch_value.operation.as_str() { + "lock" | "unlock" => { + let r = store.switch_lock(key.to_string(), true); + println!("{}", r); + Ok("") + } + _ => Err(warp::reject::custom(Error::InvalidOperation { + operation: patch_value.operation, + })), + } + } else { + Err(warp::reject::custom(Error::KeyNotFound)) + } + } else { + Err(warp::reject::custom(Error::MissingParameter { + parameter: "key".to_string(), + })) } +} - pub fn run(&self) { - let server_options = Options::default() - .thread_count(None) // TODO: [Optimisation] improve this - .output_on_listen(false); - - let mut server = Nickel::with_options(server_options); - - let store = Arc::new(RwLock::new(KvStore::new())); - - server.utilize(middleware_logging); - - // CORS - server.utilize(middleware_cors); - server.options("**", middleware!("")); - - // Web UI - if self.configuration.webui.enabled { - server.utilize(self.router_webui()); - server.utilize(StaticFilesHandler::new("assets/")); - server.utilize(StaticFilesHandler::new("webui/dist")); +fn verify_auth( + auth_header: Option, + config: Arc>, +) -> Result<(), Rejection> { + let config = config.read().unwrap(); + if config.authentication.enabled { + if let Some(auth_header) = auth_header { + if let Ok(_bearer) = jsonwebtoken::decode::( + auth_header.trim_start_matches("Bearer "), + config.authentication.secret_key.as_ref(), + &Validation::default(), + ) { + Ok(()) + } else { + Err(warp::reject::custom(Error::InvalidJwtToken)) + } + } else { + Err(warp::reject::custom(Error::MissingAuthHeader)) } + } else { + Ok(()) + } +} - // TODO: maybe define if set in the configuration file - // Robots.txt - server.get("/robots.txt", middleware!("User-agent: *\nDisallow: /")); - - // API Endpoints - // TODO: change to server.head() (https://github.com/nickel-org/nickel.rs/issues/444) - server.add_route(Method::Head, "/api/kv/:key", KvStoreMiddleware { http_verb: Method::Head, store: store.clone(), configuration: self.configuration.clone() }); - server.put("/api/kv/:key", KvStoreMiddleware { http_verb: Method::Put, store: store.clone(), configuration: self.configuration.clone() }); - server.get("/api/kv/:key", KvStoreMiddleware { http_verb: Method::Get, store: store.clone(), configuration: self.configuration.clone() }); - server.patch("/api/kv/:key", KvStoreMiddleware { http_verb: Method::Patch, store: store.clone(), configuration: self.configuration.clone() }); - server.delete("/api/kv/:key", KvStoreMiddleware { http_verb: Method::Delete, store: store.clone(), configuration: self.configuration.clone() }); - - // SSE Endpoints - server.utilize(self.router_sse()); - - // TODO: Implement HTTPS (https://github.com/nickel-org/nickel.rs/blob/master/examples/https.rs) - match self.configuration.default.use_ssl { - true => { -// use hyper::Server; -// use hyper_openssl::OpensslServer; -// let ssl = Openssl::with_cert_and_key("examples/assets/self_signed.crt", "examples/assets/key.pem").unwrap(); -// server.listen_https("127.0.0.1:7021", ssl); - }, - false => match server.listen(self.configuration.clone().get_bind_endpoint()) { - Ok(instance) => { - // TODO: move logging for using in https to - // TODO: try using server.log and getting owner - &self.log(LogLevel::Information, format!("Running Lucid server on {endpoint} | PID: {pid}", endpoint = instance.socket(), pid = std::process::id()).as_str(), None); - &self.log(LogLevel::Information, format!("Lucid API Endpoint: http://{endpoint}/api/", endpoint = instance.socket()).as_str(), None); - if self.configuration.webui.enabled { - &self.log(LogLevel::Information, format!("Lucid Web UI Path: http://{endpoint}/", endpoint = instance.socket()).as_str(), None); - } - &self.log(LogLevel::Information, "Use Ctrl+C to stop the server.", None); - } - Err(err) => { - &self.log(LogLevel::Error, "Unable to run Lucid server", Some(Box::leak(err).description())); - } - }, - } +fn process_error(err: Rejection) -> Result { + if let Some(err) = err.find_cause::() { + let code = match err { + Error::MissingBody => StatusCode::BAD_REQUEST, + Error::MissingParameter { .. } => StatusCode::BAD_REQUEST, + Error::MissingAuthHeader => StatusCode::UNAUTHORIZED, + Error::KeyNotFound => StatusCode::NOT_FOUND, + Error::InvalidOperation { .. } => StatusCode::BAD_REQUEST, + Error::InvalidJwtToken => StatusCode::UNAUTHORIZED, + Error::ValueSizeLimit { .. } => StatusCode::BAD_REQUEST, + }; + let json = warp::reply::json(&JsonMessage { + message: err.to_string(), + }); + Ok(warp::reply::with_status(json, code)) + } else if let Some(_) = err.find_cause::() { + let code = StatusCode::METHOD_NOT_ALLOWED; + let json = warp::reply::json(&JsonMessage { + message: "Method not allowed.".to_string(), + }); + Ok(warp::reply::with_status(json, code)) + } else if let Some(_) = err.find_cause::() { + let code = StatusCode::METHOD_NOT_ALLOWED; + let json = warp::reply::json(&JsonMessage { + message: "Request payload is too long.".to_string(), // TODO: find a way to format the limit into this string + }); + Ok(warp::reply::with_status(json, code)) + } else { + Err(err) } -} \ No newline at end of file +} + +#[derive(Debug, Snafu)] +enum Error { + #[snafu(display("Missing request body."))] + MissingBody, + #[snafu(display("Missing \"{}\" parameter.", parameter))] + MissingParameter { parameter: String }, + #[snafu(display("Missing Authorization header."))] + MissingAuthHeader, + #[snafu(display("The specified key does not exist."))] + KeyNotFound, + #[snafu(display("Invalid Operation \"{}\".", operation))] + InvalidOperation { operation: String }, + #[snafu(display("Invalid JWT token in Authorization header."))] + InvalidJwtToken, + #[snafu(display("The maximum allowed value size is {} bytes.", max_limit))] + ValueSizeLimit { max_limit: u64 }, +}