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)) }