feat: restore CHFS JWT session compatibility
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user