2023-09-30 15:35:41 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2023-10-03 10:56:35 +00:00
|
|
|
"context"
|
2023-10-03 12:08:50 +00:00
|
|
|
"embed"
|
2023-10-07 08:24:49 +00:00
|
|
|
"fmt"
|
2023-10-01 15:40:22 +00:00
|
|
|
"io/fs"
|
2023-09-30 16:56:37 +00:00
|
|
|
"log"
|
2023-10-01 00:17:09 +00:00
|
|
|
"net"
|
2023-09-30 15:35:41 +00:00
|
|
|
"net/http"
|
2023-10-01 00:17:09 +00:00
|
|
|
"os"
|
2023-10-03 13:49:49 +00:00
|
|
|
os_signal "os/signal"
|
2023-10-07 08:58:43 +00:00
|
|
|
"strings"
|
2023-10-03 13:49:49 +00:00
|
|
|
"syscall"
|
2023-10-03 10:56:35 +00:00
|
|
|
"time"
|
2023-09-30 15:35:41 +00:00
|
|
|
|
2023-10-01 08:21:30 +00:00
|
|
|
"git.exozy.me/exozyme/status/core"
|
2023-10-01 15:40:22 +00:00
|
|
|
"git.exozy.me/exozyme/status/scanner"
|
2023-09-30 15:35:41 +00:00
|
|
|
"github.com/cbroglie/mustache"
|
2023-10-20 08:25:18 +00:00
|
|
|
"github.com/timewasted/go-accept-headers"
|
2023-09-30 15:35:41 +00:00
|
|
|
)
|
|
|
|
|
2023-10-01 15:40:22 +00:00
|
|
|
//go:embed all:public
|
|
|
|
var staticAssets embed.FS
|
|
|
|
|
|
|
|
//go:embed all:templates
|
|
|
|
var templateAssets embed.FS
|
|
|
|
|
|
|
|
func parseAndRenderTemplate(templateName string, data interface{}) (string, error) {
|
|
|
|
rawTemplate, err := templateAssets.ReadFile(templateName)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
tmpl, err := mustache.ParseString(string(rawTemplate))
|
2023-09-30 15:35:41 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Println("Failed to parse template:", err)
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := tmpl.Render(data)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Failed to render template:", err)
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2023-10-20 08:25:18 +00:00
|
|
|
func index(w http.ResponseWriter, r *http.Request) {
|
2023-09-30 17:04:55 +00:00
|
|
|
if r.Method != "GET" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-01 08:49:02 +00:00
|
|
|
config_new, err := core.LoadConfig()
|
|
|
|
if err != nil {
|
|
|
|
log.Println("[WARN] failed to load config:", err)
|
|
|
|
} else {
|
|
|
|
config = config_new
|
|
|
|
}
|
|
|
|
|
2023-09-30 16:15:11 +00:00
|
|
|
// get service status'
|
2023-10-03 10:56:35 +00:00
|
|
|
timeout := time.Second
|
2023-10-03 12:08:50 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
2023-10-07 08:24:49 +00:00
|
|
|
defer cancel()
|
2023-10-03 12:48:42 +00:00
|
|
|
data := scanner.CheckServiceBatch(ctx, config.Service)
|
2023-10-07 08:24:49 +00:00
|
|
|
non_duplicates, err := dbm.StoreServiceStatusBatch(data, time.Now())
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err = notify(non_duplicates)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
2023-09-30 17:04:55 +00:00
|
|
|
|
2023-10-03 10:40:41 +00:00
|
|
|
// don't uncomment this in production. verbose
|
|
|
|
// log.Printf("%v", data)
|
2023-09-30 17:04:55 +00:00
|
|
|
|
2023-10-20 08:25:18 +00:00
|
|
|
ua := r.UserAgent()
|
|
|
|
|
|
|
|
prefer_text := strings.HasPrefix(ua, "curl/")
|
|
|
|
|
|
|
|
header_accept := accept.Parse(r.Header.Get("Accept"))
|
|
|
|
accept_text := header_accept.Accepts("text/plain")
|
|
|
|
accept_html := header_accept.Accepts("text/html")
|
|
|
|
|
|
|
|
if (!accept_html || prefer_text) && accept_text {
|
|
|
|
indexForVT100(w, r, data)
|
|
|
|
} else if accept_html {
|
|
|
|
indexForBrowser(w, r, data)
|
|
|
|
} else { // when no valid content type is requested
|
|
|
|
// not to-spec. still sends plain text anyway
|
|
|
|
indexForVT100(w, r, data)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func indexForVT100(w http.ResponseWriter, r *http.Request, data scanner.ServiceStatusBatch) {
|
|
|
|
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
|
|
|
|
parsedTemplate := formatPlainText(data)
|
|
|
|
w.Write([]byte(parsedTemplate))
|
|
|
|
}
|
|
|
|
|
|
|
|
func indexForBrowser(w http.ResponseWriter, r *http.Request, data scanner.ServiceStatusBatch) {
|
2023-09-30 17:57:42 +00:00
|
|
|
parsedTemplate, err := parseAndRenderTemplate("templates/index.html", data)
|
2023-09-30 15:35:41 +00:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
2023-10-07 08:24:49 +00:00
|
|
|
return
|
2023-09-30 15:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
w.Write([]byte(parsedTemplate))
|
|
|
|
}
|
|
|
|
|
2023-10-01 08:21:30 +00:00
|
|
|
var config *core.Config
|
2023-10-03 13:23:57 +00:00
|
|
|
var dbm core.Database
|
2023-10-07 08:24:49 +00:00
|
|
|
var client *core.MatrixClient
|
2023-10-01 08:21:30 +00:00
|
|
|
|
2023-10-03 13:49:49 +00:00
|
|
|
func cleanup() {
|
|
|
|
log.Println("Cleaning up")
|
|
|
|
_ = dbm.StoreSelfStatus(core.EventSelfDown, time.Now())
|
2023-10-03 18:23:25 +00:00
|
|
|
_ = dbm.Persist()
|
2023-10-03 13:49:49 +00:00
|
|
|
}
|
|
|
|
|
2023-09-30 15:35:41 +00:00
|
|
|
func main() {
|
2023-10-01 08:32:35 +00:00
|
|
|
var err error
|
|
|
|
config, err = core.LoadConfig()
|
2023-10-01 08:21:30 +00:00
|
|
|
if err != nil {
|
2023-10-03 13:49:49 +00:00
|
|
|
log.Panic(err)
|
2023-10-01 08:21:30 +00:00
|
|
|
}
|
|
|
|
|
2023-10-03 12:44:55 +00:00
|
|
|
dbm, err = core.OpenDatabase(config.Database)
|
|
|
|
if err != nil {
|
2023-10-03 13:49:49 +00:00
|
|
|
log.Panic(err)
|
2023-10-03 12:44:55 +00:00
|
|
|
}
|
|
|
|
|
2023-10-07 08:24:49 +00:00
|
|
|
client, err = core.NewMatrixClient(config.Matrix)
|
|
|
|
if err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
|
|
|
|
2023-10-20 08:25:18 +00:00
|
|
|
http.HandleFunc("/", index)
|
2023-10-01 15:40:22 +00:00
|
|
|
|
|
|
|
// serve public folder from embedfs
|
|
|
|
htmlContent, err := fs.Sub(fs.FS(staticAssets), "public")
|
|
|
|
if err != nil {
|
2023-10-03 13:49:49 +00:00
|
|
|
log.Panic(err)
|
2023-10-01 15:40:22 +00:00
|
|
|
}
|
|
|
|
fs := http.FileServer(http.FS(htmlContent))
|
2023-09-30 17:57:42 +00:00
|
|
|
http.Handle("/public/", http.StripPrefix("/public/", fs))
|
2023-09-30 15:35:41 +00:00
|
|
|
|
2023-10-01 00:17:09 +00:00
|
|
|
protocol := "tcp"
|
|
|
|
addr := "localhost:3333"
|
|
|
|
|
2023-10-01 03:34:12 +00:00
|
|
|
var unix_socket_path string
|
|
|
|
var is_unix bool
|
|
|
|
|
|
|
|
if config.UnixSocket != "" {
|
|
|
|
// get unix socket path from the config else
|
|
|
|
unix_socket_path = config.UnixSocket
|
|
|
|
is_unix = true
|
|
|
|
} else {
|
|
|
|
// fallback
|
|
|
|
unix_socket_path, is_unix = os.LookupEnv("PORT")
|
|
|
|
}
|
|
|
|
|
2023-10-01 00:17:09 +00:00
|
|
|
if is_unix {
|
|
|
|
protocol = "unix"
|
|
|
|
addr = unix_socket_path
|
|
|
|
|
2023-10-01 03:34:12 +00:00
|
|
|
// VnPower: is this unsafe?
|
2023-10-01 00:17:09 +00:00
|
|
|
removeFile(addr) // ideally this should be run at program exit. sadly Go's defer doesn't care about signals or panic
|
|
|
|
}
|
|
|
|
|
|
|
|
srv := &http.Server{Addr: addr, Handler: nil}
|
|
|
|
ln, err := net.Listen(protocol, addr)
|
|
|
|
if err != nil {
|
2023-10-03 13:49:49 +00:00
|
|
|
log.Panic(err)
|
2023-10-01 00:17:09 +00:00
|
|
|
}
|
2023-10-01 00:24:30 +00:00
|
|
|
if is_unix {
|
|
|
|
err = os.Chmod(addr, 0660)
|
|
|
|
if err != nil {
|
2023-10-03 13:49:49 +00:00
|
|
|
log.Panic(err)
|
2023-10-01 00:24:30 +00:00
|
|
|
}
|
|
|
|
log.Printf("Listening on %v\n", addr)
|
|
|
|
} else {
|
|
|
|
log.Printf("Listening on http://%v/\n", addr)
|
|
|
|
}
|
2023-10-03 13:49:49 +00:00
|
|
|
|
|
|
|
// log startup and register cleanup (on SIGTERM or panic)
|
2023-10-07 08:24:49 +00:00
|
|
|
ch_sigterm := make(chan os.Signal, 1)
|
2023-10-03 18:23:25 +00:00
|
|
|
os_signal.Notify(ch_sigterm, syscall.SIGTERM, syscall.SIGINT)
|
2023-10-03 13:49:49 +00:00
|
|
|
|
|
|
|
log.Print("Starting up")
|
|
|
|
err = dbm.StoreSelfStatus(core.EventSelfUp, time.Now())
|
|
|
|
if err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
|
|
|
defer cleanup()
|
|
|
|
|
2023-10-03 15:11:46 +00:00
|
|
|
go func() {
|
|
|
|
<-ch_sigterm
|
2023-10-03 13:49:49 +00:00
|
|
|
err := srv.Shutdown(context.Background())
|
2023-10-03 18:23:25 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Printf("Error when shutting down: %v", err)
|
|
|
|
}
|
2023-10-03 13:49:49 +00:00
|
|
|
}()
|
2023-10-03 15:11:46 +00:00
|
|
|
|
|
|
|
// timed update
|
|
|
|
|
|
|
|
go func() {
|
2023-10-07 13:51:03 +00:00
|
|
|
first_time := true
|
2023-10-03 15:11:46 +00:00
|
|
|
for {
|
2023-10-07 13:51:03 +00:00
|
|
|
err := routineCheck(5*time.Minute, first_time)
|
|
|
|
first_time = false
|
2023-10-07 08:24:49 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Printf("Error in routineCheck: %v", err)
|
|
|
|
// idea: maybe should delay here
|
|
|
|
}
|
2023-10-03 15:11:46 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2023-10-03 13:49:49 +00:00
|
|
|
// serve http
|
|
|
|
|
|
|
|
err = srv.Serve(ln)
|
|
|
|
if err != http.ErrServerClosed {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
2023-09-30 15:35:41 +00:00
|
|
|
}
|
2023-10-03 18:25:50 +00:00
|
|
|
|
2023-10-07 13:51:03 +00:00
|
|
|
func routineCheck(timeout time.Duration, onlyShowDown bool) error {
|
2023-10-03 18:25:50 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
|
|
defer cancel()
|
2023-10-07 08:24:49 +00:00
|
|
|
|
2023-10-03 18:25:50 +00:00
|
|
|
data := scanner.CheckServiceBatch(ctx, config.Service)
|
2023-10-07 08:24:49 +00:00
|
|
|
non_duplicates, err := dbm.StoreServiceStatusBatch(data, time.Now())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-07 13:51:03 +00:00
|
|
|
if onlyShowDown {
|
|
|
|
tmp := non_duplicates
|
|
|
|
non_duplicates = []scanner.ServiceStatus{}
|
|
|
|
for _, v := range tmp {
|
|
|
|
if !v.Ok {
|
|
|
|
non_duplicates = append(non_duplicates, v)
|
|
|
|
}
|
2023-10-07 08:28:25 +00:00
|
|
|
}
|
2023-10-07 08:24:49 +00:00
|
|
|
}
|
2023-10-07 13:51:03 +00:00
|
|
|
err = notify(non_duplicates)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-03 18:25:50 +00:00
|
|
|
// log.Printf("Routine check: %v", data)
|
|
|
|
<-ctx.Done()
|
2023-10-07 08:24:49 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-10-20 08:25:18 +00:00
|
|
|
func formatPlainText(updates []scanner.ServiceStatus) string {
|
|
|
|
result := ""
|
2023-10-07 08:24:49 +00:00
|
|
|
for _, v := range updates {
|
2023-10-07 08:50:48 +00:00
|
|
|
var upOrDown string
|
2023-10-07 08:24:49 +00:00
|
|
|
if v.Ok {
|
2023-10-07 08:50:48 +00:00
|
|
|
upOrDown = "UP"
|
2023-10-07 08:24:49 +00:00
|
|
|
} else {
|
2023-10-07 08:50:48 +00:00
|
|
|
upOrDown = "DOWN"
|
2023-10-07 08:24:49 +00:00
|
|
|
}
|
2023-10-07 08:50:48 +00:00
|
|
|
serviceType := fmt.Sprintf("[%s]", v.Type)
|
2023-10-20 08:25:18 +00:00
|
|
|
result += fmt.Sprintf("%-4s | %-24s %-7s %s\n", upOrDown, v.Name, serviceType, v.Status)
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func notify(updates []scanner.ServiceStatus) error {
|
|
|
|
if len(updates) == 0 {
|
|
|
|
return nil
|
2023-10-07 08:24:49 +00:00
|
|
|
}
|
2023-10-20 08:25:18 +00:00
|
|
|
messageBody := formatPlainText(updates)
|
2023-10-07 08:58:43 +00:00
|
|
|
_, err := client.SendNoticeHTML(
|
|
|
|
"```\n"+messageBody+"```",
|
2023-10-28 11:34:03 +00:00
|
|
|
"<pre><code>"+strings.TrimRight(messageBody, "\n")+"</code></pre>", &updates)
|
2023-10-07 08:50:48 +00:00
|
|
|
log.Printf("notify: err=%v\n%v", err, updates)
|
|
|
|
return err
|
2023-10-03 18:25:50 +00:00
|
|
|
}
|