فهرست منبع

feat(cdn): 增加证书解析功能并优化 HTTPS 证书处理

- 在 CDN API 中添加 ServerName 字段用于绑定证书的域名
- 实现证书解析功能,包括获取 Common Name、DNS Names、证书有效期和自签名检测
- 更新 Web 转发服务,使用证书解析结果填充 SSL 证书信息
- 优化 SSL 证书添加逻辑,包含更多证书详情
huangjl 1 ماه پیش
والد
کامیت
516916f179
4فایلهای تغییر یافته به همراه128 افزوده شده و 77 حذف شده
  1. 2 1
      api/v1/cdn.go
  2. 1 0
      internal/service/cdn.go
  3. 112 74
      internal/service/wafformatter.go
  4. 13 2
      internal/service/webforwarding.go

+ 2 - 1
api/v1/cdn.go

@@ -112,6 +112,7 @@ type SSlCert struct {
 	UserId       int64    `json:"userId" form:"userId"`             //用户id
 	Name         string   `json:"name" form:"name"`                 //证书名称
 	Description  string   `json:"description" form:"description"`   //证书描述
+	ServerName   string   `json:"serverName" form:"serverName"`     //证书绑定的域名
 	IsCA         bool     `json:"isCA" form:"isCA"`                 //是否是CA证书
 	CertData     []byte   `json:"certData" form:"certData"`         //证书内容
 	KeyData      []byte   `json:"keyData" form:"keyData"`           //证书内容
@@ -133,7 +134,7 @@ type AddSSLPolicy struct {
 	MinVersion       string   `json:"minVersion" form:"minVersion"`             //最小TLS版本
 	SslCertsJSON     []byte   `json:"sslCertsJSON" form:"sslCertsJSON"`         //SslCertsJSON
 	HstsJSON         []byte   `json:"hstsJSON" form:"hstsJSON"`                 //HstsJSON
-	ClientAuthType   int32   `json:"clientAuthType" form:"clientAuthType"`     //可选项,客户端校验类型:0 无需证书,1 需要客户端证书,2 需要任一客户端证书,3 如果客户端上传了证书才校验,4 需要客户端证书而且需要校验
+	ClientAuthType   int32    `json:"clientAuthType" form:"clientAuthType"`     //可选项,客户端校验类型:0 无需证书,1 需要客户端证书,2 需要任一客户端证书,3 如果客户端上传了证书才校验,4 需要客户端证书而且需要校验
 	CipherSuites     []string `json:"cipherSuites" form:"cipherSuites"`         //可选项,支持的TLS加密套件
 	CipherSuitesIsOn bool     `json:"cipherSuitesIsOn" form:"cipherSuitesIsOn"` //可选项,是否启用自定义加密套件
 	OcspIsOn         bool     `json:"ocspIsOn" form:"ocspIsOn"`                 //可选项,是否开启OCSP

+ 1 - 0
internal/service/cdn.go

@@ -580,6 +580,7 @@ func (s *cdnService) AddSSLCert(ctx context.Context, req v1.SSlCert) (int64, err
 		"isOn":         req.IsOn,
 		"userId":       req.UserId,
 		"name":         req.Name,
+		"serverName":   req.ServerName,
 		"description":  req.Description,
 		"isCA":         req.IsCA,
 		"certData":     req.CertData,

+ 112 - 74
internal/service/wafformatter.go

@@ -2,6 +2,8 @@ package service
 
 import (
 	"context"
+	"crypto/tls"
+	"crypto/x509"
 	"encoding/json"
 	"fmt"
 	v1 "github.com/go-nunu/nunu-layout-advanced/api/v1"
@@ -23,23 +25,26 @@ type WafFormatterService interface {
 	Require(ctx context.Context, req v1.GlobalRequire) (RequireResponse, error)
 	validateWafPortCount(ctx context.Context, hostId int) error
 	validateWafDomainCount(ctx context.Context, req v1.GlobalRequire) error
-	ConvertToWildcardDomain(ctx context.Context,domain string) (string, error)
-	AppendWafIp(ctx context.Context, req []string,returnSourceIp string) ([]v1.IpInfo, error)
+	ConvertToWildcardDomain(ctx context.Context, domain string) (string, error)
+	AppendWafIp(ctx context.Context, req []string, returnSourceIp string) ([]v1.IpInfo, error)
 	WashIps(ctx context.Context, req []string) ([]string, error)
-	PublishIpWhitelistTask(ips []string, action string,returnSourceIp string, color string)
+	PublishIpWhitelistTask(ips []string, action string, returnSourceIp string, color string)
 	PublishDomainWhitelistTask(domain, ip, action string)
 	findIpDifferences(oldIps, newIps []string) ([]string, []string)
 	WashDeleteWafIp(ctx context.Context, backendList []string) ([]string, error)
-	WashEditWafIp(ctx context.Context, newBackendList []string,oldBackendList []string) ([]string, []string, error)
+	WashEditWafIp(ctx context.Context, newBackendList []string, oldBackendList []string) ([]string, []string, error)
 	//cdn添加网站
 	AddOrigin(ctx context.Context, req v1.WebJson) (int64, error)
 	// 获取ip数量等于1的源站过白ip
 	WashDelIps(ctx context.Context, ips []string) ([]string, error)
 	// 判断域名是否是IDN,如果是,转换为 Punycode
-	ConvertToPunycodeIfIDN(ctx context.Context,domain string) (isIDN bool, punycodeDomain string, err error)
+	ConvertToPunycodeIfIDN(ctx context.Context, domain string) (isIDN bool, punycodeDomain string, err error)
+	// 解析证书
+	ParseCert(ctx context.Context, httpsCert string, httpKey string) (serverName string, commonName []string, DNSNames []string, before int64, after int64, isSelfSigned bool, err error)
 }
+
 func NewWafFormatterService(
-    service *Service,
+	service *Service,
 	globalRep repository.GlobalLimitRepository,
 	hostRep repository.HostRepository,
 	required RequiredService,
@@ -54,45 +59,45 @@ func NewWafFormatterService(
 	cdn CdnService,
 ) WafFormatterService {
 	return &wafFormatterService{
-		Service:        service,
-		globalRep: globalRep,
-		hostRep: hostRep,
-		required: required,
-		parser: parser,
-		tcpforwardingRep: tcpforwardingRep,
-		udpForWardingRep: udpForWardingRep,
-		webForwardingRep: webForwardingRep,
-		host : host,
-		mq:    mq,
-		gatewayGroupRep: gatewayGroupRep,
+		Service:           service,
+		globalRep:         globalRep,
+		hostRep:           hostRep,
+		required:          required,
+		parser:            parser,
+		tcpforwardingRep:  tcpforwardingRep,
+		udpForWardingRep:  udpForWardingRep,
+		webForwardingRep:  webForwardingRep,
+		host:              host,
+		mq:                mq,
+		gatewayGroupRep:   gatewayGroupRep,
 		gatewayGroupIpRep: gatewayGroupIpRep,
-		cdn: cdn,
+		cdn:               cdn,
 	}
 }
 
 type wafFormatterService struct {
 	*Service
-	globalRep repository.GlobalLimitRepository
-	hostRep repository.HostRepository
-	required RequiredService
-	parser ParserService
-	tcpforwardingRep repository.TcpforwardingRepository
-	udpForWardingRep repository.UdpForWardingRepository
-	webForwardingRep repository.WebForwardingRepository
-	host HostService
-	mq *rabbitmq.RabbitMQ
-	gatewayGroupRep repository.GatewayGroupRepository
+	globalRep         repository.GlobalLimitRepository
+	hostRep           repository.HostRepository
+	required          RequiredService
+	parser            ParserService
+	tcpforwardingRep  repository.TcpforwardingRepository
+	udpForWardingRep  repository.UdpForWardingRepository
+	webForwardingRep  repository.WebForwardingRepository
+	host              HostService
+	mq                *rabbitmq.RabbitMQ
+	gatewayGroupRep   repository.GatewayGroupRepository
 	gatewayGroupIpRep repository.GateWayGroupIpRepository
-	cdn CdnService
+	cdn               CdnService
 }
 
 type RequireResponse struct {
 	model.GlobalLimit `json:"globalLimit" form:"globalLimit"`
-	GatewayIps []string `json:"ips" form:"ips"`
-	Tag string `json:"tag" form:"tag"`
+	GatewayIps        []string `json:"ips" form:"ips"`
+	Tag               string   `json:"tag" form:"tag"`
 }
 
-func (s *wafFormatterService) Require(ctx context.Context,req v1.GlobalRequire) (RequireResponse, error)  {
+func (s *wafFormatterService) Require(ctx context.Context, req v1.GlobalRequire) (RequireResponse, error) {
 	var res RequireResponse
 	// 获取全局配置信息
 	globalLimit, err := s.globalRep.GetGlobalLimitByHostId(ctx, int64(req.HostId))
@@ -115,7 +120,6 @@ func (s *wafFormatterService) Require(ctx context.Context,req v1.GlobalRequire)
 	return res, nil
 }
 
-
 func (s *wafFormatterService) validateWafPortCount(ctx context.Context, hostId int) error {
 	congfig, err := s.host.GetGlobalLimitConfig(ctx, hostId)
 	if err != nil {
@@ -133,7 +137,7 @@ func (s *wafFormatterService) validateWafPortCount(ctx context.Context, hostId i
 	if err != nil {
 		return err
 	}
-	if int64(congfig.PortCount) > tcpCount + udpCount + webCount {
+	if int64(congfig.PortCount) > tcpCount+udpCount+webCount {
 		return nil
 	}
 	return fmt.Errorf("端口数量超出套餐限制,已配置%d个端口,套餐限制为%d个端口", tcpCount+udpCount+webCount, congfig.PortCount)
@@ -169,7 +173,7 @@ func (s *wafFormatterService) ConvertToWildcardDomain(ctx context.Context, domai
 	if err != nil {
 		s.logger.Error("无效的域名", zap.String("domain", domain), zap.Error(err))
 		// 如果域名无效(如 IP 地址、localhost),则返回错误。
-		return "",nil
+		return "", nil
 	}
 
 	// 2. 比较原始域名和可注册域名。
@@ -184,15 +188,15 @@ func (s *wafFormatterService) ConvertToWildcardDomain(ctx context.Context, domai
 	return domain, nil
 }
 
-func (s *wafFormatterService) AppendWafIp(ctx context.Context, req []string,returnSourceIp string) ([]v1.IpInfo, error) {
+func (s *wafFormatterService) AppendWafIp(ctx context.Context, req []string, returnSourceIp string) ([]v1.IpInfo, error) {
 	var ips []v1.IpInfo
 	for _, v := range req {
 		ips = append(ips, v1.IpInfo{
-			FType:      "0",
-			FStartIp:   v,
-			FEndIp:     v,
-			FRemark:    "宁波高防IP过白",
-			FServerIp:  returnSourceIp,
+			FType:     "0",
+			FStartIp:  v,
+			FEndIp:    v,
+			FRemark:   "宁波高防IP过白",
+			FServerIp: returnSourceIp,
 		})
 	}
 	return ips, nil
@@ -206,11 +210,11 @@ func (s *wafFormatterService) AppendWafIpByRemovePort(ctx context.Context, req [
 			return nil, err
 		}
 		ips = append(ips, v1.IpInfo{
-			FType:      "0",
-			FStartIp:   ip,
-			FEndIp:     ip,
-			FRemark:    "宁波高防IP过白",
-			FServerIp:  "",
+			FType:     "0",
+			FStartIp:  ip,
+			FEndIp:    ip,
+			FRemark:   "宁波高防IP过白",
+			FServerIp: "",
 		})
 	}
 	return ips, nil
@@ -220,7 +224,7 @@ func (s *wafFormatterService) AppendWafIpByRemovePort(ctx context.Context, req [
 func (s *wafFormatterService) WashIps(ctx context.Context, req []string) ([]string, error) {
 	var res []string
 	for _, v := range req {
-		res = append(res,v)
+		res = append(res, v)
 	}
 	return res, nil
 }
@@ -273,26 +277,25 @@ func (s *wafFormatterService) PublishDomainWhitelistTask(domain, ip, action stri
 	}
 }
 
-
-func (s *wafFormatterService) PublishIpWhitelistTask(ips []string, action string, returnSourceIp string,color string) {
+func (s *wafFormatterService) PublishIpWhitelistTask(ips []string, action string, returnSourceIp string, color string) {
 	// Define message payload, including the action
 	type ipTaskPayload struct {
-		Ips     []string `json:"ips"`
-		Action string `json:"action"`
-		ReturnSourceIp string `json:"return_source_ip"`
-		Color string `json:"color"`
+		Ips            []string `json:"ips"`
+		Action         string   `json:"action"`
+		ReturnSourceIp string   `json:"return_source_ip"`
+		Color          string   `json:"color"`
 	}
 	payload := ipTaskPayload{
-		Ips:     ips,
-		Action: action,
+		Ips:            ips,
+		Action:         action,
 		ReturnSourceIp: returnSourceIp,
-		Color: color,
+		Color:          color,
 	}
 
 	// Serialize the message
 	msgBody, err := json.Marshal(payload)
 	if err != nil {
-		s.logger.Error("序列化 IP 白名单任务消息失败", zap.Error(err), zap.Any("IPs", ips), zap.String("action", action),zap.String("color", color))
+		s.logger.Error("序列化 IP 白名单任务消息失败", zap.Error(err), zap.Any("IPs", ips), zap.String("action", action), zap.String("color", color))
 		return
 	}
 
@@ -316,13 +319,12 @@ func (s *wafFormatterService) PublishIpWhitelistTask(ips []string, action string
 	// Publish the message
 	err = s.mq.PublishWithCh(taskCfg.Exchange, routingKey, publishingMsg)
 	if err != nil {
-		s.logger.Error("发布 IP 白名单任务到 MQ 失败", zap.Error(err), zap.String("action", action),zap.String("color", color))
+		s.logger.Error("发布 IP 白名单任务到 MQ 失败", zap.Error(err), zap.String("action", action), zap.String("color", color))
 	} else {
-		s.logger.Info("成功将 IP 白名单任务发布到 MQ", zap.String("action", action),zap.String("color", color))
+		s.logger.Info("成功将 IP 白名单任务发布到 MQ", zap.String("action", action), zap.String("color", color))
 	}
 }
 
-
 func (s *wafFormatterService) findIpDifferences(oldIps, newIps []string) ([]string, []string) {
 	// 使用 map 实现 set,用于快速查找
 	oldIpsSet := make(map[string]struct{}, len(oldIps))
@@ -366,7 +368,7 @@ func (s *wafFormatterService) WashDeleteWafIp(ctx context.Context, backendList [
 	return res, nil
 }
 
-func (s *wafFormatterService) WashEditWafIp(ctx context.Context, newBackendList []string,oldBackendList []string) ([]string, []string,error) {
+func (s *wafFormatterService) WashEditWafIp(ctx context.Context, newBackendList []string, oldBackendList []string) ([]string, []string, error) {
 	var oldIps []string
 	var newIps []string
 	for _, v := range oldBackendList {
@@ -387,12 +389,9 @@ func (s *wafFormatterService) WashEditWafIp(ctx context.Context, newBackendList
 	}
 	addedIps, removedIps := s.findIpDifferences(oldIps, newIps)
 
-
-
-	return addedIps, removedIps , nil
+	return addedIps, removedIps, nil
 }
 
-
 func (s *wafFormatterService) AddOrigin(ctx context.Context, req v1.WebJson) (int64, error) {
 	ip, port, err := net.SplitHostPort(req.BackendList)
 	if err != nil {
@@ -401,14 +400,14 @@ func (s *wafFormatterService) AddOrigin(ctx context.Context, req v1.WebJson) (in
 	addr := v1.Addr{
 		Protocol: req.ApiType,
 		Host:     ip,
-		Port:  	  port,
+		Port:     port,
 	}
 	id, err := s.cdn.CreateOrigin(ctx, v1.Origin{
-		Addr: addr,
-		Weight: 10,
-		Description: req.Comment,
-		Host: req.Host,
-		IsOn: true,
+		Addr:                  addr,
+		Weight:                10,
+		Description:           req.Comment,
+		Host:                  req.Host,
+		IsOn:                  true,
 		TlsSecurityVerifyMode: "auto",
 	})
 	if err != nil {
@@ -453,7 +452,6 @@ func (s *wafFormatterService) WashDelIps(ctx context.Context, ips []string) ([]s
 		return nil, err
 	}
 
-
 	// 2. 汇总所有计数结果
 	totalCountMap := make(map[string]int)
 	// 将多个 for 循环合并到一个函数中,可以显得更整洁(可选)
@@ -478,7 +476,7 @@ func (s *wafFormatterService) WashDelIps(ctx context.Context, ips []string) ([]s
 }
 
 // 判断域名是否为 中文域名,如果是,转换为 Punycode
-func (s *wafFormatterService) ConvertToPunycodeIfIDN(ctx context.Context,domain string) (isIDN bool, punycodeDomain string, err error) {
+func (s *wafFormatterService) ConvertToPunycodeIfIDN(ctx context.Context, domain string) (isIDN bool, punycodeDomain string, err error) {
 	// 使用 idna.ToASCII 将域名转换为 Punycode。
 	// 这个函数同时会根据 IDNA 规范验证域名的合法性。
 	punycodeDomain, err = idna.ToASCII(domain)
@@ -493,4 +491,44 @@ func (s *wafFormatterService) ConvertToPunycodeIfIDN(ctx context.Context,domain
 	isIDN = !strings.EqualFold(domain, punycodeDomain)
 
 	return isIDN, punycodeDomain, nil
-}
+}
+
+func (s *wafFormatterService) ParseCert(ctx context.Context, httpsCert string, httpKey string) (serverName string, commonName []string, DNSNames []string, before int64, after int64, isSelfSigned bool, err error) {
+	cert, err := tls.X509KeyPair([]byte(httpsCert), []byte(httpKey))
+	if err != nil {
+		return "", nil, nil, 0, 0, false, fmt.Errorf("无法从字符串加载密钥对: %v", err)
+	}
+
+	if len(cert.Certificate) == 0 {
+		return "", nil, nil, 0, 0, false, fmt.Errorf("提供的证书数据中没有找到证书。")
+	}
+
+	// 解析第一个证书(通常是叶子证书)
+	x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
+	if err != nil {
+		return "", nil, nil, 0, 0, false, fmt.Errorf("无法解析证书: %v", err)
+	}
+
+	// 1. 获取 Common Name (通用名称)
+	// Common Name 位于 Subject 字段内. [1]
+	serverName = x509Cert.Subject.CommonName
+
+	// 2. 获取 DNS Names (备用主题名称中的DNS条目)
+	// DNS Names 直接是证书结构体的一个字段. [1]
+	DNSNames = x509Cert.DNSNames
+
+	// 检查证书是否为自签名
+	// 判断条件:颁发者(Issuer)和主题(Subject)相同,并且证书的签名可以由其自身的公钥验证
+	if err := x509Cert.CheckSignatureFrom(x509Cert); err == nil {
+		isSelfSigned = true
+	}
+
+	// 将CommonName放入一个切片,以匹配[]string的类型要求
+	var commonNames []string
+	if x509Cert.Subject.CommonName != "" {
+		commonNames = []string{x509Cert.Subject.CommonName}
+	}
+
+	return serverName, commonNames, DNSNames, x509Cert.NotBefore.Unix(), x509Cert.NotAfter.Unix(), isSelfSigned, nil
+
+}

+ 13 - 2
internal/service/webforwarding.go

@@ -248,16 +248,27 @@ func (s *webForwardingService) buildProxyJSONConfig(ctx context.Context, req *v1
 
 	// 判断协议类型,并处理 HTTPS 的特殊逻辑(证书)
 	if req.WebForwardingData.IsHttps == isHttps {
-		apiType = protocolHttps
+		// 处理证书信息
+		serverName, commonNames, DNSNames, before, after, isSelfSigned, err := s.wafformatter.ParseCert(ctx, req.WebForwardingData.HttpsCert, req.WebForwardingData.HttpsKey)
+		if err != nil {
+			return nil, fmt.Errorf("解析证书失败: %w", err)
+		}
+
 		// 添加 SSL 证书
+
 		sslCertId, err := s.cdn.AddSSLCert(ctx, v1.SSlCert{
 			IsOn:         true,
 			UserId:       int64(require.CdnUid),
 			Name:         req.WebForwardingData.Domain,
+			ServerName:   serverName,
 			Description:  req.WebForwardingData.Comment,
 			CertData:     []byte(req.WebForwardingData.HttpsCert),
 			KeyData:      []byte(req.WebForwardingData.HttpsKey),
-			IsSelfSigned: false,
+			TimeBeginAt:  before,
+			TimeEndAt:    after,
+			DnsNames:     DNSNames,
+			CommonNames:  commonNames,
+			IsSelfSigned: isSelfSigned,
 		})
 		if err != nil {
 			return nil, fmt.Errorf("添加SSL证书失败: %w", err)