Implement instance info endpoint (JSON, YAML, TXT)

This commit is contained in:
Matthew Esposito 2023-01-03 14:59:19 -05:00
parent 0ff92cbfe3
commit 1cc3104498
6 changed files with 177 additions and 14 deletions

31
Cargo.lock generated
View File

@ -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"

View File

@ -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"

6
build.rs Normal file
View File

@ -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}");
}

View File

@ -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<Config> = Lazy::new(Config::load);
pub(crate) static CONFIG: Lazy<Config> = 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<String>,
pub(crate) sfw_only: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_THEME")]
default_theme: Option<String>,
pub(crate) default_theme: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_FRONT_PAGE")]
default_front_page: Option<String>,
pub(crate) default_front_page: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_LAYOUT")]
default_layout: Option<String>,
pub(crate) default_layout: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_WIDE")]
default_wide: Option<String>,
pub(crate) default_wide: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_COMMENT_SORT")]
default_comment_sort: Option<String>,
pub(crate) default_comment_sort: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_POST_SORT")]
default_post_sort: Option<String>,
pub(crate) default_post_sort: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_SHOW_NSFW")]
default_show_nsfw: Option<String>,
pub(crate) default_show_nsfw: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_BLUR_NSFW")]
default_blur_nsfw: Option<String>,
pub(crate) default_blur_nsfw: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_USE_HLS")]
default_use_hls: Option<String>,
pub(crate) default_use_hls: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")]
default_hide_hls_notification: Option<String>,
pub(crate) default_hide_hls_notification: Option<String>,
}
impl Config {

108
src/instance_info.rs Normal file
View File

@ -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<InstanceInfo> = Lazy::new(InstanceInfo::new);
/// Handles instance info endpoint
pub async fn instance_info(req: Request<Body>) -> Result<Response<Body>, 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<Response<Body>, 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<Response<Body>, 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<Response<Body>, 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
)
}
}

View File

@ -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<Body>| {
Box::pin(async move {
match req.param("id").as_deref() {