Merge branch 'testing' into master
This commit is contained in:
commit
797042b97a
@ -1,25 +1,35 @@
|
|||||||
image: shockrah/freechat:0.3
|
image: shockrah/freechat:0.3
|
||||||
|
variables:
|
||||||
|
CARGO_HOME: $CI_PROJECT_DIR/.cargo
|
||||||
|
|
||||||
debug-build:
|
cache:
|
||||||
|
key: "$CI_JOB_NAME"
|
||||||
|
untracked: true
|
||||||
|
paths:
|
||||||
|
- $CARGO_HOME
|
||||||
|
- $CI_PROJECT_DIR/server/target
|
||||||
|
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- export PATH="$CARGO_HOME/bin:$PATH"
|
||||||
|
|
||||||
|
build-release:
|
||||||
stage: build
|
stage: build
|
||||||
only:
|
only:
|
||||||
- testing
|
- testing
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- cd server/
|
- cd server/
|
||||||
- cargo build
|
|
||||||
- diesel setup --database-url $DATABASE_URL
|
- diesel setup --database-url $DATABASE_URL
|
||||||
|
- cargo build --release
|
||||||
|
|
||||||
|
|
||||||
|
api-test:
|
||||||
basic-test:
|
|
||||||
stage: test
|
stage: test
|
||||||
|
needs: ["build-release"]
|
||||||
only:
|
only:
|
||||||
- testing
|
- testing
|
||||||
script:
|
script:
|
||||||
- cd server/
|
- cd server/
|
||||||
- cargo run -- -s&
|
- cargo test --release
|
||||||
- fc_id=$!
|
|
||||||
- cd tests/
|
|
||||||
- bash ./main.sh body
|
|
||||||
- kill ${fc_id}
|
|
||||||
|
|
||||||
|
1
server-api/.gitignore
vendored
1
server-api/.gitignore
vendored
@ -3,3 +3,4 @@ target
|
|||||||
|
|
||||||
static/css/
|
static/css/
|
||||||
dev-sql/
|
dev-sql/
|
||||||
|
diesel.toml
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
-- @id : id of the invite
|
-- @id : id of the invite which is also its kill date
|
||||||
|
|
||||||
-- @expires : unix timestamp of when that invite expries
|
|
||||||
-- can be set to null which means it never expires
|
|
||||||
|
|
||||||
-- @uses : can be null which means it doesn't have a use limit
|
-- @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` (
|
CREATE TABLE IF NOT EXISTS `invites` (
|
||||||
`id` bigint UNSIGNED NOT NULL,
|
`id` BIGINT UNIQUE NOT NULL,
|
||||||
`expires` bigint,
|
`uses` BIGINT,
|
||||||
`uses` integer,
|
`expires` BOOLEAN NOT NULL,
|
||||||
`max_uses` integer,
|
|
||||||
PRIMARY KEY( `id` )
|
PRIMARY KEY( `id` )
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
CREATE TABLE IF NOT EXISTS `channels` (
|
CREATE TABLE IF NOT EXISTS `channels` (
|
||||||
`id` BIGINT UNSIGNED NOT NULL auto_increment,
|
`id` BIGINT UNSIGNED NOT NULL auto_increment,
|
||||||
`name` VARCHAR(255) NOT NULL,
|
`name` UNIQUE VARCHAR(255) NOT NULL,
|
||||||
`description` VARCHAR(1024),
|
`description` VARCHAR(1024),
|
||||||
`kind` INTEGER NOT NULL,
|
`kind` INTEGER NOT NULL,
|
||||||
PRIMARY KEY(`id`), UNIQUE KEY(`name`)
|
PRIMARY KEY(`id`), UNIQUE KEY(`name`)
|
||||||
|
@ -1,62 +1,59 @@
|
|||||||
use mysql_async::Pool;
|
use bcrypt;
|
||||||
|
use mysql_async::{Conn, Pool};
|
||||||
use mysql_async::prelude::{params, Queryable};
|
use mysql_async::prelude::{params, Queryable};
|
||||||
use crate::db_types::{UBigInt, VarChar};
|
use crate::db_types::{BigInt, Integer, UBigInt, VarChar};
|
||||||
|
|
||||||
use crate::routes;
|
use crate::routes;
|
||||||
|
|
||||||
|
// used when we create a new users for the first time
|
||||||
pub const BCRYPT_COST: u32 = 14;
|
pub const BCRYPT_COST: u32 = 14;
|
||||||
pub enum AuthReason {
|
pub enum AuthReason {
|
||||||
Good, //passed regular check
|
Good, //passed regular check
|
||||||
OpenAuth, // route does not require auth
|
OpenAuth, // route does not require auth
|
||||||
NoKey,
|
NoKey, // key missing
|
||||||
|
BadKey, // key is bad
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn open_route(path: &str) -> bool {
|
fn valid_user(secret: &str, row: &Option<(VarChar, VarChar, BigInt, Integer, UBigInt)>) -> bool {
|
||||||
return path == routes::INVITE_JOIN
|
match row {
|
||||||
|
Some(row) => {
|
||||||
|
match bcrypt::verify(secret, &row.0) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(_) => return false
|
||||||
}
|
}
|
||||||
pub async fn wall_entry(path: &str, pool: &Pool, params: &mut serde_json::Value) -> Result<AuthReason, mysql_async::error::Error> {
|
},
|
||||||
use serde_json::json;
|
_ => return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wall_entry(path: &str, pool: &Pool, params: &serde_json::Value) -> Result<AuthReason, mysql_async::error::Error> {
|
||||||
// Start by Checking if the api key is in our keystore
|
// Start by Checking if the api key is in our keystore
|
||||||
if open_route(path) {
|
if routes::is_open(path) {
|
||||||
Ok(AuthReason::OpenAuth)
|
Ok(AuthReason::OpenAuth)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if let Some(key) = params.get("secret") {
|
match (params.get("id"), params.get("secret")) {
|
||||||
let key_str = key.as_str();
|
(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 conn = pool.get_conn().await?;
|
||||||
// (id, name, secret)
|
let db_tup: (Conn, Option<(VarChar, VarChar, BigInt, Integer, UBigInt)>) = conn.first_exec(
|
||||||
type RowType = Option<(UBigInt, VarChar)>;
|
"SELECT secret, name, joindate, status, permissions FROM members WHERE id = :id",
|
||||||
let db_result: Result<(_, RowType), mysql_async::error::Error> = conn
|
mysql_async::params!{"id" => id}).await?;
|
||||||
.first_exec(r"SELECT id, name FROM members WHERE secret = :secret ", mysql_async::params!{ "secret" => key_str})
|
if valid_user(secret, &db_tup.1) {
|
||||||
.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)
|
Ok(AuthReason::Good)
|
||||||
},
|
|
||||||
None => Ok(AuthReason::NoKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("\tIssue fetching auth data {:?}", e);
|
|
||||||
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 {
|
else {
|
||||||
|
Ok(AuthReason::BadKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
Ok(AuthReason::NoKey)
|
Ok(AuthReason::NoKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_secret() -> String {
|
pub fn generate_secret() -> String {
|
||||||
/*
|
/*
|
||||||
@ -66,6 +63,37 @@ pub fn generate_secret() -> String {
|
|||||||
use base64::{encode_config, URL_SAFE};
|
use base64::{encode_config, URL_SAFE};
|
||||||
|
|
||||||
let mut buf: Vec<u8> = vec![0;64];
|
let mut buf: Vec<u8> = vec![0;64];
|
||||||
getrandom(&mut buf);
|
getrandom(&mut buf).unwrap();
|
||||||
encode_config(buf,URL_SAFE)
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use hyper::{StatusCode, Response, Body};
|
use hyper::{StatusCode, Response, Body};
|
||||||
use hyper::header::HeaderValue;
|
use hyper::header::HeaderValue;
|
||||||
|
|
||||||
@ -10,6 +8,7 @@ use mysql_async::prelude::{params, Queryable};
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::db_types::{UBigInt, VarChar, Integer};
|
use crate::db_types::{UBigInt, VarChar, Integer};
|
||||||
|
use crate::common;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ChannelType {
|
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<i64>) -> 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
|
// Primary way of interpretting sql data on our channels table
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
id: u64,
|
pub id: u64,
|
||||||
name: String,
|
pub name: String,
|
||||||
description: String,
|
pub description: String,
|
||||||
kind: ChannelType
|
pub kind: ChannelType
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -180,9 +165,10 @@ pub async fn create_channel(pool: &Pool, response: &mut Response<Body>, params:
|
|||||||
match insert_channel(pool, name, desc, kind).await {
|
match insert_channel(pool, name, desc, kind).await {
|
||||||
// Server Errors are generally _ok_ to reveal in body I suppose
|
// Server Errors are generally _ok_ to reveal in body I suppose
|
||||||
Err(Error::Server(se)) => {
|
Err(Error::Server(se)) => {
|
||||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
common::db_err_response_body(response, se);
|
||||||
let b = format!("Server code: {}\nServer Message:{}", se.code, se.message);
|
//*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||||
*response.body_mut() = Body::from(b);
|
//let b = format!("Server code: {}\nServer Message:{}", se.code, se.message);
|
||||||
|
//*response.body_mut() = Body::from(b);
|
||||||
},
|
},
|
||||||
// generic errors get a 500
|
// generic errors get a 500
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@ -209,7 +195,7 @@ pub async fn delete_channel(pool: &Pool, response: &mut Response<Body>, params:
|
|||||||
match db_delete_channel(pool, name).await {
|
match db_delete_channel(pool, name).await {
|
||||||
Ok(_) => *response.status_mut() = StatusCode::OK,
|
Ok(_) => *response.status_mut() = StatusCode::OK,
|
||||||
Err(e) => {
|
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<Body>, params:
|
|||||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
*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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
38
server-api/src/common.rs
Normal file
38
server-api/src/common.rs
Normal file
@ -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<Body>, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,7 @@
|
|||||||
pub type Integer = i32;
|
pub type Integer = i32;
|
||||||
pub type UInteger = u32;
|
|
||||||
|
|
||||||
pub type UBigInt = u64;
|
pub type UBigInt = u64;
|
||||||
pub type BigInt = i64;
|
pub type BigInt = i64;
|
||||||
|
|
||||||
pub type VarChar = String;
|
pub type VarChar = String;
|
||||||
|
|
||||||
pub enum DbError {
|
|
||||||
BadParam(&'static str),
|
|
||||||
Connection(&'static str),
|
|
||||||
Internal
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use mysql_async;
|
use mysql_async;
|
||||||
use mysql_async::{Conn, Pool};
|
use mysql_async::{Conn, Pool};
|
||||||
@ -8,11 +9,15 @@ use mysql_async::prelude::{params, Queryable};
|
|||||||
use hyper::{Response, Body, StatusCode};
|
use hyper::{Response, Body, StatusCode};
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use rand::random;
|
|
||||||
struct InviteRow {
|
use crate::db_types::BigInt;
|
||||||
id: u64,
|
use crate::members::{self, Member};
|
||||||
expires: u64,
|
|
||||||
uses: i32,
|
#[derive(Serialize)]
|
||||||
|
struct Invite {
|
||||||
|
id: BigInt,
|
||||||
|
uses: Option<BigInt>, // optional because some links are permanent
|
||||||
|
expires: bool,
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Error handling:
|
* Error handling:
|
||||||
@ -20,104 +25,152 @@ struct InviteRow {
|
|||||||
* are of the enum mysql_async::error::Error
|
* are of the enum mysql_async::error::Error
|
||||||
*/
|
*/
|
||||||
|
|
||||||
impl InviteRow {
|
async fn valid_invite(pool: &Pool, id: BigInt) -> Result<bool, Error>{
|
||||||
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::<u64>(), // 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<Option<InviteRow>, 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>{
|
|
||||||
/*
|
/*
|
||||||
* By this this is called we really don't care about what happens as we've
|
* Fetches an invite from the database to check for validity
|
||||||
* 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
|
|
||||||
*/
|
*/
|
||||||
let conn = pool.get_conn().await?;
|
let conn = pool.get_conn().await?;
|
||||||
let _db_result = conn
|
let db_fetch_result: (Conn, Option<(Option<BigInt>, bool)>) =
|
||||||
.prep_exec(r"UPDATE invites SET uses = :uses WHERE id = :id", mysql_async::params!{
|
conn.first_exec("SELECT uses, expires FROM invites WHERE id = :id",
|
||||||
"uses" => data.uses - 1,
|
params!{"id" => id}).await?;
|
||||||
"id" => data.id
|
|
||||||
}).await?;
|
|
||||||
|
|
||||||
Ok(())
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn route_join_invite_code(pool: &Pool, response: &mut Response<Body>, 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<BigInt>) -> Option<Member>{
|
||||||
|
/*
|
||||||
|
* 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 {
|
else {
|
||||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
pub async fn join(pool: &Pool, response: &mut Response<Body>, 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 => {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_invite(pool: &Pool, response: &mut Response<Body>) -> Result<(), Error> {
|
async fn insert_new_invite(pool: &Pool, invite: &Invite) -> Result<(), Error>{
|
||||||
let invite = InviteRow::new();
|
|
||||||
let conn = pool.get_conn().await?;
|
let conn = pool.get_conn().await?;
|
||||||
conn.prep_exec(r"INSERT INTO invites (id, expires, uses) VALUES (:id, :expires, :uses",
|
conn.prep_exec(
|
||||||
mysql_async::params!{
|
"INSERT INTO invites (id, uses, expires)
|
||||||
|
VALUES (:id, :uses, :expires)", params!{
|
||||||
"id" => invite.id,
|
"id" => invite.id,
|
||||||
"expires" => invite.expires,
|
|
||||||
"uses" => invite.uses,
|
"uses" => invite.uses,
|
||||||
|
"expires" => invite.expires
|
||||||
}).await?;
|
}).await?;
|
||||||
|
|
||||||
*response.body_mut() = Body::from(invite.as_json_str());
|
|
||||||
*response.status_mut() = StatusCode::OK;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create(pool: &Pool, response: &mut Response<Body>, params: Value) {
|
||||||
|
/*
|
||||||
|
* Creates a new invite
|
||||||
|
*/
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -22,20 +22,21 @@ use mysql_async::Pool;
|
|||||||
|
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use clap::{Arg, App};
|
use clap::{Arg, App};
|
||||||
|
use auth::AuthReason;
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
use auth::AuthReason;
|
|
||||||
|
|
||||||
mod routes;
|
mod routes;
|
||||||
mod invites;
|
mod invites;
|
||||||
mod channels;
|
mod channels;
|
||||||
|
|
||||||
mod members;
|
mod members;
|
||||||
|
|
||||||
mod messages;
|
|
||||||
mod http_params;
|
|
||||||
mod perms;
|
mod perms;
|
||||||
|
mod messages;
|
||||||
|
|
||||||
|
mod http_params;
|
||||||
mod db_types;
|
mod db_types;
|
||||||
|
mod common;
|
||||||
|
mod testing;
|
||||||
|
|
||||||
const NO_ERR: u16 = 0;
|
const NO_ERR: u16 = 0;
|
||||||
const CONFIG_ERR: u16 = 1;
|
const CONFIG_ERR: u16 = 1;
|
||||||
@ -45,20 +46,13 @@ async fn route_dispatcher(pool: &Pool, resp: &mut Response<Body>, meth: &Method,
|
|||||||
// At some point we should have some way of hiding this obnoxious complexity
|
// At some point we should have some way of hiding this obnoxious complexity
|
||||||
use routes::resolve_dynamic_route;
|
use routes::resolve_dynamic_route;
|
||||||
match (meth, path) {
|
match (meth, path) {
|
||||||
(&Method::GET, routes::INVITE_JOIN) => {
|
/* INVITES */
|
||||||
if let Err(_) = invites::route_join_invite_code(pool, resp, params).await {
|
(&Method::GET, routes::INVITE_CREATE) => invites::create(pool, resp, params).await,
|
||||||
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
/* CHANNELS */
|
||||||
}
|
|
||||||
},
|
|
||||||
(&Method::GET, routes::INVITE_CREATE) => {
|
|
||||||
if let Err(_) = invites::create_invite(pool, resp).await {
|
|
||||||
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(&Method::GET, routes::CHANNELS_LIST) => channels::list_channels(pool, resp).await,
|
(&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_CREATE) => channels::create_channel(pool, resp, params).await,
|
||||||
(&Method::POST, routes::CHANNELS_DELETE) => channels::delete_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,
|
(&Method::POST, routes::MESSAGE_SEND) => messages::send_message(pool, resp, params).await,
|
||||||
_ => {
|
_ => {
|
||||||
// We attempt dynamic routes as fallback for a few reasons
|
// We attempt dynamic routes as fallback for a few reasons
|
||||||
@ -69,9 +63,13 @@ async fn route_dispatcher(pool: &Pool, resp: &mut Response<Body>, meth: &Method,
|
|||||||
// Computatinoal bounds are really of no concern with this api since
|
// Computatinoal bounds are really of no concern with this api since
|
||||||
// we're not doing any heavy calculations at any point
|
// we're not doing any heavy calculations at any point
|
||||||
if let Some(route) = resolve_dynamic_route(path) {
|
if let Some(route) = resolve_dynamic_route(path) {
|
||||||
*resp.status_mut() = StatusCode::OK;
|
match (meth, route.base.as_str()) {
|
||||||
println!("\tStatic part: {}", route.base);
|
(&Method::GET, routes::DYN_JOIN) => invites::join(pool, resp, params).await,
|
||||||
println!("\tDynamic part: {}", route.dynamic);
|
_ => {
|
||||||
|
println!("\tNOT FOUND: {}: {}", meth, path);
|
||||||
|
*resp.status_mut() = StatusCode::NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
println!("\tNOT FOUND: {}: {}", meth, path);
|
println!("\tNOT FOUND: {}: {}", meth, path);
|
||||||
@ -98,7 +96,7 @@ async fn main_responder(request: Request<Body>) -> Result<Response<Body>, hyper:
|
|||||||
// Deal with permissions errors at this point
|
// Deal with permissions errors at this point
|
||||||
match auth_result {
|
match auth_result {
|
||||||
OpenAuth | Good => route_dispatcher(&pool, &mut response, &method, path, params).await,
|
OpenAuth | Good => route_dispatcher(&pool, &mut response, &method, path, params).await,
|
||||||
NoKey => {
|
NoKey | BadKey => {
|
||||||
println!("\tAUTH: NoKey/BadKey");
|
println!("\tAUTH: NoKey/BadKey");
|
||||||
*response.status_mut() = StatusCode::UNAUTHORIZED
|
*response.status_mut() = StatusCode::UNAUTHORIZED
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use hyper::{Body, Response, StatusCode};
|
use mysql_async::{Conn, Pool, error::Error};
|
||||||
use hyper::header::{HeaderName, HeaderValue};
|
|
||||||
use mysql_async::{Conn, Pool, error::Error as MySqlError};
|
|
||||||
use mysql_async::prelude::{params, Queryable};
|
use mysql_async::prelude::{params, Queryable};
|
||||||
use serde_json::Value;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::db_types::{UBigInt, BigInt, Integer, VarChar};
|
use crate::db_types::{UBigInt, BigInt, Integer, VarChar};
|
||||||
|
use crate::auth;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Member {
|
pub struct Member {
|
||||||
@ -18,29 +16,16 @@ pub struct Member {
|
|||||||
pub permissions: UBigInt,
|
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();
|
pub async fn insert_new_member(p: &Pool, name: VarChar, perms: u64) -> Result<Member, Error> {
|
||||||
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<Member, MySqlError> {
|
|
||||||
use crate::auth::generate_secret;
|
use crate::auth::generate_secret;
|
||||||
|
|
||||||
let conn: Conn = p.get_conn().await?;
|
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 now: BigInt = Utc::now().timestamp();
|
||||||
|
|
||||||
let conn = conn.drop_exec(
|
let conn = conn.drop_exec(
|
||||||
@ -63,36 +48,10 @@ pub async fn insert_new_member(p: &Pool, name: VarChar, perms: u64) -> Result<Me
|
|||||||
|
|
||||||
Ok(Member {
|
Ok(Member {
|
||||||
id: db_row_result.1.unwrap(), // if we made it this far this shouldn't fail (i hope)
|
id: db_row_result.1.unwrap(), // if we made it this far this shouldn't fail (i hope)
|
||||||
secret: secret,
|
secret: secret_raw,
|
||||||
name: name,
|
name: name,
|
||||||
joindate: now,
|
joindate: now,
|
||||||
status: 0,
|
status: 0,
|
||||||
permissions: perms
|
permissions: perms
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn general_new_user(p: &Pool, resp: &mut Response<Body>, 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -43,7 +43,7 @@ pub async fn send_message(pool: &Pool, response: &mut Response<Body>, params: Va
|
|||||||
let content_r = params.get("content");
|
let content_r = params.get("content");
|
||||||
let channel_name_r = params.get("channel");
|
let channel_name_r = params.get("channel");
|
||||||
// auth module guarantees this will be there in the correct form
|
// 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();
|
.unwrap().as_u64().unwrap();
|
||||||
|
|
||||||
match (content_r, channel_name_r) {
|
match (content_r, channel_name_r) {
|
||||||
@ -75,3 +75,74 @@ pub async fn send_message(pool: &Pool, response: &mut Response<Body>, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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 INVITE_CREATE: &'static str = "/invite/create"; // requires none
|
||||||
|
|
||||||
pub const CHANNELS_LIST: &'static str = "/channels/list"; // 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
|
pub const MESSAGE_SEND: &'static str = "/message/send"; // requires @content
|
||||||
|
|
||||||
const DYNAMIC_ROUTE_BASES: [&'static str;1] = [
|
pub const SERVER_META: &'static str = "/meta"; // open
|
||||||
"/invites",
|
|
||||||
|
// 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/<name>|/dm/<name>
|
||||||
];
|
];
|
||||||
|
pub const DYN_JOIN: &'static str = DYNAMIC_ROUTE_BASES[0].0;
|
||||||
|
|
||||||
pub struct DynRoute {
|
pub struct DynRoute {
|
||||||
pub base: String,
|
pub base: String,
|
||||||
@ -21,9 +26,9 @@ pub fn resolve_dynamic_route(uri: &str) -> Option<DynRoute> {
|
|||||||
let mut valid = false;
|
let mut valid = false;
|
||||||
let mut base_ref = "";
|
let mut base_ref = "";
|
||||||
for base in DYNAMIC_ROUTE_BASES.iter() {
|
for base in DYNAMIC_ROUTE_BASES.iter() {
|
||||||
if uri.starts_with(base) {
|
if uri.starts_with(base.0) {
|
||||||
valid = true;
|
valid = true;
|
||||||
base_ref = base;
|
base_ref = base.0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,3 +42,19 @@ pub fn resolve_dynamic_route(uri: &str) -> Option<DynRoute> {
|
|||||||
None
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
38
server-api/src/testing/mod.rs
Normal file
38
server-api/src/testing/mod.rs
Normal file
@ -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<hyper::Body> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -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'
|
|
@ -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
|
|
@ -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_
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
Loading…
Reference in New Issue
Block a user