Renaming project to json-api for clarity sake

This commit is contained in:
shockrah
2021-01-24 13:34:17 -08:00
parent 84c865e194
commit b67bb6105f
52 changed files with 83 additions and 2 deletions

65
json-api/src/admin.rs Normal file
View File

@@ -0,0 +1,65 @@
// Module deals endpoints pertaining to admin-only actions
use hyper::{Response, Body};
use hyper::StatusCode;
use mysql_async::Pool;
use serde_json::Value;
use crate::perms::ADMIN_PERMS;
use db::{
self,
member::Member
};
macro_rules! get_target_id {
($obj:expr) => {
match $obj.get("target-id") {
Some(val) => val.as_u64(),
None => None
}
}
}
pub async fn new_admin(p: &Pool, response: &mut Response<Body>, params: Value) {
// @requires: owner level permission as regular admins can have conflict of interests
// @user-param: "target-id": Number
if let Some(uid) = get_target_id!(params) {
if let Err(e) = Member::update_perms(p, uid, ADMIN_PERMS).await {
eprintln!("{}", e);
}
}
else {
// this is likely the users fault providing shit ass json
*response.status_mut() = StatusCode::BAD_REQUEST;
*response.body_mut() = Body::from("Missing target user id");
}
}
pub async fn set_permissions(p: &Pool, response: &mut Response<Body>, params: Value) {
// @requiresL: admin level permissions, admins can't touch other admins
let tuid = get_target_id!(params);
let new_perms_opt = match params.get("permissions") {
Some(val) => val.as_u64(),
None => None
};
match (tuid, new_perms_opt) {
(Some(uid), Some(new_perms)) => {
// Returns Ok(Response::sucess) | Err(log)
if let Err(e) = Member::update_perms(p, uid, new_perms).await {
eprintln!("{}", e); // wil be some sql error
}
},
_ => {
*response.status_mut() = StatusCode::BAD_REQUEST;
*response.body_mut() = Body::from("Missing one or more parameters");
}
}
}

275
json-api/src/auth.rs Normal file
View File

