gin实现webhook功能
背景
之前,我写文章的时候,每次都需要 git push
之后,再到服务器上执行git pull
命令,然后再执行 npm run docs:build
命令,才能更新文章,这样非常麻烦,就想着自动化这整个过程,所以就写了一个 webhook
功能,实现自动更新文章。
实现
前期准备
1. 创建一个 gitee 仓库
这里创建一个 test 仓库,点击 管理
按钮,会打开仓库管理界面
2. 仓库管理页面
3. 打开 WebHooks 管理页面
点击 WebHooks
按钮后,打开 WebHooks 管理
页面
4. WebHooks 管理页面
点击 添加 webHook
按钮,打开添加 WebHooks 页面
5. 添加 WebHook 页面
6. 填写 URL 和 WebHook 密码
7. 填写 URL 和 WebHook 签名密钥
8. 项目的准备工作
- 创建项目目录:
shell
mkdir webhook
- 初始化 go 模块
shell
go mod init webhook
- 安装依赖:
shell
# gin 框架
go get github.com/gin-gonic/gin
# 日志
go get gopkg.in/natefinch/lumberjack.v2
# 环境变量
go get github.com/joho/godotenv
代码实现
1. 解析配置
创建 config 目录
创建 config.go 文件:
shelltouch config.go
配置参考:
txtTIMESTAMP_TOLERANCE=300 GITEE_SIGN_KEY=xxx MAX_SHELL_EXEC_CONCURRENT=1 PORT=8080 LOG_FILE_PATH=logs/app.log
写入以下内容:
gopackage config import ( "log" "os" "strconv" "github.com/joho/godotenv" ) // LoadEnv 加载 .env 文件中的配置 func LoadEnv() { err := godotenv.Load(".env") if err != nil { log.Fatalf("加载 .env 文件失败: %v", err) } } // getEnv 获取指定键的值,如果不存在则返回默认值 func getEnv(key string, defaultValue string) string { value := os.Getenv(key) if value == "" { return defaultValue } return value } func GetTimestampTolerance() int64 { value := getEnv("TIMESTAMP_TOLERANCE", "300") timestampTolerance, err := strconv.ParseInt(value, 10, 64) if err != nil { return 300 } return timestampTolerance } func GetGiteeSignKey() string { return getEnv("GITEE_SIGN_KEY", "") } func GetMaxShellExecConcurrentSize() int { value := getEnv("MAX_CONCURRENT", "1") maxShellExecConcurrentSize, err := strconv.Atoi(value) if err != nil { return 1 } if maxShellExecConcurrentSize < 1 { return 1 } return maxShellExecConcurrentSize } func GetPort() int { value := getEnv("PORT", "8080") port, err := strconv.Atoi(value) if err != nil { return 8080 } return port } func GetLogFilePath() string { return getEnv("LOG_FILE_PATH", "logs/app.log") }
2. 核心逻辑
- 创建
main.go
文件:
shell
touch main.go
- 写入以下内容:
go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
"gopkg.in/natefinch/lumberjack.v2"
"log"
"os/exec"
"strconv"
"time"
"webhook/config"
)
// 控制最大并发执行 shell 命令的数量
var maxShellExecConcurrent chan struct{}
// 处理Gitee 仓库的 Webhook请求
func test(c *gin.Context) {
err := validateGiteeToken(c)
if err != nil {
log.Printf("验证gitee token失败: %v", err)
return
}
// 执行shell命令
ExecuteShellCommandAsync("cd /www/wwwroot/test && git pull origin master && npm run docs:build")
// 返回成功响应
c.String(http.StatusOK, "webhook处理成功")
}
func validateGiteeToken(c *gin.Context) error {
// 获取Gitee请求头
giteeToken := c.GetHeader("X-Gitee-Token")
giteeTimestamp := c.GetHeader("X-Gitee-Timestamp")
if giteeToken == "" || giteeTimestamp == "" {
c.String(http.StatusBadRequest, "没有header头")
return errors.New("没有header头")
}
// 从配置中读取时间差
timestampTolerance := config.GetTimestampTolerance()
// 验证时间戳是否在有效期内
currentTime := time.Now().Unix()
timestamp, err := strconv.ParseInt(giteeTimestamp, 10, 64)
if err != nil {
c.String(http.StatusInternalServerError, "时间戳格式错误")
return errors.New("时间戳格式错误")
}
// 将毫秒转换为秒
timestampInSeconds := timestamp / 1000
if abs(currentTime-timestampInSeconds) > int64(timestampTolerance) {
c.String(http.StatusUnauthorized, "时间戳不正确")
return errors.New("时间戳不正确")
}
// 计算签名
signKey := config.GetGiteeSignKey()
secStr := fmt.Sprintf("%s\n%s", giteeTimestamp, signKey)
mac := hmac.New(sha256.New, []byte(signKey))
mac.Write([]byte(secStr))
computeToken := base64.StdEncoding.EncodeToString(mac.Sum(nil))
// 安全比较token
if !hmac.Equal([]byte(computeToken), []byte(giteeToken)) {
c.String(http.StatusUnauthorized, "token不正确")
return errors.New("token不正确")
}
return nil
}
// 计算两个数的绝对值
func abs(x int64) int64 {
if x < 0 {
return -x
}
return x
}
func ExecuteShellCommandAsync(command string) {
maxShellExecConcurrent <- struct{}{}
go func() {
// 执行 shell 命令
cmd := exec.Command("sh", "-c", command)
// 不能缺少 HOME=/root 否则会报错:
// fatal: detected dubious ownership in repository at '/www/wwwroot/test'
// To add an exception for this directory, call:
// git config --global --add safe.directory /www/wwwroot/test
cmd.Env = append(os.Environ(), "HOME=/root")
// 获取输出结果
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf("执行shell命令失败,失败原因: %s", output)
return
}
log.Printf("执行成功,输出结果: %s", output)
<-maxShellExecConcurrent
}()
}
// 新增日志配置函数
func newLogger() *lumberjack.Logger {
return &lumberjack.Logger{
Filename: config.GetLogFilePath(),
MaxSize: 10,
MaxBackups: 0,
MaxAge: 0,
Compress: false,
}
}
func main() {
// 加载环境变量
config.LoadEnv()
// 配置日志
log.SetOutput(newLogger())
// 从配置中读取执行 shell 命令的最大并发数
maxShellExecConcurrentSize := config.GetMaxShellExecConcurrentSize()
maxShellExecConcurrent = make(chan struct{}, maxShellExecConcurrentSize)
port := config.GetPort()
router := gin.Default()
// 首页
router.GET("", func(c *gin.Context) {
c.String(http.StatusOK, "shenlink gitee/github webhook")
})
// Gitee Webhook 路由分组
giteeGroup := router.Group("/test")
giteeGroup.POST("/test", test)
// 启动服务
router.Run(":" + strconv.Itoa(port))
}
部署
go 项目部署还是挺容易的,但是考虑到后续的运维工作,直接使用宝塔面板来部署了。
打开宝塔面板,点击左侧菜单栏的
网站
,打开网站管理页面后,选择Go项目
,点击添加GO项目
按钮打开添加GO项目页面后,可以看到需要填写的信息
填写项目信息
注意:这里我使用的是
root
用户,是因为git
所属的用户也是root
,一般情况下,使用www
用户就行了
点击 确定
按钮后,go 项目就会自动运行了。
打开项目管理页面
GO 项目管理页面
点击左侧的
SSL
选项,配置SSL证书
SSL证书配置页面
选择 Let's Encrypt
选项,勾选 全选
,然后点击 申请证书
按钮,稍等一段时间,证书就会申请下来了
保存证书
查看运行状态
测试运行
测试首页
测试 webhook
点击查看 webhook 结果
查看 webhook 结果