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.

189 lines
4.1 KiB
Rust

mod api;
use std::{collections::HashMap, net::ToSocketAddrs};
use hyper::{
header::HeaderValue,
service::{make_service_fn, service_fn},
Body, Request as Request_, Response as Response_,
};
use serde::{Deserialize, Serialize};
type Request = Request_<Body>;
type Response = Response_<Body>;
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: HashMap<String, String>,
services: HashMap<String, HttpService>,
}
pub struct Config {
listen_addr: String,
title: String,
description: String,
hosts: HashMap<String, String>,
services: HashMap<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")),
)
.into(),
);
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.into());
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 make_service = make_service_fn(|_conn| async {
Ok::<_, std::io::Error>(service_fn(|req| async {
let resp = route(req).await;
match resp {
Ok(r) => Ok(r),
Err(e) => {
eprintln!("{}", e);
internal_server_error()
}
}
}))
});
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();
}