diff options
author | Yigit Sever | 2021-04-13 15:36:57 +0300 |
---|---|---|
committer | Yigit Sever | 2021-04-13 15:40:03 +0300 |
commit | c0b2e354621ef1a66eaa7f12c119b825ab1ad522 (patch) | |
tree | 697429c5374bb1c6a4935768b201c67a08031372 | |
parent | 909f5317117594e4bc956d558a22698d59fd8129 (diff) | |
download | gradecoin-c0b2e354621ef1a66eaa7f12c119b825ab1ad522.tar.gz gradecoin-c0b2e354621ef1a66eaa7f12c119b825ab1ad522.tar.bz2 gradecoin-c0b2e354621ef1a66eaa7f12c119b825ab1ad522.zip |
Refactor authorized propose functions
They were getting spaghetti so;
new function: handlers::authorize_proposer(), handles the jwt stuff, NOT
async and _may_ cause trouble down the road but then again the stuff it
does used to be (repeated) in the functions so how bad can it be
If else chains were getting unwieldy;
https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html
so now everything is returning early, might make verbose error handling
easier
-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 | ||