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

This commit is contained in:
二丫讲梵
2023-04-04 15:36:22 +08:00
committed by GitHub
parent 29fc9243d6
commit ef030f0498
10 changed files with 111 additions and 50 deletions

View File

@@ -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参数不需要进行任何调整。 - `📢 注意:`如果使用docker部署那么PORT参数不需要进行任何调整。
- `📢 注意:`ALLOW_GROUPS,ALLOW_USERS,ADMIN_USERS三个参数为数组如果需要指定多个可用英文逗号分割。 - `📢 注意:`ALLOW_GROUPS,ALLOW_USERS,ADMIN_USERS三个参数为数组如果需要指定多个可用英文逗号分割。
- `📢 注意:`如果服务器节点本身就在国外或者自定义了`BASE_URL`,那么就把`HTTP_PROXY`参数留空即可。 - `📢 注意:`如果服务器节点本身就在国外或者自定义了`BASE_URL`,那么就把`HTTP_PROXY`参数留空即可。
- `📢 注意:`如果使用docker部署那么proxy地址可以直接使用如上方式部署`host.docker.internal`会指向容器所在宿主机的IP只需要更改端口为你的代理端口即可。参见[Docker容器如何优雅地访问宿主机网络](https://wiki.eryajf.net/pages/674f53/) - `📢 注意:`如果使用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 ```yaml
# 应用的日志级别info or debug
log_level: "info"
# openai api_key # openai api_key
api_key: "xxxxxxxxx" api_key: "xxxxxxxxx"
# 如果你使用官方的接口地址 https://api.openai.com则留空即可如果你想指定请求url的地址可通过这个参数进行配置注意需要带上 http 协议 # 如果你使用官方的接口地址 https://api.openai.com则留空即可如果你想指定请求url的地址可通过这个参数进行配置注意需要带上 http 协议
@@ -363,6 +365,8 @@ allow_groups:
allow_users: ["张三","李四"] allow_users: ["张三","李四"]
# 指定哪些人为此系统的管理员,如果留空,则表示没有人是管理员,如果要限制,则列表中写用户的名称,比如 ["张三","李四"] # 指定哪些人为此系统的管理员,如果留空,则表示没有人是管理员,如果要限制,则列表中写用户的名称,比如 ["张三","李四"]
admin_users: [] admin_users: []
# 钉钉机器人在应用信息中的AppSecret为了校验回调的请求是否合法如果留空将会忽略校验则该接口将会存在其他人也能随意调用的安全隐患因此强烈建议配置正确的secret
app_secret: ""
``` ```
## 常见问题 ## 常见问题

View File

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

View File

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

View File

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

14
main.go
View File

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

View File

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

View File

@@ -13,7 +13,7 @@ import (
// ProcessRequest 分析处理请求逻辑 // ProcessRequest 分析处理请求逻辑
func ProcessRequest(rmsg *dingbot.ReceiveMsg) error { func ProcessRequest(rmsg *dingbot.ReceiveMsg) error {
if public.CheckRequest(rmsg) { if CheckRequestTimes(rmsg) {
content := strings.TrimSpace(rmsg.Text.Content) content := strings.TrimSpace(rmsg.Text.Content)
switch content { switch content {
case "单聊": case "单聊":
@@ -211,3 +211,24 @@ func Do(mode string, rmsg *dingbot.ReceiveMsg) error {
} }
return nil 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
View 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
}

View File

@@ -1,14 +1,9 @@
package public package public
import ( import (
"fmt"
"strings"
"github.com/eryajf/chatgpt-dingtalk/config" "github.com/eryajf/chatgpt-dingtalk/config"
"github.com/eryajf/chatgpt-dingtalk/pkg/cache" "github.com/eryajf/chatgpt-dingtalk/pkg/cache"
"github.com/eryajf/chatgpt-dingtalk/pkg/db" "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 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 = `# 发送信息 var Welcome string = `# 发送信息
若您想给机器人发送信息,有如下两种方式: 若您想给机器人发送信息,有如下两种方式:

View File

@@ -1,6 +1,10 @@
package public package public
import ( import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
@@ -68,3 +72,14 @@ func JudgeAdminUsers(s string) bool {
func GetReadTime(t time.Time) string { func GetReadTime(t time.Time) string {
return t.Format("2006-01-02 15:04:05") 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
}