|
@@ -1,12 +1,12 @@
|
|
|
package controller
|
|
package controller
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
- "fmt"
|
|
|
|
|
"net/http"
|
|
"net/http"
|
|
|
"text/template"
|
|
"text/template"
|
|
|
"time"
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v2/web/middleware"
|
|
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
|
|
"github.com/mhsanaei/3x-ui/v2/web/session"
|
|
"github.com/mhsanaei/3x-ui/v2/web/session"
|
|
|
|
|
|
|
@@ -41,8 +41,8 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
|
|
g.GET("/", a.index)
|
|
g.GET("/", a.index)
|
|
|
g.GET("/logout", a.logout)
|
|
g.GET("/logout", a.logout)
|
|
|
|
|
|
|
|
- g.POST("/login", a.login)
|
|
|
|
|
- g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
|
|
|
|
|
|
+ g.POST("/login", middleware.CSRFMiddleware(), a.login)
|
|
|
|
|
+ g.POST("/getTwoFactorEnable", middleware.CSRFMiddleware(), a.getTwoFactorEnable)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
|
|
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
|
|
@@ -71,28 +71,51 @@ func (a *IndexController) login(c *gin.Context) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
|
|
|
|
- timeStr := time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
|
|
|
+ remoteIP := getRemoteIp(c)
|
|
|
safeUser := template.HTMLEscapeString(form.Username)
|
|
safeUser := template.HTMLEscapeString(form.Username)
|
|
|
- safePass := template.HTMLEscapeString(form.Password)
|
|
|
|
|
-
|
|
|
|
|
- if user == nil {
|
|
|
|
|
- logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
|
|
|
|
|
|
|
+ timeStr := time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
|
+ if blockedUntil, ok := defaultLoginLimiter.allow(remoteIP, form.Username); !ok {
|
|
|
|
|
+ reason := "too many failed attempts"
|
|
|
|
|
+ logger.Warningf("failed login: username=%q, IP=%q, reason=%q, blocked_until=%s", safeUser, remoteIP, reason, blockedUntil.Format(time.RFC3339))
|
|
|
|
|
+ a.tgbot.UserLoginNotify(service.LoginAttempt{
|
|
|
|
|
+ Username: safeUser,
|
|
|
|
|
+ IP: remoteIP,
|
|
|
|
|
+ Time: timeStr,
|
|
|
|
|
+ Status: service.LoginFail,
|
|
|
|
|
+ Reason: reason,
|
|
|
|
|
+ })
|
|
|
|
|
+ pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- notifyPass := safePass
|
|
|
|
|
|
|
+ user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
|
|
|
|
|
|
|
- if checkErr != nil && checkErr.Error() == "invalid 2fa code" {
|
|
|
|
|
- translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed")
|
|
|
|
|
- notifyPass = fmt.Sprintf("*** (%s)", translatedError)
|
|
|
|
|
|
|
+ if user == nil {
|
|
|
|
|
+ reason := loginFailureReason(checkErr)
|
|
|
|
|
+ if blockedUntil, blocked := defaultLoginLimiter.registerFailure(remoteIP, form.Username); blocked {
|
|
|
|
|
+ logger.Warningf("failed login: username=%q, IP=%q, reason=%q, blocked_until=%s", safeUser, remoteIP, reason, blockedUntil.Format(time.RFC3339))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ logger.Warningf("failed login: username=%q, IP=%q, reason=%q", safeUser, remoteIP, reason)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0)
|
|
|
|
|
|
|
+ a.tgbot.UserLoginNotify(service.LoginAttempt{
|
|
|
|
|
+ Username: safeUser,
|
|
|
|
|
+ IP: remoteIP,
|
|
|
|
|
+ Time: timeStr,
|
|
|
|
|
+ Status: service.LoginFail,
|
|
|
|
|
+ Reason: reason,
|
|
|
|
|
+ })
|
|
|
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
|
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, getRemoteIp(c))
|
|
|
|
|
- a.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)
|
|
|
|
|
|
|
+ defaultLoginLimiter.registerSuccess(remoteIP, form.Username)
|
|
|
|
|
+ logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, remoteIP)
|
|
|
|
|
+ a.tgbot.UserLoginNotify(service.LoginAttempt{
|
|
|
|
|
+ Username: safeUser,
|
|
|
|
|
+ IP: remoteIP,
|
|
|
|
|
+ Time: timeStr,
|
|
|
|
|
+ Status: service.LoginSuccess,
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
if err := session.SetLoginUser(c, user); err != nil {
|
|
if err := session.SetLoginUser(c, user); err != nil {
|
|
|
logger.Warning("Unable to save session:", err)
|
|
logger.Warning("Unable to save session:", err)
|
|
@@ -103,6 +126,13 @@ func (a *IndexController) login(c *gin.Context) {
|
|
|
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
|
|
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+func loginFailureReason(err error) string {
|
|
|
|
|
+ if err != nil && err.Error() == "invalid 2fa code" {
|
|
|
|
|
+ return "invalid 2FA code"
|
|
|
|
|
+ }
|
|
|
|
|
+ return "invalid credentials"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// logout handles user logout by clearing the session and redirecting to the login page.
|
|
// logout handles user logout by clearing the session and redirecting to the login page.
|
|
|
func (a *IndexController) logout(c *gin.Context) {
|
|
func (a *IndexController) logout(c *gin.Context) {
|
|
|
user := session.GetLoginUser(c)
|
|
user := session.GetLoginUser(c)
|