diff --git a/.idea/vcs.xml b/.idea/vcs.xml index c8e2b47f..35eb1ddf 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/api/api.go b/api/api.go index c27460bf..89c59005 100644 --- a/api/api.go +++ b/api/api.go @@ -3,6 +3,7 @@ package api import ( "errors" "github.com/0xJacky/Nginx-UI/internal/logger" + "github.com/0xJacky/Nginx-UI/model" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "net/http" @@ -11,6 +12,10 @@ import ( "strings" ) +func CurrentUser(c *gin.Context) *model.Auth { + return c.MustGet("user").(*model.Auth) +} + func ErrHandler(c *gin.Context, err error) { logger.GetLogger().Errorln(err) c.JSON(http.StatusInternalServerError, gin.H{ diff --git a/api/user/auth.go b/api/user/auth.go index 82feef40..26ff86cf 100644 --- a/api/user/auth.go +++ b/api/user/auth.go @@ -16,14 +16,19 @@ import ( var mutex = &sync.Mutex{} type LoginUser struct { - Name string `json:"name" binding:"required,max=255"` - Password string `json:"password" binding:"required,max=255"` + Name string `json:"name" binding:"required,max=255"` + Password string `json:"password" binding:"required,max=255"` + OTP string `json:"otp"` + RecoveryCode string `json:"recovery_code"` } const ( ErrPasswordIncorrect = 4031 ErrMaxAttempts = 4291 ErrUserBanned = 4033 + Enabled2FA = 199 + Error2FACode = 4034 + LoginSuccess = 200 ) type LoginResponse struct { @@ -80,11 +85,32 @@ func Login(c *gin.Context) { return } + // Check if the user enables 2FA + if len(u.OTPSecret) > 0 { + if json.OTP == "" && json.RecoveryCode == "" { + c.JSON(http.StatusOK, LoginResponse{ + Message: "The user has enabled 2FA", + Code: Enabled2FA, + }) + user.BanIP(clientIP) + return + } + + if err = user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil { + c.JSON(http.StatusForbidden, LoginResponse{ + Message: "Invalid 2FA or recovery code", + Code: Error2FACode, + }) + user.BanIP(clientIP) + return + } + } + // login success, clear banned record _, _ = b.Where(b.IP.Eq(clientIP)).Delete() logger.Info("[User Login]", u.Name) - token, err := user.GenerateJWT(u.Name) + token, err := user.GenerateJWT(u) if err != nil { c.JSON(http.StatusInternalServerError, LoginResponse{ Message: err.Error(), @@ -93,6 +119,7 @@ func Login(c *gin.Context) { } c.JSON(http.StatusOK, LoginResponse{ + Code: LoginSuccess, Message: "ok", Token: token, }) @@ -101,13 +128,7 @@ func Login(c *gin.Context) { func Logout(c *gin.Context) { token := c.GetHeader("Authorization") if token != "" { - err := user.DeleteToken(token) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "message": err.Error(), - }) - return - } + user.DeleteToken(token) } c.JSON(http.StatusNoContent, nil) } diff --git a/api/user/casdoor.go b/api/user/casdoor.go index 3fb10506..d8e210cb 100644 --- a/api/user/casdoor.go +++ b/api/user/casdoor.go @@ -65,7 +65,7 @@ func CasdoorCallback(c *gin.Context) { return } - userToken, err := user.GenerateJWT(u.Name) + userToken, err := user.GenerateJWT(u) if err != nil { api.ErrHandler(c, err) return diff --git a/api/user/otp.go b/api/user/otp.go new file mode 100644 index 00000000..13bcc3e8 --- /dev/null +++ b/api/user/otp.go @@ -0,0 +1,163 @@ +package user + +import ( + "bytes" + "crypto/sha1" + "encoding/base64" + "encoding/hex" + "fmt" + "github.com/0xJacky/Nginx-UI/api" + "github.com/0xJacky/Nginx-UI/internal/crypto" + "github.com/0xJacky/Nginx-UI/query" + "github.com/0xJacky/Nginx-UI/settings" + "github.com/gin-gonic/gin" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "image/jpeg" + "net/http" + "strings" +) + +func GenerateTOTP(c *gin.Context) { + user := api.CurrentUser(c) + + issuer := fmt.Sprintf("Nginx UI %s", settings.ServerSettings.Name) + issuer = strings.TrimSpace(issuer) + + otpOpts := totp.GenerateOpts{ + Issuer: issuer, + AccountName: user.Name, + Period: 30, // seconds + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + } + otpKey, err := totp.Generate(otpOpts) + if err != nil { + api.ErrHandler(c, err) + return + } + ciphertext, err := crypto.AesEncrypt([]byte(otpKey.Secret())) + if err != nil { + api.ErrHandler(c, err) + return + } + + qrCode, err := otpKey.Image(512, 512) + if err != nil { + api.ErrHandler(c, err) + return + } + + // Encode the image to a buffer + var buf []byte + buffer := bytes.NewBuffer(buf) + err = jpeg.Encode(buffer, qrCode, nil) + if err != nil { + fmt.Println("Error encoding image:", err) + return + } + + // Convert the buffer to a base64 string + base64Str := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(buffer.Bytes()) + + c.JSON(http.StatusOK, gin.H{ + "secret": base64.StdEncoding.EncodeToString(ciphertext), + "qr_code": base64Str, + }) +} + +func EnrollTOTP(c *gin.Context) { + user := api.CurrentUser(c) + if len(user.OTPSecret) > 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "User already enrolled", + }) + return + } + + var json struct { + Secret string `json:"secret" binding:"required"` + Passcode string `json:"passcode" binding:"required"` + } + if !api.BindAndValid(c, &json) { + return + } + + secret, err := base64.StdEncoding.DecodeString(json.Secret) + if err != nil { + api.ErrHandler(c, err) + return + } + + decrypted, err := crypto.AesDecrypt(secret) + if err != nil { + api.ErrHandler(c, err) + return + } + + if ok := totp.Validate(json.Passcode, string(decrypted)); !ok { + c.JSON(http.StatusNotAcceptable, gin.H{ + "message": "Invalid passcode", + }) + return + } + + ciphertext, err := crypto.AesEncrypt(decrypted) + if err != nil { + api.ErrHandler(c, err) + return + } + + u := query.Auth + _, err = u.Where(u.ID.Eq(user.ID)).Update(u.OTPSecret, ciphertext) + if err != nil { + api.ErrHandler(c, err) + return + } + + recoveryCode := sha1.Sum(ciphertext) + + c.JSON(http.StatusOK, gin.H{ + "message": "ok", + "recovery_code": hex.EncodeToString(recoveryCode[:]), + }) +} + +func ResetOTP(c *gin.Context) { + var json struct { + RecoveryCode string `json:"recovery_code"` + } + if !api.BindAndValid(c, &json) { + return + } + recoverCode, err := hex.DecodeString(json.RecoveryCode) + if err != nil { + api.ErrHandler(c, err) + return + } + user := api.CurrentUser(c) + k := sha1.Sum(user.OTPSecret) + if !bytes.Equal(k[:], recoverCode) { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Invalid recovery code", + }) + return + } + + u := query.Auth + _, err = u.Where(u.ID.Eq(user.ID)).UpdateSimple(u.OTPSecret.Null()) + if err != nil { + api.ErrHandler(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "ok", + }) +} + +func OTPStatus(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": len(api.CurrentUser(c).OTPSecret) > 0, + }) +} diff --git a/api/user/router.go b/api/user/router.go index ee50f702..565985dd 100644 --- a/api/user/router.go +++ b/api/user/router.go @@ -2,18 +2,25 @@ package user import "github.com/gin-gonic/gin" -func InitAuthRouter(r *gin.RouterGroup) { - r.POST("/login", Login) - r.DELETE("/logout", Logout) +func InitAuthRouter(r *gin.RouterGroup) { + r.POST("/login", Login) + r.DELETE("/logout", Logout) - r.GET("/casdoor_uri", GetCasdoorUri) - r.POST("/casdoor_callback", CasdoorCallback) + r.GET("/casdoor_uri", GetCasdoorUri) + r.POST("/casdoor_callback", CasdoorCallback) } func InitManageUserRouter(r *gin.RouterGroup) { - r.GET("users", GetUsers) - r.GET("user/:id", GetUser) - r.POST("user", AddUser) - r.POST("user/:id", EditUser) - r.DELETE("user/:id", DeleteUser) + r.GET("users", GetUsers) + r.GET("user/:id", GetUser) + r.POST("user", AddUser) + r.POST("user/:id", EditUser) + r.DELETE("user/:id", DeleteUser) +} + +func InitUserRouter(r *gin.RouterGroup) { + r.GET("/otp_status", OTPStatus) + r.GET("/otp_secret", GenerateTOTP) + r.POST("/otp_enroll", EnrollTOTP) + r.POST("/otp_reset", ResetOTP) } diff --git a/app.example.ini b/app.example.ini index ca687a99..ec86357f 100644 --- a/app.example.ini +++ b/app.example.ini @@ -51,3 +51,11 @@ Interval = 1440 Node = http://10.0.0.1:9000?name=node1&node_secret=my-node-secret&enabled=true Node = http://10.0.0.2:9000?name=node2&node_secret=my-node-secret&enabled=true Node = http://10.0.0.3?name=node3&node_secret=my-node-secret&enabled=true + +[auth] +IPWhiteList = +BanThresholdMinutes = 10 +MaxAttempts = 10 + +[crypto] +Secret = secret2 diff --git a/app/components.d.ts b/app/components.d.ts index 2d3e57bd..507cb693 100644 --- a/app/components.d.ts +++ b/app/components.d.ts @@ -78,6 +78,8 @@ declare module 'vue' { NginxControlNginxControl: typeof import('./src/components/NginxControl/NginxControl.vue')['default'] NodeSelectorNodeSelector: typeof import('./src/components/NodeSelector/NodeSelector.vue')['default'] NotificationNotification: typeof import('./src/components/Notification/Notification.vue')['default'] + OTPInput: typeof import('./src/components/OTPInput.vue')['default'] + OTPInputOTPInput: typeof import('./src/components/OTPInput/OTPInput.vue')['default'] PageHeaderPageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/app/package.json b/app/package.json index a0806938..5ca186b0 100644 --- a/app/package.json +++ b/app/package.json @@ -38,6 +38,7 @@ "vue3-ace-editor": "2.2.4", "vue3-apexcharts": "1.4.4", "vue3-gettext": "3.0.0-beta.4", + "vue3-otp-input": "^0.5.21", "vuedraggable": "^4.1.0" }, "devDependencies": { diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 865f2fa9..ac629e39 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: vue3-gettext: specifier: 3.0.0-beta.4 version: 3.0.0-beta.4(@vue/compiler-sfc@3.4.33)(typescript@5.3.3)(vue@3.4.33(typescript@5.3.3)) + vue3-otp-input: + specifier: ^0.5.21 + version: 0.5.21(vue@3.4.33(typescript@5.3.3)) vuedraggable: specifier: ^4.1.0 version: 4.1.0(vue@3.4.33(typescript@5.3.3)) @@ -2969,6 +2972,11 @@ packages: '@vue/compiler-sfc': '>=3.0.0' vue: '>=3.0.0' + vue3-otp-input@0.5.21: + resolution: {integrity: sha512-dRxmGJqXlU+U5dCijNCyY7ird49+pyfeQspSTqvIp2Xs+VByIluNlTOjgHrftzSdeVZggtx+Ojb8uKiRLaob4Q==} + peerDependencies: + vue: ^3.0.* + vue@3.4.33: resolution: {integrity: sha512-VdMCWQOummbhctl4QFMcW6eNtXHsFyDlX60O/tsSQuCcuDOnJ1qPOhhVla65Niece7xq/P2zyZReIO5mP+LGTQ==} peerDependencies: @@ -6175,6 +6183,10 @@ snapshots: transitivePeerDependencies: - typescript + vue3-otp-input@0.5.21(vue@3.4.33(typescript@5.3.3)): + dependencies: + vue: 3.4.33(typescript@5.3.3) + vue@3.4.33(typescript@5.3.3): dependencies: '@vue/compiler-dom': 3.4.33 diff --git a/app/src/api/auth.ts b/app/src/api/auth.ts index 7541a965..7381ed94 100644 --- a/app/src/api/auth.ts +++ b/app/src/api/auth.ts @@ -6,15 +6,16 @@ const { login, logout } = useUserStore() export interface AuthResponse { message: string token: string + code: number } const auth = { - async login(name: string, password: string) { + async login(name: string, password: string, otp: string, recoveryCode: string): Promise { return http.post('/login', { name, password, - }).then((r: AuthResponse) => { - login(r.token) + otp, + recovery_code: recoveryCode, }) }, async casdoor_login(code?: string, state?: string) { diff --git a/app/src/api/otp.ts b/app/src/api/otp.ts new file mode 100644 index 00000000..ba8f0180 --- /dev/null +++ b/app/src/api/otp.ts @@ -0,0 +1,23 @@ +import http from '@/lib/http' + +export interface OTPGenerateSecretResponse { + secret: string + qr_code: string +} + +const otp = { + status(): Promise<{ status: boolean }> { + return http.get('/otp_status') + }, + generate_secret(): Promise { + return http.get('/otp_secret') + }, + enroll_otp(secret: string, passcode: string): Promise<{ recovery_code: string }> { + return http.post('/otp_enroll', { secret, passcode }) + }, + reset(recovery_code: string) { + return http.post('/otp_reset', { recovery_code }) + }, +} + +export default otp diff --git a/app/src/components/OTPInput/OTPInput.vue b/app/src/components/OTPInput/OTPInput.vue new file mode 100644 index 00000000..39fc6947 --- /dev/null +++ b/app/src/components/OTPInput/OTPInput.vue @@ -0,0 +1,72 @@ + + + + + + + diff --git a/app/src/views/other/Login.vue b/app/src/views/other/Login.vue index 96bfbdf3..02022793 100644 --- a/app/src/views/other/Login.vue +++ b/app/src/views/other/Login.vue @@ -1,13 +1,14 @@ +
+
+

{{ $gettext('Please enter the 2FA code:') }}

+ + + +
+ +
- - - - - - - - - +

{{ $gettext('Input the recovery code:') }}

+ + + + {{ $gettext('Recovery') }} + + +
+
+ + + + diff --git a/app/src/views/preference/components/TOTP.vue b/app/src/views/preference/components/TOTP.vue new file mode 100644 index 00000000..59b2dfc0 --- /dev/null +++ b/app/src/views/preference/components/TOTP.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/go.mod b/go.mod index d923f55a..d72ce386 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/caarlos0/env/v11 v11.1.0 github.com/casdoor/casdoor-go-sdk v0.47.0 github.com/creack/pty v1.1.21 + github.com/dgraph-io/ristretto v0.1.1 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.17.0 @@ -44,7 +45,7 @@ require ( require ( aead.dev/minisign v0.3.0 // indirect - cloud.google.com/go/auth v0.7.1 // indirect + cloud.google.com/go/auth v0.7.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.5.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect @@ -73,7 +74,7 @@ require ( github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect - github.com/aliyun/alibaba-cloud-sdk-go v1.62.793 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.62.795 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect @@ -97,6 +98,8 @@ require ( github.com/bytedance/sonic v1.11.9 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/civo/civogo v0.3.73 // indirect github.com/cloudflare/cloudflare-go v0.100.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect @@ -125,20 +128,21 @@ require ( github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/goccy/go-json v0.10.3 // indirect - github.com/gofrs/flock v0.12.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/glog v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.7 // indirect + github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/gophercloud/gophercloud v1.13.0 // indirect github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect github.com/gorilla/css v1.0.1 // indirect @@ -228,8 +232,8 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/tdewolff/minify/v2 v2.20.37 // indirect github.com/tdewolff/parse/v2 v2.7.15 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.968 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.968 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.969 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.969 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect github.com/transip/gotransip/v6 v6.25.0 // indirect @@ -242,7 +246,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vultr/govultr/v2 v2.17.2 // indirect - github.com/yandex-cloud/go-genproto v0.0.0-20240715115219-0c1e192fbf5c // indirect + github.com/yandex-cloud/go-genproto v0.0.0-20240722173647-40d4f9e8b9fa // indirect github.com/yandex-cloud/go-sdk v0.0.0-20240701143239-7326d2d09169 // indirect github.com/yosssi/ace v0.0.5 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect @@ -264,10 +268,10 @@ require ( golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.23.0 // indirect - google.golang.org/api v0.188.0 // indirect - google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/api v0.189.0 // indirect + google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect @@ -278,6 +282,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/datatypes v1.2.1 // indirect gorm.io/driver/mysql v1.5.7 // indirect + gorm.io/driver/postgres v1.5.6 // indirect gorm.io/hints v1.1.2 // indirect k8s.io/api v0.30.3 // indirect k8s.io/apimachinery v0.30.3 // indirect diff --git a/go.sum b/go.sum index 95560c5c..25f77909 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/auth v0.7.1 h1:Iv1bbpzJ2OIg16m94XI9/tlzZZl3cdeR3nGVGj78N7s= -cloud.google.com/go/auth v0.7.1/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= +cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE= +cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= @@ -672,6 +672,7 @@ github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= @@ -694,8 +695,10 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/aliyun/alibaba-cloud-sdk-go v1.62.793 h1:7FmdfF5fZMxM8Y0YtwrnMLkwud+egvoB5X5xczqISNQ= -github.com/aliyun/alibaba-cloud-sdk-go v1.62.793/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.794 h1:M6YtlJdCobRVlJaILK4Eia5aMtDSpeQtxFRl4hSi+DU= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.794/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.795 h1:DjIaInK6Ru+fPnOX0Ef4ux5tkp/dCPI3pAZEijEvlvo= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.795/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= @@ -772,9 +775,12 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -821,8 +827,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deepmap/oapi-codegen v1.16.3 h1:GT9G86SbQtT1r8ZB+4Cybi9VGdu1P5ieNvNdEoCSbrA= github.com/deepmap/oapi-codegen v1.16.3/go.mod h1:JD6ErqeX0nYnhdciLc61Konj3NBASREMlkHOgHn8WAM= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= @@ -949,6 +961,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -972,6 +986,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1065,6 +1081,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -1092,6 +1110,8 @@ github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38 github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= @@ -1561,6 +1581,7 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= @@ -1614,14 +1635,14 @@ github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.967 h1:ui73H/2pKk2aDCxaBCLAeMB3JlNgdCkn0nx1x0pqvf0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.967/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.968 h1:SdgunZB3WU2vNn3H9dJQ1Z2cQK61vN79zCfnHk3Cu3Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.968/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.967 h1:4w33xHFgyrlFZYoGkPQ3uhld8tqoezpObfmCBrdlFBY= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.967/go.mod h1:T0RlPIT2imBeCxLkWfzoiEVP1r5WwzC6becSq7wvSgU= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.969 h1:rJlV77WbjuJ5uGBi+THOk09Cfp8Kskz9HgExq0enTmY= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.969/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.968 h1:h7voJALWRkUX6w7obk9CWHppnJwZuQlreQJVDldVRxY= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.968/go.mod h1:3cwvPwyqYaYkzAsR4vbrE6mb3Ju9uY7Pj+wHYSVd3aw= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.969 h1:W2DHKBCSLjpHoQjqgAkyUu7lV8deIW+FBZS95iNRf1A= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.969/go.mod h1:jIxuhjYsAyTTErdwvaX1ay+FHH021fmjdlsbnkaOgfs= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= @@ -1664,6 +1685,8 @@ github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmv github.com/yandex-cloud/go-genproto v0.0.0-20240701142715-6a03f33f8ec8/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-genproto v0.0.0-20240715115219-0c1e192fbf5c h1:GzMfpQ/oAP93MOQb5/B+3daDzdcLRRqetZ8radtnJJ4= github.com/yandex-cloud/go-genproto v0.0.0-20240715115219-0c1e192fbf5c/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= +github.com/yandex-cloud/go-genproto v0.0.0-20240722173647-40d4f9e8b9fa h1:MFb4Q81BMqa0vL64v/i3mel9C+XQkVnwgWqWbmqv10U= +github.com/yandex-cloud/go-genproto v0.0.0-20240722173647-40d4f9e8b9fa/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-sdk v0.0.0-20240701143239-7326d2d09169 h1:5LGYQ/0h1uUo3HH8MsG6R40gvSVPj/7r4D1sKVMa370= github.com/yandex-cloud/go-sdk v0.0.0-20240701143239-7326d2d09169/go.mod h1:kRqpmRyPs8rzXuYEJe57AH546a3VcSjEIzdFa1V66hY= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= @@ -2051,6 +2074,7 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2258,6 +2282,8 @@ google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45 google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw= google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag= +google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI= +google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2400,10 +2426,16 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc= google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= +google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg= +google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade h1:WxZOF2yayUHpHSbUE6NMzumUzBxYc3YGwo0YHnbzsJY= +google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -2515,8 +2547,8 @@ gorm.io/datatypes v1.2.1/go.mod h1:hYK6OTb/1x+m96PgoZZq10UXJ6RvEBb9kRDQ2yyhzGs= gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= -gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= +gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU= +gorm.io/driver/postgres v1.5.6/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= diff --git a/internal/cron/cron.go b/internal/cron/cron.go index a6b1e120..decda2e2 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -4,6 +4,7 @@ import ( "github.com/0xJacky/Nginx-UI/internal/cert" "github.com/0xJacky/Nginx-UI/internal/logger" "github.com/0xJacky/Nginx-UI/internal/logrotate" + "github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/settings" "github.com/go-co-op/gocron" "time" @@ -25,6 +26,7 @@ func InitCronJobs() { } startLogrotate() + cleanExpiredAuthToken() s.StartAsync() } @@ -43,10 +45,20 @@ func startLogrotate() { return } var err error - logrotateJob, err = s.Every(settings.LogrotateSettings.Interval).Minute().SingletonMode().Do(logrotate.Exec) - if err != nil { logger.Fatalf("LogRotate Job: %v, Err: %v\n", logrotateJob, err) } } + +func cleanExpiredAuthToken() { + job, err := s.Every(5).Minute().SingletonMode().Do(func() { + logger.Info("clean expired auth tokens") + q := query.AuthToken + _, _ = q.Where(q.ExpiredAt.Lt(time.Now().Unix())).Delete() + }) + + if err != nil { + logger.Fatalf("CleanExpiredAuthToken Job: %v, Err: %v\n", job, err) + } +} diff --git a/internal/crypto/aes.go b/internal/crypto/aes.go new file mode 100644 index 00000000..1e76d2df --- /dev/null +++ b/internal/crypto/aes.go @@ -0,0 +1,59 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "github.com/0xJacky/Nginx-UI/settings" + "github.com/pkg/errors" + "io" +) + +// AesEncrypt encrypts text and given key with AES. +func AesEncrypt(text []byte) ([]byte, error) { + if len(text) == 0 { + return nil, errors.New("AesEncrypt text is empty") + } + block, err := aes.NewCipher(settings.CryptoSettings.GetSecretMd5()) + if err != nil { + return nil, fmt.Errorf("AesEncrypt invalid key: %v", err) + } + + b := base64.StdEncoding.EncodeToString(text) + ciphertext := make([]byte, aes.BlockSize+len(b)) + iv := ciphertext[:aes.BlockSize] + if _, err = io.ReadFull(rand.Reader, iv); err != nil { + return nil, fmt.Errorf("AesEncrypt unable to read IV: %w", err) + } + + cfb := cipher.NewCFBEncrypter(block, iv) + cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b)) + + return ciphertext, nil +} + +// AesDecrypt decrypts text and given key with AES. +func AesDecrypt(text []byte) ([]byte, error) { + block, err := aes.NewCipher(settings.CryptoSettings.GetSecretMd5()) + if err != nil { + return nil, err + } + + if len(text) < aes.BlockSize { + return nil, errors.New("AesDecrypt ciphertext too short") + } + + iv := text[:aes.BlockSize] + text = text[aes.BlockSize:] + cfb := cipher.NewCFBDecrypter(block, iv) + cfb.XORKeyStream(text, text) + + data, err := base64.StdEncoding.DecodeString(string(text)) + if err != nil { + return nil, fmt.Errorf("AesDecrypt invalid decrypted base64 string: %w", err) + } + + return data, nil +} diff --git a/internal/crypto/aes_test.go b/internal/crypto/aes_test.go new file mode 100644 index 00000000..805214c0 --- /dev/null +++ b/internal/crypto/aes_test.go @@ -0,0 +1,76 @@ +package crypto + +import ( + "github.com/0xJacky/Nginx-UI/settings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func EncryptDecryptRoundTrip(text string) bool { + encrypted, err := AesEncrypt([]byte(text)) + if err != nil { + return false + } + + decrypted, err := AesDecrypt(encrypted) + if err != nil { + return false + } + + return text == string(decrypted) +} + +func EncryptsNonEmptyStringWithoutError(text string) bool { + _, err := AesEncrypt([]byte(text)) + return err == nil +} + +func DecryptsToOriginalTextAfterEncryption(text string) bool { + encrypted, _ := AesEncrypt([]byte(text)) + decrypted, err := AesDecrypt(encrypted) + if err != nil { + return false + } + + return text == string(decrypted) +} + +func FailsToDecryptWithModifiedCiphertext(text string) bool { + encrypted, _ := AesEncrypt([]byte(text)) + // Modify the ciphertext + encrypted[0] ^= 0xff + _, err := AesDecrypt(encrypted) + return err != nil +} + +func FailsToDecryptShortCiphertext() bool { + _, err := AesDecrypt([]byte("short")) + return err != nil +} + +func TestAesEncryptionDecryption(t *testing.T) { + settings.CryptoSettings.Secret = "test" + assert.True(t, EncryptDecryptRoundTrip("Hello, world!"), "should encrypt and decrypt to the original text") + assert.True(t, EncryptsNonEmptyStringWithoutError("Test String"), "should encrypt a non-empty string without error") + assert.True(t, DecryptsToOriginalTextAfterEncryption("Another Test String"), "should decrypt to the original text after encryption") + assert.True(t, FailsToDecryptWithModifiedCiphertext("Sensitive Data"), "should fail to decrypt with modified ciphertext") + assert.True(t, FailsToDecryptShortCiphertext(), "should fail to decrypt short ciphertext") +} + +func TestAesEncrypt_WithEmptyString_ReturnsError(t *testing.T) { + settings.CryptoSettings.Secret = "test" + _, err := AesEncrypt([]byte("")) + require.Error(t, err, "encrypting an empty string should return an error") +} + +func TestAesDecrypt_WithInvalidBase64_ReturnsError(t *testing.T) { + settings.CryptoSettings.Secret = "test" + // Assuming the function is modified to handle this case explicitly + encrypted, _ := AesEncrypt([]byte("valid text")) + // Invalidate the base64 encoding + encrypted[len(encrypted)-1] = '!' + _, err := AesDecrypt(encrypted) + require.Error(t, err, "decrypting an invalid base64 string should return an error") +} diff --git a/internal/kernal/boot.go b/internal/kernal/boot.go index 83f81ff0..aad0496f 100644 --- a/internal/kernal/boot.go +++ b/internal/kernal/boot.go @@ -1,20 +1,20 @@ package kernal import ( + "crypto/rand" + "encoding/hex" "github.com/0xJacky/Nginx-UI/internal/analytic" "github.com/0xJacky/Nginx-UI/internal/cert" "github.com/0xJacky/Nginx-UI/internal/cluster" + "github.com/0xJacky/Nginx-UI/internal/cron" "github.com/0xJacky/Nginx-UI/internal/logger" - "github.com/0xJacky/Nginx-UI/internal/logrotate" "github.com/0xJacky/Nginx-UI/internal/validation" "github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/settings" - "github.com/go-co-op/gocron" "github.com/google/uuid" "mime" "runtime" - "time" ) func Boot() { @@ -24,6 +24,7 @@ func Boot() { InitJsExtensionType, InitDatabase, InitNodeSecret, + InitCryptoSecret, validation.Init, } @@ -44,7 +45,7 @@ func InitAfterDatabase() { syncs := []func(){ registerPredefinedUser, cert.InitRegister, - InitCronJobs, + cron.InitCronJobs, cluster.RegisterPredefinedNodes, analytic.RetrieveNodesStatus, } @@ -83,31 +84,34 @@ func InitNodeSecret() { err := settings.Save() if err != nil { - logger.Error("Error save settings") + logger.Error("Error save settings", err) } logger.Warn("Generated NodeSecret: ", settings.ServerSettings.NodeSecret) } } +func InitCryptoSecret() { + if "" == settings.CryptoSettings.Secret { + logger.Warn("Secret is empty, generating...") + + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + logger.Error("Generate Secret failed: ", err) + return + } + + settings.CryptoSettings.Secret = hex.EncodeToString(key) + + err := settings.Save() + if err != nil { + logger.Error("Error save settings", err) + } + logger.Warn("Secret Generated") + } +} + func InitJsExtensionType() { // Hack: fix wrong Content Type of .js file on some OS platforms // See https://github.com/golang/go/issues/32350 _ = mime.AddExtensionType(".js", "text/javascript; charset=utf-8") } - -func InitCronJobs() { - s := gocron.NewScheduler(time.UTC) - job, err := s.Every(6).Hours().SingletonMode().Do(cert.AutoCert) - - if err != nil { - logger.Fatalf("AutoCert Job: %v, Err: %v\n", job, err) - } - - job, err = s.Every(settings.LogrotateSettings.Interval).Minute().SingletonMode().Do(logrotate.Exec) - - if err != nil { - logger.Fatalf("LogRotate Job: %v, Err: %v\n", job, err) - } - - s.StartAsync() -} diff --git a/internal/user/otp.go b/internal/user/otp.go new file mode 100644 index 00000000..1cfed06d --- /dev/null +++ b/internal/user/otp.go @@ -0,0 +1,39 @@ +package user + +import ( + "bytes" + "crypto/sha1" + "encoding/hex" + "github.com/0xJacky/Nginx-UI/internal/crypto" + "github.com/0xJacky/Nginx-UI/model" + "github.com/pkg/errors" + "github.com/pquerna/otp/totp" +) + +var ( + ErrOTPCode = errors.New("invalid otp code") + ErrRecoveryCode = errors.New("invalid recovery code") +) + +func VerifyOTP(user *model.Auth, otp, recoveryCode string) (err error) { + if otp != "" { + decrypted, err := crypto.AesDecrypt(user.OTPSecret) + if err != nil { + return err + } + + if ok := totp.Validate(otp, string(decrypted)); !ok { + return ErrOTPCode + } + } else { + recoverCode, err := hex.DecodeString(recoveryCode) + if err != nil { + return err + } + k := sha1.Sum(user.OTPSecret) + if !bytes.Equal(k[:], recoverCode) { + return ErrRecoveryCode + } + } + return +} diff --git a/internal/user/user.go b/internal/user/user.go index 325fe7da..75b37c5e 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -1,53 +1,84 @@ package user import ( + "github.com/0xJacky/Nginx-UI/internal/logger" "github.com/0xJacky/Nginx-UI/model" + "github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/settings" "github.com/dgrijalva/jwt-go" + "github.com/pkg/errors" + "strings" "time" ) +const ExpiredTime = 24 * time.Hour + type JWTClaims struct { - Name string `json:"name"` + Name string `json:"name"` + UserID int `json:"user_id"` jwt.StandardClaims } -func GetUser(name string) (user model.Auth, err error) { +func BuildCacheTokenKey(token string) string { + var sb strings.Builder + sb.WriteString("token:") + sb.WriteString(token) + return sb.String() +} + +func GetUser(name string) (user *model.Auth, err error) { db := model.UseDB() - err = db.Where("name", name).First(&user).Error + user = &model.Auth{} + err = db.Where("name", name).First(user).Error if err != nil { return } return } -func DeleteToken(token string) error { - db := model.UseDB() - return db.Where("token", token).Delete(&model.AuthToken{}).Error +func DeleteToken(token string) { + q := query.AuthToken + _, _ = q.Where(q.Token.Eq(token)).Delete() } -func CheckToken(token string) int64 { - db := model.UseDB() - return db.Where("token", token).Find(&model.AuthToken{}).RowsAffected +func GetTokenUser(token string) (*model.Auth, bool) { + q := query.AuthToken + authToken, err := q.Where(q.Token.Eq(token)).First() + if err != nil { + return nil, false + } + + if authToken.ExpiredAt < time.Now().Unix() { + DeleteToken(token) + return nil, false + } + + u := query.Auth + user, err := u.FirstByID(authToken.UserID) + return user, err == nil } -func GenerateJWT(name string) (string, error) { +func GenerateJWT(user *model.Auth) (string, error) { claims := JWTClaims{ - Name: name, + Name: user.Name, + UserID: user.ID, StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + ExpiresAt: time.Now().Add(ExpiredTime).Unix(), }, } + unsignedToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signedToken, err := unsignedToken.SignedString([]byte(settings.ServerSettings.JwtSecret)) if err != nil { return "", err } - db := model.UseDB() - err = db.Create(&model.AuthToken{ - Token: signedToken, - }).Error + q := query.AuthToken + err = q.Create(&model.AuthToken{ + UserID: user.ID, + Token: signedToken, + ExpiredAt: time.Now().Add(ExpiredTime).Unix(), + }) if err != nil { return "", err @@ -55,3 +86,50 @@ func GenerateJWT(name string) (string, error) { return signedToken, err } + +func ValidateJWT(token string) (claims *JWTClaims, err error) { + if token == "" { + err = errors.New("token is empty") + return + } + unsignedToken, err := jwt.ParseWithClaims( + token, + &JWTClaims{}, + func(token *jwt.Token) (interface{}, error) { + return []byte(settings.ServerSettings.JwtSecret), nil + }, + ) + if err != nil { + err = errors.New("parse with claims error") + return + } + claims, ok := unsignedToken.Claims.(*JWTClaims) + if !ok { + err = errors.New("convert to jwt claims error") + return + } + if claims.ExpiresAt < time.Now().UTC().Unix() { + err = errors.New("jwt is expired") + } + return +} + +func CurrentUser(token string) (u *model.Auth, err error) { + // validate token + var claims *JWTClaims + claims, err = ValidateJWT(token) + if err != nil { + return + } + + // get user by id + user := query.Auth + u, err = user.FirstByID(claims.UserID) + if err != nil { + return + } + + logger.Info("[Current User]", u.Name) + + return +} diff --git a/model/auth.go b/model/auth.go index 45720b52..7b361b3c 100644 --- a/model/auth.go +++ b/model/auth.go @@ -1,13 +1,16 @@ package model type Auth struct { - Model + Model - Name string `json:"name"` - Password string `json:"-"` - Status bool `json:"status" gorm:"default:1"` + Name string `json:"name"` + Password string `json:"-"` + Status bool `json:"status" gorm:"default:1"` + OTPSecret []byte `json:"-" gorm:"type:blob"` } type AuthToken struct { - Token string `json:"token"` + UserID int `json:"user_id"` + Token string `json:"token"` + ExpiredAt int64 `json:"expired_at" gorm:"default:0"` } diff --git a/query/auth_tokens.gen.go b/query/auth_tokens.gen.go index b06d7ced..56b24d8d 100644 --- a/query/auth_tokens.gen.go +++ b/query/auth_tokens.gen.go @@ -28,7 +28,9 @@ func newAuthToken(db *gorm.DB, opts ...gen.DOOption) authToken { tableName := _authToken.authTokenDo.TableName() _authToken.ALL = field.NewAsterisk(tableName) + _authToken.UserID = field.NewInt(tableName, "user_id") _authToken.Token = field.NewString(tableName, "token") + _authToken.ExpiredAt = field.NewInt64(tableName, "expired_at") _authToken.fillFieldMap() @@ -38,8 +40,10 @@ func newAuthToken(db *gorm.DB, opts ...gen.DOOption) authToken { type authToken struct { authTokenDo - ALL field.Asterisk - Token field.String + ALL field.Asterisk + UserID field.Int + Token field.String + ExpiredAt field.Int64 fieldMap map[string]field.Expr } @@ -56,7 +60,9 @@ func (a authToken) As(alias string) *authToken { func (a *authToken) updateTableName(table string) *authToken { a.ALL = field.NewAsterisk(table) + a.UserID = field.NewInt(table, "user_id") a.Token = field.NewString(table, "token") + a.ExpiredAt = field.NewInt64(table, "expired_at") a.fillFieldMap() @@ -73,8 +79,10 @@ func (a *authToken) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (a *authToken) fillFieldMap() { - a.fieldMap = make(map[string]field.Expr, 1) + a.fieldMap = make(map[string]field.Expr, 3) + a.fieldMap["user_id"] = a.UserID a.fieldMap["token"] = a.Token + a.fieldMap["expired_at"] = a.ExpiredAt } func (a authToken) clone(db *gorm.DB) authToken { diff --git a/query/auths.gen.go b/query/auths.gen.go index d27499db..1295fb71 100644 --- a/query/auths.gen.go +++ b/query/auths.gen.go @@ -35,6 +35,7 @@ func newAuth(db *gorm.DB, opts ...gen.DOOption) auth { _auth.Name = field.NewString(tableName, "name") _auth.Password = field.NewString(tableName, "password") _auth.Status = field.NewBool(tableName, "status") + _auth.OTPSecret = field.NewBytes(tableName, "otp_secret") _auth.fillFieldMap() @@ -52,6 +53,7 @@ type auth struct { Name field.String Password field.String Status field.Bool + OTPSecret field.Bytes fieldMap map[string]field.Expr } @@ -75,6 +77,7 @@ func (a *auth) updateTableName(table string) *auth { a.Name = field.NewString(table, "name") a.Password = field.NewString(table, "password") a.Status = field.NewBool(table, "status") + a.OTPSecret = field.NewBytes(table, "otp_secret") a.fillFieldMap() @@ -91,7 +94,7 @@ func (a *auth) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (a *auth) fillFieldMap() { - a.fieldMap = make(map[string]field.Expr, 7) + a.fieldMap = make(map[string]field.Expr, 8) a.fieldMap["id"] = a.ID a.fieldMap["created_at"] = a.CreatedAt a.fieldMap["updated_at"] = a.UpdatedAt @@ -99,6 +102,7 @@ func (a *auth) fillFieldMap() { a.fieldMap["name"] = a.Name a.fieldMap["password"] = a.Password a.fieldMap["status"] = a.Status + a.fieldMap["otp_secret"] = a.OTPSecret } func (a auth) clone(db *gorm.DB) auth { diff --git a/router/middleware.go b/router/middleware.go index bcf7944c..18ad6753 100644 --- a/router/middleware.go +++ b/router/middleware.go @@ -58,11 +58,14 @@ func authRequired() gin.HandlerFunc { } } - if user.CheckToken(token) < 1 { + u, ok := user.GetTokenUser(token) + if !ok { abortWithAuthFailure() return } + c.Set("user", u) + if nodeID := c.GetHeader("X-Node-ID"); nodeID != "" { c.Set("ProxyNodeID", nodeID) } diff --git a/router/routers.go b/router/routers.go index eb15a257..d1851ce9 100644 --- a/router/routers.go +++ b/router/routers.go @@ -46,6 +46,7 @@ func InitRouter() *gin.Engine { // Authorization required not websocket request g := root.Group("/", authRequired(), proxy()) { + user.InitUserRouter(g) analytic.InitRouter(g) user.InitManageUserRouter(g) nginx.InitRouter(g) diff --git a/settings/cluster_test.go b/settings/cluster_test.go index 1d01ed0c..235b47b6 100644 --- a/settings/cluster_test.go +++ b/settings/cluster_test.go @@ -11,5 +11,6 @@ func TestCluster(t *testing.T) { assert.Equal(t, []string{ "http://10.0.0.1:9000?name=node1&node_secret=my-node-secret&enabled=true", "http://10.0.0.2:9000?name=node2&node_secret=my-node-secret&enabled=true", + "http://10.0.0.3?name=node3&node_secret=my-node-secret&enabled=true", }, ClusterSettings.Node) } diff --git a/settings/crypto.go b/settings/crypto.go new file mode 100644 index 00000000..57d5e0c6 --- /dev/null +++ b/settings/crypto.go @@ -0,0 +1,14 @@ +package settings + +import "crypto/md5" + +type Crypto struct { + Secret string +} + +var CryptoSettings = Crypto{} + +func (c *Crypto) GetSecretMd5() []byte { + k := md5.Sum([]byte(c.Secret)) + return k[:] +} diff --git a/settings/crypto_test.go b/settings/crypto_test.go new file mode 100644 index 00000000..db10bae5 --- /dev/null +++ b/settings/crypto_test.go @@ -0,0 +1,48 @@ +package settings + +import ( + "crypto/md5" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetSecretMd5_WithNonEmptySecret_ReturnsExpectedMd5Hash(t *testing.T) { + // Setup + CryptoSettings.Secret = "testSecret" + expectedMd5 := md5.Sum([]byte("testSecret")) + expectedMd5String := hex.EncodeToString(expectedMd5[:]) + + // Execute + resultMd5 := CryptoSettings.GetSecretMd5() + resultMd5String := hex.EncodeToString(resultMd5[:]) + + // Verify + assert.Equal(t, expectedMd5String, resultMd5String, "MD5 hash should match for non-empty secret") +} + +func TestGetSecretMd5_WithEmptySecret_ReturnsMd5OfEmptyString(t *testing.T) { + // Setup + CryptoSettings.Secret = "" + expectedMd5 := md5.Sum([]byte("")) + expectedMd5String := hex.EncodeToString(expectedMd5[:]) + + // Execute + resultMd5 := CryptoSettings.GetSecretMd5() + resultMd5String := hex.EncodeToString(resultMd5[:]) + + // Verify + assert.Equal(t, expectedMd5String, resultMd5String, "MD5 hash of an empty string should be returned for empty secret") +} + +func TestGetSecretMd5_WithDifferentSecrets_ReturnsDifferentMd5Hashes(t *testing.T) { + // Setup + CryptoSettings.Secret = "secret1" + firstMd5 := CryptoSettings.GetSecretMd5() + CryptoSettings.Secret = "secret2" + secondMd5 := CryptoSettings.GetSecretMd5() + + // Verify + assert.NotEqual(t, firstMd5, secondMd5, "Different secrets should produce different MD5 hashes") +} diff --git a/settings/settings.go b/settings/settings.go index 12c3c6bf..b29bbbc4 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -28,6 +28,7 @@ var sections = map[string]interface{}{ "logrotate": &LogrotateSettings, "cluster": &ClusterSettings, "auth": &AuthSettings, + "crypto": &CryptoSettings, } func init() { @@ -64,6 +65,7 @@ func Setup() { parseEnv(&CasdoorSettings, "CASDOOR_") parseEnv(&LogrotateSettings, "LOGROTATE_") parseEnv(&AuthSettings, "AUTH_") + parseEnv(&CryptoSettings, "CRYPTO_") // if in official docker, set the restart cmd of nginx to "nginx -s stop", // then the supervisor of s6-overlay will start the nginx again.