@@ -0,0 +1,275 @@
use serde::{Serialize, Deserialize};
use bcrypt::{self, BcryptResult};
use mysql_async::Pool;
use chrono::{Utc, Duration};
use crate::routes;
use db::{member::Member, common::FromDB};
use db::Response;
use jsonwebtoken::EncodingKey;
lazy_static! {
static ref HMAC_SECRET: Vec<u8> = {
std::fs::read("hmac.secret").expect("Couldn't get HMAC secret")
};
static ref ENCODING_KEY: EncodingKey = {
EncodingKey::from_secret(&HMAC_SECRET)
};
}
#[derive(Debug, Serialize, Deserialize)]
struct Claim {
sub: db::UBigInt, // user id
exp: db::BigInt, // expiry date
cookie: String, // unique cookie value
}
impl Claim {
pub fn new(id: db::UBigInt) -> Claim {
Claim {
sub: id,
exp: Utc::now()
.checked_add_signed(Duration::weeks(1))
.expect("Couldn't generate an expirey date")
.timestamp(),
cookie: generate_cookie()
}
}
}
// used when we create a new users for the first time
#[derive(Debug)]
pub enum AuthReason {
Good, //passed regular check
OpenAuth, // route does not require auth
NoKey, // key missing
BadKey, // key is bad
LoginValid, // used only to access the login route which is also our refresh
ServerIssue(String) // for well 500's
}
fn valid_secret(given_pass: &str, hash: &str) -> bool {
let result = bcrypt::verify(given_pass, hash);
return match result {
Ok(result) => result,
Err(e) => {
eprintln!("{}", e);
return false;
}
}
}
fn valid_perms(member: Member, path: &str) -> bool {
use crate::perms;
// if there are perms on the current path make sure the user has them
if let Some(p) = perms::get_perm_mask(path) {
return (p & member.permissions) == p;
}
// if no perms then we don't care
else {
return true;
}
}
fn rng_secret(length: usize) -> String {
use getrandom::getrandom;
use base64::{encode_config, URL_SAFE};
let mut buf: Vec<u8> = vec![0;length];
getrandom(&mut buf).unwrap();
encode_config(buf,URL_SAFE)
}
pub fn generate_secret() -> String {
/*
* Generates a url-safe-plaintext secret for our db
* */
return rng_secret(64);
}
pub fn generate_cookie() -> String {
return rng_secret(32)
}
pub fn encrypt_secret(raw: &str) -> BcryptResult<String> {
const BCRYPT_COST: u32 = 14;
return bcrypt::hash(raw, BCRYPT_COST);
}
fn jwt_from_serde(params: &serde_json::Value) -> Option<&str> {
// gets the `token` from the parameters
// option<value> -> some(value) -> string
return params.get("jwt")?.as_str();
}
async fn valid_jwt(p: &Pool, token: &str) -> AuthReason {
use jsonwebtoken::{
decode, DecodingKey,
Validation, Algorithm
};
// crypto things that should prolly not fail assuming we're configured correctly
let algo = Algorithm::HS512;
let dk = DecodingKey::from_secret(&HMAC_SECRET);
if let Ok(decoded) = decode::<Claim>(token, &dk, &Validation::new(algo)) {
// subject used for querying speed NOT security
let listed = db::auth::listed_jwt(p, decoded.claims.sub, token).await.unwrap();
let active = Utc::now().timestamp() < decoded.claims.exp;
return match listed && active {
true => AuthReason::Good,
false => AuthReason::BadKey
};
}
else {
return AuthReason::BadKey;
}
}
fn login_params_from_serde(params: &serde_json::Value) -> Option<(db::UBigInt, &str)> {
let id_v = params.get("id");
let secret_v = params.get("secret");
return match (id_v, secret_v) {
(Some(id_v), Some(secret_v)) => {
match (id_v.as_u64(), secret_v.as_str()) {
(Some(id), Some(secret)) => Some((id, secret)),
_ => None
}
},
_ => None
}
}
pub async fn wall_entry<'path, 'pool, 'params>(
path: &'path str,
pool: &'pool Pool,
params: &'params serde_json::Value)
-> AuthReason {
// Dont need to auth if it's not required
let open_path = routes::is_open(path);
let jwt = jwt_from_serde(params);
if open_path { // ignore the parameters since they're irelevant
return AuthReason::OpenAuth;
}
if let Some(jwt) = jwt {
// get the headers here
return valid_jwt(pool, jwt).await;
}
if let Some((id, secret)) = login_params_from_serde(params) {
// Last chance we might be hitting the /login route so we have to do the heavy auth flow
if path != routes::AUTH_LOGIN {
return AuthReason::BadKey;
}
else {
return match Member::get(pool, id).await {
Response::Row(user) => {
if valid_secret(secret, &user.secret) && valid_perms(user, path){
AuthReason::LoginValid
}
else {
AuthReason::BadKey
}
},
Response::Empty => AuthReason::BadKey,
Response::Other(err) => AuthReason::ServerIssue(err),
_ => AuthReason::ServerIssue("db-lib returned garbage".into())
}
}
}
return AuthReason::NoKey;
}
pub async fn login_get_jwt(p: &Pool, response: &mut hyper::Response<hyper::Body>, params: serde_json::Value) {
// basically this route generates a jwt for the user and returns via the jwt key
// in the json response
use jsonwebtoken::{
Header, Algorithm,
encode
};
use hyper::header::HeaderValue;
let id = params.get("id").unwrap().as_u64().unwrap(); // only route where we have the "id is there guarantee"
let claim = Claim::new(id);
let header = Header::new(Algorithm::HS512);
let encoded = encode(
&header,
&claim,
&ENCODING_KEY).unwrap();
match db::auth::add_jwt(p, id, &encoded).await {
Ok(_) => {
response.headers_mut().insert("Content-Type",
HeaderValue::from_static("application/json"));
let payload = serde_json::json!({
"jwt": encoded
});
*response.body_mut() = hyper::Body::from(payload.to_string());
},
Err(e) => {
eprintln!("{}", e);
*response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
}
};
}
#[cfg(test)]
mod auth_tests {
use jsonwebtoken::{
Header, encode, Algorithm
};
#[test]
fn validity_check() {
use bcrypt::{hash, DEFAULT_COST};
let plain = super::generate_secret();
match hash(&plain, DEFAULT_COST) {
Ok(hash) => assert_eq!(super::valid_secret(&plain, &hash), true),
Err(err) => panic!("{}", err)
}
}
#[test]
fn verify_jwt() {
let claim = super::Claim::new(123); // example claim that we send out
let header = Header::new(Algorithm::HS512); // header that basically all clients get
let encoded = encode(
&header,
&claim,
&super::ENCODING_KEY).unwrap();
use jsonwebtoken::{decode, DecodingKey, Validation};
let dc = decode::<super::Claim>(
&encoded,
&DecodingKey::from_secret(&super::HMAC_SECRET),
&Validation::new(Algorithm::HS512)
); // decoding works yet fails on the debugger
assert_eq!(dc.is_ok(), true);
println!("{:?}", dc);
println!("{}", encoded);
let mut parts:Vec<String> = Vec::new();
for i in encoded.split('.') {
parts.push(i.to_string());
}
let head = &parts[0];
let claim = &parts[1];
let sig = &parts[2];
let res = jsonwebtoken::crypto::verify(
sig,
&format!("{}.{}", head, claim),
&DecodingKey::from_secret(&super::HMAC_SECRET),
Algorithm::HS512);
println!("{:?}", res);
}
}

