fixes 5-1-26

This commit is contained in:
jester 2026-05-01 14:32:53 +00:00
parent f1a245cc01
commit e3f91454f8
4 changed files with 133 additions and 8 deletions

View File

@ -27,16 +27,16 @@ func Public(methods ...string) map[string]struct{} {
} }
func Wrap(next http.Handler, policy Policy) http.Handler { 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if policy.IsPublic(r.Method, r.URL.Path) { if policy.IsPublic(r.Method, r.URL.Path) {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
if strings.TrimSpace(os.Getenv(envToken)) == "" {
log.Printf("[auth] warning: %s not set; rejecting protected request path=%s", envToken, r.URL.Path)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if !Authorized(r) { if !Authorized(r) {
http.Error(w, "unauthorized", http.StatusUnauthorized) http.Error(w, "unauthorized", http.StatusUnauthorized)
return return

View File

@ -1,11 +1,129 @@
package agenthttp package agenthttp
import ( import (
"net/http"
"net/http/httptest"
"strings"
"testing" "testing"
"github.com/gorilla/websocket"
"zlh-agent/internal/state" "zlh-agent/internal/state"
) )
func TestProtectedRoutesRequireAgentToken(t *testing.T) {
t.Setenv("ZLH_AGENT_TOKEN", "test-token")
protected := []struct {
method string
path string
body string
}{
{http.MethodGet, "/status", ""},
{http.MethodGet, "/ready", ""},
{http.MethodPost, "/config", "{}"},
{http.MethodPost, "/start", ""},
{http.MethodPost, "/stop", ""},
{http.MethodPost, "/restart", ""},
{http.MethodGet, "/game/files/list", ""},
{http.MethodGet, "/game/files/read", ""},
{http.MethodGet, "/game/files/download", ""},
{http.MethodPost, "/game/files/upload", ""},
{http.MethodPost, "/game/files/revert", ""},
{http.MethodGet, "/game/files/stat", ""},
{http.MethodGet, "/game/backups", ""},
{http.MethodPost, "/game/backups", ""},
{http.MethodPost, "/game/backups/restore", `{"id":"backup-1"}`},
{http.MethodDelete, "/game/backups/backup-1", ""},
{http.MethodGet, "/game/mods", ""},
{http.MethodPost, "/game/mods/install", "{}"},
{http.MethodPost, "/game/mods/mod-1", "{}"},
{http.MethodPost, "/agent/update", "{}"},
{http.MethodGet, "/agent/update/status", ""},
{http.MethodGet, "/metrics/process", ""},
{http.MethodPost, "/dev/codeserver/start", ""},
{http.MethodPost, "/dev/codeserver/stop", ""},
{http.MethodPost, "/dev/codeserver/restart", ""},
}
mux := NewMux()
for _, tc := range protected {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
req := httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("without token status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
req = httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
req.Header.Set("Authorization", "Bearer test-token")
rec = httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code == http.StatusUnauthorized {
t.Fatalf("with token status = %d, want authorized request to reach handler", rec.Code)
}
})
}
}
func TestProtectedRoutesFailClosedWhenAgentTokenMissing(t *testing.T) {
t.Setenv("ZLH_AGENT_TOKEN", "")
mux := NewMux()
req := httptest.NewRequest(http.MethodGet, "/status", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
}
func TestPublicRoutesDoNotRequireAgentToken(t *testing.T) {
t.Setenv("ZLH_AGENT_TOKEN", "")
mux := NewMux()
for _, path := range []string{"/health", "/version"} {
t.Run(path, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code == http.StatusUnauthorized {
t.Fatalf("status = %d, want public route", rec.Code)
}
})
}
}
func TestConsoleWebSocketRequiresAgentToken(t *testing.T) {
t.Setenv("ZLH_AGENT_TOKEN", "test-token")
server := httptest.NewServer(NewMux())
defer server.Close()
conn, resp, err := websocket.DefaultDialer.Dial(strings.Replace(server.URL, "http://", "ws://", 1)+"/console/stream", nil)
if conn != nil {
_ = conn.Close()
}
if err == nil {
t.Fatalf("websocket dial without token succeeded")
}
if resp == nil {
t.Fatalf("websocket dial response is nil: %v", err)
}
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusUnauthorized)
}
}
func TestEnsureDevCodeServerInstallsAndStartsWhenRequested(t *testing.T) { func TestEnsureDevCodeServerInstallsAndStartsWhenRequested(t *testing.T) {
restoreCodeServerTestHooks(t) restoreCodeServerTestHooks(t)

View File

@ -305,3 +305,10 @@
[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 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_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"] [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"]
[2026-04-30T21:43:50Z] action=archive_write status=begin backup_id=safe archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2034890975/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2034890975/002 paths=["world"]
[2026-04-30T21:43:50Z] action=archive_write step=add_path status=begin backup_id=safe path=world
[2026-04-30T21:43:50Z] action=archive_write step=entry backup_id=safe type=dir path=world
[2026-04-30T21:43:50Z] action=archive_write step=entry backup_id=safe type=file path=world/level.dat bytes=6 total_files=1 total_bytes=6
[2026-04-30T21:43:50Z] action=archive_write step=add_path status=complete backup_id=safe path=world files=1 bytes=6
[2026-04-30T21:43:50Z] action=archive_write status=complete backup_id=safe files=1 bytes=6
[2026-04-30T21:43:50Z] action=archive_restore status=begin archive_path=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2034890975/003/safe.tar.gz server_root=/tmp/TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting2034890975/001 paths=["../world"]

View File

@ -1,6 +1,6 @@
{ {
"status": "available", "status": "noop",
"current": "0.0.0-dev", "current": "1.0.70",
"target": "1.0.70", "target": "1.0.70",
"checked_at_utc": "2026-04-30T20:48:03Z" "checked_at_utc": "2026-05-01T14:05:18Z"
} }