➕ Add "last seen" to server list
parent
a3e76cf120
commit
7aafbba4d9
|
@ -13,6 +13,7 @@ license = "AGPL-3.0"
|
||||||
aho-corasick = "0.7.14"
|
aho-corasick = "0.7.14"
|
||||||
base64 = "0.12.3"
|
base64 = "0.12.3"
|
||||||
blake3 = "0.3.7"
|
blake3 = "0.3.7"
|
||||||
|
chrono = "0.4.19"
|
||||||
ctrlc = { version = "3.1.7", features = [ "termination" ] }
|
ctrlc = { version = "3.1.7", features = [ "termination" ] }
|
||||||
dashmap = "3.11.10"
|
dashmap = "3.11.10"
|
||||||
futures = "0.3.7"
|
futures = "0.3.7"
|
||||||
|
|
|
@ -6,12 +6,25 @@
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
.entry {
|
td {
|
||||||
display: inline-block;
|
padding: 0px;
|
||||||
padding: 20px;
|
|
||||||
min-width: 50%;
|
|
||||||
}
|
}
|
||||||
.entry_list div:nth-child(odd) {
|
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;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -21,19 +34,28 @@
|
||||||
|
|
||||||
<h1>Server list</h1>
|
<h1>Server list</h1>
|
||||||
|
|
||||||
<div class="entry_list">
|
|
||||||
|
|
||||||
{{#if servers}}
|
{{#if servers}}
|
||||||
|
<table class="entry_list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Last seen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
{{#each servers}}
|
{{#each servers}}
|
||||||
<div>
|
<tr>
|
||||||
<a class="entry" href="{{this.path}}/files/">{{this.name}}</a>
|
<td><a class="entry" href="{{this.path}}/files/">{{this.name}}</a></td>
|
||||||
</div>
|
<td><span class="grey">{{this.last_seen}}</span></td>
|
||||||
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
{{else}}
|
{{else}}
|
||||||
(No servers are running)
|
(No servers have reported since this relay started)
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -6,19 +6,25 @@
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
.entry {
|
td {
|
||||||
display: inline-block;
|
padding: 0;
|
||||||
|
}
|
||||||
|
td > * {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 100%;
|
display: block;
|
||||||
|
}
|
||||||
|
.entry {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.grey {
|
.grey {
|
||||||
color: #888;
|
color: #888;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
.entry_list {
|
.entry_list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
.entry_list tr:nth-child(even) {
|
tbody tr:nth-child(odd) {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
178
src/relay/mod.rs
178
src/relay/mod.rs
|
@ -127,12 +127,32 @@ impl From <&ConfigFile> for Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use chrono::{
|
||||||
|
DateTime,
|
||||||
|
SecondsFormat,
|
||||||
|
Utc
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive (Clone)]
|
||||||
|
pub struct ServerStatus {
|
||||||
|
last_seen: DateTime <Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServerStatus {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self {
|
||||||
|
last_seen: Utc::now (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RelayState {
|
pub struct RelayState {
|
||||||
config: Config,
|
config: Config,
|
||||||
handlebars: Arc <Handlebars <'static>>,
|
handlebars: Arc <Handlebars <'static>>,
|
||||||
|
|
||||||
// Key: Server ID
|
// Key: Server ID
|
||||||
request_rendezvous: Mutex <HashMap <String, RequestRendezvous>>,
|
request_rendezvous: Mutex <HashMap <String, RequestRendezvous>>,
|
||||||
|
server_status: Mutex <HashMap <String, ServerStatus>>,
|
||||||
|
|
||||||
// Key: Request ID
|
// Key: Request ID
|
||||||
response_rendezvous: RwLock <DashMap <String, ResponseRendezvous>>,
|
response_rendezvous: RwLock <DashMap <String, ResponseRendezvous>>,
|
||||||
|
@ -149,6 +169,7 @@ impl From <&ConfigFile> for RelayState {
|
||||||
config: Config::from (config_file),
|
config: Config::from (config_file),
|
||||||
handlebars: Arc::new (load_templates (&PathBuf::new ()).unwrap ()),
|
handlebars: Arc::new (load_templates (&PathBuf::new ()).unwrap ()),
|
||||||
request_rendezvous: Default::default (),
|
request_rendezvous: Default::default (),
|
||||||
|
server_status: Default::default (),
|
||||||
response_rendezvous: Default::default (),
|
response_rendezvous: Default::default (),
|
||||||
shutdown_watch_tx,
|
shutdown_watch_tx,
|
||||||
shutdown_watch_rx,
|
shutdown_watch_rx,
|
||||||
|
@ -205,6 +226,16 @@ async fn handle_http_listen (
|
||||||
return trip_error;
|
return trip_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// End of early returns
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut server_status = state.server_status.lock ().await;
|
||||||
|
|
||||||
|
let mut status = server_status.entry (watcher_code.clone ()).or_insert_with (Default::default);
|
||||||
|
|
||||||
|
status.last_seen = Utc::now ();
|
||||||
|
}
|
||||||
|
|
||||||
use RequestRendezvous::*;
|
use RequestRendezvous::*;
|
||||||
|
|
||||||
let (tx, rx) = oneshot::channel ();
|
let (tx, rx) = oneshot::channel ();
|
||||||
|
@ -454,6 +485,99 @@ async fn handle_http_request (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive (Debug, PartialEq)]
|
||||||
|
enum LastSeen {
|
||||||
|
Negative,
|
||||||
|
Connected,
|
||||||
|
Description (String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mnemonic is "now - last_seen"
|
||||||
|
|
||||||
|
fn pretty_print_last_seen (
|
||||||
|
now: DateTime <Utc>,
|
||||||
|
last_seen: DateTime <Utc>
|
||||||
|
) -> LastSeen
|
||||||
|
{
|
||||||
|
use LastSeen::*;
|
||||||
|
|
||||||
|
let dur = now.signed_duration_since (last_seen);
|
||||||
|
|
||||||
|
if dur < chrono::Duration::zero () {
|
||||||
|
return Negative;
|
||||||
|
}
|
||||||
|
|
||||||
|
if dur.num_minutes () < 1 {
|
||||||
|
return Connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
if dur.num_hours () < 1 {
|
||||||
|
return Description (format! ("{} m ago", dur.num_minutes ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if dur.num_days () < 1 {
|
||||||
|
return Description (format! ("{} h ago", dur.num_hours ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Description (last_seen.to_rfc3339_opts (SecondsFormat::Secs, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn handle_server_list (
|
||||||
|
state: Arc <RelayState>
|
||||||
|
) -> Response <Body>
|
||||||
|
{
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[derive (Serialize)]
|
||||||
|
struct ServerEntry <'a> {
|
||||||
|
path: String,
|
||||||
|
name: String,
|
||||||
|
last_seen: Cow <'a, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive (Serialize)]
|
||||||
|
struct ServerListPage <'a> {
|
||||||
|
servers: Vec <ServerEntry <'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let servers = {
|
||||||
|
let guard = state.server_status.lock ().await;
|
||||||
|
(*guard).clone ()
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Utc::now ();
|
||||||
|
|
||||||
|
let mut servers: Vec <_> = servers.into_iter ()
|
||||||
|
.map (|(name, server)| {
|
||||||
|
let display_name = percent_encoding::percent_decode_str (&name).decode_utf8 ().unwrap_or_else (|_| "Server name isn't UTF-8".into ()).to_string ();
|
||||||
|
|
||||||
|
use LastSeen::*;
|
||||||
|
|
||||||
|
let last_seen = match pretty_print_last_seen (now, server.last_seen) {
|
||||||
|
Negative => "Error (negative time)".into (),
|
||||||
|
Connected => "Connected".into (),
|
||||||
|
Description (s) => s.into (),
|
||||||
|
};
|
||||||
|
|
||||||
|
ServerEntry {
|
||||||
|
name: display_name,
|
||||||
|
path: name,
|
||||||
|
last_seen: last_seen,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect ();
|
||||||
|
|
||||||
|
servers.sort_by (|a, b| a.name.cmp (&b.name));
|
||||||
|
|
||||||
|
let page = ServerListPage {
|
||||||
|
servers,
|
||||||
|
};
|
||||||
|
|
||||||
|
let s = state.handlebars.render ("relay_server_list", &page).unwrap ();
|
||||||
|
ok_reply (s)
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument (level = "trace", skip (req, state))]
|
#[instrument (level = "trace", skip (req, state))]
|
||||||
async fn handle_all (req: Request <Body>, state: Arc <RelayState>)
|
async fn handle_all (req: Request <Body>, state: Arc <RelayState>)
|
||||||
-> Result <Response <Body>, Infallible>
|
-> Result <Response <Body>, Infallible>
|
||||||
|
@ -487,35 +611,7 @@ async fn handle_all (req: Request <Body>, state: Arc <RelayState>)
|
||||||
}
|
}
|
||||||
else if let Some (rest) = prefix_match ("/frontend/servers/", path) {
|
else if let Some (rest) = prefix_match ("/frontend/servers/", path) {
|
||||||
if rest == "" {
|
if rest == "" {
|
||||||
use std::borrow::Cow;
|
handle_server_list (state).await
|
||||||
|
|
||||||
#[derive (Serialize)]
|
|
||||||
struct ServerEntry <'a> {
|
|
||||||
path: &'a str,
|
|
||||||
name: Cow <'a, str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive (Serialize)]
|
|
||||||
struct ServerListPage <'a> {
|
|
||||||
servers: Vec <ServerEntry <'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut names = state.list_servers ().await;
|
|
||||||
names.sort ();
|
|
||||||
|
|
||||||
//println! ("Found {} servers", names.len ());
|
|
||||||
|
|
||||||
let page = ServerListPage {
|
|
||||||
servers: names.iter ()
|
|
||||||
.map (|name| ServerEntry {
|
|
||||||
name: percent_encoding::percent_decode_str (name).decode_utf8 ().unwrap_or_else (|_| "Server name isn't UTF-8".into ()),
|
|
||||||
path: &name,
|
|
||||||
})
|
|
||||||
.collect (),
|
|
||||||
};
|
|
||||||
|
|
||||||
let s = state.handlebars.render ("relay_server_list", &page).unwrap ();
|
|
||||||
ok_reply (s)
|
|
||||||
}
|
}
|
||||||
else if let Some (idx) = rest.find ('/') {
|
else if let Some (idx) = rest.find ('/') {
|
||||||
let listen_code = String::from (&rest [0..idx]);
|
let listen_code = String::from (&rest [0..idx]);
|
||||||
|
@ -635,5 +731,29 @@ pub async fn run_relay (
|
||||||
|
|
||||||
#[cfg (test)]
|
#[cfg (test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pretty_print_last_seen () {
|
||||||
|
use LastSeen::*;
|
||||||
|
|
||||||
|
let last_seen = DateTime::parse_from_rfc3339 ("2019-05-29T00:00:00+00:00").unwrap ().with_timezone (&Utc);
|
||||||
|
|
||||||
|
for (input, expected) in vec! [
|
||||||
|
("2019-05-28T23:59:59+00:00", Negative),
|
||||||
|
("2019-05-29T00:00:00+00:00", Connected),
|
||||||
|
("2019-05-29T00:00:59+00:00", Connected),
|
||||||
|
("2019-05-29T00:01:30+00:00", Description ("1 m ago".into ())),
|
||||||
|
("2019-05-29T00:59:30+00:00", Description ("59 m ago".into ())),
|
||||||
|
("2019-05-29T01:00:30+00:00", Description ("1 h ago".into ())),
|
||||||
|
("2019-05-29T10:00:00+00:00", Description ("10 h ago".into ())),
|
||||||
|
("2019-05-30T00:00:00+00:00", Description ("2019-05-29T00:00:00Z".into ())),
|
||||||
|
("2019-05-30T10:00:00+00:00", Description ("2019-05-29T00:00:00Z".into ())),
|
||||||
|
("2019-05-31T00:00:00+00:00", Description ("2019-05-29T00:00:00Z".into ())),
|
||||||
|
].into_iter () {
|
||||||
|
let now = DateTime::parse_from_rfc3339 (input).unwrap ().with_timezone (&Utc);
|
||||||
|
let actual = pretty_print_last_seen (now, last_seen);
|
||||||
|
assert_eq! (actual, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -685,20 +685,13 @@ mod tests {
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
path::{
|
path::{
|
||||||
Component,
|
Component,
|
||||||
Path,
|
Path,
|
||||||
PathBuf
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use maplit::*;
|
use maplit::*;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
use always_equal::test::AlwaysEqual;
|
|
||||||
|
|
||||||
use crate::http_serde::{
|
|
||||||
StatusCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn icons () {
|
fn icons () {
|
||||||
let video = "🎞️";
|
let video = "🎞️";
|
||||||
|
|
1
todo.md
1
todo.md
|
@ -10,7 +10,6 @@
|
||||||
- ETag cache based on mtime
|
- ETag cache based on mtime
|
||||||
- Server-side hash?
|
- Server-side hash?
|
||||||
- Log / audit log?
|
- Log / audit log?
|
||||||
- Add "Last check-in time" to server list
|
|
||||||
|
|
||||||
- Prevent directory traversal attacks in file_server.rs
|
- Prevent directory traversal attacks in file_server.rs
|
||||||
- Error handling
|
- Error handling
|
||||||
|
|
Loading…
Reference in New Issue