Archived
1
0
This repository has been archived on 2025-01-25. You can view files and clone it, but cannot push or open issues or pull requests.
Pslink/src/cli.rs
Dietrich 9512807eb3
Add command line interface
Add a command line interface to the binary and remove parts
that were hardcoded.

new --help is:

```
pslink 0.1.0
Dietrich <dietrich@teilgedanken.de>
A simple webservice that allows registered users to create short links including qr-codes.
Anyone can visit the shortened links. This is an ideal setup for small busines or for publishing papers.

USAGE:
    pslink [OPTIONS] [SUBCOMMAND]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
        --db <database>              The path of the sqlite database [env: PSLINK_DATABASE=]  [default: links.db]
    -i, --hostip <internal_ip>       The host (ip) that will run the pslink service [env: PSLINK_IP=]  [default:
                                     localhost]
    -p, --port <port>                The port the pslink service will run on [env: PSLINK_PORT=]  [default: 8080]
    -t, --protocol <protocol>        The protocol that is used in the qr-codes (http results in slightly smaller codes
                                     in some cases) [env: PSLINK_PROTOCOL=]  [default: http]  [possible values: http,
                                     https]
    -u, --public-url <public_url>    The host url or the page that will be part of the short urls. [env:
                                     PSLINK_PUBLIC_URL=]  [default: localhost:8080]
        --secret <secret>            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: ]

SUBCOMMANDS:
    runserver           Run the server
    create-admin        Create an admin user.
    generate-env        Generate an .env file template using default settings and exit
    migrate-database    Apply any pending migrations and exit
    help                Prints this message or the help of the given subcommand(s)
```
2021-03-07 19:14:34 +01:00

341 lines
11 KiB
Rust

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::<PathBuf>()
.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::<u32>()
.expect("Failed to parse the portnumber");
let protocol = config
.value_of("protocol")
.expect("Failed to read the protocol value")
.parse::<crate::Protocol>()
.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<Option<crate::ServerConfig>, 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(())
}