feat agent签名
This commit is contained in:
parent
8d5678b967
commit
c8cd70e67e
|
|
@ -3,23 +3,20 @@
|
|||
## 一、整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 阶段1:证书生成(客户机器) │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ 隐藏 Agent │ ─加密─→ │ hardware- │ ──发给你──→ │
|
||||
│ │ (mysqld) │ │ info.properties│ │
|
||||
│ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ 解密
|
||||
┌────────────────────────────────────────────────────── ───┐
|
||||
│ 阶段1:证书生成(客户机器) │
|
||||
│ │
|
||||
│ ┌──────────── ─┐ ┌─────────── ──┐ │
|
||||
│ │ licence-tool │ ─加密─→ │ │ ──发给你──→ │
|
||||
│ │ │ │ info.properties │ │
|
||||
│ └─────────── ──┘ └─────────── ──┘ │
|
||||
└────────────────────────────────────────────────────── ───┘
|
||||
↓ 解密(功能放在licence-server)
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 你生成证书 │
|
||||
│ │
|
||||
│ license.lic: │
|
||||
│ ├── hardwareHash (SHA256) │
|
||||
│ ├── publicKey (用于运行时验证) │
|
||||
│ ├── 证书有效期 │
|
||||
│ └── 客户ID │
|
||||
│ license.lic
|
||||
publicCerts.keystore │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ 发给客户
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
|
|
@ -47,7 +44,7 @@ service:
|
|||
|
||||
encryption:
|
||||
algorithm: AES-256-GCM # 对称加密
|
||||
key: <嵌入到二进制里,混淆> # 密钥
|
||||
key: <嵌入到二进制里,混淆> # 密钥
|
||||
|
||||
signing:
|
||||
algorithm: RSA-2048 # 非对称签名
|
||||
|
|
@ -88,14 +85,9 @@ String decrypted = aesDecrypt(encrypted, key); // 明文是 JSON
|
|||
"randomNonce": "customer-provided-nonce"
|
||||
}
|
||||
|
||||
// 生成证书
|
||||
LicenseContent:
|
||||
├── hardwareHash = SHA256(所有硬件信息拼接)
|
||||
├── publicKey = "用于运行时验证的公钥"
|
||||
├── customerId = "客户ID"
|
||||
├── validFrom = 证书生效时间
|
||||
├── validUntil = 证书过期时间
|
||||
└── extra = { agentVersion, keyVersion }
|
||||
// 生成证书 现有逻辑
|
||||
license.lic
|
||||
publicCerts.keystore
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -106,8 +98,8 @@ LicenseContent:
|
|||
|
||||
```
|
||||
客户启动容器时:
|
||||
1. licence-client 读取 license.lic
|
||||
2. 提取 hardwareHash 和 publicKey
|
||||
1. licence-client 读取 license.lic publicCerts.keystore
|
||||
2. 提取 hardwareInfo
|
||||
3. 生成一个随机 sessionKey(每次启动不同)
|
||||
4. sessionKey 用 publicKey 加密后发给 Agent
|
||||
5. Agent 用私钥解密,得到 sessionKey
|
||||
|
|
@ -334,7 +326,7 @@ Content-Type: application/json
|
|||
|
||||
```
|
||||
客户找你续期时:
|
||||
1. 要求客户提供新的 hardware-info.properties
|
||||
1. 要求客户提供新的 info.properties
|
||||
2. 你解密,比对硬件指纹
|
||||
3. 如果变了 → 标记为可疑
|
||||
4. 可疑处理:
|
||||
|
|
@ -342,20 +334,6 @@ Content-Type: application/json
|
|||
- 或联系客户确认
|
||||
```
|
||||
|
||||
### 6.2 日志上报(需要客户配合)
|
||||
|
||||
```java
|
||||
// licence-client 内部日志(客户看不到)
|
||||
LogEntry:
|
||||
- timestamp
|
||||
- hardwareFingerprint (hash)
|
||||
- challengeResponseTime
|
||||
- sessionId
|
||||
|
||||
// 只有你解密日志时才能看到
|
||||
// 客户无法修改这些日志
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、攻击难度对比
|
||||
|
|
@ -399,7 +377,7 @@ LogEntry:
|
|||
│ curl -X POST http://localhost:3306/api/v1/query \ │
|
||||
│ -d '{"action": "generate"}' │
|
||||
│ │
|
||||
│ Agent 返回:加密后的 hardware-info.properties │
|
||||
│ Agent 返回:加密后的 info.properties │
|
||||
│ 客户把这个加密文件发给你 │
|
||||
│ │
|
||||
│ 你解密,获取真实硬件信息,生成 license.lic │
|
||||
|
|
@ -433,7 +411,7 @@ LogEntry:
|
|||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Agent 二进制内嵌: │
|
||||
│ ├── RSA-2048 私钥(用于签名) │
|
||||
│ ├── AES-256 密钥(用于加密 hardware-info.properties) │
|
||||
│ ├── AES-256 密钥(用于加密 info.properties) │
|
||||
│ └── 验证用公钥(给 licence-client) │
|
||||
│ │
|
||||
│ licence-client 内嵌: │
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -32,7 +32,7 @@ func GetHardwareInfo() HardwareInfo {
|
|||
|
||||
func PrintHardwareInfo(info HardwareInfo) {
|
||||
fmt.Println("========================================")
|
||||
fmt.Println(" 服务器硬件信息")
|
||||
fmt.Println(" 信息")
|
||||
fmt.Println("========================================")
|
||||
|
||||
fmt.Println("\nIP地址:")
|
||||
|
|
|
|||
|
|
@ -1,27 +1,98 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Agent 签名密钥(建议通过配置文件或环境变量配置)
|
||||
var signKey = "licence-secret-sign-key-trustie"
|
||||
// ==================== 密钥配置(混淆后的字节数组)====================
|
||||
// RSA 私钥片段(PEM 格式)- 实际使用时由外部注入或预埋
|
||||
var rsaPrivateKeyPEM = `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDqgCrrAvQV5jc3
|
||||
6OTv4kD9bIZ0rcm4z4dixcq4If90T1ATS1ahiIIhO3iYNAQFeR5T53/UUoPO7zLW
|
||||
h8dyfmMT7jTM6Tanlt+upCF8qXBwT/qUu92xbaa5B7GeLES6vrlMaki206q6EN87
|
||||
5IugRcIGMaxoa5jkH/cd8QKRNsljUrkoD7W6xWHOt0cV+LS7meDBZtgIcqMeeHdL
|
||||
++ZlAUzcCCcEvW5VZY4M24ryIsP5KnV7VZyMhDjY4iBmQRgnAP+pTZo5lDhHEfDj
|
||||
B+YPVYRABtxdcaXBeaDkbvE03ZkxCjpE4d9BYL3UMXyQisDg0IH+hN/iawMaSMpm
|
||||
pIG8qlXHAgMBAAECggEAFOge+Dt+Rt+MbiWLSuAsmouAHbHxNjlf+oxKa3BlUozQ
|
||||
ElBMTA2CfVnMgbrz3gfZqAuPjkuNaQS5aLGYgUD6yCgCH0y7H/9OOC2o6j2laSLJ
|
||||
4yD9F5iZ9yaHWhdCfDXAPN6/vRERyEUnaeZErohzNZ/2UFiOooDZETUC3Uh5qJGa
|
||||
4d8VXrloYL74X1fQHaz4jsQMA2qPoDpM1LN75AvE+ekSTXtBWG2N0JvI9Qat37cR
|
||||
gRLm2adOAX2/RPRt5SiqAUuh8mt6ShTpuDJ1txJmY5/5GmdJ9e2Gjfs+CU3UKwyb
|
||||
2FizfjKDhTXPXYCfw8VQxr69iQc5GCJAECggzWajHQKBgQD83PQRU97DuygxDgmV
|
||||
jUH+RIp2B8qTRw/DD0pidrhXRmRUW1+mkXXgjHoDXfYnIYuViHoVRPi2QUyHJlki
|
||||
2xyIhWlb1YWbgOaTc4cxL+BpNHpdVdn0lFazIW1Ns1qfqtPjMq604Wmbjb0Eem8L
|
||||
ilQfM/dpOcUDz5AnShmMJObiXQKBgQDtaOYFXtXv/Qp3D4KXoDYy/D690O1CeAZs
|
||||
MIRTeullW+QkHRW/yaCwMF0dWyLDaFBj40tqMqJz7YS/4nW/FOqmK/k8yhJ7EwKR
|
||||
4Vu9gLjy4B9GmlI1koJY698pKZVG56aiompgxjNJAFD/m0EwK5NQ5jzi5L7O24hp
|
||||
T9uk9N7ecwKBgBFW3W2qwKXPWawzeDjHHPVgodYCJsP+pXgMvl8OIQnrC8o1EdQM
|
||||
OXcSavN8J/r68+VnHGgE+MEWrTa/THF1cPlznIvtkmHdbvJBg829qLfOH23zCyUY
|
||||
XvGdoy8m01oLjsgPm6dPpbxC9ekutBqoe6hAGgEfMwFV5F6tWB8iEKsJAoGBAOlf
|
||||
z5WB5x31iV8GgSJZ3E18RpTDFyfa4r1haALD9UCs9vRFHwcbxfjMYgwkdWTPWw8F
|
||||
9TFcuvLojjEuBP8kKZHV8jg+j7IqOsF6XXtIWQZvKG937c45yiZtHz7QYKyFtPl0
|
||||
XMvqqaleRUEutgMLItJIsZt8rtCbX1nepZRfHeJfAoGBAPUdjUocPqmx73D8mMu1
|
||||
wmwg0C/yHolrtUb+AH5H0ToAXfUNUSeADYnGR/jnLBHszJL9vBEyMBFsAwx1l7kT
|
||||
nzYrOFRRVdQui4Ng/EWljbHHO5NLAhq4/YaY8uF09yA7BEyXS2eYeZurJq7z0YFT
|
||||
lF6/M+xeORcUbU9uq+pi7rm9
|
||||
-----END PRIVATE KEY-----`
|
||||
|
||||
// 签名验证的时间窗口(分钟)
|
||||
const signTimeWindow = 5
|
||||
// AES 密钥片段(32字节 for AES-256)
|
||||
var aesKeyPart1 = []byte{0x54, 0x72, 0x75, 0x73, 0x74, 0x69, 0x65, 0x4c} // "TrustieL"
|
||||
var aesKeyPart2 = []byte{0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x65} // "icenceCe"
|
||||
var aesKeyPart3 = []byte{0x72, 0x74, 0x69, 0x66, 0x79, 0x4b, 0x65, 0x79} // "rtifyKey"
|
||||
var aesKeyPart4 = []byte{0x41, 0x45, 0x53, 0x32, 0x35, 0x36, 0x47, 0x43} // "AES256GC"
|
||||
var aesKeyPart5 = []byte{0x4d, 0x53, 0x68, 0x61, 0x72, 0x70, 0x49, 0x6e} // "MSharpIn"
|
||||
|
||||
func getRSAPrivateKey() *rsa.PrivateKey {
|
||||
block, _ := pem.Decode([]byte(rsaPrivateKeyPEM))
|
||||
if block == nil {
|
||||
fmt.Println("警告: 无法解析 RSA 私钥 PEM")
|
||||
return nil
|
||||
}
|
||||
|
||||
keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
fmt.Printf("警告: 无法解析 RSA 私钥: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
key, ok := keyInterface.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
fmt.Println("警告: 私钥类型转换失败")
|
||||
return nil
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func getAESKey() []byte {
|
||||
key := make([]byte, 0)
|
||||
key = append(key, aesKeyPart1...)
|
||||
key = append(key, aesKeyPart2...)
|
||||
key = append(key, aesKeyPart3...)
|
||||
key = append(key, aesKeyPart4...)
|
||||
key = append(key, aesKeyPart5...)
|
||||
return key
|
||||
}
|
||||
|
||||
// ==================== 硬件信息结构 ====================
|
||||
type HardwareInfo struct {
|
||||
MacAddress []string `json:"macAddress"`
|
||||
IpAddress []string `json:"ipAddress"`
|
||||
|
|
@ -29,7 +100,42 @@ type HardwareInfo struct {
|
|||
MainBoardSerial string `json:"mainBoardSerial"`
|
||||
}
|
||||
|
||||
// ==================== 请求/响应结构 ====================
|
||||
type GenerateRequest struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
type VerifyRequest struct {
|
||||
Action string `json:"action"`
|
||||
Challenge string `json:"challenge"`
|
||||
SessionId string `json:"sessionId"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type EncryptedResponse struct {
|
||||
Encrypted string `json:"encrypted"`
|
||||
Nonce string `json:"nonce"`
|
||||
Tag string `json:"tag"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type VerifyResponse struct {
|
||||
Signature string `json:"signature"`
|
||||
HardwareInfo HardwareInfo `json:"hardwareInfo"`
|
||||
InfoBytes string `json:"infoBytes"` // 原始 JSON 字节,用于验签
|
||||
}
|
||||
|
||||
// ==================== 主函数 ====================
|
||||
func main() {
|
||||
// 启动前进行完整性自检
|
||||
if !verifyIntegrity() {
|
||||
fmt.Println("错误: 完整性校验失败,Agent 可能已被篡改")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 启动反调试检测
|
||||
go startAntiDebug()
|
||||
|
||||
if len(os.Args) == 1 {
|
||||
info := GetHardwareInfo()
|
||||
PrintHardwareInfo(info)
|
||||
|
|
@ -37,7 +143,7 @@ func main() {
|
|||
}
|
||||
|
||||
if os.Args[1] == "serve" {
|
||||
port := "8899"
|
||||
port := "33306"
|
||||
if len(os.Args) >= 3 {
|
||||
port = os.Args[2]
|
||||
}
|
||||
|
|
@ -45,92 +151,233 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
fmt.Println("用法:")
|
||||
}
|
||||
|
||||
// ==================== 服务器启动 ====================
|
||||
func StartServer(port string) {
|
||||
// 创建 HTTP 服务端,设置超时
|
||||
aesKey := getAESKey()
|
||||
privateKey := getRSAPrivateKey()
|
||||
|
||||
fmt.Printf("========================================\n")
|
||||
fmt.Printf(" MySQL Agent 已启动\n")
|
||||
fmt.Printf("========================================\n")
|
||||
fmt.Printf(" 监听端口: %s\n", port)
|
||||
fmt.Printf(" 服务类型: mysqld\n")
|
||||
fmt.Printf(" 协议: MySQL Protocol\n")
|
||||
fmt.Printf("========================================\n")
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
// 从环境变量读取签名密钥
|
||||
if envKey := os.Getenv("AGENT_SIGN_KEY"); envKey != "" {
|
||||
signKey = envKey
|
||||
}
|
||||
|
||||
http.HandleFunc("/api/hardware-info", func(w http.ResponseWriter, r *http.Request) {
|
||||
// 验证签名
|
||||
if !verifySign(w, r) {
|
||||
return
|
||||
}
|
||||
info := GetHardwareInfo()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(info)
|
||||
http.HandleFunc("/api/v1/query", func(w http.ResponseWriter, r *http.Request) {
|
||||
handleQuery(w, r, aesKey, privateKey)
|
||||
})
|
||||
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte{0x0a})
|
||||
w.Write([]byte("5.7.30" + string(0x00)))
|
||||
})
|
||||
|
||||
fmt.Printf("Agent 已启动,监听端口: %s\n", port)
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
fmt.Printf("服务器启动失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
func verifySign(w http.ResponseWriter, r *http.Request) bool {
|
||||
timestampStr := r.Header.Get("X-Timestamp")
|
||||
signature := r.Header.Get("X-Signature")
|
||||
|
||||
if timestampStr == "" || signature == "" {
|
||||
http.Error(w, `{"error":"missing timestamp or signature"}`, http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
|
||||
// 解析时间戳
|
||||
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
|
||||
// ==================== 查询处理(伪装成 MySQL) ====================
|
||||
func handleQuery(w http.ResponseWriter, r *http.Request, aesKey []byte, privateKey *rsa.PrivateKey) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"invalid timestamp format"}`, http.StatusUnauthorized)
|
||||
return false
|
||||
http.Error(w, `{"error":"read body failed"}`, 400)
|
||||
return
|
||||
}
|
||||
|
||||
var req GenerateRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
handleSQLQuery(w, body, aesKey)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Action {
|
||||
case "generate":
|
||||
handleGenerate(w, aesKey)
|
||||
case "verify":
|
||||
handleVerify(w, body, aesKey, privateKey)
|
||||
default:
|
||||
http.Error(w, `{"error":"unknown action"}`, 400)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSQLQuery(w http.ResponseWriter, body []byte, aesKey []byte) {
|
||||
var sqlReq map[string]interface{}
|
||||
if err := json.Unmarshal(body, &sqlReq); err == nil {
|
||||
if sql, ok := sqlReq["sql"].(string); ok {
|
||||
if strings.Contains(sql, "SELECT") && strings.Contains(sql, "system_info") {
|
||||
handleGenerate(w, aesKey)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(200)
|
||||
errResp := []byte{0xff, 0x00, 0x00}
|
||||
w.Write(errResp)
|
||||
}
|
||||
|
||||
func handleGenerate(w http.ResponseWriter, aesKey []byte) {
|
||||
info := GetHardwareInfo()
|
||||
|
||||
infoBytes, _ := json.Marshal(info)
|
||||
|
||||
encrypted, nonce, _, err := aesGCMEncrypt(infoBytes, aesKey[:32])
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"encryption failed"}`, 500)
|
||||
return
|
||||
}
|
||||
|
||||
response := EncryptedResponse{
|
||||
Encrypted: base64.StdEncoding.EncodeToString(encrypted),
|
||||
Nonce: base64.StdEncoding.EncodeToString(nonce),
|
||||
Version: "1.0",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func handleVerify(w http.ResponseWriter, body []byte, aesKey []byte, privateKey *rsa.PrivateKey) {
|
||||
var req VerifyRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查时间戳是否在合理窗口内
|
||||
now := time.Now().UnixMilli()
|
||||
windowMs := int64(signTimeWindow * 60 * 1000)
|
||||
if now-timestamp > windowMs || timestamp-now > windowMs {
|
||||
http.Error(w, `{"error":"timestamp out of range"}`, http.StatusUnauthorized)
|
||||
return false
|
||||
windowMs := int64(5 * 60 * 1000)
|
||||
if now-req.Timestamp > windowMs || req.Timestamp-now > windowMs {
|
||||
http.Error(w, `{"error":"timestamp out of range"}`, 401)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
expectedSig := computeHmac(timestampStr, signKey)
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedSig)) {
|
||||
http.Error(w, `{"error":"invalid signature"}`, http.StatusUnauthorized)
|
||||
return false
|
||||
info := GetHardwareInfo()
|
||||
infoBytes, _ := json.Marshal(info)
|
||||
|
||||
dataToSign := append(infoBytes, []byte(req.Challenge+req.SessionId)...)
|
||||
|
||||
hash := sha256.Sum256(dataToSign)
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"sign failed"}`, 500)
|
||||
return
|
||||
}
|
||||
|
||||
response := VerifyResponse{
|
||||
Signature: base64.StdEncoding.EncodeToString(signature),
|
||||
HardwareInfo: info,
|
||||
InfoBytes: base64.StdEncoding.EncodeToString(infoBytes),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// ==================== AES-256-GCM 加密 ====================
|
||||
func aesGCMEncrypt(plaintext, key []byte) ([]byte, []byte, []byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
||||
return ciphertext, nonce, nil, nil
|
||||
}
|
||||
|
||||
// ==================== 完整性自检 ====================
|
||||
func verifyIntegrity() bool {
|
||||
exePath, err := os.ReadFile("/proc/self/exe")
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
hash := sha256.Sum256([]byte(exePath))
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
_ = hashStr
|
||||
return true
|
||||
}
|
||||
|
||||
// 计算 HMAC-SHA256 签名
|
||||
func computeHmac(timestamp string, key string) string {
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(timestamp))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
// ==================== 反调试检测 ====================
|
||||
func startAntiDebug() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
for {
|
||||
<-ticker.C
|
||||
if isBeingTraced() || isDebuggerPresent() {
|
||||
fmt.Println("检测到调试器,Agent 即将退出")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取密钥(供测试使用)
|
||||
func GetSignKey() string {
|
||||
return signKey
|
||||
func isBeingTraced() bool {
|
||||
data, err := os.ReadFile("/proc/self/status")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "TracerPid:") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 && parts[1] != "0" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isDebuggerPresent() bool {
|
||||
fdDir := "/proc/self/fd"
|
||||
entries, err := os.ReadDir(fdDir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
link, err := os.Readlink(fdDir + "/" + entry.Name())
|
||||
if err == nil && strings.Contains(link, "pipe:") {
|
||||
count := 0
|
||||
for _, e := range entries {
|
||||
link2, err2 := os.Readlink(fdDir + "/" + e.Name())
|
||||
if err2 == nil && strings.Contains(link2, "pipe:") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 10 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ==================== 获取信息 ====================
|
||||
func GetHardwareInfo() HardwareInfo {
|
||||
return HardwareInfo{
|
||||
MacAddress: GetMacAddresses(),
|
||||
|
|
@ -142,7 +389,7 @@ func GetHardwareInfo() HardwareInfo {
|
|||
|
||||
func PrintHardwareInfo(info HardwareInfo) {
|
||||
fmt.Println("========================================")
|
||||
fmt.Println(" 服务器硬件信息")
|
||||
fmt.Println(" 信息")
|
||||
fmt.Println("========================================")
|
||||
|
||||
fmt.Println("\nIP地址:")
|
||||
|
|
@ -178,8 +425,7 @@ func PrintHardwareInfo(info HardwareInfo) {
|
|||
fmt.Println("\n========================================")
|
||||
}
|
||||
|
||||
// ======= Linux 实现 =======
|
||||
|
||||
// ==================== Linux 硬件信息获取 ====================
|
||||
func GetMacAddressesLinux() []string {
|
||||
var macs []string
|
||||
interfaces, err := net.Interfaces()
|
||||
|
|
@ -196,14 +442,12 @@ func GetMacAddressesLinux() []string {
|
|||
continue
|
||||
}
|
||||
lowerMAC := strings.ToLower(mac)
|
||||
// 跳过虚拟机网卡
|
||||
if strings.HasPrefix(lowerMAC, "00:50:56") ||
|
||||
strings.HasPrefix(lowerMAC, "00:0c:29") ||
|
||||
strings.HasPrefix(lowerMAC, "08:00:27") ||
|
||||
strings.HasPrefix(lowerMAC, "52:54:00") {
|
||||
continue
|
||||
}
|
||||
// 跳过 Docker 虚拟网卡
|
||||
ifName := strings.ToLower(ifi.Name)
|
||||
if strings.HasPrefix(ifName, "veth") ||
|
||||
strings.HasPrefix(ifName, "docker") ||
|
||||
|
|
@ -303,8 +547,7 @@ func GetMainBoardSerialLinux() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// ======= Windows 实现 =======
|
||||
|
||||
// ==================== Windows 硬件信息获取 ====================
|
||||
func GetMacAddressesWindows() []string {
|
||||
var macs []string
|
||||
interfaces, err := net.Interfaces()
|
||||
|
|
@ -372,8 +615,7 @@ func GetMainBoardSerialWindows() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// ======= 平台选择 =======
|
||||
|
||||
// ==================== 平台选择 ====================
|
||||
func GetMacAddresses() []string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return GetMacAddressesWindows()
|
||||
|
|
@ -402,3 +644,21 @@ func GetMainBoardSerial() string {
|
|||
return GetMainBoardSerialLinux()
|
||||
}
|
||||
|
||||
func containsString(slice []string, s string) bool {
|
||||
for _, item := range slice {
|
||||
if item == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseMACAddress(mac string) string {
|
||||
mac = strings.ReplaceAll(mac, ":", "")
|
||||
mac = strings.ReplaceAll(mac, "-", "")
|
||||
mac = strings.ToUpper(mac)
|
||||
if matched, _ := regexp.MatchString("^[0-9A-F]{12}$", mac); !matched {
|
||||
return ""
|
||||
}
|
||||
return mac
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDqgCrrAvQV5jc3
|
||||
6OTv4kD9bIZ0rcm4z4dixcq4If90T1ATS1ahiIIhO3iYNAQFeR5T53/UUoPO7zLW
|
||||
h8dyfmMT7jTM6Tanlt+upCF8qXBwT/qUu92xbaa5B7GeLES6vrlMaki206q6EN87
|
||||
5IugRcIGMaxoa5jkH/cd8QKRNsljUrkoD7W6xWHOt0cV+LS7meDBZtgIcqMeeHdL
|
||||
++ZlAUzcCCcEvW5VZY4M24ryIsP5KnV7VZyMhDjY4iBmQRgnAP+pTZo5lDhHEfDj
|
||||
B+YPVYRABtxdcaXBeaDkbvE03ZkxCjpE4d9BYL3UMXyQisDg0IH+hN/iawMaSMpm
|
||||
pIG8qlXHAgMBAAECggEAFOge+Dt+Rt+MbiWLSuAsmouAHbHxNjlf+oxKa3BlUozQ
|
||||
ElBMTA2CfVnMgbrz3gfZqAuPjkuNaQS5aLGYgUD6yCgCH0y7H/9OOC2o6j2laSLJ
|
||||
4yD9F5iZ9yaHWhdCfDXAPN6/vRERyEUnaeZErohzNZ/2UFiOooDZETUC3Uh5qJGa
|
||||
4d8VXrloYL74X1fQHaz4jsQMA2qPoDpM1LN75AvE+ekSTXtBWG2N0JvI9Qat37cR
|
||||
gRLm2adOAX2/RPRt5SiqAUuh8mt6ShTpuDJ1txJmY5/5GmdJ9e2Gjfs+CU3UKwyb
|
||||
2FizfjKDhTXPXYCfw8VQxr69iQc5GCJAECggzWajHQKBgQD83PQRU97DuygxDgmV
|
||||
jUH+RIp2B8qTRw/DD0pidrhXRmRUW1+mkXXgjHoDXfYnIYuViHoVRPi2QUyHJlki
|
||||
2xyIhWlb1YWbgOaTc4cxL+BpNHpdVdn0lFazIW1Ns1qfqtPjMq604Wmbjb0Eem8L
|
||||
ilQfM/dpOcUDz5AnShmMJObiXQKBgQDtaOYFXtXv/Qp3D4KXoDYy/D690O1CeAZs
|
||||
MIRTeullW+QkHRW/yaCwMF0dWyLDaFBj40tqMqJz7YS/4nW/FOqmK/k8yhJ7EwKR
|
||||
4Vu9gLjy4B9GmlI1koJY698pKZVG56aiompgxjNJAFD/m0EwK5NQ5jzi5L7O24hp
|
||||
T9uk9N7ecwKBgBFW3W2qwKXPWawzeDjHHPVgodYCJsP+pXgMvl8OIQnrC8o1EdQM
|
||||
OXcSavN8J/r68+VnHGgE+MEWrTa/THF1cPlznIvtkmHdbvJBg829qLfOH23zCyUY
|
||||
XvGdoy8m01oLjsgPm6dPpbxC9ekutBqoe6hAGgEfMwFV5F6tWB8iEKsJAoGBAOlf
|
||||
z5WB5x31iV8GgSJZ3E18RpTDFyfa4r1haALD9UCs9vRFHwcbxfjMYgwkdWTPWw8F
|
||||
9TFcuvLojjEuBP8kKZHV8jg+j7IqOsF6XXtIWQZvKG937c45yiZtHz7QYKyFtPl0
|
||||
XMvqqaleRUEutgMLItJIsZt8rtCbX1nepZRfHeJfAoGBAPUdjUocPqmx73D8mMu1
|
||||
wmwg0C/yHolrtUb+AH5H0ToAXfUNUSeADYnGR/jnLBHszJL9vBEyMBFsAwx1l7kT
|
||||
nzYrOFRRVdQui4Ng/EWljbHHO5NLAhq4/YaY8uF09yA7BEyXS2eYeZurJq7z0YFT
|
||||
lF6/M+xeORcUbU9uq+pi7rm9
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1 @@
|
|||
LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tDQpNSUlFdmdJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLZ3dnZ1NrQWdFQUFvSUJBUURxZ0NyckF2UVY1amMzDQo2T1R2NGtEOWJJWjByY200ejRkaXhjcTRJZjkwVDFBVFMxYWhpSUloTzNpWU5BUUZlUjVUNTMvVVVvUE83ekxXDQpoOGR5Zm1NVDdqVE02VGFubHQrdXBDRjhxWEJ3VC9xVXU5MnhiYWE1QjdHZUxFUzZ2cmxNYWtpMjA2cTZFTjg3DQo1SXVnUmNJR01heG9hNWprSC9jZDhRS1JOc2xqVXJrb0Q3VzZ4V0hPdDBjVitMUzdtZURCWnRnSWNxTWVlSGRMDQorK1psQVV6Y0NDY0V2VzVWWlk0TTI0cnlJc1A1S25WN1ZaeU1oRGpZNGlCbVFSZ25BUCtwVFpvNWxEaEhFZkRqDQpCK1lQVllSQUJ0eGRjYVhCZWFEa2J2RTAzWmt4Q2pwRTRkOUJZTDNVTVh5UWlzRGcwSUgraE4vaWF3TWFTTXBtDQpwSUc4cWxYSEFnTUJBQUVDZ2dFQUZPZ2UrRHQrUnQrTWJpV0xTdUFzbW91QUhiSHhOamxmK294S2EzQmxVb3pRDQpFbEJNVEEyQ2ZWbk1nYnJ6M2dmWnFBdVBqa3VOYVFTNWFMR1lnVUQ2eUNnQ0gweTdILzlPT0MybzZqMmxhU0xKDQo0eUQ5RjVpWjl5YUhXaGRDZkRYQVBONi92UkVSeUVVbmFlWkVyb2h6TlovMlVGaU9vb0RaRVRVQzNVaDVxSkdhDQo0ZDhWWHJsb1lMNzRYMWZRSGF6NGpzUU1BMnFQb0RwTTFMTjc1QXZFK2VrU1RYdEJXRzJOMEp2STlRYXQzN2NSDQpnUkxtMmFkT0FYMi9SUFJ0NVNpcUFVdWg4bXQ2U2hUcHVESjF0eEptWTUvNUdtZEo5ZTJHamZzK0NVM1VLd3liDQoyRml6ZmpLRGhUWFBYWUNmdzhWUXhyNjlpUWM1R0NKQUVDZ2d6V2FqSFFLQmdRRDgzUFFSVTk3RHV5Z3hEZ21WDQpqVUgrUklwMkI4cVRSdy9ERDBwaWRyaFhSbVJVVzErbWtYWGdqSG9EWGZZbklZdVZpSG9WUlBpMlFVeUhKbGtpDQoyeHlJaFdsYjFZV2JnT2FUYzRjeEwrQnBOSHBkVmRuMGxGYXpJVzFOczFxZnF0UGpNcTYwNFdtYmpiMEVlbThMDQppbFFmTS9kcE9jVUR6NUFuU2htTUpPYmlYUUtCZ1FEdGFPWUZYdFh2L1FwM0Q0S1hvRFl5L0Q2OTBPMUNlQVpzDQpNSVJUZXVsbFcrUWtIUlcveWFDd01GMGRXeUxEYUZCajQwdHFNcUp6N1lTLzRuVy9GT3FtSy9rOHloSjdFd0tSDQo0VnU5Z0xqeTRCOUdtbEkxa29KWTY5OHBLWlZHNTZhaW9tcGd4ak5KQUZEL20wRXdLNU5RNWp6aTVMN08yNGhwDQpUOXVrOU43ZWN3S0JnQkZXM1cycXdLWFBXYXd6ZURqSEhQVmdvZFlDSnNQK3BYZ012bDhPSVFuckM4bzFFZFFNDQpPWGNTYXZOOEovcjY4K1ZuSEdnRStNRVdyVGEvVEhGMWNQbHpuSXZ0a21IZGJ2SkJnODI5cUxmT0gyM3pDeVVZDQpYdkdkb3k4bTAxb0xqc2dQbTZkUHBieEM5ZWt1dEJxb2U2aEFHZ0VmTXdGVjVGNnRXQjhpRUtzSkFvR0JBT2xmDQp6NVdCNXgzMWlWOEdnU0paM0UxOFJwVERGeWZhNHIxaGFBTEQ5VUNzOXZSRkh3Y2J4ZmpNWWd3a2RXVFBXdzhGDQo5VEZjdXZMb2pqRXVCUDhrS1pIVjhqZytqN0lxT3NGNlhYdElXUVp2S0c5MzdjNDV5aVp0SHo3UVlLeUZ0UGwwDQpYTXZxcWFsZVJVRXV0Z01MSXRKSXNadDhydENiWDFuZXBaUmZIZUpmQW9HQkFQVWRqVW9jUHFteDczRDhtTXUxDQp3bXdnMEMveUhvbHJ0VWIrQUg1SDBUb0FYZlVOVVNlQURZbkdSL2puTEJIc3pKTDl2QkV5TUJGc0F3eDFsN2tUDQpuellyT0ZSUlZkUXVpNE5nL0VXbGpiSEhPNU5MQWhxNC9ZYVk4dUYwOXlBN0JFeVhTMmVZZVp1ckpxN3owWUZUDQpsRjYvTSt4ZU9SY1ViVTl1cStwaTdybTkNCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0NCg==
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tDQpNSUlFdmdJQkFEQU5CZ2txaGtpRzl3MEJBUUVG
|
||||
QUFTQ0JLZ3dnZ1NrQWdFQUFvSUJBUURxZ0NyckF2UVY1amMzDQo2T1R2NGtEOWJJWjByY200ejRk
|
||||
aXhjcTRJZjkwVDFBVFMxYWhpSUloTzNpWU5BUUZlUjVUNTMvVVVvUE83ekxXDQpoOGR5Zm1NVDdq
|
||||
VE02VGFubHQrdXBDRjhxWEJ3VC9xVXU5MnhiYWE1QjdHZUxFUzZ2cmxNYWtpMjA2cTZFTjg3DQo1
|
||||
SXVnUmNJR01heG9hNWprSC9jZDhRS1JOc2xqVXJrb0Q3VzZ4V0hPdDBjVitMUzdtZURCWnRnSWNx
|
||||
TWVlSGRMDQorK1psQVV6Y0NDY0V2VzVWWlk0TTI0cnlJc1A1S25WN1ZaeU1oRGpZNGlCbVFSZ25B
|
||||
UCtwVFpvNWxEaEhFZkRqDQpCK1lQVllSQUJ0eGRjYVhCZWFEa2J2RTAzWmt4Q2pwRTRkOUJZTDNV
|
||||
TVh5UWlzRGcwSUgraE4vaWF3TWFTTXBtDQpwSUc4cWxYSEFnTUJBQUVDZ2dFQUZPZ2UrRHQrUnQr
|
||||
TWJpV0xTdUFzbW91QUhiSHhOamxmK294S2EzQmxVb3pRDQpFbEJNVEEyQ2ZWbk1nYnJ6M2dmWnFB
|
||||
dVBqa3VOYVFTNWFMR1lnVUQ2eUNnQ0gweTdILzlPT0MybzZqMmxhU0xKDQo0eUQ5RjVpWjl5YUhX
|
||||
aGRDZkRYQVBONi92UkVSeUVVbmFlWkVyb2h6TlovMlVGaU9vb0RaRVRVQzNVaDVxSkdhDQo0ZDhW
|
||||
WHJsb1lMNzRYMWZRSGF6NGpzUU1BMnFQb0RwTTFMTjc1QXZFK2VrU1RYdEJXRzJOMEp2STlRYXQz
|
||||
N2NSDQpnUkxtMmFkT0FYMi9SUFJ0NVNpcUFVdWg4bXQ2U2hUcHVESjF0eEptWTUvNUdtZEo5ZTJH
|
||||
amZzK0NVM1VLd3liDQoyRml6ZmpLRGhUWFBYWUNmdzhWUXhyNjlpUWM1R0NKQUVDZ2d6V2FqSFFL
|
||||
QmdRRDgzUFFSVTk3RHV5Z3hEZ21WDQpqVUgrUklwMkI4cVRSdy9ERDBwaWRyaFhSbVJVVzErbWtY
|
||||
WGdqSG9EWGZZbklZdVZpSG9WUlBpMlFVeUhKbGtpDQoyeHlJaFdsYjFZV2JnT2FUYzRjeEwrQnBO
|
||||
SHBkVmRuMGxGYXpJVzFOczFxZnF0UGpNcTYwNFdtYmpiMEVlbThMDQppbFFmTS9kcE9jVUR6NUFu
|
||||
U2htTUpPYmlYUUtCZ1FEdGFPWUZYdFh2L1FwM0Q0S1hvRFl5L0Q2OTBPMUNlQVpzDQpNSVJUZXVs
|
||||
bFcrUWtIUlcveWFDd01GMGRXeUxEYUZCajQwdHFNcUp6N1lTLzRuVy9GT3FtSy9rOHloSjdFd0tS
|
||||
DQo0VnU5Z0xqeTRCOUdtbEkxa29KWTY5OHBLWlZHNTZhaW9tcGd4ak5KQUZEL20wRXdLNU5RNWp6
|
||||
aTVMN08yNGhwDQpUOXVrOU43ZWN3S0JnQkZXM1cycXdLWFBXYXd6ZURqSEhQVmdvZFlDSnNQK3BY
|
||||
Z012bDhPSVFuckM4bzFFZFFNDQpPWGNTYXZOOEovcjY4K1ZuSEdnRStNRVdyVGEvVEhGMWNQbHpu
|
||||
SXZ0a21IZGJ2SkJnODI5cUxmT0gyM3pDeVVZDQpYdkdkb3k4bTAxb0xqc2dQbTZkUHBieEM5ZWt1
|
||||
dEJxb2U2aEFHZ0VmTXdGVjVGNnRXQjhpRUtzSkFvR0JBT2xmDQp6NVdCNXgzMWlWOEdnU0paM0Ux
|
||||
OFJwVERGeWZhNHIxaGFBTEQ5VUNzOXZSRkh3Y2J4ZmpNWWd3a2RXVFBXdzhGDQo5VEZjdXZMb2pq
|
||||
RXVCUDhrS1pIVjhqZytqN0lxT3NGNlhYdElXUVp2S0c5MzdjNDV5aVp0SHo3UVlLeUZ0UGwwDQpY
|
||||
TXZxcWFsZVJVRXV0Z01MSXRKSXNadDhydENiWDFuZXBaUmZIZUpmQW9HQkFQVWRqVW9jUHFteDcz
|
||||
RDhtTXUxDQp3bXdnMEMveUhvbHJ0VWIrQUg1SDBUb0FYZlVOVVNlQURZbkdSL2puTEJIc3pKTDl2
|
||||
QkV5TUJGc0F3eDFsN2tUDQpuellyT0ZSUlZkUXVpNE5nL0VXbGpiSEhPNU5MQWhxNC9ZYVk4dUYw
|
||||
OXlBN0JFeVhTMmVZZVp1ckpxN3owWUZUDQpsRjYvTSt4ZU9SY1ViVTl1cStwaTdybTkNCi0tLS0t
|
||||
RU5EIFBSSVZBVEUgS0VZLS0tLS0NCg==
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6oAq6wL0FeY3N+jk7+JA
|
||||
/WyGdK3JuM+HYsXKuCH/dE9QE0tWoYiCITt4mDQEBXkeU+d/1FKDzu8y1ofHcn5j
|
||||
E+40zOk2p5bfrqQhfKlwcE/6lLvdsW2muQexnixEur65TGpIttOquhDfO+SLoEXC
|
||||
BjGsaGuY5B/3HfECkTbJY1K5KA+1usVhzrdHFfi0u5ngwWbYCHKjHnh3S/vmZQFM
|
||||
3AgnBL1uVWWODNuK8iLD+Sp1e1WcjIQ42OIgZkEYJwD/qU2aOZQ4RxHw4wfmD1WE
|
||||
QAbcXXGlwXmg5G7xNN2ZMQo6ROHfQWC91DF8kIrA4NCB/oTf4msDGkjKZqSBvKpV
|
||||
xwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
|
|
@ -0,0 +1 @@
|
|||
LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNm9BcTZ3TDBGZVkzTitqazcrSkENCi9XeUdkSzNKdU0rSFlzWEt1Q0gvZEU5UUUwdFdvWWlDSVR0NG1EUUVCWGtlVStkLzFGS0R6dTh5MW9mSGNuNWoNCkUrNDB6T2sycDViZnJxUWhmS2x3Y0UvNmxMdmRzVzJtdVFleG5peEV1cjY1VEdwSXR0T3F1aERmTytTTG9FWEMNCkJqR3NhR3VZNUIvM0hmRUNrVGJKWTFLNUtBKzF1c1ZoenJkSEZmaTB1NW5nd1diWUNIS2pIbmgzUy92bVpRRk0NCjNBZ25CTDF1VldXT0ROdUs4aUxEK1NwMWUxV2NqSVE0Mk9JZ1prRVlKd0QvcVUyYU9aUTRSeEh3NHdmbUQxV0UNClFBYmNYWEdsd1htZzVHN3hOTjJaTVFvNlJPSGZRV0M5MURGOGtJckE0TkNCL29UZjRtc0RHa2pLWnFTQnZLcFYNCnh3SURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0NCg==
|
||||
|
|
@ -54,7 +54,7 @@ public class LicenseCheckListener implements ApplicationListener<ContextRefreshe
|
|||
/**
|
||||
* 宿主机 Agent API 地址
|
||||
*/
|
||||
@Value("${license.agent-api-url:}")
|
||||
@Value("${http.mysqlUrl}")
|
||||
private String agentApiUrl;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,19 +4,29 @@ import com.lcz.licence.entity.AdditionInfo;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.crypto.Cipher;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigInteger;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 宿主机硬件信息获取服务
|
||||
* 通过 HTTP 调用宿主机上的 Agent API 获取真实硬件信息
|
||||
* 支持 RSA 签名验证和 Challenge-Response 机制
|
||||
*
|
||||
* @author lcz
|
||||
* @mail lcz_0130@163.com
|
||||
|
|
@ -27,13 +37,22 @@ public class HardwareAgentService {
|
|||
private static final int CONNECT_TIMEOUT = 5000;
|
||||
private static final int READ_TIMEOUT = 10000;
|
||||
|
||||
// Agent 签名密钥(与 Agent 端保持一致)
|
||||
private static final String SIGN_KEY = "licence-secret-sign-key-trustie";
|
||||
// RSA 公钥(Base64 编码的 DER 格式,用于验签)
|
||||
// 与 Agent 端的 RSA 私钥配对
|
||||
private static final String RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6oAq6wL0FeY3N+jk7+JA/WyGdK3JuM+HYsXKuCH/dE9QE0tWoYiCITt4mDQEBXkeU+d/1FKDzu8y1ofHcn5jE+40zOk2p5bfrqQhfKlwcE/6lLvdsW2muQexnixEur65TGpIttOquhDfO+SLoEXCBjGsaGuY5B/3HfECkTbJY1K5KA+1usVhzrdHFfi0u5ngwWbYCHKjHnh3S/vmZQFM3AgnBL1uVWWODNuK8iLD+Sp1e1WcjIQ42OIgZkEYJwD/qU2aOZQ4RxHw4wfmD1WEQAbcXXGlwXmg5G7xNN2ZMQo6ROHfQWC91DF8kIrA4NCB/oTf4msDGkjKZqSBvKpVxwIDAQAB";
|
||||
|
||||
// 心跳检测相关
|
||||
private static final int HEARTBEAT_INTERVAL_MINUTES = 5;
|
||||
private static ScheduledExecutorService heartbeatScheduler;
|
||||
private static final AtomicBoolean heartbeatOk = new AtomicBoolean(true);
|
||||
|
||||
// 当前会话 ID
|
||||
private static String currentSessionId = null;
|
||||
|
||||
/**
|
||||
* 从 Agent API 获取硬件信息
|
||||
* 从 Agent API 获取硬件信息(Challenge-Response 验证模式)
|
||||
*
|
||||
* @param agentApiUrl Agent API 地址,如 http://host.docker.internal:8899/api/hardware-info
|
||||
* @param agentApiUrl Agent API 地址,如 http://host.docker.internal:8899/api/v1/query
|
||||
* @return AdditionInfo 对象,获取失败返回 null
|
||||
*/
|
||||
public static AdditionInfo fetchHardwareInfo(String agentApiUrl) {
|
||||
|
|
@ -45,27 +64,40 @@ public class HardwareAgentService {
|
|||
log.debug("尝试从 Agent API 获取硬件信息: {}", agentApiUrl);
|
||||
|
||||
try {
|
||||
// 生成或复用 sessionId
|
||||
if (currentSessionId == null) {
|
||||
currentSessionId = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
// 生成 Challenge: SHA256(randomBytes + timestamp + sessionKey)
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String randomBytes = generateRandomString(32);
|
||||
String challengeData = randomBytes + timestamp + currentSessionId;
|
||||
String challenge = sha256(challengeData);
|
||||
|
||||
// 构建请求
|
||||
URL url = new URL(agentApiUrl);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(CONNECT_TIMEOUT);
|
||||
conn.setReadTimeout(READ_TIMEOUT);
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
|
||||
// 生成签名
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String signature = computeHmacSha256(timestamp, SIGN_KEY);
|
||||
// 发送 Challenge 请求
|
||||
String requestBody = String.format(
|
||||
"{\"action\":\"verify\",\"challenge\":\"%s\",\"sessionId\":\"%s\",\"timestamp\":%d}",
|
||||
challenge, currentSessionId, timestamp
|
||||
);
|
||||
|
||||
// 添加签名相关的请求头
|
||||
conn.setRequestProperty("X-Timestamp", timestamp);
|
||||
conn.setRequestProperty("X-Signature", signature);
|
||||
conn.getOutputStream().write(requestBody.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
log.warn("Agent API 返回错误码: {}, URL: {}", responseCode, agentApiUrl);
|
||||
// 读取错误响应体
|
||||
String errorBody = readErrorBody(conn);
|
||||
log.warn("Agent API 错误响应: {}", errorBody);
|
||||
heartbeatOk.set(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +113,58 @@ public class HardwareAgentService {
|
|||
String jsonContent = response.toString();
|
||||
log.debug("Agent API 响应: {}", jsonContent);
|
||||
|
||||
return parseHardwareInfo(jsonContent);
|
||||
// 解析并验证响应
|
||||
AdditionInfo info = parseAndVerifyResponse(jsonContent, challenge, currentSessionId);
|
||||
if (info != null) {
|
||||
heartbeatOk.set(true);
|
||||
}
|
||||
|
||||
return info;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("从 Agent API 获取硬件信息失败: {}", agentApiUrl, e);
|
||||
heartbeatOk.set(false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成硬件信息(用于证书生成阶段)
|
||||
*/
|
||||
public static AdditionInfo generateHardwareInfo(String agentApiUrl) {
|
||||
if (StringUtils.isBlank(agentApiUrl)) {
|
||||
log.debug("Agent API URL 为空,跳过 Agent 获取");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
URL url = new URL(agentApiUrl);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(CONNECT_TIMEOUT);
|
||||
conn.setReadTimeout(READ_TIMEOUT);
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
|
||||
String requestBody = "{\"action\":\"generate\"}";
|
||||
conn.getOutputStream().write(requestBody.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
log.warn("Agent API 返回错误码: {}, URL: {}", responseCode, agentApiUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
return parseHardwareInfo(response.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("从 Agent API 获取硬件信息失败: {}", agentApiUrl, e);
|
||||
|
|
@ -90,32 +173,202 @@ public class HardwareAgentService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 计算 HMAC-SHA256 签名
|
||||
*
|
||||
* @param timestamp 时间戳字符串
|
||||
* @param key 签名密钥
|
||||
* @return 十六进制编码的签名字符串
|
||||
* 解析并验证 Agent 响应(含 RSA 签名验证)
|
||||
*/
|
||||
private static String computeHmacSha256(String timestamp, String key) {
|
||||
private static AdditionInfo parseAndVerifyResponse(String jsonContent, String challenge, String sessionId) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||
mac.init(secretKeySpec);
|
||||
byte[] hmacBytes = mac.doFinal(timestamp.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : hmacBytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
// 提取 signature 和 hardwareInfo
|
||||
String signature = extractJsonValue(jsonContent, "signature");
|
||||
if (StringUtils.isBlank(signature)) {
|
||||
log.error("Agent 响应中缺少签名");
|
||||
return null;
|
||||
}
|
||||
return sb.toString();
|
||||
|
||||
// 提取 infoBytes(Go Agent 发送的原始 JSON 字节)
|
||||
String infoBytesBase64 = extractJsonValue(jsonContent, "infoBytes");
|
||||
if (StringUtils.isBlank(infoBytesBase64)) {
|
||||
log.error("Agent 响应中缺少 infoBytes");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析 hardwareInfo
|
||||
AdditionInfo info = parseHardwareInfo(jsonContent);
|
||||
if (info == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证 RSA 签名(使用 Agent 发送的原始 JSON 字节)
|
||||
byte[] infoBytes = java.util.Base64.getDecoder().decode(infoBytesBase64);
|
||||
String infoJson = new String(infoBytes, StandardCharsets.UTF_8);
|
||||
String dataToVerify = infoJson + challenge + sessionId;
|
||||
|
||||
if (!verifyRSASignature(dataToVerify, signature)) {
|
||||
log.error("RSA 签名验证失败,可能遭受中间人攻击");
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info("RSA 签名验证通过");
|
||||
return info;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("计算签名失败", e);
|
||||
return "";
|
||||
log.error("解析验证 Agent 响应失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取错误响应体
|
||||
* RSA 签名验证
|
||||
*/
|
||||
private static boolean verifyRSASignature(String data, String signatureBase64) {
|
||||
try {
|
||||
// 解码公钥
|
||||
byte[] keyBytes = java.util.Base64.getDecoder().decode(RSA_PUBLIC_KEY);
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PublicKey publicKey = keyFactory.generatePublic(keySpec);
|
||||
|
||||
// 解码签名
|
||||
byte[] signatureBytes = java.util.Base64.getDecoder().decode(signatureBase64);
|
||||
|
||||
// 验证签名
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initVerify(publicKey);
|
||||
sig.update(data.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return sig.verify(signatureBytes);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("RSA 签名验证异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳检测
|
||||
*/
|
||||
public static void startHeartbeat(String agentApiUrl) {
|
||||
if (heartbeatScheduler != null) {
|
||||
heartbeatScheduler.shutdownNow();
|
||||
}
|
||||
|
||||
heartbeatScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
heartbeatScheduler.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
// 发送无效 Challenge 检测 Agent 是否正常
|
||||
String invalidChallenge = "invalid-challenge-" + System.currentTimeMillis();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String sessionId = "heartbeat-check";
|
||||
|
||||
URL url = new URL(agentApiUrl);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(CONNECT_TIMEOUT);
|
||||
conn.setReadTimeout(READ_TIMEOUT);
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
|
||||
String requestBody = String.format(
|
||||
"{\"action\":\"verify\",\"challenge\":\"%s\",\"sessionId\":\"%s\",\"timestamp\":%d}",
|
||||
invalidChallenge, sessionId, timestamp
|
||||
);
|
||||
|
||||
conn.getOutputStream().write(requestBody.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode == 200) {
|
||||
log.debug("Agent 心跳检测正常");
|
||||
heartbeatOk.set(true);
|
||||
} else {
|
||||
log.warn("Agent 心跳检测异常,响应码: {}", responseCode);
|
||||
heartbeatOk.set(false);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Agent 心跳检测失败: {}", e.getMessage());
|
||||
heartbeatOk.set(false);
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL_MINUTES, HEARTBEAT_INTERVAL_MINUTES, TimeUnit.MINUTES);
|
||||
|
||||
log.info("Agent 心跳检测已启动,间隔: {} 分钟", HEARTBEAT_INTERVAL_MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳检测
|
||||
*/
|
||||
public static void stopHeartbeat() {
|
||||
if (heartbeatScheduler != null) {
|
||||
heartbeatScheduler.shutdownNow();
|
||||
heartbeatScheduler = null;
|
||||
log.info("Agent 心跳检测已停止");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查心跳状态
|
||||
*/
|
||||
public static boolean isHeartbeatOk() {
|
||||
return heartbeatOk.get();
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
private static String sha256(String input) {
|
||||
try {
|
||||
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateRandomString(int length) {
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
int index = (int) (Math.random() * chars.length());
|
||||
sb.append(chars.charAt(index));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String hardwareInfoToJson(AdditionInfo info) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
return mapper.writeValueAsString(info);
|
||||
} catch (Exception e) {
|
||||
log.error("转换 HardwareInfo 为 JSON 失败", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractJsonValue(String content, String key) {
|
||||
// 支持数组格式: "key":["value1","value2"] 和字符串格式: "key":"value"
|
||||
// 先尝试匹配数组格式 "key":["value"]
|
||||
String arrayPattern = "\"" + key + "\"\\s*:\\s*(\\[[^\\]]+\\])";
|
||||
java.util.regex.Pattern p1 = java.util.regex.Pattern.compile(arrayPattern);
|
||||
java.util.regex.Matcher m1 = p1.matcher(content);
|
||||
if (m1.find()) {
|
||||
return m1.group(1);
|
||||
}
|
||||
// 再尝试匹配字符串格式 "key":"value"
|
||||
String strPattern = "\"" + key + "\"\\s*:\\s*\"([^\"]+)\"";
|
||||
java.util.regex.Pattern p2 = java.util.regex.Pattern.compile(strPattern);
|
||||
java.util.regex.Matcher m2 = p2.matcher(content);
|
||||
if (m2.find()) {
|
||||
return m2.group(1);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String readErrorBody(HttpURLConnection conn) {
|
||||
try {
|
||||
if (conn.getErrorStream() != null) {
|
||||
|
|
@ -135,9 +388,6 @@ public class HardwareAgentService {
|
|||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Agent API 返回的 JSON 数据
|
||||
*/
|
||||
private static AdditionInfo parseHardwareInfo(String jsonContent) {
|
||||
if (StringUtils.isBlank(jsonContent)) {
|
||||
return null;
|
||||
|
|
@ -145,7 +395,6 @@ public class HardwareAgentService {
|
|||
|
||||
AdditionInfo info = new AdditionInfo();
|
||||
|
||||
// 解析 macAddress(支持数组格式 ["mac1","mac2"] 或逗号分隔 "mac1,mac2")
|
||||
String macStr = extractJsonValue(jsonContent, "macAddress");
|
||||
if (!StringUtils.isBlank(macStr)) {
|
||||
if (macStr.startsWith("[")) {
|
||||
|
|
@ -155,7 +404,6 @@ public class HardwareAgentService {
|
|||
}
|
||||
}
|
||||
|
||||
// 解析 ipAddress
|
||||
String ipStr = extractJsonValue(jsonContent, "ipAddress");
|
||||
if (!StringUtils.isBlank(ipStr)) {
|
||||
if (ipStr.startsWith("[")) {
|
||||
|
|
@ -165,19 +413,16 @@ public class HardwareAgentService {
|
|||
}
|
||||
}
|
||||
|
||||
// 解析 cpuSerial
|
||||
String cpuSerial = extractJsonValue(jsonContent, "cpuSerial");
|
||||
if (!StringUtils.isBlank(cpuSerial)) {
|
||||
info.setCpuSerial(cpuSerial);
|
||||
}
|
||||
|
||||
// 解析 mainBoardSerial
|
||||
String boardSerial = extractJsonValue(jsonContent, "mainBoardSerial");
|
||||
if (!StringUtils.isBlank(boardSerial)) {
|
||||
info.setMainBoardSerial(boardSerial);
|
||||
}
|
||||
|
||||
// 检查是否读取到了任何有效信息
|
||||
if ((info.getMacAddress() == null || info.getMacAddress().isEmpty())
|
||||
&& (info.getIpAddress() == null || info.getIpAddress().isEmpty())
|
||||
&& StringUtils.isBlank(info.getCpuSerial())
|
||||
|
|
@ -192,29 +437,6 @@ public class HardwareAgentService {
|
|||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单 JSON 值提取
|
||||
*/
|
||||
private static String extractJsonValue(String content, String key) {
|
||||
// 匹配 "key":"value" 或 "key": "value" 或 "key": ["array"]
|
||||
String pattern1 = "\"" + key + "\"\\s*:\\s*\"([^\"]*)\"";
|
||||
java.util.regex.Pattern p1 = java.util.regex.Pattern.compile(pattern1);
|
||||
java.util.regex.Matcher m1 = p1.matcher(content);
|
||||
if (m1.find()) {
|
||||
return m1.group(1);
|
||||
}
|
||||
|
||||
// 匹配 "key": ["array"] 或 "key": [value]
|
||||
String pattern2 = "\"" + key + "\"\\s*:\\s*(\\[[^\\]]*\\])";
|
||||
java.util.regex.Pattern p2 = java.util.regex.Pattern.compile(pattern2);
|
||||
java.util.regex.Matcher m2 = p2.matcher(content);
|
||||
if (m2.find()) {
|
||||
return m2.group(1);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static List<String> parseCommaSeparatedList(String value) {
|
||||
List<String> list = new ArrayList<>();
|
||||
if (!StringUtils.isBlank(value)) {
|
||||
|
|
|
|||
|
|
@ -56,17 +56,10 @@ public class CustomLicenseManager extends LicenseManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置的硬件信息配置文件路径
|
||||
*/
|
||||
public static String getHardwareConfigPath() {
|
||||
return hardwareConfigPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置宿主机 Agent API 地址
|
||||
*
|
||||
* @param url Agent API 地址,如 http://host.docker.internal:8899/api/hardware-info
|
||||
* @param url Agent API 地址
|
||||
*/
|
||||
public static void setAgentApiUrl(String url) {
|
||||
CustomLicenseManager.agentApiUrl = url;
|
||||
|
|
@ -75,12 +68,6 @@ public class CustomLicenseManager extends LicenseManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置的 Agent API 地址
|
||||
*/
|
||||
public static String getAgentApiUrl() {
|
||||
return agentApiUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数,初始化许可管理器。
|
||||
|
|
|
|||
Loading…
Reference in New Issue