Compare commits

..

11 Commits

Author SHA1 Message Date
1611cfb9a2
Merge pull request #8 from enaut/tracing
Add tracing (eg. jaeger) and tests
2021-04-18 17:20:29 +02:00
680d28ed58
Merge branch 'master' into tracing 2021-04-18 16:40:55 +02:00
b33088057d
Add basic github actions 2021-04-18 16:18:23 +02:00
84625939de
Add more testcases 2021-04-18 16:11:43 +02:00
04170079d6
Add integration test for runserver 2021-04-18 11:38:07 +02:00
7690d301f1
Properly handle absense of a prefered language 2021-04-18 11:37:02 +02:00
322c867e94
Fix: real secret instead of censored in .env 2021-04-18 10:29:09 +02:00
ce315c429c
Add integration tests, do not show secret in logs
The code is restructured so that the library contains the actix-web
code and the binary only does commandline parsing and running of
the lib.
2021-04-18 09:41:17 +02:00
6fd36936a3
Enable jaeger + opentracing logging 2021-04-12 16:32:59 +02:00
ac172670be
simplifying apply_migrations 2021-04-12 16:30:18 +02:00
a3b757abad initial port to tracing 2021-04-11 13:14:11 +02:00
10 changed files with 1383 additions and 511 deletions

22
.github/workflows/rust.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

682
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +1,73 @@
[package]
name = "pslink"
version = "0.3.1"
description = "A simple webservice that allows registered users to create short links including qr-codes.\nAnyone can visit the shortened links. This is an ideal setup for small busines or for publishing papers."
authors = ["Dietrich <dietrich@teilgedanken.de>"]
edition = "2018"
license = "MIT OR Apache-2.0"
keywords = ["url", "link", "webpage", "actix", "web"]
build = "build.rs"
categories = ["web-programming", "network-programming", "web-programming::http-server", "command-line-utilities"]
description = "A simple webservice that allows registered users to create short links including qr-codes.\nAnyone can visit the shortened links. This is an ideal setup for small busines or for publishing papers."
edition = "2018"
keywords = ["url", "link", "webpage", "actix", "web"]
license = "MIT OR Apache-2.0"
name = "pslink"
readme = "README.md"
repository = "https://github.com/enaut/pslink/"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3"
actix-web-static-files = "3.0"
actix-slog = "0.2"
tera = "1.6"
serde = "1.0"
sqlx={version="0.4", features = [ "sqlite", "macros", "runtime-actix-rustls", "chrono", "migrate", "offline" ]}
dotenv = "0.15.0"
actix-identity = "0.3"
chrono = { version = "0.4", features = ["serde"] }
argonautica = "0.2"
slog = { version = "2", features = ["max_level_trace", "release_max_level_info"] }
slog-term = "2"
slog-async = "2"
qrcode = "0.12"
image = "0.23"
rand="0.8"
rpassword = "5.0"
clap = "2.33"
fluent-templates = { version = "0.6", features = ["tera"] }
fluent-langneg = "0.13"
thiserror = "1.0"
anyhow = "1.0"
version = "0.3.1"
[build-dependencies]
actix-web-static-files = "3.0"
# optimize for size at cost of compilation speed.
[dependencies]
actix-identity = "0.3"
actix-rt = "1.1"
actix-slog = "0.2"
actix-web = "3"
actix-web-static-files = "3.0"
anyhow = "1.0"
argonautica = "0.2"
clap = "2.33"
dotenv = "0.15.0"
fluent-langneg = "0.13"
image = "0.23"
opentelemetry = "0.13"
opentelemetry-jaeger = "0.12"
qrcode = "0.12"
rand = "0.8"
rpassword = "5.0"
serde = "1.0"
tera = "1.6"
thiserror = "1.0"
tracing-actix-web = "0.2.1"
tracing-bunyan-formatter = "0.2.0"
tracing-opentelemetry = "0.12"
[dependencies.chrono]
features = ["serde"]
version = "0.4"
[dependencies.fluent-templates]
features = ["tera"]
version = "0.6"
[dependencies.sqlx]
features = ["sqlite", "macros", "runtime-actix-rustls", "chrono", "migrate", "offline"]
version = "0.4"
[dependencies.tracing]
features = ["log"]
version = "0.1"
[dependencies.tracing-subscriber]
features = ["registry", "env-filter"]
version = "0.2.17"
[dev-dependencies]
actix-server = "1.0.4"
tempdir = "0.3"
test_bin = "0.3"
tokio = "0.2.25"
[dev-dependencies.reqwest]
features = ["cookies"]
version = "0.10.10"
[profile]
[profile.release]
lto = true
#codegen-units = 1

View File

