first commit

This commit is contained in:
aitzol 2024-08-01 13:21:20 +02:00
parent c1a55f72c5
commit a8170c19ce
10 changed files with 3527 additions and 1 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.env

2784
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "services"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-files = "0.6.6"
actix-governor = "0.5.0"
actix-session = { version = "0.9.0", features = ["cookie-session"] }
actix-web = "4.8.0"
awc = { version = "3.5.0", features = ["openssl"] }
dotenv = "0.15.0"
dotenv_config = "0.1.9"
governor = "0.6.3"
handlebars = "5.1.2"
json = "0.12.4"
lazy_static = "1.5.0"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
tokio = "1.38.1"
tokio-xmpp = "3.5.0"

View File

@ -1,3 +1,14 @@
# server-status
Check the status of the services you offer on your server.
* Create `.env` file containing the following variables
```bash
PORT=3000
XMPP_USER=user
XMPP_PASS=password
```
* Setup configuration in `config.json`
* run code with `cargo run`

9
src/config.json Normal file
View File

@ -0,0 +1,9 @@
{
"web": [
"lainoa.eus",
"apunteak.lainoa.eus",
"git.lainoa.eus",
"nextcloud.lainoa.eus"
],
"xmpp": "lainoa.eus"
}

44
src/config.rs Normal file
View File

@ -0,0 +1,44 @@
use std::fs::File;
use std::io::{Read, Error as IoError};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub web: Vec<String>,
pub xmpp: String,
}
impl Config {
pub fn new() -> Result<Self, IoError> {
//let mut file = File::open("./src/config.json").unwrap().try_into().ok()?;
let mut file = match File::open(CONFIG_FILE_PATH) {
Ok(file) => file,
Err(error) => {
match error.kind() {
std::io::ErrorKind::NotFound => {
println!("Config file not found");
return Ok(Self{ //default configuration
web: ["nextcloud.com".to_string()].to_vec(),
xmpp: "conversations.im".to_string()
});
}
_ => return Err(error),
}
}
};
let mut buff = String::new();
file.read_to_string(&mut buff).unwrap();
//println!("{}", &buff);
let foo: Config = serde_json::from_str(&buff).unwrap();
Ok(Self {
web: foo.web,
xmpp: foo.xmpp
})
}
}
const CONFIG_FILE_PATH: &str = "./src/config.json";

246
src/main.rs Normal file
View File

