diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eeeb220..5d2ef29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,25 +1,35 @@ image: shockrah/freechat:0.3 +variables: + CARGO_HOME: $CI_PROJECT_DIR/.cargo -debug-build: - stage: build - only: - - testing - script: - - cd server/ - - cargo build - - diesel setup --database-url $DATABASE_URL +cache: + key: "$CI_JOB_NAME" + untracked: true + paths: + - $CARGO_HOME + - $CI_PROJECT_DIR/server/target +before_script: + - export PATH="$CARGO_HOME/bin:$PATH" -basic-test: - stage: test - only: - - testing - script: - - cd server/ - - cargo run -- -s& - - fc_id=$! - - cd tests/ - - bash ./main.sh body - - kill ${fc_id} +build-release: + stage: build + only: + - testing + + script: + - cd server/ + - diesel setup --database-url $DATABASE_URL + - cargo build --release + + +api-test: + stage: test + needs: ["build-release"] + only: + - testing + script: + - cd server/ + - cargo test --release diff --git a/server-api/.gitignore b/server-api/.gitignore index 4f4a797..24e4eed 100644 --- a/server-api/.gitignore +++ b/server-api/.gitignore @@ -3,3 +3,4 @@ target static/css/ dev-sql/ +diesel.toml diff --git a/server-api/migrations/2020-02-04-083657_invites/up.sql b/server-api/migrations/2020-02-04-083657_invites/up.sql index 28e4c7c..f49c229 100644 --- a/server-api/migrations/2020-02-04-083657_invites/up.sql +++ b/server-api/migrations/2020-02-04-083657_invites/up.sql @@ -1,15 +1,12 @@ --- @id : id of the invite - --- @expires : unix timestamp of when that invite expries --- can be set to null which means it never expires +-- @id : id of the invite which is also its kill date -- @uses : can be null which means it doesn't have a use limit --- @max_uses : if this is null uses only ever incremented but we don't care for destroying on that parameter +-- @expires: boolean that tells wether the key expires or not + CREATE TABLE IF NOT EXISTS `invites` ( - `id` bigint UNSIGNED NOT NULL, - `expires` bigint, - `uses` integer, - `max_uses` integer, + `id` BIGINT UNIQUE NOT NULL, + `uses` BIGINT, + `expires` BOOLEAN NOT NULL, PRIMARY KEY( `id` ) ); diff --git a/server-api/migrations/2020-03-11-005217_channels/up.sql b/server-api/migrations/2020-03-11-005217_channels/up.sql index 067160f..c5ca788 100644 --- a/server-api/migrations/2020-03-11-005217_channels/up.sql +++ b/server-api/migrations/2020-03-11-005217_channels/up.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS `channels` ( `id` BIGINT UNSIGNED NOT NULL auto_increment, - `name` VARCHAR(255) NOT NULL, + `name` UNIQUE VARCHAR(255) NOT NULL, `description` VARCHAR(1024), `kind` INTEGER NOT NULL, PRIMARY KEY(`id`), UNIQUE KEY(`name`) diff --git a/server-api/src/auth.rs b/server-api/src/auth.rs index 8865fe9..afdf428 100644 --- a/server-api/src/auth.rs +++ b/server-api/src/auth.rs @@ -1,59 +1,56 @@ -use mysql_async::Pool; +use bcrypt; +use mysql_async::{Conn, Pool}; use mysql_async::prelude::{params, Queryable}; -use crate::db_types::{UBigInt, VarChar}; +use crate::db_types::{BigInt, Integer, UBigInt, VarChar}; use crate::routes; +// used when we create a new users for the first time pub const BCRYPT_COST: u32 = 14; pub enum AuthReason { - Good, //passed regular check + Good, //passed regular check OpenAuth, // route does not require auth - NoKey, + NoKey, // key missing + BadKey, // key is bad } -fn open_route(path: &str) -> bool { - return path == routes::INVITE_JOIN +fn valid_user(secret: &str, row: &Option<(VarChar, VarChar, BigInt, Integer, UBigInt)>) -> bool { + match row { + Some(row) => { + match bcrypt::verify(secret, &row.0) { + Ok(result) => result, + Err(_) => return false + } + }, + _ => return false + } } -pub async fn wall_entry(path: &str, pool: &Pool, params: &mut serde_json::Value) -> Result { - use serde_json::json; + +pub async fn wall_entry(path: &str, pool: &Pool, params: &serde_json::Value) -> Result { // Start by Checking if the api key is in our keystore - if open_route(path) { + if routes::is_open(path) { Ok(AuthReason::OpenAuth) } else { - if let Some(key) = params.get("secret") { - let key_str = key.as_str(); - let conn = pool.get_conn().await?; - // (id, name, secret) - type RowType = Option<(UBigInt, VarChar)>; - let db_result: Result<(_, RowType), mysql_async::error::Error> = conn - .first_exec(r"SELECT id, name FROM members WHERE secret = :secret ", mysql_async::params!{ "secret" => key_str}) - .await; - - match db_result { - Ok((_, row)) => { - match row{ - Some(user_row) => { - params["userid"] = json!(user_row.0); - params["username"] = json!(user_row.1); - Ok(AuthReason::Good) - }, - None => Ok(AuthReason::NoKey) - } + match (params.get("id"), params.get("secret")) { + (Some(id_v), Some(secret_v)) => { + let id = id_v.as_u64().unwrap(); + let secret = secret_v.as_str().unwrap(); + let conn = pool.get_conn().await?; + let db_tup: (Conn, Option<(VarChar, VarChar, BigInt, Integer, UBigInt)>) = conn.first_exec( + "SELECT secret, name, joindate, status, permissions FROM members WHERE id = :id", + mysql_async::params!{"id" => id}).await?; + if valid_user(secret, &db_tup.1) { + Ok(AuthReason::Good) } - Err(e) => { - println!("\tIssue fetching auth data {:?}", e); - Ok(AuthReason::NoKey) + else { + Ok(AuthReason::BadKey) } + }, + _ => { + Ok(AuthReason::NoKey) } - - //let (_con, row): (_, Option<(UBigInt, VarChar)>) = conn - // .first_exec(r"SELECT userid, name FROM keys WHERE secret = :secret ", mysql_async::params!{ "secret" => key}) - // .await; - } - else { - Ok(AuthReason::NoKey) } } } @@ -66,6 +63,37 @@ pub fn generate_secret() -> String { use base64::{encode_config, URL_SAFE}; let mut buf: Vec = vec![0;64]; - getrandom(&mut buf); + getrandom(&mut buf).unwrap(); encode_config(buf,URL_SAFE) } + + + +#[cfg(test)] +mod auth_tests { + use crate::testing::get_pool; + use serde_json::Value; + use mysql_async::prelude::Queryable; + + #[tokio::test] + async fn missing_key() { + let pool = get_pool(); + let conn = pool.get_conn().await.unwrap(); + let conn = conn.drop_exec( + r#"INSERT INTO members (id, secret, name, joindate, status,permissions) + VALUES(1, "abc", "bsname", 1,0,0) + "#, + ()).await.unwrap(); + + let params: Value = serde_json::from_str(r#" + { + "id": 1 + } + "#).unwrap(); + + let result = super::wall_entry("/channels/list", &pool, ¶ms).await; + let _ = conn.drop_exec(r#"DELETE FROM members WHERE secret = "abc""#,()).await; + assert_eq!(true, result.is_ok()); + } + +} diff --git a/server-api/src/channels.rs b/server-api/src/channels.rs index 81c3a49..fdb537d 100644 --- a/server-api/src/channels.rs +++ b/server-api/src/channels.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use hyper::{StatusCode, Response, Body}; use hyper::header::HeaderValue; @@ -10,6 +8,7 @@ use mysql_async::prelude::{params, Queryable}; use serde_json::Value; use crate::db_types::{UBigInt, VarChar, Integer}; +use crate::common; #[derive(Debug)] pub enum ChannelType { @@ -35,28 +34,14 @@ impl ChannelType { } } - - // whole ass function exists because serde_json is a walking pos - pub fn from_i64_opt(x: Option) -> ChannelType { - if let Some(i) = x { - match i { - 1 => ChannelType::Voice, - 2 => ChannelType::Text, - _ => ChannelType::Undefined - } - } - else { - ChannelType::Undefined - } - } } // Primary way of interpretting sql data on our channels table pub struct Channel { - id: u64, - name: String, - description: String, - kind: ChannelType + pub id: u64, + pub name: String, + pub description: String, + pub kind: ChannelType } #[derive(Debug)] @@ -180,9 +165,10 @@ pub async fn create_channel(pool: &Pool, response: &mut Response, params: match insert_channel(pool, name, desc, kind).await { // Server Errors are generally _ok_ to reveal in body I suppose Err(Error::Server(se)) => { - *response.status_mut() = StatusCode::BAD_REQUEST; - let b = format!("Server code: {}\nServer Message:{}", se.code, se.message); - *response.body_mut() = Body::from(b); + common::db_err_response_body(response, se); + //*response.status_mut() = StatusCode::BAD_REQUEST; + //let b = format!("Server code: {}\nServer Message:{}", se.code, se.message); + //*response.body_mut() = Body::from(b); }, // generic errors get a 500 Err(_) => { @@ -209,7 +195,7 @@ pub async fn delete_channel(pool: &Pool, response: &mut Response, params: match db_delete_channel(pool, name).await { Ok(_) => *response.status_mut() = StatusCode::OK, Err(e) => { - println!("delete_chanel sql error :\n{}", e); + *response.body_mut() = Body::from(format!("delete_chanel sql error :\n{}", e)); } } } @@ -217,3 +203,88 @@ pub async fn delete_channel(pool: &Pool, response: &mut Response, params: *response.status_mut() = StatusCode::BAD_REQUEST; } } + + +#[cfg(test)] +mod channels_tests { + use crate::testing::{get_pool, hyper_resp}; + use serde_json::Value; + use hyper::StatusCode; + const DUMMY_TRANSIENT_CHANNEL: &'static str = "sample channel"; + + #[tokio::test] + async fn list_all_channels_good() { + // Generation of data + let p = get_pool(); + let mut resp = hyper_resp(); + // @params: none + // Collection of data + super::list_channels(&p, &mut resp).await; + + // Analysis + assert_eq!(StatusCode::OK, resp.status()); + println!("list_all_channels_good : \t{:?}", resp.body()); + let _ = p.disconnect().await; + } + + #[tokio::test] + async fn delete_and_create_channel_good() { + use chrono::Utc; + let p = get_pool(); + let mut resp = hyper_resp(); + // @params: name + kind + [description] + let cname_id = Utc::now(); + let params: Value = serde_json::from_str(&format!(r#" + {{ + "name": "{}-{}", + "kind": 2, + "description": "some random bs" + }} + "#, DUMMY_TRANSIENT_CHANNEL, cname_id)).unwrap(); + + super::create_channel(&p, &mut resp, params).await; + + println!("CREATE CHANNEL: {:?}", resp.body()); + assert_eq!(StatusCode::OK, resp.status()); + + + // clean up and hopefully delete the channel properly + let mut resp_delete = hyper_resp(); + let params_delete: Value = serde_json::from_str(&format!(r#" + {{ + "name": "{}-{}" + }} + "#, DUMMY_TRANSIENT_CHANNEL, cname_id)).unwrap(); + println!("Parameters: {}", params_delete); + super::delete_channel(&p, &mut resp_delete, params_delete).await; + + println!("Body: {:?}", resp.body()); + let _ = p.disconnect().await; + } + + #[tokio::test] + async fn delete_channel_missing_name() { + let p = get_pool(); + let mut resp = hyper_resp(); + // this endpoint is super lenient for some reason btw + let param: Value = serde_json::from_str("{}").expect("JSON is not written correctly"); + + super::delete_channel(&p, &mut resp, param).await; + + assert_eq!(StatusCode::BAD_REQUEST, resp.status()); + } + + #[tokio::test] + async fn delet_channel_non_real_channel() { + let p = get_pool(); + let mut resp = hyper_resp(); + // this endpoint is super lenient for some reason btw + let param: Value = serde_json::from_str(r#"{ + "name": "this channel doesn't exist" + }"#).expect("JSON is not written correctly"); + + super::delete_channel(&p, &mut resp, param).await; + + assert_eq!(StatusCode::OK, resp.status()); + } +} diff --git a/server-api/src/common.rs b/server-api/src/common.rs new file mode 100644 index 0000000..72f6780 --- /dev/null +++ b/server-api/src/common.rs @@ -0,0 +1,38 @@ +use mysql_async::error::ServerError; +use hyper::{Body, Response, StatusCode}; +use serde::Serialize; +use serde_json::to_string as json; + +#[derive(Serialize)] +struct GenericErrData { + status: u16, + message: &'static str, + note: &'static str, +} + + +pub fn db_err_response_body(response: &mut Response, err: ServerError) { + *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + match err.code { + // Duplicate unique value was (tried to be) inserted + 1062 => { + + let s = json(&GenericErrData { + status: 1062, + message: "Duplicate key was inserted and failed", + note: "Check parameters given" + }).unwrap(); + *response.body_mut() = Body::from(s); + }, + // Generic errors + _ => { + let s = json(&GenericErrData{ + status: 500, + message: "Generic Backend error", + note:"" + }).unwrap(); + *response.body_mut() = Body::from(s); + } + } +} + diff --git a/server-api/src/db_types.rs b/server-api/src/db_types.rs index e1d15ee..4969457 100644 --- a/server-api/src/db_types.rs +++ b/server-api/src/db_types.rs @@ -1,13 +1,7 @@ pub type Integer = i32; -pub type UInteger = u32; pub type UBigInt = u64; pub type BigInt = i64; pub type VarChar = String; -pub enum DbError { - BadParam(&'static str), - Connection(&'static str), - Internal -} diff --git a/server-api/src/invites.rs b/server-api/src/invites.rs index 967fec1..29acb33 100644 --- a/server-api/src/invites.rs +++ b/server-api/src/invites.rs @@ -1,4 +1,5 @@ use serde_json::Value; +use serde::Serialize; use mysql_async; use mysql_async::{Conn, Pool}; @@ -8,11 +9,15 @@ use mysql_async::prelude::{params, Queryable}; use hyper::{Response, Body, StatusCode}; use chrono::Utc; -use rand::random; -struct InviteRow { - id: u64, - expires: u64, - uses: i32, + +use crate::db_types::BigInt; +use crate::members::{self, Member}; + +#[derive(Serialize)] +struct Invite { + id: BigInt, + uses: Option, // optional because some links are permanent + expires: bool, } /* * Error handling: @@ -20,104 +25,152 @@ struct InviteRow { * are of the enum mysql_async::error::Error */ -impl InviteRow { - pub fn new() -> InviteRow { - let dt = Utc::now() + chrono::Duration::minutes(30); - // TODO:[maybe] ensure no collisions by doing a quick database check here - let invite = InviteRow { - id: random::(), // hopefully there won't ever be collision with this size of pool - uses: 1, // default/hardcorded for now - expires: dt.timestamp() as u64 - }; - invite - } - pub fn from_tuple(tup: (u64, u64, i32)) -> InviteRow { - InviteRow { - id: tup.0, - expires: tup.1, - uses: tup.2, - } - } - - pub fn as_json_str(&self) -> String { - let id = format!("\"id\":{}", self.id); - let expires = format!("\"expires\":{}", self.expires); - let uses = format!("\"uses\":{}", self.uses); - - let mut data = String::from("{"); - data.push_str(&format!("{},", id)); - data.push_str(&format!("{},", expires)); - data.push_str(&format!("{}}}", uses)); - data - } -} - -async fn get_invite_by_code(pool: &Pool, value: Option<&str>) -> Result, Error> { - if let Some(val) = value { - let conn = pool.get_conn().await?; - let db_row_result: (Conn, Option<(u64, u64, i32)>) = conn - .first_exec(r"SELECT * FROM", mysql_async::params!{"code"=>val}) - .await?; - if let Some(tup) = db_row_result.1 { - Ok(Some(InviteRow::from_tuple(tup))) - } - else { - // basically nothing was found but nothing bad happened - Ok(None) - } - } - // again db didn't throw a fit but we don't have a good input - else {Ok(None)} -} - -async fn record_invite_usage(pool: &Pool, data: &InviteRow) -> Result<(), Error>{ +async fn valid_invite(pool: &Pool, id: BigInt) -> Result{ /* - * By this this is called we really don't care about what happens as we've - * already been querying the db and the likely hood of this seriously failing - * is low enough to write a wall of text and not a wall of error handling code - */ + * Fetches an invite from the database to check for validity + */ let conn = pool.get_conn().await?; - let _db_result = conn - .prep_exec(r"UPDATE invites SET uses = :uses WHERE id = :id", mysql_async::params!{ - "uses" => data.uses - 1, - "id" => data.id - }).await?; + let db_fetch_result: (Conn, Option<(Option, bool)>) = + conn.first_exec("SELECT uses, expires FROM invites WHERE id = :id", + params!{"id" => id}).await?; + + if let Some(row) = db_fetch_result.1 { + // if expires at all + if row.1 { + let now = Utc::now().timestamp(); + // old? + let mut status = now > id; + // used? + if row.0.is_some() && status == false { + status = row.0.unwrap() <= 0; // safe unwrap since we know its Some(_) + } + return Ok(status) + } + // no expiry date? no problem + return Ok(true); + } + // prolly not a real id + else { + return Ok(false); + } - Ok(()) } -pub async fn route_join_invite_code(pool: &Pool, response: &mut Response, params: Value) -> Result<(), Error> { - // First check that the code is there - if let Some(code) = params.get("code") { - if let Some(row) = get_invite_by_code(pool, code.as_str()).await? { - // since we have a row make sure the invite is valid - let now = Utc::now().timestamp() as u64; - // usable and expires in the future - if row.uses > 0 && row.expires > now { - record_invite_usage(pool, &row).await?; - // TODO: assign some actual data to the body - *response.status_mut() = StatusCode::OK; +async fn use_invite(pool: &Pool, code: Option) -> Option{ + /* + * Attempts to change the state of the current invite being provided + */ + use crate::perms::GENERAL_NEW; + let id = match code { + Some(id) => id, + None => 0 + }; + + if let Ok(valid) = valid_invite(pool, id).await { + if valid { + match members::insert_new_member(pool, "Anonymous".into(), GENERAL_NEW).await { + Ok(member) => return Some(member), + Err(_) => return None } } + else { + return None; + } } - else{ - *response.status_mut() = StatusCode::BAD_REQUEST; + else { + return None; } +} + +pub async fn join(pool: &Pool, response: &mut Response, params: Value) { + /* + * Main dispatcher for dealing with an attempted join via a given invide code + */ + let code = match params.get("invite-id") { + Some(val) => val.as_i64(), + None => None + }; + + match use_invite(&pool, code).await { + Some(new_account) => *response.body_mut() = Body::from(serde_json::to_string(&new_account).unwrap()), + None => { + } + } +} + +async fn insert_new_invite(pool: &Pool, invite: &Invite) -> Result<(), Error>{ + let conn = pool.get_conn().await?; + conn.prep_exec( + "INSERT INTO invites (id, uses, expires) + VALUES (:id, :uses, :expires)", params!{ + "id" => invite.id, + "uses" => invite.uses, + "expires" => invite.expires + }).await?; Ok(()) } -pub async fn create_invite(pool: &Pool, response: &mut Response) -> Result<(), Error> { - let invite = InviteRow::new(); - let conn = pool.get_conn().await?; - conn.prep_exec(r"INSERT INTO invites (id, expires, uses) VALUES (:id, :expires, :uses", - mysql_async::params!{ - "id" => invite.id, - "expires" => invite.expires, - "uses" => invite.uses, - }).await?; +pub async fn create(pool: &Pool, response: &mut Response, params: Value) { + /* + * Creates a new invite + */ - *response.body_mut() = Body::from(invite.as_json_str()); - *response.status_mut() = StatusCode::OK; - Ok(()) -} \ No newline at end of file + let use_count = match params.get("uses") { + Some(val) => val.as_i64(), + None => None + }; + + // TODO: remove the unwrap + let expires = match params.get("expires") { + Some(val) => val.as_bool().unwrap_or(true), + None => true + }; + + let invite = Invite { + id: (Utc::now() + chrono::Duration::minutes(30)).timestamp(), + uses: use_count, + expires: expires + }; + + match insert_new_invite(&pool, &invite).await { + Ok(_) => {}, + Err(mysqle) => { + println!("\tINVITES::CREATE::ERROR: {}", mysqle); + *response.status_mut() = StatusCode::BAD_REQUEST; + } + } +} + + +#[cfg(test)] +mod invites_test { + /* + * INVITE CREATION + * Good - Bad - Malicious + */ + + use crate::testing::{get_pool, hyper_resp}; + use hyper::StatusCode; + use serde_json::Value; + + #[tokio::test] + async fn create_invite_good() { + // Generation of data + let p = get_pool(); + let mut resp = hyper_resp(); + // expected params + let params: Value = serde_json::from_str(r#" + { + "uses": 3, + "expire": null + } + "#).unwrap(); + + // Collection + super::join(&p, &mut resp, params).await; + let _ = p.disconnect().await; + + assert_eq!(StatusCode::OK, resp.status()); + } +} diff --git a/server-api/src/main.rs b/server-api/src/main.rs index c7b478a..dd9dd5f 100644 --- a/server-api/src/main.rs +++ b/server-api/src/main.rs @@ -22,20 +22,21 @@ use mysql_async::Pool; use dotenv::dotenv; use clap::{Arg, App}; +use auth::AuthReason; mod auth; -use auth::AuthReason; mod routes; mod invites; mod channels; - mod members; - -mod messages; -mod http_params; mod perms; +mod messages; + +mod http_params; mod db_types; +mod common; +mod testing; const NO_ERR: u16 = 0; const CONFIG_ERR: u16 = 1; @@ -45,20 +46,13 @@ async fn route_dispatcher(pool: &Pool, resp: &mut Response, meth: &Method, // At some point we should have some way of hiding this obnoxious complexity use routes::resolve_dynamic_route; match (meth, path) { - (&Method::GET, routes::INVITE_JOIN) => { - if let Err(_) = invites::route_join_invite_code(pool, resp, params).await { - *resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - } - }, - (&Method::GET, routes::INVITE_CREATE) => { - if let Err(_) = invites::create_invite(pool, resp).await { - *resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - } - }, + /* INVITES */ + (&Method::GET, routes::INVITE_CREATE) => invites::create(pool, resp, params).await, + /* CHANNELS */ (&Method::GET, routes::CHANNELS_LIST) => channels::list_channels(pool, resp).await, (&Method::POST, routes::CHANNELS_CREATE) => channels::create_channel(pool, resp, params).await, (&Method::POST, routes::CHANNELS_DELETE) => channels::delete_channel(pool, resp, params).await, - + /* MESSAGING */ (&Method::POST, routes::MESSAGE_SEND) => messages::send_message(pool, resp, params).await, _ => { // We attempt dynamic routes as fallback for a few reasons @@ -69,9 +63,13 @@ async fn route_dispatcher(pool: &Pool, resp: &mut Response, meth: &Method, // Computatinoal bounds are really of no concern with this api since // we're not doing any heavy calculations at any point if let Some(route) = resolve_dynamic_route(path) { - *resp.status_mut() = StatusCode::OK; - println!("\tStatic part: {}", route.base); - println!("\tDynamic part: {}", route.dynamic); + match (meth, route.base.as_str()) { + (&Method::GET, routes::DYN_JOIN) => invites::join(pool, resp, params).await, + _ => { + println!("\tNOT FOUND: {}: {}", meth, path); + *resp.status_mut() = StatusCode::NOT_FOUND + } + } } else { println!("\tNOT FOUND: {}: {}", meth, path); @@ -98,7 +96,7 @@ async fn main_responder(request: Request) -> Result, hyper: // Deal with permissions errors at this point match auth_result { OpenAuth | Good => route_dispatcher(&pool, &mut response, &method, path, params).await, - NoKey => { + NoKey | BadKey => { println!("\tAUTH: NoKey/BadKey"); *response.status_mut() = StatusCode::UNAUTHORIZED }, diff --git a/server-api/src/members.rs b/server-api/src/members.rs index 944e0b5..2e0c559 100644 --- a/server-api/src/members.rs +++ b/server-api/src/members.rs @@ -1,12 +1,10 @@ use chrono::Utc; -use hyper::{Body, Response, StatusCode}; -use hyper::header::{HeaderName, HeaderValue}; -use mysql_async::{Conn, Pool, error::Error as MySqlError}; +use mysql_async::{Conn, Pool, error::Error}; use mysql_async::prelude::{params, Queryable}; -use serde_json::Value; use serde::Serialize; use crate::db_types::{UBigInt, BigInt, Integer, VarChar}; +use crate::auth; #[derive(Serialize)] pub struct Member { @@ -18,29 +16,16 @@ pub struct Member { pub permissions: UBigInt, } -struct InsertableMember<'n> { - name: &'n str, - permissions: u64, -} -impl<'n> InsertableMember<'n> { - fn new(name: &'n str) -> InsertableMember<'n> { - use crate::perms::{JOIN_VOICE, SEND_MESSAGES}; - let now: BigInt = Utc::now().timestamp_millis(); - let default_perms = JOIN_VOICE | SEND_MESSAGES; - InsertableMember { - name: name, - permissions: default_perms, - } - } -} - - -pub async fn insert_new_member(p: &Pool, name: VarChar, perms: u64) -> Result { +pub async fn insert_new_member(p: &Pool, name: VarChar, perms: u64) -> Result { use crate::auth::generate_secret; let conn: Conn = p.get_conn().await?; - let secret: String = generate_secret(); + let secret_raw: String = generate_secret(); + let secret = match bcrypt::hash(&secret_raw, auth::BCRYPT_COST) { + Ok(value) => value, + Err(e) => panic!("\tCould not insert member due to bcrypt failure:\n\t\t{}",e) + }; let now: BigInt = Utc::now().timestamp(); let conn = conn.drop_exec( @@ -63,36 +48,10 @@ pub async fn insert_new_member(p: &Pool, name: VarChar, perms: u64) -> Result, params: Value) { - /* - * @name: string => desired default name - */ - use crate::perms; - let default_name = serde_json::json!("NewUser"); - let name = params.get("name") - .unwrap_or(&default_name) - .as_str().unwrap_or("NewUser"); - - let pre_mem = InsertableMember::new(name); - match insert_new_member(p, name.to_string(), perms::GENERAL_NEW).await { - Ok(new_member) => { - *resp.status_mut() = StatusCode::OK; - let json_hdr_name = HeaderName::from_static("Content-Type"); - let json_hdr_val = HeaderValue::from_static("application/json"); - resp.headers_mut().insert(json_hdr_name, json_hdr_val); - *resp.body_mut() = Body::from(serde_json::to_string(&new_member).unwrap()); - }, - Err(_) => { - *resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - *resp.body_mut() = Body::from("Could not process input"); - } - } -} diff --git a/server-api/src/messages.rs b/server-api/src/messages.rs index 399f3b4..ffe0f6f 100644 --- a/server-api/src/messages.rs +++ b/server-api/src/messages.rs @@ -43,7 +43,7 @@ pub async fn send_message(pool: &Pool, response: &mut Response, params: Va let content_r = params.get("content"); let channel_name_r = params.get("channel"); // auth module guarantees this will be there in the correct form - let author = params.get("userid") + let author = params.get("id") .unwrap().as_u64().unwrap(); match (content_r, channel_name_r) { @@ -75,3 +75,74 @@ pub async fn send_message(pool: &Pool, response: &mut Response, params: Va } } } + +#[cfg(test)] +mod messaging_tests { + use crate::testing::{get_pool, hyper_resp}; + use serde_json::Value; + use hyper::StatusCode; + + #[tokio::test] + async fn send_message_test_missing_channel() { + /* + * Attempt to send a message i na channel that does not exist + */ + let p = get_pool(); + let mut resp = hyper_resp(); + + let params: Value = serde_json::from_str(r#" + { + "channel": "this does not exist", + "content": "bs message", + "id": 420 + } + "#).unwrap(); + + super::send_message(&p, &mut resp, params).await; + + assert_ne!(StatusCode::OK, resp.status()); + } + + #[tokio::test]#[ignore] + async fn send_message_good() { + use crate::members::insert_new_member; + use crate::perms::GENERAL_NEW; + use mysql_async::params; + use mysql_async::prelude::Queryable; + use crate::testing::tmp_channel_params; + + let p = get_pool(); + let mut resp = hyper_resp(); + + let tmp_chan = tmp_channel_params(&p, "sample").await; + + const TMP_NAME: &'static str = "bs user"; + let temp_member = insert_new_member(&p, TMP_NAME.into(), GENERAL_NEW).await.unwrap(); + + + let params: Value = serde_json::from_str(&format!(r#" + {{ + "id": {}, + "channel": "{}", + "content": "bs message" + }} + "#, temp_member.id, tmp_chan.name)).unwrap(); + + super::send_message(&p, &mut resp, params).await; + + if resp.status() == StatusCode::BAD_REQUEST { + panic!("{:?}", resp.body()); + } + + // Destroy the the message and the user that we created + let conn = match p.get_conn().await { + Ok(c) => c, + Err(e) => panic!("Could not get connection to db during send_message_good:\nIssue:\t{}", e) + }; + + let conn = conn.drop_exec("DELETE FROM messages WHERE author_id = :id", params!{"id" => temp_member.id}).await.unwrap(); + let conn = conn.drop_exec("DELETE FROM members WHERE id = :id", params!{"id" => temp_member.id}).await.unwrap(); + let _ = conn.drop_exec("DELETE FROM channels WHERE name = :name", params!{"name" => tmp_chan.name}).await; + } +} + diff --git a/server-api/src/routes.rs b/server-api/src/routes.rs index 054c839..391bffb 100644 --- a/server-api/src/routes.rs +++ b/server-api/src/routes.rs @@ -1,4 +1,3 @@ -pub const INVITE_JOIN: &'static str = "/invite/join"; // requires @code pub const INVITE_CREATE: &'static str = "/invite/create"; // requires none pub const CHANNELS_LIST: &'static str = "/channels/list"; // requires none @@ -7,9 +6,15 @@ pub const CHANNELS_DELETE: &'static str = "/channels/delete"; // requires @name pub const MESSAGE_SEND: &'static str = "/message/send"; // requires @content -const DYNAMIC_ROUTE_BASES: [&'static str;1] = [ - "/invites", +pub const SERVER_META: &'static str = "/meta"; // open + +// potentially adding more bases later +pub const DYNAMIC_ROUTE_BASES: [(&'static str, bool);3] = [ + ("/join", true), // open + ("/public", true), // open : valid sections := neighbors|rules|description + ("/user", false), // TODO: valid sections := /meta/|/dm/ ]; +pub const DYN_JOIN: &'static str = DYNAMIC_ROUTE_BASES[0].0; pub struct DynRoute { pub base: String, @@ -21,9 +26,9 @@ pub fn resolve_dynamic_route(uri: &str) -> Option { let mut valid = false; let mut base_ref = ""; for base in DYNAMIC_ROUTE_BASES.iter() { - if uri.starts_with(base) { + if uri.starts_with(base.0) { valid = true; - base_ref = base; + base_ref = base.0; break; } } @@ -37,3 +42,19 @@ pub fn resolve_dynamic_route(uri: &str) -> Option { None } } + +pub fn is_open(path: &str) -> bool { + /* + * Simple interface for determining if a route/base is open + * i.e. requires authentication or not + */ + let mut ret = path == SERVER_META; + for route in DYNAMIC_ROUTE_BASES.iter() { + if route.1 == true || ret == true{ + ret = true; + break; + } + } + return ret; +} + diff --git a/server-api/src/testing/mod.rs b/server-api/src/testing/mod.rs new file mode 100644 index 0000000..31cdd81 --- /dev/null +++ b/server-api/src/testing/mod.rs @@ -0,0 +1,38 @@ +// Functions which are only really useful for the unit tests but which show up +// constantly in the tests themselves + +#[cfg(test)] +pub fn get_pool() -> mysql_async::Pool { + use dotenv::dotenv; + use mysql_async::Pool; + + dotenv().ok(); + return Pool::new(&std::env::var("DATABASE_URL").unwrap()) +} + +#[cfg(test)] +pub fn hyper_resp() -> hyper::Response { + use hyper::{Response, Body}; + + Response::new(Body::empty()) +} + + + +#[cfg(test)] +pub async fn tmp_channel_params(p: &mysql_async::Pool, chan_name: &'static str) -> crate::channels::Channel { + use crate::channels::{Channel, ChannelType}; + use mysql_async::{params, prelude::Queryable}; + + let conn = p.get_conn().await.unwrap(); + let _ = conn.prep_exec( + "INSERT INTO channels (name, description, kind) VALUES (:name, :description, :kind)", + params!{"name" => chan_name, "kind" => 0, "description" => "no description for testing"}).await; + + Channel { + id: 0, + name: chan_name.into(), + kind: ChannelType::Text, + description: "no description for testing".into() + } +} diff --git a/server-api/tests/common.sh b/server-api/tests/common.sh deleted file mode 100644 index a996878..0000000 --- a/server-api/tests/common.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -# Details for our bs user when testing things -export id=1 -export secret=secret -export name=godrah -export joindate=123 -export status=1 -export permissions=69 - -export simple_key='{"secret":"secret"}' - -export url='localhost:8888' - -export GET='-X GET' -export POST='-X POST' - -export arrows='>>>>>' -export line='=============' - -export crl='curl --silent -i' diff --git a/server-api/tests/main.sh b/server-api/tests/main.sh deleted file mode 100644 index c790e9a..0000000 --- a/server-api/tests/main.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# This script is basically just a convenient launch pad script for running all -# the tests at once -# Most tests should be runnable by doing ./script.sh name_of_test - - -# First the 'good' input tests -# This is to say that we get input that: -# 1. is properly formatted -# 2. has all the info we need & none we don't -# 3. has basically nothing malicious about it - -log_result() { - name=$1 - expect=$2 - actual=$3 - result=$4 - - green='\033[1;32m' - red='\033[1;91m' - nc='\033[0m' - if [ $expect != $actual ];then - echo -e ${red}${name}${nc} ${green}$expect ${red}$actual${nc} - echo -e ${red}==========${nc} - echo "$result" | sed 's/^/\t/g' - echo -e ${red}==========${nc} - else - echo -e ${green}${name}${nc} $expect $actual - if [ ! -z "$_show_body" ];then - echo ========== - echo "$result" | sed 's/^/\t/g' - echo ========== - fi - fi -} - -if [ "$1" = "body" ];then - export _show_body=1 -fi - -source ./common.sh -export -f log_result -echo TestName ExpectedCode ActualCode - -bash ./verify_basic_cases.sh - -bash ./verify_err_cases.sh - -bash ./verify_mal_cases.sh diff --git a/server-api/tests/status.md b/server-api/tests/status.md deleted file mode 100644 index 8211e23..0000000 --- a/server-api/tests/status.md +++ /dev/null @@ -1,19 +0,0 @@ -# State of Tests - -Here is a description of what is passing and what is failing where - -## Full passes - -_Nothing for now_ - -## Basic Passes - -_Nothing for now_ - -## Err Passes - -_Nothing for now_ - -## Mal Passes - -_Nothing for now_ diff --git a/server-api/tests/todo.md b/server-api/tests/todo.md deleted file mode 100644 index af47f70..0000000 --- a/server-api/tests/todo.md +++ /dev/null @@ -1,25 +0,0 @@ -Testing happens on a per-modules basis - -# Messages - -All required, none finished - -# Channels - -* list\_all\_channels - - Good and bad users done - - Malicious users not done - -* create\_channel - sql driver is totally fucked m80 - -* delete\_channel - not ready for testing - -* set\_channel\_attribute - not ready for testing - -# Invites - -* create - not tested - -* use - not tested diff --git a/server-api/tests/verify_basic_cases.sh b/server-api/tests/verify_basic_cases.sh deleted file mode 100644 index b23e796..0000000 --- a/server-api/tests/verify_basic_cases.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -# Available tests marked with `TEST` - ez grep usage - -active_tests='list_all_channels create_channel delete_channel -send_message -' - -list_all_channels() { # TEST - result=$(curl --silent -i $GET $url/channels/list -d $simple_key) - code=$(echo "$result" | grep HTTP\/1.1 | awk '{print $2}') - log_result "good_list_all_channels" 200 $code "$result" -} - -create_channel() { - kv='{"secret":"secret", "name":"sample", "kind":2, "description":"some bs description"}' - result=$($crl $POST $url/channels/create -d "$kv") - code=$(echo "$result" | grep HTTP\/1.1 | awk '{print $2}') - log_result good_create_channel 200 $code "$result" -} - -delete_channel() { - kv='{"secret":"secret", "name":"sample"}' - result=$($crl $POST $url/channels/delete -d "$kv") - code=$(echo "$result" | grep HTTP\/1.1 | awk '{print $2}') - log_result good_delete_channel 200 $code "$result" -} - -send_message() { - # ignoring the reaction to this as its not _completely_ relevant for this test - $crl $POST $url/channels/create -d '{"secret":"secret","name":"msgchannel","kind":2}' > /dev/null - - # now we can try sending the right parameters to send a basic message - kv='{"secret":"secret", "content":"message sample", "channel":"msgchannel"}' - result=$($crl $POST $url/message/send -d "$kv") - code=$(echo "$result" | grep HTTP\/1.1 | awk '{print $2}') - # non-existant channel for now but whatever ignore for now - log_result good_send_message 200 $code "$result" -} - -# Dispatcher to run our tests -if [ -z $1 ];then - for cmd in $active_tests;do - $cmd - done -else - for cmd in $@;do - $cmd - echo '\n'$? - done -fi diff --git a/server-api/tests/verify_err_cases.sh b/server-api/tests/verify_err_cases.sh deleted file mode 100644 index 35bdffa..0000000 --- a/server-api/tests/verify_err_cases.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -active_tests='list_channels_no_key list_channels_bad_key delete_channel_missing_param delete_channel_no_channel' - -list_channels_no_key() { - result=$($crl $GET $url/channels/list) - code=$(echo "$result" | grep HTTP\/1.1 | awk '{print $2}') - log_result list_channels_no_key 401 $code "$result" -} - -list_channels_bad_key() { - result=$($crl $GET $url/channels/list -d '{"secret":"something else"}') - code=$(echo "$result" | grep HTTP\/1.1 | awk '{print $2}') - log_result list_channels_bad_key 401 $code "$result" -} - -delete_channel_missing_param() { - kv='{"secret":"secret"}' - result=$($crl $POST $url/channels/delete -d "$kv") - code=$(echo "$result" | grep HTTP\/1.1 | awk '{print $2}') - log_result delete_channel_missing_param 400 $code "$result" -} - -delete_channel_no_channel() { - # Should 200 as the api just drops the result - kv='{"secret":"secret", "name":"yes"}' - result=$($crl $POST $url/channels/delete -d "$kv") - code=$(echo "$result" | grep HTTP\/1.1 | awk '{print $2}') - log_result delete_channel_no_channel_found 200 $code "$result" -} - -# Dispatcher to run our tests -if [ -z $1 ];then - for cmd in $active_tests;do - $cmd - done -else - for cmd in $@;do - $cmd - done -fi diff --git a/server-api/tests/verify_mal_cases.sh b/server-api/tests/verify_mal_cases.sh deleted file mode 100644 index 7c97afd..0000000 --- a/server-api/tests/verify_mal_cases.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -active_tests='malicious_list_channels' - -malicious_list_channels() { - key='{"secret": ";-- select * from members;"}' - result=$(curl --silent -i -X GET localhost:8888/channels/list -d '{"secret": "-- select * from members;"}') - code=$(echo "$result" | grep HTTP\/1.1 | awk '{print $2}') - log_result malicious_list_channels 401 $code "$result" -} - - -# Dispatcher to run our tests -if [ -z $1 ];then - for cmd in $active_tests;do - $cmd - done -else - for cmd in $@;do - $cmd - echo '\n'$? - done -fi diff --git a/server-api/todo b/server-api/todo deleted file mode 100644 index 537a1fd..0000000 --- a/server-api/todo +++ /dev/null @@ -1,28 +0,0 @@ -for now we'll acheive these things via some tests -users: - - create new users via some tests - - search users - we should be able to ask for the first _n_ users in a server - this _n_ value will be 250 for now since user names should be pretty short and we're only going to care about the usernames+id's - - - update - whenever a user wants to change their display name or something on the server - - - remove users - whenever a user wants to be removed from a server - all we need for this one is the userid for that server then we should remove them - -# todo for later but these schemas are there for sake of brevity and completeness -# they're just not being dealth with atm -channels: - -# mod::invites - -Better random number generation in use_invite function - -# Walls - -Right now there's literally 0 security checks in place and thats because: -1. im lazy with that at the moment -2. if the underlying logic is fucked then the security won't do anything -3. finally the code is built to add things onto it \ No newline at end of file