diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/auth.rs | 112 | ||||
-rw-r--r-- | src/error.rs | 63 | ||||
-rw-r--r-- | src/handlers.rs | 18 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/routes.rs | 38 | ||||
-rw-r--r-- | src/schema.rs | 14 |
6 files changed, 242 insertions, 5 deletions
diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..e22262c --- /dev/null +++ b/src/auth.rs | |||
@@ -0,0 +1,112 @@ | |||
1 | use crate::error::Error; | ||
2 | use crate::schema::{Db, Transaction}; | ||
3 | use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; | ||
4 | use serde::{Deserialize, Serialize}; | ||
5 | use warp::header::headers_cloned; | ||
6 | use warp::http::header::{HeaderMap, HeaderValue, AUTHORIZATION}; | ||
7 | use warp::{reject, Filter, Rejection}; | ||
8 | use thiserror::Error; | ||
9 | use anyhow::*; | ||
10 | |||
11 | const BEARER: &str = "Bearer "; | ||
12 | const PUBLIC_KEY_PEM: &str = "-----BEGIN PUBLIC KEY----- | ||
13 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4nU0G4WjkmcQUx0hq6LQ | ||
14 | uV5Q+ACmUFL/OjoYMDwC/O/6pCd1UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBiny | ||
15 | rUpnY4mhy0SQUwoeCw7YkcHAyhCjNT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1 | ||
16 | ptSuhFgQxziItamn8maoJ6JUSVEXVO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2n | ||
17 | EbFdTHS6pHqtZNHQndTmEKwRfh0RYtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIG | ||
18 | GHwdFxy1niLkXwtHNjV7lnIOkTbx6+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138 | ||
19 | sQIDAQAB | ||
20 | -----END PUBLIC KEY-----"; | ||
21 | |||
22 | // const private_key_pem: &str = "-----BEGIN RSA PRIVATE KEY----- | ||
23 | // MIIEpAIBAAKCAQEA4nU0G4WjkmcQUx0hq6LQuV5Q+ACmUFL/OjoYMDwC/O/6pCd1 | ||
24 | // UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBinyrUpnY4mhy0SQUwoeCw7YkcHAyhCj | ||
25 | // NT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1ptSuhFgQxziItamn8maoJ6JUSVEX | ||
26 | // VO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2nEbFdTHS6pHqtZNHQndTmEKwRfh0R | ||
27 | // YtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIGGHwdFxy1niLkXwtHNjV7lnIOkTbx | ||
28 | // 6+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138sQIDAQABAoIBAD23nYTmrganag6M | ||
29 | // wPFrBSGP79c3Lhx0EjUHQjJbGKFgsdltG48qM3ut+DF9ACy0Z+/7bbC7+39vaIOq | ||
30 | // 1jLR2d6aiYTaLKseO4s2FawD1sgamvU3BZPsXn0gAhnnU5Gyy8Nas1dccvhoc9wI | ||
31 | // neaZUPrvucQ90AzLfo6r9yacDbYHB1lOyomApUvpJxOgHISGEtc9qGPDrdH19aF0 | ||
32 | // 8fCv2bbQRh+TChgN3IB0o5w0wXaI7YAyAouAv/AzHCoEMpt7OGjFTkjh/ujlPL9O | ||
33 | // +FLuJNsQRHDN0gJo2pcvwGwDCsioMixQ9bZ7ZrUu2BNpEQygyeSbj9ZI1iRvhosO | ||
34 | // JU3rwEECgYEA9MppTYA6A9WQbCCwPH1QMpUAmPNVSWVhUVag4lGOEhdCDRcz9ook | ||
35 | // DohQMKctiEB1luKuvDokxo0uMOfMO9/YwjsRB7qjQip7Th1zMJIjD+A+juLzHK4r | ||
36 | // /RiRtWYGAnF8mptDvE+93JsPb3C/lQLvIhio5GQYWBqPJu6SpeosIskCgYEA7NPi | ||
37 | // Gbffzr2UQhW8BNKmctEEh8yFRVojFo3wwwWxSNUVXGSmSm31CL+Q8h817R+2OkPV | ||
38 | // 1ZMUOBU4UJiqFt28kIvTDFqbAJlJQGCpY2mY7OLQiD2A+TVLcFrHmoCaPfCAK1Qd | ||
39 | // hQ0PmFK7Mf8qClpA3E5chop/WfKQfiu46sZv1qkCgYAhGdXPcw1lQ1W6KVlrdI6J | ||
40 | // qHhiNlVMDXdxZkNvFxQdAiQeXQrbxaZGiMw/J/wSNpUwCAsUzM/4QVMDrfSCDCzl | ||
41 | // ZtNQtj4pTlFKKNVQthIjrXEIJUw2jp7IJLBfVSJu5iWxSlmId0f3MsiNizN81N69 | ||
42 | // P5Rm/doE3+KHoy8VXGsHcQKBgQCkNh62enqjHWypjex6450qS6f6iWN3PRLLVsw0 | ||
43 | // TcQpniZblCaBwVCAKmRUnjOEIdL2/4ZLutnwMTaFG/YEOOfAylMiY8jKV38lNmD9 | ||
44 | // X4D78CFr9klxgvS2CRwSE03f2NzmLkLxuKaxldvaxPTfjMkgeO1LFMlNExYBhkuH | ||
45 | // 7uQpUQKBgQCKX6qMNh2gSdgG7qyxfTFZ4y5EGOBoKe/dE+IcVF3Vnh6DZVbCAbBL | ||
46 | // 5EdFWZSrCnDjA4xiKW55mwp95Ud9EZsZAb13L8V9t82eK+UDBoWlb7VRNYpda/x1 | ||
47 | // 5/i4qQJ28x2UNJDStpYFpnp4Ba1lvXjKngIbDPkjU+hbBJ+BNGAIeg== | ||
48 | // -----END RSA PRIVATE KEY-----"; | ||
49 | |||
50 | /// sub: Subject, user identifier | ||
51 | /// exp: Expiration date, Unix Time, epoch | ||
52 | /// puk: Public Key? Not 100% on this | ||
53 | #[derive(Debug, Serialize, Deserialize)] | ||
54 | struct Claims { | ||
55 | sub: String, | ||
56 | exp: usize, | ||
57 | puk: String, | ||
58 | } | ||
59 | |||
60 | // #[derive(Error, Debug)] | ||
61 | // pub enum Nope { | ||
62 | // #[error("Invalid header")] | ||
63 | // InvalidHeader { | ||
64 | // expected: String, | ||
65 | // found: String, | ||
66 | // }, | ||
67 | // } | ||
68 | |||
69 | pub fn with_auth( | ||
70 | db: Db, | ||
71 | t: Transaction, | ||
72 | ) -> impl Filter<Extract = (String,), Error = Rejection> + Clone { | ||
73 | headers_cloned() | ||
74 | .map(move |headers: HeaderMap<HeaderValue>| (db.clone(), headers)) | ||
75 | .and_then(authorize) | ||
76 | } | ||
77 | |||
78 | impl warp::reject::Reject for Nope {} | ||
79 | |||
80 | async fn authorize((db, headers): (Db, HeaderMap<HeaderValue>)) -> Result<String, Error> { | ||
81 | match jwt_from_header(&headers) { | ||
82 | Ok(jwt) => { | ||
83 | let decoded = decode::<Claims>( | ||
84 | &jwt, | ||
85 | // TODO: what key are we using here? pass db/pw store here to get the claimant's | ||
86 | // public key <10-04-21, yigit> // | ||
87 | &DecodingKey::from_rsa_pem(PUBLIC_KEY_PEM.as_bytes()).unwrap(), | ||
88 | &Validation::new(Algorithm::HS512), | ||
89 | ) | ||
90 | .map_err(|_| reject::custom(Error::JWTTokenError)) | ||
91 | .unwrap(); | ||
92 | |||
93 | Ok(decoded.claims.puk) | ||
94 | } | ||
95 | Err(e) => return Err(anyhow!("missing!")); | ||
96 | } | ||
97 | } | ||
98 | |||
99 | fn jwt_from_header(headers: &HeaderMap<HeaderValue>) -> Result<String, Error> { | ||
100 | let header = match headers.get(AUTHORIZATION) { | ||
101 | Some(v) => v, | ||
102 | None => return Err(Error::NoAuthHeaderError), | ||
103 | }; | ||
104 | let auth_header = match std::str::from_utf8(header.as_bytes()) { | ||
105 | Ok(v) => v, | ||
106 | Err(_) => return Err(Error::NoAuthHeaderError), | ||
107 | }; | ||
108 | if !auth_header.starts_with(BEARER) { | ||
109 | return Err(Error::InvalidAuthHeaderError); | ||
110 | } | ||
111 | Ok(auth_header.trim_start_matches(BEARER).to_owned()) | ||
112 | } | ||
diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..0db26c4 --- /dev/null +++ b/src/error.rs | |||
@@ -0,0 +1,63 @@ | |||
1 | use serde::Serialize; | ||
2 | use std::convert::Infallible; | ||
3 | use thiserror::Error; | ||
4 | use warp::{http::StatusCode, Rejection, Reply}; | ||
5 | |||
6 | #[derive(Error, Debug)] | ||
7 | pub enum Error { | ||
8 | // #[error("wrong credentials")] | ||
9 | // WrongCredentialsError, | ||
10 | #[error("jwt token not valid")] | ||
11 | JWTTokenError, | ||
12 | // #[error("jwt token creation error")] | ||
13 | // JWTTokenCreationError, | ||
14 | #[error("no auth header")] | ||
15 | NoAuthHeaderError, | ||
16 | #[error("invalid auth header")] | ||
17 | InvalidAuthHeaderError, | ||
18 | // #[error("no permission")] | ||
19 | // NoPermissionError, | ||
20 | } | ||
21 | |||
22 | #[derive(Serialize, Debug)] | ||
23 | struct ErrorResponse { | ||
24 | message: String, | ||
25 | status: String, | ||
26 | } | ||
27 | |||
28 | impl warp::reject::Reject for Error {} | ||
29 | |||
30 | pub async fn handle_rejection(err: Rejection) -> std::result::Result<impl Reply, Infallible> { | ||
31 | let (code, message) = if err.is_not_found() { | ||
32 | (StatusCode::NOT_FOUND, "Not Found".to_string()) | ||
33 | } else if let Some(e) = err.find::<Error>() { | ||
34 | match e { | ||
35 | // Error::WrongCredentialsError => (StatusCode::FORBIDDEN, e.to_string()), | ||
36 | // Error::NoPermissionError => (StatusCode::UNAUTHORIZED, e.to_string()), | ||
37 | Error::JWTTokenError => (StatusCode::UNAUTHORIZED, e.to_string()), | ||
38 | // Error::JWTTokenCreationError => ( | ||
39 | // StatusCode::INTERNAL_SERVER_ERROR, | ||
40 | // "Internal Server Error".to_string(), | ||
41 | // ), | ||
42 | _ => (StatusCode::BAD_REQUEST, e.to_string()), | ||
43 | } | ||
44 | } else if err.find::<warp::reject::MethodNotAllowed>().is_some() { | ||
45 | ( | ||
46 | StatusCode::METHOD_NOT_ALLOWED, | ||
47 | "Method Not Allowed".to_string(), | ||
48 | ) | ||
49 | } else { | ||
50 | eprintln!("unhandled error: {:?}", err); | ||
51 | ( | ||
52 | StatusCode::INTERNAL_SERVER_ERROR, | ||
53 | "Internal Server Error".to_string(), | ||
54 | ) | ||
55 | }; | ||
56 | |||
57 | let json = warp::reply::json(&ErrorResponse { | ||
58 | status: code.to_string(), | ||
59 | message, | ||
60 | }); | ||
61 | |||
62 | Ok(warp::reply::with_status(json, code)) | ||
63 | } | ||
diff --git a/src/handlers.rs b/src/handlers.rs index 856970d..256e72a 100644 --- a/src/handlers.rs +++ b/src/handlers.rs | |||
@@ -58,6 +58,24 @@ pub async fn propose_transaction( | |||
58 | Ok(StatusCode::CREATED) | 58 | Ok(StatusCode::CREATED) |
59 | } | 59 | } |
60 | 60 | ||
61 | /// POST /transaction, authenticated | ||
62 | /// The transaction arrived in this method has been authored by the public key in the source | ||
63 | pub async fn propose_authenticated_transaction( | ||
64 | pubkey: String, | ||
65 | new_transaction: Transaction, | ||
66 | db: Db, | ||
67 | ) -> Result<impl warp::Reply, warp::Rejection> { | ||
68 | debug!("new transaction request {:?}", new_transaction); | ||
69 | |||
70 | // let mut transactions = db.lock().await; | ||
71 | let mut transactions = db.pending_transactions.write(); | ||
72 | |||
73 | transactions.insert(new_transaction.source.to_owned(), new_transaction); | ||
74 | |||
75 | Ok(StatusCode::CREATED) | ||
76 | } | ||
77 | |||
78 | |||
61 | /// POST /block | 79 | /// POST /block |
62 | /// Proposes a new block for the next round | 80 | /// Proposes a new block for the next round |
63 | /// Can reject the block | 81 | /// Can reject the block |
diff --git a/src/main.rs b/src/main.rs index 7ef2597..91f6757 100644 --- a/src/main.rs +++ b/src/main.rs | |||
@@ -5,6 +5,8 @@ mod custom_filters; | |||
5 | mod handlers; | 5 | mod handlers; |
6 | mod routes; | 6 | mod routes; |
7 | mod schema; | 7 | mod schema; |
8 | mod auth; | ||
9 | mod error; | ||
8 | // mod validators; | 10 | // mod validators; |
9 | 11 | ||
10 | #[tokio::main] | 12 | #[tokio::main] |
diff --git a/src/routes.rs b/src/routes.rs index 95138e6..499ba35 100644 --- a/src/routes.rs +++ b/src/routes.rs | |||
@@ -1,8 +1,9 @@ | |||
1 | use warp::{Filter, Rejection, Reply}; | 1 | use warp::{Filter, Rejection, Reply}; |
2 | 2 | ||
3 | use crate::auth::with_auth; | ||
3 | use crate::custom_filters; | 4 | use crate::custom_filters; |
4 | use crate::handlers; | 5 | use crate::handlers; |
5 | use crate::schema::Db; | 6 | use crate::schema::{Db, Transaction}; |
6 | 7 | ||
7 | /// Root, all routes combined | 8 | /// Root, all routes combined |
8 | pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | 9 | pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |
@@ -14,7 +15,8 @@ pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rej | |||
14 | 15 | ||
15 | /// GET /transaction warp route | 16 | /// GET /transaction warp route |
16 | pub fn transaction_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | 17 | pub fn transaction_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |
17 | warp::path!("transaction") | 18 | warp::path("transaction") |
19 | .and(warp::path::end()) | ||
18 | .and(warp::get()) | 20 | .and(warp::get()) |
19 | .and(custom_filters::with_db(db)) | 21 | .and(custom_filters::with_db(db)) |
20 | .and_then(handlers::list_transactions) | 22 | .and_then(handlers::list_transactions) |
@@ -30,13 +32,43 @@ pub fn block_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection | |||
30 | 32 | ||
31 | /// POST /transaction warp route | 33 | /// POST /transaction warp route |
32 | pub fn transaction_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | 34 | pub fn transaction_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |
33 | warp::path!("transaction") | 35 | warp::path("transaction") |
36 | .and(warp::path::end()) | ||
34 | .and(warp::post()) | 37 | .and(warp::post()) |
35 | .and(custom_filters::transaction_json_body()) | 38 | .and(custom_filters::transaction_json_body()) |
36 | .and(custom_filters::with_db(db)) | 39 | .and(custom_filters::with_db(db)) |
37 | .and_then(handlers::propose_transaction) | 40 | .and_then(handlers::propose_transaction) |
38 | } | 41 | } |
39 | 42 | ||
43 | /// POST /transaction warp route with authentication | ||
44 | pub fn authenticated_transaction_propose( | ||
45 | db: Db, | ||
46 | ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | ||
47 | warp::path("transaction") | ||
48 | .and(warp::path::end()) | ||
49 | .and(warp::post()) | ||
50 | .and(custom_filters::transaction_json_body()) | ||
51 | .map(|t: Transaction| { | ||
52 | with_auth(db.clone(), t) | ||
53 | }) | ||
54 | .untuple_one() | ||
55 | .and(custom_filters::transaction_json_body()) | ||
56 | .and(custom_filters::with_db(db)) | ||
57 | .and_then(handlers::propose_authenticated_transaction) | ||
58 | |||
59 | // .and(custom_filters::transaction_json_body()) | ||
60 | // // TODO: you might have to restore this | ||
61 | // // what we're trying to do is knowing which public key to use to decode the jwt in the | ||
62 | // // header of the request, we will either request it through a header (ugly, ugh) or get it | ||
63 | // // from json (then how do we chain these ugh) or we can just validate/check (move the | ||
64 | // // header/jwt logic to propose_transaction but that doesn't feel right either | ||
65 | // // good luck <10-04-21, yigit> // | ||
66 | // .map(|t: Transaction| with_auth(db.clone(), t)) | ||
67 | // .and(custom_filters::transaction_json_body()) | ||
68 | // .and(custom_filters::with_db(db)) | ||
69 | // .and_then(handlers::propose_transaction) | ||
70 | } | ||
71 | |||
40 | /// POST /block warp route | 72 | /// POST /block warp route |
41 | pub fn block_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | 73 | pub fn block_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |
42 | warp::path!("block") | 74 | warp::path!("block") |
diff --git a/src/schema.rs b/src/schema.rs index 556e625..c4917ab 100644 --- a/src/schema.rs +++ b/src/schema.rs | |||
@@ -29,6 +29,8 @@ pub struct Db { | |||
29 | pub blockchain: Arc<RwLock<Vec<Block>>>, | 29 | pub blockchain: Arc<RwLock<Vec<Block>>>, |
30 | // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier | 30 | // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier |
31 | pub pending_transactions: Arc<RwLock<HashMap<String, Transaction>>>, | 31 | pub pending_transactions: Arc<RwLock<HashMap<String, Transaction>>>, |
32 | // this was bound to happen eventually | ||
33 | pub users: Arc<RwLock<HashMap<String, User>>>, | ||
32 | } | 34 | } |
33 | 35 | ||
34 | impl Db { | 36 | impl Db { |
@@ -36,12 +38,15 @@ impl Db { | |||
36 | Db { | 38 | Db { |
37 | blockchain: Arc::new(RwLock::new(Vec::new())), | 39 | blockchain: Arc::new(RwLock::new(Vec::new())), |
38 | pending_transactions: Arc::new(RwLock::new(HashMap::new())), | 40 | pending_transactions: Arc::new(RwLock::new(HashMap::new())), |
41 | users: Arc::new(RwLock::new(HashMap::new())), | ||
39 | } | 42 | } |
40 | } | 43 | } |
41 | } | 44 | } |
42 | 45 | ||
43 | /// A transaction between `source` and `target` that moves `amount` | 46 | /// A transaction between `source` and `target` that moves `amount` Note: |
44 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] | 47 | /// https://serde.rs/container-attrs.html might be valueable to normalize the serialize/deserialize |
48 | /// conventions as these will be hashed | ||
49 | #[derive(Serialize, Deserialize, Debug)] | ||
45 | pub struct Transaction { | 50 | pub struct Transaction { |
46 | pub source: String, | 51 | pub source: String, |
47 | pub target: String, | 52 | pub target: String, |
@@ -65,5 +70,10 @@ pub struct Block { | |||
65 | pub hash: String, // future proof'd baby | 70 | pub hash: String, // future proof'd baby |
66 | } | 71 | } |
67 | 72 | ||
73 | #[derive(Serialize, Deserialize, Debug)] | ||
74 | pub struct User { | ||
75 | username: String, | ||
76 | token: String | ||
77 | } | ||
68 | 78 | ||
69 | // TODO: write schema tests using the original repo <09-04-21, yigit> // | 79 | // TODO: write schema tests using the original repo <09-04-21, yigit> // |