in-memory audit logging

main
_ 2021-04-02 23:19:33 -05:00
parent 6927707501
commit 832794f844
4 changed files with 172 additions and 16 deletions

View File

@ -96,13 +96,18 @@ async fn handle_http_request (
req: http::request::Parts, req: http::request::Parts,
uri: String, uri: String,
state: Arc <RelayState>, state: Arc <RelayState>,
watcher_code: String server_name: String
) )
-> Result <Response <Body>, http::Error> -> Result <Response <Body>, http::Error>
{ {
use crate::relay_state::{
AuditData,
AuditEvent,
};
{ {
let config = state.config.read ().await; let config = state.config.read ().await;
if ! config.servers.contains_key (&watcher_code) { if ! config.servers.contains_key (&server_name) {
return error_reply (StatusCode::NOT_FOUND, "Unknown server"); return error_reply (StatusCode::NOT_FOUND, "Unknown server");
} }
} }
@ -115,13 +120,18 @@ async fn handle_http_request (
let (tx, rx) = oneshot::channel (); let (tx, rx) = oneshot::channel ();
let req_id = rusty_ulid::generate_ulid_string (); let req_id = rusty_ulid::generate_ulid_string ();
state.audit_log.push (AuditEvent::new (AuditData::WebClientGet {
req_id: req_id.clone (),
server_name: server_name.clone (),
})).await;
trace! ("Created request {}", req_id);
{ {
let response_rendezvous = state.response_rendezvous.read ().await; let response_rendezvous = state.response_rendezvous.read ().await;
response_rendezvous.insert (req_id.clone (), tx); response_rendezvous.insert (req_id.clone (), tx);
} }
trace! ("Created request {}", req_id);
{ {
use RequestRendezvous::*; use RequestRendezvous::*;
@ -132,7 +142,7 @@ async fn handle_http_request (
req, req,
}; };
let new_rendezvous = match request_rendezvous.remove (&watcher_code) { let new_rendezvous = match request_rendezvous.remove (&server_name) {
Some (ParkedClients (mut v)) => { Some (ParkedClients (mut v)) => {
debug! ("Parking request {} ({} already queued)", req_id, v.len ()); debug! ("Parking request {} ({} already queued)", req_id, v.len ());
v.push (wrapped); v.push (wrapped);
@ -150,7 +160,7 @@ async fn handle_http_request (
trace! ( trace! (
"Sending request {} directly to server {}", "Sending request {} directly to server {}",
req_id, req_id,
watcher_code, server_name,
); );
ParkedClients (vec! []) ParkedClients (vec! [])
@ -168,7 +178,7 @@ async fn handle_http_request (
}, },
}; };
request_rendezvous.insert (watcher_code, new_rendezvous); request_rendezvous.insert (server_name, new_rendezvous);
} }
let timeout = tokio::time::sleep (std::time::Duration::from_secs (30)); let timeout = tokio::time::sleep (std::time::Duration::from_secs (30));
@ -256,6 +266,11 @@ struct ServerListPage <'a> {
servers: Vec <ServerEntry <'a>>, servers: Vec <ServerEntry <'a>>,
} }
#[derive (Serialize)]
struct UnregisteredServerListPage {
unregistered_servers: Vec <UnregisteredServer>,
}
#[derive (Serialize)] #[derive (Serialize)]
struct UnregisteredServer { struct UnregisteredServer {
name: String, name: String,
@ -264,10 +279,12 @@ struct UnregisteredServer {
} }
#[derive (Serialize)] #[derive (Serialize)]
struct UnregisteredServerListPage { struct AuditLogPage {
unregistered_servers: Vec <UnregisteredServer>, audit_log: Vec <String>,
} }
async fn handle_server_list_internal (state: &Arc <RelayState>) async fn handle_server_list_internal (state: &Arc <RelayState>)
-> ServerListPage <'static> -> ServerListPage <'static>
{ {
@ -338,6 +355,17 @@ async fn handle_unregistered_servers_internal (state: &Arc <RelayState>)
} }
} }
async fn handle_audit_log_internal (state: &Arc <RelayState>)
-> AuditLogPage
{
let audit_log = state.audit_log.to_vec ().await
.iter ().rev ().map (|e| format! ("{:?}", e)).collect ();
AuditLogPage {
audit_log,
}
}
async fn handle_server_list ( async fn handle_server_list (
state: Arc <RelayState>, state: Arc <RelayState>,
handlebars: Arc <Handlebars <'static>> handlebars: Arc <Handlebars <'static>>
@ -360,6 +388,17 @@ async fn handle_unregistered_servers (
Ok (ok_reply (s)?) Ok (ok_reply (s)?)
} }
async fn handle_audit_log (
state: Arc <RelayState>,
handlebars: Arc <Handlebars <'static>>
) -> Result <Response <Body>, RequestError>
{
let page = handle_audit_log_internal (&state).await;
let s = handlebars.render ("audit_log", &page)?;
Ok (ok_reply (s)?)
}
async fn handle_endless_sink (req: Request <Body>) -> Result <Response <Body>, http::Error> async fn handle_endless_sink (req: Request <Body>) -> Result <Response <Body>, http::Error>
{ {
let (_parts, mut body) = req.into_parts (); let (_parts, mut body) = req.into_parts ();
@ -489,6 +528,9 @@ async fn handle_all (
else if path == "/frontend/unregistered_servers" { else if path == "/frontend/unregistered_servers" {
Ok (handle_unregistered_servers (state, handlebars).await?) Ok (handle_unregistered_servers (state, handlebars).await?)
} }
else if path == "/frontend/audit_log" {
Ok (handle_audit_log (state, handlebars).await?)
}
else if let Some (rest) = prefix_match ("/frontend/debug/", &path) { else if let Some (rest) = prefix_match ("/frontend/debug/", &path) {
if rest.is_empty () { if rest.is_empty () {
let s = handlebars.render ("debug", &())?; let s = handlebars.render ("debug", &())?;
@ -534,6 +576,7 @@ pub fn load_templates (asset_root: &Path)
let asset_root = asset_root.join ("handlebars/relay"); let asset_root = asset_root.join ("handlebars/relay");
for (k, v) in &[ for (k, v) in &[
("audit_log", "audit_log.hbs"),
("debug", "debug.hbs"), ("debug", "debug.hbs"),
("root", "root.hbs"), ("root", "root.hbs"),
("server_list", "server_list.hbs"), ("server_list", "server_list.hbs"),
@ -577,6 +620,11 @@ pub async fn run_relay (
) )
-> Result <(), RelayError> -> Result <(), RelayError>
{ {
use crate::relay_state::{
AuditData,
AuditEvent,
};
if let Some (config_reload_path) = config_reload_path { if let Some (config_reload_path) = config_reload_path {
let state_2 = state.clone (); let state_2 = state.clone ();
tokio::spawn (async move { tokio::spawn (async move {
@ -616,6 +664,8 @@ pub async fn run_relay (
let server = Server::bind (&addr) let server = Server::bind (&addr)
.serve (make_svc); .serve (make_svc);
state.audit_log.push (AuditEvent::new (AuditData::RelayStart)).await;
server.with_graceful_shutdown (async { server.with_graceful_shutdown (async {
use ShuttingDownError::ShuttingDown; use ShuttingDownError::ShuttingDown;

View File

@ -1,6 +1,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
convert::TryFrom, convert::TryFrom,
time::Instant,
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@ -85,6 +86,43 @@ pub struct RelayState {
// List of recently rejected server names (Used to approve servers) // List of recently rejected server names (Used to approve servers)
pub unregistered_servers: BoundedVec <RejectedServer>, pub unregistered_servers: BoundedVec <RejectedServer>,
// Memory backend for audit logging
// TODO: Add file / database / network server logging backend
pub audit_log: BoundedVec <AuditEvent>,
}
#[derive (Clone)]
pub struct RejectedServer {
pub name: String,
pub tripcode: blake3::Hash,
pub seen: DateTime <Utc>,
}
#[derive (Clone, Debug)]
pub struct AuditEvent {
time_monotonic: Instant,
time_utc: DateTime <Utc>,
data: AuditData,
}
#[derive (Clone, Debug)]
pub enum AuditData {
RelayStart,
WebClientGet {
req_id: String,
server_name: String,
},
}
impl AuditEvent {
pub fn new (data: AuditData) -> Self {
Self {
time_monotonic: Instant::now (),
time_utc: Utc::now (),
data,
}
}
} }
pub struct BoundedVec <T: Clone> { pub struct BoundedVec <T: Clone> {
@ -119,13 +157,6 @@ impl <T: Clone> BoundedVec <T> {
} }
} }
#[derive (Clone)]
pub struct RejectedServer {
pub name: String,
pub tripcode: blake3::Hash,
pub seen: DateTime <Utc>,
}
impl TryFrom <Config> for RelayState { impl TryFrom <Config> for RelayState {
type Error = RelayError; type Error = RelayError;
@ -140,6 +171,7 @@ impl TryFrom <Config> for RelayState {
shutdown_watch_tx, shutdown_watch_tx,
shutdown_watch_rx, shutdown_watch_rx,
unregistered_servers: BoundedVec::new (20), unregistered_servers: BoundedVec::new (20),
audit_log: BoundedVec::new (256),
}) })
} }
} }

View File

@ -0,0 +1,70 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAlJSUAAGIAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAIiIiIgAAAAAAAAAgAAAAAAAAAAAAADIiIiIi
IiIjMhERERERESMyERERERERIzIREREREREjMhERIRERESMyERIiIiERIzIRESEREREjMhERERER
ESMyERERERERIzIREREREREjMiIiIiIiIiMAAAAAAAAAAAAAAAAAAAAAAAAAAIABAACAAQAAgAEA
AIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAA" rel="icon" type="image/x-icon" />
<style>
body {
font-family: sans-serif;
}
td {
padding: 0px;
}
td > * {
padding: 20px;
display: block;
}
.entry {
}
.grey {
color: #888;
text-align: right;
}
.entry_list {
width: 100%;
border-collapse: collapse;
}
tbody tr:nth-child(odd) {
background-color: #ddd;
}
</style>
<title>Audit log</title>
</head>
<body>
<h1>Audit log</h1>
<p>
(Newest events at the top)
<div style="padding-top: 1em;">
{{#if audit_log}}
<table class="entry_list">
<thead>
<tr>
<th>Debug</th>
</tr>
</thead>
<tbody>
{{#each audit_log}}
<tr>
<td><span>{{this}}</span></td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
(No audit events yet (escalate this to someone!))
{{/if}}
</div>
</body>
</html>

View File

@ -44,8 +44,12 @@ AIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAA" rel="icon" type="image/x-icon" />
<div style="color: red;">Relay is in dev mode. This should never be seen in production!</div> <div style="color: red;">Relay is in dev mode. This should never be seen in production!</div>
{{/if}} {{/if}}
<p>
<a href="../unregistered_servers">Unregistered servers</a> <a href="../unregistered_servers">Unregistered servers</a>
<p>
<a href="../audit_log">Audit log</a>
<div style="padding-top: 1em;"> <div style="padding-top: 1em;">
{{#if servers}} {{#if servers}}
<table class="entry_list"> <table class="entry_list">