renamed server/ to api/ since this is really only the api portion of the typical fc server
This commit is contained in:
6
server-api/.env
Normal file
6
server-api/.env
Normal file
@@ -0,0 +1,6 @@
|
||||
DATABASE_URL=mysql://freechat_dev:password@localhost:3306/freechat
|
||||
DATABASE_NAME=freechat
|
||||
DATABASE_PASS=password
|
||||
DATABASE_USER=freechat_dev
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=3306
|
||||
5
server-api/.gitignore
vendored
Normal file
5
server-api/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
target
|
||||
**/*.rs.bk
|
||||
|
||||
static/css/
|
||||
dev-sql/
|
||||
1912
server-api/Cargo.lock
generated
Normal file
1912
server-api/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
server-api/Cargo.toml
Normal file
30
server-api/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "freechat-server"
|
||||
version = "0.1.0"
|
||||
authors = ["shockrah <alejandros714@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "0.2", features=["full"] }
|
||||
hyper = "0.13"
|
||||
futures = "0.3"
|
||||
url = "2.1.1"
|
||||
mysql_async = "0.23.1"
|
||||
|
||||
dotenv = "0.9.0"
|
||||
|
||||
chrono = "0.4.0"
|
||||
time = "0.2"
|
||||
|
||||
getrandom = "0.1"
|
||||
bcrypt = "0.6"
|
||||
base64 = "0.12.1"
|
||||
rand = "0.7.3"
|
||||
|
||||
clap = "2.32.2"
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0.114", features = [ "derive" ] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.2.1"
|
||||
0
server-api/migrations/.gitkeep
Normal file
0
server-api/migrations/.gitkeep
Normal file
2
server-api/migrations/2020-02-04-083657_invites/down.sql
Normal file
2
server-api/migrations/2020-02-04-083657_invites/down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE `invites`;
|
||||
15
server-api/migrations/2020-02-04-083657_invites/up.sql
Normal file
15
server-api/migrations/2020-02-04-083657_invites/up.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- @id : id of the invite
|
||||
|
||||
-- @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
|
||||
|
||||
-- @max_uses : if this is null uses only ever incremented but we don't care for destroying on that parameter
|
||||
CREATE TABLE IF NOT EXISTS `invites` (
|
||||
`id` bigint UNSIGNED NOT NULL,
|
||||
`expires` bigint,
|
||||
`uses` integer,
|
||||
`max_uses` integer,
|
||||
PRIMARY KEY( `id` )
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE `channels`;
|
||||
7
server-api/migrations/2020-03-11-005217_channels/up.sql
Normal file
7
server-api/migrations/2020-03-11-005217_channels/up.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS `channels` (
|
||||
`id` BIGINT UNSIGNED NOT NULL auto_increment,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` VARCHAR(1024),
|
||||
`kind` INTEGER NOT NULL,
|
||||
PRIMARY KEY(`id`), UNIQUE KEY(`name`)
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE `sessions`;
|
||||
8
server-api/migrations/2020-05-17-225334_sessions/up.sql
Normal file
8
server-api/migrations/2020-05-17-225334_sessions/up.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- id's are given back to the user otherwise everything is server sided
|
||||
-- NOTE: the expires column is not explicitly a date because the code required
|
||||
-- to make the DATE field work with diesel is ass and looks annoying
|
||||
CREATE TABLE IF NOT EXISTS `sessions` (
|
||||
`secret` varchar(255) NOT NULL,
|
||||
`expires` bigint UNSIGNED NOT NULL,
|
||||
PRIMARY KEY(`secret`)
|
||||
);
|
||||
1
server-api/migrations/2020-07-05-215114_members/down.sql
Normal file
1
server-api/migrations/2020-07-05-215114_members/down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE `members`;
|
||||
11
server-api/migrations/2020-07-05-215114_members/up.sql
Normal file
11
server-api/migrations/2020-07-05-215114_members/up.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- TODO: add rate limiter in some form
|
||||
-- PERMISSIONS start at 0 and full perms => all F's
|
||||
CREATE TABLE IF NOT EXISTS `members`(
|
||||
`id` bigint UNSIGNED NOT NULL auto_increment,
|
||||
`secret` varchar(256) NOT NULL,
|
||||
`name` varchar(256) NOT NULL,
|
||||
`joindate` bigint NOT NULL,
|
||||
`status` integer NOT NULL,
|
||||
`permissions` bigint UNSIGNED NOT NULL,
|
||||
PRIMARY KEY( `id` , `secret` )
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE `messages`;
|
||||
11
server-api/migrations/2020-07-06-022319_messages/up.sql
Normal file
11
server-api/migrations/2020-07-06-022319_messages/up.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Time stamp is _not_ in ms
|
||||
CREATE TABLE IF NOT EXISTS `messages`(
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`time` BIGINT NOT NULL,
|
||||
`content` VARCHAR(2048) NOT NULL,
|
||||
`author_id` BIGINT UNSIGNED NOT NULL,
|
||||
`channel_name` VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
FOREIGN KEY (`author_id`) REFERENCES members(`id`),
|
||||
FOREIGN KEY (`channel_name`) REFERENCES channels(`name`)
|
||||
);
|
||||
11
server-api/readme.md
Normal file
11
server-api/readme.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Freechat API
|
||||
|
||||
Built with rocket this API lets client apps query the server for data like recent messages
|
||||
channels in a server.
|
||||
|
||||
# What's Being worked on
|
||||
|
||||
Authorization model has been implemented and is now being integrated with endpoints
|
||||
where required.
|
||||
Not all endpoints require authentication but really those are more like exceptions
|
||||
rather than the rule as most of this API is behind some kind of authentication.
|
||||
8
server-api/scripts/setup-dev-user.sh
Normal file
8
server-api/scripts/setup-dev-user.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Running the
|
||||
mysql -u root -p \
|
||||
"create user 'freechat_dev'@'localhost' identified by 'password';"
|
||||
|
||||
mysql -u root -p \
|
||||
"GRANT ALL PRIVILEGES ON freechat.* TO 'freechat_dev'@'localhost';"
|
||||
71
server-api/src/auth.rs
Normal file
71
server-api/src/auth.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use mysql_async::Pool;
|
||||
use mysql_async::prelude::{params, Queryable};
|
||||
use crate::db_types::{UBigInt, VarChar};
|
||||
|
||||
use crate::routes;
|
||||
|
||||
pub const BCRYPT_COST: u32 = 14;
|
||||
pub enum AuthReason {
|
||||
Good, //passed regular check
|
||||
OpenAuth, // route does not require auth
|
||||
NoKey,
|
||||
}
|
||||
|
||||
|
||||
fn open_route(path: &str) -> bool {
|
||||
return path == routes::INVITE_JOIN
|
||||
}
|
||||
pub async fn wall_entry(path: &str, pool: &Pool, params: &mut serde_json::Value) -> Result<AuthReason, mysql_async::error::Error> {
|
||||
use serde_json::json;
|
||||
// Start by Checking if the api key is in our keystore
|
||||
if open_route(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)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
Ok(AuthReason::NoKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_secret() -> String {
|
||||
/*
|
||||
* Generates a url-safe-plaintext secret for our db
|
||||
* */
|
||||
use getrandom::getrandom;
|
||||
use base64::{encode_config, URL_SAFE};
|
||||
|
||||
let mut buf: Vec<u8> = vec![0;64];
|
||||
getrandom(&mut buf);
|
||||
encode_config(buf,URL_SAFE)
|
||||
}
|
||||
219
server-api/src/channels.rs
Normal file
219
server-api/src/channels.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use hyper::{StatusCode, Response, Body};
|
||||
use hyper::header::HeaderValue;
|
||||
|
||||
use mysql_async::{Conn, Pool};
|
||||
use mysql_async::error::Error;
|
||||
use mysql_async::prelude::{params, Queryable};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::db_types::{UBigInt, VarChar, Integer};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ChannelType {
|
||||
Voice,
|
||||
Text,
|
||||
Undefined
|
||||
}
|
||||
|
||||
impl ChannelType {
|
||||
// These funcs are mainly here to help translation from mysql
|
||||
pub fn from_i32(x: i32) -> ChannelType {
|
||||
match x {
|
||||
1 => ChannelType::Voice,
|
||||
2 => ChannelType::Text,
|
||||
_ => ChannelType::Undefined
|
||||
}
|
||||
}
|
||||
pub fn as_i32(&self) -> i32 {
|
||||
match self {
|
||||
ChannelType::Voice => 1,
|
||||
ChannelType::Text => 2,
|
||||
ChannelType::Undefined => 3,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
pub struct Channel {
|
||||
id: u64,
|
||||
name: String,
|
||||
description: String,
|
||||
kind: ChannelType
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InsertableChannel {
|
||||
name: String,
|
||||
kind: ChannelType
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
/*
|
||||
* When our sql library queries things we generally get back tuples rather reasily
|
||||
* we can use this method to get something that makes more sense
|
||||
*/
|
||||
fn from_tup(tup: (UBigInt, VarChar, Option<VarChar>, Integer)) -> Channel {
|
||||
let desc = match tup.2 {
|
||||
Some(val) => val,
|
||||
None => "None".into()
|
||||
};
|
||||
|
||||
Channel {
|
||||
id: tup.0,
|
||||
name: tup.1,
|
||||
description: desc,
|
||||
kind: ChannelType::from_i32(tup.3)
|
||||
}
|
||||
}
|
||||
/*
|
||||
* When responding with some channel data to the client we use json
|
||||
* this itemizes a single struct as the following(without the pretty output)
|
||||
* {
|
||||
* "id": id<i32>,
|
||||
* "name": "<some name here>",
|
||||
* "description": Option<"<description here>">,
|
||||
* "kind": kind<i32>
|
||||
* }
|
||||
*/
|
||||
fn as_json_str(&self) -> String {
|
||||
let mut base = String::from("{");
|
||||
base.push_str(&format!("\"id\":{},", self.id));
|
||||
base.push_str(&format!("\"name\":\"{}\",", self.name));
|
||||
base.push_str(&format!("\"description\":\"{}\",", self.description));
|
||||
base.push_str(&format!("\"kind\":{}}}", self.kind.as_i32()));
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Forwarding SQL errors as we can handle those error in caller site for this leaf function
|
||||
* On success we back a Vec<Channel> which we can JSON'ify later and use as a response
|
||||
*/
|
||||
async fn get_channels_vec(conn: Conn) -> Result<Vec<Channel>, Error> {
|
||||
let rows_db = conn.prep_exec(r"SELECT * FROM channels", ()).await?;
|
||||
let (_, rows) = rows_db.map_and_drop(|row| {
|
||||
let (id, name, desc, kind): (UBigInt, VarChar, Option<VarChar>, Integer) = mysql_async::from_row(row);
|
||||
Channel::from_tup((id, name, desc, kind))
|
||||
}).await?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub async fn list_channels(pool: &Pool, response: &mut Response<Body>) {
|
||||
/*
|
||||
* Primary dispatcher for dealing with the CHANNELS_LIST route
|
||||
* For the most part this function will have a lot more error handling as it
|
||||
* should know what kind of issues its child functions will have
|
||||
*/
|
||||
if let Ok(conn) = pool.get_conn().await {
|
||||
match get_channels_vec(conn).await {
|
||||
Ok(chans) => {
|
||||
*response.status_mut() = StatusCode::OK;
|
||||
response.headers_mut().insert("Content-Type",
|
||||
HeaderValue::from_static("application/json"));
|
||||
|
||||
// At this point we build the content of our response body
|
||||
// which is a json payload hence why there is weird string manipulation
|
||||
// because we're trying to avoid dependancy issues and serializing things ourselves
|
||||
let mut new_body = String::from("{\"channels\":[");
|
||||
for chan in chans.iter() {
|
||||
let s = format!("{},", chan.as_json_str());
|
||||
new_body.push_str(&s);
|
||||
}
|
||||
if new_body.ends_with(',') {new_body.pop();}
|
||||
new_body.push_str("]}");
|
||||
|
||||
*response.body_mut() = Body::from(new_body);
|
||||
},
|
||||
Err(_) => {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn insert_channel(pool: &Pool, name: &str, desc: &str, kind: i64) -> Result<(), Error>{
|
||||
let conn = pool.get_conn().await?;
|
||||
conn.prep_exec(
|
||||
"INSERT INTO channels (name, description, kind) VALUES (:name, :description, :kind)",
|
||||
params!{"name" => name, "kind" => kind, "description" => desc}).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_channel(pool: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
/*
|
||||
* Create a channel base on a few parameters that may or may not be there
|
||||
*/
|
||||
// Theres an extra un-needed unwrap to be cut out from this proc
|
||||
// specifically with the desc parameter
|
||||
let req_params: (Option<&str>, Option<&str>, Option<i64>) =
|
||||
match (params.get("name"), params.get("description"), params.get("kind")) {
|
||||
(Some(name), Some(desc), Some(kind)) => (name.as_str(), desc.as_str(), kind.as_i64()),
|
||||
(Some(name), None, Some(kind)) => (name.as_str(), Some("No Description"), kind.as_i64()),
|
||||
_ => (None, None, None)
|
||||
};
|
||||
match req_params {
|
||||
(Some(name), Some(desc), Some(kind)) => {
|
||||
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);
|
||||
},
|
||||
// generic errors get a 500
|
||||
Err(_) => {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
// Nothing to do when things go right
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
// basically one of the parameter gets failed so we bail on all of this
|
||||
_ => *response.status_mut() = StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
async fn db_delete_channel(pool: &Pool, name: &Value) -> Result<(), Error> {
|
||||
let conn = pool.get_conn().await?;
|
||||
conn.prep_exec(r"DELETE FROM channels WHERE name = :name", params!{"name" => name.as_str().unwrap()}).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_channel(pool: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
// make sure we have the right parameters provided
|
||||
if let Some(name) = params.get("name") {
|
||||
match db_delete_channel(pool, name).await {
|
||||
Ok(_) => *response.status_mut() = StatusCode::OK,
|
||||
Err(e) => {
|
||||
println!("delete_chanel sql error :\n{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
13
server-api/src/db_types.rs
Normal file
13
server-api/src/db_types.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
}
|
||||
16
server-api/src/http_params.rs
Normal file
16
server-api/src/http_params.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use serde_json::{self, Value};
|
||||
use hyper::body::to_bytes;
|
||||
use hyper::Body;
|
||||
use std::u8;
|
||||
|
||||
pub async fn parse_params(body_raw: &mut Body) -> Result<Value, serde_json::error::Error> {
|
||||
let bytes: &[u8] = &*to_bytes(body_raw).await.unwrap(); // rarely fails
|
||||
let values: Value;
|
||||
if bytes.len() == 0 {
|
||||
values = serde_json::from_str("{}")?;
|
||||
}
|
||||
else {
|
||||
values = serde_json::from_slice(bytes)?;
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
123
server-api/src/invites.rs
Normal file
123
server-api/src/invites.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use mysql_async;
|
||||
use mysql_async::{Conn, Pool};
|
||||
use mysql_async::error::Error;
|
||||
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,
|
||||
}
|
||||
/*
|
||||
* Error handling:
|
||||
* All errors raisable from this module come from mysql_async and thus
|
||||
* 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::<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
|
||||
* 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 _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?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_invite(pool: &Pool, response: &mut Response<Body>) -> 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?;
|
||||
|
||||
*response.body_mut() = Body::from(invite.as_json_str());
|
||||
*response.status_mut() = StatusCode::OK;
|
||||
Ok(())
|
||||
}
|
||||
225
server-api/src/main.rs
Normal file
225
server-api/src/main.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
extern crate chrono;
|
||||
extern crate clap;
|
||||
extern crate dotenv;
|
||||
extern crate getrandom;
|
||||
extern crate bcrypt;
|
||||
extern crate base64;
|
||||
extern crate serde;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::convert::Infallible; // our main dispatcher basically never fails hence why we use this
|
||||
use std::env::{self, set_var};
|
||||
|
||||
use tokio;
|
||||
use hyper::{
|
||||
self,
|
||||
Server,
|
||||
Response, Request, Body,
|
||||
Method, StatusCode,
|
||||
service::{make_service_fn, service_fn}
|
||||
};
|
||||
use mysql_async::Pool;
|
||||
|
||||
use dotenv::dotenv;
|
||||
use clap::{Arg, App};
|
||||
|
||||
mod auth;
|
||||
use auth::AuthReason;
|
||||
|
||||
mod routes;
|
||||
mod invites;
|
||||
mod channels;
|
||||
|
||||
mod members;
|
||||
|
||||
mod messages;
|
||||
mod http_params;
|
||||
mod perms;
|
||||
mod db_types;
|
||||
|
||||
const NO_ERR: u16 = 0;
|
||||
const CONFIG_ERR: u16 = 1;
|
||||
const SHUTDOWN_ERR: u16 = 2;
|
||||
|
||||
async fn route_dispatcher(pool: &Pool, resp: &mut Response<Body>, meth: &Method, path: &str, params: serde_json::Value) {
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
(&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,
|
||||
|
||||
(&Method::POST, routes::MESSAGE_SEND) => messages::send_message(pool, resp, params).await,
|
||||
_ => {
|
||||
// We attempt dynamic routes as fallback for a few reasons
|
||||
// 1. theres less of these than there are the static routes
|
||||
// 2. because of the above and that this is wholly more expensive than static routse
|
||||
// we can justify putting in tons of branches since we're likely to:
|
||||
// far jump here, lose cache, and still be be network bound
|
||||
// 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);
|
||||
}
|
||||
else {
|
||||
println!("\tNOT FOUND: {}: {}", meth, path);
|
||||
*resp.status_mut() = StatusCode::NOT_FOUND
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn main_responder(request: Request<Body>) -> Result<Response<Body>, hyper::Error>{
|
||||
use AuthReason::*;
|
||||
let mut response = Response::new(Body::empty());
|
||||
|
||||
let (parts, mut body) = request.into_parts();
|
||||
let method = parts.method;
|
||||
let path = parts.uri.path();
|
||||
println!("{}: {}", method, path);
|
||||
|
||||
let params_res = http_params::parse_params(&mut body).await;
|
||||
|
||||
if let Ok(mut params) = params_res {
|
||||
let pool = Pool::new(&env::var("DATABASE_URL").unwrap());
|
||||
if let Ok(auth_result) = auth::wall_entry(path, &pool, &mut params).await {
|
||||
// Deal with permissions errors at this point
|
||||
match auth_result {
|
||||
OpenAuth | Good => route_dispatcher(&pool, &mut response, &method, path, params).await,
|
||||
NoKey => {
|
||||
println!("\tAUTH: NoKey/BadKey");
|
||||
*response.status_mut() = StatusCode::UNAUTHORIZED
|
||||
},
|
||||
}
|
||||
}
|
||||
else {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
else {
|
||||
println!("\tPARSER: Parameter parsing failed");
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to capture ctrl-c signal");
|
||||
}
|
||||
|
||||
async fn start_server(ecode: u16) -> u16 {
|
||||
println!("Servering on localhost:8888");
|
||||
let addr = SocketAddr::from(([127,0,0,1], 8888));
|
||||
let service = make_service_fn(|_conn| async {
|
||||
Ok::<_, Infallible>(service_fn(main_responder))
|
||||
});
|
||||
let server = Server::bind(&addr).serve(service);
|
||||
let graceful_shutdown = server.with_graceful_shutdown(shutdown_signal());
|
||||
|
||||
if let Err(e) = graceful_shutdown.await {
|
||||
eprintln!("Server shutdown error: {}", e);
|
||||
return ecode | SHUTDOWN_ERR;
|
||||
}
|
||||
else {
|
||||
return ecode
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), u16>{
|
||||
|
||||
let mut main_ret: u16 = 0;
|
||||
let d_result = dotenv();
|
||||
|
||||
// check for a database_url before the override we get from the cmd line
|
||||
if let Err(_d) = d_result {
|
||||
if let Err(_e) = env::var("DATABASE_URL") {
|
||||
main_ret |= CONFIG_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
let args = App::new("Freechat Server")
|
||||
.version("0.1")
|
||||
.author("shockrah")
|
||||
.about("Decentralized chat system")
|
||||
.arg(Arg::with_name("db-url")
|
||||
.short("d")
|
||||
.long("db-url")
|
||||
.value_name("DATABASE URL")
|
||||
.help("Sets the DATABASE URL via an environment variable")
|
||||
.takes_value(true))
|
||||
.arg(Arg::with_name("create-owner")
|
||||
.short("c")
|
||||
.long("create-owner")
|
||||
.value_name("Owner")
|
||||
.help("Creates an account with full permissions in the SQL database."))
|
||||
.arg(Arg::with_name("server")
|
||||
.short("s")
|
||||
.long("server")
|
||||
.help("Starts the API server"))
|
||||
.get_matches();
|
||||
|
||||
|
||||
if args.args.len() == 0 {
|
||||
println!("Freechat Server 0.1
|
||||
shockrah
|
||||
Decentralized chat system
|
||||
|
||||
USAGE:
|
||||
freechat-server [FLAGS] [OPTIONS]
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-s, --server Starts the API server
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-c, --create-owner <Owner> Creates an account with full permissions in the SQL database.
|
||||
-d, --db-url <DATABASE URL> Sets the DATABASE URL via an environment variable");
|
||||
}
|
||||
|
||||
if let Some(db_url) = args.value_of("db-url") {
|
||||
set_var("DATABASE_URL", db_url);
|
||||
}
|
||||
|
||||
if let Some(owner_name) = args.value_of("create-owner") {
|
||||
let p = Pool::new(&env::var("DATABASE_URL").unwrap());
|
||||
println!("Creating owner {{ {} }}...", owner_name);
|
||||
if let Ok(owner) = members::insert_new_member(&p, owner_name.to_string(), std::u64::MAX).await {
|
||||
println!("{}", serde_json::to_string(&owner).unwrap());
|
||||
}
|
||||
p.disconnect();
|
||||
}
|
||||
|
||||
if args.is_present("server") {
|
||||
if main_ret == NO_ERR {
|
||||
main_ret = start_server(main_ret).await;
|
||||
}
|
||||
}
|
||||
|
||||
if main_ret != 0 {
|
||||
// dumb as heck loggin method here
|
||||
if main_ret & CONFIG_ERR != 0 {println!("ERROR: Config was not setup properly => Missing {{DATABASE_URL}}");}
|
||||
if main_ret & SHUTDOWN_ERR != 0 {println!("ERROR: Couldn't shutdown gracefully");}
|
||||
Err(main_ret)
|
||||
}
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
98
server-api/src/members.rs
Normal file
98
server-api/src/members.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
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::prelude::{params, Queryable};
|
||||
use serde_json::Value;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::db_types::{UBigInt, BigInt, Integer, VarChar};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Member {
|
||||
pub id: UBigInt,
|
||||
pub secret: VarChar,
|
||||
pub name: VarChar,
|
||||
pub joindate: BigInt,
|
||||
pub status: Integer,
|
||||
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<Member, MySqlError> {
|
||||
use crate::auth::generate_secret;
|
||||
|
||||
let conn: Conn = p.get_conn().await?;
|
||||
let secret: String = generate_secret();
|
||||
let now: BigInt = Utc::now().timestamp();
|
||||
|
||||
let conn = conn.drop_exec(
|
||||
"INSERT INTO members(secret, name, joindate, status, permissions)
|
||||
VALUES(:secret, :name, :joindate, :status, :permissions)",
|
||||
mysql_async::params!{
|
||||
"secret" => secret.clone(),
|
||||
"name" => name.clone(),
|
||||
"joindate" => now,
|
||||
"status" => 0,
|
||||
"permissions" => perms
|
||||
}).await?;
|
||||
|
||||
// now pull back the user from our db and return that row
|
||||
let db_row_result: (Conn, Option<UBigInt>) = conn.first_exec(
|
||||
"SELECT id FROM members WHERE secret = :secret",
|
||||
params!{
|
||||
"secret" => secret.clone()
|
||||
}).await?;
|
||||
|
||||
Ok(Member {
|
||||
id: db_row_result.1.unwrap(), // if we made it this far this shouldn't fail (i hope)
|
||||
secret: secret,
|
||||
name: name,
|
||||
joindate: now,
|
||||
status: 0,
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
77
server-api/src/messages.rs
Normal file
77
server-api/src/messages.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use mysql_async::{Pool, params};
|
||||
use mysql_async::prelude::{Queryable};
|
||||
use mysql_async::error::Error;
|
||||
use hyper::{Response, Body, StatusCode};
|
||||
use serde_json::Value;
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::db_types::{UBigInt};
|
||||
|
||||
|
||||
pub async fn insert_message(pool: &Pool, content: &Value, channel_name: &Value, author_id: UBigInt)
|
||||
-> Result<(), Error>{
|
||||
match (content.as_str(), channel_name.as_str()) {
|
||||
(Some(content), Some(channel)) => {
|
||||
let conn = pool.get_conn().await?;
|
||||
let time = Utc::now().timestamp();
|
||||
conn.prep_exec(
|
||||
r"INSERT INTO messages
|
||||
(time, content, author_id, channel_name)
|
||||
VALUES(:time, :content, :author, :channel)",
|
||||
params!{
|
||||
"time" => time,
|
||||
"content" => content,
|
||||
"author" => author_id,
|
||||
"channel" => channel
|
||||
}).await?;
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
let e = Cow::from("Required parameter missing");
|
||||
Err(Error::Other(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_message(pool: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
/*
|
||||
* @content: expecting string type
|
||||
* @id: expecting the channel id that we're posting data to
|
||||
*/
|
||||
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")
|
||||
.unwrap().as_u64().unwrap();
|
||||
|
||||
match (content_r, channel_name_r) {
|
||||
(Some(content), Some(channel_name)) => {
|
||||
match insert_message(pool, content, channel_name, author).await {
|
||||
Ok(_) => *response.status_mut() = StatusCode::OK,
|
||||
Err(err) => {
|
||||
use mysql_async::error::Error::{Server};
|
||||
println!("\tDB Error::send_message: {:?}", err);
|
||||
// doing this to avoid client confusion as some input does cause sql errors
|
||||
if let Server(se) = err {
|
||||
if se.code == 1452 {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
*response.body_mut() = Body::from(format!("{} does not exist", channel_name));
|
||||
}
|
||||
else {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
else {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
*response.body_mut() = Body::from("content/channel missing from json parameters");
|
||||
}
|
||||
}
|
||||
}
|
||||
4
server-api/src/perms.rs
Normal file
4
server-api/src/perms.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub const JOIN_VOICE:u64 = 1;
|
||||
pub const SEND_MESSAGES:u64 = 2;
|
||||
|
||||
pub const GENERAL_NEW: u64 = JOIN_VOICE | SEND_MESSAGES;
|
||||
39
server-api/src/routes.rs
Normal file
39
server-api/src/routes.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
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
|
||||
pub const CHANNELS_CREATE: &'static str = "/channels/create"; // requires @name @kind
|
||||
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 struct DynRoute {
|
||||
pub base: String,
|
||||
pub dynamic: String,
|
||||
}
|
||||
|
||||
|
||||
pub fn resolve_dynamic_route(uri: &str) -> Option<DynRoute> {
|
||||
let mut valid = false;
|
||||
let mut base_ref = "";
|
||||
for base in DYNAMIC_ROUTE_BASES.iter() {
|
||||
if uri.starts_with(base) {
|
||||
valid = true;
|
||||
base_ref = base;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if valid {
|
||||
Some(DynRoute {
|
||||
base: base_ref.into(),
|
||||
dynamic: uri.to_string().replace(base_ref, "")
|
||||
})
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
20
server-api/tests/common.sh
Normal file
20
server-api/tests/common.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/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'
|
||||
50
server-api/tests/main.sh
Normal file
50
server-api/tests/main.sh
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/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
|
||||
19
server-api/tests/status.md
Normal file
19
server-api/tests/status.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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_
|
||||
25
server-api/tests/todo.md
Normal file
25
server-api/tests/todo.md
Normal file
@@ -0,0 +1,25 @@
|
||||
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
|
||||
51
server-api/tests/verify_basic_cases.sh
Normal file
51
server-api/tests/verify_basic_cases.sh
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/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
|
||||
41
server-api/tests/verify_err_cases.sh
Normal file
41
server-api/tests/verify_err_cases.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/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
|
||||
23
server-api/tests/verify_mal_cases.sh
Normal file
23
server-api/tests/verify_mal_cases.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/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
|
||||
28
server-api/todo
Normal file
28
server-api/todo
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
Reference in New Issue
Block a user