Skip to content

Commit 253786a

Browse files
feat: fail2ban-friendly log format (#61)
1 parent af4c3ed commit 253786a

File tree

4 files changed

+143
-15
lines changed

4 files changed

+143
-15
lines changed

src/handlers/desktop_client_mfa.rs

+10-4
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@ async fn start_client_mfa(
2121
device_info: Option<DeviceInfo>,
2222
Json(req): Json<ClientMfaStartRequest>,
2323
) -> Result<Json<ClientMfaStartResponse>, ApiError> {
24-
info!("Starting desktop client authorization");
24+
info!("Starting desktop client authorization {req:?}");
2525
let rx = state.grpc_server.send(
26-
Some(core_request::Payload::ClientMfaStart(req)),
26+
Some(core_request::Payload::ClientMfaStart(req.clone())),
2727
device_info,
2828
)?;
2929
let payload = get_core_response(rx).await?;
3030
match payload {
31-
core_response::Payload::ClientMfaStart(response) => Ok(Json(response)),
31+
core_response::Payload::ClientMfaStart(response) => {
32+
info!("Started desktop client authorization {req:?}");
33+
Ok(Json(response))
34+
}
3235
_ => {
3336
error!("Received invalid gRPC response type: {payload:?}");
3437
Err(ApiError::InvalidResponseType)
@@ -49,7 +52,10 @@ async fn finish_client_mfa(
4952
)?;
5053
let payload = get_core_response(rx).await?;
5154
match payload {
52-
core_response::Payload::ClientMfaFinish(response) => Ok(Json(response)),
55+
core_response::Payload::ClientMfaFinish(response) => {
56+
info!("Finished desktop client authorization");
57+
Ok(Json(response))
58+
}
5359
_ => {
5460
error!("Received invalid gRPC response type: {payload:?}");
5561
Err(ApiError::InvalidResponseType)

src/handlers/enrollment.rs

+19-6
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ pub async fn start_enrollment_process(
4242
let payload = get_core_response(rx).await?;
4343
match payload {
4444
core_response::Payload::EnrollmentStart(response) => {
45+
info!(
46+
"Started enrollment process for user {:?} by admin {:?}",
47+
response.user, response.admin
48+
);
4549
// set session cookie
46-
info!("Started enrollment process: {response:?}");
4750
let cookie = Cookie::build((ENROLLMENT_COOKIE_NAME, token))
4851
.expires(OffsetDateTime::from_unix_timestamp(response.deadline_timestamp).unwrap());
4952

@@ -63,7 +66,8 @@ pub async fn activate_user(
6366
mut private_cookies: PrivateCookieJar,
6467
Json(mut req): Json<ActivateUserRequest>,
6568
) -> Result<PrivateCookieJar, ApiError> {
66-
info!("Activating user");
69+
let phone = req.phone_number.clone();
70+
info!("Activating user - phone number {phone:?}");
6771

6872
// set auth info
6973
req.token = private_cookies
@@ -77,6 +81,7 @@ pub async fn activate_user(
7781
match payload {
7882
core_response::Payload::Empty(_) => {
7983
if let Some(cookie) = private_cookies.get(ENROLLMENT_COOKIE_NAME) {
84+
info!("Activated user - phone number {phone:?}");
8085
debug!("Enrollment finished. Removing session cookie");
8186
private_cookies = private_cookies.remove(cookie);
8287
}
@@ -97,7 +102,8 @@ pub async fn create_device(
97102
private_cookies: PrivateCookieJar,
98103
Json(mut req): Json<NewDevice>,
99104
) -> Result<Json<DeviceConfigResponse>, ApiError> {
100-
info!("Adding new device");
105+
let (name, pubkey) = (req.name.clone(), req.pubkey.clone());
106+
info!("Adding new device {name} {pubkey}");
101107

102108
// set auth info
103109
req.token = private_cookies
@@ -109,7 +115,10 @@ pub async fn create_device(
109115
.send(Some(core_request::Payload::NewDevice(req)), device_info)?;
110116
let payload = get_core_response(rx).await?;
111117
match payload {
112-
core_response::Payload::DeviceConfig(response) => Ok(Json(response)),
118+
core_response::Payload::DeviceConfig(response) => {
119+
info!("Added new device {name} {pubkey}");
120+
Ok(Json(response))
121+
}
113122
_ => {
114123
error!("Received invalid gRPC response type: {payload:?}");
115124
Err(ApiError::InvalidResponseType)
@@ -123,7 +132,8 @@ pub async fn get_network_info(
123132
private_cookies: PrivateCookieJar,
124133
Json(mut req): Json<ExistingDevice>,
125134
) -> Result<Json<DeviceConfigResponse>, ApiError> {
126-
info!("Getting network info");
135+
let pubkey = req.pubkey.clone();
136+
info!("Getting network info for device {pubkey}");
127137

128138
// set auth info
129139
req.token = private_cookies
@@ -135,7 +145,10 @@ pub async fn get_network_info(
135145
.send(Some(core_request::Payload::ExistingDevice(req)), None)?;
136146
let payload = get_core_response(rx).await?;
137147
match payload {
138-
core_response::Payload::DeviceConfig(response) => Ok(Json(response)),
148+
core_response::Payload::DeviceConfig(response) => {
149+
info!("Got network info for device {pubkey}");
150+
Ok(Json(response))
151+
}
139152
_ => {
140153
error!("Received invalid gRPC response type: {payload:?}");
141154
Err(ApiError::InvalidResponseType)

src/handlers/password_reset.rs

+7-3
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ pub async fn request_password_reset(
2828
info!("Starting password reset request for {}", req.email);
2929

3030
let rx = state.grpc_server.send(
31-
Some(core_request::Payload::PasswordResetInit(req)),
31+
Some(core_request::Payload::PasswordResetInit(req.clone())),
3232
device_info,
3333
)?;
3434
let payload = get_core_response(rx).await?;
3535
match payload {
36-
core_response::Payload::Empty(_) => Ok(()),
36+
core_response::Payload::Empty(_) => {
37+
info!("Started password reset request for {}", req.email);
38+
Ok(())
39+
}
3740
_ => {
3841
error!("Received invalid gRPC response type: {payload:?}");
3942
Err(ApiError::InvalidResponseType)
@@ -69,6 +72,7 @@ pub async fn start_password_reset(
6972
let cookie = Cookie::build((PASSWORD_RESET_COOKIE_NAME, token))
7073
.expires(OffsetDateTime::from_unix_timestamp(response.deadline_timestamp).unwrap());
7174

75+
info!("Started password reset process");
7276
Ok((private_cookies.add(cookie), Json(response)))
7377
}
7478
_ => {
@@ -99,7 +103,7 @@ pub async fn reset_password(
99103
match payload {
100104
core_response::Payload::Empty(_) => {
101105
if let Some(cookie) = private_cookies.get(PASSWORD_RESET_COOKIE_NAME) {
102-
debug!("Password reset finished. Removing session cookie");
106+
info!("Password reset finished. Removing session cookie");
103107
private_cookies = private_cookies.remove(cookie);
104108
}
105109
Ok(private_cookies)

src/tracing.rs

+107-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,122 @@
1+
use rust_tracing::{Event, Subscriber};
2+
use tracing_subscriber::{
3+
fmt::{
4+
format::{self, FormatEvent, FormatFields, Writer},
5+
time::{FormatTime, SystemTime},
6+
FmtContext, FormattedFields,
7+
},
8+
registry::LookupSpan,
9+
};
10+
111
use tracing::log::LevelFilter;
212
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
313

414
// Initializes tracing with the specified log level.
515
// Allows fine-grained filtering with `EnvFilter` directives.
616
// The directives are read from `DEFGUARD_PROXY_LOG_FILTER` env variable.
7-
// For more info check: <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html>
17+
// For more info read: <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html>
818
pub fn init_tracing(level: &LevelFilter) {
919
tracing_subscriber::registry()
1020
.with(
1121
EnvFilter::try_from_env("DEFGUARD_PROXY_LOG_FILTER")
1222
.unwrap_or_else(|_| level.to_string().into()),
1323
)
14-
.with(fmt::layer())
24+
.with(fmt::layer().event_format(HttpFormatter::default()))
1525
.init();
1626
info!("Tracing initialized");
1727
}
28+
29+
/// Implements fail2ban-friendly log format using `tracing_subscriber::fmt::format::FormatEvent` trait.
30+
/// HTTP info (if available) is extracted from the specified tracing span. The format is as follows:
31+
/// TIMESTAMP LEVEL CLIENT_ADDR METHOD URI LOG_MESSAGE || TRACING_DATA
32+
pub(crate) struct HttpFormatter<'a> {
33+
span: &'a str,
34+
timer: SystemTime,
35+
}
36+
37+
impl<'a> Default for HttpFormatter<'a> {
38+
fn default() -> Self {
39+
Self {
40+
span: "http_request",
41+
timer: SystemTime,
42+
}
43+
}
44+
}
45+
46+
impl HttpFormatter<'_> {
47+
fn format_timestamp(&self, writer: &mut Writer<'_>) -> std::fmt::Result {
48+
if self.timer.format_time(writer).is_err() {
49+
writer.write_str("<unknown time>")?;
50+
}
51+
writer.write_char(' ')
52+
}
53+
}
54+
55+
impl<S, N> FormatEvent<S, N> for HttpFormatter<'_>
56+
where
57+
S: Subscriber + for<'a> LookupSpan<'a>,
58+
N: for<'a> FormatFields<'a> + 'static,
59+
{
60+
fn format_event(
61+
&self,
62+
ctx: &FmtContext<'_, S, N>,
63+
mut writer: format::Writer<'_>,
64+
event: &Event<'_>,
65+
) -> std::fmt::Result {
66+
let meta = event.metadata();
67+
68+
// timestamp & level
69+
self.format_timestamp(&mut writer)?;
70+
write!(writer, "{} ", meta.level())?;
71+
72+
// iterate and accumulate spans storing our special span in separate variable if encountered
73+
let mut context_logs: Vec<String> = Vec::new();
74+
let mut http_log: Option<String> = None;
75+
if let Some(scope) = ctx.event_scope() {
76+
let mut seen = false;
77+
for span in scope.from_root() {
78+
let span_name = span.metadata().name();
79+
context_logs.push(span_name.to_string());
80+
seen = true;
81+
82+
if let Some(fields) = span.extensions().get::<FormattedFields<N>>() {
83+
if !fields.is_empty() {
84+
match span_name {
85+
x if x == self.span => http_log = Some(format!("{fields}")),
86+
_ => context_logs.push(format!("{{{fields}}}")),
87+
}
88+
}
89+
}
90+
context_logs.push(":".into());
91+
}
92+
if seen {
93+
context_logs.push(' '.into());
94+
}
95+
};
96+
97+
// write http context log (ip, method, path)
98+
if let Some(log) = http_log {
99+
let split: Vec<&str> = log.split(['=', ' ']).collect();
100+
let method = split.get(1).unwrap_or(&"unknown");
101+
let path = split.get(3).unwrap_or(&"unknown");
102+
103+
let addr = split.get(5).and_then(|s| Some(s.replace('"', "")));
104+
let ip = addr
105+
.and_then(|s| s.split(":").next().map(|s| s.to_string()))
106+
.unwrap_or("unknown".to_string());
107+
write!(writer, "{ip} {method} {path} ")?;
108+
}
109+
110+
// write actual log message
111+
ctx.format_fields(writer.by_ref(), event)?;
112+
113+
// write span context
114+
if !context_logs.is_empty() {
115+
write!(writer, " || Tracing data: ")?;
116+
for log in context_logs {
117+
write!(writer, "{log}")?
118+
}
119+
}
120+
writeln!(writer)
121+
}
122+
}

0 commit comments

Comments
 (0)