extern crate db; 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}; use auth::AuthReason; mod auth; mod routes; mod invites; mod channels; mod members; mod perms; mod messages; mod admin; mod http_params; 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, 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; 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, /* 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, /* 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, _ => { // 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) { match (meth, route.base.as_str()) { (&Method::GET, routes::DYN_JOIN) => invites::join(pool, resp, params).await, _ => { println!("\tNOT FOUND: {}: {}", meth, path); *resp.status_mut() = StatusCode::NOT_FOUND } } } else { println!("\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, 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 | BadKey => { 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 Creates an account with full permissions in the SQL database. -d, --db-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()); eprintln!("Creating owner {{ {} }}...", owner_name); let owner_secret = auth::generate_secret(); // creation of owner should just dump straight to stdout since this fires // from a commandline parameter anyway if let Ok(enc_secret) = auth::encrypt_secret(&owner_secret) { match db::member::Member::add(&p, owner_name, &enc_secret, perms::OWNER).await { Ok(response) => { match response { db::Response::Row(owner) => 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"); }, _ => {} }; }, Err(_) => { eprintln!("Could not communicate with the SQL server, check your configs!"); } } } else { eprintln!("Could not generate a proper secret"); } //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(()) } }