Skip to content

Commit c95f0a7

Browse files
committed
feat(config): 添加回调密钥配置
在 `backup.conf` 中为回调功能引入了新的配置项。新增了 `callback_secret` 变量,用于验证回调请求的安全性,为新的通知功能奠定基础。 feat(config): add callback secret configuration Introduces a new section for callback settings in `backup.conf`. This adds the `callback_secret` variable to secure callback requests, laying the groundwork for the new notification feature.
1 parent f372e6c commit c95f0a7

File tree

8 files changed

+572
-1
lines changed

8 files changed

+572
-1
lines changed

backup.conf

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,10 @@ RUN_BACKUP_VERIFICATION=true
214214
# - CONTAINER_EXCLUDE_MOUNTS=true/false
215215
# - CONTAINER_PAUSE_DURING_BACKUP=true/false
216216
# - CONTAINER_PRE_BACKUP_SCRIPT="/path/to/script.sh"
217-
# - CONTAINER_POST_BACKUP_SCRIPT="/path/to/script.sh"
217+
# - CONTAINER_POST_BACKUP_SCRIPT="/path/to/script.sh"
218+
# ============================================================================
219+
# 回调配置
220+
# ============================================================================
221+
222+
# 回调密钥(用于验证回调请求的安全性)
223+
callback_secret="a_very_secure_random_string_12345!@#$%^&*()"

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module dcoker_backup_script
2+
3+
go 1.24.4

go/callback/README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# 安全回调服务器
2+
3+
## 功能简介
4+
5+
此工具提供了一个带签名验证的安全回调接口,用于触发 Docker 容器和卷的备份操作。
6+
7+
## 配置
8+
9+
在使用此回调服务器之前,您需要在项目根目录下的 `backup.conf` 文件中配置 `callback_secret`
10+
11+
```bash
12+
# backup.conf
13+
callback_secret=your_secret_key_here
14+
```
15+
16+
## 编译服务器
17+
18+
`go/callback/` 目录中运行以下命令来编译服务器:
19+
20+
```bash
21+
go build -o ../../callback_server .
22+
```
23+
24+
这将在项目根目录生成一个名为 `callback_server` 的可执行文件。
25+
26+
## 运行服务器
27+
28+
切换到项目根目录并运行生成的可执行文件:
29+
30+
```bash
31+
./callback_server
32+
```
33+
34+
服务器将在 `localhost:8080` 启动。
35+
36+
## API 端点
37+
38+
### `POST /backup`
39+
40+
此端点用于触发备份操作。
41+
42+
**Headers:**
43+
44+
- `X-Signature`: 请求签名,用于验证请求来源。签名是使用配置的 `callback_secret` 对请求体进行 HMAC-SHA256 计算得出的。
45+
46+
**Request Body:**
47+
48+
```json
49+
{
50+
"args": ["arg1", "arg2"]
51+
}
52+
```
53+
54+
- `args`: 传递给备份脚本的参数数组。
55+
56+
## 编译并使用客户端示例
57+
58+
`go/callback/` 目录中运行以下命令来编译客户端示例:
59+
60+
```bash
61+
go build -o client_example client_example.go
62+
```
63+
64+
由于 `client_example.go` 文件被构建标签 `//go:build ignore` 标记,所以编译时必须显式指定文件名。
65+
66+
然后可以使用以下命令来调用回调接口:
67+
68+
```bash
69+
./client_example -a --volumes
70+
```
71+
72+
## Client Examples
73+
74+
### Python 示例
75+
76+
依赖安装: `pip install requests`,运行命令: `python client_example.py [args...]`
77+
78+
### JavaScript (Node.js) 示例
79+
80+
依赖安装: 无需额外依赖,运行命令: `node client_example.js [args...]`
81+
82+
### TypeScript 示例
83+
84+
依赖安装: `npm install -g typescript ts-node`,运行命令: `ts-node client_example.ts [args...]`
85+
86+
### cURL 示例 (Linux/macOS)
87+
88+
您也可以使用 `curl` 和其他命令行工具直接触发回调。这是一个一键执行的示例,它会自动从 `backup.conf` 读取密钥、生成签名并发起请求:
89+
90+
```shell
91+
# 注意:请在 go/callback 目录下运行此命令
92+
SECRET=$(grep 'callback_secret' ../../backup.conf | cut -d '"' -f 2)
93+
BODY='{"args": ["-a", "--volumes"]}'
94+
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
95+
96+
curl -X POST \
97+
-H "Content-Type: application/json" \
98+
-H "X-Signature: $SIGNATURE" \
99+
-d "$BODY" \
100+
http://localhost:47731/backup
101+
```

