renamed server/ to api/ since this is really only the api portion of the typical fc server

This commit is contained in:
shockrah
2020-08-22 15:52:37 -07:00
parent afcc03959a
commit 0822be3d20
35 changed files with 0 additions and 0 deletions

6
server-api/.env Normal file
View 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
View File

@@ -0,0 +1,5 @@
target
**/*.rs.bk
static/css/
dev-sql/

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
View 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"

View File

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE `invites`;

View 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` )
);

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE `channels`;

View 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`)
);

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE `sessions`;

View 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`)
);

View File

@@ -0,0 +1 @@
DROP TABLE `members`;

View 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` )
);

View File

@@ -0,0 +1 @@
DROP TABLE `messages`;

View 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
View 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.

View 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
View 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
View 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;
}
}

View 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
}

View 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
View 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
View 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
View 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");
}
}
}

View 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
View 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
View 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
}
}

View 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
View 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

View 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
View 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

View 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

View 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

View 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
View 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