mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-10-31 02:01:44 +08:00
388 lines
14 KiB
Go
388 lines
14 KiB
Go
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
|
|
}
|