You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

205 lines
4.7 KiB
Rust

mod api;
mod util;
use std::{collections::BTreeMap, net::ToSocketAddrs};
use flate2::Compression;
use hyper::{
header::HeaderValue,
service::{make_service_fn, service_fn},
Body, Request as Request_, Response as Response_,
};
use serde::{Deserialize, Serialize};
use tower::ServiceBuilder;
type Request = Request_<Body>;
type Response = Response_<String>;
pub type Result<T> = core::result::Result<T, std::io::Error>;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpService {
url: String,
public_url: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ConfigSchema {
listen_addr: Option<String>,
title: Option<String>,
description: Option<String>,
hosts: BTreeMap<String, String>,
services: BTreeMap<String, HttpService>,
}
pub struct Config {
listen_addr: String,
title: String,
description: String,
hosts: BTreeMap<String, String>,
services: BTreeMap<String, HttpService>,
}
fn not_found() -> Result<Response> {
Ok(Response_::builder()
.status(404)
.body("Not found".into())
.unwrap())
}
fn internal_server_error() -> Result<Response> {
Ok(Response_::builder()
.status(500)
.body("Internal service erorr".into())
.unwrap())
}
async fn index() -> Result<Response> {
let config = read_config().await?;
let mut resp = Response::new(format!(
r###"
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{description}">
<script type="module">
{script}
</script>
<style>
{style}
</style>
<title>{title}</title>
</head>
<body>
<noscript>
You need to enable JavaScript to view this page.
</noscript>
</body>
</html>
"###,
title = config.title,
description = config.description,
style = include_str!("../frontend/style.css"),
script = include_str!(concat!(env!("OUT_DIR"), "/frontend.js")),
));
resp.headers_mut().insert(
"content-type",
HeaderValue::from_static("text/html; charset=utf-8"),
);
Ok(resp)
}
fn json<T: Serialize>(v: &T) -> Result<Response> {
let json = serde_json::to_string(&v)?;
let mut resp = Response::new(json);
resp.headers_mut()
.insert("content-type", HeaderValue::from_static("application/json"));
Ok(resp)
}
async fn route(req: Request) -> Result<Response> {
let parts = req.uri().path().split('/').skip(1).collect::<Vec<_>>();
let resp = match parts.as_slice() {
["api", "v1", ref rest @ ..] => match rest {
["configuration", "get"] => {
json(&api::v1::get_configuration().await?)?
}
["host", "getStatus"] => json(
&api::v1::get_host_status(serde_json::from_slice(
&hyper::body::to_bytes(req.into_body()).await.unwrap(),
)?)
.await?,
)?,
["service", "getStatus"] => json(
&api::v1::get_service_status(serde_json::from_slice(
&hyper::body::to_bytes(req.into_body()).await.unwrap(),
)?)
.await?,
)?,
_ => return not_found(),
},
[""] => index().await?,
_ => return not_found(),
};
Ok(resp)
}
pub async fn read_config() -> Result<Config> {
let config = tokio::fs::read_to_string("./config.json").await?;
let parsed: ConfigSchema = serde_json::from_str(&config)?;
Ok(Config {
listen_addr: parsed
.listen_addr
.unwrap_or_else(|| "127.0.0.1:8888".into()),
title: parsed.title.clone().unwrap_or_else(|| "Statue".into()),
description: match parsed.description {
Some(d) => d,
None => match parsed.title {
Some(t) => t,
None => "Statue - a simple status monitor".into(),
},
},
hosts: parsed.hosts,
services: parsed.services,
})
}
async fn serve() -> Result<()> {
let config = read_config().await?;
let addr = config.listen_addr.to_socket_addrs()?.next().unwrap();
let service = service_fn(|req| async {
let resp = route(req).await;
match resp {
Ok(r) => Ok(r),
Err(e) => {
eprintln!("{}", e);
internal_server_error()
}
}
});
let make_service = make_service_fn(|_conn| async move {
let service = ServiceBuilder::new()
.layer(util::Log)
.layer(util::Compress {
algorithms: util::AlgorithmPreferences {
brotli: Some((3, util::brotli_default_params(4, true))),
gzip: Some((2, Compression::fast())),
deflate: Some((1, Compression::fast())),
},
predicate: util::CompressPredicate {
min_size: 256,
content_type_predicate: |_: &str| true,
},
})
.service(service);
Ok::<_, std::convert::Infallible>(service)
});
let server = hyper::server::Server::bind(&addr).serve(make_service);
println!("Listening on {}", addr);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
Ok(())
}
#[tokio::main]
async fn main() {
serve().await.unwrap();
}