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::{
 | 
			
		||||
    decode, encode, errors::Result as JwtResult, DecodingKey, EncodingKey, Header, Validation,
 | 
			
		||||
};
 | 
			
		||||
use log::*;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
const DAYS_VALID: i64 = 7;
 | 
			
		||||
const JWT_SECRET: &[u8] = "secret".as_bytes();
 | 
			
		||||
 | 
			
		||||
/// Claims holds the data that will be encoded into the JWT token.
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
pub struct Claims {
 | 
			
		||||
    #[serde(rename = "sub")]
 | 
			
		||||
    pub sub: String,
 | 
			
		||||
    #[serde(rename = "iss")]
 | 
			
		||||
    pub iss: String,
 | 
			
		||||
    pub aud: String,
 | 
			
		||||
    #[serde(rename = "iat")]
 | 
			
		||||
    pub iat: usize,
 | 
			
		||||
    #[serde(rename = "exp")]
 | 
			
		||||
    pub exp: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JwtResult is just a predefined error from the jsonwebtoken crate
 | 
			
		||||
pub fn token_factory(user: &str) -> JwtResult<String> {
 | 
			
		||||
    info!("Issuing JWT token for {}", user);
 | 
			
		||||
 | 
			
		||||
    let token = encode(
 | 
			
		||||
        &Header::default(),
 | 
			
		||||
        &Claims {
 | 
			
		||||
            sub: user.to_string(),
 | 
			
		||||
impl Claims {
 | 
			
		||||
    pub fn new(sub: &str, days: i64) -> Self {
 | 
			
		||||
        Claims {
 | 
			
		||||
            sub: sub.to_string(),
 | 
			
		||||
            iss: "frostbyte".to_string(),
 | 
			
		||||
            aud: "frostbyte".to_string(),
 | 
			
		||||
            iat: chrono::Utc::now().timestamp() as usize,
 | 
			
		||||
            exp: (chrono::Utc::now() + chrono::Duration::days(DAYS_VALID)).timestamp() as usize,
 | 
			
		||||
        },
 | 
			
		||||
        &EncodingKey::from_secret(JWT_SECRET),
 | 
			
		||||
    )
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    Ok(token)
 | 
			
		||||
            exp: (chrono::Utc::now() + chrono::Duration::days(days)).timestamp() as usize,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JwtResult is just a predefined error from the jsonwebtoken crate
 | 
			
		||||
// This function is incomplete and should be expanded to check for more things
 | 
			
		||||
pub fn validate_token(token: &str) -> JwtResult<Claims> {
 | 
			
		||||
    let token_data = decode::<Claims>(
 | 
			
		||||
        token,
 | 
			
		||||
        &DecodingKey::from_secret(JWT_SECRET),
 | 
			
		||||
        &Validation::default(),
 | 
			
		||||
    );
 | 
			
		||||
/// Authentication holds the data needed to encode and decode JWT tokens.
 | 
			
		||||
// This is then passed to the AuthenticationMiddleware
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct Authentication {
 | 
			
		||||
    encoding_key: EncodingKey,
 | 
			
		||||
    decoding_key: DecodingKey,
 | 
			
		||||
    validation: Validation,
 | 
			
		||||
    days_valid: i64, // chrono::Duration::days() takes an i64, we don't want to cast it every time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    match token_data {
 | 
			
		||||
        Ok(token_data) => {
 | 
			
		||||
            info!("Token validated for {}", token_data.claims.sub);
 | 
			
		||||
            Ok(token_data.claims)
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Token validation failed: {}", e);
 | 
			
		||||
            Err(e)
 | 
			
		||||
impl Authentication {
 | 
			
		||||
    /// Create a new Authentication struct
 | 
			
		||||
    pub fn new(secret: &[u8]) -> Self {
 | 
			
		||||
        Authentication {
 | 
			
		||||
            encoding_key: EncodingKey::from_secret(secret),
 | 
			
		||||
            decoding_key: DecodingKey::from_secret(secret),
 | 
			
		||||
            validation: Validation::default(),
 | 
			
		||||
            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 util;
 | 
			
		||||
 | 
			
		||||
use jwt::Authentication;
 | 
			
		||||
use routes::{get_comments, get_posts, login, new_comment, new_post, post_by_id, register};
 | 
			
		||||
use state::CaptchaState;
 | 
			
		||||
use state::ServerState;
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +24,7 @@ async fn main() -> std::io::Result<()> {
 | 
			
		|||
 | 
			
		||||
    let data = ServerState::new().await;
 | 
			
		||||
    let capt_db = CaptchaState::new();
 | 
			
		||||
    let auth = Authentication::new("secret".as_bytes());
 | 
			
		||||
 | 
			
		||||
    #[cfg(debug_assertions)]
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -37,10 +39,13 @@ async fn main() -> std::io::Result<()> {
 | 
			
		|||
    HttpServer::new(move || {
 | 
			
		||||
        let cors = Cors::default()
 | 
			
		||||
            .allowed_origin("https://shitpost.se")
 | 
			
		||||
            .allowed_origin("http://localhost:8080")
 | 
			
		||||
            .allowed_methods(vec!["GET", "POST"])
 | 
			
		||||
            .max_age(3600);
 | 
			
		||||
 | 
			
		||||
        // In debug mode, allow localhost
 | 
			
		||||
        #[cfg(debug_assertions)]
 | 
			
		||||
        let cors = cors.allowed_origin("http://localhost:8080");
 | 
			
		||||
 | 
			
		||||
        App::new()
 | 
			
		||||
            .wrap(cors)
 | 
			
		||||
            .wrap(middleware::Compress::default())
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +61,8 @@ async fn main() -> std::io::Result<()> {
 | 
			
		|||
                    .service(login)
 | 
			
		||||
                    .service(register)
 | 
			
		||||
                    .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(
 | 
			
		||||
                Files::new("/", "./public")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
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::ServerState;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -31,8 +31,9 @@ pub async fn get_comments(
 | 
			
		|||
pub async fn new_comment(
 | 
			
		||||
    data: Json<NewComment>,
 | 
			
		||||
    state: Data<ServerState>,
 | 
			
		||||
    auth: Data<Authentication>,
 | 
			
		||||
) -> 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
 | 
			
		||||
    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::jwt::validate_token;
 | 
			
		||||
use crate::jwt::Authentication;
 | 
			
		||||
use crate::types::{NewPost, PostQueryParams};
 | 
			
		||||
use crate::ServerState;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -23,8 +23,12 @@ pub async fn get_posts(
 | 
			
		|||
 | 
			
		||||
/// Creates a new post, requires a token in release mode
 | 
			
		||||
#[post("/posts")]
 | 
			
		||||
pub async fn new_post(new_post: Json<NewPost>, state: Data<ServerState>) -> Result<impl Responder> {
 | 
			
		||||
    let user_claims = validate_token(&new_post.token);
 | 
			
		||||
pub async fn new_post(
 | 
			
		||||
    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 {
 | 
			
		||||
        info!("Error validating token: {}", e);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
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::types::{AuthResponse, LoginData, RegisterData};
 | 
			
		||||
use crate::ServerState;
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +15,7 @@ pub async fn register(
 | 
			
		|||
    data: Json<RegisterData>,
 | 
			
		||||
    state: Data<ServerState>,
 | 
			
		||||
    captcha_state: Data<CaptchaState>,
 | 
			
		||||
    auth: Data<Authentication>,
 | 
			
		||||
) -> Result<impl Responder> {
 | 
			
		||||
    if !captcha_state
 | 
			
		||||
        .capthca_db
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +32,7 @@ pub async fn register(
 | 
			
		|||
            info!("User: {} registered", &user.username);
 | 
			
		||||
            Ok(HttpResponse::Ok().json(AuthResponse {
 | 
			
		||||
                username: user.username.clone(),
 | 
			
		||||
                token: token_factory(&user.username).unwrap(),
 | 
			
		||||
                token: auth.encode(&user.username).unwrap(),
 | 
			
		||||
            }))
 | 
			
		||||
        }
 | 
			
		||||
        None => {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,14 +43,18 @@ pub async fn register(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
#[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;
 | 
			
		||||
 | 
			
		||||
    match result {
 | 
			
		||||
        Some(_) => {
 | 
			
		||||
            return Ok(HttpResponse::Ok().json(AuthResponse {
 | 
			
		||||
                username: data.username.clone(),
 | 
			
		||||
                token: token_factory(&data.username).unwrap(),
 | 
			
		||||
                token: auth.encode(&data.username).unwrap(),
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
        None => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,9 +55,12 @@ impl ServerState {
 | 
			
		|||
            None => error!("Failed to create default user..."),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // We want dummy posts
 | 
			
		||||
        lipsum_setup(&pool).await.unwrap();
 | 
			
		||||
 | 
			
		||||
        #[cfg(debug_assertions)]
 | 
			
		||||
        debug_setup(&pool).await.unwrap();
 | 
			
		||||
        lipsum_setup(&pool).await.unwrap();
 | 
			
		||||
 | 
			
		||||
        Self { pool }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -66,48 +69,18 @@ impl ServerState {
 | 
			
		|||
// Mostly useful for debugging new posts, as we need to satisfy foreign key constraints.
 | 
			
		||||
#[cfg(debug_assertions)]
 | 
			
		||||
async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> {
 | 
			
		||||
    use lipsum::lipsum;
 | 
			
		||||
    use rand::prelude::*;
 | 
			
		||||
    use sqlx::query;
 | 
			
		||||
 | 
			
		||||
    use crate::db::db_new_user;
 | 
			
		||||
 | 
			
		||||
    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(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Inserts a bunch of dummy posts into the database
 | 
			
		||||
async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> {
 | 
			
		||||
    use lipsum::lipsum;
 | 
			
		||||
    use rand::prelude::*;
 | 
			
		||||
    use sqlx::query;
 | 
			
		||||
 | 
			
		||||
    // Fetch any user
 | 
			
		||||
    let user_exist = query!("SELECT * FROM users",)
 | 
			
		||||
        .fetch_one(pool)
 | 
			
		||||
        .await
 | 
			
		||||
| 
						 | 
				
			
			@ -117,6 +90,7 @@ async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> {
 | 
			
		|||
    if user_exist {
 | 
			
		||||
        let mut rng = rand::thread_rng();
 | 
			
		||||
 | 
			
		||||
        // Insert a 100 dummy posts.
 | 
			
		||||
        // This requires that the user with id 1 exists in the user table
 | 
			
		||||
        for _ in 0..100 {
 | 
			
		||||
            query!(
 | 
			
		||||
| 
						 | 
				
			
			@ -130,3 +104,16 @@ async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> {
 | 
			
		|||
 | 
			
		||||
    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