mirror of
https://github.com/eryajf/chatgpt-dingtalk.git
synced 2025-10-08 09:30:16 +08:00
feat: 添加对回调请求校验的能力,解决可被其他人调用的安全隐患 (#171)
This commit is contained in:
10
README.md
10
README.md
@@ -146,17 +146,17 @@
|
||||
```
|
||||
第一种:基于环境变量运行
|
||||
# 运行项目
|
||||
$ docker run -itd --name chatgpt -p 8090:8090 -v ./data:/app/data --add-host="host.docker.internal:host-gateway" -e APIKEY=换成你的key -e BASE_URL="" -e MODEL="gpt-3.5-turbo" -e SESSION_TIMEOUT=600 -e HTTP_PROXY="http://host.docker.internal:15732" -e DEFAULT_MODE="单聊" -e MAX_REQUEST=0 -e PORT=8090 -e SERVICE_URL="你当前服务外网可访问的URL" -e CHAT_TYPE="0" -e ALLOW_GROUPS=a,b -e ALLOW_USERS=a,b ADMIN_USERS=a,b --restart=always dockerproxy.com/eryajf/chatgpt-dingtalk:latest
|
||||
$ docker run -itd --name chatgpt -p 8090:8090 -v ./data:/app/data --add-host="host.docker.internal:host-gateway" -e LOG_LEVEL="info" -e APIKEY=换成你的key -e BASE_URL="" -e MODEL="gpt-3.5-turbo" -e SESSION_TIMEOUT=600 -e HTTP_PROXY="http://host.docker.internal:15732" -e DEFAULT_MODE="单聊" -e MAX_REQUEST=0 -e PORT=8090 -e SERVICE_URL="你当前服务外网可访问的URL" -e CHAT_TYPE="0" -e ALLOW_GROUPS=a,b -e ALLOW_USERS=a,b ADMIN_USERS=a,b -e APP_SECRET="" --restart=always dockerproxy.com/eryajf/chatgpt-dingtalk:latest
|
||||
```
|
||||
|
||||
> 运行命令中映射的配置文件参考下边的[配置文件说明](#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AF%B4%E6%98%8E)。
|
||||
|
||||
- `📢 注意:`如果使用docker部署,那么PORT参数不需要进行任何调整。
|
||||
- `📢 注意:`ALLOW_GROUPS,ALLOW_USERS,ADMIN_USERS三个参数为数组,如果需要指定多个,可用英文逗号分割。
|
||||
- `📢 注意:`如果服务器节点本身就在国外或者自定义了`BASE_URL`,那么就把`HTTP_PROXY`参数留空即可。
|
||||
- `📢 注意:`如果使用docker部署,那么proxy地址可以直接使用如上方式部署,`host.docker.internal`会指向容器所在宿主机的IP,只需要更改端口为你的代理端口即可。参见:[Docker容器如何优雅地访问宿主机网络](https://wiki.eryajf.net/pages/674f53/)
|
||||
|
||||
|
||||
运行命令中映射的配置文件参考下边的[配置文件说明](#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AF%B4%E6%98%8E)。
|
||||
|
||||
```
|
||||
第二种:基于配置文件挂载运行
|
||||
# 复制配置文件,根据自己实际情况,调整配置里的内容
|
||||
@@ -335,6 +335,8 @@ $ go run main.go
|
||||
## 配置文件说明
|
||||
|
||||
```yaml
|
||||
# 应用的日志级别,info or debug
|
||||
log_level: "info"
|
||||
# openai api_key
|
||||
api_key: "xxxxxxxxx"
|
||||
# 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议
|
||||
@@ -363,6 +365,8 @@ allow_groups:
|
||||
allow_users: ["张三","李四"]
|
||||
# 指定哪些人为此系统的管理员,如果留空,则表示没有人是管理员,如果要限制,则列表中写用户的名称,比如 ["张三","李四"]
|
||||
admin_users: []
|
||||
# 钉钉机器人在应用信息中的AppSecret,为了校验回调的请求是否合法,如果留空,将会忽略校验,则该接口将会存在其他人也能随意调用的安全隐患,因此强烈建议配置正确的secret
|
||||
app_secret: ""
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# 应用的日志级别,info or debug
|
||||
log_level: "info"
|
||||
# openai api_key
|
||||
api_key: "xxxxxxxxx"
|
||||
# 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议
|
||||
@@ -24,3 +26,5 @@ allow_groups: []
|
||||
allow_users: []
|
||||
# 指定哪些人为此系统的管理员,如果留空,则表示没有人是管理员,如果要限制,则列表中写用户的名称,比如 ["张三","李四"]
|
||||
admin_users: []
|
||||
# 钉钉机器人在应用信息中的AppSecret,为了校验回调的请求是否合法,如果留空,将会忽略校验,则该接口将会存在其他人也能随意调用的安全隐患,因此强烈建议配置正确的secret
|
||||
app_secret: ""
|
@@ -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"
|
||||
}
|
||||
|
@@ -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:
|
||||
|
14
main.go
14
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,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)
|
||||
}
|
||||
|
@@ -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
public/chat.go
Normal file
22
public/chat.go
Normal file
@@ -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
|
||||
}
|
@@ -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 = `# 发送信息
|
||||
|
||||
若您想给机器人发送信息,有如下两种方式:
|
||||
|
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user