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 {
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)
store := storage.Storage(multi)
if len(cfg.Path) == 1 {
+1
View File
@@ -5,6 +5,7 @@ go 1.22
require (
github.com/Bios-Marcel/wastebasket v0.0.3
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
+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-webdav v0.7.0 h1:cp6aBWXBf8Sjzguka9VJarr4XTkGc2IHxXI1Gq3TKpA=
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=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
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)
}
func account(r *http.Request, s *Server) *auth.Account {
if c, e := r.Cookie("chfs_token"); e == 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)) {
c, err := r.Cookie("JWT")
if err != nil {
return s.Auth.Guest
}
username, err := auth.GetAccFromToken(c.Value)
if err != nil {
return s.Auth.Guest
}
if a, ok := s.Auth.Accounts[username]; ok {
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
}
@@ -256,15 +252,20 @@ func (s *Server) exist(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"exist": e == nil})
}
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()
if r.Method == http.MethodDelete {
jwtValue, _ := r.Cookie("JWT")
userValue, _ := r.Cookie("user")
jwt := ""
user := ""
if jwtValue != nil {
jwt = jwtValue.Value
}
http.SetCookie(w, &http.Cookie{Name: "chfs_token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true})
writeJSON(w, map[string]any{"ok": true})
if userValue != nil {
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
}
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")
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
token, err := auth.NewToken(a.Username)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "failed to create token")
return
}
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
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})
expires := time.Now().Add(365 * 24 * time.Hour)
http.SetCookie(w, &http.Cookie{Name: "JWT", Value: token, Path: "/", Expires: expires, HttpOnly: true})
http.SetCookie(w, &http.Cookie{Name: "user", Value: a.Username, Path: "/", Expires: expires})
w.WriteHeader(http.StatusCreated)
}
func (s *Server) newtext(w http.ResponseWriter, r *http.Request) {
+59 -10
View File
@@ -1,10 +1,11 @@
package auth
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
jwt "github.com/golang-jwt/jwt/v4"
)
type PermMask uint8
@@ -37,6 +38,8 @@ func ParseMask(s string) PermMask {
type PathRule struct {
Dir string
Mask PermMask
Deny bool
ChildrenOnly bool
}
type Account struct {
Username, Password string
@@ -91,11 +94,21 @@ func (a *Account) MaskFor(path string) PermMask {
mask := a.Mask
for _, r := range a.Rules {
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)
if r.Deny {
mask = None
} else {
mask = r.Mask
}
}
}
return mask
}
func matchPath(pattern, path string) bool {
@@ -143,13 +156,49 @@ func matchSegment(pattern, value string) bool {
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 NewToken() string {
b := make([]byte, 32)
if _, e := rand.Read(b); e != nil {
h := sha256.Sum256([]byte("fallback"))
return hex.EncodeToString(h[:])
const jwtSecret = "_pengyichen2015_"
type jwtClaims struct {
Acc string `json:"acc"`
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"
)
type AccountConfig struct {
Username string
Password string
Default string
None []string
Read []string
Write []string
Delete []string
}
type Config struct {
Path []string
Port int
@@ -22,6 +32,7 @@ type Config struct {
HTMLNotice string
SSLCert string
SSLKey string
Accounts []AccountConfig
}
func Default() Config {
@@ -36,17 +47,45 @@ func Load(path string) (Config, error) {
}
defer f.Close()
s := bufio.NewScanner(f)
var section *AccountConfig
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
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, '=')
if i < 0 {
continue
}
key, val := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:])
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) {
case "path":
c.Path = splitPath(val)
@@ -126,3 +165,13 @@ func (c Config) Validate() error {
}
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
}