teg/backend/external/agent_main.go
2026-06-12 16:51:33 +02:00

967 lines
24 KiB
Go

// backend\external\agent_main.go
//
// Eigenständiger Ordner-Agent für den Milestone-/Recording-Server.
//
// Endpunkte:
// GET /health
// GET /server-folders?rootType=storage&path=D:\Milestone
// POST /server-folders
// PATCH /server-folders
// DELETE /server-folders
// PUT /agent-token (Token zur Laufzeit setzen / rotieren)
//
// Start per Parameter (ohne Token):
// server-folders-agent.exe -port 8099 -storage-root "Storage D=D:\Milestone\Storage" -archive-root "Archive E=E:\Milestone\Archive"
//
// Alternativ per ENV:
// LISTEN_ADDR=:8099
// SERVER_STORAGE_ROOTS=Storage D=D:\Milestone\Storage
// SERVER_ARCHIVE_ROOTS=Archive E=E:\Milestone\Archive
//
// Token-Handling:
// Der Agent wird ohne Token gestartet. Das Backend erzeugt beim Speichern der
// Milestone-Einstellungen automatisch einen Token und schiebt ihn per
// PUT /agent-token an den Agent. Der Token wird in einer Datei persistiert
// (Standard: neben der .exe, siehe -token-file / SERVER_FOLDER_AGENT_TOKEN_FILE)
// und überlebt damit einen Neustart.
//
// Solange noch kein Token gesetzt ist, ist PUT /agent-token offen (Bootstrap).
// Sobald ein Token gesetzt ist, muss der Client ihn senden:
// X-Server-Folders-Token: <token>
// oder:
// Authorization: Bearer <token>
//
// -token / SERVER_FOLDER_AGENT_TOKEN sind optional und dienen nur als
// Start-Seed, falls noch keine Token-Datei existiert.
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
const (
defaultListenAddr = ":8099"
defaultTokenFileName = "server-folders-agent.token"
listenAddrEnv = "LISTEN_ADDR"
serverStorageRootsEnv = "SERVER_STORAGE_ROOTS"
serverArchiveRootsEnv = "SERVER_ARCHIVE_ROOTS"
legacyFolderRootsEnv = "SERVER_FOLDER_ROOTS"
agentTokenEnv = "SERVER_FOLDER_AGENT_TOKEN"
agentTokenFileEnv = "SERVER_FOLDER_AGENT_TOKEN_FILE"
)
type serverFolderRootType string
const (
serverFolderRootTypeStorage serverFolderRootType = "storage"
serverFolderRootTypeArchive serverFolderRootType = "archive"
)
type appConfig struct {
ListenAddr string
StorageRoots string
ArchiveRoots string
Token string
TokenFile string
}
func (config appConfig) rootsForType(rootType serverFolderRootType) string {
if rootType == serverFolderRootTypeArchive {
return strings.TrimSpace(config.ArchiveRoots)
}
return strings.TrimSpace(config.StorageRoots)
}
type serverFolderRoot struct {
Label string `json:"label"`
Path string `json:"path"`
}
type serverFolderNode struct {
Name string `json:"name"`
Path string `json:"path"`
HasChildren bool `json:"hasChildren"`
}
type listServerFoldersResponse struct {
RootType string `json:"rootType"`
Path string `json:"path"`
ParentPath string `json:"parentPath,omitempty"`
Roots []serverFolderRoot `json:"roots"`
Folders []serverFolderNode `json:"folders"`
}
type createServerFolderRequest struct {
Path string `json:"path"`
ParentPath string `json:"parentPath"`
Name string `json:"name"`
}
type renameServerFolderRequest struct {
Path string `json:"path"`
Name string `json:"name"`
}
func main() {
config := readConfigFromFlagsAndEnv()
store := newTokenStore(config.TokenFile, config.Token)
mux := http.NewServeMux()
mux.HandleFunc("GET /health", handleHealth)
mux.HandleFunc("GET /server-folders", requireAgentToken(store, func(w http.ResponseWriter, r *http.Request) {
handleListServerFolders(w, r, config)
}))
mux.HandleFunc("POST /server-folders", requireAgentToken(store, func(w http.ResponseWriter, r *http.Request) {
handleCreateServerFolder(w, r, config)
}))
mux.HandleFunc("PATCH /server-folders", requireAgentToken(store, func(w http.ResponseWriter, r *http.Request) {
handleRenameServerFolder(w, r, config)
}))
mux.HandleFunc("DELETE /server-folders", requireAgentToken(store, func(w http.ResponseWriter, r *http.Request) {
handleDeleteServerFolder(w, r, config)
}))
mux.HandleFunc("PUT /agent-token", requireAgentToken(store, func(w http.ResponseWriter, r *http.Request) {
handleSetAgentToken(w, r, store)
}))
server := &http.Server{
Addr: config.ListenAddr,
Handler: logRequests(mux),
ReadHeaderTimeout: 10 * time.Second,
}
log.Printf("server-folders-agent listening on %s", config.ListenAddr)
log.Printf("storage roots: %q", config.StorageRoots)
log.Printf("archive roots: %q", config.ArchiveRoots)
log.Printf("token file: %s", config.TokenFile)
log.Printf("token enabled: %t", store.get() != "")
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("server failed: %v", err)
}
}
func readConfigFromFlagsAndEnv() appConfig {
addrFlag := flag.String("addr", "", "Listen address, e.g. :8099 or 0.0.0.0:8099")
portFlag := flag.String("port", "", "Listen port, e.g. 8099")
legacyRootsFlag := flag.String("roots", "", "Legacy fallback roots for both storage and archive, e.g. Milestone D=D:\\Milestone")
storageRootsFlag := flag.String("storage-root", "", "Allowed storage roots, e.g. Storage D=D:\\Milestone\\Storage;Storage E=E:\\Milestone\\Storage")
archiveRootsFlag := flag.String("archive-root", "", "Allowed archive roots, e.g. Archive D=D:\\Milestone\\Archive;Archive E=E:\\Milestone\\Archive")
tokenFlag := flag.String("token", "", "Optional start seed token (used only if no token file exists yet)")
tokenFileFlag := flag.String("token-file", "", "Path to persist the runtime token (default: next to the executable)")
flag.Parse()
listenAddr := strings.TrimSpace(*addrFlag)
if listenAddr == "" {
listenAddr = strings.TrimSpace(os.Getenv(listenAddrEnv))
}
port := strings.TrimSpace(*portFlag)
if listenAddr == "" && port != "" {
port = strings.TrimPrefix(port, ":")
listenAddr = ":" + port
}
if listenAddr == "" {
listenAddr = defaultListenAddr
}
legacyRoots := strings.TrimSpace(*legacyRootsFlag)
if legacyRoots == "" {
legacyRoots = strings.TrimSpace(os.Getenv(legacyFolderRootsEnv))
}
storageRoots := strings.TrimSpace(*storageRootsFlag)
if storageRoots == "" {
storageRoots = strings.TrimSpace(os.Getenv(serverStorageRootsEnv))
}
if storageRoots == "" {
storageRoots = legacyRoots
}
archiveRoots := strings.TrimSpace(*archiveRootsFlag)
if archiveRoots == "" {
archiveRoots = strings.TrimSpace(os.Getenv(serverArchiveRootsEnv))
}
if archiveRoots == "" {
archiveRoots = legacyRoots
}
token := strings.TrimSpace(*tokenFlag)
if token == "" {
token = strings.TrimSpace(os.Getenv(agentTokenEnv))
}
tokenFile := strings.TrimSpace(*tokenFileFlag)
if tokenFile == "" {
tokenFile = strings.TrimSpace(os.Getenv(agentTokenFileEnv))
}
if tokenFile == "" {
tokenFile = defaultTokenFilePath()
}
return appConfig{
ListenAddr: listenAddr,
StorageRoots: storageRoots,
ArchiveRoots: archiveRoots,
Token: token,
TokenFile: tokenFile,
}
}
func defaultTokenFilePath() string {
if exePath, err := os.Executable(); err == nil {
return filepath.Join(filepath.Dir(exePath), defaultTokenFileName)
}
return defaultTokenFileName
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
})
}
func handleListServerFolders(w http.ResponseWriter, r *http.Request, config appConfig) {
rootType := serverFolderRootTypeFromRequest(r)
roots, err := getConfiguredServerFolderRoots(config.rootsForType(rootType), rootType)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
requestedPath := strings.TrimSpace(r.URL.Query().Get("path"))
if requestedPath == "" {
requestedPath = roots[0].Path
}
targetPath, err := cleanAbsolutePath(requestedPath)
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültiger Pfad")
return
}
if !isPathAllowed(targetPath, roots) {
writeError(w, http.StatusForbidden, "Pfad ist nicht freigegeben")
return
}
resolvedTargetPath, err := resolveExistingPath(targetPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
writeError(w, http.StatusNotFound, "Ordner wurde nicht gefunden")
return
}
writeError(w, http.StatusBadRequest, "Ordner konnte nicht geprüft werden")
return
}
if !isPathAllowed(resolvedTargetPath, roots) {
writeError(w, http.StatusForbidden, "Pfad verlässt den freigegebenen Bereich")
return
}
info, err := os.Stat(resolvedTargetPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
writeError(w, http.StatusNotFound, "Ordner wurde nicht gefunden")
return
}
writeError(w, http.StatusBadRequest, "Ordner konnte nicht gelesen werden")
return
}
if !info.IsDir() {
writeError(w, http.StatusBadRequest, "Pfad ist kein Ordner")
return
}
entries, err := os.ReadDir(resolvedTargetPath)
if err != nil {
writeError(w, http.StatusForbidden, "Ordnerinhalt konnte nicht gelesen werden")
return
}
folders := make([]serverFolderNode, 0, len(entries))
for _, entry := range entries {
if !entry.IsDir() {
continue
}
childPath := filepath.Join(resolvedTargetPath, entry.Name())
childPath, err = cleanAbsolutePath(childPath)
if err != nil {
continue
}
if !isPathAllowed(childPath, roots) {
continue
}
folders = append(folders, serverFolderNode{
Name: entry.Name(),
Path: childPath,
HasChildren: folderHasVisibleSubdirectories(childPath, roots),
})
}
sort.SliceStable(folders, func(i int, j int) bool {
return strings.ToLower(folders[i].Name) < strings.ToLower(folders[j].Name)
})
writeJSON(w, http.StatusOK, listServerFoldersResponse{
RootType: string(rootType),
Path: resolvedTargetPath,
ParentPath: allowedParentPath(resolvedTargetPath, roots),
Roots: roots,
Folders: folders,
})
}
func handleCreateServerFolder(w http.ResponseWriter, r *http.Request, config appConfig) {
rootType := serverFolderRootTypeFromRequest(r)
roots, err := getConfiguredServerFolderRoots(config.rootsForType(rootType), rootType)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
var input createServerFolderRequest
if err := readJSON(r, &input); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
return
}
targetPath, err := resolveCreateFolderTarget(input)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if !isPathAllowed(targetPath, roots) {
writeError(w, http.StatusForbidden, "Zielpfad ist nicht freigegeben")
return
}
parentPath := filepath.Dir(targetPath)
resolvedParentPath, err := resolveExistingPath(parentPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
writeError(w, http.StatusNotFound, "Übergeordneter Ordner wurde nicht gefunden")
return
}
writeError(w, http.StatusBadRequest, "Übergeordneter Ordner konnte nicht geprüft werden")
return
}
if !isPathAllowed(resolvedParentPath, roots) {
writeError(w, http.StatusForbidden, "Übergeordneter Ordner ist nicht freigegeben")
return
}
parentInfo, err := os.Stat(resolvedParentPath)
if err != nil {
writeError(w, http.StatusBadRequest, "Übergeordneter Ordner konnte nicht gelesen werden")
return
}
if !parentInfo.IsDir() {
writeError(w, http.StatusBadRequest, "Übergeordneter Pfad ist kein Ordner")
return
}
if err := os.Mkdir(targetPath, 0750); err != nil {
if errors.Is(err, fs.ErrExist) {
writeError(w, http.StatusConflict, "Ordner existiert bereits")
return
}
writeError(w, http.StatusForbidden, "Ordner konnte nicht erstellt werden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"folder": serverFolderNode{
Name: filepath.Base(targetPath),
Path: targetPath,
HasChildren: false,
},
})
}
func handleRenameServerFolder(w http.ResponseWriter, r *http.Request, config appConfig) {
rootType := serverFolderRootTypeFromRequest(r)
roots, err := getConfiguredServerFolderRoots(config.rootsForType(rootType), rootType)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
var input renameServerFolderRequest
if err := readJSON(r, &input); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
return
}
sourcePath, err := resolveExistingPath(input.Path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
writeError(w, http.StatusNotFound, "Ordner wurde nicht gefunden")
return
}
writeError(w, http.StatusBadRequest, "Ordner konnte nicht geprüft werden")
return
}
if !isPathAllowed(sourcePath, roots) {
writeError(w, http.StatusForbidden, "Pfad ist nicht freigegeben")
return
}
if isConfiguredRootPath(sourcePath, roots) {
writeError(w, http.StatusBadRequest, "Root-Ordner darf nicht umbenannt werden")
return
}
info, err := os.Stat(sourcePath)
if err != nil {
writeError(w, http.StatusBadRequest, "Ordner konnte nicht gelesen werden")
return
}
if !info.IsDir() {
writeError(w, http.StatusBadRequest, "Pfad ist kein Ordner")
return
}
nextName := strings.TrimSpace(input.Name)
if !isSafeSingleFolderName(nextName) {
writeError(w, http.StatusBadRequest, "Neuer Ordnername ist ungültig")
return
}
parentPath := filepath.Dir(sourcePath)
targetPath, err := cleanAbsolutePath(filepath.Join(parentPath, nextName))
if err != nil {
writeError(w, http.StatusBadRequest, "Neuer Zielpfad ist ungültig")
return
}
if !isPathAllowed(targetPath, roots) {
writeError(w, http.StatusForbidden, "Neuer Zielpfad ist nicht freigegeben")
return
}
if strings.EqualFold(sourcePath, targetPath) {
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"folder": serverFolderNode{
Name: filepath.Base(sourcePath),
Path: sourcePath,
HasChildren: folderHasVisibleSubdirectories(sourcePath, roots),
},
})
return
}
if _, err := os.Stat(targetPath); err == nil {
writeError(w, http.StatusConflict, "Zielordner existiert bereits")
return
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
writeError(w, http.StatusBadRequest, "Zielordner konnte nicht geprüft werden")
return
}
if err := os.Rename(sourcePath, targetPath); err != nil {
writeError(w, http.StatusForbidden, "Ordner konnte nicht umbenannt werden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"folder": serverFolderNode{
Name: filepath.Base(targetPath),
Path: targetPath,
HasChildren: folderHasVisibleSubdirectories(targetPath, roots),
},
})
}
func handleDeleteServerFolder(w http.ResponseWriter, r *http.Request, config appConfig) {
rootType := serverFolderRootTypeFromRequest(r)
roots, err := getConfiguredServerFolderRoots(config.rootsForType(rootType), rootType)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
requestedPath := strings.TrimSpace(r.URL.Query().Get("path"))
if requestedPath == "" && r.Body != nil {
var input struct {
Path string `json:"path"`
}
if err := readJSON(r, &input); err == nil {
requestedPath = strings.TrimSpace(input.Path)
}
}
if requestedPath == "" {
writeError(w, http.StatusBadRequest, "Pfad fehlt")
return
}
targetPath, err := resolveExistingPath(requestedPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
writeError(w, http.StatusNotFound, "Ordner wurde nicht gefunden")
return
}
writeError(w, http.StatusBadRequest, "Ordner konnte nicht geprüft werden")
return
}
if !isPathAllowed(targetPath, roots) {
writeError(w, http.StatusForbidden, "Pfad ist nicht freigegeben")
return
}
if isConfiguredRootPath(targetPath, roots) {
writeError(w, http.StatusBadRequest, "Root-Ordner darf nicht gelöscht werden")
return
}
info, err := os.Stat(targetPath)
if err != nil {
writeError(w, http.StatusBadRequest, "Ordner konnte nicht gelesen werden")
return
}
if !info.IsDir() {
writeError(w, http.StatusBadRequest, "Pfad ist kein Ordner")
return
}
if err := os.Remove(targetPath); err != nil {
if errors.Is(err, os.ErrNotExist) {
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"deleted": false,
"path": targetPath,
})
return
}
writeError(w, http.StatusConflict, "Ordner konnte nicht gelöscht werden. Er ist vermutlich nicht leer.")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"deleted": true,
"path": targetPath,
})
}
func isConfiguredRootPath(pathValue string, roots []serverFolderRoot) bool {
pathValue, err := cleanAbsolutePath(pathValue)
if err != nil {
return false
}
for _, root := range roots {
rootPath, err := cleanAbsolutePath(root.Path)
if err != nil {
continue
}
if strings.EqualFold(pathValue, rootPath) {
return true
}
}
return false
}
func serverFolderRootTypeFromRequest(r *http.Request) serverFolderRootType {
value := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("rootType")))
if value == "" {
value = strings.TrimSpace(strings.ToLower(r.URL.Query().Get("type")))
}
switch value {
case "archive", "archive-root", "archive_root":
return serverFolderRootTypeArchive
default:
return serverFolderRootTypeStorage
}
}
func getConfiguredServerFolderRoots(rawValue string, rootType serverFolderRootType) ([]serverFolderRoot, error) {
rawValue = strings.TrimSpace(rawValue)
if rawValue == "" {
return nil, fmt.Errorf("%s-root fehlt", rootType)
}
parts := strings.Split(rawValue, ";")
roots := make([]serverFolderRoot, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
label := ""
pathValue := part
if before, after, found := strings.Cut(part, "="); found {
label = strings.TrimSpace(before)
pathValue = strings.TrimSpace(after)
}
rootPath, err := cleanAbsolutePath(pathValue)
if err != nil {
return nil, fmt.Errorf("ungültiger Root-Pfad: %s", pathValue)
}
if label == "" {
label = rootPath
}
roots = append(roots, serverFolderRoot{
Label: label,
Path: rootPath,
})
}
if len(roots) == 0 {
return nil, fmt.Errorf("keine gültigen Root-Pfade konfiguriert")
}
return roots, nil
}
func cleanAbsolutePath(value string) (string, error) {
value = strings.TrimSpace(value)
if value == "" {
return "", fmt.Errorf("Pfad fehlt")
}
cleaned := filepath.Clean(value)
absolute, err := filepath.Abs(cleaned)
if err != nil {
return "", err
}
return absolute, nil
}
func resolveExistingPath(value string) (string, error) {
cleaned, err := cleanAbsolutePath(value)
if err != nil {
return "", err
}
resolved, err := filepath.EvalSymlinks(cleaned)
if err != nil {
return "", err
}
return cleanAbsolutePath(resolved)
}
func resolveCreateFolderTarget(input createServerFolderRequest) (string, error) {
if strings.TrimSpace(input.Path) != "" {
return cleanAbsolutePath(input.Path)
}
parentPath := strings.TrimSpace(input.ParentPath)
folderName := strings.TrimSpace(input.Name)
if parentPath == "" {
return "", fmt.Errorf("parentPath fehlt")
}
if !isSafeSingleFolderName(folderName) {
return "", fmt.Errorf("Ordnername ist ungültig")
}
return cleanAbsolutePath(filepath.Join(parentPath, folderName))
}
func isSafeSingleFolderName(value string) bool {
value = strings.TrimSpace(value)
if value == "" || value == "." || value == ".." {
return false
}
if strings.ContainsAny(value, `/\`) {
return false
}
if strings.ContainsAny(value, `<>:"|?*`) {
return false
}
return true
}
func isPathAllowed(pathValue string, roots []serverFolderRoot) bool {
pathValue, err := cleanAbsolutePath(pathValue)
if err != nil {
return false
}
for _, root := range roots {
rootPath, err := cleanAbsolutePath(root.Path)
if err != nil {
continue
}
if pathWithinRoot(pathValue, rootPath) {
return true
}
}
return false
}
func pathWithinRoot(pathValue string, rootPath string) bool {
rel, err := filepath.Rel(rootPath, pathValue)
if err != nil {
return false
}
return rel == "." ||
(rel != ".." &&
!strings.HasPrefix(rel, ".."+string(filepath.Separator)) &&
!filepath.IsAbs(rel))
}
func allowedParentPath(pathValue string, roots []serverFolderRoot) string {
parentPath := filepath.Dir(pathValue)
if parentPath == pathValue {
return ""
}
if !isPathAllowed(parentPath, roots) {
return ""
}
return parentPath
}
func folderHasVisibleSubdirectories(pathValue string, roots []serverFolderRoot) bool {
entries, err := os.ReadDir(pathValue)
if err != nil {
return false
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
childPath, err := cleanAbsolutePath(filepath.Join(pathValue, entry.Name()))
if err != nil {
continue
}
if isPathAllowed(childPath, roots) {
return true
}
}
return false
}
// tokenStore hält den zur Laufzeit gültigen Agent-Token. Der Token wird in einer
// Datei persistiert, damit er einen Neustart des Agents übersteht.
type tokenStore struct {
mu sync.RWMutex
token string
filePath string
}
func newTokenStore(filePath string, seed string) *tokenStore {
store := &tokenStore{filePath: strings.TrimSpace(filePath)}
if persisted := store.loadFromFile(); persisted != "" {
store.token = persisted
return store
}
if seed = strings.TrimSpace(seed); seed != "" {
store.token = seed
if err := store.persist(seed); err != nil {
log.Printf("token seed konnte nicht persistiert werden: %v", err)
}
}
return store
}
func (t *tokenStore) get() string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.token
}
func (t *tokenStore) set(token string) error {
token = strings.TrimSpace(token)
t.mu.Lock()
defer t.mu.Unlock()
if err := t.persist(token); err != nil {
return err
}
t.token = token
return nil
}
func (t *tokenStore) loadFromFile() string {
if t.filePath == "" {
return ""
}
data, err := os.ReadFile(t.filePath)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
func (t *tokenStore) persist(token string) error {
if t.filePath == "" {
return nil
}
return os.WriteFile(t.filePath, []byte(token), 0600)
}
type setAgentTokenRequest struct {
Token string `json:"token"`
}
func handleSetAgentToken(w http.ResponseWriter, r *http.Request, store *tokenStore) {
var input setAgentTokenRequest
if err := readJSON(r, &input); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
return
}
nextToken := strings.TrimSpace(input.Token)
if nextToken == "" {
writeError(w, http.StatusBadRequest, "Token fehlt")
return
}
if err := store.set(nextToken); err != nil {
log.Printf("Token konnte nicht gespeichert werden: %v", err)
writeError(w, http.StatusInternalServerError, "Token konnte nicht gespeichert werden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
})
}
func requireAgentToken(store *tokenStore, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
requiredToken := store.get()
if requiredToken == "" {
next(w, r)
return
}
token := strings.TrimSpace(r.Header.Get("X-Server-Folders-Token"))
if token == "" {
authHeader := strings.TrimSpace(r.Header.Get("Authorization"))
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
token = strings.TrimSpace(authHeader[len("bearer "):])
}
}
if token != requiredToken {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
next(w, r)
}
}
func logRequests(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s from %s", r.Method, r.URL.RequestURI(), r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
func readJSON(r *http.Request, target any) error {
defer r.Body.Close()
decoder := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
decoder.DisallowUnknownFields()
return decoder.Decode(target)
}
func writeJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(value); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]any{
"error": message,
})
}