package waftask import ( "SamWaf/common/zlog" "SamWaf/customtype" "SamWaf/enums" "SamWaf/global" "SamWaf/innerbean" "SamWaf/model" "SamWaf/model/spec" "SamWaf/service/waf_service" "SamWaf/utils" "fmt" "time" ) var ( wafHostService = waf_service.WafHostServiceApp wafSslConfigService = waf_service.WafSslConfigServiceApp wafSslOrderService = waf_service.WafSSLOrderServiceApp wafSslExpireService = waf_service.WafSslExpireServiceApp ) /* * SSL路径自动加载—_证书自动续期 */ func SSLReload() { innerLogName := "TaskSSL" zlog.Info(innerLogName, "准备进行ssl证书文件路径自动加载") if global.GCONFIG_RECORD_AUTO_LOAD_SSL == 1 { zlog.Info(innerLogName, "自动加载ssl开关:开启") //1.遍历所有证书夹得内容 //2.如果对应得位置有内容 //3.就开始判断数据是否是正常的,如果正常则备份现有,然后现有的证书替换掉 //4.重新查询让关联的主机信息重新加载 sslConfigReps, err := wafSslConfigService.GetAllListInner() if err != nil { zlog.Error(innerLogName, "ssl config:", err) return } if len(sslConfigReps) <= 0 { zlog.Info(innerLogName, "没有ssl证书") return } zlog.Info(innerLogName, fmt.Sprintf("共 %d 个证书夹条目待检查路径自动加载", len(sslConfigReps))) for _, rep := range sslConfigReps { //该证书夹条目已关闭路径自动加载(例如由 SamWaf 自动申请管理),跳过,避免覆盖 if rep.AutoLoadPath == 0 { zlog.Info(innerLogName, fmt.Sprintf("证书夹[%s] 已关闭路径自动加载,跳过", rep.Domains)) continue } zlog.Info(innerLogName, fmt.Sprintf("证书夹[%s] 开始检查路径加载: crt路径=%s key路径=%s", rep.Domains, rep.CertPath, rep.KeyPath)) err, updateSslConfig, backSslConfig, diag := rep.CheckKeyAndCertFileLoad() //文件状态(存在性 + 最后修改时间),便于排查“磁盘文件到底改没改” zlog.Info(innerLogName, fmt.Sprintf("证书夹[%s] 文件状态: crt存在=%v(修改时间=%s) key存在=%v(修改时间=%s)", rep.Domains, diag.CertExists, fmtTime(diag.CertModTime), diag.KeyExists, fmtTime(diag.KeyModTime))) if err != nil { //未更新:根据原因给出合适的日志级别(正常跳过=Info,数据问题=Warn) msg := fmt.Sprintf("证书夹[%s] 未更新: %s | 现有序列号=%s 现有到期=%s | 文件序列号=%s 文件到期=%s", rep.Domains, err.Error(), nonEmpty(diag.OldSerial), fmtTime(diag.OldValidTo), nonEmpty(diag.NewSerial), fmtTime(diag.NewValidTo)) switch diag.SkipLevel { case "info": zlog.Info(innerLogName, msg) case "error": zlog.Error(innerLogName, msg) default: zlog.Warn(innerLogName, msg) } continue } zlog.Info(innerLogName, fmt.Sprintf("证书夹[%s] 检测到新证书,准备更新: 序列号 %s -> %s, 到期时间 %s -> %s", rep.Domains, nonEmpty(diag.OldSerial), nonEmpty(diag.NewSerial), fmtTime(diag.OldValidTo), fmtTime(diag.NewValidTo))) wafSslConfigService.CreateNewIdInner(backSslConfig) err = wafSslConfigService.ModifyInner(updateSslConfig) if err != nil { zlog.Error(innerLogName, "ssl modify inner config:", err.Error()) } //若管理端证书绑定了该证书夹条目,顺带刷新管理端证书 waf_service.RefreshManagerCertBySslConfig(updateSslConfig.Id, updateSslConfig.CertContent, updateSslConfig.KeyContent) //证书导出(落盘):路径自动加载进来的新证书同样同步一份到导出路径(issue #929) //导出失败只记录日志,不影响本次加载已经生效的结果 if exportMsg, exportErr := wafSslConfigService.ExportById(updateSslConfig.Id); exportErr != nil { zlog.Error(innerLogName, fmt.Sprintf("证书夹[%s] 证书导出失败: %v", rep.Domains, exportErr)) } else if exportMsg != "" { zlog.Info(innerLogName, fmt.Sprintf("证书夹[%s] %s", rep.Domains, exportMsg)) } hostCount := 0 for _, hosts := range wafHostService.GetHostBySSLConfigId(updateSslConfig.Id) { //1.更新主机信息 2.发送主机通知 err = wafHostService.UpdateSSLInfo(updateSslConfig.CertContent, updateSslConfig.KeyContent, hosts.Code) if err != nil { zlog.Error(innerLogName, "ssl host update:", err.Error()) continue } hosts.Keyfile = updateSslConfig.KeyContent hosts.Certfile = updateSslConfig.CertContent var chanInfo = spec.ChanCommonHost{ HostCode: hosts.Code, Type: enums.ChanTypeSSL, Content: hosts, OldContent: hosts, } global.GWAF_CHAN_MSG <- chanInfo hostCount++ } zlog.Info(innerLogName, fmt.Sprintf("证书夹[%s] 处理完成,重新加载 %d 个关联主机 (crt=%s key=%s)", rep.Domains, hostCount, rep.CertPath, rep.KeyPath)) } } else { zlog.Info(innerLogName, "自动加载ssl开关:关闭") } } // fmtTime 格式化时间用于日志,零值显示为 "-" func fmtTime(t time.Time) string { if t.IsZero() { return "-" } return t.Format("2006-01-02 15:04:05") } // nonEmpty 空字符串显示为 "-",便于日志阅读 func nonEmpty(s string) string { if s == "" { return "-" } return s } // SSLOrderReload SSL证书自动续期 远程申请 func SSLOrderReload() { innerLogName := "TaskSSLOrder" zlog.Info(innerLogName, "准备进行ssl证书自动续期检测") //1.找出来所有得SSL得主机 2.查询是否存在自动SSL订单得最新得数据 3.查询没有到期得最后一条信息来申请延期 allSSLHost, _, err := wafHostService.GetAllSSLBindHost() if err == nil { for _, hostBean := range allSSLHost { zlog.Info(innerLogName, fmt.Sprintf("正在检测域名: %s:%d", hostBean.Host, hostBean.Port)) lastSslOrderInfo, err := wafSslOrderService.GetLastedInfo(hostBean.Code) if err != nil { zlog.Error(innerLogName, "ssl order get lasted info:", err.Error()) } else { if lastSslOrderInfo.Id == "" { //未找到关联信息 continue } isExpire, availDay, msg, err := lastSslOrderInfo.ExpirationMessage() if err != nil { zlog.Error(innerLogName, "ssl order get lasted info:", err.Error()) } else { // 判断是IP证书还是域名证书,使用不同的过期检测天数 isIpSSL := utils.IsIP(hostBean.Host) var expireDays int64 var certType string if isIpSSL { expireDays = global.GCONFIG_RECORD_SSL_IP_EXPIRE_DAY certType = "IP证书" } else { expireDays = global.GCONFIG_RECORD_SSLOrder_EXPIRE_DAY certType = "域名证书" } zlog.Info(fmt.Sprintf("%s %s %s 是否过期 %v 天数:%v 信息 %v ,系统检测超期天数 %v 天", innerLogName, certType, hostBean.Host, isExpire, availDay, msg, expireDays)) if isExpire == false && availDay <= int(expireDays) { // http01 续期要求 80 端口是 HTTP 明文:若被设成 HTTPS 或没有 80, // 挑战必失败且会消耗 ACME 服务商的失败频率配额,这里跳过并明确告警, // 不静默重试(静默重试只会让配额耗尽、真到期时也申请不下来) if lastSslOrderInfo.ApplyMethod == "http01" && !hostHTTP01Ready(hostBean) { msg := fmt.Sprintf("站点 %s 的证书自动续期已跳过:文件验证(http01)需要 80 端口以 HTTP 明文提供服务,"+ "当前该站点没有 HTTP 协议的 80 端口。请在网站编辑的「监听端口」里把 80 设为 HTTP,或把该证书订单改用 DNS 验证方式", hostBean.Host) zlog.Error(innerLogName, msg) global.GQEQUE_MESSAGE_DB.Enqueue(innerbean.OpResultMessageInfo{ BaseMessageInfo: innerbean.BaseMessageInfo{OperaType: "提示信息", Server: global.GWAF_CUSTOM_SERVER_NAME}, Msg: msg, Success: "false", }) continue } //没过期 且是指定天数 就才处理 var chanInfo = spec.ChanSslOrder{ Type: enums.ChanSslOrderrenew, Content: lastSslOrderInfo, } global.GWAF_CHAN_SSLOrder <- chanInfo } } } } } } /* * SSL证书过期检测 */ func SSLExpireCheck() { // 无论正常结束还是中途 return/出错,都释放运行标志位,避免前端一直卡在“正在进行中” defer func() { global.GWAF_RUNTIME_SSL_EXPIRE_CHECK = false }() innerLogName := "SSLExpireCheck" if global.GCONFIG_SSL_EXPIRE_AUTO_SYNC_HOST == 1 { SyncHostToSslCheck() } zlog.Info(innerLogName, "准备进行SSL证书过期检测") sslExpires, cnt, err := wafSslExpireService.GetAllList() if err != nil { zlog.Error(innerLogName, "ssl expire list:", err.Error()) return } if cnt == 0 { zlog.Error(innerLogName, "无检测数据任务") return } for _, expireBean := range sslExpires { zlog.Info(innerLogName, fmt.Sprintf("正在检测域名: %s:%d", expireBean.Domain, expireBean.Port)) host := fmt.Sprintf("%s:%d", expireBean.Domain, expireBean.Port) expiryTime, err := utils.CheckSSLCertificateExpiry(host) expireBean.LastDetect = customtype.JsonTime(time.Now()) if err != nil { expireBean.VisitLog = err.Error() } else { expireBean.ValidTo = customtype.JsonTime(expiryTime) expireBean.VisitLog = "" // 检查证书是否即将过期,并发送通知 checkAndNotifySSLExpire(expireBean.Domain, expiryTime) } //更新数据 wafSslExpireService.Modify(expireBean) } zlog.Info(innerLogName, "进行SSL证书过期检测完毕") } // SyncHostToSslCheck 同步已存在主机到SSL证书检测任务里 func SyncHostToSslCheck() { // 无论正常结束还是中途 return/出错,都释放运行标志位,避免前端一直卡在“正在进行中” defer func() { global.GWAF_RUNTIME_SSL_SYNC_HOST = false }() innerLogName := "SyncHostToSslCheck" zlog.Info(innerLogName, "准备进行同步已存在主机到SSL证书检测任务里") //检测信息 如果主机和端口不存在 则新增 sslHost, sslHostCnt, err := wafHostService.GetAllSSLHost() if err != nil { zlog.Error(innerLogName, "sync host to ssl expire:", err.Error()) return } if sslHostCnt == 0 { zlog.Error(innerLogName, "sync host not find ssl host") return } for _, hostBean := range sslHost { if wafSslExpireService.CheckIsExist(hostBean.Host, hostBean.Port) == 0 { //插入新的数据 wafSslExpireService.Add(hostBean.Host, hostBean.Port) } } } // checkAndNotifySSLExpire 检查SSL证书是否即将过期,并发送通知 func checkAndNotifySSLExpire(domain string, expiryTime time.Time) { // 计算剩余天数 daysLeft := int(time.Until(expiryTime).Hours() / 24) // 判断是IP证书还是域名证书 isIpCert := utils.IsIP(domain) var expireDays int var certType string if isIpCert { // IP证书:使用IP证书专用配置(默认3天) expireDays = int(global.GCONFIG_RECORD_SSL_IP_EXPIRE_DAY) certType = "IP证书" if expireDays <= 0 { expireDays = 3 // IP证书默认提前3天 } } else { // 域名证书:使用域名证书配置(默认30天) expireDays = int(global.GCONFIG_RECORD_SSLOrder_EXPIRE_DAY) certType = "域名证书" if expireDays <= 0 { expireDays = 30 // 域名证书默认提前30天 } } // 定义需要提醒的天数阈值 // 基于配置的天数,设置多个提醒节点 alertThresholds := []int{} alertThresholds = append(alertThresholds, expireDays) // 配置的天数 // IP证书由于有效期短(通常7天),提醒策略更密集 if isIpCert { // IP证书:3天、1天 if expireDays > 1 { alertThresholds = append(alertThresholds, 1) // 1天 } } else { // 域名证书:配置天数、一半、7天、3天、1天 if expireDays > 15 { alertThresholds = append(alertThresholds, expireDays/2) // 例如:15天 } if expireDays > 7 { alertThresholds = append(alertThresholds, 7) // 7天 } if expireDays > 3 { alertThresholds = append(alertThresholds, 3) // 3天 } alertThresholds = append(alertThresholds, 1) // 1天 } // 检查是否需要提醒:剩余天数等于某个阈值,或者少于最小阈值 shouldAlert := false // 情况1:剩余天数正好在阈值上 for _, threshold := range alertThresholds { if daysLeft == threshold { shouldAlert = true break } } // 情况2:剩余天数小于配置天数(在提醒范围内) if daysLeft <= expireDays && daysLeft >= 0 { shouldAlert = true } // 情况3:已经过期 if daysLeft < 0 { shouldAlert = true } // 如果需要提醒,发送通知 if shouldAlert { serverName := global.GWAF_CUSTOM_SERVER_NAME if serverName == "" { serverName = "未命名服务器" } var noticeMsg string if daysLeft < 0 { noticeMsg = fmt.Sprintf("%s已过期 %d 天", certType, -daysLeft) } else if daysLeft == 0 { noticeMsg = fmt.Sprintf("%s今天过期", certType) } else if daysLeft == 1 { noticeMsg = fmt.Sprintf("%s明天过期", certType) } else { noticeMsg = fmt.Sprintf("%s即将在 %d 天后过期", certType, daysLeft) } zlog.Info("SSLExpireCheck", fmt.Sprintf("[%s] %s: %s (剩余%d天)", certType, domain, noticeMsg, daysLeft)) // 发送SSL证书过期通知到消息队列 global.GQEQUE_MESSAGE_DB.Enqueue(innerbean.SSLExpireMessageInfo{ BaseMessageInfo: innerbean.BaseMessageInfo{ OperaType: "SSL证书过期提醒", Server: serverName, }, Domain: domain, ExpireTime: expiryTime.Format("2006-01-02 15:04:05"), DaysLeft: daysLeft, }) } } // hostHTTP01Ready 站点是否具备 ACME http01 文件验证条件:存在协议为 HTTP 的 80 端口。 // 与 api/waf_sslorder.go 的 check80Port 同一判据(那里还要区分"没有80"与"80是HTTPS"以给不同文案)。 func hostHTTP01Ready(host model.Hosts) bool { for _, listen := range utils.ResolveHostListens(host) { if listen.Port == 80 { return listen.Protocol == utils.ListenProtoHTTP } } return false }