Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions core/app/api/v2/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v2

import (
"encoding/base64"
"github.com/1Panel-dev/1Panel/core/utils/common"
"os"
"path"

Expand Down Expand Up @@ -29,12 +30,15 @@ func (b *BaseApi) Login(c *gin.Context) {
return
}

if !req.IgnoreCaptcha {
ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
if needCaptcha {
if errMsg := captcha.VerifyCode(req.CaptchaID, req.Captcha); errMsg != "" {
helper.BadAuth(c, errMsg, nil)
return
}
}

entranceItem := c.Request.Header.Get("EntranceCode")
var entrance []byte
if len(entranceItem) != 0 {
Expand All @@ -50,13 +54,18 @@ func (b *BaseApi) Login(c *gin.Context) {
user, msgKey, err := authService.Login(c, req, string(entrance))
go saveLoginLogs(c, err)
if msgKey == "ErrAuth" || msgKey == "ErrEntrance" {
if msgKey == "ErrAuth" {
global.IPTracker.SetNeedCaptcha(ip)
}
helper.BadAuth(c, msgKey, err)
return
}
if err != nil {
global.IPTracker.SetNeedCaptcha(ip)
helper.InternalServer(c, err)
return
}
global.IPTracker.Clear(ip)
helper.SuccessWithData(c, user)
}

Expand Down Expand Up @@ -151,15 +160,18 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) {
helper.InternalServer(c, err)
return
}
ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
res := &dto.LoginSetting{
IsDemo: global.CONF.Base.IsDemo,
IsIntl: global.CONF.Base.IsIntl,
IsFxplay: global.CONF.Base.IsFxplay,
IsOffLine: global.CONF.Base.IsOffLine,
Language: settingInfo.Language,
MenuTabs: settingInfo.MenuTabs,
PanelName: settingInfo.PanelName,
Theme: settingInfo.Theme,
IsDemo: global.CONF.Base.IsDemo,
IsIntl: global.CONF.Base.IsIntl,
IsFxplay: global.CONF.Base.IsFxplay,
IsOffLine: global.CONF.Base.IsOffLine,
Language: settingInfo.Language,
MenuTabs: settingInfo.MenuTabs,
PanelName: settingInfo.PanelName,
Theme: settingInfo.Theme,
NeedCaptcha: needCaptcha,
}
helper.SuccessWithData(c, res)
}
Expand Down
11 changes: 5 additions & 6 deletions core/app/dto/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ type MfaCredential struct {
}

type Login struct {
Name string `json:"name" validate:"required"`
Password string `json:"password" validate:"required"`
IgnoreCaptcha bool `json:"ignoreCaptcha"`
Captcha string `json:"captcha"`
CaptchaID string `json:"captchaID"`
Language string `json:"language" validate:"required,oneof=zh en 'zh-Hant' ko ja ru ms 'pt-BR' tr 'es-ES'"`
Name string `json:"name" validate:"required"`
Password string `json:"password" validate:"required"`
Captcha string `json:"captcha"`
CaptchaID string `json:"captchaID"`
Language string `json:"language" validate:"required,oneof=zh en 'zh-Hant' ko ja ru ms 'pt-BR' tr 'es-ES'"`
}

type MFALogin struct {
Expand Down
17 changes: 9 additions & 8 deletions core/app/dto/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,13 @@ type AppstoreConfig struct {
}

type LoginSetting struct {
IsDemo bool `json:"isDemo"`
IsIntl bool `json:"isIntl"`
IsOffLine bool `json:"isOffLine"`
IsFxplay bool `json:"isFxplay"`
Language string `json:"language"`
MenuTabs string `json:"menuTabs"`
PanelName string `json:"panelName"`
Theme string `json:"theme"`
IsDemo bool `json:"isDemo"`
IsIntl bool `json:"isIntl"`
IsOffLine bool `json:"isOffLine"`
IsFxplay bool `json:"isFxplay"`
Language string `json:"language"`
MenuTabs string `json:"menuTabs"`
PanelName string `json:"panelName"`
Theme string `json:"theme"`
NeedCaptcha bool `json:"needCaptcha"`
}
3 changes: 1 addition & 2 deletions core/app/service/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package service
import (
"crypto/hmac"
"encoding/base64"
"strconv"

"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/1Panel-dev/1Panel/core/buserr"
Expand All @@ -13,6 +11,7 @@ import (
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
"github.com/1Panel-dev/1Panel/core/utils/mfa"
"github.com/gin-gonic/gin"
"strconv"
)

type AuthService struct{}
Expand Down
3 changes: 3 additions & 0 deletions core/global/global.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package global

import (
"github.com/1Panel-dev/1Panel/core/init/auth"
"github.com/1Panel-dev/1Panel/core/init/session/psession"
"github.com/go-playground/validator/v10"
"github.com/nicksnyder/go-i18n/v2/i18n"
Expand Down Expand Up @@ -28,6 +29,8 @@ var (
Cron *cron.Cron

ScriptSyncJobID cron.EntryID

IPTracker *auth.IPTracker
)

type DBOption func(*gorm.DB) *gorm.DB
99 changes: 99 additions & 0 deletions core/init/auth/ip_tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package auth

import (
"sync"
"time"
)

const (
MaxIPCount = 100
ExpireDuration = 30 * time.Minute
)

type IPRecord struct {
NeedCaptcha bool
LastUpdate time.Time
}

type IPTracker struct {
records map[string]*IPRecord
ipOrder []string
mu sync.RWMutex
}

func NewIPTracker() *IPTracker {
return &IPTracker{
records: make(map[string]*IPRecord),
ipOrder: make([]string, 0),
}
}

func (t *IPTracker) NeedCaptcha(ip string) bool {
t.mu.Lock()
defer t.mu.Unlock()

record, exists := t.records[ip]
if !exists {
return false
}

if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
return false
}

return record.NeedCaptcha
}

func (t *IPTracker) SetNeedCaptcha(ip string) {
t.mu.Lock()
defer t.mu.Unlock()

if record, exists := t.records[ip]; exists {
if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
} else {
record.NeedCaptcha = true
record.LastUpdate = time.Now()
return
}
}

if len(t.records) >= MaxIPCount {
t.removeOldestUnsafe()
}

t.records[ip] = &IPRecord{
NeedCaptcha: true,
LastUpdate: time.Now(),
}
t.ipOrder = append(t.ipOrder, ip)
}

func (t *IPTracker) Clear(ip string) {
t.mu.Lock()
defer t.mu.Unlock()

t.removeIPUnsafe(ip)
}

func (t *IPTracker) removeIPUnsafe(ip string) {
delete(t.records, ip)

for i, storedIP := range t.ipOrder {
if storedIP == ip {
t.ipOrder = append(t.ipOrder[:i], t.ipOrder[i+1:]...)
break
}
}
}

func (t *IPTracker) removeOldestUnsafe() {
if len(t.ipOrder) == 0 {
return
}

oldestIP := t.ipOrder[0]
delete(t.records, oldestIP)
t.ipOrder = t.ipOrder[1:]
}
3 changes: 3 additions & 0 deletions core/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/tls"
"encoding/gob"
"fmt"
"github.com/1Panel-dev/1Panel/core/init/auth"
"net"
"net/http"
"os"
Expand Down Expand Up @@ -54,6 +55,8 @@ func Start() {
gin.SetMode(gin.ReleaseMode)
}

global.IPTracker = auth.NewIPTracker()

tcpItem := "tcp4"
if global.CONF.Conn.Ipv6 == constant.StatusEnable {
tcpItem = "tcp"
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/interface/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export namespace Login {
export interface ReqLoginForm {
name: string;
password: string;
ignoreCaptcha: boolean;
captcha: string;
captchaID: string;
authMethod: string;
Expand Down Expand Up @@ -36,5 +35,6 @@ export namespace Login {
panelName: string;
theme: string;
isOffLine: boolean;
needCaptcha: boolean;
}
}
3 changes: 1 addition & 2 deletions frontend/src/views/login/components/login-form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@ const loginFormRef = ref<FormInstance>();
const loginForm = reactive({
name: '',
password: '',
ignoreCaptcha: true,
captcha: '',
captchaID: '',
authMethod: 'session',
Expand Down Expand Up @@ -318,7 +317,6 @@ const login = (formEl: FormInstance | undefined) => {
let requestLoginForm = {
name: loginForm.name,
password: encryptPassword(loginForm.password),
ignoreCaptcha: globalStore.ignoreCaptcha,
captcha: loginForm.captcha,
captchaID: captcha.captchaID,
authMethod: 'session',
Expand Down Expand Up @@ -418,6 +416,7 @@ const getSetting = async () => {
isFxplay.value = res.data.isFxplay;
globalStore.isFxplay = isFxplay.value;
globalStore.isOffLine = res.data.isOffLine;
globalStore.ignoreCaptcha = !res.data.needCaptcha;

document.title = res.data.panelName;
i18n.warnHtmlMessage = false;
Expand Down
Loading