Browse Source

feat: 添加对回调请求校验的能力,解决可被其他人调用的安全隐患 (#171)

二丫讲梵 2 years ago
parent
commit
ef030f0498
10 changed files with 111 additions and 50 deletions
  1. 7 3
      README.md
  2. 5 1
      config.example.yml
  3. 17 0
      config/config.go
  4. 2 0
      docker-compose.yml
  5. 11 3
      main.go
  6. 10 1
      pkg/logger/logger.go
  7. 22 1
      pkg/process/process_request.go
  8. 22 0
      public/chat.go
  9. 0 41
      public/public.go
  10. 15 0
      public/tools.go

File diff suppressed because it is too large
+ 7 - 3
README.md


+ 5 - 1
config.example.yml

@@ -1,3 +1,5 @@
+# 应用的日志级别,info or debug
+log_level: "info"
 # openai api_key
 api_key: "xxxxxxxxx"
 # 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议
@@ -23,4 +25,6 @@ allow_groups: []
 # 哪些用户可以进行对话,如果留空,则表示允许所有用户,如果要限制,则列表中写用户的名称,比如 ["张三","李四"]
 allow_users: []
 # 指定哪些人为此系统的管理员,如果留空,则表示没有人是管理员,如果要限制,则列表中写用户的名称,比如 ["张三","李四"]
-admin_users: []
+admin_users: []
+# 钉钉机器人在应用信息中的AppSecret,为了校验回调的请求是否合法,如果留空,将会忽略校验,则该接口将会存在其他人也能随意调用的安全隐患,因此强烈建议配置正确的secret
+app_secret: ""

+ 17 - 0
config/config.go

@@ -16,6 +16,8 @@ import (
 
 // Configuration 项目配置
 type Configuration struct {
+	// 日志级别,info或者debug
+	LogLevel string `yaml:"log_level"`
 	// gtp apikey
 	ApiKey string `yaml:"api_key"`
 	// 请求的 URL 地址
@@ -42,6 +44,8 @@ type Configuration struct {
 	AllowUsers []string `yaml:"allow_users"`
 	// 指定哪些人为此系统的管理员,必须指定,否则所有人都是
 	AdminUsers []string `yaml:"admin_users"`
+	// 钉钉机器人在应用信息中的AppSecret,为了校验回调的请求是否合法
+	AppSecret string `yaml:"app_secret"`
 }
 
 var config *Configuration
@@ -62,6 +66,10 @@ func LoadConfig() *Configuration {
 		}
 
 		// 如果环境变量有配置,读取环境变量
+		logLevel := os.Getenv("LOG_LEVEL")
+		if logLevel != "" {
+			config.LogLevel = logLevel
+		}
 		apiKey := os.Getenv("APIKEY")
 		if apiKey != "" {
 			config.ApiKey = apiKey
@@ -122,7 +130,16 @@ func LoadConfig() *Configuration {
 		if adminUsers != "" {
 			config.AdminUsers = strings.Split(adminUsers, ",")
 		}
+		appSecret := os.Getenv("APP_SECRET")
+		if appSecret != "" {
+			config.AppSecret = appSecret
+		}
 	})
+
+	// 一些默认值
+	if config.LogLevel == "" {
+		config.LogLevel = "info"
+	}
 	if config.Model == "" {
 		config.Model = "gpt-3.5-turbo"
 	}

+ 2 - 0
docker-compose.yml

@@ -6,6 +6,7 @@ services:
     image: eryajf/chatgpt-dingtalk:latest
     restart: always
     environment:
+      LOG_LEVEL: "info" # 应用的日志级别 info/debug
       APIKEY: xxxxxx  # 你的 api_key
       BASE_URL: ""  # 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议
       MODEL: "gpt-3.5-turbo" # 指定模型,默认为 gpt-3.5-turbo , 可选参数有: "gpt-4-0314", "gpt-4", "gpt-3.5-turbo-0301", "gpt-3.5-turbo",如果使用gpt-4,请确认自己是否有接口调用白名单
@@ -19,6 +20,7 @@ services:
       ALLOW_GROUPS: "" # 哪些群组可以进行对话,如果留空,则表示允许所有群组,如果要限制,则填写群组的名字,比如 "aa,bb"
       ALLOW_USERS: "" # 哪些用户可以进行对话,如果留空,则表示允许所有用户,如果要限制,则填写用户的名字,比如 "张三,李四"
       ADMIN_USERS: "" # 指定哪些人为此系统的管理员,如果留空,则表示没有人是管理员,如果要限制,则填写用户的名字,比如 "张三,李四"
+      APP_SECRET: "" # 钉钉机器人在应用信息中的AppSecret,为了校验回调的请求是否合法,如果留空,将会忽略校验,则该接口将会存在其他人也能随意调用的安全隐患,因此强烈建议配置正确的secret
     volumes:
       - ./data:/app/data
     ports:

+ 11 - 3
main.go

@@ -19,6 +19,7 @@ import (
 
 func init() {
 	public.InitSvc()
+	logger.InitLogger(public.Config.LogLevel)
 }
 func main() {
 	Start()
@@ -32,15 +33,22 @@ func Start() {
 		if err != nil {
 			return ship.ErrBadRequest.New(fmt.Errorf("bind to receivemsg failed : %v", err))
 		}
+		// 先校验回调是否合法
+		if !public.CheckRequest(c.GetReqHeader("timestamp"), c.GetReqHeader("sign")) {
+			logger.Warning("该请求不合法,可能是其他企业或者未经允许的应用调用所致,请知悉!")
+			return nil
+		}
+		// 再校验回调参数是否有价值
 		if msgObj.Text.Content == "" || msgObj.ChatbotUserID == "" {
 			logger.Warning("从钉钉回调过来的内容为空,根据过往的经验,或许重新创建一下机器人,能解决这个问题")
 			return ship.ErrBadRequest.New(fmt.Errorf("从钉钉回调过来的内容为空,根据过往的经验,或许重新创建一下机器人,能解决这个问题"))
 		}
 		// 去除问题的前后空格
 		msgObj.Text.Content = strings.TrimSpace(msgObj.Text.Content)
-		// 打印钉钉回调过来的请求明细
-		// logger.Info(fmt.Sprintf("dingtalk callback parameters: %#v", msgObj))
-		// TODO: 校验请求
+		// 打印钉钉回调过来的请求明细,调试时打开
+		fmt.Println("=======", logger.Logger.GetLevel().String())
+		logger.Debug(fmt.Sprintf("dingtalk callback parameters: %#v", msgObj))
+
 		if public.Config.ChatType != "0" && msgObj.ConversationType != public.Config.ChatType {
 			_, err = msgObj.ReplyToDingtalk(string(dingbot.TEXT), "抱歉,管理员禁用了这种聊天方式,请选择其他聊天方式与机器人对话!")
 			if err != nil {

+ 10 - 1
pkg/logger/logger.go

@@ -10,10 +10,15 @@ import (
 var Logger *log.Logger
 var once sync.Once
 
-func init() {
+func InitLogger(level string) {
 	once.Do(func() {
 		Logger = log.New(os.Stderr)
 	})
+	if level == "debug" {
+		Logger.SetLevel(log.DebugLevel)
+	} else {
+		Logger.SetLevel(log.InfoLevel)
+	}
 }
 
 func Info(args ...interface{}) {
@@ -24,6 +29,10 @@ func Warning(args ...interface{}) {
 	Logger.Warn(args)
 }
 
+func Debug(args ...interface{}) {
+	Logger.Debug(args)
+}
+
 func Error(args ...interface{}) {
 	Logger.Error(args)
 }

+ 22 - 1
pkg/process/process_request.go

@@ -13,7 +13,7 @@ import (
 
 // ProcessRequest 分析处理请求逻辑
 func ProcessRequest(rmsg *dingbot.ReceiveMsg) error {
-	if public.CheckRequest(rmsg) {
+	if CheckRequestTimes(rmsg) {
 		content := strings.TrimSpace(rmsg.Text.Content)
 		switch content {
 		case "单聊":
@@ -211,3 +211,24 @@ func Do(mode string, rmsg *dingbot.ReceiveMsg) error {
 	}
 	return nil
 }
+
+// CheckRequestTimes 分析处理请求逻辑
+// 主要提供单日请求限额的功能
+func CheckRequestTimes(rmsg *dingbot.ReceiveMsg) bool {
+	if public.Config.MaxRequest == 0 {
+		return true
+	}
+	count := public.UserService.GetUseRequestCount(rmsg.GetSenderIdentifier())
+	// 判断访问次数是否超过限制
+	if count >= public.Config.MaxRequest {
+		logger.Info(fmt.Sprintf("亲爱的: %s,您今日请求次数已达上限,请明天再来,交互发问资源有限,请务必斟酌您的问题,给您带来不便,敬请谅解!", rmsg.SenderNick))
+		_, err := rmsg.ReplyToDingtalk(string(dingbot.TEXT), fmt.Sprintf("一个好的问题,胜过十个好的答案!\n亲爱的: %s,您今日请求次数已达上限,请明天再来,交互发问资源有限,请务必斟酌您的问题,给您带来不便,敬请谅解!", rmsg.SenderNick))
+		if err != nil {
+			logger.Warning(fmt.Errorf("send message error: %v", err))
+		}
+		return false
+	}
+	// 访问次数未超过限制,将计数加1
+	public.UserService.SetUseRequestCount(rmsg.GetSenderIdentifier(), count+1)
+	return true
+}

+ 22 - 0
public/chat.go

@@ -0,0 +1,22 @@
+package public
+
+import (
+	"strings"
+
+	"github.com/eryajf/chatgpt-dingtalk/pkg/dingbot"
+)
+
+func FirstCheck(rmsg *dingbot.ReceiveMsg) bool {
+	lc := UserService.GetUserMode(rmsg.GetSenderIdentifier())
+	if lc == "" {
+		if Config.DefaultMode == "串聊" {
+			return true
+		} else {
+			return false
+		}
+	}
+	if lc != "" && strings.Contains(lc, "串聊") {
+		return true
+	}
+	return false
+}

+ 0 - 41
public/public.go

@@ -1,14 +1,9 @@
 package public
 
 import (
-	"fmt"
-	"strings"
-
 	"github.com/eryajf/chatgpt-dingtalk/config"
 	"github.com/eryajf/chatgpt-dingtalk/pkg/cache"
 	"github.com/eryajf/chatgpt-dingtalk/pkg/db"
-	"github.com/eryajf/chatgpt-dingtalk/pkg/dingbot"
-	"github.com/eryajf/chatgpt-dingtalk/pkg/logger"
 )
 
 var UserService cache.UserServiceInterface
@@ -30,42 +25,6 @@ func InitSvc() {
 	// }
 }
 
-func FirstCheck(rmsg *dingbot.ReceiveMsg) bool {
-	lc := UserService.GetUserMode(rmsg.GetSenderIdentifier())
-	if lc == "" {
-		if Config.DefaultMode == "串聊" {
-			return true
-		} else {
-			return false
-		}
-	}
-	if lc != "" && strings.Contains(lc, "串聊") {
-		return true
-	}
-	return false
-}
-
-// ProcessRequest 分析处理请求逻辑
-// 主要提供单日请求限额的功能
-func CheckRequest(rmsg *dingbot.ReceiveMsg) bool {
-	if Config.MaxRequest == 0 {
-		return true
-	}
-	count := UserService.GetUseRequestCount(rmsg.GetSenderIdentifier())
-	// 判断访问次数是否超过限制
-	if count >= Config.MaxRequest {
-		logger.Info(fmt.Sprintf("亲爱的: %s,您今日请求次数已达上限,请明天再来,交互发问资源有限,请务必斟酌您的问题,给您带来不便,敬请谅解!", rmsg.SenderNick))
-		_, err := rmsg.ReplyToDingtalk(string(dingbot.TEXT), fmt.Sprintf("一个好的问题,胜过十个好的答案!\n亲爱的: %s,您今日请求次数已达上限,请明天再来,交互发问资源有限,请务必斟酌您的问题,给您带来不便,敬请谅解!", rmsg.SenderNick))
-		if err != nil {
-			logger.Warning(fmt.Errorf("send message error: %v", err))
-		}
-		return false
-	}
-	// 访问次数未超过限制,将计数加1
-	UserService.SetUseRequestCount(rmsg.GetSenderIdentifier(), count+1)
-	return true
-}
-
 var Welcome string = `# 发送信息
 
 若您想给机器人发送信息,有如下两种方式:

+ 15 - 0
public/tools.go

@@ -1,6 +1,10 @@
 package public
 
 import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"strings"
@@ -68,3 +72,14 @@ func JudgeAdminUsers(s string) bool {
 func GetReadTime(t time.Time) string {
 	return t.Format("2006-01-02 15:04:05")
 }
+
+func CheckRequest(ts, sg string) bool {
+	appSecret := Config.AppSecret
+	if appSecret == "" {
+		return true
+	}
+	stringToSign := fmt.Sprintf("%s\n%s", ts, appSecret)
+	mac := hmac.New(sha256.New, []byte(appSecret))
+	_, _ = mac.Write([]byte(stringToSign))
+	return base64.StdEncoding.EncodeToString(mac.Sum(nil)) == sg
+}