aboutsummaryrefslogtreecommitdiff
path: root/internal/admin
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin')
-rw-r--r--internal/admin/api_files.go16
-rw-r--r--internal/admin/api_map.go10
-rw-r--r--internal/admin/server.go50
-rw-r--r--internal/admin/undo.go8
4 files changed, 79 insertions, 5 deletions
diff --git a/internal/admin/api_files.go b/internal/admin/api_files.go
index 96b5e81..2aa0e5f 100644
--- a/internal/admin/api_files.go
+++ b/internal/admin/api_files.go
@@ -108,6 +108,22 @@ func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) {
return
}
+ // A raw rename changes the ID-bearing filename. Rebuild the room index
+ // and drop the old room's cached state (mirroring the DELETE path), and
+ // refresh item/mob/drop caches, or the old ID resolves to a deleted file
+ // and the new one resolves to nothing until a restart.
+ s.evictDeletedRoomFile(relPath)
+ s.invalidateEntityCacheForPath(newAbs)
+ // Hazards and courses have their own runtime caches that a raw rename
+ // must refresh (the undo path does the same). Techs and triggers keep the
+ // documented in-game `reload` requirement.
+ if pathUnderDir(s.dataDir, "hazards", newAbs) {
+ s.world.ClearHazardCache()
+ }
+ if pathUnderDir(s.dataDir, "courses", newAbs) {
+ s.reloadCoursesIfWired()
+ }
+
newRelPath := filepath.Join(filepath.Dir(relPath), newName)
if filepath.Dir(relPath) == "." {
newRelPath = newName
diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go
index 5eac385..d0372df 100644
--- a/internal/admin/api_map.go
+++ b/internal/admin/api_map.go
@@ -24,6 +24,16 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
}
dir := r.URL.Query().Get("dir")
+ if dir != "" {
+ // Route the map's directory through the same guard every other room
+ // API uses, so ?dir=../../.. cannot read/walk outside data/rooms.
+ clean, err := s.resolveRoomSubdir(dir)
+ if err != nil {
+ failJSON(w, err.Error())
+ return
+ }
+ dir = clean
+ }
seed := s.cfg.StartingRoom
if dir != "" {
if low, ok := findLowestLoadableRoom(s, dir); ok {
diff --git a/internal/admin/server.go b/internal/admin/server.go
index 4a54c78..4ea22d8 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -365,6 +365,20 @@ func (s *AdminServer) authMiddleware(next http.Handler) http.Handler {
})
}
+// UndoStack returns the server's undo history. Two admin transports over the
+// same data directory must share one stack: each server otherwise loads and
+// rewrites the same history file independently, so records made through one
+// are silently dropped by the next write from the other.
+func (s *AdminServer) UndoStack() *UndoStack { return s.undoStack }
+
+// SetUndoStack replaces the server's undo history with a shared stack. Call
+// before serving requests.
+func (s *AdminServer) SetUndoStack(us *UndoStack) {
+ if us != nil {
+ s.undoStack = us
+ }
+}
+
func (s *AdminServer) checkAuth(r *http.Request) bool {
cookie, err := r.Cookie("admin_session")
if err != nil {
@@ -389,8 +403,16 @@ func (s *AdminServer) checkAuth(r *http.Request) bool {
return false
}
+ // Bind the signature to the current password hash as well as the issue
+ // time, so changing an admin password invalidates every previously issued
+ // cookie instead of leaving it valid for the full TTL. The admin check
+ // above already loaded the account; reload it here for the hash.
+ acc, err := s.accountStore.LoadAccount(account)
+ if err != nil || !acc.Admin {
+ return false
+ }
mac := hmac.New(sha256.New, s.cookieSecret)
- mac.Write([]byte(account + ":" + parts[1]))
+ mac.Write([]byte(account + ":" + parts[1] + ":" + acc.PasswordHash))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sig), []byte(expected))
}
@@ -552,9 +574,15 @@ func clientIP(remoteAddr string) string {
}
func (s *AdminServer) setAuthCookie(w http.ResponseWriter, account string) {
+ // Include the current password hash in the MAC so a password change
+ // revokes outstanding cookies (see checkAuth).
+ hash := ""
+ if acc, err := s.accountStore.LoadAccount(account); err == nil {
+ hash = acc.PasswordHash
+ }
issued := strconv.FormatInt(time.Now().Unix(), 10)
mac := hmac.New(sha256.New, s.cookieSecret)
- mac.Write([]byte(account + ":" + issued))
+ mac.Write([]byte(account + ":" + issued + ":" + hash))
sig := hex.EncodeToString(mac.Sum(nil))
http.SetCookie(w, &http.Cookie{
Name: "admin_session",
@@ -607,6 +635,16 @@ func (s *AdminServer) handleLogin(w http.ResponseWriter, r *http.Request) {
account := strings.TrimSpace(r.FormValue("account"))
password := r.FormValue("password")
+ // Validate before the name is joined into a filesystem path: LoadAccount
+ // does no validation, so an unvalidated name is an arbitrary-*.yaml read
+ // primitive ("account=../../rooms/1"). Every other account-name entry
+ // point validates first.
+ if err := player.ValidName(account); err != nil {
+ limiter.fail(ip)
+ s.renderPage(w, r, "login.html", map[string]any{"Error": "Invalid account name or password."})
+ return
+ }
+
acc, err := s.accountStore.LoadAccount(account)
if err != nil || !player.CheckPassword(password, acc.PasswordHash) {
limiter.fail(ip)
@@ -1004,6 +1042,14 @@ func (s *AdminServer) invalidateEntityCacheForPath(path string) {
}
}
+// pathUnderDir reports whether path lies inside dataDir/subdir (and is not the
+// directory itself). Used by the raw-rename path to refresh caches that the
+// shared invalidateEntityCacheForPath does not own.
+func pathUnderDir(dataDir, subdir, path string) bool {
+ rel, err := filepath.Rel(filepath.Join(dataDir, subdir), path)
+ return err == nil && rel != "." && !strings.HasPrefix(rel, "..")
+}
+
func changeFiles(change *ChangeDesc) []string {
files := []string{change.FilePath}
if change.NewFilePath != "" {
diff --git a/internal/admin/undo.go b/internal/admin/undo.go
index 7bfbe09..24ce9c1 100644
--- a/internal/admin/undo.go
+++ b/internal/admin/undo.go
@@ -9,6 +9,8 @@ import (
"strings"
"sync"
"time"
+
+ "thehouseoficarus/internal/persistence"
)
type ExtraFile struct {
@@ -537,12 +539,12 @@ func (us *UndoStack) save() {
log.Printf("undo: failed to marshal history: %v", err)
return
}
- if err := os.WriteFile(us.filePath, b, 0600); err != nil {
+ if err := persistence.WriteFile(us.filePath, b, 0600); err != nil {
log.Printf("undo: failed to write history file: %v", err)
return
}
- // os.WriteFile does not change an existing file's mode: tighten a history
- // file that a previous version created world-readable.
+ // persistence.WriteFile preserves an existing file's mode: tighten a
+ // history file that a previous version created world-readable.
if err := os.Chmod(us.filePath, 0600); err != nil {
log.Printf("undo: failed to secure history file: %v", err)
}