diff options
| -rw-r--r-- | src/handlers.rs | 315 | ||||
| -rw-r--r-- | src/routes.rs | 4 |
2 files changed, 192 insertions, 127 deletions
diff --git a/src/handlers.rs b/src/handlers.rs index b896ac2..beae999 100644 --- a/src/handlers.rs +++ b/src/handlers.rs | |||
| @@ -1,7 +1,8 @@ | |||
| 1 | /// API handlers, the ends of each filter chain | 1 | /// API handlers, the ends of each filter chain |
| 2 | use blake2::{Blake2s, Digest}; | 2 | use blake2::{Blake2s, Digest}; |
| 3 | use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; | 3 | use jsonwebtoken::errors::ErrorKind; |
| 4 | use log::debug; | 4 | use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; |
| 5 | use log::{debug, warn}; | ||
| 5 | use md5::Md5; | 6 | use md5::Md5; |
| 6 | use parking_lot::RwLockUpgradableReadGuard; | 7 | use parking_lot::RwLockUpgradableReadGuard; |
| 7 | use serde_json; | 8 | use serde_json; |
| @@ -82,102 +83,109 @@ pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> { | |||
| 82 | } | 83 | } |
| 83 | 84 | ||
| 84 | /// POST /block | 85 | /// POST /block |
| 85 | /// Proposes a new block for the next round | 86 | /// |
| 87 | /// Proposes a new block for the next round. | ||
| 86 | /// Can reject the block | 88 | /// Can reject the block |
| 87 | pub async fn auth_propose_block( | 89 | /// |
| 90 | /// TODO: WHO IS PROPOSING THIS BLOCK OH GOD <13-04-21, yigit> // ok let's say the proposer has | ||
| 91 | /// to put their transaction as the first transaction of the transaction_list | ||
| 92 | /// that's not going to backfire in any way | ||
| 93 | /// | ||
| 94 | /// TODO: after a block is accepted, it's transactions should play out and the proposer should | ||
| 95 | /// get something for their efforts <13-04-21, yigit> // | ||
| 96 | pub async fn authorized_propose_block( | ||
| 88 | new_block: Block, | 97 | new_block: Block, |
| 89 | token: String, | 98 | token: String, |
| 90 | db: Db, | 99 | db: Db, |
| 91 | ) -> Result<impl warp::Reply, warp::Rejection> { | 100 | ) -> Result<impl warp::Reply, warp::Rejection> { |
| 92 | debug!("POST request to /block, auth_propose_block"); | 101 | debug!("POST request to /block, authorized_propose_block"); |
| 102 | |||
| 103 | let users_store = db.users.read(); | ||
| 104 | |||
| 105 | let internal_user = match users_store.get(&new_block.transaction_list[0]) { | ||
| 106 | Some(existing_user) => existing_user, | ||
| 107 | None => { | ||
| 108 | debug!( | ||
| 109 | "A user with public key signature {:?} is not found in the database", | ||
| 110 | new_block.transaction_list[0] | ||
| 111 | ); | ||
| 112 | // TODO: verbose error here <13-04-21, yigit> // | ||
| 113 | return Ok(StatusCode::BAD_REQUEST); | ||
| 114 | } | ||
| 115 | }; | ||
| 93 | 116 | ||
| 94 | // Authorization check | 117 | let proposer_public_key = &internal_user.public_key; |
| 95 | let raw_jwt = token.trim_start_matches(BEARER).to_owned(); | ||
| 96 | debug!("raw_jwt: {:?}", raw_jwt); | ||
| 97 | 118 | ||
| 98 | // TODO: WHO IS PROPOSING THIS BLOCK OH GOD <13-04-21, yigit> // ok let's say the proposer has | 119 | let token_payload = match authorize_proposer(token, &proposer_public_key) { |
| 99 | // to put their transaction as the first transaction of the transaction_list | 120 | Ok(data) => data, |
| 100 | // that's not going to backfire in any way | 121 | Err(below) => { |
| 101 | // TODO: after a block is accepted, it's transactions should play out and the proposer should | 122 | debug!("Something went wrong below {:?}", below); |
| 102 | // get something for their efforts <13-04-21, yigit> // | 123 | return Ok(StatusCode::BAD_REQUEST); |
| 103 | if let Some(user) = db.users.read().get(&new_block.transaction_list[0]) { | 124 | } |
| 104 | let proposer_public_key = &user.public_key; | 125 | }; |
| 105 | |||
| 106 | if let Ok(decoded) = decode::<Claims>( | ||
| 107 | &raw_jwt, | ||
| 108 | &DecodingKey::from_rsa_pem(proposer_public_key.as_bytes()).unwrap(), | ||
| 109 | &Validation::new(Algorithm::RS256), | ||
| 110 | ) { | ||
| 111 | if decoded.claims.tha != new_block.hash { | ||
| 112 | debug!("Authorization unsuccessful"); | ||
| 113 | return Ok(StatusCode::BAD_REQUEST); | ||
| 114 | } | ||
| 115 | |||
| 116 | debug!("authorized for block proposal"); | ||
| 117 | |||
| 118 | let pending_transactions = db.pending_transactions.upgradable_read(); | ||
| 119 | let blockchain = db.blockchain.upgradable_read(); | ||
| 120 | |||
| 121 | for transaction_hash in new_block.transaction_list.iter() { | ||
| 122 | if !pending_transactions.contains_key(transaction_hash) { | ||
| 123 | return Ok(StatusCode::BAD_REQUEST); | ||
| 124 | } | ||
| 125 | } | ||
| 126 | 126 | ||
| 127 | let naked_block = NakedBlock { | 127 | debug!("authorized for block proposal"); |
| 128 | transaction_list: new_block.transaction_list.clone(), | ||
| 129 | nonce: new_block.nonce.clone(), | ||
| 130 | timestamp: new_block.timestamp.clone(), | ||
| 131 | }; | ||
| 132 | 128 | ||
| 133 | let naked_block_flat = serde_json::to_vec(&naked_block).unwrap(); | 129 | if token_payload.claims.tha != new_block.hash { |
| 130 | debug!( | ||
| 131 | "The Hash of the block {:?} did not match the hash given in jwt {:?}", | ||
| 132 | new_block.hash, token_payload.claims.tha | ||
| 133 | ); | ||
| 134 | return Ok(StatusCode::BAD_REQUEST); | ||
| 135 | } | ||
| 134 | 136 | ||
| 135 | let hashvalue = Blake2s::digest(&naked_block_flat); | 137 | debug!("clear for block proposal"); |
| 136 | let hash_string = format!("{:x}", hashvalue); | 138 | let pending_transactions = db.pending_transactions.upgradable_read(); |
| 139 | let blockchain = db.blockchain.upgradable_read(); | ||
| 137 | 140 | ||
| 138 | // 6 rightmost bits are zero? | 141 | for transaction_hash in new_block.transaction_list.iter() { |
| 139 | let should_zero = hashvalue[31] as i32 + hashvalue[30] as i32 + hashvalue[29] as i32; | 142 | if !pending_transactions.contains_key(transaction_hash) { |
| 143 | return Ok(StatusCode::BAD_REQUEST); | ||
| 144 | } | ||
| 145 | } | ||
| 140 | 146 | ||
| 141 | if should_zero == 0 { | 147 | let naked_block = NakedBlock { |
| 142 | // one last check to see if block is telling the truth | 148 | transaction_list: new_block.transaction_list.clone(), |
| 143 | if hash_string == new_block.hash { | 149 | nonce: new_block.nonce.clone(), |
| 144 | let mut blockchain = RwLockUpgradableReadGuard::upgrade(blockchain); | 150 | timestamp: new_block.timestamp.clone(), |
| 151 | }; | ||
| 145 | 152 | ||
| 146 | let block_json = serde_json::to_string(&new_block).unwrap(); | 153 | let naked_block_flat = serde_json::to_vec(&naked_block).unwrap(); |
| 147 | 154 | ||
| 148 | fs::write( | 155 | let hashvalue = Blake2s::digest(&naked_block_flat); |
| 149 | format!("blocks/{}.block", new_block.timestamp.timestamp()), | 156 | let hash_string = format!("{:x}", hashvalue); |
| 150 | block_json, | ||
| 151 | ) | ||
| 152 | .unwrap(); | ||
| 153 | 157 | ||
| 154 | *blockchain = new_block; | 158 | // 6 rightmost bits are zero? |
| 159 | let should_zero = hashvalue[31] as i32 + hashvalue[30] as i32 + hashvalue[29] as i32; | ||
| 155 | 160 | ||
| 156 | let mut pending_transactions = | 161 | if should_zero != 0 { |
| 157 | RwLockUpgradableReadGuard::upgrade(pending_transactions); | 162 | debug!("the hash does not have 6 rightmost zero bits"); |
| 158 | pending_transactions.clear(); | 163 | return Ok(StatusCode::BAD_REQUEST); |
| 164 | } | ||
| 159 | 165 | ||
| 160 | Ok(StatusCode::CREATED) | 166 | // one last check to see if block is telling the truth |
| 161 | } else { | 167 | if hash_string != new_block.hash { |
| 162 | debug!("request was not telling the truth, hash values do not match"); | 168 | debug!("request was not telling the truth, hash values do not match"); |
| 163 | // TODO: does this condition make more sense _before_ the hash 0s check? <13-04-21, yigit> // | 169 | // TODO: does this condition make more sense _before_ the hash 0s check? <13-04-21, yigit> // |
| 164 | Ok(StatusCode::BAD_REQUEST) | 170 | return Ok(StatusCode::BAD_REQUEST); |
| 165 | } | ||
| 166 | } else { | ||
| 167 | debug!("the hash does not have 6 rightmost zero bits"); | ||
| 168 | Ok(StatusCode::BAD_REQUEST) | ||
| 169 | } | ||
| 170 | } else { | ||
| 171 | debug!("authorization failed"); | ||
| 172 | Ok(StatusCode::BAD_REQUEST) | ||
| 173 | } | ||
| 174 | } else { | ||
| 175 | debug!( | ||
| 176 | "A user with public key signature {:?} is not found in the database", | ||
| 177 | new_block.transaction_list[0] | ||
| 178 | ); | ||
| 179 | Ok(StatusCode::BAD_REQUEST) | ||
| 180 | } | 171 | } |
| 172 | |||
| 173 | let mut blockchain = RwLockUpgradableReadGuard::upgrade(blockchain); | ||
| 174 | |||
| 175 | let block_json = serde_json::to_string(&new_block).unwrap(); | ||
| 176 | |||
| 177 | fs::write( | ||
| 178 | format!("blocks/{}.block", new_block.timestamp.timestamp()), | ||
| 179 | block_json, | ||
| 180 | ) | ||
| 181 | .unwrap(); | ||
| 182 | |||
| 183 | *blockchain = new_block; | ||
| 184 | |||
| 185 | let mut pending_transactions = RwLockUpgradableReadGuard::upgrade(pending_transactions); | ||
| 186 | pending_transactions.clear(); | ||
| 187 | |||
| 188 | Ok(StatusCode::CREATED) | ||
| 181 | } | 189 | } |
| 182 | 190 | ||
| 183 | /// POST /transaction | 191 | /// POST /transaction |
| @@ -190,64 +198,66 @@ pub async fn auth_propose_block( | |||
| 190 | /// * `db` - Global [`Db`] instance | 198 | /// * `db` - Global [`Db`] instance |
| 191 | /// | 199 | /// |
| 192 | /// TODO This method should check if the user has enough balance for the transaction | 200 | /// TODO This method should check if the user has enough balance for the transaction |
| 193 | /// | 201 | pub async fn authorized_propose_transaction( |
| 194 | /// TODO: refactor this https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html | ||
| 195 | pub async fn auth_propose_transaction( | ||
| 196 | new_transaction: Transaction, | 202 | new_transaction: Transaction, |
| 197 | token: String, | 203 | token: String, |
| 198 | db: Db, | 204 | db: Db, |
| 199 | ) -> Result<impl warp::Reply, warp::Rejection> { | 205 | ) -> Result<impl warp::Reply, warp::Rejection> { |
| 200 | debug!("POST request to /transaction, propose_transaction"); | 206 | debug!("POST request to /transaction, authorized_propose_transaction"); |
| 201 | debug!("The transaction request: {:?}", new_transaction); | 207 | debug!("The transaction request: {:?}", new_transaction); |
| 202 | 208 | ||
| 203 | let raw_jwt = token.trim_start_matches(BEARER).to_owned(); | 209 | let users_store = db.users.read(); |
| 204 | println!("raw_jwt: {:?}", raw_jwt); | 210 | |
| 205 | 211 | // Is this transaction from an authorized source? | |
| 206 | // Authorization check first | 212 | let internal_user = match users_store.get(&new_transaction.by) { |
| 207 | if let Some(user) = db.users.read().get(&new_transaction.by) { | 213 | Some(existing_user) => existing_user, |
| 208 | // This public key was already written to the database, we can panic if it's not valid at | 214 | None => { |
| 209 | // *this* point | 215 | debug!( |
| 210 | let by_public_key = &user.public_key; | 216 | "A user with public key signature {:?} is not found in the database", |
| 211 | 217 | new_transaction.by | |
| 212 | if let Ok(decoded) = decode::<Claims>( | 218 | ); |
| 213 | &raw_jwt, | 219 | // TODO: verbose error here <13-04-21, yigit> // |
| 214 | &DecodingKey::from_rsa_pem(by_public_key.as_bytes()).unwrap(), | 220 | return Ok(StatusCode::BAD_REQUEST); |
| 215 | &Validation::new(Algorithm::RS256), | ||
| 216 | ) { | ||
| 217 | // this transaction was already checked for correctness at custom_filters, we can panic | ||
| 218 | // here if it has been changed since | ||
| 219 | debug!("authorized for transaction proposal"); | ||
| 220 | |||
| 221 | let hashed_transaction = Md5::digest(&serde_json::to_vec(&new_transaction).unwrap()); | ||
| 222 | |||
| 223 | if decoded.claims.tha == format!("{:x}", hashed_transaction) { | ||
| 224 | let mut transactions = db.pending_transactions.write(); | ||
| 225 | |||
| 226 | transactions.insert(new_transaction.source.to_owned(), new_transaction); | ||
| 227 | |||
| 228 | Ok(StatusCode::CREATED) | ||
| 229 | } else { | ||
| 230 | debug!( | ||
| 231 | "the hash of the request {:x} did not match with the hash given in jwt {:?}", | ||
| 232 | hashed_transaction, decoded.claims.tha | ||
| 233 | ); | ||
| 234 | Ok(StatusCode::BAD_REQUEST) | ||
| 235 | } | ||
| 236 | } else { | ||
| 237 | debug!("raw_jwt was malformed {:?}", raw_jwt); | ||
| 238 | Ok(StatusCode::BAD_REQUEST) | ||
| 239 | } | 221 | } |
| 240 | } else { | 222 | }; |
| 223 | |||
| 224 | // `user` is an authenticated student, can propose | ||
| 225 | |||
| 226 | // This public key was already written to the database, we can panic if it's not valid at | ||
| 227 | // *this* point | ||
| 228 | let proposer_public_key = &internal_user.public_key; | ||
| 229 | |||
| 230 | let token_payload = match authorize_proposer(token, &proposer_public_key) { | ||
| 231 | Ok(data) => data, | ||
| 232 | Err(below) => { | ||
| 233 | debug!("Something went wrong below {:?}", below); | ||
| 234 | return Ok(StatusCode::BAD_REQUEST); | ||
| 235 | } | ||
| 236 | }; | ||
| 237 | |||
| 238 | // this transaction was already checked for correctness at custom_filters, we can panic | ||
| 239 | // here if it has been changed since | ||
| 240 | debug!("authorized for transaction proposal"); | ||
| 241 | |||
| 242 | let hashed_transaction = Md5::digest(&serde_json::to_vec(&new_transaction).unwrap()); | ||
| 243 | |||
| 244 | if token_payload.claims.tha != format!("{:x}", hashed_transaction) { | ||
| 241 | debug!( | 245 | debug!( |
| 242 | "A user with public key signature {:?} is not found in the database", | 246 | "the hash of the request {:x} did not match the hash given in jwt {:?}", |
| 243 | new_transaction.by | 247 | hashed_transaction, token_payload.claims.tha |
| 244 | ); | 248 | ); |
| 245 | Ok(StatusCode::BAD_REQUEST) | 249 | return Ok(StatusCode::BAD_REQUEST); |
| 246 | } | 250 | } |
| 251 | |||
| 252 | debug!("clear for transaction proposal"); | ||
| 253 | |||
| 254 | let mut transactions = db.pending_transactions.write(); | ||
| 255 | transactions.insert(new_transaction.source.to_owned(), new_transaction); | ||
| 256 | Ok(StatusCode::CREATED) | ||
| 247 | } | 257 | } |
| 248 | 258 | ||
| 249 | /// GET /block | 259 | /// GET /block |
| 250 | /// Returns JSON array of blocks | 260 | /// Returns the last block's JSON |
| 251 | /// Cannot fail | 261 | /// Cannot fail |
| 252 | /// Mostly around for debug purposes | 262 | /// Mostly around for debug purposes |
| 253 | pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> { | 263 | pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> { |
| @@ -257,3 +267,58 @@ pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> { | |||
| 257 | 267 | ||
| 258 | Ok(reply::with_status(reply::json(&*block), StatusCode::OK)) | 268 | Ok(reply::with_status(reply::json(&*block), StatusCode::OK)) |
| 259 | } | 269 | } |
| 270 | |||
| 271 | /// Handles the JWT Authorization | ||
| 272 | /// | ||
| 273 | /// *[`jwt_token`]: The raw JWT token, "Bearer aaa.bbb.ccc" | ||
| 274 | /// *[`user_pem`]: User Public Key, "BEGIN RSA" | ||
| 275 | /// NOT async, might look into it if this becomes a bottleneck | ||
| 276 | fn authorize_proposer( | ||
| 277 | jwt_token: String, | ||
| 278 | user_pem: &String, | ||
| 279 | ) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> { | ||
| 280 | // Throw away the "Bearer " part | ||
| 281 | let raw_jwt = jwt_token.trim_start_matches(BEARER).to_owned(); | ||
| 282 | debug!("raw_jwt: {:?}", raw_jwt); | ||
| 283 | |||
| 284 | // Extract a jsonwebtoken compatible decoding_key from user's public key | ||
| 285 | let decoding_key = match DecodingKey::from_rsa_pem(user_pem.as_bytes()) { | ||
| 286 | Ok(key) => key, | ||
| 287 | Err(j) => { | ||
| 288 | warn!( | ||
| 289 | "user has invalid RSA key we should crash and burn here {:?}", | ||
| 290 | j | ||
| 291 | ); | ||
| 292 | return Err(j); | ||
| 293 | } | ||
| 294 | }; | ||
| 295 | |||
| 296 | // Extract the payload inside the JWT | ||
| 297 | let token_payload = | ||
| 298 | match decode::<Claims>(&raw_jwt, &decoding_key, &Validation::new(Algorithm::RS256)) { | ||
| 299 | Ok(decoded) => decoded, | ||
| 300 | Err(err) => match *err.kind() { | ||
| 301 | ErrorKind::InvalidToken => { | ||
| 302 | // TODO: verbose error here <13-04-21, yigit> // | ||
| 303 | debug!("raw_jwt={:?} was malformed err={:?}", raw_jwt, err); | ||
| 304 | return Err(err); | ||
| 305 | } | ||
| 306 | ErrorKind::InvalidRsaKey => { | ||
| 307 | // TODO: verbose error here <13-04-21, yigit> // | ||
| 308 | debug!("the RSA key does not have a valid format, {:?}", err); | ||
| 309 | return Err(err); | ||
| 310 | } | ||
| 311 | ErrorKind::ExpiredSignature => { | ||
| 312 | // TODO: verbose error here <13-04-21, yigit> // | ||
| 313 | debug!("this token has expired {:?}", err); | ||
| 314 | return Err(err); | ||
| 315 | } | ||
| 316 | _ => { | ||
| 317 | warn!("AN UNSPECIFIED ERROR: {:?}", err); | ||
| 318 | return Err(err); | ||
| 319 | } | ||
| 320 | }, | ||
| 321 | }; | ||
| 322 | |||
| 323 | Ok(token_payload) | ||
| 324 | } | ||
diff --git a/src/routes.rs b/src/routes.rs index 0fb61c4..280de35 100644 --- a/src/routes.rs +++ b/src/routes.rs | |||
| @@ -48,7 +48,7 @@ pub fn auth_transaction_propose( | |||
| 48 | .and(custom_filters::transaction_json_body()) | 48 | .and(custom_filters::transaction_json_body()) |
| 49 | .and(custom_filters::auth_header()) | 49 | .and(custom_filters::auth_header()) |
| 50 | .and(custom_filters::with_db(db)) | 50 | .and(custom_filters::with_db(db)) |
| 51 | .and_then(handlers::auth_propose_transaction) | 51 | .and_then(handlers::authorized_propose_transaction) |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | /// POST /block warp route | 54 | /// POST /block warp route |
| @@ -58,6 +58,6 @@ pub fn auth_block_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = R | |||
| 58 | .and(custom_filters::block_json_body()) | 58 | .and(custom_filters::block_json_body()) |
| 59 | .and(custom_filters::auth_header()) | 59 | .and(custom_filters::auth_header()) |
| 60 | .and(custom_filters::with_db(db)) | 60 | .and(custom_filters::with_db(db)) |
| 61 | .and_then(handlers::auth_propose_block) | 61 | .and_then(handlers::authorized_propose_block) |
| 62 | } | 62 | } |
| 63 | 63 | ||