go/callback/client_example.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//go:build ignore
2+
3+
package main
4+
5+
import (
6+
"bytes"
7+
"crypto/hmac"
8+
"crypto/sha256"
9+
"encoding/hex"
10+
"encoding/json"
11+
"fmt"
12+
"io"
13+
"net/http"
14+
"os"
15+
"strings"
16+
)
17+
18+
const (
19+
callbackURL = "http://localhost:47731/backup"
20+
signatureHeader = "X-Signature"
21+
)
22+
23+
var configFilePath = "../../backup.conf"
24+
25+
// RequestBody represents the JSON structure for the request
26+
type RequestBody struct {
27+
Args []string `json:"args"`
28+
}
29+
30+
// readConfig reads the configuration file and extracts the callback secret.
31+
func readConfig() (string, error) {
32+
content, err := os.ReadFile(configFilePath)
33+
if err != nil {
34+
return "", fmt.Errorf("failed to read config file: %w", err)
35+
}
36+
37+
lines := strings.Split(string(content), "\n")
38+
for _, line := range lines {
39+
// Skip comments and empty lines
40+
trimmedLine := strings.TrimSpace(line)
41+
if trimmedLine == "" || strings.HasPrefix(trimmedLine, "#") {
42+
continue
43+
}
44+
45+
// Check if the line contains the callback_secret
46+
if strings.HasPrefix(trimmedLine, "callback_secret") {
47+
parts := strings.SplitN(trimmedLine, "=", 2)
48+
if len(parts) == 2 {
49+
// Remove quotes if present
50+
secret := strings.TrimSpace(parts[1])
51+
secret = strings.Trim(secret, "\"'")
52+
return secret, nil
53+
}
54+
}
55+
}
56+
return "", fmt.Errorf("callback_secret not found in config file")
57+
}
58+
59+
// generateSignature generates an HMAC-SHA256 signature for the given body using the provided secret.
60+
func generateSignature(body []byte, secret string) string {
61+
h := hmac.New(sha256.New, []byte(secret))
62+
h.Write(body)
63+
signature := h.Sum(nil)
64+
return "sha256=" + hex.EncodeToString(signature)
65+
}
66+
67+
func main() {
68+
if len(os.Args) < 2 {
69+
fmt.Println("Usage: client_example.go <arg1> [arg2] ...")
70+
os.Exit(1)
71+
}
72+
73+
// Read the callback secret from the config file
74+
secret, err := readConfig()
75+
if err != nil {
76+
fmt.Printf("Error reading config: %v\n", err)
77+
os.Exit(1)
78+
}
79+
80+
// Get command line arguments (excluding program name)
81+
args := os.Args[1:]
82+
83+
// Create the request body
84+
requestBody := RequestBody{
85+
Args: args,
86+
}
87+
88+
// Marshal the request body to JSON
89+
jsonBody, err := json.Marshal(requestBody)
90+
if err != nil {
91+
fmt.Printf("Error marshaling JSON: %v\n", err)
92+
os.Exit(1)
93+
}
94+
95+
// Generate the signature
96+
signature := generateSignature(jsonBody, secret)
97+
98+
// Create the HTTP request
99+
req, err := http.NewRequest("POST", callbackURL, bytes.NewBuffer(jsonBody))
100+
if err != nil {
101+
fmt.Printf("Error creating request: %v\n", err)
102+
os.Exit(1)
103+
}
104+
105+
// Set headers
106+
req.Header.Set("Content-Type", "application/json")
107+
req.Header.Set(signatureHeader, signature)
108+
109+
// Send the request
110+
client := &http.Client{}
111+
resp, err := client.Do(req)
112+
if err != nil {
113+
fmt.Printf("Error sending request: %v\n", err)
114+
os.Exit(1)
115+
}
116+
defer resp.Body.Close()
117+
118+
// Read and print the response
119+
respBody, err := io.ReadAll(resp.Body)
120+
if err != nil {
121+
fmt.Printf("Error reading response: %v\n", err)
122+
os.Exit(1)
123+
}
124+
125+
fmt.Printf("Response Status: %s\n", resp.Status)
126+
fmt.Printf("Response Body: %s\n", string(respBody))
127+
}

