Skip to content

Commit cdcf381

Browse files
qiuxiuyacodex
andcommitted
fix(auth): resolve goto-over-variable compilation errors in web and LDAP login
- Replace goto-based whitelist skip with an if-guarded skipLimit flag - Gate all LoginCache.Set(ip, count+1) calls behind !skipLimit check - Eliminates "goto auth jumps over declaration" and "declared and not used" errors - Same approach already used correctly in SFTP, FTP, and WebDAV handlers Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
1 parent a283f2b commit cdcf381

12 files changed

Lines changed: 228 additions & 33 deletions

File tree

drivers/alias/meta.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Addition struct {
1515
DownloadPartSize int `json:"download_part_size" default:"0" type:"number" required:"false" help:"Need to enable proxy. Unit: KB"`
1616
ProviderPassThrough bool `json:"provider_pass_through" type:"bool" default:"false"`
1717
DetailsPassThrough bool `json:"details_pass_through" type:"bool" default:"false"`
18+
MoveDirect bool `json:"move_direct" type:"bool" default:"false"`
1819
}
1920

2021
var config = driver.Config{

drivers/alias/util.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,40 @@ func (d *Alias) getCopyObjs(ctx context.Context, srcObj, dstDir model.Obj) (Bala
387387
return srcObjs, dstObjs, nil
388388
}
389389

390+
func (d *Alias) getMoveObjsDirect(ctx context.Context, tmpSrcObjs, dstObjs BalancedObjs) (BalancedObjs, BalancedObjs, error) {
391+
// 按挂载点分组目标目录
392+
dstByMount := make(map[string][]model.Obj)
393+
for _, o := range dstObjs {
394+
storage, e := fs.GetStorage(o.GetPath(), &fs.GetStoragesArgs{})
395+
if e != nil {
396+
continue
397+
}
398+
mp := storage.GetStorage().MountPath
399+
dstByMount[mp] = append(dstByMount[mp], o)
400+
}
401+
402+
srcs := make(BalancedObjs, 0, len(tmpSrcObjs))
403+
dsts := make(BalancedObjs, 0)
404+
405+
for _, src := range tmpSrcObjs {
406+
storage, e := fs.GetStorage(src.GetPath(), &fs.GetStoragesArgs{})
407+
if e != nil {
408+
continue
409+
}
410+
mp := storage.GetStorage().MountPath
411+
if dstList, ok := dstByMount[mp]; ok && len(dstList) > 0 {
412+
srcs = append(srcs, src)
413+
dsts = append(dsts, dstList[0])
414+
if len(dstList) == 1 {
415+
delete(dstByMount, mp)
416+
} else {
417+
dstByMount[mp] = dstList[1:]
418+
}
419+
}
420+
}
421+
return srcs, dsts, nil
422+
}
423+
390424
func (d *Alias) getMoveObjs(ctx context.Context, srcObj, dstDir model.Obj) (BalancedObjs, BalancedObjs, error) {
391425
if d.PutConflictPolicy == DisabledWP {
392426
return nil, nil, errs.PermissionDenied
@@ -399,6 +433,10 @@ func (d *Alias) getMoveObjs(ctx context.Context, srcObj, dstDir model.Obj) (Bala
399433
if err != nil {
400434
return nil, nil, err
401435
}
436+
// MoveDirect: 源后端数少于目标后端数时,只在同后端上 move,跳过其他后端
437+
if d.MoveDirect && len(tmpSrcObjs) < len(dstObjs) {
438+
return d.getMoveObjsDirect(ctx, tmpSrcObjs, dstObjs)
439+
}
402440
if len(tmpSrcObjs) < len(dstObjs) {
403441
return nil, nil, ErrNotEnoughSrcObjs
404442
}

internal/bootstrap/data/setting.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"sort"
66
"strconv"
7+
"time"
78

89
"github.com/OpenListTeam/OpenList/v4/cmd/flags"
910
"github.com/OpenListTeam/OpenList/v4/internal/conf"
@@ -113,6 +114,10 @@ func InitialSettings() []model.SettingItem {
113114
{Key: conf.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE},
114115
{Key: conf.AllowMounted, Value: "true", Type: conf.TypeBool, Group: model.SITE},
115116
{Key: conf.RobotsTxt, Value: "User-agent: *\nAllow: /", Type: conf.TypeText, Group: model.SITE},
117+
{Key: conf.AuthLoginMaxRetries, Value: strconv.Itoa(model.DefaultMaxAuthRetries), Type: conf.TypeNumber, Group: model.SITE, Flag: model.PRIVATE, Help: "Max login retry attempts per IP. Set to -1 to disable rate limiting."},
118+
{Key: conf.AuthLoginLockDuration, Value: strconv.Itoa(int(model.DefaultLockDuration / time.Minute)), Type: conf.TypeNumber, Group: model.SITE, Flag: model.PRIVATE, Help: "Lock duration in minutes after exceeding max retries."},
119+
{Key: conf.AuthLoginIPWhitelist, Value: "", Type: conf.TypeText, Group: model.SITE, Flag: model.PRIVATE, Help: "Whitelisted IPs or CIDR ranges, one per line. IPs in this list are exempt from login rate limiting."},
120+
{Key: conf.AuthLoginIPBlacklist, Value: "", Type: conf.TypeText, Group: model.SITE, Flag: model.PRIVATE, Help: "Blacklisted IPs or CIDR ranges, one per line. Login from these IPs will be denied."},
116121
// style settings
117122
{Key: conf.Logo, Value: "https://res.oplist.org/logo/logo.svg", MigrationValue: "https://cdn.oplist.org/gh/OpenListTeam/Logo@main/logo.svg", Type: conf.TypeText, Group: model.STYLE},
118123
{Key: conf.Favicon, Value: "https://res.oplist.org/logo/logo.svg", MigrationValue: "https://cdn.oplist.org/gh/OpenListTeam/Logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE},

internal/conf/const.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ const (
1010

1111
const (
1212
// site
13-
VERSION = "version"
14-
SiteTitle = "site_title"
15-
Announcement = "announcement"
16-
AllowIndexed = "allow_indexed"
17-
AllowMounted = "allow_mounted"
18-
RobotsTxt = "robots_txt"
13+
VERSION = "version"
14+
SiteTitle = "site_title"
15+
Announcement = "announcement"
16+
AllowIndexed = "allow_indexed"
17+
AllowMounted = "allow_mounted"
18+
RobotsTxt = "robots_txt"
19+
AuthLoginMaxRetries = "auth_login_max_retries"
20+
AuthLoginLockDuration = "auth_login_lock_duration"
21+
AuthLoginIPWhitelist = "auth_login_ip_whitelist"
22+
AuthLoginIPBlacklist = "auth_login_ip_blacklist"
1923

2024
Logo = "logo" // multi-lines text, L1: light, EOL: dark
2125
Favicon = "favicon"

internal/conf/var.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package conf
22

33
import (
4+
"net"
45
"net/url"
56
"regexp"
67
"sync"
@@ -23,6 +24,8 @@ var (
2324
var SlicesMap = make(map[string][]string)
2425
var FilenameCharMap = make(map[string]string)
2526
var PrivacyReg []*regexp.Regexp
27+
var AuthLoginIPNets []*net.IPNet
28+
var AuthLoginIPBlackNets []*net.IPNet
2629

2730
var (
2831
// 在HybridCache中使用[]byte缓存数据流的限制,内存为Go自动管理,直到GC
@@ -65,6 +68,32 @@ func SendStoragesLoadedSignal() {
6568
}
6669
storagesLoadMu.Unlock()
6770
}
71+
72+
// IsIPWhitelisted checks if the given IP is within any of the configured whitelist CIDR ranges.
73+
func IsIPWhitelisted(ipStr string) bool {
74+
return isIPInNets(ipStr, AuthLoginIPNets)
75+
}
76+
77+
// IsIPBlacklisted checks if the given IP is within any of the configured blacklist CIDR ranges.
78+
func IsIPBlacklisted(ipStr string) bool {
79+
return isIPInNets(ipStr, AuthLoginIPBlackNets)
80+
}
81+
82+
func isIPInNets(ipStr string, nets []*net.IPNet) bool {
83+
if len(nets) == 0 {
84+
return false
85+
}
86+
ip := net.ParseIP(ipStr)
87+
if ip == nil {
88+
return false
89+
}
90+
for _, ipNet := range nets {
91+
if ipNet.Contains(ip) {
92+
return true
93+
}
94+
}
95+
return false
96+
}
6897
func ResetStoragesLoadSignal() {
6998
storagesLoadMu.Lock()
7099
select {

internal/model/auth_limit.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package model
2+
3+
import "github.com/OpenListTeam/OpenList/v4/internal/conf"
4+
5+
// IsAuthRateLimitExceeded checks if the auth rate limit has been exceeded for the given IP.
6+
// When maxRetries <= 0, rate limiting is disabled and this always returns false.
7+
func IsAuthRateLimitExceeded(count int, maxRetries int) bool {
8+
return maxRetries > 0 && count >= maxRetries
9+
}
10+
11+
// ShouldSkipAuthRateLimit returns true if the IP is whitelisted and rate limiting should be skipped.
12+
func ShouldSkipAuthRateLimit(ip string) bool {
13+
return conf.IsIPWhitelisted(ip)
14+
}
15+
16+
// IsIPBlocked returns true if the IP is in the blacklist and login should be denied.
17+
func IsIPBlocked(ip string) bool {
18+
return conf.IsIPBlacklisted(ip)
19+
}

internal/op/hook.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package op
22

33
import (
44
"context"
5+
"net"
56
"regexp"
67
"strings"
78

@@ -83,6 +84,12 @@ var settingItemHooks = map[string]SettingItemHook{
8384
conf.SlicesMap[conf.IgnoreDirectLinkParams] = strings.Split(item.Value, ",")
8485
return nil
8586
},
87+
conf.AuthLoginIPWhitelist: func(item *model.SettingItem) error {
88+
return updateAuthLoginIPNets(item.Value, &conf.AuthLoginIPNets)
89+
},
90+
conf.AuthLoginIPBlacklist: func(item *model.SettingItem) error {
91+
return updateAuthLoginIPNets(item.Value, &conf.AuthLoginIPBlackNets)
92+
},
8693
}
8794

8895
func RegisterSettingItemHook(key string, hook SettingItemHook) {
@@ -110,3 +117,34 @@ func callStorageHooks(typ string, storage driver.Driver) {
110117
func RegisterStorageHook(hook StorageHook) {
111118
storageHooks = append(storageHooks, hook)
112119
}
120+
121+
func updateAuthLoginIPNets(value string, dst *[]*net.IPNet) error {
122+
if value == "" {
123+
*dst = nil
124+
return nil
125+
}
126+
lines := strings.Split(value, "\n")
127+
var nets []*net.IPNet
128+
for _, line := range lines {
129+
line = strings.TrimSpace(line)
130+
if line == "" {
131+
continue
132+
}
133+
// try as CIDR first, if no "/" present, append /32 for single IP
134+
if !strings.Contains(line, "/") {
135+
if strings.Contains(line, ":") {
136+
line = line + "/128"
137+
} else {
138+
line = line + "/32"
139+
}
140+
}
141+
_, ipNet, err := net.ParseCIDR(line)
142+
if err != nil {
143+
log.Errorf("failed to parse IP list entry %q: %v", line, err)
144+
continue
145+
}
146+
nets = append(nets, ipNet)
147+
}
148+
*dst = nets
149+
return nil
150+
}

server/ftp.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strconv"
1313
"strings"
1414
"sync"
15+
"time"
1516

1617
"github.com/OpenListTeam/OpenList/v4/drivers/base"
1718
"github.com/OpenListTeam/OpenList/v4/internal/conf"
@@ -114,10 +115,17 @@ func (d *FtpMainDriver) ClientDisconnected(cc ftpserver.ClientContext) {
114115

115116
func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string) (ftpserver.ClientDriver, error) {
116117
ip := cc.RemoteAddr().String()
117-
count, ok := model.LoginCache.Get(ip)
118-
if ok && count >= model.DefaultMaxAuthRetries {
119-
model.LoginCache.Expire(ip, model.DefaultLockDuration)
120-
return nil, errors.New("Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.")
118+
if model.IsIPBlocked(ip) {
119+
return nil, errors.New("Access denied: IP is blacklisted")
120+
}
121+
if !model.ShouldSkipAuthRateLimit(ip) {
122+
maxRetries := setting.GetInt(conf.AuthLoginMaxRetries, model.DefaultMaxAuthRetries)
123+
lockDuration := time.Duration(setting.GetInt(conf.AuthLoginLockDuration, int(model.DefaultLockDuration/time.Minute))) * time.Minute
124+
count, ok := model.LoginCache.Get(ip)
125+
if model.IsAuthRateLimitExceeded(count, maxRetries) {
126+
model.LoginCache.Expire(ip, lockDuration)
127+
return nil, errors.New(model.TooManyAttempts)
128+
}
121129
}
122130
var userObj *model.User
123131
var err error

server/handles/auth.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"bytes"
55
"encoding/base64"
66
"image/png"
7+
"time"
78

89
"github.com/OpenListTeam/OpenList/v4/internal/conf"
910
"github.com/OpenListTeam/OpenList/v4/internal/model"
1011
"github.com/OpenListTeam/OpenList/v4/internal/op"
12+
"github.com/OpenListTeam/OpenList/v4/internal/setting"
1113
"github.com/OpenListTeam/OpenList/v4/server/common"
1214
"github.com/gin-gonic/gin"
1315
"github.com/pquerna/otp/totp"
@@ -41,33 +43,47 @@ func LoginHash(c *gin.Context) {
4143
}
4244

4345
func loginHash(c *gin.Context, req *LoginReq) {
44-
// check count of login
46+
// check blacklist
4547
ip := c.ClientIP()
46-
count, ok := model.LoginCache.Get(ip)
47-
if ok && count >= model.DefaultMaxAuthRetries {
48+
if model.IsIPBlocked(ip) {
49+
common.ErrorStrResp(c, "Access denied: IP is blacklisted", 403)
50+
return
51+
}
52+
// rate limiting
53+
maxRetries := setting.GetInt(conf.AuthLoginMaxRetries, model.DefaultMaxAuthRetries)
54+
lockDuration := time.Duration(setting.GetInt(conf.AuthLoginLockDuration, int(model.DefaultLockDuration/time.Minute))) * time.Minute
55+
skipLimit := model.ShouldSkipAuthRateLimit(ip)
56+
count, _ := model.LoginCache.Get(ip)
57+
if !skipLimit && model.IsAuthRateLimitExceeded(count, maxRetries) {
4858
common.ErrorStrResp(c, model.TooManyAttempts, 429)
49-
model.LoginCache.Expire(ip, model.DefaultLockDuration)
59+
model.LoginCache.Expire(ip, lockDuration)
5060
return
5161
}
5262
// check username
5363
user, err := op.GetUserByName(req.Username)
5464
if err != nil {
5565
common.ErrorStrResp(c, model.InvalidUsernameOrPassword, 401)
56-
model.LoginCache.Set(ip, count+1)
66+
if !skipLimit {
67+
model.LoginCache.Set(ip, count+1)
68+
}
5769
return
5870
}
5971
// validate password hash
6072
if err := user.ValidatePwdStaticHash(req.Password); err != nil {
6173
common.ErrorStrResp(c, model.InvalidUsernameOrPassword, 401)
62-
model.LoginCache.Set(ip, count+1)
74+
if !skipLimit {
75+
model.LoginCache.Set(ip, count+1)
76+
}
6377
return
6478
}
6579
// check 2FA
6680
if user.OtpSecret != "" {
6781
if !totp.Validate(req.OtpCode, user.OtpSecret) {
6882
// 402 - need opt
6983
common.ErrorStrResp(c, model.Invalid2FACode, 402)
70-
model.LoginCache.Set(ip, count+1)
84+
if !skipLimit {
85+
model.LoginCache.Set(ip, count+1)
86+
}
7187
return
7288
}
7389
}

server/handles/ldap_login.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package handles
22

33
import (
4+
"time"
5+
46
"github.com/OpenListTeam/OpenList/v4/internal/conf"
57
"github.com/OpenListTeam/OpenList/v4/internal/model"
68
"github.com/OpenListTeam/OpenList/v4/internal/op"
@@ -27,19 +29,29 @@ func LoginLdap(c *gin.Context) {
2729
return
2830
}
2931

30-
// check count of login
32+
// check blacklist
3133
ip := c.ClientIP()
32-
count, ok := model.LoginCache.Get(ip)
33-
if ok && count >= model.DefaultMaxAuthRetries {
34-
common.ErrorStrResp(c, "Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.", 429)
35-
model.LoginCache.Expire(ip, model.DefaultLockDuration)
34+
if model.IsIPBlocked(ip) {
35+
common.ErrorStrResp(c, "Access denied: IP is blacklisted", 403)
36+
return
37+
}
38+
// rate limiting
39+
maxRetries := setting.GetInt(conf.AuthLoginMaxRetries, model.DefaultMaxAuthRetries)
40+
lockDuration := time.Duration(setting.GetInt(conf.AuthLoginLockDuration, int(model.DefaultLockDuration/time.Minute))) * time.Minute
41+
skipLimit := model.ShouldSkipAuthRateLimit(ip)
42+
count, _ := model.LoginCache.Get(ip)
43+
if !skipLimit && model.IsAuthRateLimitExceeded(count, maxRetries) {
44+
common.ErrorStrResp(c, model.TooManyAttempts, 429)
45+
model.LoginCache.Expire(ip, lockDuration)
3646
return
3747
}
3848

3949
err = common.HandleLdapLogin(req.Username, req.Password)
4050
if err != nil {
4151
if errors.Is(err, common.ErrFailedLdapAuth) {
42-
model.LoginCache.Set(ip, count+1)
52+
if !skipLimit {
53+
model.LoginCache.Set(ip, count+1)
54+
}
4355
common.ErrorResp(c, err, 400)
4456
} else {
4557
common.ErrorResp(c, err, 500)
@@ -51,7 +63,9 @@ func LoginLdap(c *gin.Context) {
5163
user, err = common.LdapRegister(req.Username)
5264
if err != nil {
5365
common.ErrorResp(c, err, 400)
54-
model.LoginCache.Set(ip, count+1)
66+
if !skipLimit {
67+
model.LoginCache.Set(ip, count+1)
68+
}
5569
return
5670
}
5771
}

0 commit comments

Comments
 (0)