From 2ba17d89ef2197cb9c50a2715b724c6f210ebeed Mon Sep 17 00:00:00 2001 From: zhengkunwang <31820853+zhengkunwang223@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:31:07 +0800 Subject: [PATCH] feat(system-security): Optimized unauthenticated settings to enhance system security (#7142) --- backend/app/api/v1/auth.go | 38 ++---- backend/app/service/auth.go | 53 ++++++--- backend/constant/common.go | 84 ++++++++++++++ backend/init/router/router.go | 122 +++++++++++++++++++- backend/router/ro_base.go | 1 - cmd/server/res/error_msg.go | 6 + cmd/server/res/html/200.html | 55 +++++++++ cmd/server/res/html/200_en.html | 55 +++++++++ cmd/server/res/html/400.html | 7 ++ cmd/server/res/html/401.html | 7 ++ cmd/server/res/html/403.html | 7 ++ cmd/server/res/html/404.html | 7 ++ cmd/server/res/html/408.html | 7 ++ cmd/server/res/html/416.html | 7 ++ cmd/server/res/html/500.html | 7 ++ frontend/src/api/modules/auth.ts | 4 - frontend/src/routers/modules/toolbox.ts | 2 +- frontend/src/views/login/entrance/index.vue | 30 ++--- frontend/src/views/login/index.vue | 20 ---- 19 files changed, 426 insertions(+), 93 deletions(-) create mode 100644 cmd/server/res/error_msg.go create mode 100644 cmd/server/res/html/200.html create mode 100644 cmd/server/res/html/200_en.html create mode 100644 cmd/server/res/html/400.html create mode 100644 cmd/server/res/html/401.html create mode 100644 cmd/server/res/html/403.html create mode 100644 cmd/server/res/html/404.html create mode 100644 cmd/server/res/html/408.html create mode 100644 cmd/server/res/html/416.html create mode 100644 cmd/server/res/html/500.html diff --git a/backend/app/api/v1/auth.go b/backend/app/api/v1/auth.go index 435c36a7c..16b1feede 100644 --- a/backend/app/api/v1/auth.go +++ b/backend/app/api/v1/auth.go @@ -2,14 +2,11 @@ package v1 import ( "encoding/base64" - "net/http" - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/middleware" "github.com/1Panel-dev/1Panel/backend/utils/captcha" "github.com/1Panel-dev/1Panel/backend/utils/qqwry" "github.com/gin-gonic/gin" @@ -37,11 +34,18 @@ func (b *BaseApi) Login(c *gin.Context) { return } } + entranceItem := c.Request.Header.Get("EntranceCode") var entrance []byte if len(entranceItem) != 0 { entrance, _ = base64.StdEncoding.DecodeString(entranceItem) } + if len(entrance) == 0 { + cookieValue, err := c.Cookie("SecurityEntrance") + if err == nil { + entrance, _ = base64.StdEncoding.DecodeString(cookieValue) + } + } user, err := authService.Login(c, req, string(entrance)) go saveLoginLogs(c, err) @@ -108,34 +112,6 @@ func (b *BaseApi) Captcha(c *gin.Context) { helper.SuccessWithData(c, captcha) } -// @Tags Auth -// @Summary Load safety status -// @Description 获取系统安全登录状态 -// @Success 200 -// @Router /auth/issafety [get] -func (b *BaseApi) CheckIsSafety(c *gin.Context) { - code := c.DefaultQuery("code", "") - status, err := authService.CheckIsSafety(code) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - if status == "disable" && len(code) != 0 { - helper.ErrResponse(c, http.StatusNotFound) - return - } - if status == "unpass" { - code := middleware.LoadErrCode() - if code != 200 { - helper.ErrResponse(c, code) - return - } - helper.ErrorWithDetail(c, constant.CodeErrEntrance, constant.ErrTypeInternalServer, nil) - return - } - helper.SuccessWithOutData(c) -} - func (b *BaseApi) GetResponsePage(c *gin.Context) { pageCode, err := authService.GetResponsePage() if err != nil { diff --git a/backend/app/service/auth.go b/backend/app/service/auth.go index 118ffd982..2e3a2c9e0 100644 --- a/backend/app/service/auth.go +++ b/backend/app/service/auth.go @@ -2,6 +2,7 @@ package service import ( "crypto/hmac" + "encoding/base64" "strconv" "github.com/1Panel-dev/1Panel/backend/app/dto" @@ -19,12 +20,13 @@ import ( type AuthService struct{} type IAuthService interface { - CheckIsSafety(code string) (string, error) GetResponsePage() (string, error) VerifyCode(code string) (bool, error) Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, error) LogOut(c *gin.Context) error MFALogin(c *gin.Context, info dto.MFALogin, entrance string) (*dto.UserLoginInfo, error) + GetSecurityEntrance() string + IsLogin(c *gin.Context) bool } func NewIAuthService() IAuthService { @@ -64,7 +66,16 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*d if mfa.Value == "enable" { return &dto.UserLoginInfo{Name: nameSetting.Value, MfaStatus: mfa.Value}, nil } - return u.generateSession(c, info.Name, info.AuthMethod) + + loginUser, err := u.generateSession(c, info.Name, info.AuthMethod) + if err != nil { + return nil, err + } + if entrance != "" { + entranceValue := base64.StdEncoding.EncodeToString([]byte(entrance)) + c.SetCookie("SecurityEntrance", entranceValue, 0, "", "", false, true) + } + return loginUser, nil } func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance string) (*dto.UserLoginInfo, error) { @@ -103,7 +114,15 @@ func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance strin return nil, constant.ErrAuth } - return u.generateSession(c, info.Name, info.AuthMethod) + loginUser, err := u.generateSession(c, info.Name, info.AuthMethod) + if err != nil { + return nil, err + } + if entrance != "" { + entranceValue := base64.StdEncoding.EncodeToString([]byte(entrance)) + c.SetCookie("SecurityEntrance", entranceValue, 0, "", "", false, true) + } + return loginUser, nil } func (u *AuthService) generateSession(c *gin.Context, name, authMethod string) (*dto.UserLoginInfo, error) { @@ -173,24 +192,30 @@ func (u *AuthService) VerifyCode(code string) (bool, error) { return hmac.Equal([]byte(setting.Value), []byte(code)), nil } -func (u *AuthService) CheckIsSafety(code string) (string, error) { - status, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance")) +func (u *AuthService) GetResponsePage() (string, error) { + pageCode, err := settingRepo.Get(settingRepo.WithByKey("NoAuthSetting")) if err != nil { return "", err } - if len(status.Value) == 0 { - return "disable", nil + return pageCode.Value, nil +} + +func (u *AuthService) GetSecurityEntrance() string { + status, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance")) + if err != nil { + return "" } - if status.Value == code { - return "pass", nil + if len(status.Value) == 0 { + return "" } - return "unpass", nil + return status.Value } -func (u *AuthService) GetResponsePage() (string, error) { - pageCode, err := settingRepo.Get(settingRepo.WithByKey("NoAuthSetting")) +func (u *AuthService) IsLogin(c *gin.Context) bool { + sID, _ := c.Cookie(constant.SessionName) + _, err := global.SESSION.Get(sID) if err != nil { - return "", err + return false } - return pageCode.Value, nil + return true } diff --git a/backend/constant/common.go b/backend/constant/common.go index cca64e21a..6aa330985 100644 --- a/backend/constant/common.go +++ b/backend/constant/common.go @@ -23,3 +23,87 @@ const ( DateTimeLayout = "2006-01-02 15:04:05" // or use time.DateTime while go version >= 1.20 DateTimeSlimLayout = "20060102150405" ) + +var WebUrlMap = map[string]struct{}{ + "/apps": {}, + "/apps/all": {}, + "/apps/installed": {}, + "/apps/upgrade": {}, + + "/containers": {}, + "/containers/container": {}, + "/containers/image": {}, + "/containers/network": {}, + "/containers/volume": {}, + "/containers/repo": {}, + "/containers/compose": {}, + "/containers/template": {}, + "/containers/setting": {}, + + "/cronjobs": {}, + + "/databases": {}, + "/databases/mysql": {}, + "/databases/mysql/remote": {}, + "/databases/postgresql": {}, + "/databases/postgresql/remote": {}, + "/databases/redis": {}, + "/databases/redis/remote": {}, + + "/hosts": {}, + "/hosts/files": {}, + "/hosts/monitor/monitor": {}, + "/hosts/monitor/setting": {}, + "/hosts/terminal": {}, + "/hosts/firewall/port": {}, + "/hosts/firewall/forward": {}, + "/hosts/firewall/ip": {}, + "/hosts/process/process": {}, + "/hosts/process/network": {}, + "/hosts/ssh/ssh": {}, + "/hosts/ssh/log": {}, + "/hosts/ssh/session": {}, + + "/logs": {}, + "/logs/operation": {}, + "/logs/login": {}, + "/logs/website": {}, + "/logs/system": {}, + "/logs/ssh": {}, + + "/settings": {}, + "/settings/panel": {}, + "/settings/backupaccount": {}, + "/settings/license": {}, + "/settings/about": {}, + "/settings/safe": {}, + "/settings/snapshot": {}, + "/settings/expired": {}, + + "/toolbox": {}, + "/toolbox/device": {}, + "/toolbox/supervisor": {}, + "/toolbox/clam": {}, + "/toolbox/clam/setting": {}, + "/toolbox/ftp": {}, + "/toolbox/fail2ban": {}, + "/toolbox/clean": {}, + + "/websites": {}, + "/websites/ssl": {}, + "/websites/runtimes/php": {}, + "/websites/runtimes/node": {}, + "/websites/runtimes/java": {}, + "/websites/runtimes/net": {}, + "/websites/runtimes/go": {}, + "/websites/runtimes/python": {}, + + "/login": {}, +} + +var DynamicRoutes = []string{ + `^/containers/composeDetail/[^/]+$`, + `^/databases/mysql/setting/[^/]+/[^/]+$`, + `^/databases/postgresql/setting/[^/]+/[^/]+$`, + `^/websites/[^/]+/config/[^/]+$`, +} diff --git a/backend/init/router/router.go b/backend/init/router/router.go index 214cfcc6a..0b1e0de26 100644 --- a/backend/init/router/router.go +++ b/backend/init/router/router.go @@ -1,8 +1,15 @@ package router import ( + "encoding/base64" "fmt" + "github.com/1Panel-dev/1Panel/backend/app/service" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/cmd/server/res" "net/http" + "regexp" + "strconv" + "strings" "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/i18n" @@ -20,8 +27,90 @@ var ( Router *gin.Engine ) +func toIndexHtml(c *gin.Context) { + c.Writer.WriteHeader(http.StatusOK) + _, _ = c.Writer.Write(web.IndexByte) + c.Writer.Header().Add("Accept", "text/html") + c.Writer.Flush() +} + +func isEntrancePath(c *gin.Context) bool { + entrance := service.NewIAuthService().GetSecurityEntrance() + if entrance != "" && strings.TrimSuffix(c.Request.URL.Path, "/") == "/"+entrance { + return true + } + return false +} + +func isFrontendPath(c *gin.Context) bool { + reqUri := strings.TrimSuffix(c.Request.URL.Path, "/") + if _, ok := constant.WebUrlMap[reqUri]; ok { + return true + } + for _, route := range constant.DynamicRoutes { + if match, _ := regexp.MatchString(route, reqUri); match { + return true + } + } + return false +} + +func checkFrontendPath(c *gin.Context) bool { + if !isFrontendPath(c) { + return false + } + authService := service.NewIAuthService() + if authService.GetSecurityEntrance() != "" { + return authService.IsLogin(c) + } + return true +} + +func checkEntrance(c *gin.Context) bool { + authService := service.NewIAuthService() + entrance := authService.GetSecurityEntrance() + if entrance == "" { + return true + } + + cookieValue, err := c.Cookie("SecurityEntrance") + if err != nil { + return false + } + entranceValue, err := base64.StdEncoding.DecodeString(cookieValue) + if err != nil { + return false + } + return string(entranceValue) == entrance +} + +func handleNoRoute(c *gin.Context) { + resPage, err := service.NewIAuthService().GetResponsePage() + if err != nil { + c.String(http.StatusInternalServerError, "Internal Server Error") + return + } + file := fmt.Sprintf("html/%s.html", resPage) + if resPage == "200" && c.GetHeader("Accept-Language") == "en" { + file = "html/200_en.html" + } + + data, err := res.ErrorMsg.ReadFile(file) + if err != nil { + c.String(http.StatusInternalServerError, "Internal Server Error") + return + } + statusCode, err := strconv.Atoi(resPage) + if err != nil { + c.String(http.StatusInternalServerError, "Internal Server Error") + return + } + c.Data(statusCode, "text/html; charset=utf-8", data) +} + func setWebStatic(rootRouter *gin.RouterGroup) { rootRouter.StaticFS("/public", http.FS(web.Favicon)) + rootRouter.StaticFS("/favicon.ico", http.FS(web.Favicon)) rootRouter.Static("/api/v1/images", "./uploads") rootRouter.Use(func(c *gin.Context) { c.Next() @@ -31,7 +120,27 @@ func setWebStatic(rootRouter *gin.RouterGroup) { staticServer := http.FileServer(http.FS(web.Assets)) staticServer.ServeHTTP(c.Writer, c.Request) }) + + authService := service.NewIAuthService() + entrance := authService.GetSecurityEntrance() + if entrance != "" { + rootRouter.GET("/"+entrance, func(c *gin.Context) { + entrance = authService.GetSecurityEntrance() + if entrance == "" { + handleNoRoute(c) + return + } + c.Writer.WriteHeader(http.StatusOK) + _, _ = c.Writer.Write(web.IndexByte) + c.Writer.Header().Add("Accept", "text/html") + c.Writer.Flush() + }) + } rootRouter.GET("/", func(c *gin.Context) { + if !checkEntrance(c) { + handleNoRoute(c) + return + } staticServer := http.FileServer(http.FS(web.IndexHtml)) staticServer.ServeHTTP(c.Writer, c.Request) }) @@ -47,10 +156,15 @@ func Routers() *gin.Engine { } Router.NoRoute(func(c *gin.Context) { - c.Writer.WriteHeader(http.StatusOK) - _, _ = c.Writer.Write(web.IndexByte) - c.Writer.Header().Add("Accept", "text/html") - c.Writer.Flush() + if checkFrontendPath(c) { + toIndexHtml(c) + return + } + if isEntrancePath(c) { + toIndexHtml(c) + return + } + handleNoRoute(c) }) Router.Use(i18n.UseI18n()) diff --git a/backend/router/ro_base.go b/backend/router/ro_base.go index 0ea3808b1..dff0cf8f1 100644 --- a/backend/router/ro_base.go +++ b/backend/router/ro_base.go @@ -14,7 +14,6 @@ func (s *BaseRouter) InitRouter(Router *gin.RouterGroup) { baseRouter.GET("/captcha", baseApi.Captcha) baseRouter.POST("/mfalogin", baseApi.MFALogin) baseRouter.POST("/login", baseApi.Login) - baseRouter.GET("/issafety", baseApi.CheckIsSafety) baseRouter.POST("/logout", baseApi.LogOut) baseRouter.GET("/demo", baseApi.CheckIsDemo) baseRouter.GET("/language", baseApi.GetLanguage) diff --git a/cmd/server/res/error_msg.go b/cmd/server/res/error_msg.go new file mode 100644 index 000000000..fb958d1e6 --- /dev/null +++ b/cmd/server/res/error_msg.go @@ -0,0 +1,6 @@ +package res + +import "embed" + +//go:embed html/* +var ErrorMsg embed.FS diff --git a/cmd/server/res/html/200.html b/cmd/server/res/html/200.html new file mode 100644 index 000000000..7a9ab4c63 --- /dev/null +++ b/cmd/server/res/html/200.html @@ -0,0 +1,55 @@ + + + + + + 暂无法访问 + + + +
+

暂时无法访问

+

当前环境已经开启了安全入口登录。

+

在 SSH 终端输入以下命令来查看面板入口

+

1pctl user-info

+
+ + diff --git a/cmd/server/res/html/200_en.html b/cmd/server/res/html/200_en.html new file mode 100644 index 000000000..c21b7f15a --- /dev/null +++ b/cmd/server/res/html/200_en.html @@ -0,0 +1,55 @@ + + + + + + Access Temporarily Unavailable + + + +
+

Access Temporarily Unavailable

+

The current environment has enabled secure login access.

+

Please enter the following command in the SSH terminal to view the panel login URL:

+

1pctl user-info

+
+ + diff --git a/cmd/server/res/html/400.html b/cmd/server/res/html/400.html new file mode 100644 index 000000000..0d502a0a3 --- /dev/null +++ b/cmd/server/res/html/400.html @@ -0,0 +1,7 @@ + + +400 Bad Request + +

400 Bad Request

+
nginx
+ \ No newline at end of file diff --git a/cmd/server/res/html/401.html b/cmd/server/res/html/401.html new file mode 100644 index 000000000..60e1498f7 --- /dev/null +++ b/cmd/server/res/html/401.html @@ -0,0 +1,7 @@ + + +401 Unauthorized + +

401 Unauthorized

+
nginx
+ \ No newline at end of file diff --git a/cmd/server/res/html/403.html b/cmd/server/res/html/403.html new file mode 100644 index 000000000..a77b7cb64 --- /dev/null +++ b/cmd/server/res/html/403.html @@ -0,0 +1,7 @@ + + +403 Forbidden + +

403 Forbidden

+
nginx
+ \ No newline at end of file diff --git a/cmd/server/res/html/404.html b/cmd/server/res/html/404.html new file mode 100644 index 000000000..6748be25f --- /dev/null +++ b/cmd/server/res/html/404.html @@ -0,0 +1,7 @@ + + +404 Not Found + +

404 Not Found

+
nginx
+ \ No newline at end of file diff --git a/cmd/server/res/html/408.html b/cmd/server/res/html/408.html new file mode 100644 index 000000000..15ba0cdaa --- /dev/null +++ b/cmd/server/res/html/408.html @@ -0,0 +1,7 @@ + + +408 Request Timeout + +

408 Request Timeout

+
nginx
+ \ No newline at end of file diff --git a/cmd/server/res/html/416.html b/cmd/server/res/html/416.html new file mode 100644 index 000000000..8104e724c --- /dev/null +++ b/cmd/server/res/html/416.html @@ -0,0 +1,7 @@ + + +416 Requested Not Satisfiable + +

416 Requested Not Satisfiable

+
nginx
+ \ No newline at end of file diff --git a/cmd/server/res/html/500.html b/cmd/server/res/html/500.html new file mode 100644 index 000000000..0d502a0a3 --- /dev/null +++ b/cmd/server/res/html/500.html @@ -0,0 +1,7 @@ + + +400 Bad Request + +

400 Bad Request

+
nginx
+ \ No newline at end of file diff --git a/frontend/src/api/modules/auth.ts b/frontend/src/api/modules/auth.ts index 937344d41..e9f7fbf67 100644 --- a/frontend/src/api/modules/auth.ts +++ b/frontend/src/api/modules/auth.ts @@ -17,10 +17,6 @@ export const logOutApi = () => { return http.post(`/auth/logout`); }; -export const checkIsSafety = (code: string) => { - return http.get(`/auth/issafety?code=${code}`); -}; - export const checkIsDemo = () => { return http.get('/auth/demo'); }; diff --git a/frontend/src/routers/modules/toolbox.ts b/frontend/src/routers/modules/toolbox.ts index d77a80b78..fd3f374ed 100644 --- a/frontend/src/routers/modules/toolbox.ts +++ b/frontend/src/routers/modules/toolbox.ts @@ -68,7 +68,7 @@ const toolboxRouter = { }, }, { - path: 'fail2Ban', + path: 'fail2ban', name: 'Fail2ban', component: () => import('@/views/toolbox/fail2ban/index.vue'), hidden: true, diff --git a/frontend/src/views/login/entrance/index.vue b/frontend/src/views/login/entrance/index.vue index 6ddf0f247..4cffd009b 100644 --- a/frontend/src/views/login/entrance/index.vue +++ b/frontend/src/views/login/entrance/index.vue @@ -41,7 +41,6 @@