generated from sendnrw/template_golang
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39c3e1f695 | |||
| 99e55a2e87 | |||
| a7bb94ecab | |||
| f4ed36121e |
@@ -20,6 +20,8 @@
|
|||||||
- sc.exe description GoSysInfoService "Hilden PC-Info"
|
- sc.exe description GoSysInfoService "Hilden PC-Info"
|
||||||
- sc.exe start GoSysInfoService
|
- sc.exe start GoSysInfoService
|
||||||
|
|
||||||
|
- http://localhost:24000/api/devices?only_usb=1
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|||||||
363
main.go
363
main.go
@@ -14,8 +14,10 @@ import (
|
|||||||
"os/user"
|
"os/user"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
@@ -81,11 +83,261 @@ type InstalledApp struct {
|
|||||||
Source string `json:"source"` // HKLM-64, HKLM-32, HKCU
|
Source string `json:"source"` // HKLM-64, HKLM-32, HKCU
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeviceInfo struct {
|
||||||
|
InstanceID string `json:"instance_id"`
|
||||||
|
Class string `json:"class"`
|
||||||
|
ClassGUID string `json:"class_guid"`
|
||||||
|
FriendlyName string `json:"friendly_name"`
|
||||||
|
Manufacturer string `json:"manufacturer"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Present bool `json:"present"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
TargetUser string `json:"target_user"` // "" = Broadcast an alle User
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
notifyMu sync.Mutex
|
||||||
|
notifySeq int64
|
||||||
|
notifyRing []Notification
|
||||||
|
)
|
||||||
|
|
||||||
|
// maximale Anzahl vorgehaltener Nachrichten (FIFO-Ring)
|
||||||
|
const maxNotifications = 1000
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
lastCPUUsage float64
|
lastCPUUsage float64
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func addNotification(n Notification) Notification {
|
||||||
|
notifyMu.Lock()
|
||||||
|
defer notifyMu.Unlock()
|
||||||
|
|
||||||
|
notifySeq++
|
||||||
|
n.ID = notifySeq
|
||||||
|
if n.CreatedAt.IsZero() {
|
||||||
|
n.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyRing = append(notifyRing, n)
|
||||||
|
if len(notifyRing) > maxNotifications {
|
||||||
|
// älteste abschneiden
|
||||||
|
notifyRing = notifyRing[len(notifyRing)-maxNotifications:]
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// userName: z.B. "DOMAIN\\user" oder "user"
|
||||||
|
// sinceID: letzte gesehene ID des Agents
|
||||||
|
func getNotificationsForUser(userName string, sinceID int64) []Notification {
|
||||||
|
notifyMu.Lock()
|
||||||
|
defer notifyMu.Unlock()
|
||||||
|
|
||||||
|
userNameLower := strings.ToLower(strings.TrimSpace(userName))
|
||||||
|
var res []Notification
|
||||||
|
for _, n := range notifyRing {
|
||||||
|
if n.ID <= sinceID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Broadcast
|
||||||
|
if strings.TrimSpace(n.TargetUser) == "" {
|
||||||
|
res = append(res, n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Ziel-User matchen (case-insensitive)
|
||||||
|
if strings.EqualFold(n.TargetUser, userName) ||
|
||||||
|
strings.EqualFold(n.TargetUser, userNameLower) {
|
||||||
|
res = append(res, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotifyRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
TargetUser string `json:"target_user"` // optional, "" = an alle
|
||||||
|
}
|
||||||
|
|
||||||
|
func notifyFromServerHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional: Authentifizierung per Token
|
||||||
|
token := os.Getenv("NOTIFY_TOKEN")
|
||||||
|
if token != "" && r.Header.Get("X-Notify-Token") != token {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req NotifyRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Title = strings.TrimSpace(req.Title)
|
||||||
|
req.Message = strings.TrimSpace(req.Message)
|
||||||
|
req.TargetUser = strings.TrimSpace(req.TargetUser)
|
||||||
|
|
||||||
|
if req.Message == "" {
|
||||||
|
http.Error(w, "message must not be empty", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Title == "" {
|
||||||
|
req.Title = "Benachrichtigung"
|
||||||
|
}
|
||||||
|
|
||||||
|
n := addNotification(Notification{
|
||||||
|
Title: req.Title,
|
||||||
|
Message: req.Message,
|
||||||
|
TargetUser: req.TargetUser,
|
||||||
|
})
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func notificationsForAgentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userName := strings.TrimSpace(r.URL.Query().Get("user"))
|
||||||
|
if userName == "" {
|
||||||
|
http.Error(w, "missing 'user' query parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sinceStr := r.URL.Query().Get("since_id")
|
||||||
|
var sinceID int64
|
||||||
|
if sinceStr != "" {
|
||||||
|
if v, err := strconv.ParseInt(sinceStr, 10, 64); err == nil {
|
||||||
|
sinceID = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional: Zugriff auf localhost beschränken
|
||||||
|
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if host != "127.0.0.1" && host != "::1" {
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs := getNotificationsForUser(userName, sinceID)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(notifs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPnpDevices() ([]DeviceInfo, error) {
|
||||||
|
// PowerShell: aktuelle PnP-Geräte, inkl. ClassGuid
|
||||||
|
cmd := exec.Command(
|
||||||
|
"powershell",
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-Command",
|
||||||
|
`Get-PnpDevice -PresentOnly | Select-Object InstanceId,Class,ClassGuid,FriendlyName,Manufacturer,Status,Present | ConvertTo-Json -Depth 3`,
|
||||||
|
)
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("getPnpDevices: powershell error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return []DeviceInfo{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PS gibt entweder ein Objekt oder ein Array zurück → wie bei getWinNetProfiles behandeln
|
||||||
|
type psDevice struct {
|
||||||
|
InstanceId string `json:"InstanceId"`
|
||||||
|
Class string `json:"Class"`
|
||||||
|
ClassGuid string `json:"ClassGuid"`
|
||||||
|
FriendlyName string `json:"FriendlyName"`
|
||||||
|
Manufacturer string `json:"Manufacturer"`
|
||||||
|
Status string `json:"Status"`
|
||||||
|
Present interface{} `json:"Present"`
|
||||||
|
}
|
||||||
|
|
||||||
|
parsePresent := func(v interface{}) bool {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return t
|
||||||
|
case string:
|
||||||
|
// "True"/"False" etc.
|
||||||
|
return strings.EqualFold(t, "true")
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var arr []psDevice
|
||||||
|
if err := json.Unmarshal(out, &arr); err == nil {
|
||||||
|
res := make([]DeviceInfo, 0, len(arr))
|
||||||
|
for _, d := range arr {
|
||||||
|
res = append(res, DeviceInfo{
|
||||||
|
InstanceID: d.InstanceId,
|
||||||
|
Class: d.Class,
|
||||||
|
ClassGUID: d.ClassGuid,
|
||||||
|
FriendlyName: d.FriendlyName,
|
||||||
|
Manufacturer: d.Manufacturer,
|
||||||
|
Status: d.Status,
|
||||||
|
Present: parsePresent(d.Present),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ein einzelnes Objekt
|
||||||
|
var single psDevice
|
||||||
|
if err := json.Unmarshal(out, &single); err != nil {
|
||||||
|
log.Printf("getPnpDevices: cannot unmarshal: %v -- out: %s", err, string(out))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []DeviceInfo{
|
||||||
|
{
|
||||||
|
InstanceID: single.InstanceId,
|
||||||
|
Class: single.Class,
|
||||||
|
ClassGUID: single.ClassGuid,
|
||||||
|
FriendlyName: single.FriendlyName,
|
||||||
|
Manufacturer: single.Manufacturer,
|
||||||
|
Status: single.Status,
|
||||||
|
Present: parsePresent(single.Present),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func devicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
devs, err := getPnpDevices()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to enumerate devices: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Filter nur USB-Geräte (InstanceID beginnt mit "USB\")
|
||||||
|
if r.URL.Query().Get("only_usb") == "1" {
|
||||||
|
filtered := make([]DeviceInfo, 0, len(devs))
|
||||||
|
for _, d := range devs {
|
||||||
|
if strings.HasPrefix(strings.ToUpper(d.InstanceID), "USB\\") {
|
||||||
|
filtered = append(filtered, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
devs = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
_ = enc.Encode(devs)
|
||||||
|
}
|
||||||
|
|
||||||
func readUninstallKey(base registry.Key, path, src string) []InstalledApp {
|
func readUninstallKey(base registry.Key, path, src string) []InstalledApp {
|
||||||
k, err := registry.OpenKey(base, path, registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
|
k, err := registry.OpenKey(base, path, registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -459,13 +711,24 @@ var page = template.Must(template.New("index").Parse(`<!doctype html>
|
|||||||
<h2><div class="v stereo" id="hostname2"></div></h2>
|
<h2><div class="v stereo" id="hostname2"></div></h2>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="k">Host</div>
|
<div class="k">Host</div>
|
||||||
<div class="row"><div>Hostname</div><div class="v stereo" id="hostname"></div></div>
|
<div class="row"><div>Hostname</div><div class="v stereo" id="hostname"></div></div>
|
||||||
<div class="row"><div>User</div><div class="v mono" id="username"></div></div>
|
<div class="row"><div>User</div><div class="v mono" id="username"></div></div>
|
||||||
<div class="row"><div>Uptime</div><div class="v" id="uptime"></div></div>
|
<div class="row"><div>Uptime</div><div class="v" id="uptime"></div></div>
|
||||||
<div class="row"><div>Boot</div><div class="v" id="boottime"></div></div>
|
<div class="row"><div>Boot</div><div class="v" id="boottime"></div></div>
|
||||||
<div class="row"><div>Arch</div><div class="v mono" id="arch"></div></div>
|
<div class="row"><div>Arch</div><div class="v mono" id="arch"></div></div>
|
||||||
</div>
|
|
||||||
|
<div class="row" style="margin-top:8px;">
|
||||||
|
<div>Aktionen</div>
|
||||||
|
<div>
|
||||||
|
<button id="btnGpupdate" class="pill2" style="cursor:pointer;">gpupdate /force</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div></div>
|
||||||
|
<div class="mono" id="gpupdateResult" style="font-size:12px;color:var(--muted);white-space:pre-wrap;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="k">OS</div>
|
<div class="k">OS</div>
|
||||||
@@ -634,6 +897,29 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
renderApps(filtered);
|
renderApps(filtered);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const btnGp = document.getElementById('btnGpupdate');
|
||||||
|
const resGp = document.getElementById('gpupdateResult');
|
||||||
|
if (btnGp && resGp) {
|
||||||
|
btnGp.addEventListener('click', async ()=>{
|
||||||
|
resGp.textContent = 'gpupdate /force wird gestartet …';
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/gpupdate', { method: 'POST' });
|
||||||
|
if (!r.ok) {
|
||||||
|
resGp.textContent = 'Fehler: HTTP ' + r.status + ' ' + r.statusText;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.ok) {
|
||||||
|
resGp.textContent = 'gpupdate /force ausgeführt (ExitCode ' + (j.exit_code ?? 0) + ').';
|
||||||
|
} else {
|
||||||
|
resGp.textContent = 'Fehler bei gpupdate (ExitCode ' + (j.exit_code ?? -1) + '): ' + (j.error || '');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resGp.textContent = 'Request-Fehler: ' + e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
@@ -667,6 +953,11 @@ func runHTTP(ctx context.Context) error {
|
|||||||
mux.HandleFunc("/", indexHandler)
|
mux.HandleFunc("/", indexHandler)
|
||||||
mux.HandleFunc("/api/apps", appsHandler)
|
mux.HandleFunc("/api/apps", appsHandler)
|
||||||
mux.HandleFunc("/api/summary", apiHandler)
|
mux.HandleFunc("/api/summary", apiHandler)
|
||||||
|
mux.HandleFunc("/api/devices", devicesHandler)
|
||||||
|
mux.HandleFunc("/api/gpupdate", gpupdateHandler)
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/notify", notifyFromServerHandler)
|
||||||
|
mux.HandleFunc("/api/notifications", notificationsForAgentHandler)
|
||||||
|
|
||||||
addr := ":24000"
|
addr := ":24000"
|
||||||
|
|
||||||
@@ -824,3 +1115,61 @@ func getWinNetProfiles() (map[int]string, error) {
|
|||||||
res[single.InterfaceIndex] = normalizeCat(single.NetworkCategory)
|
res[single.InterfaceIndex] = normalizeCat(single.NetworkCategory)
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GPUpdateResponse struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
ExitCode int `json:"exit_code"`
|
||||||
|
Stdout string `json:"stdout"`
|
||||||
|
Stderr string `json:"stderr"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func gpupdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// nur lokale Aufrufe zulassen
|
||||||
|
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if host != "127.0.0.1" && host != "::1" {
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "cmd", "/C", "gpupdate", "/force")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
resp := GPUpdateResponse{
|
||||||
|
Stdout: string(out),
|
||||||
|
Stderr: "",
|
||||||
|
OK: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout?
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
resp.OK = false
|
||||||
|
resp.Error = "gpupdate /force: Timeout (2 Minuten)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exitcode auswerten
|
||||||
|
if err != nil {
|
||||||
|
resp.OK = false
|
||||||
|
resp.Error = err.Error()
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
if status, ok2 := exitErr.Sys().(syscall.WaitStatus); ok2 {
|
||||||
|
resp.ExitCode = status.ExitStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if cmd.ProcessState != nil {
|
||||||
|
if status, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
|
||||||
|
resp.ExitCode = status.ExitStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user