106
json-api/src/channels.rs Normal file
View File

@@ -0,0 +1,106 @@
use hyper::{StatusCode, Response, Body};
use mysql_async::Pool;
use serde_json::{json, Value};
use db::{
self,
common::FromDB,
channels::Channel
};
use crate::http::set_json_body;
pub async fn list_channels(pool: &Pool, response: &mut Response<Body>) {
/*
* @user-params -> for now none as i don't feel like dealing with it
* @TODO: add in a let var that actually
*/
return match db::channels::Channel::filter(pool, 0).await {
db::Response::Set(channels) => {
set_json_body(response, json!(channels));
},
db::Response::Other(_msg) => *response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR,
_ => *response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR,
};
}
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
* @responds with the data of the newly created channel
*/
// Theres an extra un-needed unwrap to be cut out from this proc
// specifically with the desc parameter
use std::convert::TryInto;
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)) => {
use db::channels::{TEXT_CHANNEL, VOICE_CHANNEL};
if kind < VOICE_CHANNEL as i64 || kind > TEXT_CHANNEL as i64 {
*response.status_mut() = StatusCode::BAD_REQUEST; // restriciting to 1|2 for valid channel kinds
}
else {
// Send the data up to the db, then return the new channel back to the user(?)
match db::channels::Channel::add(pool, name, desc, kind.try_into().unwrap()).await {
db::Response::Row(row) => {
set_json_body(response, json!(row));
},
// user error that the db doesn't deal with so we just blame the user
db::Response::RestrictedInput(msg) => {
*response.status_mut() = StatusCode::BAD_REQUEST;
set_json_body(response, json!({"error": msg}));
},
// inserted but could not fetch
db::Response::Empty => *response.status_mut() = StatusCode::NOT_FOUND,
//like legit issues past here
db::Response::Other(msg) => {
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; // conn issue probably
eprintln!("\t[ Channels ] {}", msg);
},
_ => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR // ngmi
}
}
},
// basically one of the parameter gets failed so we bail on all of this
_ => *response.status_mut() = StatusCode::BAD_REQUEST
}
}
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("channel_id") {
if let Some(id) = name.as_u64() {
// TODO: something more intelligent with the logging im ngl
match Channel::delete(pool, id).await {
db::Response::Success => {},
db::Response::Other(data) => {
eprintln!("\t{}", data);
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
}
_ => {
eprintln!("\tBro like restart the server");
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
}
}
}
else {
*response.status_mut() = StatusCode::BAD_REQUEST;
}
}
else {
*response.status_mut() = StatusCode::BAD_REQUEST;
}
}

