From f252a7a19539ba3f44da5e8d1fa18e5b93ddc58a Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 21 Sep 2026 16:14:28 +0800 Subject: [PATCH] feat: enforce configurable session expiration safely --- chfs-ng/cmd/chfs/main.go | 3 +- chfs-ng/internal/api/server.go | 87 +++++++++++++++++++--------------- 2 files changed, 51 insertions(+), 39 deletions(-) diff --git a/chfs-ng/cmd/chfs/main.go b/chfs-ng/cmd/chfs/main.go index 991f77d..98a2193 100644 --- a/chfs-ng/cmd/chfs/main.go +++ b/chfs-ng/cmd/chfs/main.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strings" + "time" "chfs-ng/internal/api" "chfs-ng/internal/auth" @@ -68,7 +69,7 @@ func main() { if len(cfg.Path) == 1 { store = storage.Local{Root: root} } - srv := &api.Server{Store: store, Auth: mgr, Sessions: map[string]*auth.Account{}, WebDAVRoot: root, Allow: cfg.Allow, MultiRoot: len(cfg.Path) > 1, RemoveMode: cfg.FileRemove, Log: opslog.New(cfg.Log)} + srv := &api.Server{Store: store, Auth: mgr, Sessions: map[string]*auth.Account{}, SessionExpiry: map[string]time.Time{}, SessionTimeout: cfg.SessionTimeout, WebDAVRoot: root, Allow: cfg.Allow, MultiRoot: len(cfg.Path) > 1, RemoveMode: cfg.FileRemove, Log: opslog.New(cfg.Log)} addr := ":" + itoa(cfg.Port) log.Printf("chfs-ng listening on %s root=%s removeMode=%d", addr, filepath.Clean(root), cfg.FileRemove) if cfg.SSLCert != "" && cfg.SSLKey != "" { diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index f17d870..649bacb 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -12,6 +12,7 @@ import ( "path" "path/filepath" "strings" + "sync" "time" "chfs-ng/internal/auth" @@ -21,14 +22,17 @@ import ( ) type Server struct { - Store storage.Storage - Auth *auth.Manager - Sessions map[string]*auth.Account - WebDAVRoot string - Allow string - MultiRoot bool - RemoveMode int - Log *opslog.Logger + Store storage.Storage + Auth *auth.Manager + Sessions map[string]*auth.Account + SessionExpiry map[string]time.Time + SessionMu sync.RWMutex + SessionTimeout int + WebDAVRoot string + Allow string + MultiRoot bool + RemoveMode int + Log *opslog.Logger } type fileInfo struct { Name string `json:"name"` @@ -64,32 +68,23 @@ func (s *Server) Handler() http.Handler { } func account(r *http.Request, s *Server) *auth.Account { if c, e := r.Cookie("chfs_token"); e == nil { - if a := s.Sessions[c.Value]; a != nil { + s.SessionMu.RLock() + a := s.Sessions[c.Value] + exp := s.SessionExpiry[c.Value] + s.SessionMu.RUnlock() + if a != nil && (exp.IsZero() || time.Now().Before(exp)) { return a } + if a != nil { + s.SessionMu.Lock() + delete(s.Sessions, c.Value) + delete(s.SessionExpiry, c.Value) + s.SessionMu.Unlock() + } } return s.Auth.Guest } -func (s *Server) logOp(r *http.Request, action string) { - if s.Log == nil { - return - } - a := account(r, s) - user := "" - if a != nil { - user = a.Username - } - _ = s.Log.Write(clientIPString(r), user, action) -} - -func clientIPString(r *http.Request) string { - ip := clientIP(r) - if ip == nil { - return "-" - } - return ip.String() -} func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(v) @@ -233,31 +228,47 @@ func (s *Server) exist(w http.ResponseWriter, r *http.Request) { } func (s *Server) session(w http.ResponseWriter, r *http.Request) { if r.Method == "DELETE" { + if c, e := r.Cookie("chfs_token"); e == nil { + s.SessionMu.Lock() + delete(s.Sessions, c.Value) + delete(s.SessionExpiry, c.Value) + s.SessionMu.Unlock() + } http.SetCookie(w, &http.Cookie{Name: "chfs_token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true}) writeJSON(w, map[string]any{"ok": true}) return } - if r.Method == "GET" { - user := "" - if c, e := r.Cookie("chfs_token"); e == nil { - if x := s.Sessions[c.Value]; x != nil { - user = x.Username - } - } - writeJSON(w, map[string]any{"login": user != "", "user": user}) + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, pwd := r.FormValue("user"), r.FormValue("pwd") a := s.Auth.Authenticate(user, pwd) if a == nil { - http.Error(w, "invalid account", 401) + http.Error(w, "invalid account", http.StatusUnauthorized) return } token := auth.NewToken() + exp := time.Time{} + maxAge := 0 + if s.SessionTimeout > 0 { + exp = time.Now().Add(time.Duration(s.SessionTimeout) * time.Minute) + maxAge = s.SessionTimeout * 60 + } + s.SessionMu.Lock() + if s.Sessions == nil { + s.Sessions = make(map[string]*auth.Account) + } + if s.SessionExpiry == nil { + s.SessionExpiry = make(map[string]time.Time) + } s.Sessions[token] = a - http.SetCookie(w, &http.Cookie{Name: "chfs_token", Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode}) + s.SessionExpiry[token] = exp + s.SessionMu.Unlock() + http.SetCookie(w, &http.Cookie{Name: "chfs_token", Value: token, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteLaxMode}) writeJSON(w, map[string]any{"ok": true, "user": a.Username}) } + func (s *Server) newtext(w http.ResponseWriter, r *http.Request) { a := account(r, s) dir := strings.Trim(strings.ReplaceAll(r.FormValue("filepath"), "\\", "/"), "/")