
➕ /channels/list now takes a "type" parameter which defaults to TEXT_CHANNEL(1) ✨ Refactoring db::Channel::filter to use a more latency friendly approach ✨ db::Channel::Filter now returns Result<Response<Self>, SqlError>
270 lines
8.8 KiB
Rust
270 lines
8.8 KiB
Rust
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 std::collections::HashMap;
|
|
|
|
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,
|
|
body: Body,
|
|
params: HashMap<String, String>) {
|
|
|
|
const GET: &Method = &Method::GET;
|
|
const POST: &Method = &Method::POST;
|
|
const DELETE: &Method = &Method::DELETE;
|
|
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, 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,
|
|
(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,
|
|
_ => {
|
|
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, body) = request.into_parts();
|
|
let method = parts.method;
|
|
let path = parts.uri.path();
|
|
let qs = parts.uri.query();
|
|
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(params) = params_opt {
|
|
let mysql_pool = Pool::new(&env::var("DATABASE_URL").unwrap());
|
|
match auth::wall_entry(path, &mysql_pool, ¶ms).await {
|
|
OpenAuth | Good => route_dispatcher(&mysql_pool, &mut response, &method, path, body, 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, port: u16) -> u16 {
|
|
println!("Servering 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) {
|
|
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
|
|
let server_config = serde_json::json!({
|
|
"user": owner,
|
|
"server": meta::get_config()
|
|
});
|
|
println!("{}", serde_json::to_string_pretty(&server_config).unwrap());
|
|
},
|
|
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"))
|
|
.arg(Arg::with_name("port")
|
|
.short("p")
|
|
.long("port")
|
|
.default_value("4536")
|
|
.help("Set the port to use: Default is 4536"))
|
|
.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);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
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(())
|
|
}
|
|
}
|