Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: download debugging/support information and logs or send them via email #277

Merged
merged 21 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 2 additions & 43 deletions src/bin/defguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,26 @@ use defguard::{
config::{Command, DefGuardConfig},
db::{init_db, AppEvent, GatewayEvent, User},
grpc::{run_grpc_server, GatewayMap, WorkerState},
init_dev_env,
init_dev_env, logging,
mail::{run_mail_handler, Mail},
run_web_server,
wireguard_stats_purge::run_periodic_stats_purge,
SERVER_CONFIG,
};
use fern::{
colors::{Color, ColoredLevelConfig},
Dispatch,
};
use log::{LevelFilter, SetLoggerError};
use std::{
fs::read_to_string,
str::FromStr,
sync::{Arc, Mutex},
};
use tokio::sync::{broadcast, mpsc::unbounded_channel};

/// Configures fern logging library.
fn logger_setup(log_level: &str) -> Result<(), SetLoggerError> {
let colors = ColoredLevelConfig::new()
.trace(Color::BrightWhite)
.debug(Color::BrightCyan)
.info(Color::BrightGreen)
.warn(Color::BrightYellow)
.error(Color::BrightRed);
Dispatch::new()
.format(move |out, message, record| {
// explicitly handle potentially malicious escape sequences
let mut formatted_message = String::new();
for c in message.to_string().chars() {
match c {
'\n' => formatted_message.push_str("\\n"),
'\r' => formatted_message.push_str("\\r"),
'\u{0008}' => formatted_message.push_str("\\u{{0008}}"),
_ => formatted_message.push(c),
}
}

out.finish(format_args!(
"[{}][{}][{}] {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
colors.color(record.level()),
record.target(),
formatted_message
));
})
.level(LevelFilter::from_str(log_level).unwrap_or(LevelFilter::Info))
.level_for("sqlx", LevelFilter::Warn)
.chain(std::io::stdout())
.apply()
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
if dotenvy::from_filename(".env.local").is_err() {
dotenvy::dotenv().ok();
}
let config = DefGuardConfig::new();
logging::init(&config.log_level, &config.log_file)?;
SERVER_CONFIG.set(config.clone())?;
logger_setup(&config.log_level)?;
match config.openid_signing_key {
Some(_) => log::info!("Using RSA OpenID signing key"),
None => log::info!("Using HMAC OpenID signing key"),
Expand Down
11 changes: 10 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ use openidconnect::{core::CoreRsaPrivateSigningKey, JsonWebKeyId};
use reqwest::Url;
use rsa::{pkcs1::EncodeRsaPrivateKey, pkcs8::DecodePrivateKey, PublicKeyParts, RsaPrivateKey};

#[derive(Clone, Debug, Parser)]
#[derive(Clone, Parser, Serialize, Debug)]
#[command(version)]
pub struct DefGuardConfig {
#[arg(long, env = "DEFGUARD_LOG_LEVEL", default_value = "info")]
pub log_level: String,

#[arg(long, env = "DEFGUARD_LOG_FILE")]
pub log_file: Option<String>,

#[arg(long, env = "DEFGUARD_AUTH_SESSION_LIFETIME")]
pub session_auth_lifetime: Option<i64>,

Expand Down Expand Up @@ -54,6 +57,7 @@ pub struct DefGuardConfig {
pub default_admin_password: String,

#[arg(long, env = "DEFGUARD_OPENID_KEY", value_parser = Self::parse_openid_key)]
#[serde(skip_serializing)]
pub openid_signing_key: Option<RsaPrivateKey>,

// relying party id and relying party origin for WebAuthn
Expand Down Expand Up @@ -133,25 +137,30 @@ pub struct DefGuardConfig {
pub disable_stats_purge: bool,

#[arg(long, env = "DEFGUARD_STATS_PURGE_FREQUENCY", default_value = "24h")]
#[serde(skip_serializing)]
pub stats_purge_frequency: Duration,

#[arg(long, env = "DEFGUARD_STATS_PURGE_THRESHOLD", default_value = "30d")]
#[serde(skip_serializing)]
pub stats_purge_threshold: Duration,

#[arg(long, env = "DEFGUARD_ENROLLMENT_URL", value_parser = Url::parse, default_value = "http://localhost:8080")]
pub enrollment_url: Url,

#[arg(long, env = "DEFGUARD_ENROLLMENT_TOKEN_TIMEOUT", default_value = "24h")]
#[serde(skip_serializing)]
pub enrollment_token_timeout: Duration,

#[arg(
long,
env = "DEFGUARD_ENROLLMENT_SESSION_TIMEOUT",
default_value = "10m"
)]
#[serde(skip_serializing)]
pub enrollment_session_timeout: Duration,

#[command(subcommand)]
#[serde(skip_serializing)]
pub cmd: Option<Command>,
}

Expand Down
4 changes: 2 additions & 2 deletions src/db/models/enrollment.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use crate::{
db::{DbPool, Settings, User},
handlers::VERSION,
mail::Mail,
random::gen_alphanumeric,
templates::{self, TemplateError},
SERVER_CONFIG,
SERVER_CONFIG, VERSION,
};
use chrono::{Duration, NaiveDateTime, Utc};
use reqwest::Url;
Expand Down Expand Up @@ -362,6 +361,7 @@ impl User {
&enrollment.id,
)
.map_err(|err| EnrollmentError::NotificationError(err.to_string()))?,
attachments: Vec::new(),
result_tx: None,
};
match mail_tx.send(mail) {
Expand Down
2 changes: 1 addition & 1 deletion src/db/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub enum MFAMethod {
Web3,
}

#[derive(Model, PartialEq)]
#[derive(Model, PartialEq, Serialize)]
pub struct User {
pub id: Option<i64>,
pub username: String,
Expand Down
2 changes: 2 additions & 0 deletions src/grpc/enrollment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ impl Enrollment {
to: user.email.clone(),
subject: settings.enrollment_welcome_email_subject.clone().unwrap(),
content: self.get_welcome_email_content(&mut *transaction).await?,
attachments: Vec::new(),
result_tx: None,
};
match mail_tx.send(mail) {
Expand Down Expand Up @@ -351,6 +352,7 @@ impl Enrollment {
to: admin.email.clone(),
subject: "[defguard] User enrollment completed".into(),
content: templates::enrollment_admin_notification(user, admin)?,
attachments: Vec::new(),
result_tx: None,
};
match mail_tx.send(mail) {
Expand Down
106 changes: 94 additions & 12 deletions src/handlers/mail.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
use std::fmt::Display;

use chrono::Utc;
use lettre::message::header::ContentType;
use rocket::{
http::Status,
serde::json::{serde_json::json, Json},
State,
};
use tokio::sync::oneshot::channel;
use tokio::sync::mpsc::unbounded_channel;

use crate::{
appstate::AppState,
auth::{AdminRole, SessionInfo},
config::DefGuardConfig,
handlers::{ApiResponse, ApiResult},
mail::Mail,
templates,
mail::{Attachment, Mail},
support::dump_config,
templates::{self, support_data_mail},
};

const TEST_MAIL_SUBJECT: &str = "Defguard email test";
const SUPPORT_EMAIL_ADDRESS: &str = "support@defguard.net";
const SUPPORT_EMAIL_SUBJECT: &str = "Defguard support data";

#[derive(Clone, Deserialize)]
pub struct TestMail {
pub to: String,
}

/// Handles logging the error and returns ApiResponse that contains it
fn internal_error(from: &str, to: &str, subject: &str, error: &impl Display) -> ApiResponse {
error!("Error sending mail from: {from}, to {to}, subject: {subject}, error: {error}");
fn internal_error(to: &str, subject: &str, error: &impl Display) -> ApiResponse {
error!("Error sending mail to {to}, subject: {subject}, error: {error}");
ApiResponse {
json: json!({
"error": error.to_string(),
Expand All @@ -45,17 +51,18 @@ pub async fn test_mail(
session.user.username, data.to
);

let (tx, rx) = channel();
let (tx, mut rx) = unbounded_channel();
let mail = Mail {
to: data.to.clone(),
subject: TEST_MAIL_SUBJECT.to_string(),
content: templates::test_mail()?,
attachments: Vec::new(),
result_tx: Some(tx),
};
let (from, to, subject) = (data.to.clone(), mail.to.clone(), mail.subject.clone());
let (to, subject) = (mail.to.clone(), mail.subject.clone());
match appstate.mail_tx.send(mail) {
Ok(_) => match rx.await {
Ok(Ok(_)) => {
Ok(_) => match rx.recv().await {
Some(Ok(_)) => {
info!(
"User {} sent test mail to {}",
session.user.username, data.to
Expand All @@ -65,9 +72,84 @@ pub async fn test_mail(
status: Status::Ok,
})
}
Ok(Err(err)) => Ok(internal_error(&from, &to, &subject, &err)),
Err(err) => Ok(internal_error(&from, &to, &subject, &err)),
Some(Err(err)) => Ok(internal_error(&to, &subject, &err)),
None => Ok(internal_error(
&to,
&subject,
&String::from("None received"),
)),
},
Err(err) => Ok(internal_error(&from, &to, &subject, &err)),
Err(err) => Ok(internal_error(&to, &subject, &err)),
}
}

async fn read_logs(config: &DefGuardConfig) -> String {
let path = match &config.log_file {
Some(path) => path,
None => return "Log file not configured".to_string(),
};

match tokio::fs::read_to_string(path).await {
Ok(logs) => logs,
Err(err) => {
error!("Error dumping app logs: {err}");
format!("Error dumping app logs: {err}")
}
}
}

#[post("/support", format = "json")]
pub async fn send_support_data(
_admin: AdminRole,
session: SessionInfo,
appstate: &State<AppState>,
) -> ApiResult {
debug!(
"User {} sending support mail to {}",
session.user.username, SUPPORT_EMAIL_ADDRESS
);
let config = dump_config(&appstate.pool, &appstate.config).await;
let config =
serde_json::to_string_pretty(&config).unwrap_or("Json formatting error".to_string());
let config = Attachment {
filename: format!("defguard-support-data-{}.json", Utc::now()),
content: config.into(),
content_type: ContentType::TEXT_PLAIN,
};
let logs = read_logs(&appstate.config).await;
let logs = Attachment {
filename: format!("defguard-logs-{}.txt", Utc::now()),
content: logs.into(),
content_type: ContentType::TEXT_PLAIN,
};
let (tx, mut rx) = unbounded_channel();
let mail = Mail {
to: SUPPORT_EMAIL_ADDRESS.to_string(),
subject: SUPPORT_EMAIL_SUBJECT.to_string(),
content: support_data_mail()?,
attachments: vec![config, logs],
result_tx: Some(tx),
};
let (to, subject) = (mail.to.clone(), mail.subject.clone());
match appstate.mail_tx.send(mail) {
Ok(_) => match rx.recv().await {
Some(Ok(_)) => {
info!(
"User {} sent support mail to {}",
session.user.username, SUPPORT_EMAIL_ADDRESS
);
Ok(ApiResponse {
json: json!({}),
status: Status::Ok,
})
}
Some(Err(err)) => Ok(internal_error(&to, &subject, &err)),
None => Ok(internal_error(
&to,
&subject,
&String::from("None received"),
)),
},
Err(err) => Ok(internal_error(&to, &subject, &err)),
}
}
5 changes: 2 additions & 3 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ use crate::{
auth::SessionInfo,
db::{DbPool, User, UserInfo},
error::OriWebError,
VERSION,
};
use rocket::{
http::{ContentType, Status},
request::Request,
response::{Responder, Response},
serde::json::{serde_json::json, Value},
};
use std::env;
use webauthn_rs::prelude::RegisterPublicKeyCredential;

pub(crate) mod app_info;
Expand All @@ -24,6 +24,7 @@ pub mod openid_clients;
#[cfg(feature = "openid")]
pub mod openid_flow;
pub(crate) mod settings;
pub(crate) mod support;
pub(crate) mod user;
pub(crate) mod webhooks;
#[cfg(feature = "wireguard")]
Expand All @@ -37,8 +38,6 @@ pub struct ApiResponse {
pub status: Status,
}

pub const VERSION: &str = env!("CARGO_PKG_VERSION");

pub type ApiResult = Result<ApiResponse, OriWebError>;

impl<'r, 'o: 'r> Responder<'r, 'o> for OriWebError {
Expand Down
6 changes: 3 additions & 3 deletions src/handlers/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use rocket::{
State,
};

#[get("/settings", format = "json")]
#[get("/", format = "json")]
pub async fn get_settings(appstate: &State<AppState>) -> ApiResult {
debug!("Retrieving settings");
let settings = Settings::find_by_id(&appstate.pool, 1).await?;
Expand All @@ -22,7 +22,7 @@ pub async fn get_settings(appstate: &State<AppState>) -> ApiResult {
})
}

#[put("/settings", format = "json", data = "<data>")]
#[put("/", format = "json", data = "<data>")]
pub async fn update_settings(
_admin: AdminRole,
appstate: &State<AppState>,
Expand All @@ -36,7 +36,7 @@ pub async fn update_settings(
Ok(ApiResponse::default())
}

#[get("/settings/<id>", format = "json")]
#[get("/<id>", format = "json")]
pub async fn set_default_branding(
_admin: AdminRole,
appstate: &State<AppState>,
Expand Down
Loading