Compare commits

..

9 Commits

Author SHA1 Message Date
Trisha 800dbcb019 add hit counter 2022-03-25 16:39:26 -05:00
Trisha 365c878a90 ♻️ refactor: extract `refresh_label` for GUI 2022-03-25 16:17:09 -05:00
Trisha fb9b0c67f5 add multi-root 2022-03-25 16:14:22 -05:00
Trisha 436adb98ef 🚧 swich to the new routing func 2022-03-25 16:04:51 -05:00
Trisha de5338f4f2 ♻️ refactor: extract routing func 2022-03-25 15:47:34 -05:00
Trisha e6273209f9 🚧 wip: threading the multi-root stuff into the file server module 2022-03-25 15:40:36 -05:00
Trisha 81141e2faf add `file_server_roots` config 2022-03-25 15:30:38 -05:00
Trisha 8c4e7d484c ♻️ refactor: load server GUI config before creating the GUI 2022-03-25 15:24:45 -05:00
Trisha 1d9ef8f510 ⬆️ new FLTK 2022-03-25 14:42:50 -05:00
6 changed files with 193 additions and 38 deletions

36
Cargo.lock generated
View File

@ -246,6 +246,26 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crossbeam-channel"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
dependencies = [
"cfg-if",
"lazy_static",
]
[[package]] [[package]]
name = "ct-logs" name = "ct-logs"
version = "0.8.0" version = "0.8.0"
@ -345,20 +365,22 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]] [[package]]
name = "fltk" name = "fltk"
version = "1.2.8" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "889d9b2176b88c6f8f90ba64b0b030e52807ed1d6e416df0c15611225b40cc1d" checksum = "14c92a8adbc0189c9cade37f90bb50b3023cdef1c0c4de7eb5c36238dfdd2a23"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossbeam-channel",
"fltk-sys", "fltk-sys",
"paste", "paste",
"ttf-parser",
] ]
[[package]] [[package]]
name = "fltk-sys" name = "fltk-sys"
version = "1.2.8" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e38b2f3fb23b4bd46fc492d5d8d099b0bf766a7ab5d18b8424d93089ae934a48" checksum = "cbc9366cd150615afd138f261903e0029f40db720bceabe8e81bc7dac8f9cc11"
dependencies = [ dependencies = [
"cmake", "cmake",
] ]
@ -2155,6 +2177,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "ttf-parser"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c74c96594835e10fa545e2a51e8709f30b173a092bfd6036ef2cec53376244f3"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.14.0" version = "1.14.0"

View File