@ -12,7 +12,7 @@ use std::{
use pslink::{models::NewUser, models::User, ServerConfig, ServerError};
use slog::{Drain, Logger};
use tracing::{error, info, trace, warn};
static MIGRATOR: Migrator = sqlx::migrate!();
@ -122,7 +122,7 @@ fn generate_cli() -> App<'static, 'static> {
)
}
async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConfig {
async fn parse_args_to_config(config: ArgMatches<'_>) -> ServerConfig {
let secret = config
.value_of("secret")
.expect("Failed to read the secret")
@ -132,20 +132,11 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
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.");
warn!("No secret was found! Use the environment variable PSLINK_SECRET to set one.");
warn!("If you change the secret all passwords will be invalid");
warn!("Using an auto generated one for this run.");
} else {
slog_warn!(
log,
"The provided secret was too short. Using an autogenerated one."
)
warn!("The provided secret was too short. Using an autogenerated one.")
}
thread_rng()
@ -156,6 +147,7 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
} else {
secret
};
let secret = pslink::Secret::new(secret);
let db = config
.value_of("database")
.expect(concat!(
@ -195,8 +187,6 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
.parse::<pslink::Protocol>()
.expect("Failed to parse the protocol");
let log = log.new(slog_o!("host" => public_url.clone()));
crate::ServerConfig {
secret,
db,
@ -205,7 +195,6 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
internal_ip,
port,
protocol,
log,
empty_forward_url,
brand_name,
}
@ -214,16 +203,9 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
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");
info!("Launching Pslink a 'Private short link generator'");
trace!("logging initialized");
let app = generate_cli();
@ -238,9 +220,12 @@ pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError>
))
.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() {
trace!("No database file found {}", db.display());
if !(config.subcommand_matches("migrate-database").is_none()
| config.subcommand_matches("generate-env").is_none())
{
let msg = format!(
concat!(
"Database not found at {}!",
@ -249,17 +234,16 @@ pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError>
),
db.display()
);
slog_error!(log, "{}", msg);
error!("{}", msg);
eprintln!("{}", msg);
return Ok(None);
}
slog_trace!(log, "Creating database: {}", db.display());
trace!("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;
let server_config: crate::ServerConfig = parse_args_to_config(config.clone()).await;
if let Some(_migrate_config) = config.subcommand_matches("generate-env") {
return match generate_env_file(&server_config) {
@ -284,21 +268,15 @@ pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError>
let num_users = User::count_admins(&server_config).await?;
if num_users.number < 1 {
slog_warn!(
&server_config.log,
concat!(
warn!(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.");
trace!("At least one admin user is found.");
}
slog_trace!(
&server_config.log,
"Initialization finished starting the service."
);
trace!("Initialization finished starting the service.");
Ok(Some(server_config))
} else {
println!("{}", config.usage());
@ -308,7 +286,7 @@ pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError>
/// Interactively create a new admin user.
async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
slog_info!(&config.log, "Creating an admin user.");
info!("Creating an admin user.");
let sin = io::stdin();
// wait for logging:
@ -325,11 +303,9 @@ async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
print!("Please enter the password for {}: ", new_username);
io::stdout().flush().unwrap();
let password = rpassword::read_password().unwrap();
slog_info!(
&config.log,
info!(
"Creating {} ({}) with given password ",
&new_username,
&new_email
&new_username, &new_email
);
let new_admin = NewUser::new(new_username.clone(), new_email.clone(), &password, config)?;
@ -338,14 +314,13 @@ async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
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);
info!("Admin user created: {}", new_username);
Ok(())
}
async fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> {
slog_info!(
config.log,
info!(
"Creating a database file and running the migrations in the file {}:",
&config.db.display()
);
@ -355,31 +330,25 @@ async fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> {
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")?;
return Err(ServerError::User(
"ERROR: There already is a .env file - ABORT!".to_string(),
));
}
info!(
r#"Creating a .env file with default options
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())
for line in &conf_file_content {
file.write_all(line.as_bytes())
.expect("failed to write .env file")
});
slog_info!(server_config.log, "Successfully created the env file!")
}
info!("Successfully created the env file!");
Ok(())
}

View File

@ -1,212 +1,62 @@
extern crate sqlx;
#[allow(unused_imports)]
#[macro_use(
slog_o,
slog_trace,
slog_info,
slog_warn,
slog_error,
slog_log,
slog_record,
slog_record_static,
slog_b,
slog_kv
)]
extern crate slog;
extern crate slog_async;
mod cli;
mod views;
use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{web, App, HttpServer};
use anyhow::{Context, Result};
use fluent_templates::{static_loader, FluentLoader};
use tera::Tera;
use pslink::ServerConfig;
use pslink::{ServerConfig, ServerError};
use tracing::instrument;
use tracing::{subscriber::set_global_default, Subscriber};
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
/// Compose multiple layers into a `tracing`'s subscriber.
#[must_use]
pub fn get_subscriber(name: &str, env_filter: &str) -> impl Subscriber + Send + Sync {
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter));
// Create a jaeger exporter pipeline for a `trace_demo` service.
let tracer = opentelemetry_jaeger::new_pipeline()
.with_service_name(name)
.install_simple()
.expect("Error initializing Jaeger exporter");
let formatting_layer = tracing_subscriber::fmt::layer().with_target(false);
static_loader! {
static LOCALES = {
locales: "./locales",
fallback_language: "en",
};
// Create a layer with the configured tracer
let otel_layer = OpenTelemetryLayer::new(tracer);
// Use the tracing subscriber `Registry`, or any other subscriber
// that impls `LookupSpan`
Registry::default()
.with(otel_layer)
.with(env_filter)
.with(formatting_layer)
}
fn build_tera() -> Result<Tera> {
let mut tera = Tera::default();
// Add translation support
tera.register_function("fluent", FluentLoader::new(&*LOCALES));
tera.add_raw_templates(vec![
("admin.html", include_str!("../../../templates/admin.html")),
("base.html", include_str!("../../../templates/base.html")),
(
"edit_link.html",
include_str!("../../../templates/edit_link.html"),
),
(
"edit_profile.html",
include_str!("../../../templates/edit_profile.html"),
),
(
"index_users.html",
include_str!("../../../templates/index_users.html"),
),
("index.html", include_str!("../../../templates/index.html")),
("login.html", include_str!("../../../templates/login.html")),
(
"not_found.html",
include_str!("../../../templates/not_found.html"),
),
(
"signup.html",
include_str!("../../../templates/signup.html"),
),
(
"submission.html",
include_str!("../../../templates/submission.html"),
),
(
"view_link.html",
include_str!("../../../templates/view_link.html"),
),
(
"view_profile.html",
include_str!("../../../templates/view_profile.html"),
),
])
.context("Failed to load Templates")?;
Ok(tera)
}
#[allow(clippy::future_not_send, clippy::too_many_lines)]
async fn webservice(server_config: ServerConfig) -> Result<()> {
let host_port = format!("{}:{}", &server_config.internal_ip, &server_config.port);
let cfg = server_config.clone();
slog_info!(
cfg.log,
"Running on: {}://{}/admin/login/",
&server_config.protocol,
host_port
);
slog_info!(
cfg.log,
"If the public url is set up correctly it should be accessible via: {}://{}/admin/login/",
&server_config.protocol,
&server_config.public_url
);
let tera = build_tera()?;
slog_trace!(cfg.log, "The tera templates are ready");
HttpServer::new(move || {
let generated = generate();
App::new()
.data(server_config.clone())
.wrap(actix_slog::StructuredLogger::new(
server_config.log.new(slog_o!("log_type" => "access")),
))
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&[0; 32])
.name("auth-cookie")
.secure(false),
))
.data(tera.clone())
.service(actix_web_static_files::ResourceFiles::new(
"/static", generated,
))
// directly go to the main page set the target with the environment variable.
.route("/", web::get().to(views::redirect_empty))
// admin block
.service(
web::scope("/admin")
// list all links
.route("/index/", web::get().to(views::index))
// invite users
.route("/signup/", web::get().to(views::signup))
.route("/signup/", web::post().to(views::process_signup))
// logout
.route("/logout/", web::to(views::logout))
// submit a new url for shortening
.route("/submit/", web::get().to(views::create_link))
.route("/submit/", web::post().to(views::process_link_creation))
// view an existing url
.service(
web::scope("/view")
.service(
web::scope("/link")
.route("/{redirect_id}", web::get().to(views::view_link))
.route("/", web::get().to(views::view_link_empty)),
)
.service(
web::scope("/profile")
.route("/{user_id}", web::get().to(views::view_profile)),
)
.route("/users/", web::get().to(views::index_users)),
)
.service(
web::scope("/edit")
.service(
web::scope("/link")
.route("/{redirect_id}", web::get().to(views::edit_link))
.route(
"/{redirect_id}",
web::post().to(views::process_link_edit),
),
)
.service(
web::scope("/profile")
.route("/{user_id}", web::get().to(views::edit_profile))
.route(
"/{user_id}",
web::post().to(views::process_edit_profile),
),
)
.route("/set_admin/{user_id}", web::get().to(views::toggle_admin))
.route(
"/set_language/{language}",
web::get().to(views::set_language),
),
)
.service(
web::scope("/delete").service(
web::scope("/link")
.route("/{redirect_id}", web::get().to(views::process_link_delete)),
),
)
.service(
web::scope("/download")
.route("/png/{redirect_id}", web::get().to(views::download_png)),
)
// login to the admin area
.route("/login/", web::get().to(views::login))
.route("/login/", web::post().to(views::process_login)),
)
// redirect to the url hidden behind the code
.route("/{redirect_id}", web::get().to(views::redirect))
})
.bind(host_port)
.context("Failed to bind to port")
.map_err(|e| {
slog_error!(cfg.log, "Failed to bind to port!");
e
})?
.run()
.await
.context("Failed to run the webservice")
/// Register a subscriber as global default to process span data.
///
/// It should only be called once!
pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
set_global_default(subscriber).expect("Failed to set subscriber");
}
#[instrument]
#[actix_web::main]
async fn main() -> std::result::Result<(), ServerError> {
async fn main() -> std::result::Result<(), std::io::Error> {
let subscriber = get_subscriber("fhs.li", "info");
init_subscriber(subscriber);
match cli::setup().await {
Ok(Some(server_config)) => webservice(server_config).await.map_err(|e| {
Ok(Some(server_config)) => {
pslink::webservice(server_config)
.await
.map_err(|e| {
println!("{:?}", e);
std::thread::sleep(std::time::Duration::from_millis(100));
std::process::exit(0);
}),
})
.expect("Failed to launch the service")
.await
}
Ok(None) => {
std::thread::sleep(std::time::Duration::from_millis(100));
std::process::exit(0);

View File

@ -1,30 +1,23 @@
extern crate sqlx;
#[allow(unused_imports)]
#[macro_use(
slog_o,
slog_info,
slog_warn,
slog_error,
slog_log,
slog_record,
slog_record_static,
slog_b,
slog_kv
)]
extern crate slog;
extern crate slog_async;
pub mod forms;
pub mod models;
pub mod queries;
mod views;
use std::{fmt::Display, path::PathBuf, str::FromStr};
use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::HttpResponse;
use actix_web::{web, App, HttpServer};
use fluent_templates::{static_loader, FluentLoader};
use qrcode::types::QrError;
use sqlx::{Pool, Sqlite};
use std::{fmt::Display, path::PathBuf, str::FromStr};
use tera::Tera;
use thiserror::Error;
use tracing::instrument;
use tracing::{error, info, trace};
use tracing_actix_web::TracingLogger;
#[derive(Error, Debug)]
pub enum ServerError {
#[error("Failed to encrypt the password {0} - aborting!")]
@ -162,16 +155,39 @@ impl FromStr for Protocol {
}
}
#[derive(Clone)]
pub struct Secret {
secret: String,
}
impl Secret {
#[must_use]
pub const fn new(secret: String) -> Self {
Self { secret }
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("*****SECRET*****")
}
}
impl std::fmt::Display for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("*****SECRET*****")
}
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub secret: String,
pub secret: Secret,
pub db: PathBuf,
pub db_pool: Pool<Sqlite>,
pub public_url: String,
pub internal_ip: String,
pub port: u32,
pub protocol: Protocol,
pub log: slog::Logger,
pub empty_forward_url: String,
pub brand_name: String,
}
@ -192,7 +208,176 @@ impl ServerConfig {
"# If it is changed all existing passwords are invalid.\n"
)
.to_owned(),
format!("PSLINK_SECRET=\"{}\"\n", self.secret),
format!("PSLINK_SECRET=\"{}\"\n", self.secret.secret),
]
}
}
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
static_loader! {
static LOCALES = {
locales: "./locales",
fallback_language: "en",
};
}
#[instrument]
fn build_tera() -> Result<Tera, ServerError> {
let mut tera = Tera::default();
// Add translation support
tera.register_function("fluent", FluentLoader::new(&*LOCALES));
tera.add_raw_templates(vec![
("admin.html", include_str!("../templates/admin.html")),
("base.html", include_str!("../templates/base.html")),
(
"edit_link.html",
include_str!("../templates/edit_link.html"),
),
(
"edit_profile.html",
include_str!("../templates/edit_profile.html"),
),
(
"index_users.html",
include_str!("../templates/index_users.html"),
),
("index.html", include_str!("../templates/index.html")),
("login.html", include_str!("../templates/login.html")),
(
"not_found.html",
include_str!("../templates/not_found.html"),
),
("signup.html", include_str!("../templates/signup.html")),
(
"submission.html",
include_str!("../templates/submission.html"),
),
(
"view_link.html",
include_str!("../templates/view_link.html"),
),
(
"view_profile.html",
include_str!("../templates/view_profile.html"),
),
])?;
Ok(tera)
}
/// Launch the pslink-webservice
///
/// # Errors
/// This produces a [`ServerError`] if:
/// * Tera failed to build its templates
/// * The server failed to bind to the designated port.
#[allow(clippy::future_not_send, clippy::too_many_lines)]
pub async fn webservice(
server_config: ServerConfig,
) -> Result<actix_web::dev::Server, std::io::Error> {
let host_port = format!("{}:{}", &server_config.internal_ip, &server_config.port);
info!(
"Running on: {}://{}/admin/login/",
&server_config.protocol, host_port
);
info!(
"If the public url is set up correctly it should be accessible via: {}://{}/admin/login/",
&server_config.protocol, &server_config.public_url
);
let tera = build_tera().expect("Failed to build Templates");
trace!("The tera templates are ready");
let server = HttpServer::new(move || {
let generated = generate();
App::new()
.data(server_config.clone())
.wrap(TracingLogger)
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&[0; 32])
.name("auth-cookie")
.secure(false),
))
.data(tera.clone())
.service(actix_web_static_files::ResourceFiles::new(
"/static", generated,
))
// directly go to the main page set the target with the environment variable.
.route("/", web::get().to(views::redirect_empty))
// admin block
.service(
web::scope("/admin")
// list all links
.route("/index/", web::get().to(views::index))
// invite users
.route("/signup/", web::get().to(views::signup))
.route("/signup/", web::post().to(views::process_signup))
// logout
.route("/logout/", web::to(views::logout))
// submit a new url for shortening
.route("/submit/", web::get().to(views::create_link))
.route("/submit/", web::post().to(views::process_link_creation))
// view an existing url
.service(
web::scope("/view")
.service(
web::scope("/link")
.route("/{redirect_id}", web::get().to(views::view_link))
.route("/", web::get().to(views::view_link_empty)),
)
.service(
web::scope("/profile")
.route("/{user_id}", web::get().to(views::view_profile)),
)
.route("/users/", web::get().to(views::index_users)),
)
.service(
web::scope("/edit")
.service(
web::scope("/link")
.route("/{redirect_id}", web::get().to(views::edit_link))
.route(
"/{redirect_id}",
web::post().to(views::process_link_edit),
),
)
.service(
web::scope("/profile")
.route("/{user_id}", web::get().to(views::edit_profile))
.route(
"/{user_id}",
web::post().to(views::process_edit_profile),
),
)
.route("/set_admin/{user_id}", web::get().to(views::toggle_admin))
.route(
"/set_language/{language}",
web::get().to(views::set_language),
),
)
.service(
web::scope("/delete").service(
web::scope("/link")
.route("/{redirect_id}", web::get().to(views::process_link_delete)),
),
)
.service(
web::scope("/download")
.route("/png/{redirect_id}", web::get().to(views::download_png)),
)
// login to the admin area
.route("/login/", web::get().to(views::login))
.route("/login/", web::post().to(views::process_login)),
)
// redirect to the url hidden behind the code
.route("/{redirect_id}", web::get().to(views::redirect))
})
.bind(host_port)
.map_err(|e| {
error!("Failed to bind to port!");
e
})?
.run();
Ok(server)
}