37
json-api/src/http.rs Normal file
View File

@@ -0,0 +1,37 @@
use serde_json::{self, Value};
use hyper::http::HeaderValue;
use hyper::Response;
use hyper::Body;
use hyper::body::to_bytes;
use std::u8;
const APP_JSON_HEADER: &'static str = "application/json";
const CONTENT_TYPE: &'static str = "Content-Type";
pub fn set_json_body(response: &mut Response<Body>, values: Value) {
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static(APP_JSON_HEADER));
*response.body_mut() = Body::from(values.to_string());
}
pub async fn parse_json_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)
}
#[inline]
pub fn extract_uid(values: &Value) -> u64 {
// pulling 'id' from user params is safe because the
// auth modules guarantees this to be there already
values.get("id").unwrap().as_u64().unwrap()
}

View File

@@ -0,0 +1,2 @@

218
json-api/src/invites.rs Normal file
View File

@@ -0,0 +1,218 @@
use serde_json::Value;
use serde::Serialize;
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 db::{UBigInt, BigInt};
use db::common::FromDB;
use db::member::Member;
#[derive(Serialize)]
struct Invite {
id: BigInt, // doubles as the timestamp for when it dies
uses: Option<BigInt>, // optional because some links are permanent
expires: bool,
}
/*
* Error handling:
* All errors raisable from this module come from mysql_async and thus
* are of the enum mysql_async::error::Error
*/
async fn valid_invite(pool: &Pool, id: BigInt) -> bool {
/*
* Fetches an invite from the database to check for validity
*/
let query: Option<db::invites::Invite> = match db::invites::Invite::get(pool, id as u64).await {
db::Response::Row(invite) => { Some(invite) },
_ => { None }
};
if let Some(invite) = query {
// if expires at all
if invite.expires {
let now = Utc::now().timestamp();
// old?
let mut valid_status = now > invite.id;
// used?
if invite.uses.is_some() && valid_status == false {
valid_status = invite.uses.unwrap() <= 0; // safe unwrap since we know its Some(_)
}
return valid_status
}
// no expiry date? no problem
return true
}
// prolly not a real id
return false
}
async fn use_invite(pool: &Pool, code: Option<BigInt>) -> Option<Member>{
/*
* Attempts to change the state of the current invite being provided
*/
use crate::auth;
use crate::perms::GENERAL_NEW;
let id = match code {
Some(id) => id,
None => 0
};
// some random comment
if valid_invite(pool, id).await {
let raw_secret = auth::generate_secret();
if let Ok(secret) = auth::encrypt_secret(&raw_secret) {
return match db::member::Member::add(pool, "Anonymous".into(), &secret, GENERAL_NEW).await {
Ok(response) => {
match response {
db::Response::Row(member) => Some(member),
_ => None,
}
},
// TODO: logggin or something idk
Err(_) => return None
}
}
// Returning None because we couldn't actually create a proper secret to store
else {
return None;
}
}
// The invite itself was not valid
else {
return None;
}
}
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 => {
}
}
}
async fn insert_new_invite(pool: &Pool, invite: &Invite) -> Result<(), Error>{
let conn = pool.get_conn().await?;
conn.prep_exec(
"INSERT INTO invites (id, uses, expires)
VALUES (:id, :uses, :expires)", params!{
"id" => invite.id,
"uses" => invite.uses,
"expires" => invite.expires
}).await?;
Ok(())
}
async fn process_expires_parameter(p: &Pool, exp: &Value, id: UBigInt) -> bool {
// TODO: fix this somewhat unsafe code
// NOTE: its unsafe because of these lazy as heck unwraps everywhere
use crate::perms::{CREATE_PERM_INVITES, CREATE_TMP_INVITES};
let conn = p.get_conn().await.unwrap();
let db_tup: (Conn, Option<UBigInt>) = conn.first_exec(
"SELECT permissions FROM members WHERE id = :id",
params!{"id" => id})
.await.unwrap();
// depending on what type of invite we requested we should make sure we have the
// right permissions to do so
let real_perms = db_tup.1.unwrap(); // safe via auth module
if let Some(exp) = exp.as_bool() {
// perma?
if exp {
return (real_perms & CREATE_PERM_INVITES) == CREATE_PERM_INVITES;
}
else {
return (real_perms & CREATE_TMP_INVITES) == CREATE_TMP_INVITES;
}
}
else {
return false;
}
}
pub async fn create(pool: &Pool, response: &mut Response<Body>, params: Value) {
/*
* Creates a new invite
*/
// no user can actually have an id of 0 this won't find anyone on the backend
let id = match params.get("id") {
Some(val) => val.as_u64().unwrap_or(0),
None => 0
};
let use_count = match params.get("uses") {
Some(val) => val.as_i64(),
None => None
};
let expires = match params.get("expires") {
Some(exp_val) => process_expires_parameter(pool, exp_val, id).await,
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());
}
}