@ -5,7 +5,7 @@
use std::{ use std::{
cmp::min, cmp::min,
collections::HashMap, collections::*,
convert::{Infallible, TryFrom}, convert::{Infallible, TryFrom},
io::SeekFrom, io::SeekFrom,
path::{ path::{
@ -56,6 +56,7 @@ use errors::FileServerError;
#[derive (Default)] #[derive (Default)]
pub struct Config { pub struct Config {
pub file_server_root: PathBuf, pub file_server_root: PathBuf,
pub file_server_roots: BTreeMap <String, PathBuf>,
} }
pub struct FileServer { pub struct FileServer {
@ -68,7 +69,7 @@ pub struct FileServer {
impl FileServer { impl FileServer {
pub fn new ( pub fn new (
file_server_root: PathBuf, config: Config,
asset_root: &Path, asset_root: &Path,
name: String, name: String,
metrics_interval: Arc <ArcSwap <Option <metrics::Interval>>>, metrics_interval: Arc <ArcSwap <Option <metrics::Interval>>>,
@ -76,9 +77,7 @@ impl FileServer {
) -> Result <Self, FileServerError> ) -> Result <Self, FileServerError>
{ {
Ok (Self { Ok (Self {
config: Config { config,
file_server_root,
},
handlebars: load_templates (asset_root)?, handlebars: load_templates (asset_root)?,
metrics_startup: metrics::Startup::new (name), metrics_startup: metrics::Startup::new (name),
metrics_interval, metrics_interval,
@ -374,9 +373,12 @@ impl FileServer {
resp resp
} }
let root: &std::path::Path = &self.config.file_server_root; let roots = internal::FileRoots {
files: &self.config.file_server_root,
dirs: &self.config.file_server_roots,
};
Ok (match internal::serve_all (root, method, uri, headers, self.hidden_path.as_deref ()).await? { Ok (match internal::serve_all (roots, method, uri, headers, self.hidden_path.as_deref ()).await? {
Favicon => serve_error (StatusCode::NotFound, "Not found\n"), Favicon => serve_error (StatusCode::NotFound, "Not found\n"),
Forbidden => serve_error (StatusCode::Forbidden, "403 Forbidden\n"), Forbidden => serve_error (StatusCode::Forbidden, "403 Forbidden\n"),
MethodNotAllowed => serve_error (StatusCode::MethodNotAllowed, "Unsupported method\n"), MethodNotAllowed => serve_error (StatusCode::MethodNotAllowed, "Unsupported method\n"),

View File

@ -4,7 +4,7 @@
// human-readable HTML // human-readable HTML
use std::{ use std::{
collections::HashMap, collections::*,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@ -244,11 +244,59 @@ async fn serve_api (
Ok (NotFound) Ok (NotFound)
} }
#[derive (Clone, Copy)]
pub struct FileRoots <'a> {
pub files: &'a Path,
pub dirs: &'a BTreeMap <String, PathBuf>,
}
struct RoutedPath <'a> {
root: &'a Path,
path_s: std::borrow::Cow <'a, str>,
}
impl <'a> FileRoots <'a> {
fn route (self, input: &'a str) -> Result <Option <RoutedPath>, FileServerError> {
// TODO: There is totally a dir traversal attack in here somewhere
if let Some (path) = input.strip_prefix ("/dirs/") {
if let Some ((dir, path)) = path.split_once ('/') {
let root = match self.dirs.get (dir) {
None => return Ok (None),
Some (x) => x,
};
let path_s = percent_decode (path.as_bytes ()).decode_utf8 ().map_err (FileServerError::PathNotUtf8)?;
return Ok (Some (RoutedPath {
root,
path_s,
}));
}
else {
return Ok (None);
}
}
if let Some (path) = input.strip_prefix ("/files/") {
let encoded_path = &path [0..];
let path_s = percent_decode (encoded_path.as_bytes ()).decode_utf8 ().map_err (FileServerError::PathNotUtf8)?;
return Ok (Some (RoutedPath {
root: self.files,
path_s,
}));
}
return Ok (None);
}
}
// Handle the requests internally without knowing anything about PTTH or // Handle the requests internally without knowing anything about PTTH or
// HTML / handlebars // HTML / handlebars
pub async fn serve_all ( pub async fn serve_all (
root: &Path, roots: FileRoots <'_>,
method: Method, method: Method,
uri: &str, uri: &str,
headers: &HashMap <String, Vec <u8>>, headers: &HashMap <String, Vec <u8>>,
@ -283,19 +331,17 @@ pub async fn serve_all (
} }
if let Some (path) = path.strip_prefix ("/api") { if let Some (path) = path.strip_prefix ("/api") {
return serve_api (root, &uri, hidden_path, path).await; return serve_api (roots.files, &uri, hidden_path, path).await;
} }
let path = match path.strip_prefix ("/files/") { let RoutedPath {
Some (x) => x, root,
path_s,
} = match roots.route (path)? {
None => return Ok (NotFound), None => return Ok (NotFound),
Some (x) => x,
}; };
// TODO: There is totally a dir traversal attack in here somewhere
let encoded_path = &path [0..];
let path_s = percent_decode (encoded_path.as_bytes ()).decode_utf8 ().map_err (FileServerError::PathNotUtf8)?;
let path = Path::new (&*path_s); let path = Path::new (&*path_s);
let full_path = root.join (path); let full_path = root.join (path);

View File

@ -207,9 +207,14 @@ pub struct ConfigFile {
pub relay_url: String, pub relay_url: String,
/// Directory that the file server module will expose to clients /// Directory that the file server module will expose to clients
/// over the relay. If None, the current working dir is used. /// over the relay, under `/files`. If None, the current working dir is used.
pub file_server_root: PathBuf, pub file_server_root: PathBuf,
/// The file server module will expose these directories to clients under
/// `/dirs`. If symlinks can't be used (like on Windows), this allows PTTH
/// to serve multiple directories easily.
pub file_server_roots: BTreeMap <String, PathBuf>,
/// For debugging. /// For debugging.
pub throttle_upload: bool, pub throttle_upload: bool,
@ -226,6 +231,7 @@ impl ConfigFile {
api_key, api_key,
relay_url, relay_url,
file_server_root: PathBuf::from ("."), file_server_root: PathBuf::from ("."),
file_server_roots: Default::default (),
throttle_upload: false, throttle_upload: false,
client_keys: Default::default (), client_keys: Default::default (),
allow_any_client: true, allow_any_client: true,
@ -268,6 +274,7 @@ impl Builder {
api_key: ptth_core::gen_key (), api_key: ptth_core::gen_key (),
relay_url, relay_url,
file_server_root: PathBuf::from ("."), file_server_root: PathBuf::from ("."),
file_server_roots: Default::default (),
throttle_upload: false, throttle_upload: false,
client_keys: Default::default (), client_keys: Default::default (),
allow_any_client: true, allow_any_client: true,
@ -301,7 +308,8 @@ pub async fn run_server (
config_file: ConfigFile, config_file: ConfigFile,
shutdown_oneshot: oneshot::Receiver <()>, shutdown_oneshot: oneshot::Receiver <()>,
hidden_path: Option <PathBuf>, hidden_path: Option <PathBuf>,
asset_root: Option <PathBuf> asset_root: Option <PathBuf>,
hit_counter: Option <mpsc::Sender <()>>,
) )
-> Result <(), ServerError> -> Result <(), ServerError>
{ {
@ -312,7 +320,10 @@ pub async fn run_server (
}); });
let file_server = file_server::FileServer::new ( let file_server = file_server::FileServer::new (
config_file.file_server_root.clone (), file_server::Config {
file_server_root: config_file.file_server_root.clone (),
file_server_roots: config_file.file_server_roots.clone (),
},
&asset_root.clone ().unwrap_or_else (|| PathBuf::from (".")), &asset_root.clone ().unwrap_or_else (|| PathBuf::from (".")),
config_file.name.clone (), config_file.name.clone (),
metrics_interval, metrics_interval,
@ -330,8 +341,13 @@ pub async fn run_server (
let mut spawn_handler = || { let mut spawn_handler = || {
let file_server = Arc::clone (&file_server); let file_server = Arc::clone (&file_server);
let hit_counter = hit_counter.clone ();
|req: http_serde::RequestParts| async move { |req: http_serde::RequestParts| async move {
if let Some (hit_tx) = &hit_counter {
eprintln! ("hit_tx.send");
hit_tx.send (()).await;
}
Ok (file_server.serve_all (req.method, &req.uri, &req.headers).await?) Ok (file_server.serve_all (req.method, &req.uri, &req.headers).await?)
} }
}; };
@ -529,6 +545,12 @@ pub mod executable {
api_key: config_file.api_key, api_key: config_file.api_key,
relay_url: opt.relay_url.or (config_file.relay_url).ok_or (anyhow::anyhow! ("`--relay-url` must be provided in command line or `relay_url` in config file"))?, relay_url: opt.relay_url.or (config_file.relay_url).ok_or (anyhow::anyhow! ("`--relay-url` must be provided in command line or `relay_url` in config file"))?,
file_server_root: opt.file_server_root.or (config_file.file_server_root).unwrap_or_else (PathBuf::new), file_server_root: opt.file_server_root.or (config_file.file_server_root).unwrap_or_else (PathBuf::new),
file_server_roots: vec! [
("c", "C:/"),
("d", "D:/"),
].into_iter ()
.map (|(k, v)| (String::from (k), PathBuf::from (v)))
.collect (),
throttle_upload: opt.throttle_upload, throttle_upload: opt.throttle_upload,
allow_any_client: true, allow_any_client: true,
client_keys: Default::default (), client_keys: Default::default (),
@ -544,7 +566,8 @@ pub mod executable {
config_file, config_file,
ptth_core::graceful_shutdown::init (), ptth_core::graceful_shutdown::init (),
Some (path), Some (path),
asset_root asset_root,
None,
).await?; ).await?;
Ok (()) Ok (())

View File

@ -6,7 +6,7 @@ edition = "2018"
[dependencies] [dependencies]
anyhow = "1.0.38" anyhow = "1.0.38"
fltk = "1.1.0" fltk = "1.3.1"
serde = {version = "1.0.117", features = ["derive"]} serde = {version = "1.0.117", features = ["derive"]}
tokio = "1.4.0" tokio = "1.4.0"
tracing = "0.1.25" tracing = "0.1.25"

View File

@ -1,4 +1,5 @@
use std::{ use std::{
collections::*,
path::PathBuf, path::PathBuf,
}; };
@ -13,7 +14,10 @@ use fltk::{
}; };
use tokio::{ use tokio::{
sync::oneshot, sync::{
mpsc,
oneshot,
},
}; };
struct ServerInstance { struct ServerInstance {
@ -23,6 +27,7 @@ struct ServerInstance {
#[derive (Clone, Copy)] #[derive (Clone, Copy)]
enum Message { enum Message {
Hit,
RunServer, RunServer,
StopServer, StopServer,
} }
@ -42,7 +47,22 @@ fn main ()
let app = app::App::default(); let app = app::App::default();
let mut wind = Window::new (100, 100, 500, 180, "PTTH server"); let mut wind = Window::new (100, 100, 500, 180, "PTTH server");
let mut gui = Gui::new (fltk_tx); let config_file_opt = ptth_server::load_toml::load::<ConfigFile, _> ("./config/ptth_server.toml").ok ();
let (hit_tx, mut hit_rx) = mpsc::channel (1);
{
let fltk_tx = fltk_tx.clone ();
rt.spawn (async move {
eprintln! ("Entering channel task");
while let Some (_) = hit_rx.recv ().await {
eprintln! ("fltk_tx");
fltk_tx.send (Message::Hit);
}
});
}
let mut gui = Gui::new (fltk_tx, config_file_opt.as_ref ());
gui.set_server_running (false); gui.set_server_running (false);
wind.end (); wind.end ();
@ -50,25 +70,40 @@ fn main ()
while app.wait () { while app.wait () {
match fltk_rx.recv () { match fltk_rx.recv () {
Some (Message::Hit) => {
gui.hits += 1;
gui.refresh_label ();
},
Some (Message::RunServer) => { Some (Message::RunServer) => {
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel (); let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel ();
let roots = match &config_file_opt {
None => Default::default (),
Some (cf) => match &cf.file_server_roots {
None => Default::default (),
Some (x) => x.clone (),
},
};
let config_file = ptth_server::ConfigFile { let config_file = ptth_server::ConfigFile {
name: gui.input_name.value ().to_string (), name: gui.input_name.value ().to_string (),
api_key: gui.input_api_key.value ().to_string (), api_key: gui.input_api_key.value ().to_string (),
relay_url: gui.input_relay_url.value ().to_string (), relay_url: gui.input_relay_url.value ().to_string (),
file_server_root: std::path::PathBuf::from (gui.input_file_server_root.value ()), file_server_root: PathBuf::from (gui.input_file_server_root.value ()),
file_server_roots: roots,
throttle_upload: false, throttle_upload: false,
client_keys: Default::default (), client_keys: Default::default (),
allow_any_client: true, allow_any_client: true,
}; };
let hit_tx = hit_tx.clone ();
let task = rt.spawn (async { let task = rt.spawn (async {
if let Err (e) = ptth_server::run_server ( if let Err (e) = ptth_server::run_server (
config_file, config_file,
shutdown_rx, shutdown_rx,
None, None,
None None,
Some (hit_tx),
).await ).await
{ {
tracing::error! ("ptth_server crashed: {}", e); tracing::error! ("ptth_server crashed: {}", e);
@ -100,6 +135,9 @@ struct Gui {
input_relay_url: Input, input_relay_url: Input,
input_file_server_root: Input, input_file_server_root: Input,
input_api_key: SecretInput, input_api_key: SecretInput,
server_is_running: bool,
hits: u64,
} }
#[derive (Default, serde::Deserialize)] #[derive (Default, serde::Deserialize)]
@ -108,10 +146,16 @@ pub struct ConfigFile {
pub api_key: String, pub api_key: String,
pub relay_url: Option <String>, pub relay_url: Option <String>,
pub file_server_root: Option <PathBuf>, pub file_server_root: Option <PathBuf>,
pub file_server_roots: Option <BTreeMap <String, PathBuf>>,
} }
impl Gui { impl Gui {
fn new (fltk_tx: app::Sender <Message>) -> Self { fn new (
fltk_tx: app::Sender <Message>,
config_file_opt: Option <&ConfigFile>,
)
-> Self
{
let mut input_name = Input::new (200, 10, 290, 20, "name"); let mut input_name = Input::new (200, 10, 290, 20, "name");
input_name.set_value ("my_ptth_server"); input_name.set_value ("my_ptth_server");
@ -134,7 +178,7 @@ impl Gui {
but_stop.set_trigger (CallbackTrigger::Changed); but_stop.set_trigger (CallbackTrigger::Changed);
but_stop.emit (fltk_tx, Message::StopServer); but_stop.emit (fltk_tx, Message::StopServer);
if let Ok (config_file) = ptth_server::load_toml::load::<ConfigFile, _> ("./config/ptth_server.toml") if let Some (config_file) = config_file_opt
{ {
if let Some (v) = config_file.name.as_ref () { if let Some (v) = config_file.name.as_ref () {
input_name.set_value (v); input_name.set_value (v);
@ -158,16 +202,16 @@ impl Gui {
input_relay_url, input_relay_url,
input_file_server_root, input_file_server_root,
input_api_key, input_api_key,
server_is_running: false,
hits: 0,
} }
} }
fn set_server_running (&mut self, b: bool) { fn set_server_running (&mut self, b: bool) {
self.frame.set_label (if b { self.server_is_running = b;
"Running"
} self.refresh_label ();
else {
"Stopped"
});
set_active (&mut self.but_run, ! b); set_active (&mut self.but_run, ! b);
set_active (&mut self.but_stop, b); set_active (&mut self.but_stop, b);
@ -179,6 +223,18 @@ impl Gui {
set_active (&mut self.input_file_server_root, ! b); set_active (&mut self.input_file_server_root, ! b);
set_active (&mut self.input_api_key, ! b); set_active (&mut self.input_api_key, ! b);
} }
fn refresh_label (&mut self) {
let s =
if self.server_is_running {
format! ("Running. Requests: {}", self.hits)
}
else {
"Stopped".to_string ()
};
self.frame.set_label (&s);
}
} }
fn set_active <W: WidgetExt> (w: &mut W, b: bool) { fn set_active <W: WidgetExt> (w: &mut W, b: bool) {