From 23e4f52284ad5fa0b068220c54a255ff9fa7b18f Mon Sep 17 00:00:00 2001 From: Yigit Sever Date: Tue, 13 Apr 2021 20:37:54 +0300 Subject: Add verbose error messages Not happy with the solution one bit but using error.rs at https://blog.logrocket.com/create-an-async-crud-web-service-in-rust-with-warp/ was not working Basically we just handcraft every single response on the spot, there is some repetition and it's ugly but need to move on --- src/bin/main.rs | 5 +- src/custom_filters.rs | 2 +- src/error.rs | 38 ++++++++++ src/handlers.rs | 197 +++++++++++++++++++++++++++++++++++--------------- src/lib.rs | 1 + tester.sh | 8 +- 6 files changed, 188 insertions(+), 63 deletions(-) create mode 100644 src/error.rs diff --git a/src/bin/main.rs b/src/bin/main.rs index 598a2e1..8b61e5c 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,6 +1,7 @@ use std::env; use warp::Filter; +// use gradecoin::error; use gradecoin::routes::consensus_routes; use gradecoin::schema::create_database; @@ -9,6 +10,7 @@ use gradecoin::schema::create_database; #[tokio::main] async fn main() { // Show debug logs by default by setting `RUST_LOG=gradecoin=debug` + // TODO: write logs to file? <13-04-21, yigit> // if env::var_os("RUST_LOG").is_none() { env::set_var("RUST_LOG", "gradecoin=debug"); } @@ -21,5 +23,6 @@ async fn main() { let routes = api.with(warp::log("gradecoin")); // Start the server - warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; + let point = ([127, 0, 0, 1], 8080); + warp::serve(routes).run(point).await; } diff --git a/src/custom_filters.rs b/src/custom_filters.rs index dfdae04..ae8a56c 100644 --- a/src/custom_filters.rs +++ b/src/custom_filters.rs @@ -28,7 +28,7 @@ pub fn transaction_json_body() -> impl Filter impl Filter + Clone { - warp::header::header::("Authorization") + warp::header::("Authorization") } /// Extracts an `Block` JSON body from the request diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..7339a06 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,38 @@ +use log::warn; +use serde::Serialize; +use std::convert::Infallible; +use warp::{http::StatusCode, Rejection, Reply}; + +#[derive(Serialize)] +struct ErrorResponse { + message: String, +} + +pub async fn handle_rejection(err: Rejection) -> std::result::Result { + let code; + let message; + + if err.is_not_found() { + code = StatusCode::NOT_FOUND; + message = "Requested resource is not found"; + } else if let Some(_) = err.find::() { + code = StatusCode::BAD_REQUEST; + message = "Error: JSON body is not formatted correctly, check your payload"; + } else if let Some(_) = err.find::() { + code = StatusCode::METHOD_NOT_ALLOWED; + message = "Error: Authorization header missing, cannot authorize"; + } else if let Some(_) = err.find::() { + code = StatusCode::METHOD_NOT_ALLOWED; + message = "Error: method not allowed on this endpoint"; + } else { + warn!("unhandled error: {:?}", err); + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "Internal Server Error"; + } + + let json = warp::reply::json(&ErrorResponse { + message: message.to_owned(), + }); + + Ok(warp::reply::with_status(json, code)) +} diff --git a/src/handlers.rs b/src/handlers.rs index beae999..1189b35 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -5,10 +5,23 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; use log::{debug, warn}; use md5::Md5; use parking_lot::RwLockUpgradableReadGuard; +use serde::Serialize; use serde_json; use std::convert::Infallible; use std::fs; -use warp::{http::Response, http::StatusCode, reply}; +use warp::{http::StatusCode, reply}; + +#[derive(Serialize, Debug)] +struct GradeCoinResponse { + res: ResponseType, + message: String, +} + +#[derive(Debug, Serialize)] +enum ResponseType { + Success, + Error, +} use crate::schema::{AuthRequest, Block, Claims, Db, MetuId, NakedBlock, Transaction, User}; @@ -24,45 +37,52 @@ pub async fn authenticate_user( db: Db, ) -> Result { debug!("POST request to /register, authenticate_user"); - let given_id = request.student_id.clone(); + let provided_id = request.student_id.clone(); - if let Some(priv_student_id) = MetuId::new(request.student_id) { - let userlist = db.users.upgradable_read(); + let priv_student_id = match MetuId::new(request.student_id) { + Some(id) => id, + None => { + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "This user cannot have a gradecoin account".to_owned(), + }); - if userlist.contains_key(&given_id) { - let res = Response::builder() - .status(StatusCode::BAD_REQUEST) - .body("This user is already authenticated"); + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); + } + }; - Ok(res) - } else { - let new_user = User { - user_id: priv_student_id, - public_key: request.public_key, - balance: 0, - }; + let userlist = db.users.upgradable_read(); - let user_json = serde_json::to_string(&new_user).unwrap(); + if userlist.contains_key(&provided_id) { + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "This user is already authenticated".to_owned(), + }); - fs::write(format!("users/{}.guy", new_user.user_id), user_json).unwrap(); + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); + } - let mut userlist = RwLockUpgradableReadGuard::upgrade(userlist); - userlist.insert(given_id, new_user); - // TODO: signature of the public key, please <11-04-21, yigit> // + // TODO: audit public key, is it valid? <13-04-21, yigit> // + let new_user = User { + user_id: priv_student_id, + public_key: request.public_key, + balance: 0, + }; - let res = Response::builder() - .status(StatusCode::CREATED) - .body("Ready to use Gradecoin"); + let user_json = serde_json::to_string(&new_user).unwrap(); - Ok(res) - } - } else { - let res = Response::builder() - .status(StatusCode::BAD_REQUEST) - .body("This user cannot have a gradecoin account"); + fs::write(format!("users/{}.guy", new_user.user_id), user_json).unwrap(); - Ok(res) - } + let mut userlist = RwLockUpgradableReadGuard::upgrade(userlist); + userlist.insert(provided_id, new_user); + // TODO: signature of the public key, please <11-04-21, yigit> // + + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Success, + message: "User authenticated to use Gradecoin".to_owned(), + }); + + Ok(warp::reply::with_status(res_json, StatusCode::CREATED)) } /// GET /transaction @@ -106,11 +126,17 @@ pub async fn authorized_propose_block( Some(existing_user) => existing_user, None => { debug!( - "A user with public key signature {:?} is not found in the database", + "User with public key signature {:?} is not found in the database", new_block.transaction_list[0] ); - // TODO: verbose error here <13-04-21, yigit> // - return Ok(StatusCode::BAD_REQUEST); + + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "User with the given public key signature is not found in the database" + .to_owned(), + }); + + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); } }; @@ -120,7 +146,13 @@ pub async fn authorized_propose_block( Ok(data) => data, Err(below) => { debug!("Something went wrong below {:?}", below); - return Ok(StatusCode::BAD_REQUEST); + + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: below, + }); + + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); } }; @@ -131,7 +163,12 @@ pub async fn authorized_propose_block( "The Hash of the block {:?} did not match the hash given in jwt {:?}", new_block.hash, token_payload.claims.tha ); - return Ok(StatusCode::BAD_REQUEST); + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "The hash of the block did not match the hash given in JWT".to_owned(), + }); + + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); } debug!("clear for block proposal"); @@ -140,7 +177,12 @@ pub async fn authorized_propose_block( for transaction_hash in new_block.transaction_list.iter() { if !pending_transactions.contains_key(transaction_hash) { - return Ok(StatusCode::BAD_REQUEST); + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "Block contains unknown transaction".to_owned(), + }); + + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); } } @@ -157,17 +199,28 @@ pub async fn authorized_propose_block( // 6 rightmost bits are zero? let should_zero = hashvalue[31] as i32 + hashvalue[30] as i32 + hashvalue[29] as i32; + // TODO: this can be offloaded to validator <13-04-21, yigit> // if should_zero != 0 { debug!("the hash does not have 6 rightmost zero bits"); - return Ok(StatusCode::BAD_REQUEST); + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "Given block hash is larger than target value".to_owned(), + }); + + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); } // one last check to see if block is telling the truth if hash_string != new_block.hash { debug!("request was not telling the truth, hash values do not match"); // TODO: does this condition make more sense _before_ the hash 0s check? <13-04-21, yigit> // - return Ok(StatusCode::BAD_REQUEST); + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "Given hash value does not match the actual block hash".to_owned(), + }); + + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); } let mut blockchain = RwLockUpgradableReadGuard::upgrade(blockchain); @@ -185,7 +238,13 @@ pub async fn authorized_propose_block( let mut pending_transactions = RwLockUpgradableReadGuard::upgrade(pending_transactions); pending_transactions.clear(); - Ok(StatusCode::CREATED) + Ok(warp::reply::with_status( + warp::reply::json(&GradeCoinResponse { + res: ResponseType::Success, + message: "Block accepted".to_owned(), + }), + StatusCode::CREATED, + )) } /// POST /transaction @@ -213,11 +272,18 @@ pub async fn authorized_propose_transaction( Some(existing_user) => existing_user, None => { debug!( - "A user with public key signature {:?} is not found in the database", + "User with public key signature {:?} is not found in the database", new_transaction.by ); - // TODO: verbose error here <13-04-21, yigit> // - return Ok(StatusCode::BAD_REQUEST); + + return Ok(warp::reply::with_status( + warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "User with the given public key signature is not authorized" + .to_owned(), + }), + StatusCode::BAD_REQUEST, + )); } }; @@ -231,7 +297,13 @@ pub async fn authorized_propose_transaction( Ok(data) => data, Err(below) => { debug!("Something went wrong below {:?}", below); - return Ok(StatusCode::BAD_REQUEST); + return Ok(warp::reply::with_status( + warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: below, + }), + StatusCode::BAD_REQUEST, + )); } }; @@ -246,14 +318,26 @@ pub async fn authorized_propose_transaction( "the hash of the request {:x} did not match the hash given in jwt {:?}", hashed_transaction, token_payload.claims.tha ); - return Ok(StatusCode::BAD_REQUEST); + return Ok(warp::reply::with_status( + warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "The hash of the block did not match the hash given in JWT".to_owned(), + }), + StatusCode::BAD_REQUEST, + )); } debug!("clear for transaction proposal"); let mut transactions = db.pending_transactions.write(); transactions.insert(new_transaction.source.to_owned(), new_transaction); - Ok(StatusCode::CREATED) + Ok(warp::reply::with_status( + warp::reply::json(&GradeCoinResponse { + res: ResponseType::Success, + message: "Transaction accepted".to_owned(), + }), + StatusCode::CREATED, + )) } /// GET /block @@ -273,15 +357,13 @@ pub async fn list_blocks(db: Db) -> Result { /// *[`jwt_token`]: The raw JWT token, "Bearer aaa.bbb.ccc" /// *[`user_pem`]: User Public Key, "BEGIN RSA" /// NOT async, might look into it if this becomes a bottleneck -fn authorize_proposer( - jwt_token: String, - user_pem: &String, -) -> Result, jsonwebtoken::errors::Error> { +fn authorize_proposer(jwt_token: String, user_pem: &String) -> Result, String> { // Throw away the "Bearer " part let raw_jwt = jwt_token.trim_start_matches(BEARER).to_owned(); debug!("raw_jwt: {:?}", raw_jwt); // Extract a jsonwebtoken compatible decoding_key from user's public key + // TODO: just use this for reading users pem key <13-04-21, yigit> // let decoding_key = match DecodingKey::from_rsa_pem(user_pem.as_bytes()) { Ok(key) => key, Err(j) => { @@ -289,7 +371,7 @@ fn authorize_proposer( "user has invalid RSA key we should crash and burn here {:?}", j ); - return Err(j); + return Err(String::from("This User's RSA key is invalid")); } }; @@ -299,23 +381,20 @@ fn authorize_proposer( Ok(decoded) => decoded, Err(err) => match *err.kind() { ErrorKind::InvalidToken => { - // TODO: verbose error here <13-04-21, yigit> // debug!("raw_jwt={:?} was malformed err={:?}", raw_jwt, err); - return Err(err); + return Err(String::from("Invalid Token")); } ErrorKind::InvalidRsaKey => { - // TODO: verbose error here <13-04-21, yigit> // - debug!("the RSA key does not have a valid format, {:?}", err); - return Err(err); + debug!("The RSA key does not have a valid format, {:?}", err); + return Err(String::from("The RSA key does not have a valid format")); } ErrorKind::ExpiredSignature => { - // TODO: verbose error here <13-04-21, yigit> // debug!("this token has expired {:?}", err); - return Err(err); + return Err(String::from("This token has expired")); } _ => { warn!("AN UNSPECIFIED ERROR: {:?}", err); - return Err(err); + return Err(String::from("Unspecified error")); } }, }; diff --git a/src/lib.rs b/src/lib.rs index 6e51899..42def0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,3 +25,4 @@ pub mod custom_filters; pub mod handlers; pub mod routes; pub mod schema; +pub mod error; diff --git a/tester.sh b/tester.sh index 67b997a..44bd8de 100755 --- a/tester.sh +++ b/tester.sh @@ -26,6 +26,7 @@ curl --request POST \ curl --request POST \ --header 'Content-Type: application/json' \ --data '{ + "by": "old_transaction_hash_1", "source": "old_transaction_hash_1", "target": "target_account", "amount": 20, @@ -35,7 +36,7 @@ curl --request POST \ ## new transaction curl --request POST \ - --header 'Content-Type: application/json' \ + --header 'Content-Length: 36864' \ --data '{ "source": "old_transaction_hash_2", "target": "target_account", @@ -47,7 +48,9 @@ curl --request POST \ ## new transaction curl --request POST \ --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer arstarst.arstarst.arstarst' \ --data '{ + "by": "e254275", "source": "old_transaction_hash_3", "target": "target_account", "amount": 20, @@ -60,6 +63,7 @@ printf "\n\nList of current transactions\n\n" curl localhost:8080/transaction curl --header "Content-Type: application/json" \ + --header "Authorization: aaa.bbb.ccc" \ --request POST \ --data '{ "transaction_list": [ @@ -67,7 +71,7 @@ curl --header "Content-Type: application/json" \ "old_transaction_hash_2", "old_transaction_hash_3" ], - "nonce": "not_a_thing_yet", + "nonce": 0, "timestamp": "2021-04-08T12:30:30", "hash": "not_a_thing_yet" }' \ -- cgit v1.2.3-70-g09d2