Merge branch 'master' of git.silversoft.se:Imbus/FrostByte
This commit is contained in:
		
						commit
						44373dd2e2
					
				
					 7 changed files with 424 additions and 279 deletions
				
			
		
							
								
								
									
										475
									
								
								server/Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										475
									
								
								server/Cargo.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -1,58 +1,114 @@ | ||||||
| use jsonwebtoken::{ | use jsonwebtoken::{ | ||||||
|     decode, encode, errors::Result as JwtResult, DecodingKey, EncodingKey, Header, Validation, |     decode, encode, errors::Result as JwtResult, DecodingKey, EncodingKey, Header, Validation, | ||||||
| }; | }; | ||||||
| use log::*; |  | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| 
 | 
 | ||||||
| const DAYS_VALID: i64 = 7; | /// Claims holds the data that will be encoded into the JWT token.
 | ||||||
| const JWT_SECRET: &[u8] = "secret".as_bytes(); |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Serialize, Deserialize)] | #[derive(Debug, Serialize, Deserialize)] | ||||||
| pub struct Claims { | pub struct Claims { | ||||||
|  |     #[serde(rename = "sub")] | ||||||
|     pub sub: String, |     pub sub: String, | ||||||
|  |     #[serde(rename = "iss")] | ||||||
|     pub iss: String, |     pub iss: String, | ||||||
|     pub aud: String, |     #[serde(rename = "iat")] | ||||||
|     pub iat: usize, |     pub iat: usize, | ||||||
|  |     #[serde(rename = "exp")] | ||||||
|     pub exp: usize, |     pub exp: usize, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // JwtResult is just a predefined error from the jsonwebtoken crate
 | impl Claims { | ||||||
| pub fn token_factory(user: &str) -> JwtResult<String> { |     pub fn new(sub: &str, days: i64) -> Self { | ||||||
|     info!("Issuing JWT token for {}", user); |         Claims { | ||||||
| 
 |             sub: sub.to_string(), | ||||||
|     let token = encode( |  | ||||||
|         &Header::default(), |  | ||||||
|         &Claims { |  | ||||||
|             sub: user.to_string(), |  | ||||||
|             iss: "frostbyte".to_string(), |             iss: "frostbyte".to_string(), | ||||||
|             aud: "frostbyte".to_string(), |  | ||||||
|             iat: chrono::Utc::now().timestamp() as usize, |             iat: chrono::Utc::now().timestamp() as usize, | ||||||
|             exp: (chrono::Utc::now() + chrono::Duration::days(DAYS_VALID)).timestamp() as usize, |             exp: (chrono::Utc::now() + chrono::Duration::days(days)).timestamp() as usize, | ||||||
|         }, |         } | ||||||
|         &EncodingKey::from_secret(JWT_SECRET), |     } | ||||||
|     ) |  | ||||||
|     .unwrap(); |  | ||||||
| 
 |  | ||||||
|     Ok(token) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // JwtResult is just a predefined error from the jsonwebtoken crate
 | /// Authentication holds the data needed to encode and decode JWT tokens.
 | ||||||
| // This function is incomplete and should be expanded to check for more things
 | // This is then passed to the AuthenticationMiddleware
 | ||||||
| pub fn validate_token(token: &str) -> JwtResult<Claims> { | #[derive(Clone)] | ||||||
|     let token_data = decode::<Claims>( | pub struct Authentication { | ||||||
|         token, |     encoding_key: EncodingKey, | ||||||
|         &DecodingKey::from_secret(JWT_SECRET), |     decoding_key: DecodingKey, | ||||||
|         &Validation::default(), |     validation: Validation, | ||||||
|     ); |     days_valid: i64, // chrono::Duration::days() takes an i64, we don't want to cast it every time
 | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     match token_data { | impl Authentication { | ||||||
|         Ok(token_data) => { |     /// Create a new Authentication struct
 | ||||||
|             info!("Token validated for {}", token_data.claims.sub); |     pub fn new(secret: &[u8]) -> Self { | ||||||
|             Ok(token_data.claims) |         Authentication { | ||||||
|         } |             encoding_key: EncodingKey::from_secret(secret), | ||||||
|         Err(e) => { |             decoding_key: DecodingKey::from_secret(secret), | ||||||
|             error!("Token validation failed: {}", e); |             validation: Validation::default(), | ||||||
|             Err(e) |             days_valid: 7, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /// Encode the Claims struct into a JWT token, this is the raw version
 | ||||||
|  |     fn encode_raw(&self, claims: Claims) -> JwtResult<String> { | ||||||
|  |         encode(&Header::default(), &claims, &self.encoding_key) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Wrapper for encode_raw that takes a username (sub) and creates a Claims struct
 | ||||||
|  |     pub fn encode(&self, sub: &str) -> JwtResult<String> { | ||||||
|  |         let claims = Claims::new(sub, self.days_valid); | ||||||
|  |         self.encode_raw(claims) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Decode a JWT token into a Claims struct
 | ||||||
|  |     // If this faie, it means the token is invalid
 | ||||||
|  |     pub fn decode(&self, token: &str) -> JwtResult<Claims> { | ||||||
|  |         decode::<Claims>(token, &self.decoding_key, &self.validation).map(|data| data.claims) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_auth() { | ||||||
|  |         let username: &str = "testuser"; | ||||||
|  |         let auth = Authentication::new("secret".as_bytes()); | ||||||
|  |         assert!(auth.encode(username).is_ok()); | ||||||
|  | 
 | ||||||
|  |         let token = auth.encode(username); | ||||||
|  |         assert!(!token.is_err()); | ||||||
|  | 
 | ||||||
|  |         let token = token.unwrap(); | ||||||
|  |         assert!(!token.is_empty()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_validate() { | ||||||
|  |         let username: &str = "testuser"; | ||||||
|  |         let auth = Authentication::new("secret".as_bytes()); | ||||||
|  |         let token = auth.encode(username).unwrap(); | ||||||
|  |         let claims = auth.decode(&token).unwrap(); | ||||||
|  |         assert_eq!(claims.sub, username); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_invalid() { | ||||||
|  |         let auth = Authentication::new("secret".as_bytes()); | ||||||
|  |         let token = auth.encode("testuser").unwrap(); | ||||||
|  | 
 | ||||||
|  |         // Remove the first character should invalidate the token
 | ||||||
|  |         let token = token[1..].to_string(); | ||||||
|  |         assert!(auth.decode(&token).is_err()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_expired() { | ||||||
|  |         let auth = Authentication::new("secret".as_bytes()); | ||||||
|  | 
 | ||||||
|  |         // Chrono::duration allows negative durations, -1 is yesterday in this case
 | ||||||
|  |         let claims = Claims::new("testuser", -1); | ||||||
|  |         let token = auth.encode_raw(claims).unwrap(); | ||||||
|  |         assert!(auth.decode(&token).is_err()); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ mod state; | ||||||
| mod types; | mod types; | ||||||
| mod util; | mod util; | ||||||
| 
 | 
 | ||||||
|  | use jwt::Authentication; | ||||||
| use routes::{get_comments, get_posts, login, new_comment, new_post, post_by_id, register}; | use routes::{get_comments, get_posts, login, new_comment, new_post, post_by_id, register}; | ||||||
| use state::CaptchaState; | use state::CaptchaState; | ||||||
| use state::ServerState; | use state::ServerState; | ||||||
|  | @ -23,6 +24,7 @@ async fn main() -> std::io::Result<()> { | ||||||
| 
 | 
 | ||||||
|     let data = ServerState::new().await; |     let data = ServerState::new().await; | ||||||
|     let capt_db = CaptchaState::new(); |     let capt_db = CaptchaState::new(); | ||||||
|  |     let auth = Authentication::new("secret".as_bytes()); | ||||||
| 
 | 
 | ||||||
|     #[cfg(debug_assertions)] |     #[cfg(debug_assertions)] | ||||||
|     { |     { | ||||||
|  | @ -37,10 +39,13 @@ async fn main() -> std::io::Result<()> { | ||||||
|     HttpServer::new(move || { |     HttpServer::new(move || { | ||||||
|         let cors = Cors::default() |         let cors = Cors::default() | ||||||
|             .allowed_origin("https://shitpost.se") |             .allowed_origin("https://shitpost.se") | ||||||
|             .allowed_origin("http://localhost:8080") |  | ||||||
|             .allowed_methods(vec!["GET", "POST"]) |             .allowed_methods(vec!["GET", "POST"]) | ||||||
|             .max_age(3600); |             .max_age(3600); | ||||||
| 
 | 
 | ||||||
|  |         // In debug mode, allow localhost
 | ||||||
|  |         #[cfg(debug_assertions)] | ||||||
|  |         let cors = cors.allowed_origin("http://localhost:8080"); | ||||||
|  | 
 | ||||||
|         App::new() |         App::new() | ||||||
|             .wrap(cors) |             .wrap(cors) | ||||||
|             .wrap(middleware::Compress::default()) |             .wrap(middleware::Compress::default()) | ||||||
|  | @ -56,7 +61,8 @@ async fn main() -> std::io::Result<()> { | ||||||
|                     .service(login) |                     .service(login) | ||||||
|                     .service(register) |                     .service(register) | ||||||
|                     .app_data(Data::new(data.clone())) |                     .app_data(Data::new(data.clone())) | ||||||
|                     .app_data(Data::new(capt_db.clone())), |                     .app_data(Data::new(capt_db.clone())) | ||||||
|  |                     .app_data(Data::new(auth.clone())), | ||||||
|             ) |             ) | ||||||
|             .service( |             .service( | ||||||
|                 Files::new("/", "./public") |                 Files::new("/", "./public") | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| use crate::db::{db_get_comments, db_new_comment}; | use crate::db::{db_get_comments, db_new_comment}; | ||||||
| use crate::jwt::validate_token; | use crate::jwt::Authentication; | ||||||
| use crate::types::{CommentQueryParams, NewComment}; | use crate::types::{CommentQueryParams, NewComment}; | ||||||
| use crate::ServerState; | use crate::ServerState; | ||||||
| 
 | 
 | ||||||
|  | @ -31,8 +31,9 @@ pub async fn get_comments( | ||||||
| pub async fn new_comment( | pub async fn new_comment( | ||||||
|     data: Json<NewComment>, |     data: Json<NewComment>, | ||||||
|     state: Data<ServerState>, |     state: Data<ServerState>, | ||||||
|  |     auth: Data<Authentication>, | ||||||
| ) -> Result<impl Responder> { | ) -> Result<impl Responder> { | ||||||
|     let user_claims = validate_token(&data.user_token); |     let user_claims = auth.decode(&data.user_token); | ||||||
| 
 | 
 | ||||||
|     // Bail if the token is invalid
 |     // Bail if the token is invalid
 | ||||||
|     if let Err(e) = user_claims { |     if let Err(e) = user_claims { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| use crate::db::{db_get_latest_posts, db_get_post, db_new_post}; | use crate::db::{db_get_latest_posts, db_get_post, db_new_post}; | ||||||
| use crate::jwt::validate_token; | use crate::jwt::Authentication; | ||||||
| use crate::types::{NewPost, PostQueryParams}; | use crate::types::{NewPost, PostQueryParams}; | ||||||
| use crate::ServerState; | use crate::ServerState; | ||||||
| 
 | 
 | ||||||
|  | @ -23,8 +23,12 @@ pub async fn get_posts( | ||||||
| 
 | 
 | ||||||
| /// Creates a new post, requires a token in release mode
 | /// Creates a new post, requires a token in release mode
 | ||||||
| #[post("/posts")] | #[post("/posts")] | ||||||
| pub async fn new_post(new_post: Json<NewPost>, state: Data<ServerState>) -> Result<impl Responder> { | pub async fn new_post( | ||||||
|     let user_claims = validate_token(&new_post.token); |     new_post: Json<NewPost>, | ||||||
|  |     state: Data<ServerState>, | ||||||
|  |     auth: Data<Authentication>, | ||||||
|  | ) -> Result<impl Responder> { | ||||||
|  |     let user_claims = auth.decode(&new_post.token); | ||||||
| 
 | 
 | ||||||
|     if let Err(e) = user_claims { |     if let Err(e) = user_claims { | ||||||
|         info!("Error validating token: {}", e); |         info!("Error validating token: {}", e); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| use crate::db::{db_new_user, db_user_login}; | use crate::db::{db_new_user, db_user_login}; | ||||||
| use crate::jwt::token_factory; | use crate::jwt::Authentication; | ||||||
|  | // use crate::jwt::token_factory;
 | ||||||
| use crate::state::CaptchaState; | use crate::state::CaptchaState; | ||||||
| use crate::types::{AuthResponse, LoginData, RegisterData}; | use crate::types::{AuthResponse, LoginData, RegisterData}; | ||||||
| use crate::ServerState; | use crate::ServerState; | ||||||
|  | @ -14,6 +15,7 @@ pub async fn register( | ||||||
|     data: Json<RegisterData>, |     data: Json<RegisterData>, | ||||||
|     state: Data<ServerState>, |     state: Data<ServerState>, | ||||||
|     captcha_state: Data<CaptchaState>, |     captcha_state: Data<CaptchaState>, | ||||||
|  |     auth: Data<Authentication>, | ||||||
| ) -> Result<impl Responder> { | ) -> Result<impl Responder> { | ||||||
|     if !captcha_state |     if !captcha_state | ||||||
|         .capthca_db |         .capthca_db | ||||||
|  | @ -30,7 +32,7 @@ pub async fn register( | ||||||
|             info!("User: {} registered", &user.username); |             info!("User: {} registered", &user.username); | ||||||
|             Ok(HttpResponse::Ok().json(AuthResponse { |             Ok(HttpResponse::Ok().json(AuthResponse { | ||||||
|                 username: user.username.clone(), |                 username: user.username.clone(), | ||||||
|                 token: token_factory(&user.username).unwrap(), |                 token: auth.encode(&user.username).unwrap(), | ||||||
|             })) |             })) | ||||||
|         } |         } | ||||||
|         None => { |         None => { | ||||||
|  | @ -41,14 +43,18 @@ pub async fn register( | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[post("/login")] | #[post("/login")] | ||||||
| pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<impl Responder> { | pub async fn login( | ||||||
|  |     data: Json<LoginData>, | ||||||
|  |     state: Data<ServerState>, | ||||||
|  |     auth: Data<Authentication>, | ||||||
|  | ) -> Result<impl Responder> { | ||||||
|     let result = db_user_login(data.username.clone(), data.password.clone(), &state.pool).await; |     let result = db_user_login(data.username.clone(), data.password.clone(), &state.pool).await; | ||||||
| 
 | 
 | ||||||
|     match result { |     match result { | ||||||
|         Some(_) => { |         Some(_) => { | ||||||
|             return Ok(HttpResponse::Ok().json(AuthResponse { |             return Ok(HttpResponse::Ok().json(AuthResponse { | ||||||
|                 username: data.username.clone(), |                 username: data.username.clone(), | ||||||
|                 token: token_factory(&data.username).unwrap(), |                 token: auth.encode(&data.username).unwrap(), | ||||||
|             })); |             })); | ||||||
|         } |         } | ||||||
|         None => { |         None => { | ||||||
|  |  | ||||||
|  | @ -55,9 +55,12 @@ impl ServerState { | ||||||
|             None => error!("Failed to create default user..."), |             None => error!("Failed to create default user..."), | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         // We want dummy posts
 | ||||||
|  |         lipsum_setup(&pool).await.unwrap(); | ||||||
|  | 
 | ||||||
|         #[cfg(debug_assertions)] |         #[cfg(debug_assertions)] | ||||||
|         debug_setup(&pool).await.unwrap(); |         debug_setup(&pool).await.unwrap(); | ||||||
|         lipsum_setup(&pool).await.unwrap(); | 
 | ||||||
|         Self { pool } |         Self { pool } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -66,48 +69,18 @@ impl ServerState { | ||||||
| // Mostly useful for debugging new posts, as we need to satisfy foreign key constraints.
 | // Mostly useful for debugging new posts, as we need to satisfy foreign key constraints.
 | ||||||
| #[cfg(debug_assertions)] | #[cfg(debug_assertions)] | ||||||
| async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> { | async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> { | ||||||
|     use lipsum::lipsum; |  | ||||||
|     use rand::prelude::*; |  | ||||||
|     use sqlx::query; |  | ||||||
| 
 |  | ||||||
|     use crate::db::db_new_user; |     use crate::db::db_new_user; | ||||||
| 
 |  | ||||||
|     db_new_user("user".to_string(), "pass".to_string(), pool).await; |     db_new_user("user".to_string(), "pass".to_string(), pool).await; | ||||||
| 
 |  | ||||||
|     // Check if the demo post already exists
 |  | ||||||
|     let no_posts = query!("SELECT * FROM posts WHERE id = 1",) |  | ||||||
|         .fetch_one(pool) |  | ||||||
|         .await |  | ||||||
|         .ok() |  | ||||||
|         .is_none(); |  | ||||||
| 
 |  | ||||||
|     // If the demo user already has a post, don't insert another one
 |  | ||||||
|     if no_posts { |  | ||||||
|         let mut rng = rand::thread_rng(); |  | ||||||
| 
 |  | ||||||
|         // This requires that the user with id 1 exists in the user table
 |  | ||||||
|         for _ in 0..100 { |  | ||||||
|             query!( |  | ||||||
|                 "INSERT INTO posts (user_id, content) VALUES (1, $1)", |  | ||||||
|                 lipsum(rng.gen_range(10..100)) |  | ||||||
|             ) |  | ||||||
|             .execute(pool) |  | ||||||
|             .await?; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world! The demo username is user and the password is pass.')",) |  | ||||||
|         .execute(pool) |  | ||||||
|         .await?; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Inserts a bunch of dummy posts into the database
 | ||||||
| async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { | async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { | ||||||
|     use lipsum::lipsum; |     use lipsum::lipsum; | ||||||
|     use rand::prelude::*; |     use rand::prelude::*; | ||||||
|     use sqlx::query; |     use sqlx::query; | ||||||
| 
 | 
 | ||||||
|  |     // Fetch any user
 | ||||||
|     let user_exist = query!("SELECT * FROM users",) |     let user_exist = query!("SELECT * FROM users",) | ||||||
|         .fetch_one(pool) |         .fetch_one(pool) | ||||||
|         .await |         .await | ||||||
|  | @ -117,6 +90,7 @@ async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { | ||||||
|     if user_exist { |     if user_exist { | ||||||
|         let mut rng = rand::thread_rng(); |         let mut rng = rand::thread_rng(); | ||||||
| 
 | 
 | ||||||
|  |         // Insert a 100 dummy posts.
 | ||||||
|         // This requires that the user with id 1 exists in the user table
 |         // This requires that the user with id 1 exists in the user table
 | ||||||
|         for _ in 0..100 { |         for _ in 0..100 { | ||||||
|             query!( |             query!( | ||||||
|  | @ -130,3 +104,16 @@ async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { | ||||||
| 
 | 
 | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_server_state() { | ||||||
|  |         let state = CaptchaState::new(); | ||||||
|  |         assert!(state.capthca_db.lock().unwrap().is_empty()); | ||||||
|  |         state.capthca_db.lock().unwrap().insert("test".to_string()); | ||||||
|  |         assert!(!state.capthca_db.lock().unwrap().is_empty()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Imbus
						Imbus