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.

386 lines
12 KiB
Rust

use clap::{
app_from_crate, crate_authors, crate_description, crate_name, crate_version, App, Arg,
ArgMatches, SubCommand,
};
use dotenv::dotenv;
use sqlx::{migrate::Migrator, Pool, Sqlite};
use std::{
fs::File,
io::{self, BufRead, Write},
path::PathBuf,
};
use pslink::{models::NewUser, models::User, ServerConfig, ServerError};
use slog::{Drain, Logger};
static MIGRATOR: Migrator = sqlx::migrate!();
#[allow(clippy::clippy::too_many_lines)]
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("empty_forward_url")
.long("empty-url")
.short("e")
.help("The the url that / will redirect to. Usually your homepage.")
.env("PSLINK_EMPTY_FORWARD_URL")
.default_value("https://github.com/enaut/pslink")
.global(true),
)
.arg(
Arg::with_name("brand_name")
.long("brand-name")
.short("b")
.help("The Brandname that will apper in various places.")
.env("PSLINK_BRAND_NAME")
.default_value("Pslink")
.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),
)
}
async 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 db_pool = Pool::<Sqlite>::connect(&db.display().to_string())
.await
.expect("Error: Failed to connect to database!");
let public_url = config
.value_of("public_url")
.expect("Failed to read the host value")
.to_owned();
let empty_forward_url = config
.value_of("empty_forward_url")
.expect("Failed to read the empty_forward_url value")
.to_owned();
let brand_name = config
.value_of("brand_name")
.expect("Failed to read the brand_name 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::<pslink::Protocol>()
.expect("Failed to parse the protocol");
let log = log.new(slog_o!("host" => public_url.clone()));
crate::ServerConfig {
secret,
db,
db_pool,
public_url,
internal_ip,
port,
protocol,
log,
empty_forward_url,
brand_name,
}
}
pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError> {
dotenv().ok();
// initiallize the logger
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"));
// Print launch info
slog_info!(log, "Launching Pslink a 'Private short link generator'");
slog_trace!(log, "logging initialized");
let app = generate_cli();
let config = app.get_matches();
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.");
if !db.exists() {
slog_trace!(log, "No database file found {}", db.display());
if config.subcommand_matches("migrate-database").is_none() {
let msg = format!(
concat!(
"Database not found at {}!",
" Create a new database with: `pslink migrate-database`",
"or adjust the databasepath."
),
db.display()
);
slog_error!(log, "{}", msg);
eprintln!("{}", msg);
return Ok(None);
}
slog_trace!(log, "Creating database: {}", db.display());
// create an empty database file. The if above makes sure that this file does not exist.
File::create(db)?;
};
let server_config: crate::ServerConfig = parse_args_to_config(config.clone(), log).await;
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).await {
Ok(_) => Ok(None),
Err(e) => Err(e),
};
}
if let Some(_create_config) = config.subcommand_matches("create-admin") {
return match create_admin(&server_config).await {
Ok(_) => Ok(None),
Err(e) => Err(e),
};
}
if let Some(_runserver_config) = config.subcommand_matches("runserver") {
let num_users = User::count_admins(&server_config).await?;
if num_users.number < 1 {
slog_warn!(
&server_config.log,
concat!(
"No admin user created you will not be",
" able to do anything as the service is invite only.",
" Create a user with `pslink create-admin`"
)
);
} else {
slog_trace!(&server_config.log, "At least one admin user is found.");
}
slog_trace!(
&server_config.log,
"Initialization finished starting the service."
);
Ok(Some(server_config))
} else {
println!("{}", config.usage());
Err(ServerError::User("Print usage.".into()))
}
}
/// Interactively create a new admin user.
async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
slog_info!(&config.log, "Creating an admin user.");
let sin = io::stdin();
// 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 new_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,
&new_email
);
let new_admin = NewUser::new(new_username.clone(), new_email.clone(), &password, config)?;
new_admin.insert_user(config).await?;
let created_user = User::get_user_by_name(&new_username, config).await?;
created_user.toggle_admin(config).await?;
slog_info!(&config.log, "Admin user created: {}", new_username);
Ok(())
}
async fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> {
slog_info!(
config.log,
"Creating a database file and running the migrations in the file {}:",
&config.db.display()
);
MIGRATOR.run(&config.db_pool).await?;
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(())
}