use clap::{ app_from_crate, crate_authors, crate_description, crate_name, crate_version, App, Arg, ArgMatches, SubCommand, }; use diesel::prelude::*; use dotenv::dotenv; use std::{ io::{self, BufRead, Write}, path::PathBuf, }; use crate::{models::NewUser, ServerConfig, ServerError}; use crate::{queries, schema}; use slog::{Drain, Logger}; fn generate_cli() -> App<'static, 'static> { app_from_crate!() .arg( Arg::with_name("database") .long("db") .help("The path of the sqlite database") .env("PSLINK_DATABASE") .default_value("links.db") .global(true), ) .arg( Arg::with_name("port") .long("port") .short("p") .help("The port the pslink service will run on") .env("PSLINK_PORT") .default_value("8080") .global(true), ) .arg( Arg::with_name("public_url") .long("public-url") .short("u") .help("The host url or the page that will be part of the short urls.") .env("PSLINK_PUBLIC_URL") .default_value("localhost:8080") .global(true), ) .arg( Arg::with_name("internal_ip") .long("hostip") .short("i") .help("The host (ip) that will run the pslink service") .env("PSLINK_IP") .default_value("localhost") .global(true), ) .arg( Arg::with_name("protocol") .long("protocol") .short("t") .help(concat!( "The protocol that is used in the qr-codes", " (http results in slightly smaller codes in some cases)" )) .env("PSLINK_PROTOCOL") .default_value("http") .possible_values(&["http", "https"]) .global(true), ) .arg( Arg::with_name("secret") .long("secret") .help(concat!( "The secret that is used to encrypt the", " password database keep this as inacessable as possible.", " As commandlineparameters are visible", " to all users", " it is not wise to use this as", " a commandline parameter but rather as an environment variable.", )) .env("PSLINK_SECRET") .default_value("") .global(true), ) .subcommand( SubCommand::with_name("runserver") .about("Run the server") .display_order(1), ) .subcommand( SubCommand::with_name("migrate-database") .about("Apply any pending migrations and exit") .display_order(2), ) .subcommand( SubCommand::with_name("generate-env") .about("Generate an .env file template using default settings and exit") .display_order(2), ) .subcommand( SubCommand::with_name("create-admin") .about("Create an admin user.") .display_order(2), ) } fn parse_args_to_config(config: &ArgMatches, log: &Logger) -> ServerConfig { let secret = config .value_of("secret") .expect("Failed to read the secret") .to_owned(); let secret = if secret.len() < 5 { use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; if secret.is_empty() { slog_warn!( log, "No secret was found! Use the environment variable PSLINK_SECRET to set one." ); slog_warn!( log, "If you change the secret all passwords will be invalid" ); slog_warn!(log, "Using an auto generated one for this run."); } else { slog_warn!( log, "The provided secret was too short. Using an autogenerated one." ) } thread_rng() .sample_iter(&Alphanumeric) .take(30) .map(char::from) .collect() } else { secret }; let db = config .value_of("database") .expect(concat!( "Neither the DATABASE_URL environment variable", " nor the commandline parameters", " contain a valid database location." )) .parse::() .expect("Failed to parse Database path."); let public_url = config .value_of("public_url") .expect("Failed to read the host value") .to_owned(); let internal_ip = config .value_of("internal_ip") .expect("Failed to read the host value") .to_owned(); let port = config .value_of("port") .expect("Failed to read the port value") .parse::() .expect("Failed to parse the portnumber"); let protocol = config .value_of("protocol") .expect("Failed to read the protocol value") .parse::() .expect("Failed to parse the protocol"); let log = log.new(slog_o!("host" => public_url.clone())); crate::ServerConfig { secret, db, public_url, internal_ip, port, protocol, log, } } pub(crate) fn setup() -> Result, ServerError> { dotenv().ok(); let decorator = slog_term::TermDecorator::new().build(); let drain = slog_term::FullFormat::new(decorator).build().fuse(); let drain = slog_async::Async::new(drain).build().fuse(); let log = slog::Logger::root(drain, slog_o!("name" => "Pslink")); slog_info!(log, "Launching Pslink a 'Private short link generator'"); slog_info!(log, ".env file setup, logging initialized"); let app = generate_cli(); let config = app.get_matches(); let server_config: crate::ServerConfig = parse_args_to_config(&config, &log); if let Some(_migrate_config) = config.subcommand_matches("generate-env") { return match generate_env_file(&server_config) { Ok(_) => Ok(None), Err(e) => Err(e), }; } if let Some(_migrate_config) = config.subcommand_matches("migrate-database") { return match apply_migrations(&server_config) { Ok(_) => Ok(None), Err(e) => Err(e), }; } if let Some(_create_config) = config.subcommand_matches("create-admin") { return match create_admin(&server_config) { Ok(_) => Ok(None), Err(e) => Err(e), }; } if let Some(_runserver_config) = config.subcommand_matches("runserver") { let connection = if server_config.db.exists() { queries::establish_connection(&server_config.db)? } else { let msg = format!( concat!( "Database not found at {}!", " Create a new database with: `pslink migrate-database`", "or adjust the databasepath." ), server_config.db.display() ); slog_error!(&server_config.log, "{}", msg); eprintln!("{}", msg); return Ok(None); }; let num_users: i64 = schema::users::dsl::users .filter(schema::users::dsl::role.eq(2)) .select(diesel::dsl::count_star()) .first(&connection) .expect("Failed to count the users"); if num_users < 1 { slog_warn!( &server_config.log, concat!( "No user created you will not be", " able to do anything as the service is invite only.", " Create a user with `pslink create-admin`" ) ); } slog_info!( &server_config.log, "Initialization finished starting the service." ); Ok(Some(server_config)) } else { println!("{}", config.usage()); Err(ServerError::User("Print usage.".into())) } } fn create_admin(config: &ServerConfig) -> Result<(), ServerError> { use schema::users; use schema::users::dsl::{id, role}; slog_info!(&config.log, "Creating an admin user."); let sin = io::stdin(); let connection = queries::establish_connection(&config.db)?; // wait for logging: std::thread::sleep(std::time::Duration::from_millis(100)); print!("Please enter the Username of the admin: "); io::stdout().flush().unwrap(); let new_username = sin.lock().lines().next().unwrap().unwrap(); print!("Please enter the emailadress for {}: ", new_username); io::stdout().flush().unwrap(); let email = sin.lock().lines().next().unwrap().unwrap(); print!("Please enter the password for {}: ", new_username); io::stdout().flush().unwrap(); let password = rpassword::read_password().unwrap(); slog_info!( &config.log, "Creating {} ({}) with given password ", &new_username, &email ); let new_admin = NewUser::new(new_username, email, &password, config)?; diesel::insert_into(users::table) .values(&new_admin) .execute(&connection)?; // Add admin rights to the first user (which should be the only one) diesel::update(users::dsl::users.filter(id.eq(&1))) .set((role.eq(2),)) .execute(&connection)?; slog_info!(&config.log, "Admin user created: {}", &new_admin.username); Ok(()) } fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> { slog_info!( config.log, "Creating a database file and running the migrations:" ); let connection = queries::establish_connection(&config.db)?; crate::embedded_migrations::run_with_output(&connection, &mut std::io::stdout())?; Ok(()) } fn generate_env_file(server_config: &ServerConfig) -> Result<(), ServerError> { if std::path::Path::new(".env").exists() { slog_error!( server_config.log, "ERROR: There already is a .env file - ABORT!" ) } else { slog_info!( server_config.log, "Creating a .env file with default options" ); slog_info!( server_config.log, concat!( "The SECRET_KEY variable is used for password encryption.", "If it is changed all existing passwords are invalid." ) ); let mut file = std::fs::File::create(".env")?; let conf_file_content = server_config.to_env_strings(); conf_file_content.iter().for_each(|l| { file.write_all(l.as_bytes()) .expect("failed to write .env file") }); slog_info!(server_config.log, "Successfully created the env file!") } Ok(()) }