@ -0,0 +1,246 @@
mod services;
use crate::services::Service;
mod config;
use crate::config::Config;
use actix_files::Files;
use actix_governor::{Governor, GovernorConfigBuilder, KeyExtractor, SimpleKeyExtractionError};
use actix_web::{get, web,
http::{
header::ContentType,
StatusCode,
// Method, StatusCode,
},
//cookie::{ Key, SameSite, Cookie },
App, /*HttpRequest,*/ HttpServer, HttpResponse, Responder, HttpResponseBuilder
};
use actix_web::dev::ServiceRequest;
use governor::{NotUntil, clock::{Clock, QuantaInstant, DefaultClock}};
use awc::Client;
use serde_json::{Map, Value};
use handlebars::Handlebars;
use handlebars::handlebars_helper;
use tokio_xmpp::SimpleClient as XmppClient;
#[macro_use]
extern crate lazy_static;
lazy_static! {
static ref CONFIG: config::Config =
Config::new().expect("to load config file");
}
#[get("/")]
async fn index(/*req: HttpRequest, */ hb: web::Data<Handlebars<'_>>) -> impl Responder {
/*
if let Some(val) = req.peer_addr() {
println!("Address {:?}", val.ip());
};
println!("ip: {:?}", req.connection_info().realip_remote_addr());
*/
let mut list:Vec<String> = Vec::new();
// add web services
let web_services = &CONFIG.web;
for web_service in web_services.into_iter() {
let mut service = "web:".to_string();
service.push_str(web_service);
list.push(service);
}
//add xmpp service
let xmpp_service = &CONFIG.xmpp;
let mut service = "xmpp:".to_string();
service.push_str(xmpp_service);
list.push(service);
// check services
let res = check(list.to_vec()).await;
let body = hb.render("index", &res).unwrap();
HttpResponse::Ok()
.content_type("text/html")
.body(body)
}
async fn check(list:Vec<String>) -> Map<String, Value>{
let mut obj = Map::new();
let mut vmap = Vec::new();
let mut map = Map::new();
#[derive(Debug)]
//#[derive(Debug, strum_macros::Display)]
enum Class {
Web,
Mail,
Xmpp,
Unknown
}
impl Class {
fn as_str(&self) -> &'static str {
match self {
Class::Web => "Web",
Class::Mail => "Mail",
Class::Xmpp => "Xmpp",
Class::Unknown => "Unknown",
}
}
}
//https://stackoverflow.com/questions/65040158/can-i-create-string-enum
for service in list.into_iter(){
let class = match &service {
s if s.starts_with("web") => Class::Web,
s if s.starts_with("xmpp") => Class::Xmpp,
s if s.starts_with("mail") => Class::Mail,
_ => Class::Unknown,
};
match class {
Class::Web => {
let n: Vec<&str> = service.split(":").collect();
let name = n[1];
let mut url = "https://".to_string();
url.push_str(&name);
//println!("service type: {:?}", class.as_str().to_string());
let web_service = Service::new(name.to_string(), ws(url.to_string()).await, class.as_str().to_string());
obj.insert("name".to_string(), Value::String(web_service.name));
obj.insert("status".to_string(), Value::String(web_service.status));
obj.insert("class".to_string(), Value::String(web_service.class));
vmap.push(obj.clone());
},
Class::Xmpp => {
let n: Vec<&str> = service.split(":").collect();
let name = n[1];
let xmpp_service = Service::new(name.to_string(), xs(name.to_string()).await, class.as_str().to_string());
obj.insert("name".to_string(), Value::String(xmpp_service.name));
obj.insert("status".to_string(), Value::String(xmpp_service.status));
obj.insert("class".to_string(), Value::String(xmpp_service.class));
vmap.push(obj.clone());
},
Class::Mail => println!("mail service"),
Class::Unknown => println!("Unknown service")
}
}
map.insert("data".to_string(), serde_json::json!(vmap).into());
map
}
//xmpp server check
async fn xs(server: String) -> StatusCode {
dotenv::dotenv().ok();
let xmpp_user = std::env::var("XMPP_USER").unwrap_or("user".to_string());
let jid = format!("{}@{}",xmpp_user,server);
let password = std::env::var("XMPP_PASS").unwrap_or("secret".to_string());
// Xmpp Client instance
let client = XmppClient::new(&jid, password.to_owned()).await;
match client {
Ok(_) => {
//println!("Client connected!");
client.expect("REASON").end().await.unwrap();
StatusCode::OK
},
Err(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
// web service check
async fn ws(url: String) -> StatusCode {
let client = Client::default();
let res = client
.get(url)
.send()
.await;
//res.unwrap().status()
match res {
Ok(status) => status.status(),
Err(..) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok();
let port = std::env::var("PORT").unwrap_or("8080".to_string());
let address = format!("127.0.0.1:{}", port);
//println!("web: {:?}", CONFIG.web);
//println!("web: {:?}", CONFIG.web[0]);
// Allow bursts with up to five requests per IP address
// and replenishes one element every two seconds
let governor_conf = GovernorConfigBuilder::default()
.per_second(4)
.burst_size(2)
.finish()
.unwrap();
handlebars_helper!(compare: |a: String, b: String | a == b);
let mut handlebars = Handlebars::new();
handlebars.register_helper("compare", Box::new(compare));
handlebars
.register_template_file("index", "./static/index.html")
.unwrap();
let handlebars_ref = web::Data::new(handlebars);
HttpServer::new(move || {
App::new()
// Enable Governor middleware
.wrap(Governor::new(&governor_conf))
// Route hello world service
//.route("/", web::get().to(index))
.app_data(handlebars_ref.clone())
.service(Files::new("/static", "static").show_files_listing())
.service(index)
})
.bind(&address)?
.run()
.await
}
#[derive(Clone)]
//#[warn(dead_code)]
#[allow(dead_code)]
//#[allow(unused_variables)]
struct Foo;
// will return 500 error and 'Extract error' as content
impl KeyExtractor for Foo {
type Key = ();
type KeyExtractionError = SimpleKeyExtractionError<&'static str>;
fn extract(&self, _req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
Err(SimpleKeyExtractionError::new("Extract error"))
}
fn exceed_rate_limit_response(
&self,
negative: &NotUntil<QuantaInstant>,
mut response: HttpResponseBuilder,
) -> HttpResponse {
let wait_time = negative
.wait_time_from(DefaultClock::default().now())
.as_secs();
response
.content_type(ContentType::plaintext())
.body(format!("Too many requests, retry in {}s", wait_time))
}
}

27
src/services.rs Normal file
View File

@ -0,0 +1,27 @@
use awc::http::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Service {
pub name: String,
pub status: String,
pub class: String,
}
impl Service {
pub fn new(name: String, code: StatusCode, class: String) -> Self {
Self {
name,
status: code.to_string(),
class,
}
}
}
/*
#[derive(Deserialize, Serialize)]
pub struct Config {
pub web: Vec<String>,
pub xmpp: String,
pub email: String
}
*/

48
static/index.html Normal file
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<title>Zerbitzuak</title>
<meta charset="UTF-8" />
<!--<link rel="shortcut icon" type="image/x-icon" href="/favicon">-->
<link rel="stylesheet" href="./static/styles.css">
</head>
<body>
<h1>Zerbitzuen egoera</h1>
<h4 class="logs">Web zerbitzuak</h4>
{{#each data}}
{{#if (compare this.class "Web")}}
<div class="grid-container">
<div class="grid-item">
{{this.name}}
</div>
<div class="grid-item">
{{#if (compare this.status "200 OK")}}
<div class="ok"></div>
{{else}}
<div class="down"></div>
{{/if}}
</div>
</div>
{{/if}}
{{/each}}
<h4 class="logs">Xmpp mezularitza</h4>
{{#each data}}
{{#if (compare this.class "Xmpp")}}
<div class="grid-container">
<div class="grid-item">
{{this.name}}
</div>
<div class="grid-item">
{{#if (compare this.status "200 OK")}}
<div class="ok"></div>
{{else}}
<div class="down"></div>
{{/if}}
</div>
</div>
{{/if}}
{{/each}}
</body>
</html>

334
static/styles.css Normal file
View File

@ -0,0 +1,334 @@
body {
font-family: sans-serif;
color: #333;
}
main {
margin: 0 auto;
}
h1 {
font-size: 2em;
margin-bottom: 2.5em;
margin-top: 2em;
text-align: center;
}
h1 span{
text-transform: capitalize;
}
a:link{
text-decoration: none;
}
a:active {
color: #1F1F1F;
}
/*
.user p{
text-transform: capitalize;
}
form[name="fullNameForm"] input{
text-transform: capitalize;
}
*/
form, .logs {
/* border-radius: 0.2rem;
border: 1px solid #CCC;*/
margin: 0 auto;
max-width: 16rem;
padding: 2rem 2.5rem 1.5rem 2.5rem;
}
input {
background-color: #FAFAFA;
border-radius: 0.2rem;
border: 1px solid #CCC;
box-shadow: inset 0 1px 3px #DDD;
box-sizing: border-box;
display: block;
font-size: 1em;
padding: 0.4em 0.6em;
vertical-align: middle;
width: 100%;
}
.form-buttons{
display:flex;
flex-direction:row;
}
.form-buttons > a, .form-buttons > button{
flex-grow:1;
}
input:focus {
background-color: #FFF;
border-color: #51A7E8;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075) inset, 0 0 5px rgba(81, 167, 232, 0.5);
outline: 0;
}
input.not-required {
background-color: #E0E0E0;
}
label {
color: #666;
display: block;
font-size: 0.9em;
font-weight: bold;
margin: 1em 0 0.25em 0;
}
button {
border-radius: 0.2rem;
border: 1px solid;
box-sizing: border-box;
color: #fff;
cursor: pointer;
display: block;
font-size: 0.9em;
font-weight: bold;
margin: 2em 0 0.5em 0;
padding: 0.5em 0.7em;
text-align: center;
text-decoration: none;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3);
user-select: none;
vertical-align: middle;
white-space: nowrap;
}
button.green {
background-color: #60B044;
background-image: linear-gradient(#8ADD6D, #60B044);
border-color: #5CA941;
}
button.green:focus,
button.green:hover {
background-color: #569E3D;
background-image: linear-gradient(#79D858, #569E3D);
border-color: #4A993E;
}
button.red {
background-color: #E74C3C;
background-image: linear-gradient(#CF0B04, #E74C3C);
border-color: #C0392B;
}
button.red:focus,
button.red:hover{
background-color: #D13E2E;
background-image: linear-gradient(#CF0000, #D13E2E);
border-color: #C92F1E;
}
.last-login-info {
background-color: #efefef;
/*background-image: linear-gradient(#fcfcfc, #f5f5f5);*/
background-image: linear-gradient(#f9e028, #ffd966);
padding-top: 1px;
padding-bottom: 1px;
border-radius: 0.2rem;
text-align: center;
}
.date {
font-weight: 100;
font-size: 0.8em;
}
.device-list li{
background-color: #efefef;
background-image: linear-gradient(#fcfcfc, #f6f6f6);
list-style: none;
font-size: 0.7em;
margin-bottom: 2px;
padding: 2px;
border-radius: 0.2rem;
}
/*alerts*/
.alerts {
margin: 2rem auto 0 auto;
max-width: 30rem;
animation-duration: 5s;
animation-fill-mode: both;
-webkit-animation-duration: 5s;
-webkit-animation-fill-mode: both;
}
.alert {
border-radius: 0.2rem;
border: 1px solid;
color: #fff;
padding: 0.7em 1.5em;
}
.alert.error {
background-color: #E74C3C;
border-color: #C0392B;
}
.alert.success {
background-color: #60B044;
border-color: #5CA941;
}
@-webkit-keyframes fadeOut {
0% {opacity: 1;}
70% {opacity: 1;}
100% {opacity: 0;}
}
@keyframes fadeOut {
0% {opacity: 1;}
60% {opacity: 1;}
100% {opacity: 0;}
}
.fadeOut {
-webkit-animation-name: fadeOut;
animation-name: fadeOut;
}
/**/
.qr-code {
margin: 0 auto;
width: max-content;
text-align: center;
}
/**/
.info {
margin: 0 auto;
max-width: 24rem;
padding: 0 2.5rem 0 2.5rem;
text-align: justify;
}
/**/
.grid-container {
display: grid;
grid-template-columns: auto max-content;
margin: 0 auto;
max-width: 16rem;
padding: 0 2.5rem 0 2.5rem;
align-items: first baseline;
}
.grid-item {
overflow-x: auto;
}
.grid-item a {
color: #222;
}
.grid-item a:visited {
color: #888;
}
.grid-item h5 {
text-transform: capitalize;
}
.grid-item input{
display: unset;
max-width: max-content;
}
/**/
.ok,.down {
min-width: 18px;
border-radius: 9px;
min-height: 18px;
}
.ok {
background-color: lawngreen;
}
.down {
background-color: red;
}
/**/
@media only screen and (max-width: 480px) {
form {
border: 0;
}
.info {
max-width: 16rem;
}
}
@media only screen and (min-width: 481px) and (max-width: 760px) {
.info {
max-width: 22rem;
}
}
/*
* https://remixicon.com
* https://github.com/Remix-Design/RemixIcon
* Copyright RemixIcon.com
* Released under the Apache License Version 2.0
*/
@font-face {
font-family: "remixicon";
src: url('fonts/remixicon.eot?t=1700036445706'); /* IE9*/
src: url('fonts/remixicon.eot?t=1700036445706#iefix') format('embedded-opentype'), /* IE6-IE8 */
url("fonts/remixicon.woff2?t=1700036445706") format("woff2"),
url("fonts/remixicon.woff?t=1700036445706") format("woff"),
url('fonts/remixicon.ttf?t=1700036445706') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('fonts/remixicon.svg?t=1700036445706#remixicon') format('svg'); /* iOS 4.1- */
font-display: swap;
}
[class^="ri-"], [class*="ri-"] {
font-family: 'remixicon' !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.ri-lg { font-size: 1.3333em; line-height: 0.75em; vertical-align: -.0667em; }
.ri-xl { font-size: 1.5em; line-height: 0.6666em; vertical-align: -.075em; }
.ri-xxs { font-size: .5em; }
.ri-xs { font-size: .75em; }
.ri-sm { font-size: .875em }
.ri-1x { font-size: 1em; }
.ri-2x { font-size: 2em; }
.ri-3x { font-size: 3em; }
.ri-4x { font-size: 4em; }
.ri-5x { font-size: 5em; }
.ri-6x { font-size: 6em; }
.ri-7x { font-size: 7em; }
.ri-8x { font-size: 8em; }
.ri-9x { font-size: 9em; }
.ri-10x { font-size: 10em; }
.ri-fw { text-align: center; width: 1.25em; }
.ri-home-line:before { content: "\ee2b"; }
.ri-home-fill:before { content: "\ee26"; }
.ri-mail-line:before { content: "\eef6"; }
.ri-mail-fill:before { content: "\eef3"; }
.ri-send-plane-line:before { content: "\f0da"; }
.ri-send-plane-fill:before { content: "\f0d9"; }
.ri-chat-3-line:before { content: "\eb51"; }
.ri-chat-3-fill:before { content: "\eb50"; }
.ri-pencil-line:before { content: "\efe0"; }
.ri-pencil-fill:before { content: "\efdf"; }
.ri-file-line:before { content: "\eceb"; }
.ri-file-fill:before { content: "\ece0"; }
.ri-settings-3-line:before { content: "\f0e6"; }
.ri-settings-3-fill:before { content: "\f0e5"; }
.ri-user-line:before { content: "\f264"; }
.ri-user-fill:before { content: "\f25f"; }
.ri-account-circle-line:before { content: "\ea09"; }
.ri-account-circle-fill:before { content: "\ea08"; }
.ri-delete-bin-line:before { content: "\ec2a"; }
.ri-delete-bin-fill:before { content: "\ec29"; }
.ri-toggle-line:before { content: "\f219"; }
.ri-toggle-fill:before { content: "\f218"; }
.ri-history-line:before { content: "\ee17"; }
.ri-history-fill:before { content: "\ee16"; }}