If using ssh keys be sure the host is added to ~/ssh/known_hosts for
///the user that runs busser, including root
#[derive(Clone, Serialize, Deserialize)]
pub struct GitAuthConfig
@@ -188,7 +191,7 @@ pub struct Config
pub relay: Option
>
}
-impl Config
+impl Config
{
pub fn default() -> Config
{
@@ -239,7 +242,7 @@ pub fn read_config(path: &str) -> Option
let config: Config = match serde_json::from_str(&data)
{
Ok(data) => {data},
- Err(why) =>
+ Err(why) =>
{
crate::debug(format!("Error reading configuration file {}\n{}", path, why), None);
return None
@@ -248,7 +251,7 @@ pub fn read_config(path: &str) -> Option
Some(config)
}
- else
+ else
{
crate::debug(format!("Error configuration file {} does not exist", path), None);
None
diff --git a/src/content/error_page.rs b/src/content/error_page.rs
new file mode 100644
index 0000000..53aa466
--- /dev/null
+++ b/src/content/error_page.rs
@@ -0,0 +1,91 @@
+use crate::{config::Config, filesystem::file::read_file_utf8};
+
+pub const DEFAULT_BODY: &str = r#"
+
+
+
+
+
+ Error ERROR_CODE
+
+
+
+
+
+
+
+
That's a ERROR_CODE error.
+
Let's go home.
+
+
+
+
+"#;
+
+pub struct ErrorPage
+{
+ pub body_template: String
+}
+
+impl ErrorPage
+{
+ fn expand_template(template: String, config: &Config) -> String
+ {
+ template.replace("LINK_TO_HOME", &format!("https://{}",&config.domain))
+ }
+
+ pub fn expand_error_code(&self, code: &str) -> String
+ {
+ self.body_template.replace("ERROR_CODE", code)
+ }
+
+ pub fn from(config: &Config) -> ErrorPage
+ {
+ if let Some(ref path) = config.content.error_template
+ {
+ if let Some(body) = read_file_utf8(&path)
+ {
+ return ErrorPage {body_template: Self::expand_template(body, config)}
+ }
+ }
+ ErrorPage {body_template: Self::expand_template(DEFAULT_BODY.to_string(), config)}
+ }
+}
\ No newline at end of file
diff --git a/src/content/mod.rs b/src/content/mod.rs
index 58c093b..f15944e 100644
--- a/src/content/mod.rs
+++ b/src/content/mod.rs
@@ -17,9 +17,10 @@ use self::mime_type::{Mime, MIME};
pub mod mime_type;
pub mod filter;
pub mod sitemap;
+pub mod error_page;
-/// Store web content
-///
+/// Store web content
+///
/// - The body is unpopulated until [Content::load_from_file] is called
/// - The body may be converted to a utf8 string using [Content::utf8_body]
/// - A hash of the file is used to check it is stale, used by [Observed]
@@ -38,9 +39,9 @@ pub struct Content
tag_insertion: bool
}
-pub trait HasUir
+pub trait HasUir
{
- fn get_uri(&self) -> String;
+ fn get_uri(&self) -> String;
}
impl PartialEq for Content
@@ -96,7 +97,7 @@ impl Observed for Content
}
}
- fn last_refreshed(&self) -> SystemTime
+ fn last_refreshed(&self) -> SystemTime
{
self.last_refreshed.clone()
}
@@ -114,11 +115,11 @@ impl Content
{
pub fn new(uri: &str, disk_path: &str, server_cache: u16, browser_cache: u16, tag_insertion: bool) -> Content
{
- Content
- {
- uri: uri.to_string(),
- body: vec![],
- disk_path: disk_path.to_string(),
+ Content
+ {
+ uri: uri.to_string(),
+ body: vec![],
+ disk_path: disk_path.to_string(),
content_type: ::infer_mime_type(disk_path),
server_cache_period_seconds: server_cache,
browser_cache_period_seconds: browser_cache,
@@ -145,14 +146,14 @@ impl Content
{
match self.read_bytes()
{
- Some(data) =>
+ Some(data) =>
{
self.body = data.clone();
self.hash = hash(data);
self.last_refreshed = SystemTime::now();
Ok(())
}
- None =>
+ None =>
{
self.last_refreshed = SystemTime::now();
Err(FileError { why: format!("Could not read bytes from {}", self.disk_path)})
@@ -180,7 +181,7 @@ impl Content
let preview_body = match self.utf8_body()
{
Ok(s) => s[0..min(s.len(), n)].to_string(),
- Err(_e) =>
+ Err(_e) =>
{
dump_bytes(&self.body)[0..min(self.body.len(), n)].to_string()
}
@@ -193,13 +194,13 @@ impl Content
/// this may be disabled by launching as busser --no-tagging
pub fn insert_tag(body: String)
-> String
-{
+{
format!("\n{}", program_version(), body)
}
impl IntoResponse for Content {
fn into_response(self) -> Response {
-
+
let mut response = if self.content_type == MIME::TextHtml
{
let mut string_body = match self.utf8_body()
@@ -229,22 +230,22 @@ impl IntoResponse for Content {
response.headers_mut()
.insert("cache-control", format!("public, max-age={}", self.browser_cache_period_seconds).parse().unwrap());
-
+
response
}
}
pub fn is_page(uri: &str, domain: &str) -> bool
{
- if uri == "/"
+ if uri == "/"
{
return true
}
-
+
let domain_escaped = domain.replace("https://", "").replace("http://", "").replace(".", r"\.");
match Regex::new(format!(r"((^|(http)(s|)://){})(/|/[^\.]+|/[^\.]+.html|$)$",domain_escaped).as_str())
{
- Ok(re) =>
+ Ok(re) =>
{
re.is_match(uri)
},
diff --git a/src/content/sitemap.rs b/src/content/sitemap.rs
index 10ffb20..fe8495d 100644
--- a/src/content/sitemap.rs
+++ b/src/content/sitemap.rs
@@ -1,5 +1,5 @@
-use std::{collections::BTreeMap, future::Future, sync::Arc, time::{Duration, Instant, SystemTime}, vec};
+use std::{collections::BTreeMap, sync::Arc, time::{Duration, Instant, SystemTime}, vec};
use openssl::sha::Sha256;
use tokio::sync::Mutex;
@@ -64,8 +64,8 @@ impl ContentTree
{
router = router.route
(
- &uri,
- get(move || async move
+ &uri,
+ get(move || async move
{
let mut content = content.lock().await;
if !static_router && content.server_cache_expired() && content.is_stale()
@@ -106,7 +106,7 @@ impl ContentTree
/// List all uris
pub fn collect_uris(&self) -> Vec
- {
+ {
let mut uris: Vec = self.contents.keys().cloned().collect();
for (_, child) in &self.children
{
@@ -130,7 +130,7 @@ impl ContentTree
match uri_stem.find("/")
{
- Some(loc) =>
+ Some(loc) =>
{
if loc < uri_stem.len()-1
{
@@ -145,7 +145,7 @@ impl ContentTree
{
if !self.children.contains_key(&child_uri_stem)
{
- self.children.insert(child_uri_stem.clone(), ContentTree::new(&reduced_uri_stem.clone()));
+ self.children.insert(child_uri_stem.clone(), ContentTree::new(&reduced_uri_stem.clone()));
}
self.children.get_mut(&child_uri_stem).unwrap().push(reduced_uri_stem, content);
@@ -221,7 +221,7 @@ impl ContentTree
Ok(())
})
{
- Ok(_) =>
+ Ok(_) =>
{
if buffer.len() > 0
{
@@ -244,7 +244,7 @@ impl ContentTree
/// If no sitemap.xml or robots.txt is present
/// these will be generated by calling [SiteMap::to_xml]
/// and inserting the resulting sitemap.xml
-///
+///
/// Convertable to a router, see [ContentTree] for dynamic
/// options
#[derive(Clone)]
@@ -309,7 +309,7 @@ impl SiteMap
contents.sort_by_key(|x|x.get_uri());
tic = Instant::now();
- let bar = if !silent
+ let bar = if !silent
{
println!("Building sitemap");
Some(ProgressBar::new(contents.len() as u64))
@@ -322,7 +322,7 @@ impl SiteMap
let mut content_tree = ContentTree::new("/");
for content in contents
- {
+ {
if content.get_uri().contains("config.json") { continue }
crate::debug(format!("Adding content {:?}", content.preview(64)), None);
let path = config.content.path.clone()+"/";
@@ -354,10 +354,10 @@ impl SiteMap
let home = Content::new
(
- "/",
- &config.content.home.clone(),
- config.content.server_cache_period_seconds,
- config.content.browser_cache_period_seconds,
+ "/",
+ &config.content.home.clone(),
+ config.content.server_cache_period_seconds,
+ config.content.browser_cache_period_seconds,
tag
);
@@ -449,7 +449,7 @@ impl SiteMap
content.append(&mut content_buffer);
content_buffer = content;
}
-
+
let mut str_content = String::from_utf8(content_buffer)?;
let lines = str_content.matches("\n").count();
if lines > 1
@@ -490,7 +490,7 @@ impl Into for SiteMap
}
}
-/// Format for lastmod (t) in an xml sitemap
+/// Format for lastmod (t) in an xml sitemap
pub fn lastmod(t: SystemTime) -> String
{
let date: DateTime = t.into();
diff --git a/src/main.rs b/src/main.rs
index f056b11..73a4f46 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,14 +7,14 @@ use busser::integrations::git::clean_and_clone;
use busser::server::http::ServerHttp;
use busser::server::https::Server;
use busser::util::formatted_differences;
-use busser::{openssl_version, program_version, task};
+use busser::{openssl_version, program_version};
use tokio::task::spawn;
#[tokio::main]
async fn main() {
let args: Vec = std::env::args().collect();
-
+
if args.iter().any(|x| x == "-v")
{
println!("Version: {}\n{}", program_version(), openssl_version());
@@ -39,13 +39,13 @@ async fn main() {
{
true
};
-
+
let http_server = ServerHttp::new(0,0,0,0);
let _http_redirect = spawn(http_server.serve());
match read_config(CONFIG_PATH)
{
- Some(c) =>
+ Some(c) =>
{
if c.git.is_some()
{
@@ -81,9 +81,9 @@ async fn main() {
/// Serve by observing the site content found at the path [busser::config::ContentConfig]
/// every [busser::config::ContentConfig::server_cache_period_seconds] the sitemap
-/// hash (see [busser::content::sitemap::SiteMap::get_hash]) is checked, if it is
-/// different the server is re-served.
-///
+/// hash (see [busser::content::sitemap::SiteMap::get_hash]) is checked, if it is
+/// different the server is re-served.
+///
/// On a re-serve if [busser::config::ContentConfig::message_on_sitemap_reload] is true
/// A status message with (uri) additions and removals will be posted to Discord.
async fn serve_observed(insert_tag: bool)
@@ -101,13 +101,13 @@ async fn serve_observed(insert_tag: bool)
let mut server_handle = server.get_handle();
let mut thread_handle = spawn(async move {server.serve()}.await);
let mut task_handle = spawn(async move {tasks.run()}.await);
-
+
loop
{
-
+
busser::debug(format!("Next sitemap check: {}s", config.content.server_cache_period_seconds), None);
tokio::time::sleep(Duration::from_secs(config.content.server_cache_period_seconds.into())).await;
-
+
let new_sitemap = SiteMap::build(&config, insert_tag, false);
let sitemap_hash = new_sitemap.get_hash();
diff --git a/src/server/https.rs b/src/server/https.rs
index 519d71a..a68e90b 100644
--- a/src/server/https.rs
+++ b/src/server/https.rs
@@ -1,18 +1,18 @@
use crate::
{
- config::{read_config, CONFIG_PATH}, content::sitemap::SiteMap, integrations::{git::refresh::GitRefreshTask, github::filter_github}, server::throttle::{handle_throttle, IpThrottler}, task::{schedule_from_option, TaskPool}, CRAB
+ config::{read_config, CONFIG_PATH}, content::{error_page::ErrorPage, sitemap::SiteMap}, integrations::{git::refresh::GitRefreshTask, github::filter_github}, server::throttle::{handle_throttle, IpThrottler}, task::{schedule_from_option, TaskPool}, CRAB
};
use core::time;
use std::{net::{IpAddr, Ipv4Addr, SocketAddr}, time::SystemTime};
use std::path::PathBuf;
use std::sync::Arc;
+
use tokio::sync::Mutex;
use axum::
{
- middleware,
- Router
+ middleware, response::Html, Router
};
use axum_server::{tls_rustls::RustlsConfig, Handle};
@@ -47,16 +47,16 @@ pub fn parse_uri(uri: String, path: String) -> String
}
}
-impl Server
+impl Server
{
- pub fn new
+ pub fn new
(
a: u8,
b: u8,
c: u8,
d: u8,
sitemap: SiteMap
- )
+ )
-> (Server, TaskPool)
{
@@ -71,13 +71,13 @@ impl Server
let requests: IpThrottler = IpThrottler::new
(
- config.throttle.max_requests_per_second,
+ config.throttle.max_requests_per_second,
config.throttle.timeout_millis,
config.throttle.clear_period_seconds
);
let throttle_state = Arc::new(Mutex::new(requests));
-
+
let mut router: Router = sitemap.into();
let stats = Arc::new(Mutex::new(
@@ -94,6 +94,9 @@ impl Server
router = router.layer(middleware::from_fn_with_state(repo_mutex.clone(), filter_github));
router = router.layer(middleware::from_fn(filter_relay));
+ let error_page = ErrorPage::from(&config);
+ router = router.fallback(Html(error_page.expand_error_code("404")));
+
let server = Server
{
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(a,b,c,d)), config.port_https),
@@ -112,9 +115,9 @@ impl Server
(
StatsSaveTask::new
(
- stats.clone(),
+ stats.clone(),
schedule_from_option(config.stats.save_schedule.clone())
- )
+ )
)
);
@@ -124,9 +127,9 @@ impl Server
(
StatsDigestTask::new
(
- stats.clone(),
+ stats.clone(),
schedule_from_option(config.stats.digest_schedule.clone())
- )
+ )
)
);
@@ -173,7 +176,7 @@ impl Server
.await
{
Ok(c) => c,
- Err(e) =>
+ Err(e) =>
{
println!("error while reading certificates in {} and key {}\n{}", cert_path, key_path, e);
std::process::exit(1);
@@ -194,7 +197,7 @@ impl Server
{
println!("(or https://127.0.0.1)");
}
-
+
axum_server::bind_rustls(self.addr, config)
.handle(self.handle.clone())
.serve(self.router.clone().into_make_service_with_connect_info::())
diff --git a/tests/config.json b/tests/config.json
index 96a6175..fd58858 100644
--- a/tests/config.json
+++ b/tests/config.json
@@ -1,13 +1,13 @@
{
"port_https": 443,
- "port_http": 80,
- "throttle":
+ "port_http": 80,
+ "throttle":
{
- "max_requests_per_second": 64.0,
- "timeout_millis": 5000,
+ "max_requests_per_second": 64.0,
+ "timeout_millis": 5000,
"clear_period_seconds": 3600
},
- "stats":
+ "stats":
{
"path": "tests/stats",
"hit_cooloff_seconds": 60,
@@ -15,7 +15,7 @@
"digest_schedule": "0 0 1 * * Fri *",
"ignore_regexes": ["/favicon.ico"]
},
- "content":
+ "content":
{
"path": "tests/pages",
"home": "tests/pages/a.html",
diff --git a/tests/test_config.rs b/tests/test_config.rs
index 44b57b9..040c62e 100644
--- a/tests/test_config.rs
+++ b/tests/test_config.rs
@@ -44,6 +44,7 @@ mod config
assert_eq!(config.content.ignore_regexes.unwrap(), vec!["/.git", "workspace"]);
assert_eq!(config.content.static_content, None);
assert_eq!(config.content.generate_sitemap, Some(false));
+ assert_eq!(config.content.error_template, None);
assert!(config.relay.is_some());
let relay = config.relay.unwrap();
@@ -95,6 +96,7 @@ mod config
assert_eq!(content.server_cache_period_seconds, 3600);
assert_eq!(content.static_content, Some(false));
assert_eq!(content.message_on_sitemap_reload, Some(false));
+ assert_eq!(content.error_template, None);
let config = Config::default();
@@ -174,6 +176,7 @@ mod config
assert_eq!(config.content.browser_cache_period_seconds, 3600);
assert_eq!(config.content.server_cache_period_seconds, 1);
assert_eq!(config.content.ignore_regexes.unwrap(), vec!["/.git", "workspace"]);
+ assert_eq!(config.content.error_template, None);
}
#[test]
diff --git a/tests/test_content.rs b/tests/test_content.rs
index 7f60aa7..0883ef9 100644
--- a/tests/test_content.rs
+++ b/tests/test_content.rs
@@ -5,7 +5,7 @@ mod test_content
{
use std::{collections::HashMap, fs::remove_file, path::Path, thread::sleep, time};
- use busser::{content::{filter::ContentFilter, get_content, insert_tag, is_page, mime_type::MIME, Content, HasUir}, filesystem::file::{file_hash, write_file_bytes, Observed}, util::read_bytes};
+ use busser::{config::{read_config, Config}, content::{error_page::{ErrorPage, DEFAULT_BODY}, filter::ContentFilter, get_content, insert_tag, is_page, mime_type::MIME, Content, HasUir}, filesystem::file::{file_hash, write_file_bytes, Observed}, util::read_bytes};
#[test]
fn test_load_content()
@@ -233,5 +233,31 @@ mod test_content
}
}
+ #[test]
+ fn test_error_page()
+ {
+ let mut config = Config::default();
+ let default_error = ErrorPage::from(&config);
+ assert_ne!(default_error.body_template, DEFAULT_BODY);
+ assert!(default_error.body_template.contains("127.0.0.1"));
+ assert!(!default_error.body_template.contains("LINK_TO_HOME"));
+ assert!(default_error.body_template.contains("ERROR_CODE"));
+
+ let body = default_error.expand_error_code("404");
+ assert!(!body.contains("ERROR_CODE"));
+ assert!(body.contains("404"));
+
+ let config = read_config("tests/config.json").unwrap();
+ let default_error = ErrorPage::from(&config);
+ assert_ne!(default_error.body_template, DEFAULT_BODY);
+ assert!(default_error.body_template.contains("127.0.0.1"));
+ assert!(!default_error.body_template.contains("LINK_TO_HOME"));
+ assert!(default_error.body_template.contains("ERROR_CODE"));
+
+ let body = default_error.expand_error_code("404");
+ assert!(!body.contains("ERROR_CODE"));
+ assert!(body.contains("404"));
+ }
+
}