Files
pcinfosender/main.go
2025-12-18 07:15:24 +01:00

351 lines
8.2 KiB
Go

//go:build windows
package main
import (
"bytes"
"encoding/json"
"html/template"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type NotifyRequest struct {
Title string `json:"title"`
Message string `json:"message"`
TargetUser string `json:"target_user"`
}
type PageData struct {
ClientHost string
Port string
Title string
Message string
TargetUser string
Status string
Error string
}
var (
tpl *template.Template
notifyToken string
defaultPort = "24000"
listenAddr = ":8088"
defaultTitle = "Benachrichtigung"
)
func main() {
notifyToken = os.Getenv("NOTIFY_TOKEN")
if v := os.Getenv("SENDER_LISTEN_ADDR"); v != "" {
listenAddr = v
}
if v := os.Getenv("DEFAULT_PORT"); v != "" {
defaultPort = v
}
var err error
tpl, err = template.New("index").Parse(pageHTML)
if err != nil {
log.Fatalf("template parse error: %v", err)
}
http.HandleFunc("/", indexHandler)
log.Printf("Notification-Sender Web UI läuft auf http://localhost%s/", listenAddr)
log.Fatalf("ListenAndServe: %v", http.ListenAndServe(listenAddr, nil))
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
render(w, PageData{
Port: defaultPort,
})
case http.MethodPost:
if err := r.ParseForm(); err != nil {
render(w, PageData{
Error: "Formular konnte nicht gelesen werden: " + err.Error(),
Port: defaultPort,
})
return
}
data := PageData{
ClientHost: strings.TrimSpace(r.FormValue("client_host")),
Port: strings.TrimSpace(r.FormValue("port")),
Title: strings.TrimSpace(r.FormValue("title")),
Message: strings.TrimSpace(r.FormValue("message")),
TargetUser: strings.TrimSpace(r.FormValue("target_user")),
}
if data.Port == "" {
data.Port = defaultPort
}
if data.ClientHost == "" {
data.Error = "Client-Host/IP darf nicht leer sein."
render(w, data)
return
}
if data.Message == "" {
data.Error = "Nachricht darf nicht leer sein."
render(w, data)
return
}
if data.Title == "" {
data.Title = defaultTitle
}
if err := sendNotification(data); err != nil {
data.Error = "Fehler beim Senden: " + err.Error()
} else {
data.Status = "Benachrichtigung wurde gesendet."
}
render(w, data)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func render(w http.ResponseWriter, data PageData) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tpl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func sendNotification(d PageData) error {
// Port validieren (nur zur Sicherheit, falls jemand Unsinn einträgt)
if _, err := strconv.Atoi(d.Port); err != nil {
return err
}
// URL bauen:
// Wenn ClientHost schon ein Schema enthält (http://...), nutzen wir das,
// sonst "http://<host>:<port>/api/notify"
var url string
if strings.Contains(d.ClientHost, "://") {
url = strings.TrimRight(d.ClientHost, "/") + "/api/notify"
} else {
url = "http://" + d.ClientHost + ":" + d.Port + "/api/notify"
}
bodyStruct := NotifyRequest{
Title: d.Title,
Message: d.Message,
TargetUser: d.TargetUser,
}
body, err := json.Marshal(bodyStruct)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if notifyToken != "" {
req.Header.Set("X-Notify-Token", notifyToken)
}
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return &httpError{Status: resp.Status}
}
return nil
}
type httpError struct {
Status string
}
func (e *httpError) Error() string {
return "HTTP-Status vom Client: " + e.Status
}
const pageHTML = `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Notification Sender</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root {
--bg: #0f172a;
--card: #111827;
--border: #1f2937;
--fg: #e5e7eb;
--muted: #9ca3af;
--accent: #3b82f6;
--error: #f87171;
--success: #4ade80;
}
*{box-sizing:border-box}
body{
margin:0;
padding:24px;
font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif;
background: radial-gradient(circle at top, #1f2937 0, #020617 55%);
color:var(--fg);
display:flex;
justify-content:center;
}
.shell{
width:100%;
max-width: 640px;
}
h1{
margin:0 0 8px 0;
font-size:22px;
letter-spacing:0.03em;
}
p.subtitle{
margin:0 0 16px 0;
color:var(--muted);
font-size:14px;
}
.card{
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.9));
border-radius:18px;
border:1px solid var(--border);
padding:20px 18px 18px 18px;
box-shadow: 0 20px 45px rgba(0,0,0,0.55);
}
label{
display:block;
font-size:13px;
text-transform:uppercase;
letter-spacing:.08em;
color:var(--muted);
margin-bottom:4px;
}
input[type=text], textarea{
width:100%;
background:#020617;
border-radius:10px;
border:1px solid #1f2937;
padding:7px 9px;
color: var(--fg);
font-size:14px;
outline:none;
}
input[type=text]:focus, textarea:focus{
border-color: var(--accent);
box-shadow:0 0 0 1px rgba(59,130,246,0.35);
}
textarea{
min-height:80px;
resize:vertical;
}
.row{
display:flex;
gap:10px;
}
.field{
margin-bottom:12px;
flex:1;
}
button{
margin-top:6px;
border:none;
padding:9px 16px;
border-radius:999px;
background: radial-gradient(circle at top left, #60a5fa, #2563eb);
color:white;
font-weight:600;
font-size:14px;
cursor:pointer;
display:inline-flex;
align-items:center;
gap:6px;
}
button:hover{
filter:brightness(1.05);
}
.status{
margin-top:10px;
font-size:13px;
}
.status.ok{color:var(--success);}
.status.err{color:var(--error);}
.badge{
display:inline-block;
font-size:11px;
padding:2px 7px;
border-radius:999px;
background:#111827;
border:1px solid #1f2937;
color:var(--muted);
margin-left:6px;
}
</style>
</head>
<body>
<div class="shell">
<h1>Notification Sender</h1>
<p class="subtitle">Sende Benachrichtigungen an deine GoSysInfo-Clients (POST /api/notify).</p>
<div class="card">
<form method="POST" action="/">
<div class="row">
<div class="field">
<label for="client_host">Client Host / IP</label>
<input type="text" id="client_host" name="client_host" value="{{.ClientHost}}" placeholder="z.B. pc-123.stadt-hilden.de oder 10.10.10.5">
</div>
<div class="field" style="max-width:120px;">
<label for="port">Port</label>
<input type="text" id="port" name="port" value="{{.Port}}">
</div>
</div>
<div class="field">
<label for="title">Titel</label>
<input type="text" id="title" name="title" value="{{.Title}}" placeholder="optional, Standard: Benachrichtigung">
</div>
<div class="field">
<label for="message">Nachricht</label>
<textarea id="message" name="message" placeholder="Text für den Toast">{{.Message}}</textarea>
</div>
<div class="field">
<label for="target_user">Target User (optional)</label>
<input type="text" id="target_user" name="target_user" value="{{.TargetUser}}" placeholder="z.B. DOMAIN\user oder leer für alle">
</div>
<button type="submit">
<span>Notification senden</span>
<span>➜</span>
</button>
{{if .Status}}
<div class="status ok">{{.Status}}</div>
{{end}}
{{if .Error}}
<div class="status err">{{.Error}}</div>
{{end}}
</form>
<div style="margin-top:10px;font-size:11px;color:var(--muted);">
Hinweis: Dieses Tool sendet JSON an <code>http://&lt;Client&gt;:&lt;Port&gt;/api/notify</code> und nutzt optional den Header <code>X-Notify-Token</code>, wenn die Umgebungsvariable <code>NOTIFY_TOKEN</code> gesetzt ist.
<span class="badge">UI-Service</span>
</div>
</div>
</div>
</body>
</html>`