From 25a6cc2a1a47ca536470a2d0d9e8e80b6d9750ab Mon Sep 17 00:00:00 2001 From: cynfo3000 Date: Tue, 3 Mar 2026 23:37:31 +0100 Subject: [PATCH] Installer-Paket System: Backend speichert/served ZIP, Frontend Upload+Download, install.sh auto-download von Backend statt Git --- backend/api/handlers.go | 5 ++ backend/api/installer.go | 106 +++++++++++++++++++++++ backend/db/postgres.go | 62 +++++++++++++ frontend/src/api/client.js | 27 ++++++ frontend/src/components/InstallGuide.jsx | 5 +- frontend/src/pages/Firmware.jsx | 99 ++++++++++++++++++++- 6 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 backend/api/installer.go diff --git a/backend/api/handlers.go b/backend/api/handlers.go index 2b510e0..4ed023e 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -68,6 +68,11 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) { mux.HandleFunc("DELETE /api/v1/agents/{id}/request-update", h.cancelUpdate) mux.HandleFunc("POST /api/v1/agents/request-update-all", h.requestUpdateAll) + // Installer Routes + mux.HandleFunc("GET /api/v1/installer", h.getInstallerInfo) + mux.HandleFunc("POST /api/v1/installer/upload", h.uploadInstaller) + mux.HandleFunc("GET /api/v1/installer/download", h.downloadInstaller) + // WebSocket und Tunnel-Routes h.setupTunnelRoutes(mux, hub) } diff --git a/backend/api/installer.go b/backend/api/installer.go new file mode 100644 index 0000000..fac9aaf --- /dev/null +++ b/backend/api/installer.go @@ -0,0 +1,106 @@ +package api + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "log" + "net/http" +) + +// POST /api/v1/installer/upload?platform=freebsd — Installer-Paket hochladen (ZIP oder Script) +func (h *Handler) uploadInstaller(w http.ResponseWriter, r *http.Request) { + platform := r.URL.Query().Get("platform") + if platform == "" { + platform = "freebsd" + } + validPlatforms := map[string]bool{"freebsd": true, "linux": true} + if !validPlatforms[platform] { + writeError(w, http.StatusBadRequest, "Ungueltige Plattform (freebsd, linux)") + return + } + + // Max 50MB + r.Body = http.MaxBytesReader(w, r.Body, 50*1024*1024) + + // Multipart oder raw body + var data []byte + var filename string + var err error + + if ct := r.Header.Get("Content-Type"); ct != "" && len(ct) > 19 && ct[:19] == "multipart/form-data" { + file, header, ferr := r.FormFile("file") + if ferr != nil { + writeError(w, http.StatusBadRequest, "Datei fehlt im Upload") + return + } + defer file.Close() + data, err = io.ReadAll(file) + filename = header.Filename + } else { + data, err = io.ReadAll(r.Body) + filename = r.URL.Query().Get("filename") + if filename == "" { + filename = fmt.Sprintf("installer-%s.zip", platform) + } + } + + if err != nil || len(data) == 0 { + writeError(w, http.StatusBadRequest, "Leere oder fehlerhafte Datei") + return + } + + hash := sha256.Sum256(data) + hashStr := hex.EncodeToString(hash[:]) + + if err := h.db.SaveInstaller(platform, filename, hashStr, data); err != nil { + log.Printf("Installer speichern fehlgeschlagen: %v", err) + writeError(w, http.StatusInternalServerError, "Speichern fehlgeschlagen") + return + } + + log.Printf("Installer fuer %s hochgeladen: %s (%d bytes, SHA256: %s)", platform, filename, len(data), hashStr[:16]) + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "platform": platform, + "filename": filename, + "sha256": hashStr, + "size": len(data), + "message": "Installer hochgeladen", + }) +} + +// GET /api/v1/installer — Installer-Info (alle Plattformen) +func (h *Handler) getInstallerInfo(w http.ResponseWriter, r *http.Request) { + all, err := h.db.GetAllInstallerInfo() + if err != nil || len(all) == 0 { + writeJSON(w, http.StatusOK, map[string]interface{}{ + "installers": []interface{}{}, + }) + return + } + writeJSON(w, http.StatusOK, map[string]interface{}{ + "installers": all, + }) +} + +// GET /api/v1/installer/download?platform=freebsd — Installer herunterladen +func (h *Handler) downloadInstaller(w http.ResponseWriter, r *http.Request) { + platform := r.URL.Query().Get("platform") + if platform == "" { + platform = "freebsd" + } + + filename, hashStr, data, err := h.db.GetInstaller(platform) + if err != nil { + writeError(w, http.StatusNotFound, fmt.Sprintf("Kein Installer fuer Plattform '%s' vorhanden", platform)) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + w.Header().Set("X-Installer-SHA256", hashStr) + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data))) + w.Write(data) +} diff --git a/backend/db/postgres.go b/backend/db/postgres.go index 4439356..967a510 100644 --- a/backend/db/postgres.go +++ b/backend/db/postgres.go @@ -191,6 +191,17 @@ func (d *Database) migrate() error { last_used TIMESTAMPTZ )`) + // Installer-Pakete (install.sh + Plugin als ZIP pro Plattform) + d.db.Exec(`CREATE TABLE IF NOT EXISTS installers ( + id SERIAL PRIMARY KEY, + platform TEXT NOT NULL UNIQUE, + filename TEXT NOT NULL, + data BYTEA NOT NULL, + sha256 TEXT NOT NULL, + size INTEGER NOT NULL, + uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + )`) + // Retention Policy (90 Tage) d.setupRetention() @@ -604,6 +615,57 @@ func (d *Database) GetAllFirmwareInfo() ([]map[string]interface{}, error) { return results, nil } +// --- Installer --- + +func (d *Database) SaveInstaller(platform, filename, sha256hash string, data []byte) error { + _, err := d.db.Exec(` + INSERT INTO installers (platform, filename, data, sha256, size, uploaded_at) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (platform) DO UPDATE SET filename=$2, data=$3, sha256=$4, size=$5, uploaded_at=NOW() + `, platform, filename, data, sha256hash, len(data)) + return err +} + +func (d *Database) GetInstaller(platform string) (string, string, []byte, error) { + var filename, sha256hash string + var data []byte + err := d.db.QueryRow("SELECT filename, sha256, data FROM installers WHERE platform = $1", platform).Scan(&filename, &sha256hash, &data) + return filename, sha256hash, data, err +} + +func (d *Database) GetInstallerInfo(platform string) (string, string, int, string, error) { + var filename, sha256hash string + var size int + var uploadedAt time.Time + err := d.db.QueryRow("SELECT filename, sha256, size, uploaded_at FROM installers WHERE platform = $1", platform).Scan(&filename, &sha256hash, &size, &uploadedAt) + return filename, sha256hash, size, uploadedAt.Format(time.RFC3339), err +} + +func (d *Database) GetAllInstallerInfo() ([]map[string]interface{}, error) { + rows, err := d.db.Query("SELECT platform, filename, sha256, size, uploaded_at FROM installers ORDER BY platform") + if err != nil { + return nil, err + } + defer rows.Close() + var results []map[string]interface{} + for rows.Next() { + var platform, filename, sha256hash string + var size int + var uploadedAt time.Time + if err := rows.Scan(&platform, &filename, &sha256hash, &size, &uploadedAt); err != nil { + return nil, err + } + results = append(results, map[string]interface{}{ + "platform": platform, + "filename": filename, + "sha256": sha256hash, + "size": size, + "uploaded_at": uploadedAt, + }) + } + return results, nil +} + func (d *Database) DeleteAgent(id string) (bool, error) { res, err := d.db.Exec("DELETE FROM agents WHERE id = $1", id) if err != nil { diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 81e9147..e8fa183 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -272,6 +272,33 @@ class ApiClient { requestUpdateAll() { return this.post('/api/v1/agents/request-update-all') } + + // Installer + getInstallerInfo() { + return this.get('/api/v1/installer') + } + + async uploadInstaller(platform, file) { + const headers = {} + if (this.token) headers['Authorization'] = `Bearer ${this.token}` + const formData = new FormData() + formData.append('file', file) + const res = await fetch(`${API_BASE}/api/v1/installer/upload?platform=${encodeURIComponent(platform)}`, { + method: 'POST', + headers, + body: formData, + }) + if (res.status === 401) { + this.setToken(null) + window.location.href = '/login' + throw new Error('Unauthorized') + } + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })) + throw new Error(err.error || 'Upload fehlgeschlagen') + } + return res.json() + } } export const api = new ApiClient() diff --git a/frontend/src/components/InstallGuide.jsx b/frontend/src/components/InstallGuide.jsx index 65a9b1b..2b58124 100644 --- a/frontend/src/components/InstallGuide.jsx +++ b/frontend/src/components/InstallGuide.jsx @@ -3,7 +3,6 @@ import { Copy, Check, Terminal, Monitor } from 'lucide-react' const BACKEND_URL = 'https://dsbmueller.spdns.org:8443' const API_KEY = '01532e5a7c9e70bf2757df77a2f5b9b9' -const INSTALL_SH_URL = 'https://git.cynfo.net/christian/rmm/raw/branch/main/opnsense-plugin/install.sh' export default function InstallGuide({ agentName, platform: initialPlatform }) { const [platform, setPlatform] = useState(initialPlatform || 'freebsd') @@ -49,7 +48,9 @@ export default function InstallGuide({ agentName, platform: initialPlatform }) { ) - const opnInstallCmd = `fetch -o /tmp/install.sh "${INSTALL_SH_URL}" && sh /tmp/install.sh` + const opnInstallCmd = `fetch --no-verify-peer --no-verify-hostname -o /tmp/installer.zip \\ + "${BACKEND_URL}/api/v1/installer/download?platform=freebsd&api_key=${API_KEY}" \\ + && cd /tmp && unzip -o installer.zip && sh /tmp/install.sh` const linuxInstallCmd = `#!/bin/sh # RMM Agent + Updater Installation diff --git a/frontend/src/pages/Firmware.jsx b/frontend/src/pages/Firmware.jsx index af5b83a..3daa37f 100644 --- a/frontend/src/pages/Firmware.jsx +++ b/frontend/src/pages/Firmware.jsx @@ -1,6 +1,6 @@ import { useEffect, useState, useRef } from 'react' import api from '../api/client' -import { Upload, RefreshCw, CheckCircle, AlertTriangle, ArrowUpCircle, X, Monitor, Server, Cpu } from 'lucide-react' +import { Upload, RefreshCw, CheckCircle, AlertTriangle, ArrowUpCircle, X, Monitor, Server, Cpu, Package, Copy, Check } from 'lucide-react' const PLATFORMS = [ { id: 'freebsd', label: 'FreeBSD / OPNsense', icon: Server }, @@ -8,25 +8,34 @@ const PLATFORMS = [ { id: 'windows', label: 'Windows', icon: Cpu }, ] +const BACKEND_URL = 'https://dsbmueller.spdns.org:8443' + export default function Firmware() { const [firmwareData, setFirmwareData] = useState(null) + const [installerData, setInstallerData] = useState(null) const [agents, setAgents] = useState([]) const [loading, setLoading] = useState(true) const [uploading, setUploading] = useState(false) + const [installerUploading, setInstallerUploading] = useState(false) const [version, setVersion] = useState('') const [platform, setPlatform] = useState('freebsd') + const [installerPlatform, setInstallerPlatform] = useState('freebsd') const [msg, setMsg] = useState('') const [msgType, setMsgType] = useState('info') const fileRef = useRef(null) + const installerFileRef = useRef(null) + const [copied, setCopied] = useState(false) const load = async () => { try { - const [fw, ag] = await Promise.all([ + const [fw, ag, inst] = await Promise.all([ api.getFirmwareInfo().catch(() => null), api.getAgents().catch(() => []), + api.getInstallerInfo().catch(() => null), ]) setFirmwareData(fw) setAgents(Array.isArray(ag) ? ag : []) + setInstallerData(inst) } finally { setLoading(false) } @@ -202,6 +211,92 @@ export default function Firmware() { + {/* Installer Packages */} +
+

Installer-Pakete

+

+ ZIP-Pakete mit install.sh und Plugin-Dateien. Werden auf neuen Geraeten per fetch heruntergeladen und ausgefuehrt. +

+ + {/* Existing installers */} + {installerData?.installers?.length > 0 && ( +
+ {installerData.installers.map(inst => ( +
+
+
+ + {inst.platform === 'linux' ? 'Linux' : 'OPNsense / FreeBSD'} +
+
+ {inst.filename} — {formatSize(inst.size)} — {new Date(inst.uploaded_at).toLocaleString('de-DE')} +
+
{inst.sha256?.substring(0, 32)}...
+
+
+ ))} +
+ )} + + {/* Install command */} + {installerData?.installers?.some(i => i.platform === 'freebsd') && ( +
+
Installationsbefehl (OPNsense):
+
{`fetch --no-verify-peer --no-verify-hostname -o /tmp/installer.zip "${BACKEND_URL}/api/v1/installer/download?platform=freebsd&api_key=01532e5a7c9e70bf2757df77a2f5b9b9" && cd /tmp && unzip -o installer.zip && sh /tmp/install.sh`}
+ +
+ )} + + {/* Upload */} +
+ + + +
+
+ {/* Agent List */}