feat: restore CHFS JWT session compatibility
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -69,20 +69,16 @@ 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)) {
|
||||
return a
|
||||
}
|
||||
if a != nil {
|
||||
s.SessionMu.Lock()
|
||||
delete(s.Sessions, c.Value)
|
||||
delete(s.SessionExpiry, c.Value)
|
||||
s.SessionMu.Unlock()
|
||||
}
|
||||
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
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
@@ -35,8 +36,10 @@ func ParseMask(s string) PermMask {
|
||||
}
|
||||
|
||||
type PathRule struct {
|
||||
Dir string
|
||||
Mask PermMask
|
||||
Dir string
|
||||
Mask PermMask
|
||||
Deny bool
|
||||
ChildrenOnly bool
|
||||
}
|
||||
type Account struct {
|
||||
Username, Password string
|
||||
@@ -91,9 +94,19 @@ 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)
|
||||
mask = r.Mask
|
||||
if r.Deny {
|
||||
mask = None
|
||||
} else {
|
||||
mask = r.Mask
|
||||
}
|
||||
}
|
||||
}
|
||||
return mask
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user