View File

@ -146,7 +146,7 @@ impl NewUser {
let hash = Hasher::default()
.with_password(password)
.with_secret_key(secret)
.with_secret_key(&secret.secret)
.hash()?;
Ok(hash)
@ -197,7 +197,7 @@ impl Link {
let link = sqlx::query_as!(Self, "Select * from links where code = ? ", code)
.fetch_one(&server_config.db_pool)
.await;
slog_info!(server_config.log, "Found link: {:?}", &link);
tracing::info!("Found link: {:?}", &link);
link.map_err(ServerError::Database)
}

View File

@ -1,6 +1,7 @@
use actix_identity::Identity;
use actix_web::web;
use serde::Serialize;
use tracing::info;
use super::models::{Count, Link, NewUser, User};
use crate::{
@ -170,7 +171,7 @@ pub async fn get_user(
server_config: &ServerConfig,
) -> Result<Item<User>, ServerError> {
if let Ok(uid) = user_id.parse::<i64>() {
slog_info!(server_config.log, "Getting user {}", uid);
info!("Getting user {}", uid);
let auth = authenticate(id, server_config).await?;
if auth.admin_or_self(uid) {
match auth {
@ -214,7 +215,7 @@ pub async fn create_user(
data: &web::Form<NewUser>,
server_config: &ServerConfig,
) -> Result<Item<User>, ServerError> {
slog_info!(server_config.log, "Creating a User: {:?}", &data);
info!("Creating a User: {:?}", &data);
let auth = authenticate(id, server_config).await?;
match auth {
Role::Admin { user } => {
@ -258,7 +259,7 @@ pub async fn update_user(
if auth.admin_or_self(uid) {
match auth {
Role::Admin { .. } | Role::Regular { .. } => {
slog_info!(server_config.log, "Updating userinfo: ");
info!("Updating userinfo: ");
let password = if data.password.len() > 3 {
NewUser::hash_password(&data.password, server_config)?
} else {
@ -303,21 +304,17 @@ pub async fn toggle_admin(
let auth = authenticate(id, server_config).await?;
match auth {
Role::Admin { .. } => {
slog_info!(server_config.log, "Changing administrator priviledges: ");
info!("Changing administrator priviledges: ");
let unchanged_user = User::get_user(uid, server_config).await?;
let old = unchanged_user.role;
unchanged_user.toggle_admin(server_config).await?;
slog_info!(server_config.log, "Toggling role: old was {}", old);
info!("Toggling role: old was {}", old);
let changed_user = User::get_user(uid, server_config).await?;
slog_info!(
server_config.log,
"Toggled role: new is {}",
changed_user.role
);
info!("Toggled role: new is {}", changed_user.role);
Ok(Item {
user: changed_user.clone(),
item: changed_user,
@ -382,10 +379,10 @@ pub async fn get_link_simple(
link_code: &str,
server_config: &ServerConfig,
) -> Result<Link, ServerError> {
slog_info!(server_config.log, "Getting link for {:?}", link_code);
info!("Getting link for {:?}", link_code);
let link = Link::get_link_by_code(link_code, server_config).await?;
slog_info!(server_config.log, "Foun d link for {:?}", link);
info!("Foun d link for {:?}", link);
Ok(link)
}
@ -394,7 +391,7 @@ pub async fn get_link_simple(
/// # Errors
/// Fails with [`ServerError`] if access to the database fails.
pub async fn click_link(link_id: i64, server_config: &ServerConfig) -> Result<(), ServerError> {
slog_info!(server_config.log, "Clicking on {:?}", link_id);
info!("Clicking on {:?}", link_id);
let new_click = NewClick::new(link_id);
new_click.insert_click(server_config).await?;
Ok(())
@ -429,12 +426,7 @@ pub async fn update_link(
data: web::Form<LinkForm>,
server_config: &ServerConfig,
) -> Result<Item<Link>, ServerError> {
slog_info!(
server_config.log,
"Changing link to: {:?} {:?}",
&data,
&link_code
);
info!("Changing link to: {:?} {:?}", &data, &link_code);
let auth = authenticate(id, server_config).await?;
match auth {
Role::Admin { .. } | Role::Regular { .. } => {
@ -472,9 +464,9 @@ pub async fn create_link(
match auth {
Role::Admin { user } | Role::Regular { user } => {
let code = data.code.clone();
slog_info!(server_config.log, "Creating link for: {}", &code);
info!("Creating link for: {}", &code);
let new_link = NewLink::from_link_form(data.into_inner(), user.id);
slog_info!(server_config.log, "Creating link for: {:?}", &new_link);
info!("Creating link for: {:?}", &new_link);
new_link.insert(server_config).await?;
let new_link = get_link_simple(&code, server_config).await?;

View File

@ -15,12 +15,14 @@ use image::{DynamicImage, ImageOutputFormat, Luma};
use qrcode::{render::svg, QrCode};
use queries::{authenticate, Role};
use tera::{Context, Tera};
use tracing::{info, instrument, trace, warn};
use pslink::forms::LinkForm;
use pslink::models::{LoginUser, NewUser};
use pslink::queries;
use pslink::ServerError;
use crate::forms::LinkForm;
use crate::models::{LoginUser, NewUser};
use crate::queries;
use crate::ServerError;
#[instrument]
fn redirect_builder(target: &str) -> HttpResponse {
HttpResponse::SeeOther()
.set(CacheControl(vec![
@ -33,6 +35,7 @@ fn redirect_builder(target: &str) -> HttpResponse {
.body(format!("Redirect to {}", target))
}
#[instrument]
fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
let requested = parse_accepted_languages(
request
@ -62,9 +65,11 @@ fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
}
/// Show the list of all available links if a user is authenticated
#[instrument(skip(id, tera))]
pub async fn index(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
if let Ok(links) = queries::list_all_allowed(&id, &config).await {
@ -80,9 +85,10 @@ pub async fn index(
}
/// Show the list of all available links if a user is authenticated
#[instrument(skip(id, tera))]
pub async fn index_users(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
if let Ok(users) = queries::list_users(&id, &config).await {
@ -97,17 +103,20 @@ pub async fn index_users(
Ok(redirect_builder("/admin/login"))
}
}
#[instrument(skip(id, tera))]
pub async fn view_link_empty(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
view_link(tera, config, id, web::Path::from("".to_owned())).await
}
#[instrument(skip(id, tera))]
pub async fn view_link(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
link_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
@ -144,13 +153,14 @@ pub async fn view_link(
}
}
#[instrument(skip(id, tera))]
pub async fn view_profile(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
user_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
slog_info!(config.log, "Viewing Profile!");
info!("Viewing Profile!");
if let Ok(query) = queries::get_user(&id, &user_id.0, &config).await {
let mut data = Context::new();
data.insert("user", &query.user);
@ -171,13 +181,14 @@ pub async fn view_profile(
}
}
#[instrument(skip(id, tera))]
pub async fn edit_profile(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
user_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
slog_info!(config.log, "Editing Profile!");
info!("Editing Profile!");
if let Ok(query) = queries::get_user(&id, &user_id.0, &config).await {
let mut data = Context::new();
data.insert("user", &query.user);
@ -197,9 +208,10 @@ pub async fn edit_profile(
}
}
#[instrument(skip(id))]
pub async fn process_edit_profile(
data: web::Form<NewUser>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
user_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
@ -210,9 +222,10 @@ pub async fn process_edit_profile(
)))
}
#[instrument(skip(id))]
pub async fn download_png(
id: Identity,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
link_code: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
match queries::get_link(&id, &link_code.0, &config).await {
@ -234,9 +247,10 @@ pub async fn download_png(
}
}
#[instrument(skip(id, tera))]
pub async fn signup(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
match queries::authenticate(&id, &config).await? {
@ -254,12 +268,13 @@ pub async fn signup(
}
}
#[instrument(skip(id))]
pub async fn process_signup(
data: web::Form<NewUser>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
slog_info!(config.log, "Creating a User: {:?}", &data);
info!("Creating a User: {:?}", &data);
match queries::create_user(&id, &data, &config).await {
Ok(item) => {
Ok(HttpResponse::Ok().body(format!("Successfully saved user: {}", item.item.username)))
@ -268,9 +283,10 @@ pub async fn process_signup(
}
}
#[instrument(skip(id))]
pub async fn toggle_admin(
data: web::Path<String>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let update = queries::toggle_admin(&id, &data.0, &config).await?;
@ -280,23 +296,25 @@ pub async fn toggle_admin(
)))
}
#[instrument(skip(id))]
pub async fn set_language(
data: web::Path<String>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
queries::set_language(&id, &data.0, &config).await?;
Ok(redirect_builder("/admin/index/"))
}
#[instrument(skip(tera, id))]
pub async fn login(
tera: web::Data<Tera>,
id: Identity,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
req: HttpRequest,
) -> Result<HttpResponse, ServerError> {
let language_code = detect_language(&req)?;
slog_info!(config.log, "Detected languagecode: {}", &language_code);
let language_code = detect_language(&req).unwrap_or_else(|_| "en".to_string());
info!("Detected languagecode: {}", &language_code);
let mut data = Context::new();
data.insert("title", "Login");
data.insert("language", &language_code);
@ -305,8 +323,7 @@ pub async fn login(
if let Ok(r) = authenticate(&id, &config).await {
match r {
Role::Admin { user } | Role::Regular { user } => {
slog_trace!(
config.log,
trace!(
"This user ({}) is already logged in redirecting to /admin/index/",
user.username
);
@ -315,7 +332,7 @@ pub async fn login(
Role::Disabled | Role::NotAuthenticated => (),
}
}
slog_warn!(config.log, "Invalid user session. The user might be deleted or something tampered with the cookies.");
warn!("Invalid user session. The user might be deleted or something tampered with the cookies.");
id.forget();
}
@ -323,9 +340,10 @@ pub async fn login(
Ok(HttpResponse::Ok().body(rendered))
}
#[instrument(skip(id))]
pub async fn process_login(
data: web::Form<LoginUser>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let user = queries::get_user_by_name(&data.username, &config).await;
@ -336,11 +354,11 @@ pub async fn process_login(
let valid = Verifier::default()
.with_hash(&u.password)
.with_password(&data.password)
.with_secret_key(secret)
.with_secret_key(&secret.secret)
.verify()?;
if valid {
slog_info!(config.log, "Log-in of user: {}", &u.username);
info!("Log-in of user: {}", &u.username);
let session_token = u.username;
id.remember(session_token);
Ok(redirect_builder("/admin/index/"))
@ -349,42 +367,42 @@ pub async fn process_login(
}
}
Err(e) => {
slog_info!(config.log, "Failed to login: {}", e);
info!("Failed to login: {}", e);
Ok(redirect_builder("/admin/login/"))
}
}
}
#[instrument(skip(id))]
pub async fn logout(id: Identity) -> Result<HttpResponse, ServerError> {
info!("Logging out the user");
id.forget();
Ok(redirect_builder("/admin/login/"))
}
#[instrument]
pub async fn redirect(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
data: web::Path<String>,
req: HttpRequest,
) -> Result<HttpResponse, ServerError> {
slog_info!(config.log, "Redirecting to {:?}", data);
info!("Redirecting to {:?}", data);
let link = queries::get_link_simple(&data.0, &config).await;
slog_info!(config.log, "link: {:?}", link);
info!("link: {:?}", link);
match link {
Ok(link) => {
queries::click_link(link.id, &config).await?;
Ok(redirect_builder(&link.target))
}
Err(ServerError::Database(e)) => {
slog_info!(
config.log,
info!(
"Link was not found: http://{}/{} \n {}",
&config.public_url,
&data.0,
e
&config.public_url, &data.0, e
);
let mut data = Context::new();
data.insert("title", "Wurde gel\u{f6}scht");
let language = detect_language(&req)?;
let language = detect_language(&req).unwrap_or_else(|_| "en".to_string());
data.insert("language", &language);
let rendered = tera.render("not_found.html", &data)?;
Ok(HttpResponse::NotFound().body(rendered))
@ -393,15 +411,17 @@ pub async fn redirect(
}
}
#[instrument]
pub async fn redirect_empty(
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
) -> Result<HttpResponse, ServerError> {
Ok(redirect_builder(&config.empty_forward_url))
}
#[instrument(skip(id))]
pub async fn create_link(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
match queries::authenticate(&id, &config).await? {
@ -419,9 +439,10 @@ pub async fn create_link(
}
}
#[instrument(skip(id))]
pub async fn process_link_creation(
data: web::Form<LinkForm>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let new_link = queries::create_link(&id, data, &config).await?;
@ -431,9 +452,10 @@ pub async fn process_link_creation(
)))
}
#[instrument(skip(id))]
pub async fn edit_link(
tera: web::Data<Tera>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
link_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
@ -450,7 +472,7 @@ pub async fn edit_link(
}
pub async fn process_link_edit(
data: web::Form<LinkForm>,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
id: Identity,
link_code: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
@ -463,9 +485,10 @@ pub async fn process_link_edit(
}
}
#[instrument(skip(id))]
pub async fn process_link_delete(
id: Identity,
config: web::Data<pslink::ServerConfig>,
config: web::Data<crate::ServerConfig>,
link_code: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
queries::delete_link(&id, &link_code.0, &config).await?;

349
tests/integration-tests.rs Normal file
View File

@ -0,0 +1,349 @@
#[test]
fn test_help_of_command_for_breaking_changes() {
let output = test_bin::get_test_bin("pslink")
.output()
.expect("Failed to start pslink");
assert!(String::from_utf8_lossy(&output.stdout).contains("USAGE"));
let output = test_bin::get_test_bin("pslink")
.args(&["--help"])
.output()
.expect("Failed to start pslink");
let outstring = String::from_utf8_lossy(&output.stdout);
let args = &[
"USAGE",
"-h",
"--help",
"-b",
"-e",
"-i",
"-p",
"-t",
"-u",
"runserver",
"create-admin",
"generate-env",
"migrate-database",
"help",
];
for s in args {
assert!(
outstring.contains(s),
"{} was not found in the help - this is a breaking change",
s
);
}
}
#[test]
fn test_generate_env() {
use std::io::BufRead;
let tmp_dir = tempdir::TempDir::new("pslink_test_env").expect("create temp dir");
let output = test_bin::get_test_bin("pslink")
.args(&["generate-env", "--secret", "abcdefghijklmnopqrstuvw"])
.current_dir(&tmp_dir)
.output()
.expect("Failed to start pslink");
let envfile = tmp_dir.path().join(".env");
let dbfile = tmp_dir.path().join("links.db");
println!("{}", envfile.display());
println!("{}", dbfile.display());
println!("{}", String::from_utf8_lossy(&output.stdout));
assert!(envfile.exists(), "No .env-file was created!");
assert!(dbfile.exists(), "No database-file was created!");
let envfile = std::fs::File::open(envfile).unwrap();
let envcontent: Vec<Result<String, _>> = std::io::BufReader::new(envfile).lines().collect();
assert!(
envcontent
.iter()
.any(|s| s.as_ref().unwrap().starts_with("PSLINK_PORT=")),
"Failed to find PSLINK_PORT in the generated .env file."
);
assert!(
envcontent
.iter()
.any(|s| s.as_ref().unwrap().starts_with("PSLINK_SECRET=")),
"Failed to find PSLINK_SECRET in the generated .env file."
);
assert!(
!envcontent.iter().any(|s| {
let r = s.as_ref().unwrap().contains("***SECRET***");
r
}),
"It seems that a censored secret was used in the .env file."
);
assert!(
envcontent.iter().any(|s| {
let r = s.as_ref().unwrap().contains("abcdefghijklmnopqrstuvw");
r
}),
"The secret has not made it into the .env file!"
);
let output = test_bin::get_test_bin("pslink")
.args(&["generate-env"])
.current_dir(&tmp_dir)
.output()
.expect("Failed to start pslink");
let second_out = String::from_utf8_lossy(&output.stdout);
assert!(!second_out.contains("secret"));
}
#[actix_rt::test]
async fn test_migrate_database() {
use std::io::Write;
#[derive(serde::Serialize, Debug)]
pub struct Count {
pub number: i32,
}
let tmp_dir = tempdir::TempDir::new("pslink_test_env").expect("create temp dir");
// generate .env file
let _output = test_bin::get_test_bin("pslink")
.args(&["generate-env"])
.current_dir(&tmp_dir)
.output()
.expect("Failed generate .env");
// migrate the database
let output = test_bin::get_test_bin("pslink")
.args(&["migrate-database"])
.current_dir(&tmp_dir)
.output()
.expect("Failed to migrate the database");
println!("{}", String::from_utf8_lossy(&output.stdout));
// check if the users table exists by counting the number of admins.
let db_pool = sqlx::pool::Pool::<sqlx::sqlite::Sqlite>::connect(
&tmp_dir.path().join("links.db").display().to_string(),
)
.await
.expect("Error: Failed to connect to database!");
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
.fetch_one(&db_pool)
.await
.unwrap();
// initially no admin is present
assert_eq!(num.number, 0, "Failed to create the database!");
// create a new admin
let mut input = test_bin::get_test_bin("pslink")
.args(&["create-admin"])
.current_dir(&tmp_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("Failed to migrate the database");
let mut procin = input.stdin.take().unwrap();
procin.write_all(b"test\n").unwrap();
procin.write_all(b"test@mail.test\n").unwrap();
procin.write_all(b"testpw\n").unwrap();
let r = input.wait().unwrap();
println!("Exitstatus is: {}", r);
println!("{}", String::from_utf8_lossy(&output.stdout));
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
.fetch_one(&db_pool)
.await
.unwrap();
// now 1 admin is there
assert_eq!(num.number, 1, "Failed to create an admin!");
}
async fn run_server() {
use std::io::Write;
#[derive(serde::Serialize, Debug)]
pub struct Count {
pub number: i32,
}
let tmp_dir = tempdir::TempDir::new("pslink_test_env").expect("create temp dir");
// generate .env file
let _output = test_bin::get_test_bin("pslink")
.args(&["generate-env", "--secret", "abcdefghijklmnopqrstuvw"])
.current_dir(&tmp_dir)
.output()
.expect("Failed generate .env");
// migrate the database
let output = test_bin::get_test_bin("pslink")
.args(&["migrate-database"])
.current_dir(&tmp_dir)
.output()
.expect("Failed to migrate the database");
// create a database connection.
let db_pool = sqlx::pool::Pool::<sqlx::sqlite::Sqlite>::connect(
&tmp_dir.path().join("links.db").display().to_string(),
)
.await
.expect("Error: Failed to connect to database!"); // create a new admin
let mut input = test_bin::get_test_bin("pslink")
.args(&["create-admin"])
.current_dir(&tmp_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("Failed to migrate the database");
let mut procin = input.stdin.take().unwrap();
procin.write_all(b"test\n").unwrap();
procin.write_all(b"test@mail.test\n").unwrap();
procin.write_all(b"testpw\n").unwrap();
let r = input.wait().unwrap();
println!("Exitstatus is: {}", r);
println!("{}", String::from_utf8_lossy(&output.stdout));
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
.fetch_one(&db_pool)
.await
.unwrap();
// now 1 admin is there
assert_eq!(
num.number, 1,
"Failed to create an admin! See previous tests!"
);
let server_config = pslink::ServerConfig {
secret: pslink::Secret::new("abcdefghijklmnopqrstuvw".to_string()),
db: std::path::PathBuf::from("links.db"),
db_pool,
public_url: "localhost:8080".to_string(),
internal_ip: "localhost".to_string(),
port: 8080,
protocol: pslink::Protocol::Http,
empty_forward_url: "https://github.com/enaut/pslink".to_string(),
brand_name: "Pslink".to_string(),
};
let server = pslink::webservice(server_config);
let _neveruse = tokio::spawn(server);
}
#[actix_rt::test]
async fn test_web_paths() {
run_server().await;
// We need to bring in `reqwest`
// to perform HTTP requests against our application.
let client = reqwest::Client::builder()
.cookie_store(true)
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
// Act
let response = client
.get("http://localhost:8080/")
.send()
.await
.expect("Failed to execute request.");
// The basic redirection is working!
assert!(response.status().is_redirection());
let location = response.headers().get("location").unwrap();
assert!(location.to_str().unwrap().contains("github"));
// Act
let response = client
.get("http://localhost:8080/admin/login/")
.send()
.await
.expect("Failed to execute request.");
// The Loginpage is reachable and contains a password field!
assert!(response.status().is_success());
let content = response.text().await.unwrap();
assert!(
content.contains(r#"<input type="password"#),
"No password field was found!"
);
// Act
let formdata = &[("username", "test"), ("password", "testpw")];
let response = client
.post("http://localhost:8080/admin/login/")
.form(formdata)
.send()
.await
.expect("Failed to execute request.");
// It is possible to login
assert!(response.status().is_redirection());
let location = response.headers().get("location").unwrap();
assert_eq!("/admin/index/", location.to_str().unwrap());
assert!(
response.headers().get("set-cookie").is_some(),
"A auth cookie is not set even though authentication succeeds"
);
// After login this should return a redirect
let response = client
.get("http://localhost:8080/admin/login/")
.send()
.await
.expect("Failed to execute request.");
// The Loginpage redirects to link index when logged in
assert!(
response.status().is_redirection(),
"/admin/login/ is not redirecting correctly when logged in!"
);
let location = response.headers().get("location").unwrap();
assert_eq!("/admin/index/", location.to_str().unwrap());
// After login this should return a redirect
let response = client
.get("http://localhost:8080/admin/index/")
.send()
.await
.expect("Failed to execute request.");
// The Loginpage redirects to link index when logged in
assert!(
response.status().is_success(),
"Could not access /admin/index/"
);
let content = response.text().await.unwrap();
assert!(
content.contains(r#"<a href="/admin/logout/">"#),
"No Logout Button was found on /admin/index/!"
);
// Act title=haupt&target=http%3A%2F%2Fdas.geht%2Fjetzt%2F&code=tpuah
let formdata = &[
("title", "haupt"),
("target", "https://das.geht/jetzt/"),
("code", "tpuah"),
];
let response = client
.post("http://localhost:8080/admin/submit/")
.form(formdata)
.send()
.await
.expect("Failed to execute request.");
// It is possible to login
assert!(response.status().is_redirection());
let location = response.headers().get("location").unwrap();
assert_eq!("/admin/view/link/tpuah", location.to_str().unwrap());
// Act
let response = client
.get("http://localhost:8080/tpuah")
.send()
.await
.expect("Failed to execute request.");
// The basic redirection is working!
assert!(response.status().is_redirection());
let location = response.headers().get("location").unwrap();
assert!(location
.to_str()
.unwrap()
.contains("https://das.geht/jetzt/"));
}