💄 Show servers in the server list even if they aren't connected yet
parent
bf4e5c7a5b
commit
28ce6a32cd
|
@ -20,6 +20,7 @@ futures = "0.3.7"
|
||||||
handlebars = "3.5.1"
|
handlebars = "3.5.1"
|
||||||
http = "0.2.1"
|
http = "0.2.1"
|
||||||
hyper = "0.13.8"
|
hyper = "0.13.8"
|
||||||
|
itertools = "0.9.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
|
@ -30,7 +31,7 @@ reqwest = { version = "0.10.8", features = ["stream"] }
|
||||||
rmp-serde = "0.14.4"
|
rmp-serde = "0.14.4"
|
||||||
serde = {version = "1.0.117", features = ["derive"]}
|
serde = {version = "1.0.117", features = ["derive"]}
|
||||||
structopt = "0.3.20"
|
structopt = "0.3.20"
|
||||||
# thiserror = "1.0.22"
|
thiserror = "1.0.22"
|
||||||
tokio = { version = "0.2.22", features = ["full"] }
|
tokio = { version = "0.2.22", features = ["full"] }
|
||||||
tracing = "0.1.21"
|
tracing = "0.1.21"
|
||||||
tracing-futures = "0.2.4"
|
tracing-futures = "0.2.4"
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
{{#each servers}}
|
{{#each servers}}
|
||||||
<tr>
|
<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>
|
<td><span class="grey">{{this.last_seen}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
|
@ -12,7 +12,10 @@ use tracing_subscriber::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use ptth::relay;
|
use ptth::relay;
|
||||||
use ptth::relay::RelayState;
|
use ptth::relay::{
|
||||||
|
Config,
|
||||||
|
RelayState,
|
||||||
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main () -> Result <(), Box <dyn Error>> {
|
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_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);
|
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 (
|
forced_shutdown.wrap_server (
|
||||||
relay::run_relay (
|
relay::run_relay (
|
||||||
Arc::new (RelayState::from (&config_file)),
|
Arc::new (RelayState::from (config)),
|
||||||
shutdown_rx,
|
shutdown_rx,
|
||||||
Some (config_path)
|
Some (config_path)
|
||||||
)
|
)
|
||||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -43,6 +43,7 @@ pub fn password_is_bad (mut password: String) -> bool {
|
||||||
#[cfg (test)]
|
#[cfg (test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::{
|
||||||
|
convert::TryFrom,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
|
@ -104,14 +105,17 @@ mod tests {
|
||||||
let api_key = "AnacondaHardcoverGrannyUnlatchLankinessMutate";
|
let api_key = "AnacondaHardcoverGrannyUnlatchLankinessMutate";
|
||||||
let tripcode = base64::encode (blake3::hash (api_key.as_bytes ()).as_bytes ());
|
let tripcode = base64::encode (blake3::hash (api_key.as_bytes ()).as_bytes ());
|
||||||
debug! ("Relay is expecting tripcode {}", tripcode);
|
debug! ("Relay is expecting tripcode {}", tripcode);
|
||||||
let config_file = relay::ConfigFile {
|
let config_file = relay::config::file::Config {
|
||||||
port: None,
|
port: None,
|
||||||
server_tripcodes: hashmap! {
|
servers: hashmap! {
|
||||||
server_name.into () => tripcode,
|
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 relay_state_2 = relay_state.clone ();
|
||||||
let (stop_relay_tx, stop_relay_rx) = oneshot::channel ();
|
let (stop_relay_tx, stop_relay_rx) = oneshot::channel ();
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
125
src/relay/mod.rs
125
src/relay/mod.rs
|
@ -27,7 +27,6 @@ use hyper::{
|
||||||
};
|
};
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use serde::{
|
use serde::{
|
||||||
Deserialize,
|
|
||||||
Serialize,
|
Serialize,
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
|
@ -51,6 +50,10 @@ use crate::{
|
||||||
prefix_match,
|
prefix_match,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
pub use config::{Config, ConfigError};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Here's what we need to handle:
|
Here's what we need to handle:
|
||||||
|
@ -92,48 +95,6 @@ enum RequestRendezvous {
|
||||||
|
|
||||||
type ResponseRendezvous = oneshot::Sender <Result <(http_serde::ResponseParts, Body), RelayError>>;
|
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::{
|
use chrono::{
|
||||||
DateTime,
|
DateTime,
|
||||||
SecondsFormat,
|
SecondsFormat,
|
||||||
|
@ -168,12 +129,12 @@ pub struct RelayState {
|
||||||
shutdown_watch_rx: watch::Receiver <bool>,
|
shutdown_watch_rx: watch::Receiver <bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From <&ConfigFile> for RelayState {
|
impl From <Config> for RelayState {
|
||||||
fn from (config_file: &ConfigFile) -> Self {
|
fn from (config: Config) -> Self {
|
||||||
let (shutdown_watch_tx, shutdown_watch_rx) = watch::channel (false);
|
let (shutdown_watch_tx, shutdown_watch_rx) = watch::channel (false);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
config: Config::from (config_file).into (),
|
config: config.into (),
|
||||||
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 (),
|
server_status: Default::default (),
|
||||||
|
@ -222,12 +183,12 @@ async fn handle_http_listen (
|
||||||
let expected_tripcode = {
|
let expected_tripcode = {
|
||||||
let config = state.config.read ().await;
|
let config = state.config.read ().await;
|
||||||
|
|
||||||
match config.server_tripcodes.get (&watcher_code) {
|
match config.servers.get (&watcher_code) {
|
||||||
None => {
|
None => {
|
||||||
error! ("Denied http_listen for non-existent server name {}", watcher_code);
|
error! ("Denied http_listen for non-existent server name {}", watcher_code);
|
||||||
return trip_error;
|
return trip_error;
|
||||||
},
|
},
|
||||||
Some (x) => (*x).clone (),
|
Some (x) => (*x).tripcode.clone (),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let actual_tripcode = blake3::hash (api_key);
|
let actual_tripcode = blake3::hash (api_key);
|
||||||
|
@ -395,7 +356,7 @@ async fn handle_http_request (
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
let config = state.config.read ().await;
|
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");
|
return error_reply (StatusCode::NOT_FOUND, "Unknown server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -538,8 +499,8 @@ fn pretty_print_last_seen (
|
||||||
|
|
||||||
#[derive (Serialize)]
|
#[derive (Serialize)]
|
||||||
struct ServerEntry <'a> {
|
struct ServerEntry <'a> {
|
||||||
path: String,
|
id: String,
|
||||||
name: String,
|
display_name: String,
|
||||||
last_seen: Cow <'a, str>,
|
last_seen: Cow <'a, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,39 +512,56 @@ struct ServerListPage <'a> {
|
||||||
async fn handle_server_list_internal (state: &Arc <RelayState>)
|
async fn handle_server_list_internal (state: &Arc <RelayState>)
|
||||||
-> ServerListPage <'static>
|
-> ServerListPage <'static>
|
||||||
{
|
{
|
||||||
let all_servers: Vec <_> = {
|
let display_names: HashMap <String, String> = {
|
||||||
let guard = state.config.read ().await;
|
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;
|
let guard = state.server_status.lock ().await;
|
||||||
(*guard).clone ()
|
(*guard).clone ()
|
||||||
};
|
};
|
||||||
|
|
||||||
let now = Utc::now ();
|
let now = Utc::now ();
|
||||||
|
|
||||||
let mut servers: Vec <_> = server_status.into_iter ()
|
let mut servers: Vec <_> = display_names.into_iter ()
|
||||||
.map (|(name, server)| {
|
.map (|(id, display_name)| {
|
||||||
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::*;
|
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 (),
|
Negative => "Error (negative time)".into (),
|
||||||
Connected => "Connected".into (),
|
Connected => "Connected".into (),
|
||||||
Description (s) => s.into (),
|
Description (s) => s.into (),
|
||||||
};
|
};
|
||||||
|
|
||||||
ServerEntry {
|
ServerEntry {
|
||||||
name: display_name,
|
display_name,
|
||||||
path: name,
|
id,
|
||||||
last_seen: last_seen,
|
last_seen,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect ();
|
.collect ();
|
||||||
|
|
||||||
servers.sort_by (|a, b| a.name.cmp (&b.name));
|
servers.sort_by (|a, b| a.display_name.cmp (&b.display_name));
|
||||||
|
|
||||||
ServerListPage {
|
ServerListPage {
|
||||||
servers,
|
servers,
|
||||||
|
@ -681,26 +659,15 @@ pub fn load_templates (asset_root: &Path)
|
||||||
async fn reload_config (
|
async fn reload_config (
|
||||||
state: &Arc <RelayState>,
|
state: &Arc <RelayState>,
|
||||||
config_reload_path: &Path
|
config_reload_path: &Path
|
||||||
) -> Option <()> {
|
) -> Result <(), ConfigError> {
|
||||||
use tokio::prelude::*;
|
let new_config = Config::from_file (config_reload_path).await?;
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
let mut config = state.config.write ().await;
|
let mut config = state.config.write ().await;
|
||||||
(*config) = new_config;
|
(*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 (
|
pub async fn run_relay (
|
||||||
|
@ -722,7 +689,7 @@ pub async fn run_relay (
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
reload_interval.tick ().await;
|
reload_interval.tick ().await;
|
||||||
reload_config (&state_2, &config_reload_path).await;
|
reload_config (&state_2, &config_reload_path).await.ok ();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
2
todo.md
2
todo.md
|
@ -1,5 +1,3 @@
|
||||||
- Server list should include offline or never-launched servers
|
|
||||||
- Allow relay to rename servers
|
|
||||||
- Estimate bandwidth per server?
|
- Estimate bandwidth per server?
|
||||||
- "Preview as" feature for Markdown (It's not threaded through the relay yet)
|
- "Preview as" feature for Markdown (It's not threaded through the relay yet)
|
||||||
- Remote `tail -f` (_Complicated_) (Maybe use chunked encoding or something?)
|
- Remote `tail -f` (_Complicated_) (Maybe use chunked encoding or something?)
|
||||||
|
|
Loading…
Reference in New Issue