From ce315c429cb9160fc7292cb4e075efb43e341740 Mon Sep 17 00:00:00 2001 From: Dietrich Date: Sun, 18 Apr 2021 09:41:17 +0200 Subject: [PATCH] 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. --- Cargo.lock | 291 +++++++++++++++++++++++++++++++++- Cargo.toml | 99 +++++++----- src/bin/pslink/cli.rs | 7 +- src/bin/pslink/main.rs | 196 ++--------------------- src/lib.rs | 208 +++++++++++++++++++++++- src/models.rs | 2 +- src/{bin/pslink => }/views.rs | 54 +++---- tests/integration-tests.rs | 215 +++++++++++++++++++++++++ 8 files changed, 813 insertions(+), 259 deletions(-) rename src/{bin/pslink => }/views.rs (92%) create mode 100644 tests/integration-tests.rs diff --git a/Cargo.lock b/Cargo.lock index 1825e85..7d0b35f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -897,6 +897,22 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + [[package]] name = "cpuid-bool" version = "0.1.2" @@ -1247,6 +1263,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -1625,12 +1656,28 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes 0.5.6", + "http", +] + [[package]] name = "httparse" version = "1.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc35c995b9d93ec174cf9a27d425c7892722101e14993cd227fdb51d70cf9589" +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + [[package]] name = "humansize" version = "1.1.0" @@ -1646,6 +1693,43 @@ dependencies = [ "quick-error", ] +[[package]] +name = "hyper" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +dependencies = [ + "bytes 0.5.6", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project 1.0.6", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +dependencies = [ + "bytes 0.5.6", + "hyper", + "native-tls", + "tokio", + "tokio-tls", +] + [[package]] name = "idna" version = "0.2.2" @@ -1757,9 +1841,15 @@ dependencies = [ "socket2", "widestring", "winapi 0.3.9", - "winreg", + "winreg 0.6.2", ] +[[package]] +name = "ipnet" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + [[package]] name = "itoa" version = "0.4.7" @@ -2016,6 +2106,24 @@ dependencies = [ "getrandom 0.2.2", ] +[[package]] +name = "native-tls" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "net2" version = "0.2.37" @@ -2125,6 +2233,39 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-sys" +version = "0.9.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f" +dependencies = [ + "autocfg 1.0.1", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.13.0" @@ -2433,6 +2574,7 @@ name = "pslink" version = "0.3.1" dependencies = [ "actix-identity", + "actix-rt", "actix-slog", "actix-web", "actix-web-static-files", @@ -2448,10 +2590,13 @@ dependencies = [ "opentelemetry-jaeger", "qrcode", "rand 0.8.3", + "reqwest", "rpassword", "serde", "sqlx", + "tempdir", "tera", + "test_bin", "thiserror", "tracing", "tracing-actix-web", @@ -2779,6 +2924,41 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "reqwest" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" +dependencies = [ + "base64 0.13.0", + "bytes 0.5.6", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.6", + "serde", + "serde_urlencoded", + "tokio", + "tokio-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.7.0", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -2863,6 +3043,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -2891,6 +3081,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" @@ -3340,6 +3553,20 @@ dependencies = [ "remove_dir_all", ] +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.3", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + [[package]] name = "tera" version = "1.7.0" @@ -3371,6 +3598,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test_bin" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1937bab04b0bd0f2c19628d3408fb6dd78faf5aa60f4bdb03212507a9b7314ba" + [[package]] name = "textwrap" version = "0.11.0" @@ -3518,6 +3751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" dependencies = [ "bytes 0.5.6", + "fnv", "futures-core", "iovec", "lazy_static", @@ -3556,6 +3790,16 @@ dependencies = [ "webpki", ] +[[package]] +name = "tokio-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.3.1" @@ -3570,6 +3814,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + [[package]] name = "tracing" version = "0.1.25" @@ -3738,6 +3988,12 @@ dependencies = [ "trust-dns-proto", ] +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + [[package]] name = "type-map" version = "0.4.0" @@ -3981,6 +4237,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -4000,6 +4266,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" dependencies = [ "cfg-if 1.0.0", + "serde", + "serde_json", "wasm-bindgen-macro", ] @@ -4018,6 +4286,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.73" @@ -4160,6 +4440,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index fe55603..b6b77ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,51 +1,68 @@ [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 "] -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" -tracing = { version = "0.1", features = ["log"] } -tracing-bunyan-formatter = "0.2.0" -tracing-subscriber = { version = "0.2.17", features = ["registry", "env-filter"] } -tracing-actix-web = "0.2.1" -tracing-opentelemetry = "0.12" -opentelemetry = "0.13" -opentelemetry-jaeger="0.12" -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] +reqwest = "0.10.10" +tempdir = "0.3" +test_bin = "0.3" + +[profile] [profile.release] -lto = true +lto = true #timize for size at cost of compilation speed. #codegen-units = 1 \ No newline at end of file diff --git a/src/bin/pslink/cli.rs b/src/bin/pslink/cli.rs index db4b6a4..e92ed8d 100644 --- a/src/bin/pslink/cli.rs +++ b/src/bin/pslink/cli.rs @@ -147,6 +147,7 @@ async fn parse_args_to_config(config: ArgMatches<'_>) -> ServerConfig { } else { secret }; + let secret = pslink::Secret::new(secret); let db = config .value_of("database") .expect(concat!( @@ -219,9 +220,12 @@ pub(crate) async fn setup() -> Result, ServerError> )) .parse::() .expect("Failed to parse Database path."); + if !db.exists() { trace!("No database file found {}", db.display()); - if config.subcommand_matches("migrate-database").is_none() { + if !(config.subcommand_matches("migrate-database").is_none() + | config.subcommand_matches("generate-env").is_none()) + { let msg = format!( concat!( "Database not found at {}!", @@ -239,7 +243,6 @@ pub(crate) async fn setup() -> Result, ServerError> // 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()).await; if let Some(_migrate_config) = config.subcommand_matches("generate-env") { diff --git a/src/bin/pslink/main.rs b/src/bin/pslink/main.rs index f47d891..4122311 100644 --- a/src/bin/pslink/main.rs +++ b/src/bin/pslink/main.rs @@ -1,20 +1,11 @@ extern crate sqlx; 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, ServerError}; +use pslink::ServerConfig; use tracing::instrument; -use tracing::{error, info, trace}; use tracing::{subscriber::set_global_default, Subscriber}; -use tracing_actix_web::TracingLogger; use tracing_opentelemetry::OpenTelemetryLayer; use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; @@ -48,185 +39,24 @@ pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) { set_global_default(subscriber).expect("Failed to set subscriber"); } -include!(concat!(env!("OUT_DIR"), "/generated.rs")); - -static_loader! { - static LOCALES = { - locales: "./locales", - fallback_language: "en", - }; -} - -#[instrument] -fn build_tera() -> Result { - 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); - 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()?; - trace!("The tera templates are ready"); - - 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) - .context("Failed to bind to port") - .map_err(|e| { - error!("Failed to bind to port!"); - e - })? - .run() - .await - .context("Failed to run the webservice") -} - #[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| { - println!("{:?}", e); - std::thread::sleep(std::time::Duration::from_millis(100)); - std::process::exit(0); - }), + 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); diff --git a/src/lib.rs b/src/lib.rs index d35be74..7402241 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,14 +3,21 @@ extern crate sqlx; 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!")] @@ -148,9 +155,33 @@ 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, pub public_url: String, @@ -181,3 +212,172 @@ impl ServerConfig { ] } } + +include!(concat!(env!("OUT_DIR"), "/generated.rs")); + +static_loader! { + static LOCALES = { + locales: "./locales", + fallback_language: "en", + }; +} + +#[instrument] +fn build_tera() -> Result { + 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 { + 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()?; + 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) +} diff --git a/src/models.rs b/src/models.rs index 033460d..c88f826 100644 --- a/src/models.rs +++ b/src/models.rs @@ -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) diff --git a/src/bin/pslink/views.rs b/src/views.rs similarity index 92% rename from src/bin/pslink/views.rs rename to src/views.rs index 5695010..7963ece 100644 --- a/src/bin/pslink/views.rs +++ b/src/views.rs @@ -17,10 +17,10 @@ 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 { @@ -69,7 +69,7 @@ fn detect_language(request: &HttpRequest) -> Result { #[instrument(skip(id, tera))] pub async fn index( tera: web::Data, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { if let Ok(links) = queries::list_all_allowed(&id, &config).await { @@ -88,7 +88,7 @@ pub async fn index( #[instrument(skip(id, tera))] pub async fn index_users( tera: web::Data, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { if let Ok(users) = queries::list_users(&id, &config).await { @@ -107,7 +107,7 @@ pub async fn index_users( #[instrument(skip(id, tera))] pub async fn view_link_empty( tera: web::Data, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { view_link(tera, config, id, web::Path::from("".to_owned())).await @@ -116,7 +116,7 @@ pub async fn view_link_empty( #[instrument(skip(id, tera))] pub async fn view_link( tera: web::Data, - config: web::Data, + config: web::Data, id: Identity, link_id: web::Path, ) -> Result { @@ -156,7 +156,7 @@ pub async fn view_link( #[instrument(skip(id, tera))] pub async fn view_profile( tera: web::Data, - config: web::Data, + config: web::Data, id: Identity, user_id: web::Path, ) -> Result { @@ -184,7 +184,7 @@ pub async fn view_profile( #[instrument(skip(id, tera))] pub async fn edit_profile( tera: web::Data, - config: web::Data, + config: web::Data, id: Identity, user_id: web::Path, ) -> Result { @@ -211,7 +211,7 @@ pub async fn edit_profile( #[instrument(skip(id))] pub async fn process_edit_profile( data: web::Form, - config: web::Data, + config: web::Data, id: Identity, user_id: web::Path, ) -> Result { @@ -225,7 +225,7 @@ pub async fn process_edit_profile( #[instrument(skip(id))] pub async fn download_png( id: Identity, - config: web::Data, + config: web::Data, link_code: web::Path, ) -> Result { match queries::get_link(&id, &link_code.0, &config).await { @@ -250,7 +250,7 @@ pub async fn download_png( #[instrument(skip(id, tera))] pub async fn signup( tera: web::Data, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { match queries::authenticate(&id, &config).await? { @@ -271,7 +271,7 @@ pub async fn signup( #[instrument(skip(id))] pub async fn process_signup( data: web::Form, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { info!("Creating a User: {:?}", &data); @@ -286,7 +286,7 @@ pub async fn process_signup( #[instrument(skip(id))] pub async fn toggle_admin( data: web::Path, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { let update = queries::toggle_admin(&id, &data.0, &config).await?; @@ -299,18 +299,18 @@ pub async fn toggle_admin( #[instrument(skip(id))] pub async fn set_language( data: web::Path, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { queries::set_language(&id, &data.0, &config).await?; Ok(redirect_builder("/admin/index/")) } -#[instrument(skip(id))] +#[instrument(skip(tera, id))] pub async fn login( tera: web::Data, id: Identity, - config: web::Data, + config: web::Data, req: HttpRequest, ) -> Result { let language_code = detect_language(&req)?; @@ -343,7 +343,7 @@ pub async fn login( #[instrument(skip(id))] pub async fn process_login( data: web::Form, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { let user = queries::get_user_by_name(&data.username, &config).await; @@ -354,7 +354,7 @@ 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 { @@ -383,7 +383,7 @@ pub async fn logout(id: Identity) -> Result { #[instrument] pub async fn redirect( tera: web::Data, - config: web::Data, + config: web::Data, data: web::Path, req: HttpRequest, ) -> Result { @@ -413,7 +413,7 @@ pub async fn redirect( #[instrument] pub async fn redirect_empty( - config: web::Data, + config: web::Data, ) -> Result { Ok(redirect_builder(&config.empty_forward_url)) } @@ -421,7 +421,7 @@ pub async fn redirect_empty( #[instrument(skip(id))] pub async fn create_link( tera: web::Data, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { match queries::authenticate(&id, &config).await? { @@ -442,7 +442,7 @@ pub async fn create_link( #[instrument(skip(id))] pub async fn process_link_creation( data: web::Form, - config: web::Data, + config: web::Data, id: Identity, ) -> Result { let new_link = queries::create_link(&id, data, &config).await?; @@ -455,7 +455,7 @@ pub async fn process_link_creation( #[instrument(skip(id))] pub async fn edit_link( tera: web::Data, - config: web::Data, + config: web::Data, id: Identity, link_id: web::Path, ) -> Result { @@ -472,7 +472,7 @@ pub async fn edit_link( } pub async fn process_link_edit( data: web::Form, - config: web::Data, + config: web::Data, id: Identity, link_code: web::Path, ) -> Result { @@ -488,7 +488,7 @@ pub async fn process_link_edit( #[instrument(skip(id))] pub async fn process_link_delete( id: Identity, - config: web::Data, + config: web::Data, link_code: web::Path, ) -> Result { queries::delete_link(&id, &link_code.0, &config).await?; diff --git a/tests/integration-tests.rs b/tests/integration-tests.rs new file mode 100644 index 0000000..d9ee3f6 --- /dev/null +++ b/tests/integration-tests.rs @@ -0,0 +1,215 @@ +#[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"]) + .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 mut envcontent = std::io::BufReader::new(envfile).lines(); + assert!( + envcontent.any(|s| s.as_ref().unwrap().starts_with("PSLINK_PORT=")), + "Failed to find DATABASE_URL in the generated .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::::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"]) + .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::::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 output = test_bin::get_test_bin("pslink") + .args(&["runserver"]) + .current_dir(&tmp_dir) + .spawn() + .expect("Failed to migrate the database"); + let out = output.wait_with_output().unwrap(); + println!("{}", String::from_utf8_lossy(&out.stdout)); +} + +#[actix_rt::test] +async fn test_web_paths() { + actix_rt::spawn(run_server()); + + std::thread::sleep(std::time::Duration::new(5, 0)); + // We need to bring in `reqwest` + // to perform HTTP requests against our application. + let client = reqwest::Client::new(); + + // Act + let response = client + .get("http://127.0.0.1:8080/admin/login/") + .send() + .await + .expect("Failed to execute request."); + + // Assert + assert!(response.status().is_success()); + //assert_eq!(Some(0), response.content_length()); +} */