From 2c6dbd01ac8716966131e654367e7f4c39e42bf5 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Thu, 1 Sep 2022 20:26:54 +0200 Subject: [PATCH] http tls redirect following --- Cargo.lock | 3 ++ warpgate-common/src/config/mod.rs | 5 +- warpgate-core/src/consts.rs | 1 + warpgate-protocol-http/src/proxy.rs | 23 ++++++++- warpgate/Cargo.toml | 3 ++ warpgate/src/commands/setup.rs | 73 +++++++++++++++++++++++------ warpgate/src/config.rs | 7 +-- 7 files changed, 92 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba9466c..3162f55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4796,6 +4796,8 @@ dependencies = [ "qrcode", "rcgen", "sd-notify", + "sea-orm", + "serde_json", "serde_yaml", "time 0.3.11", "tokio", @@ -4805,6 +4807,7 @@ dependencies = [ "warpgate-admin", "warpgate-common", "warpgate-core", + "warpgate-db-entities", "warpgate-protocol-http", "warpgate-protocol-mysql", "warpgate-protocol-ssh", diff --git a/warpgate-common/src/config/mod.rs b/warpgate-common/src/config/mod.rs index 240a5a7..db790da 100644 --- a/warpgate-common/src/config/mod.rs +++ b/warpgate-common/src/config/mod.rs @@ -170,7 +170,7 @@ impl Default for MySQLConfig { fn default() -> Self { MySQLConfig { enable: false, - listen: _default_http_listen(), + listen: _default_mysql_listen(), certificate: "".to_owned(), key: "".to_owned(), } @@ -225,12 +225,15 @@ pub enum ConfigProviderKind { #[derive(Debug, Deserialize, Serialize, Clone)] pub struct WarpgateConfigStore { #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] pub targets: Vec, #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] pub users: Vec, #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] pub roles: Vec, #[serde(default)] diff --git a/warpgate-core/src/consts.rs b/warpgate-core/src/consts.rs index 832b3c2..9d3506d 100644 --- a/warpgate-core/src/consts.rs +++ b/warpgate-core/src/consts.rs @@ -1,2 +1,3 @@ pub static BUILTIN_ADMIN_TARGET_NAME: &str = "warpgate:admin"; pub static BUILTIN_ADMIN_ROLE_NAME: &str = "warpgate:admin"; +pub static BUILTIN_ADMIN_USERNAME: &str = "admin"; diff --git a/warpgate-protocol-http/src/proxy.rs b/warpgate-protocol-http/src/proxy.rs index a0e3894..84bc37e 100644 --- a/warpgate-protocol-http/src/proxy.rs +++ b/warpgate-protocol-http/src/proxy.rs @@ -101,7 +101,11 @@ fn construct_uri(req: &Request, options: &TargetHTTPOptions, websocket: bool) -> .clone(), ); - let scheme = target_uri.scheme().context("No scheme in the URL")?; + let scheme = match options.tls.mode { + TlsMode::Disabled => &Scheme::HTTP, + TlsMode::Preferred => target_uri.scheme().context("No scheme in the URL")?, + TlsMode::Required => &Scheme::HTTPS, + }; uri = uri.scheme(scheme.clone()); #[allow(clippy::unwrap_used)] @@ -228,6 +232,23 @@ pub async fn proxy_normal_request( if let TlsMode::Required = options.tls.mode { client = client.https_only(true); } + + client = client.redirect(reqwest::redirect::Policy::custom({ + let tls_mode = options.tls.mode.clone(); + let uri = uri.clone(); + move |attempt| { + if tls_mode == TlsMode::Preferred + && uri.scheme() == Some(&Scheme::HTTP) + && attempt.url().scheme() == "https" + { + debug!("Following HTTP->HTTPS redirect"); + attempt.follow() + } else { + attempt.stop() + } + } + })); + if !options.tls.verify { client = client.danger_accept_invalid_certs(true); } diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index 3de0933..9461314 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -21,7 +21,9 @@ futures = "0.3" notify = "^5.0.0-beta.1" qrcode = "0.12" rcgen = {version = "0.9", features = ["zeroize"]} +serde_json = "1.0" serde_yaml = "0.8.23" +sea-orm = { version = "^0.9", default-features = false } time = "0.3" tokio = {version = "1.20", features = ["tracing", "signal", "macros"]} tracing = "0.1" @@ -30,6 +32,7 @@ uuid = "1.0" warpgate-admin = {version = "*", path = "../warpgate-admin"} warpgate-common = {version = "*", path = "../warpgate-common"} warpgate-core = {version = "*", path = "../warpgate-core"} +warpgate-db-entities = {version = "*", path = "../warpgate-db-entities"} warpgate-protocol-http = {version = "*", path = "../warpgate-protocol-http"} warpgate-protocol-mysql = {version = "*", path = "../warpgate-protocol-mysql"} warpgate-protocol-ssh = {version = "*", path = "../warpgate-protocol-ssh"} diff --git a/warpgate/src/commands/setup.rs b/warpgate/src/commands/setup.rs index 254b716..55ffc68 100644 --- a/warpgate/src/commands/setup.rs +++ b/warpgate/src/commands/setup.rs @@ -6,16 +6,18 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use dialoguer::theme::ColorfulTheme; use rcgen::generate_simple_self_signed; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use tracing::*; use uuid::Uuid; use warpgate_common::helpers::fs::{secure_directory, secure_file}; use warpgate_common::helpers::hash::hash_password; use warpgate_common::{ - HTTPConfig, ListenEndpoint, MySQLConfig, SSHConfig, Secret, User, UserAuthCredential, - UserPasswordCredential, WarpgateConfigStore, + HTTPConfig, ListenEndpoint, MySQLConfig, SSHConfig, Secret, UserAuthCredential, + UserPasswordCredential, UserRequireCredentialsPolicy, WarpgateConfigStore, WarpgateError, }; -use warpgate_core::consts::BUILTIN_ADMIN_ROLE_NAME; +use warpgate_core::consts::{BUILTIN_ADMIN_ROLE_NAME, BUILTIN_ADMIN_USERNAME}; use warpgate_core::Services; +use warpgate_db_entities::{Role, User, UserRoleAssignment}; use crate::config::load_config; @@ -187,20 +189,10 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { // --- - let password = dialoguer::Password::with_theme(&theme) + let admin_password = dialoguer::Password::with_theme(&theme) .with_prompt("Set a password for the Warpgate admin user") .interact()?; - store.users.push(User { - id: Uuid::new_v4(), - username: "admin".into(), - credentials: vec![UserAuthCredential::Password(UserPasswordCredential { - hash: Secret::new(hash_password(&password)), - })], - credential_policy: None, - roles: vec![BUILTIN_ADMIN_ROLE_NAME.into()], - }); - // --- info!("Generated configuration:"); @@ -211,10 +203,61 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { info!("Saved into {}", cli.config.display()); let config = load_config(&cli.config, true)?; - Services::new(config.clone()).await?; + let services = Services::new(config.clone()).await?; warpgate_protocol_ssh::generate_host_keys(&config)?; warpgate_protocol_ssh::generate_client_keys(&config)?; + { + let db = services.db.lock().await; + + let admin_role = Role::Entity::find() + .filter(Role::Column::Name.eq(BUILTIN_ADMIN_ROLE_NAME)) + .all(&*db) + .await? + .into_iter() + .next() + .ok_or(anyhow::anyhow!("Database inconsistent: no admin role"))?; + + let admin_user = match User::Entity::find() + .filter(User::Column::Username.eq(BUILTIN_ADMIN_USERNAME)) + .all(&*db) + .await? + .first() + { + Some(x) => x.to_owned(), + None => { + let values = User::ActiveModel { + id: Set(Uuid::new_v4()), + username: Set(BUILTIN_ADMIN_USERNAME.to_owned()), + credentials: Set(serde_json::to_value(vec![UserAuthCredential::Password( + UserPasswordCredential { + hash: Secret::new(hash_password(&admin_password)), + }, + )])?), + credential_policy: Set(serde_json::to_value( + None::, + )?), + }; + values.insert(&*db).await.map_err(WarpgateError::from)? + } + }; + + if UserRoleAssignment::Entity::find() + .filter(UserRoleAssignment::Column::UserId.eq(admin_user.id)) + .filter(UserRoleAssignment::Column::RoleId.eq(admin_role.id)) + .all(&*db) + .await? + .is_empty() + { + let values = UserRoleAssignment::ActiveModel { + user_id: Set(admin_user.id), + role_id: Set(admin_role.id), + ..Default::default() + }; + values.insert(&*db).await.map_err(WarpgateError::from)?; + } + } + { info!("Generating a TLS certificate"); let cert = generate_simple_self_signed(vec![ diff --git a/warpgate/src/config.rs b/warpgate/src/config.rs index bbb54e6..bf2a10e 100644 --- a/warpgate/src/config.rs +++ b/warpgate/src/config.rs @@ -32,12 +32,7 @@ pub fn load_config(path: &Path, secure: bool) -> Result { paths_relative_to: path.parent().context("FS root reached")?.to_path_buf(), }; - info!( - "Using config: {path:?} (users: {}, targets: {}, roles: {})", - config.store.users.len(), - config.store.targets.len(), - config.store.roles.len(), - ); + info!("Using config: {path:?}"); Ok(config) }