243
json-api/src/main.rs Normal file
View File

@@ -0,0 +1,243 @@
extern crate db;
extern crate chrono;
extern crate clap;
extern crate dotenv;
extern crate getrandom;
extern crate bcrypt;
extern crate base64;
extern crate serde;
extern crate jsonwebtoken;
#[macro_use] extern crate lazy_static;
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};
use auth::AuthReason;
mod auth;
mod routes;
mod meta;
mod invites;
mod channels;
mod members;
mod perms;
mod messages;
mod admin;
mod http;
mod testing;
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
const GET: &Method = &Method::GET;
const POST: &Method = &Method::POST;
const DELETE: &Method = &Method::DELETE;
match (meth, path) {
/* INVITES */
(GET, routes::INVITE_CREATE) => invites::create(pool, resp, params).await,
(GET, routes::INVITE_JOIN) => invites::join(pool, resp, params).await,
/* CHANNELS */
(GET, routes::CHANNELS_LIST) => channels::list_channels(pool, resp).await,
(POST, routes::CHANNELS_CREATE) => channels::create_channel(pool, resp, params).await,
(DELETE, routes::CHANNELS_DELETE) => channels::delete_channel(pool, resp, params).await,
/* MESSAGING */
(POST, routes::MESSAGE_SEND) => messages::send_message(pool, resp, params).await,
(GET, routes::MESSAGE_TIME_RANGE) => messages::get_by_time(pool, resp, params).await,
(GET, routes::MESSAGE_FROM_ID) =>messages::from_id(pool, resp, params).await,
/* ADMIN */
(POST, routes::SET_PERMS_BY_ADMIN) => admin::set_permissions(pool, resp, params).await,
/* MEMBERS */
(GET, routes::GET_ONLINE_MEMBERS) => members::get_online_members(pool, resp).await,
/* OWNER */
(POST, routes::SET_NEW_ADMIN) => admin::new_admin(pool, resp, params).await,
/* META ROUTE */
(GET, routes::META) => meta::server_meta(resp).await,
_ => {
eprintln!("\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();
let params_res = http::parse_json_params(&mut body).await;
if let Ok(params) = params_res {
let mysql_pool = Pool::new(&env::var("DATABASE_URL").unwrap());
match auth::wall_entry(path, &mysql_pool, &params).await {
OpenAuth | Good => route_dispatcher(&mysql_pool, &mut response, &method, path, params).await,
LoginValid => auth::login_get_jwt(&mysql_pool, &mut response, params).await,
NoKey | BadKey => *response.status_mut() = StatusCode::UNAUTHORIZED,
ServerIssue(msg) => {
println!("\tAUTH : 500 [{}]", msg);
*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
}
}
async fn attempt_owner_creation(name: &str) {
/*
* Attempts to create an owner level account 'name' as the name
* Writes succesful output to stdout
* Writes error output to stderr
* NOTE: Messy because there's 0 other places where this kind of direct
* functionality is required. db-lib is basically built to talk to the api
* */
let p = Pool::new(&env::var("DATABASE_URL").unwrap());
let owner_secret = auth::generate_secret();
if let Ok(enc_secret) = auth::encrypt_secret(&owner_secret) {
if let Ok(response) = db::member::Member::add(&p, name, &enc_secret, perms::OWNER).await {
match response {
db::Response::Row(mut owner) => {
owner.secret = owner_secret; // giving the secret itself back to the user
println!("{}", serde_json::to_string(&owner).expect("SQL query passed but serde couldn't parse the data for some reason"))
},
db::Response::Empty => {
eprintln!("SQL server failed to return owner data, check configs and also the members table to make sure there's nothing there by accident");
},
_ => {}
};
}
else {
eprintln!("Could not communicate with the SQL server, check your configs!");
}
}
else {
eprintln!("Could not generate a proper secret");
}
p.disconnect();
}
#[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") {
attempt_owner_creation(owner_name).await;
}
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(())
}
}

21
json-api/src/members.rs Normal file
View File

@@ -0,0 +1,21 @@
use hyper::{Response, Body, StatusCode};
use mysql_async::Pool;
use serde_json::json;
use db::member::STATUS_ONLINE;
use db::common::FromDB;
use crate::http::set_json_body;
pub async fn get_online_members(p: &Pool, response: &mut Response<Body>) {
// TODO: at some point we should provide a way of not querying literally every user in
// existance
// TODO: loggin at some point or something idklol
return match db::channels::Channel::filter(p, STATUS_ONLINE).await {
db::Response::Set(users) => {
set_json_body(response, json!(users));
},
db::Response::Other(_msg) => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR,
_ => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR
}
}

194
json-api/src/messages.rs Normal file
View File

@@ -0,0 +1,194 @@
use mysql_async::Pool;
use hyper::{Response, Body, StatusCode};
use serde_json::Value;
use serde_json::json;
use crate::http::{self, set_json_body};
use db::messages::Message;
pub async fn get_by_time(pool: &Pool, response: &mut Response<Body>, params: Value) {
/*
* Has a ton of required parameters just be warned
* @channel: channel id we're looking at
* @start-time: how long ago do we start searching
* @end-time: how long ago do we stop searching
* {
* "channel": 1,
* "start-time": unix_now - 24 hours
* "end-time": unix_now - 23 hours
* }
*
*/
let channel = match params.get("channel") {
Some(chan_v) => chan_v.as_u64(),
None => None
};
let start_time = match params.get("start-time") {
Some(val) => val.as_i64(),
None => None
};
let end_time = match params.get("end-time") {
Some(val) => val.as_i64(),
None => None
};
let limit = match params.get("limit") {
Some(val) => val.as_u64(),
None => None
};
// TODO: flatten me mommy
if let (Some(channel), Some(start), Some(end)) = (channel, start_time, end_time) {
match Message::get_time_range(pool, channel, start, end, limit).await {
Ok(db_response) => {
match db_response {
// this absolute lack of data streaming is prolly gonna suck like
// a whore in hell week for performance but lets pretend servers don't get massive
db::Response::Set(messages) => {
set_json_body(response, json!({"messages": messages}));
},
db::Response::RestrictedInput(_/*error message to log*/) => {
*response.status_mut() = StatusCode::BAD_REQUEST;
}
_ => {
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
}
};
},
Err(e) => {
eprintln!("{}", e);
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
}
};
} else {
*response.status_mut() = StatusCode::BAD_REQUEST;
}
}
pub async fn send_message(pool: &Pool, response: &mut Response<Body>, params: Value) {
/*
* @content: expecting string type
* @channel: channel id that we're going to send a message to
*/
// NOTE: auth module guarantees this will be there in the correct form
let author = http::extract_uid(&params);
match (params.get("content") , params.get("channel")) {
(Some(content_v), Some(channel_id_v)) => {
let (content, channel) = (content_v.as_str(), channel_id_v.as_u64());
if let (Some(message), Some(cid)) = (content, channel) {
// call returns empty on sucess so we don't need to do anything
// TODO: loggin
let db_result = db::messages::Message::send(pool, message, cid, author).await;
if let Err(issue) = db_result {
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
eprintln!("\t{}", issue);
}
else {
match db_result.unwrap() {
db::Response::RestrictedInput(msg) => {
// user issue
*response.status_mut() = StatusCode::BAD_REQUEST;
set_json_body(response, json!({"msg": msg}))
},
db::Response::Empty => {}, // nothing to do hyper defaults to 200
db::Response::Other(msg) => {
eprintln!("{}", msg);
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
},
_ => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR
};
}
}
else {
*response.status_mut() = StatusCode::BAD_REQUEST;
}
},
_ => *response.status_mut() = StatusCode::BAD_REQUEST
}
}
pub async fn from_id(pool: &Pool, response: &mut Response<Body>, params: Value) {
/*
* @start-id: u64
* @limit: optional<u64>
* @channel: u64
* {
* "channel": 1,
* "start": 123,
* "limit": 100
* }
*/
let channel = match params.get("channel") {
Some(chan_v) => chan_v.as_u64(),
None => None
};
let start_id = match params.get("start") {
Some(val) => val.as_u64(),
None => None
};
let limit = match params.get("limit") {
Some(val) => val.as_u64(),
None => None
};
if let (Some(channel), Some(start_id)) = (channel, start_id) {
match Message::get_from_id(pool, channel, start_id, limit).await {
Ok(db_response) => {
match db_response {
db::Response::Set(messages) => {
// NOTE this check is here because the db's check doesn't
// correctly with async and caching and magic idfk its here
// it works its correct and the cost is the same as putting
// it in the db layer so whatever
if messages.len() == 0 {
*response.status_mut() = StatusCode::NOT_FOUND;
}
else {
set_json_body(response, json!({"messages": messages}));
}
},
_ => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR
};
},
Err(err) => {
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
eprintln!("{}", err);
}
};
}
else {
*response.status_mut() = StatusCode::BAD_REQUEST;
}
}
#[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());
}
}

