freechat/json-api/src/main.rs
shockrah adc5b261e8 + ADD_NEIGHBOR route now supported in API backend
! Requiring a special event in the RTC server docs for this change
2021-05-09 23:14:02 -07:00

327 lines
11 KiB
Rust

extern crate db;
extern crate clap;
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 std::collections::HashMap;
use tokio;
use hyper::{
self,
Server,
Response, Request, Body,
Method, StatusCode,
service::{make_service_fn, service_fn},
HeaderMap
};
use mysql_async::Pool;
use serde::Deserialize;
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 rtc;
mod http;
mod testing;
const NO_ERR: u16 = 0;
const CONFIG_ERR: u16 = 1;
const SHUTDOWN_ERR: u16 = 2;
lazy_static! {
static ref DB_POOL: Pool = {
Pool::new(&env::var("DATABASE_URL").unwrap())
};
}
async fn route_dispatcher(
pool: &Pool,
resp: &mut Response<Body>,
meth: &Method,
path: &str,
body: Body,
params: HashMap<String, String>,
headers: HeaderMap,
claims: Option<auth::Claim>/* Faster id/perms access from here */) {
const GET: &Method = &Method::GET;
const POST: &Method = &Method::POST;
const DELETE: &Method = &Method::DELETE;
println!("[HTTP] {}: {}", meth, path);
match (meth, path) {
/* INVITES */
(POST, 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, params).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, body, headers, params).await,
(GET, routes::MESSAGE_TIME_RANGE) => messages::get_by_time(pool, resp, params).await,
(GET, routes::MESSAGE_LAST_N) => messages::recent_messages(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,
(GET, routes::GET_MYSELF) => members::get_self(pool, resp, params).await,
(GET, routes::GET_MEMBER) => members::get_member(pool, resp, params).await,
(POST, routes::SELF_UPDATE_NICKNAME) => members::post_self_nickname(pool, resp, params).await,
/* OWNER */
(POST, routes::SET_NEW_ADMIN) => admin::new_admin(pool, resp, params).await,
/* META ROUTE */
(GET, routes::META) => meta::server_meta(resp).await,
/* Federated Routes */
(GET, routes::GET_NEIGHBORS) => meta::server_neighbors(pool, resp).await,
(POST, routes::ADD_NEIGHBOR) => meta::add_neighbor(pool, resp, params).await,
_ => {
println!("[HTTP]\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, body) = request.into_parts();
let method = parts.method;
let path = parts.uri.path();
let qs = parts.uri.query();
let headers = parts.headers;
let params_opt: Option<HashMap<String, String>> = if let Some(query_string) = qs {
Some(http::parse_query_string(query_string))
} else {
None
};
if let Some(qs) = params_opt {
match auth::wall_entry(path, &DB_POOL, &qs).await {
OpenAuth => {
route_dispatcher(&DB_POOL, &mut response, &method, path, body, qs, headers, None).await;
},
// Only with typical routes do we have to inject the permissions
Good(claim) => {
route_dispatcher(&DB_POOL, &mut response, &method, path, body, qs, headers, Some(claim)).await;
},
LoginValid(member) => {
println!("[HTTP] POST /login");
auth::login_get_jwt(&DB_POOL, &mut response, member).await;
},
NoKey | BadKey => {
*response.status_mut() = StatusCode::UNAUTHORIZED;
},
ServerIssue(msg) => {
eprintln!("{}", msg);
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
}
}
}
else {
println!("[HTTP] PARSER: 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, port: u16) -> u16 {
println!("[INFO] HTTP listening on localhost:{}", port);
let addr = SocketAddr::from(([127,0,0,1], port));
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) {
match db::Member::add(&p, name, &enc_secret, perms::OWNER).await {
Ok(response) => match response {
db::Response::Row(mut owner) => {
owner.secret = owner_secret; // giving the secret itself back to the user
let server_config = serde_json::json!({
"user": owner,
"server": meta::get_config()
});
println!("{}", serde_json::to_string_pretty(&server_config).unwrap());
},
_ => eprintln!("SQL server failed to return owner data, check configs and also the members table to make sure there's nothing there by accident")
},
Err(e) => eprintln!("Error communicating with database : {}", e)
};
}
else {
eprintln!("Could not generate a proper secret");
}
let _ = p.disconnect().await;
}
fn init_config() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Deserialize, Debug)]
struct RequiredFields {
pub database_url: String,
pub hmac_path: String,
pub wss_hmac_path: String,
pub name: String,
pub description: Option<String>,
pub url: String,
pub wsurl: String,
pub tags: Option<Vec<String>>
}
use std::fs::File;
use std::io::BufReader;
let file = File::open("config.json")?;
let reader = BufReader::new(file);
let fields: RequiredFields = serde_json::from_reader(reader)?;
// Now we can setup each environment variable for this process from config.json
// Note that we only have to do this once since all of these are read from
// lazy statics so the cost is very minimal
set_var("DATABASE_URL", fields.database_url);
set_var("HMAC_PATH", fields.hmac_path);
set_var("WSS_HMAC_PATH", fields.wss_hmac_path);
set_var("SERVER_NAME", fields.name);
set_var("SERVER_DESCRIPTION", fields.description.unwrap_or("".into()));
set_var("PUBLIC_URL", fields.url);
set_var("PUBLIC_WS_URL", fields.wsurl);
// Mega cheesy way of forcing config initialization
if meta::get_config().tags.len() == 0 {
eprintln!("[API] [WARN] No tags have been set", );
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), u16>{
let mut main_ret: u16 = 0; let d_result = init_config();
// check for a database_url before the override we get from the cmd line
if let Err(d) = d_result {
eprintln!("Config error: {}", d);
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"))
.arg(Arg::with_name("port")
.short("p")
.long("port")
.default_value("4536")
.help("Set the port to use: Default is 4536"))
.arg(Arg::with_name("hmac")
.short("H")
.long("hmac")
.value_name("HMAC")
.help("Sets the path to the hmac.secret file"))
.arg(Arg::with_name("wss-hmac")
.short("w")
.long("wss-hmac")
.value_name("WSS_HMAC")
.help("Sets the path the wss-hmac.secret file"))
.get_matches();
// safe because we have a default value set in code
let port = args.value_of("port").unwrap().to_string();
let port: u16 = port.parse().unwrap_or(4536);
if let Some(owner_name) = args.value_of("create-owner") {
attempt_owner_creation(owner_name).await;
}
// Here we override some of the config.json variables
if let Some(hmac) = args.value_of("hmac") {
std::env::set_var("HMAC_PATH", hmac);
}
if let Some(wss_hmac) = args.value_of("wss-hmac") {
std::env::set_var("WSS_HMAC_PATH", wss_hmac);
}
if args.is_present("server") {
if main_ret == NO_ERR {
main_ret = start_server(main_ret, port).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(())
}
}