253 lines
7.2 KiB
Go
253 lines
7.2 KiB
Go
package httpdns
|
||
|
||
import (
|
||
"context"
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"crypto/tls"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
|
||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||
"github.com/iwind/TeaGo/rands"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// HTTPDNSSandboxService HTTPDNS解析测试服务
|
||
type HTTPDNSSandboxService struct {
|
||
services.BaseService
|
||
pb.UnimplementedHTTPDNSSandboxServiceServer
|
||
}
|
||
|
||
// nodeResolveResponse 节点返回的 JSON 结构(对齐 EdgeHttpDNS resolve_server.go)
|
||
type nodeResolveResponse struct {
|
||
Code string `json:"code"`
|
||
Message string `json:"message"`
|
||
RequestID string `json:"requestId"`
|
||
Data *nodeResolveData `json:"data,omitempty"`
|
||
}
|
||
|
||
type nodeResolveData struct {
|
||
Domain string `json:"domain"`
|
||
QType string `json:"qtype"`
|
||
TTL int32 `json:"ttl"`
|
||
Records []*nodeResolveRecord `json:"records"`
|
||
Client *nodeClientInfo `json:"client"`
|
||
Summary string `json:"summary"`
|
||
}
|
||
|
||
type nodeResolveRecord struct {
|
||
Type string `json:"type"`
|
||
IP string `json:"ip"`
|
||
Weight int32 `json:"weight"`
|
||
Line string `json:"line"`
|
||
Region string `json:"region"`
|
||
}
|
||
|
||
type nodeClientInfo struct {
|
||
IP string `json:"ip"`
|
||
Region string `json:"region"`
|
||
Carrier string `json:"carrier"`
|
||
Country string `json:"country"`
|
||
}
|
||
|
||
func (this *HTTPDNSSandboxService) TestHTTPDNSResolve(ctx context.Context, req *pb.TestHTTPDNSResolveRequest) (*pb.TestHTTPDNSResolveResponse, error) {
|
||
_, _, err := this.ValidateAdminAndUser(ctx, true)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if len(req.AppId) == 0 || len(req.Domain) == 0 {
|
||
return nil, errors.New("appId 和 domain 不能为空")
|
||
}
|
||
|
||
app, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(this.NullTx(), req.AppId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if app == nil || !app.IsOn {
|
||
return &pb.TestHTTPDNSResolveResponse{
|
||
Code: "APP_NOT_FOUND_OR_DISABLED",
|
||
Message: "找不到指定的应用,或该应用已下线",
|
||
RequestId: "rid-" + rands.HexString(12),
|
||
}, nil
|
||
}
|
||
if req.ClusterId > 0 && req.ClusterId != int64(app.PrimaryClusterId) && req.ClusterId != int64(app.BackupClusterId) {
|
||
return &pb.TestHTTPDNSResolveResponse{
|
||
Code: "APP_CLUSTER_MISMATCH",
|
||
Message: "当前应用未绑定到该集群 (主集群: " + strconv.FormatInt(int64(app.PrimaryClusterId), 10) + ", 备用集群: " + strconv.FormatInt(int64(app.BackupClusterId), 10) + ")",
|
||
RequestId: "rid-" + rands.HexString(12),
|
||
}, nil
|
||
}
|
||
|
||
qtype := strings.ToUpper(strings.TrimSpace(req.Qtype))
|
||
if qtype == "" {
|
||
qtype = "A"
|
||
}
|
||
|
||
// 获取集群服务域名
|
||
clusterId := req.ClusterId
|
||
if clusterId <= 0 {
|
||
clusterId = int64(app.PrimaryClusterId)
|
||
}
|
||
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(this.NullTx(), clusterId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if cluster == nil {
|
||
return &pb.TestHTTPDNSResolveResponse{
|
||
Code: "CLUSTER_NOT_FOUND",
|
||
Message: "找不到指定的集群",
|
||
RequestId: "rid-" + rands.HexString(12),
|
||
}, nil
|
||
}
|
||
|
||
serviceDomain := strings.TrimSpace(cluster.ServiceDomain)
|
||
if len(serviceDomain) == 0 {
|
||
return &pb.TestHTTPDNSResolveResponse{
|
||
Code: "NO_SERVICE_DOMAIN",
|
||
Message: "该集群未配置服务域名",
|
||
RequestId: "rid-" + rands.HexString(12),
|
||
}, nil
|
||
}
|
||
|
||
// 构造请求转发到 EdgeHttpDNS 节点
|
||
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
query := url.Values{}
|
||
query.Set("appId", req.AppId)
|
||
query.Set("dn", req.Domain)
|
||
query.Set("qtype", qtype)
|
||
if len(req.ClientIP) > 0 {
|
||
query.Set("cip", req.ClientIP)
|
||
}
|
||
if len(req.Sid) > 0 {
|
||
query.Set("sid", req.Sid)
|
||
}
|
||
if len(req.SdkVersion) > 0 {
|
||
query.Set("sdk_version", req.SdkVersion)
|
||
}
|
||
if len(req.Os) > 0 {
|
||
query.Set("os", req.Os)
|
||
}
|
||
|
||
// 应用开启验签时,沙盒自动生成签名参数,避免测试请求被拒绝
|
||
if secret != nil && secret.SignEnabled {
|
||
signSecret := strings.TrimSpace(secret.SignSecret)
|
||
if len(signSecret) == 0 {
|
||
return &pb.TestHTTPDNSResolveResponse{
|
||
Code: "SIGN_INVALID",
|
||
Message: "应用开启了请求验签,但未配置有效加签 Secret",
|
||
RequestId: "rid-" + rands.HexString(12),
|
||
Domain: req.Domain,
|
||
Qtype: qtype,
|
||
}, nil
|
||
}
|
||
|
||
exp := strconv.FormatInt(time.Now().Unix()+300, 10)
|
||
nonce := "sandbox-" + rands.HexString(16)
|
||
sign := buildSandboxResolveSign(signSecret, req.AppId, req.Domain, qtype, exp, nonce)
|
||
|
||
query.Set("exp", exp)
|
||
query.Set("nonce", nonce)
|
||
query.Set("sign", sign)
|
||
}
|
||
|
||
resolveURL := "https://" + serviceDomain + "/resolve?" + query.Encode()
|
||
|
||
httpClient := &http.Client{
|
||
Timeout: 5 * time.Second,
|
||
Transport: &http.Transport{
|
||
TLSClientConfig: &tls.Config{
|
||
InsecureSkipVerify: true, // 沙盒测试环境允许自签名证书
|
||
},
|
||
},
|
||
}
|
||
|
||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, resolveURL, nil)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("构建请求失败: %w", err)
|
||
}
|
||
|
||
resp, err := httpClient.Do(httpReq)
|
||
if err != nil {
|
||
return &pb.TestHTTPDNSResolveResponse{
|
||
Code: "NODE_UNREACHABLE",
|
||
Message: "无法连接到 HTTPDNS 节点: " + err.Error(),
|
||
RequestId: "rid-" + rands.HexString(12),
|
||
Domain: req.Domain,
|
||
Qtype: qtype,
|
||
}, nil
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取节点响应失败: %w", err)
|
||
}
|
||
|
||
// 解析节点返回的 JSON
|
||
var nodeResp nodeResolveResponse
|
||
if err := json.Unmarshal(body, &nodeResp); err != nil {
|
||
return &pb.TestHTTPDNSResolveResponse{
|
||
Code: "PARSE_ERROR",
|
||
Message: "解析节点返回数据失败: " + err.Error(),
|
||
RequestId: "rid-" + rands.HexString(12),
|
||
Domain: req.Domain,
|
||
Qtype: qtype,
|
||
}, nil
|
||
}
|
||
|
||
// 映射节点响应到 protobuf 响应
|
||
pbResp := &pb.TestHTTPDNSResolveResponse{
|
||
Code: nodeResp.Code,
|
||
Message: nodeResp.Message,
|
||
RequestId: nodeResp.RequestID,
|
||
Domain: req.Domain,
|
||
Qtype: qtype,
|
||
}
|
||
|
||
if nodeResp.Data != nil {
|
||
pbResp.Ttl = nodeResp.Data.TTL
|
||
pbResp.Summary = nodeResp.Data.Summary
|
||
|
||
if nodeResp.Data.Client != nil {
|
||
pbResp.ClientIP = nodeResp.Data.Client.IP
|
||
pbResp.ClientRegion = nodeResp.Data.Client.Region
|
||
pbResp.ClientCarrier = nodeResp.Data.Client.Carrier
|
||
pbResp.ClientCountry = nodeResp.Data.Client.Country
|
||
}
|
||
|
||
for _, rec := range nodeResp.Data.Records {
|
||
pbResp.Records = append(pbResp.Records, &pb.HTTPDNSResolveRecord{
|
||
Type: rec.Type,
|
||
Ip: rec.IP,
|
||
Ttl: nodeResp.Data.TTL,
|
||
Weight: rec.Weight,
|
||
Line: rec.Line,
|
||
Region: rec.Region,
|
||
})
|
||
}
|
||
}
|
||
|
||
return pbResp, nil
|
||
}
|
||
|
||
func buildSandboxResolveSign(signSecret string, appID string, domain string, qtype string, exp string, nonce string) string {
|
||
raw := strings.TrimSpace(appID) + "|" + strings.ToLower(strings.TrimSpace(domain)) + "|" + strings.ToUpper(strings.TrimSpace(qtype)) + "|" + strings.TrimSpace(exp) + "|" + strings.TrimSpace(nonce)
|
||
mac := hmac.New(sha256.New, []byte(strings.TrimSpace(signSecret)))
|
||
_, _ = mac.Write([]byte(raw))
|
||
return hex.EncodeToString(mac.Sum(nil))
|
||
}
|