feat: restore CHFS JWT session compatibility

This commit is contained in:
OpenClaw Agent
2026-09-21 17:36:21 +08:00
parent a7192fd96e
commit d771c3c521
6 changed files with 165 additions and 53 deletions
+20
View File
@@ -64,6 +64,26 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for _, ac := range cfg.Accounts {
a := &auth.Account{Username: ac.Username, Password: ac.Password, Mask: auth.ParseMask(ac.Default)}
for _, d := range ac.None {
a.Rules = append(a.Rules, auth.PathRule{Dir: d, Deny: true})
}
for _, d := range ac.Read {
a.Rules = append(a.Rules, auth.PathRule{Dir: d, Mask: auth.Read, ChildrenOnly: true})
}
for _, d := range ac.Write {
a.Rules = append(a.Rules, auth.PathRule{Dir: d, Mask: auth.Write, ChildrenOnly: true})
}
for _, d := range ac.Delete {
a.Rules = append(a.Rules, auth.PathRule{Dir: d, Mask: auth.Delete, ChildrenOnly: true})
}
if ac.Username == "guest" {
mgr.Guest = a
} else {
mgr.Accounts[ac.Username] = a
}
}
multi := storage.NewMulti(cfg.Path) multi := storage.NewMulti(cfg.Path)
store := storage.Storage(multi) store := storage.Storage(multi)
if len(cfg.Path) == 1 { if len(cfg.Path) == 1 {
+1
View File
@@ -5,6 +5,7 @@ go 1.22
require ( require (
github.com/Bios-Marcel/wastebasket v0.0.3 github.com/Bios-Marcel/wastebasket v0.0.3
github.com/emersion/go-webdav v0.7.0 github.com/emersion/go-webdav v0.7.0
github.com/golang-jwt/jwt/v4 v4.5.2
) )
require golang.org/x/sys v0.3.0 // indirect require golang.org/x/sys v0.3.0 // indirect
+2
View File
@@ -4,6 +4,8 @@ github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegN
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-webdav v0.7.0 h1:cp6aBWXBf8Sjzguka9VJarr4XTkGc2IHxXI1Gq3TKpA= github.com/emersion/go-webdav v0.7.0 h1:cp6aBWXBf8Sjzguka9VJarr4XTkGc2IHxXI1Gq3TKpA=
github.com/emersion/go-webdav v0.7.0/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= github.com/emersion/go-webdav v0.7.0/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4= github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+30 -39
View File
@@ -69,21 +69,17 @@ func (s *Server) Handler() http.Handler {
return s.ipFilter(mux) return s.ipFilter(mux)
} }
func account(r *http.Request, s *Server) *auth.Account { func account(r *http.Request, s *Server) *auth.Account {
if c, e := r.Cookie("chfs_token"); e == nil { c, err := r.Cookie("JWT")
s.SessionMu.RLock() if err != nil {
a := s.Sessions[c.Value] return s.Auth.Guest
exp := s.SessionExpiry[c.Value] }
s.SessionMu.RUnlock() username, err := auth.GetAccFromToken(c.Value)
if a != nil && (exp.IsZero() || time.Now().Before(exp)) { if err != nil {
return s.Auth.Guest
}
if a, ok := s.Auth.Accounts[username]; ok {
return a 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 return s.Auth.Guest
} }
@@ -256,15 +252,20 @@ func (s *Server) exist(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"exist": e == nil}) writeJSON(w, map[string]any{"exist": e == nil})
} }
func (s *Server) session(w http.ResponseWriter, r *http.Request) { func (s *Server) session(w http.ResponseWriter, r *http.Request) {
if r.Method == "DELETE" { if r.Method == http.MethodDelete {
if c, e := r.Cookie("chfs_token"); e == nil { jwtValue, _ := r.Cookie("JWT")
s.SessionMu.Lock() userValue, _ := r.Cookie("user")
delete(s.Sessions, c.Value) jwt := ""
delete(s.SessionExpiry, c.Value) user := ""
s.SessionMu.Unlock() if jwtValue != nil {
jwt = jwtValue.Value
} }
http.SetCookie(w, &http.Cookie{Name: "chfs_token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true}) if userValue != nil {
writeJSON(w, map[string]any{"ok": true}) user = userValue.Value
}
http.SetCookie(w, &http.Cookie{Name: "JWT", Value: jwt, Path: "/", MaxAge: -1})
http.SetCookie(w, &http.Cookie{Name: "user", Value: user, Path: "/", MaxAge: -1})
w.WriteHeader(http.StatusNoContent)
return return
} }
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
@@ -281,25 +282,15 @@ func (s *Server) session(w http.ResponseWriter, r *http.Request) {
writeJSONError(w, http.StatusNotFound, "user or password is wrong") writeJSONError(w, http.StatusNotFound, "user or password is wrong")
return return
} }
token := auth.NewToken() token, err := auth.NewToken(a.Username)
exp := time.Time{} if err != nil {
maxAge := 0 writeJSONError(w, http.StatusInternalServerError, "failed to create token")
if s.SessionTimeout > 0 { return
exp = time.Now().Add(time.Duration(s.SessionTimeout) * time.Minute)
maxAge = s.SessionTimeout * 60
} }
s.SessionMu.Lock() expires := time.Now().Add(365 * 24 * time.Hour)
if s.Sessions == nil { http.SetCookie(w, &http.Cookie{Name: "JWT", Value: token, Path: "/", Expires: expires, HttpOnly: true})
s.Sessions = make(map[string]*auth.Account) http.SetCookie(w, &http.Cookie{Name: "user", Value: a.Username, Path: "/", Expires: expires})
} w.WriteHeader(http.StatusCreated)
if s.SessionExpiry == nil {
s.SessionExpiry = make(map[string]time.Time)
}
s.Sessions[token] = a
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) { func (s *Server) newtext(w http.ResponseWriter, r *http.Request) {
+59 -10
View File
@@ -1,10 +1,11 @@
package auth package auth
import ( import (
"crypto/rand" "fmt"
"crypto/sha256"
"encoding/hex"
"strings" "strings"
"time"
jwt "github.com/golang-jwt/jwt/v4"
) )
type PermMask uint8 type PermMask uint8
@@ -37,6 +38,8 @@ func ParseMask(s string) PermMask {
type PathRule struct { type PathRule struct {
Dir string Dir string
Mask PermMask Mask PermMask
Deny bool
ChildrenOnly bool
} }
type Account struct { type Account struct {
Username, Password string Username, Password string
@@ -91,11 +94,21 @@ func (a *Account) MaskFor(path string) PermMask {
mask := a.Mask mask := a.Mask
for _, r := range a.Rules { for _, r := range a.Rules {
d := strings.Trim(r.Dir, "/") d := strings.Trim(r.Dir, "/")
if matchPath(d, path) && len(d) > best { if !matchPath(d, path) {
continue
}
if r.ChildrenOnly && matchPathExact(d, path) {
continue
}
if len(d) > best {
best = len(d) best = len(d)
if r.Deny {
mask = None
} else {
mask = r.Mask mask = r.Mask
} }
} }
}
return mask return mask
} }
func matchPath(pattern, path string) bool { func matchPath(pattern, path string) bool {
@@ -143,13 +156,49 @@ func matchSegment(pattern, value string) bool {
return dp[len(p)][len(v)] return dp[len(p)][len(v)]
} }
func matchPathExact(pattern, path string) bool {
pp := strings.Split(strings.Trim(pattern, "/"), "/")
ps := strings.Split(strings.Trim(path, "/"), "/")
if len(pp) != len(ps) {
return false
}
return matchPath(pattern, path)
}
func MatchPath(pattern, path string) bool { return matchPath(pattern, path) } func MatchPath(pattern, path string) bool { return matchPath(pattern, path) }
func NewToken() string { const jwtSecret = "_pengyichen2015_"
b := make([]byte, 32)
if _, e := rand.Read(b); e != nil { type jwtClaims struct {
h := sha256.Sum256([]byte("fallback")) Acc string `json:"acc"`
return hex.EncodeToString(h[:]) jwt.StandardClaims
} }
return hex.EncodeToString(b)
func NewToken(username string) (string, error) {
claims := jwtClaims{Acc: username, StandardClaims: jwt.StandardClaims{ExpiresAt: time.Now().Add(365 * 24 * time.Hour).Unix()}}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jwtSecret))
}
func GetAccFromToken(raw string) (string, error) {
if raw == "" {
return "", fmt.Errorf("empty token")
}
var claims jwtClaims
token, err := jwt.ParseWithClaims(raw, &claims, func(t *jwt.Token) (interface{}, error) {
if t.Method != jwt.SigningMethodHS256 {
return nil, fmt.Errorf("unexpected signing method: %s", t.Method.Alg())
}
return []byte(jwtSecret), nil
})
if err != nil || token == nil || !token.Valid {
if err == nil {
err = fmt.Errorf("invalid token")
}
return "", err
}
if claims.Acc == "" {
return "", fmt.Errorf("token missing account")
}
return claims.Acc, nil
} }
+49
View File
@@ -8,6 +8,16 @@ import (
"strings" "strings"
) )
type AccountConfig struct {
Username string
Password string
Default string
None []string
Read []string
Write []string
Delete []string
}
type Config struct { type Config struct {
Path []string Path []string
Port int Port int
@@ -22,6 +32,7 @@ type Config struct {
HTMLNotice string HTMLNotice string
SSLCert string SSLCert string
SSLKey string SSLKey string
Accounts []AccountConfig
} }
func Default() Config { func Default() Config {
@@ -36,17 +47,45 @@ func Load(path string) (Config, error) {
} }
defer f.Close() defer f.Close()
s := bufio.NewScanner(f) s := bufio.NewScanner(f)
var section *AccountConfig
for s.Scan() { for s.Scan() {
line := strings.TrimSpace(s.Text()) line := strings.TrimSpace(s.Text())
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") { if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue continue
} }
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
name := strings.TrimSpace(line[1 : len(line)-1])
if name != "" {
c.Accounts = append(c.Accounts, AccountConfig{Username: name})
section = &c.Accounts[len(c.Accounts)-1]
} else {
section = nil
}
continue
}
i := strings.IndexByte(line, '=') i := strings.IndexByte(line, '=')
if i < 0 { if i < 0 {
continue continue
} }
key, val := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:]) key, val := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:])
val = strings.Trim(val, "\\\"") val = strings.Trim(val, "\\\"")
if section != nil {
switch strings.ToLower(key) {
case "password":
section.Password = val
case "rule.default":
section.Default = val
case "rule.none":
section.None = appendNonEmpty(section.None, val)
case "rule.r":
section.Read = appendNonEmpty(section.Read, val)
case "rule.w":
section.Write = appendNonEmpty(section.Write, val)
case "rule.d":
section.Delete = appendNonEmpty(section.Delete, val)
}
continue
}
switch strings.ToLower(key) { switch strings.ToLower(key) {
case "path": case "path":
c.Path = splitPath(val) c.Path = splitPath(val)
@@ -126,3 +165,13 @@ func (c Config) Validate() error {
} }
return nil return nil
} }
func appendNonEmpty(dst []string, value string) []string {
for _, item := range strings.Split(value, "|") {
item = strings.TrimSpace(item)
if item != "" {
dst = append(dst, item)
}
}
return dst
}