25
json-api/src/meta.rs Normal file
View File

@@ -0,0 +1,25 @@
// Basic handler for getting meta data about the server
use std::env::var;
use hyper::{Response, Body};
use serde_json::to_string;
use serde::Serialize;
#[derive( Serialize)]
struct Config {
name: String,
description: String,
url: String,
port: u16
}
pub async fn server_meta(response: &mut Response<Body>) {
let payload = Config {
name: var("SERVER_NAME").unwrap_or("No name".into()),
description: var("SERVER_DESCRIPTION").unwrap_or("No description".into()),
url: var("SERVER_URL").expect("Couldn't get url from environment"),
port: var("SERVER_PORT").expect("Couldn't get port from environment").parse::<u16>().unwrap(),
};
*response.body_mut() = Body::from(to_string(&payload).unwrap());
}

36
json-api/src/perms.rs Normal file
View File

@@ -0,0 +1,36 @@
// GENERAL PERMISSIONS
pub const JOIN_VOICE:u64 = 1;
pub const SEND_MESSAGES:u64 = 2;
pub const CHANGE_NICK:u64 = 16;
pub const ALLOW_PFP:u64 = 32;
pub const CREATE_TMP_INVITES:u64 = 4;
pub const CREATE_PERM_INVITES:u64 = 8; // to make perma invites you need both flags
pub const _ADMIN: u64 = 1 << 62; // can make other admins but can't really touch the owner
// ADMIN PERMS
pub const CREATE_CHANNEL:u64 = 64;
pub const DELETE_CHANNEL:u64 = 128;
// BELOW ARE COLLECTIVE PERMISSION SETS
pub const OWNER: u64 = std::u64::MAX;
pub const GENERAL_NEW: u64 = JOIN_VOICE | SEND_MESSAGES | ALLOW_PFP | CHANGE_NICK;
pub const ADMIN_PERMS: u64 = !(std::u64::MAX & OWNER); // filter the only perm admins don't get
pub fn get_perm_mask(path: &str) -> Option<u64> {
use crate::routes::{
INVITE_CREATE,
CHANNELS_LIST, CHANNELS_CREATE, CHANNELS_DELETE,
MESSAGE_SEND,
};
match path {
INVITE_CREATE => Some(CREATE_TMP_INVITES),
CHANNELS_LIST => None,
CHANNELS_CREATE => Some(CREATE_CHANNEL),
CHANNELS_DELETE => Some(DELETE_CHANNEL),
MESSAGE_SEND => Some(SEND_MESSAGES),
_ => Some(0)
}
}

