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, meth: &Method, path: &str, body: Body, params: HashMap, headers: HeaderMap, claims: Option/* 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) -> Result, 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> = 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> { #[derive(Deserialize, Debug)] struct RequiredFields { pub database_url: String, pub hmac_path: String, pub wss_hmac_path: String, pub name: String, pub description: Option, pub url: String, pub wsurl: String, pub tags: Option> } 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(()) } }