diff --git a/Cargo.lock b/Cargo.lock index 1900bf4..2d64005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,6 +679,7 @@ dependencies = [ "sealed_test", "serde", "serde_json", + "serde_yaml", "time", "tokio", "toml", @@ -774,6 +775,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.16.0" @@ -1158,6 +1168,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92b5b431e8907b50339b51223b97d102db8d987ced36f6e4d03621db9316c834" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha2" version = "0.10.6" @@ -1267,6 +1290,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa", + "libc", + "num_threads", "serde", "time-core", "time-macros", @@ -1435,6 +1460,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 40b3577..3715279 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,13 +21,14 @@ percent-encoding = "2.2.0" route-recognizer = "0.3.1" serde_json = "1.0.87" tokio = { version = "1.21.2", features = ["full"] } -time = "0.3.17" +time = { version = "0.3.17", features = ["local-offset"] } url = "2.3.1" rust-embed = { version = "6.4.2", features = ["include-exclude"] } libflate = "1.2.0" brotli = { version = "3.3.4", features = ["std"] } toml = "0.5.9" once_cell = "1.16.0" +serde_yaml = "0.9.16" [dev-dependencies] lipsum = "0.8.2" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..2a91e1e --- /dev/null +++ b/build.rs @@ -0,0 +1,6 @@ +use std::process::Command; +fn main() { + let output = Command::new("git").args(["rev-parse", "HEAD"]).output().unwrap(); + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={git_hash}"); +} diff --git a/src/config.rs b/src/config.rs index c2d2055..fb672e1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; use std::{env::var, fs::read_to_string}; // Waiting for https://github.com/rust-lang/rust/issues/74465 to land, so we @@ -6,44 +7,47 @@ use std::{env::var, fs::read_to_string}; // // This is the local static that is initialized at runtime (technically at // first request) and contains the instance settings. -static CONFIG: Lazy = Lazy::new(Config::load); +pub(crate) static CONFIG: Lazy = Lazy::new(Config::load); /// Stores the configuration parsed from the environment variables and the /// config file. `Config::Default()` contains None for each setting. -#[derive(Default, serde::Deserialize)] +/// When adding more config settings, add it to `Config::load`, +/// `get_setting_from_config`, both below, as well as +/// instance_info::InstanceInfo.to_string(), README.md and app.json. +#[derive(Default, Serialize, Deserialize, Clone)] pub struct Config { #[serde(rename = "LIBREDDIT_SFW_ONLY")] - sfw_only: Option, + pub(crate) sfw_only: Option, #[serde(rename = "LIBREDDIT_DEFAULT_THEME")] - default_theme: Option, + pub(crate) default_theme: Option, #[serde(rename = "LIBREDDIT_DEFAULT_FRONT_PAGE")] - default_front_page: Option, + pub(crate) default_front_page: Option, #[serde(rename = "LIBREDDIT_DEFAULT_LAYOUT")] - default_layout: Option, + pub(crate) default_layout: Option, #[serde(rename = "LIBREDDIT_DEFAULT_WIDE")] - default_wide: Option, + pub(crate) default_wide: Option, #[serde(rename = "LIBREDDIT_DEFAULT_COMMENT_SORT")] - default_comment_sort: Option, + pub(crate) default_comment_sort: Option, #[serde(rename = "LIBREDDIT_DEFAULT_POST_SORT")] - default_post_sort: Option, + pub(crate) default_post_sort: Option, #[serde(rename = "LIBREDDIT_DEFAULT_SHOW_NSFW")] - default_show_nsfw: Option, + pub(crate) default_show_nsfw: Option, #[serde(rename = "LIBREDDIT_DEFAULT_BLUR_NSFW")] - default_blur_nsfw: Option, + pub(crate) default_blur_nsfw: Option, #[serde(rename = "LIBREDDIT_DEFAULT_USE_HLS")] - default_use_hls: Option, + pub(crate) default_use_hls: Option, #[serde(rename = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")] - default_hide_hls_notification: Option, + pub(crate) default_hide_hls_notification: Option, } impl Config { diff --git a/src/instance_info.rs b/src/instance_info.rs new file mode 100644 index 0000000..d9d42a8 --- /dev/null +++ b/src/instance_info.rs @@ -0,0 +1,108 @@ +use hyper::{http::Error, Body, Request, Response}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use crate::{ + config::{Config, CONFIG}, + server::RequestExt, +}; + +// This is the local static that is intialized at runtime (technically at +// the first request *to the instance-info endpoint) and contains the data +// retrieved from the instance-info endpoint. +pub(crate) static INSTANCE_INFO: Lazy = Lazy::new(InstanceInfo::new); + +/// Handles instance info endpoint +pub async fn instance_info(req: Request) -> Result, String> { + let extension = req.param("extension").unwrap_or("json".into()); + let response = match extension.as_str() { + "yaml" => info_yaml(), + "txt" => info_txt(), + "json" | _ => info_json(), + }; + response.map_err(|err| format!("{err}")) +} + +fn info_json() -> Result, Error> { + let body = serde_json::to_string(&*INSTANCE_INFO).unwrap_or("Error serializing JSON.".into()); + Response::builder().status(200).header("content-type", "application/json").body(body.into()) +} + +fn info_yaml() -> Result, Error> { + let body = serde_yaml::to_string(&*INSTANCE_INFO).unwrap_or("Error serializing YAML.".into()); + // https://github.com/ietf-wg-httpapi/mediatypes/blob/main/draft-ietf-httpapi-yaml-mediatypes.md + Response::builder().status(200).header("content-type", "application/yaml").body(body.into()) +} + +fn info_txt() -> Result, Error> { + Response::builder() + .status(200) + .header("content-type", "text/plain") + .body((INSTANCE_INFO.to_string()).into()) +} + +#[derive(Serialize, Deserialize, Default)] +pub(crate) struct InstanceInfo { + crate_version: String, + git_commit: String, + deploy_date: String, + compile_mode: String, + deploy_unix_ts: i64, + config: Config, +} + +impl InstanceInfo { + pub fn new() -> Self { + Self { + crate_version: env!("CARGO_PKG_VERSION").to_string(), + git_commit: env!("GIT_HASH").to_string(), + deploy_date: OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()).to_string(), + #[cfg(debug_assertions)] + compile_mode: "Debug".into(), + #[cfg(not(debug_assertions))] + compile_mode: "Release".into(), + deploy_unix_ts: OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()).unix_timestamp(), + config: CONFIG.clone(), + } + } +} +impl ToString for InstanceInfo { + fn to_string(&self) -> String { + format!( + "Crate version: {}\n + Git commit: {}\n + Deploy date: {}\n + Deploy timestamp: {}\n + Compile mode: {}\n + Config:\n + SFW only: {:?}\n + Default theme: {:?}\n + Default front page: {:?}\n + Default layout: {:?}\n + Default wide: {:?}\n + Default comment sort: {:?}\n + Default post sort: {:?}\n + Default show NSFW: {:?}\n + Default blur NSFW: {:?}\n + Default use HLS: {:?}\n + Default hide HLS notification: {:?}\n", + self.crate_version, + self.git_commit, + self.deploy_date, + self.deploy_unix_ts, + self.compile_mode, + self.config.sfw_only, + self.config.default_theme, + self.config.default_front_page, + self.config.default_layout, + self.config.default_wide, + self.config.default_comment_sort, + self.config.default_post_sort, + self.config.default_show_nsfw, + self.config.default_blur_nsfw, + self.config.default_use_hls, + self.config.default_hide_hls_notification + ) + } +} diff --git a/src/main.rs b/src/main.rs index bffe99c..1c282db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ // Reference local files mod config; mod duplicates; +mod instance_info; mod post; mod search; mod settings; @@ -20,6 +21,7 @@ use hyper::{header::HeaderValue, Body, Request, Response}; mod client; use client::{canonical_path, proxy}; +use once_cell::sync::Lazy; use server::RequestExt; use utils::{error, redirect, ThemeAssets}; @@ -156,6 +158,13 @@ async fn main() { // Begin constructing a server let mut app = server::Server::new(); + // Force evaluation of statics. In instance_info case, we need to evaluate + // the timestamp so deploy date is accurate - in config case, we need to + // evaluate the configuration to avoid paying penalty at first request. + + Lazy::force(&config::CONFIG); + Lazy::force(&instance_info::INSTANCE_INFO); + // Define default headers (added to all responses) app.default_headers = headers! { "Referrer-Policy" => "no-referrer", @@ -283,6 +292,10 @@ async fn main() { // Handle about pages app.at("/about").get(|req| error(req, "About pages aren't added yet".to_string()).boxed()); + // Instance info page + app.at("/instance-info").get(|r| instance_info::instance_info(r).boxed()); + app.at("/instance-info.:extension").get(|r| instance_info::instance_info(r).boxed()); + app.at("/:id").get(|req: Request| { Box::pin(async move { match req.param("id").as_deref() {