27
json-api/src/routes.rs Normal file
View File

@@ -0,0 +1,27 @@
type Rstr = &'static str;
pub const AUTH_LOGIN: Rstr = "/login"; // requires @id @secret
pub const META: Rstr = "/meta"; // @ perms none @ requires JWT however
pub const INVITE_CREATE: Rstr = "/invite/create"; // @ perms::CREATE_INVITE
pub const INVITE_JOIN: Rstr = "/join"; // @ none for new accounts
pub const CHANNELS_LIST: Rstr = "/channels/list"; // requires none
pub const CHANNELS_CREATE: Rstr = "/channels/create"; // requires @name @kind perms::CREATE_CHANNEl
pub const CHANNELS_DELETE: Rstr = "/channels/delete"; // requires @name perms::DELETE_CHANNEL
pub const MESSAGE_SEND: Rstr = "/message/send"; // requires @content perms::MESSAGE_SEND
pub const MESSAGE_TIME_RANGE: Rstr = "/message/get_range"; // requires @channel(id) @start-time @end-time
pub const MESSAGE_FROM_ID: Rstr = "/message/from_id"; // requires @channel(id) requires @start(id) @<optional>limit(1..1000)
pub const GET_ONLINE_MEMBERS: Rstr = "/members/get_online";
// ADMIN ROUTES
pub const SET_PERMS_BY_ADMIN: Rstr = "/admin/setpermisions"; // @requires perms::ADMIN
pub const SET_NEW_ADMIN: Rstr = "/owner/newadmin"; // @requiers: owner perms
pub fn is_open(path: &str) -> bool {
return path.starts_with("/join") || path.starts_with("/meta");
}

54
json-api/src/schema.rs Normal file
View File

@@ -0,0 +1,54 @@
table! {
channels (id) {
id -> Unsigned<Bigint>,
name -> Varchar,
description -> Nullable<Varchar>,
kind -> Integer,
}
}
table! {
invites (id) {
id -> Bigint,
uses -> Nullable<Bigint>,
expires -> Bool,
}
}
table! {
jwt (id) {
id -> Unsigned<Bigint>,
token -> Varchar,
}
}
table! {
members (id, secret) {
id -> Unsigned<Bigint>,
secret -> Varchar,
name -> Varchar,
joindate -> Bigint,
status -> Integer,
permissions -> Unsigned<Bigint>,
}
}
table! {
messages (id) {
id -> Unsigned<Bigint>,
time -> Bigint,
content -> Varchar,
author_id -> Unsigned<Bigint>,
channel_id -> Unsigned<Bigint>,
}
}
joinable!(messages -> channels (channel_id));
allow_tables_to_appear_in_same_query!(
channels,
invites,
jwt,
members,
messages,
);

View File

@@ -0,0 +1,20 @@
// 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())
}