agent security updates 4-30-26
This commit is contained in:
parent
2517a41ddf
commit
f1a245cc01
90
internal/auth/auth.go
Normal file
90
internal/auth/auth.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
envToken = "ZLH_AGENT_TOKEN"
|
||||||
|
HeaderToken = "X-ZLH-Agent-Token"
|
||||||
|
QueryTokenParam = "agent_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Policy struct {
|
||||||
|
Public map[string]map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Public(methods ...string) map[string]struct{} {
|
||||||
|
out := make(map[string]struct{}, len(methods))
|
||||||
|
for _, method := range methods {
|
||||||
|
out[strings.ToUpper(strings.TrimSpace(method))] = struct{}{}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func Wrap(next http.Handler, policy Policy) http.Handler {
|
||||||
|
if strings.TrimSpace(os.Getenv(envToken)) == "" {
|
||||||
|
log.Printf("[auth] warning: %s not set; agent auth enforcement disabled", envToken)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if policy.IsPublic(r.Method, r.URL.Path) {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !Authorized(r) {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Policy) IsPublic(method, path string) bool {
|
||||||
|
methods, ok := p.Public[path]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok = methods[strings.ToUpper(method)]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func Authorized(r *http.Request) bool {
|
||||||
|
expected := strings.TrimSpace(os.Getenv(envToken))
|
||||||
|
if expected == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return constantTimeEqual(bearerToken(r.Header.Get("Authorization")), expected) ||
|
||||||
|
constantTimeEqual(r.Header.Get(HeaderToken), expected) ||
|
||||||
|
constantTimeEqual(r.URL.Query().Get(QueryTokenParam), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bearerToken(header string) string {
|
||||||
|
header = strings.TrimSpace(header)
|
||||||
|
if header == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
before, after, ok := strings.Cut(header, " ")
|
||||||
|
if !ok || !strings.EqualFold(before, "Bearer") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(after)
|
||||||
|
}
|
||||||
|
|
||||||
|
func constantTimeEqual(got, expected string) bool {
|
||||||
|
got = strings.TrimSpace(got)
|
||||||
|
expected = strings.TrimSpace(expected)
|
||||||
|
if got == "" || expected == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(got) != len(expected) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return subtle.ConstantTimeCompare([]byte(got), []byte(expected)) == 1
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package agenthttp
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -12,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"zlh-agent/internal/alloy"
|
"zlh-agent/internal/alloy"
|
||||||
|
"zlh-agent/internal/auth"
|
||||||
agentfiles "zlh-agent/internal/files"
|
agentfiles "zlh-agent/internal/files"
|
||||||
agenthandlers "zlh-agent/internal/handlers"
|
agenthandlers "zlh-agent/internal/handlers"
|
||||||
mcstatus "zlh-agent/internal/minecraft"
|
mcstatus "zlh-agent/internal/minecraft"
|
||||||
@ -31,7 +31,10 @@ import (
|
|||||||
"zlh-agent/internal/version"
|
"zlh-agent/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ReadinessTimeout = 60 * time.Second
|
const (
|
||||||
|
ReadinessTimeout = 60 * time.Second
|
||||||
|
MaxConfigBytes = 1 << 20
|
||||||
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
|
||||||
@ -331,14 +334,19 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := io.ReadAll(r.Body)
|
|
||||||
|
|
||||||
var cfg state.Config
|
var cfg state.Config
|
||||||
if err := json.Unmarshal(body, &cfg); err != nil {
|
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, MaxConfigBytes))
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
if err := dec.Decode(&cfg); err != nil {
|
||||||
endOp()
|
endOp()
|
||||||
http.Error(w, "bad json", http.StatusBadRequest)
|
http.Error(w, "bad json", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := state.ValidateConfig(&cfg); err != nil {
|
||||||
|
endOp()
|
||||||
|
http.Error(w, "invalid config: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Printf("[http] vmid=%d action=config status=received type=%s runtime=%s game=%s variant=%s version=%s", cfg.VMID, cfg.ContainerType, cfg.Runtime, cfg.Game, cfg.Variant, cfg.Version)
|
log.Printf("[http] vmid=%d action=config status=received type=%s runtime=%s game=%s variant=%s version=%s", cfg.VMID, cfg.ContainerType, cfg.Runtime, cfg.Game, cfg.Variant, cfg.Version)
|
||||||
|
|
||||||
if err := state.SaveConfig(&cfg); err != nil {
|
if err := state.SaveConfig(&cfg); err != nil {
|
||||||
@ -898,7 +906,7 @@ func handleGamePlayers(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
func NewMux() *http.ServeMux {
|
func NewMux() http.Handler {
|
||||||
m := http.NewServeMux()
|
m := http.NewServeMux()
|
||||||
|
|
||||||
m.HandleFunc("/config", handleConfig)
|
m.HandleFunc("/config", handleConfig)
|
||||||
@ -937,5 +945,10 @@ func NewMux() *http.ServeMux {
|
|||||||
})
|
})
|
||||||
|
|
||||||
log.Println("[agent] routes registered")
|
log.Println("[agent] routes registered")
|
||||||
return m
|
return auth.Wrap(m, auth.Policy{
|
||||||
|
Public: map[string]map[string]struct{}{
|
||||||
|
"/health": auth.Public(http.MethodGet),
|
||||||
|
"/version": auth.Public(http.MethodGet),
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ type consoleSession struct {
|
|||||||
key string
|
key string
|
||||||
cfg *state.Config
|
cfg *state.Config
|
||||||
ptyFile *os.File
|
ptyFile *os.File
|
||||||
|
ownsPTY bool
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
lastActive time.Time
|
lastActive time.Time
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ func getConsoleSession(cfg *state.Config) (*consoleSession, bool, error) {
|
|||||||
key: key,
|
key: key,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
ptyFile: ptyFile,
|
ptyFile: ptyFile,
|
||||||
|
ownsPTY: cfg.ContainerType == "dev",
|
||||||
createdAt: time.Now(),
|
createdAt: time.Now(),
|
||||||
lastActive: time.Now(),
|
lastActive: time.Now(),
|
||||||
conns: make(map[*websocket.Conn]*consoleConn),
|
conns: make(map[*websocket.Conn]*consoleConn),
|
||||||
@ -215,7 +217,7 @@ func (s *consoleSession) destroy() {
|
|||||||
s.ptyFile = nil
|
s.ptyFile = nil
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
if pty != nil {
|
if pty != nil && s.ownsPTY {
|
||||||
_ = pty.Close()
|
_ = pty.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,6 @@ func TestResolveProxyVersion(t *testing.T) {
|
|||||||
origRoot := fabricProxyArtifactRoot
|
origRoot := fabricProxyArtifactRoot
|
||||||
origMap := fabricProxyMap
|
origMap := fabricProxyMap
|
||||||
origErr := fabricProxyMapErr
|
origErr := fabricProxyMapErr
|
||||||
origOnce := fabricProxyMapOnce
|
|
||||||
fabricProxyArtifactRoot = "zpacks/minecraft/fabric/fabric-proxy-lite"
|
fabricProxyArtifactRoot = "zpacks/minecraft/fabric/fabric-proxy-lite"
|
||||||
fabricProxyMap = nil
|
fabricProxyMap = nil
|
||||||
fabricProxyMapErr = nil
|
fabricProxyMapErr = nil
|
||||||
@ -40,7 +39,7 @@ func TestResolveProxyVersion(t *testing.T) {
|
|||||||
fabricProxyArtifactRoot = origRoot
|
fabricProxyArtifactRoot = origRoot
|
||||||
fabricProxyMap = origMap
|
fabricProxyMap = origMap
|
||||||
fabricProxyMapErr = origErr
|
fabricProxyMapErr = origErr
|
||||||
fabricProxyMapOnce = origOnce
|
fabricProxyMapOnce = sync.Once{}
|
||||||
})
|
})
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -342,7 +343,113 @@ func SaveConfig(cfg *Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(configPath, b, 0o644)
|
return os.WriteFile(configPath, b, 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateConfig(cfg *Config) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("config required")
|
||||||
|
}
|
||||||
|
if cfg.VMID <= 0 {
|
||||||
|
return fmt.Errorf("vmid must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeContainerType(cfg)
|
||||||
|
switch strings.ToLower(strings.TrimSpace(cfg.ContainerType)) {
|
||||||
|
case "dev":
|
||||||
|
return validateDevConfig(cfg)
|
||||||
|
case "game":
|
||||||
|
return validateGameConfig(cfg)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported container_type: %s", cfg.ContainerType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeContainerType(cfg *Config) {
|
||||||
|
if strings.TrimSpace(cfg.ContainerType) != "" {
|
||||||
|
cfg.ContainerType = strings.ToLower(strings.TrimSpace(cfg.ContainerType))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vmidStr := strconv.Itoa(cfg.VMID)
|
||||||
|
if len(vmidStr) > 0 {
|
||||||
|
switch vmidStr[0] {
|
||||||
|
case '6':
|
||||||
|
cfg.ContainerType = "dev"
|
||||||
|
case '5':
|
||||||
|
cfg.ContainerType = "game"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.ContainerType == "" && strings.TrimSpace(cfg.Runtime) != "" {
|
||||||
|
cfg.ContainerType = "dev"
|
||||||
|
}
|
||||||
|
if cfg.ContainerType == "" {
|
||||||
|
cfg.ContainerType = "game"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDevConfig(cfg *Config) error {
|
||||||
|
if strings.TrimSpace(cfg.Game) != "" || strings.TrimSpace(cfg.Variant) != "" {
|
||||||
|
return fmt.Errorf("game and variant are not valid for dev containers")
|
||||||
|
}
|
||||||
|
runtime := strings.ToLower(strings.TrimSpace(cfg.Runtime))
|
||||||
|
switch runtime {
|
||||||
|
case "node", "python", "go", "java", "dotnet":
|
||||||
|
cfg.Runtime = runtime
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported dev runtime: %s", cfg.Runtime)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Version) == "" {
|
||||||
|
return fmt.Errorf("version required for dev containers")
|
||||||
|
}
|
||||||
|
return validateMemory(cfg.MemoryMB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateGameConfig(cfg *Config) error {
|
||||||
|
if cfg.EnableCodeServer {
|
||||||
|
return fmt.Errorf("code-server is only valid for dev containers")
|
||||||
|
}
|
||||||
|
game := strings.ToLower(strings.TrimSpace(cfg.Game))
|
||||||
|
if game == "" {
|
||||||
|
return fmt.Errorf("game required for game containers")
|
||||||
|
}
|
||||||
|
cfg.Game = game
|
||||||
|
|
||||||
|
switch game {
|
||||||
|
case "minecraft":
|
||||||
|
variant := strings.ToLower(strings.TrimSpace(cfg.Variant))
|
||||||
|
switch variant {
|
||||||
|
case "vanilla", "fabric", "paper", "purpur", "quilt", "forge", "neoforge":
|
||||||
|
cfg.Variant = variant
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported minecraft variant: %s", cfg.Variant)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Version) == "" {
|
||||||
|
return fmt.Errorf("version required for minecraft containers")
|
||||||
|
}
|
||||||
|
case "valheim", "rust", "terraria", "projectzomboid":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported game: %s", cfg.Game)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateMemory(cfg.MemoryMB); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, port := range cfg.Ports {
|
||||||
|
if port < 1 || port > 65535 {
|
||||||
|
return fmt.Errorf("invalid port: %d", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateMemory(memoryMB int) error {
|
||||||
|
if memoryMB == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if memoryMB < 256 || memoryMB > 262144 {
|
||||||
|
return fmt.Errorf("memory_mb out of range: %d", memoryMB)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() (*Config, error) {
|
func LoadConfig() (*Config, error) {
|
||||||
@ -356,23 +463,7 @@ func LoadConfig() (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.ContainerType == "" {
|
normalizeContainerType(&cfg)
|
||||||
vmidStr := strconv.Itoa(cfg.VMID)
|
|
||||||
if len(vmidStr) > 0 {
|
|
||||||
switch vmidStr[0] {
|
|
||||||
case '6':
|
|
||||||
cfg.ContainerType = "dev"
|
|
||||||
case '5':
|
|
||||||
cfg.ContainerType = "game"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cfg.ContainerType == "" && cfg.Runtime != "" {
|
|
||||||
cfg.ContainerType = "dev"
|
|
||||||
}
|
|
||||||
if cfg.ContainerType == "" {
|
|
||||||
cfg.ContainerType = "game"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -270,3 +270,38 @@
|
|||||||
[2026-04-18T16:40:02Z] action=archive_write step=add_path status=complete backup_id=safe path=world files=1 bytes=6
|
[2026-04-18T16:40:02Z] action=archive_write step=add_path status=complete backup_id=safe path=world files=1 bytes=6
|
||||||
[2026-04-18T16:40:02Z] action=archive_write status=complete backup_id=safe files=1 bytes=6
|
[2026-04-18T16:40:02Z] action=archive_write status=complete backup_id=safe files=1 bytes=6
|
||||||
[2026-04-18T16:40:02Z] action=archive_restore status=begin archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting1871575453/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting1871575453/001 paths=["../world"]
|
[2026-04-18T16:40:02Z] action=archive_restore status=begin archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting1871575453/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting1871575453/001 paths=["../world"]
|
||||||
|
[2026-04-24T16:43:38Z] action=archive_write status=begin backup_id=safe archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2012860903/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2012860903/002 paths=["world"]
|
||||||
|
[2026-04-24T16:43:38Z] action=archive_write step=add_path status=begin backup_id=safe path=world
|
||||||
|
[2026-04-24T16:43:38Z] action=archive_write step=entry backup_id=safe type=dir path=world
|
||||||
|
[2026-04-24T16:43:38Z] action=archive_write step=entry backup_id=safe type=file path=world/level.dat bytes=6 total_files=1 total_bytes=6
|
||||||
|
[2026-04-24T16:43:38Z] action=archive_write step=add_path status=complete backup_id=safe path=world files=1 bytes=6
|
||||||
|
[2026-04-24T16:43:38Z] action=archive_write status=complete backup_id=safe files=1 bytes=6
|
||||||
|
[2026-04-24T16:43:38Z] action=archive_restore status=begin archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2012860903/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2012860903/001 paths=["../world"]
|
||||||
|
[2026-04-29T12:50:49Z] action=archive_write status=begin backup_id=safe archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting3857932451/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting3857932451/002 paths=["world"]
|
||||||
|
[2026-04-29T12:50:49Z] action=archive_write step=add_path status=begin backup_id=safe path=world
|
||||||
|
[2026-04-29T12:50:49Z] action=archive_write step=entry backup_id=safe type=dir path=world
|
||||||
|
[2026-04-29T12:50:49Z] action=archive_write step=entry backup_id=safe type=file path=world/level.dat bytes=6 total_files=1 total_bytes=6
|
||||||
|
[2026-04-29T12:50:49Z] action=archive_write step=add_path status=complete backup_id=safe path=world files=1 bytes=6
|
||||||
|
[2026-04-29T12:50:49Z] action=archive_write status=complete backup_id=safe files=1 bytes=6
|
||||||
|
[2026-04-29T12:50:49Z] action=archive_restore status=begin archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting3857932451/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting3857932451/001 paths=["../world"]
|
||||||
|
[2026-04-30T19:29:21Z] action=archive_write status=begin backup_id=safe archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2552956638/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2552956638/002 paths=["world"]
|
||||||
|
[2026-04-30T19:29:21Z] action=archive_write step=add_path status=begin backup_id=safe path=world
|
||||||
|
[2026-04-30T19:29:21Z] action=archive_write step=entry backup_id=safe type=dir path=world
|
||||||
|
[2026-04-30T19:29:21Z] action=archive_write step=entry backup_id=safe type=file path=world/level.dat bytes=6 total_files=1 total_bytes=6
|
||||||
|
[2026-04-30T19:29:21Z] action=archive_write step=add_path status=complete backup_id=safe path=world files=1 bytes=6
|
||||||
|
[2026-04-30T19:29:21Z] action=archive_write status=complete backup_id=safe files=1 bytes=6
|
||||||
|
[2026-04-30T19:29:21Z] action=archive_restore status=begin archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2552956638/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2552956638/001 paths=["../world"]
|
||||||
|
[2026-04-30T20:07:33Z] action=archive_write status=begin backup_id=safe archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2513795887/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2513795887/002 paths=["world"]
|
||||||
|
[2026-04-30T20:07:33Z] action=archive_write step=add_path status=begin backup_id=safe path=world
|
||||||
|
[2026-04-30T20:07:33Z] action=archive_write step=entry backup_id=safe type=dir path=world
|
||||||
|
[2026-04-30T20:07:33Z] action=archive_write step=entry backup_id=safe type=file path=world/level.dat bytes=6 total_files=1 total_bytes=6
|
||||||
|
[2026-04-30T20:07:33Z] action=archive_write step=add_path status=complete backup_id=safe path=world files=1 bytes=6
|
||||||
|
[2026-04-30T20:07:33Z] action=archive_write status=complete backup_id=safe files=1 bytes=6
|
||||||
|
[2026-04-30T20:07:33Z] action=archive_restore status=begin archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2513795887/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2513795887/001 paths=["../world"]
|
||||||
|
[2026-04-30T20:08:10Z] action=archive_write status=begin backup_id=safe archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting3515028932/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting3515028932/002 paths=["world"]
|
||||||
|
[2026-04-30T20:08:10Z] action=archive_write step=add_path status=begin backup_id=safe path=world
|
||||||
|
[2026-04-30T20:08:10Z] action=archive_write step=entry backup_id=safe type=dir path=world
|
||||||
|
[2026-04-30T20:08:10Z] action=archive_write step=entry backup_id=safe type=file path=world/level.dat bytes=6 total_files=1 total_bytes=6
|
||||||
|
[2026-04-30T20:08:10Z] action=archive_write step=add_path status=complete backup_id=safe path=world files=1 bytes=6
|
||||||
|
[2026-04-30T20:08:10Z] action=archive_write status=complete backup_id=safe files=1 bytes=6
|
||||||
|
[2026-04-30T20:08:10Z] action=archive_restore status=begin archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting3515028932/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting3515028932/001 paths=["../world"]
|
||||||
|
|||||||
12
main.go
12
main.go
@ -57,11 +57,13 @@ func main() {
|
|||||||
update.StartPeriodic(version.AgentVersion)
|
update.StartPeriodic(version.AgentVersion)
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
WriteTimeout: 30 * time.Second,
|
ReadTimeout: 30 * time.Second,
|
||||||
IdleTimeout: 60 * time.Second,
|
WriteTimeout: 30 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
MaxHeaderBytes: 1 << 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"status": "available",
|
"status": "available",
|
||||||
"current": "0.0.0-dev",
|
"current": "0.0.0-dev",
|
||||||
"target": "1.0.68",
|
"target": "1.0.70",
|
||||||
"checked_at_utc": "2026-04-19T21:48:03Z"
|
"checked_at_utc": "2026-04-30T20:48:03Z"
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user