go/callback/client_example.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const fs = require("fs");
2+
const crypto = require("crypto");
3+
const http = require("http");
4+
5+
// 从 ../../backup.conf 同步读取 callback_secret
6+
const configPath = "../../backup.conf";
7+
const configContent = fs.readFileSync(configPath, "utf8");
8+
const callbackSecret = configContent.trim();
9+
10+
// 获取命令行参数(排除 node 和脚本路径)
11+
const args = process.argv.slice(2);
12+
13+
// 构造 JSON 字符串
14+
const requestBody = JSON.stringify({ args: args });
15+
16+
// 使用 HMAC-SHA256 生成签名
17+
const signature = crypto
18+
.createHmac("sha256", callbackSecret)
19+
.update(requestBody)
20+
.digest("hex");
21+
22+
// 定义请求选项
23+
const options = {
24+
hostname: "localhost",
25+
port: 47731,
26+
path: "/backup",
27+
method: "POST",
28+
headers: {
29+
"Content-Type": "application/json",
30+
"X-Signature": signature,
31+
"Content-Length": Buffer.byteLength(requestBody),
32+
},
33+
};
34+
35+
// 发送 HTTP 请求
36+
const req = http.request(options, (res) => {
37+
let data = "";
38+
39+
// 监听数据接收
40+
res.on("data", (chunk) => {
41+
data += chunk;
42+
});
43+
44+
// 监听响应结束
45+
res.on("end", () => {
46+
console.log("Response from server:");
47+
console.log(data);
48+
});
49+
});
50+
51+
// 监听请求错误
52+
req.on("error", (e) => {
53+
console.error(`Problem with request: ${e.message}`);
54+
});
55+
56+
// 发送请求体
57+
req.write(requestBody);
58+
req.end();

go/callback/client_example.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import sys
5+
import json
6+
import hmac
7+
import hashlib
8+
import requests
9+
import os
10+
11+
12+
def load_callback_secret(config_path):
13+
"""Load callback_secret from the configuration file."""
14+
try:
15+
with open(config_path, "r") as f:
16+
for line in f:
17+
if line.startswith("callback_secret="):
18+
# Remove the 'callback_secret=' prefix and any surrounding whitespace or quotes
19+
secret = line[len("callback_secret=") :].strip().strip("\"'")
20+
return secret
21+
except FileNotFoundError:
22+
print(f"Error: Configuration file '{config_path}' not found.")
23+
sys.exit(1)
24+
except Exception as e:
25+
print(f"Error reading configuration file: {e}")
26+
sys.exit(1)
27+
28+
print("Error: 'callback_secret' not found in configuration file.")
29+
sys.exit(1)
30+
31+
32+
def main():
33+
# Define the path to the configuration file
34+
config_path = os.path.join(os.path.dirname(__file__), "..", "..", "backup.conf")
35+
36+
# Load the callback secret
37+
callback_secret = load_callback_secret(config_path)
38+
39+
# Get command line arguments
40+
args = sys.argv[1:]
41+
42+
# Create the JSON payload
43+
payload = {"args": args}
44+
json_data = json.dumps(payload, separators=(",", ":"))
45+
46+
# Generate HMAC-SHA256 signature
47+
signature = hmac.new(
48+
callback_secret.encode("utf-8"), json_data.encode("utf-8"), hashlib.sha256
49+
).hexdigest()
50+
51+
# Prepare headers
52+
headers = {"Content-Type": "application/json", "X-Signature": signature}
53+
54+
# Send POST request
55+
url = "http://localhost:47731/backup"
56+
try:
57+
response = requests.post(url, data=json_data, headers=headers)
58+
print(f"Status Code: {response.status_code}")
59+
print(f"Response Body: {response.text}")
60+
except requests.exceptions.RequestException as e:
61+
print(f"Error sending request: {e}")
62+
sys.exit(1)
63+
64+
65+
if __name__ == "__main__":
66+
main()

0 commit comments

Comments
 (0)