💄 Show servers in the server list even if they aren't connected yet

main
_ 2020-11-26 23:30:33 +00:00
parent bf4e5c7a5b
commit 28ce6a32cd
7 changed files with 171 additions and 90 deletions

View File

@ -20,6 +20,7 @@ futures = "0.3.7"
handlebars = "3.5.1"
http = "0.2.1"
hyper = "0.13.8"
itertools = "0.9.0"
lazy_static = "1.4.0"
maplit = "1.0.2"
percent-encoding = "2.1.0"
@ -30,7 +31,7 @@ reqwest = { version = "0.10.8", features = ["stream"] }
rmp-serde = "0.14.4"
serde = {version = "1.0.117", features = ["derive"]}
structopt = "0.3.20"
# thiserror = "1.0.22"
thiserror = "1.0.22"
tokio = { version = "0.2.22", features = ["full"] }
tracing = "0.1.21"
tracing-futures = "0.2.4"

View File

@ -46,7 +46,7 @@
{{#each servers}}
<tr>
<td><a class="entry" href="{{this.path}}/files/">{{this.name}}</a></td>
<td><a class="entry" href="{{this.id}}/files/">{{this.display_name}}</a></td>
<td><span class="grey">{{this.last_seen}}</span></td>
</tr>
{{/each}}

View File

@ -12,7 +12,10 @@ use tracing_subscriber::{
};
use ptth::relay;
use ptth::relay::RelayState;
use ptth::relay::{
Config,
RelayState,
};
#[tokio::main]
async fn main () -> Result <(), Box <dyn Error>> {
@ -23,7 +26,7 @@ async fn main () -> Result <(), Box <dyn Error>> {
;
let config_path = PathBuf::from ("config/ptth_relay.toml");
let config_file = ptth::load_toml::load_public (&config_path);
let config = Config::from_file (&config_path).await?;
info! ("ptth_relay Git version: {:?}", ptth::git_version::GIT_VERSION);
@ -31,7 +34,7 @@ async fn main () -> Result <(), Box <dyn Error>> {
forced_shutdown.wrap_server (
relay::run_relay (
Arc::new (RelayState::from (&config_file)),
Arc::new (RelayState::from (config)),
shutdown_rx,
Some (config_path)
)

View File

@ -43,6 +43,7 @@ pub fn password_is_bad (mut password: String) -> bool {
#[cfg (test)]
mod tests {
use std::{
convert::TryFrom,
sync::{
Arc,
},
@ -104,14 +105,17 @@ mod tests {
let api_key = "AnacondaHardcoverGrannyUnlatchLankinessMutate";
let tripcode = base64::encode (blake3::hash (api_key.as_bytes ()).as_bytes ());
debug! ("Relay is expecting tripcode {}", tripcode);
let config_file = relay::ConfigFile {
let config_file = relay::config::file::Config {
port: None,
server_tripcodes: hashmap! {
server_name.into () => tripcode,
servers: hashmap! {
server_name.into () => relay::config::file::Server {
tripcode,
display_name: None,
},
},
};
let relay_state = Arc::new (relay::RelayState::from (&config_file));
let relay_state = Arc::new (relay::RelayState::from (relay::config::Config::try_from (config_file).unwrap ()));
let relay_state_2 = relay_state.clone ();
let (stop_relay_tx, stop_relay_rx) = oneshot::channel ();

108
src/relay/config.rs Normal file
View File

@ -0,0 +1,108 @@
use std::{
collections::*,
convert::{TryFrom, TryInto},
iter::FromIterator,
path::Path,
};
use serde::Deserialize;
use thiserror::Error;
#[derive (Error, Debug)]
pub enum ConfigError {
#[error ("I/O error")]
Io (#[from] std::io::Error),
#[error ("UTF-8 decoding failed")]
Utf8 (#[from] std::string::FromUtf8Error),
#[error ("TOML parsing failed")]
Toml (#[from] toml::de::Error),
#[error ("base64 decoding failed")]
Base64Decode (#[from] base64::DecodeError),
#[error ("tripcode not 32 bytes after decoding")]
TripcodeBadLength,
#[error ("unknown config error")]
Unknown,
}
// Stuff we need to load from the config file and use to
// set up the HTTP server
pub mod file {
use super::*;
#[derive (Deserialize)]
pub struct Server {
pub tripcode: String,
pub display_name: Option <String>,
}
#[derive (Deserialize)]
pub struct Config {
pub port: Option <u16>,
pub servers: HashMap <String, Server>,
}
}
// Stuff we actually need at runtime
pub struct Server {
pub tripcode: blake3::Hash,
pub display_name: Option <String>,
}
pub struct Config {
pub servers: HashMap <String, Server>,
}
impl TryFrom <file::Server> for Server {
type Error = ConfigError;
fn try_from (f: file::Server) -> Result <Self, Self::Error> {
let bytes: Vec <u8> = base64::decode (f.tripcode)?;
let bytes: [u8; 32] = (&bytes [..]).try_into ().map_err (|_| ConfigError::TripcodeBadLength)?;
let tripcode = blake3::Hash::from (bytes);
Ok (Self {
tripcode,
display_name: f.display_name,
})
}
}
impl TryFrom <file::Config> for Config {
type Error = ConfigError;
fn try_from (f: file::Config) -> Result <Self, Self::Error> {
let servers = f.servers.into_iter ()
.map (|(k, v)| Ok::<_, ConfigError> ((k, v.try_into ()?)));
let servers = itertools::process_results (servers, |i| HashMap::from_iter (i))?;
Ok (Self {
servers,
})
}
}
impl Config {
pub async fn from_file (path: &Path) -> Result <Self, ConfigError> {
use tokio::prelude::*;
let mut f = tokio::fs::File::open (path).await?;
let mut buffer = vec! [0u8; 4096];
let bytes_read = f.read (&mut buffer).await?;
buffer.truncate (bytes_read);
let config_s = String::from_utf8 (buffer)?;
let new_config: file::Config = toml::from_str (&config_s)?;
Self::try_from (new_config)
}
}

View File

@ -27,7 +27,6 @@ use hyper::{
};
use hyper::service::{make_service_fn, service_fn};
use serde::{
Deserialize,
Serialize,
};
use tokio::{
@ -51,6 +50,10 @@ use crate::{
prefix_match,
};
pub mod config;
pub use config::{Config, ConfigError};
/*
Here's what we need to handle:
@ -92,48 +95,6 @@ enum RequestRendezvous {
type ResponseRendezvous = oneshot::Sender <Result <(http_serde::ResponseParts, Body), RelayError>>;
// Stuff we need to load from the config file and use to
// set up the HTTP server
#[derive (Default, Deserialize)]
pub struct ConfigServers {
pub tripcodes: HashMap <String, String>,
pub display_names: HashMap <String, String>,
}
#[derive (Default, Deserialize)]
pub struct ConfigFile {
pub port: Option <u16>,
pub servers: ConfigServers,
}
// Stuff we actually need at runtime
struct Config {
server_tripcodes: HashMap <String, blake3::Hash>,
}
impl From <&ConfigFile> for Config {
fn from (f: &ConfigFile) -> Self {
let server_tripcodes = HashMap::from_iter (f.servers.tripcodes.iter ()
.map (|(k, v)| {
use std::convert::TryInto;
let bytes: Vec <u8> = base64::decode (v).unwrap ();
let bytes: [u8; 32] = (&bytes [..]).try_into ().unwrap ();
let v = blake3::Hash::from (bytes);
debug! ("Tripcode {} => {}", k, v.to_hex ());
(k.clone (), v)
}));
Self {
server_tripcodes,
}
}
}
use chrono::{
DateTime,
SecondsFormat,
@ -168,12 +129,12 @@ pub struct RelayState {
shutdown_watch_rx: watch::Receiver <bool>,
}
impl From <&ConfigFile> for RelayState {
fn from (config_file: &ConfigFile) -> Self {
impl From <Config> for RelayState {
fn from (config: Config) -> Self {
let (shutdown_watch_tx, shutdown_watch_rx) = watch::channel (false);
Self {
config: Config::from (config_file).into (),
config: config.into (),
handlebars: Arc::new (load_templates (&PathBuf::new ()).unwrap ()),
request_rendezvous: Default::default (),
server_status: Default::default (),
@ -222,12 +183,12 @@ async fn handle_http_listen (
let expected_tripcode = {
let config = state.config.read ().await;
match config.server_tripcodes.get (&watcher_code) {
match config.servers.get (&watcher_code) {
None => {
error! ("Denied http_listen for non-existent server name {}", watcher_code);
return trip_error;
},
Some (x) => (*x).clone (),
Some (x) => (*x).tripcode.clone (),
}
};
let actual_tripcode = blake3::hash (api_key);
@ -395,7 +356,7 @@ async fn handle_http_request (
{
{
let config = state.config.read ().await;
if ! config.server_tripcodes.contains_key (&watcher_code) {
if ! config.servers.contains_key (&watcher_code) {
return error_reply (StatusCode::NOT_FOUND, "Unknown server");
}
}
@ -538,8 +499,8 @@ fn pretty_print_last_seen (
#[derive (Serialize)]
struct ServerEntry <'a> {
path: String,
name: String,
id: String,
display_name: String,
last_seen: Cow <'a, str>,
}
@ -551,39 +512,56 @@ struct ServerListPage <'a> {
async fn handle_server_list_internal (state: &Arc <RelayState>)
-> ServerListPage <'static>
{
let all_servers: Vec <_> = {
let display_names: HashMap <String, String> = {
let guard = state.config.read ().await;
(*guard).server_tripcodes.keys ().cloned ().collect ()
let servers = (*guard).servers.iter ()
.map (|(k, v)| {
let display_name = v.display_name
.clone ()
.unwrap_or_else (|| k.clone ());
(k.clone (), display_name)
});
HashMap::from_iter (servers)
};
let server_status = {
let server_statuses = {
let guard = state.server_status.lock ().await;
(*guard).clone ()
};
let now = Utc::now ();
let mut servers: Vec <_> = server_status.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 ();
let mut servers: Vec <_> = display_names.into_iter ()
.map (|(id, display_name)| {
use LastSeen::*;
let last_seen = match pretty_print_last_seen (now, server.last_seen) {
let status = match server_statuses.get (&id) {
None => return ServerEntry {
display_name,
id,
last_seen: "Never".into (),
},
Some (x) => x,
};
let last_seen = match pretty_print_last_seen (now, status.last_seen) {
Negative => "Error (negative time)".into (),
Connected => "Connected".into (),
Description (s) => s.into (),
};
ServerEntry {
name: display_name,
path: name,
last_seen: last_seen,
display_name,
id,
last_seen,
}
})
.collect ();
servers.sort_by (|a, b| a.name.cmp (&b.name));
servers.sort_by (|a, b| a.display_name.cmp (&b.display_name));
ServerListPage {
servers,
@ -681,26 +659,15 @@ pub fn load_templates (asset_root: &Path)
async fn reload_config (
state: &Arc <RelayState>,
config_reload_path: &Path
) -> Option <()> {
use tokio::prelude::*;
let mut f = tokio::fs::File::open (config_reload_path).await.ok ()?;
let mut buffer = vec! [0u8; 4096];
let bytes_read = f.read (&mut buffer).await.ok ()?;
buffer.truncate (bytes_read);
let config_s = String::from_utf8 (buffer).ok ()?;
let new_config: ConfigFile = toml::from_str (&config_s).ok ()?;
let new_config = Config::from (&new_config);
) -> Result <(), ConfigError> {
let new_config = Config::from_file (config_reload_path).await?;
let mut config = state.config.write ().await;
(*config) = new_config;
debug! ("Loaded {} server tripcodes", config.server_tripcodes.len ());
debug! ("Loaded {} server configs", config.servers.len ());
Some (())
Ok (())
}
pub async fn run_relay (
@ -722,7 +689,7 @@ pub async fn run_relay (
loop {
reload_interval.tick ().await;
reload_config (&state_2, &config_reload_path).await;
reload_config (&state_2, &config_reload_path).await.ok ();
}
});
}

View File

@ -1,5 +1,3 @@
- Server list should include offline or never-launched servers
- Allow relay to rename servers
- Estimate bandwidth per server?
- "Preview as" feature for Markdown (It's not threaded through the relay yet)
- Remote `tail -f` (_Complicated_) (Maybe use chunked encoding or something?)