代码平平无奇,需要注意的是http client初始化部分。
因为是短连接,如果是大量域名去验证的话,短时间内将导致大量TIME_WAIT
go
package util
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/panjf2000/ants/v2"
log "github.com/sirupsen/logrus"
)
var client = &http.Client{
Transport: &http.Transport{
// 注意如果证书已过期,那么只有在关闭证书校验的情况下链接才能建立成功
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
DisableKeepAlives: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
d := net.Dialer{Timeout: 10 * time.Second}
conn, err := d.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
tcpConn, ok := conn.(*net.TCPConn)
if ok {
tcpConn.SetLinger(0) // RST fix TIME_WAIT问题
return tcpConn, nil
}
return conn, nil
},
},
}
func BatchCheckSsl(domains []string, batch int) (map[string]CheckSslResp, error) {
domains = Uniq(domains)
var domainSyncMap sync.Map
var wg sync.WaitGroup
pool, err := ants.NewPool(batch)
if err != nil {
log.Errorln(err)
return nil, err
}
defer pool.Release()
wg.Add(len(domains))
for _, item := range domains {
err = pool.Submit(wrapperCheckSSL(item, &domainSyncMap, &wg))
if err != nil {
log.Errorln(err)
continue
}
}
wg.Wait()
fmt.Printf("running goroutines: %d\n", ants.Running())
fmt.Printf("finish all tasks.\n")
result := make(map[string]CheckSslResp, 0)
domainSyncMap.Range(func(key, value interface{}) bool {
vv, ok := value.(*CheckSslResp)
if !ok {
log.Println(fmt.Sprintf("CheckSslResp类型转换失败:%s", key))
return false
}
result[fmt.Sprintf("%v", key)] = *vv
return true
})
return result, nil
}
func wrapperCheckSSL(hostname string, domainSslMap *sync.Map, wg *sync.WaitGroup) func() {
return func() {
checkSsl(hostname, domainSslMap, wg)
}
}
func checkSsl(hostname string, domainSslMap *sync.Map, wg *sync.WaitGroup) {
defer wg.Done()
log.Println(fmt.Sprintf("开始检查:%s", hostname))
notAfter, err := CheckSSL(hostname)
if err != nil {
log.Errorln(err)
}
log.Println(fmt.Sprintf("完成检查:%s,notAfter:%v:", hostname, notAfter))
ri := new(CheckSslResp)
ri.Domain = hostname
if err != nil {
ri.Success = false
ri.Error = err
ri.NotAfter = nil
} else {
ri.Success = true
ri.NotAfter = notAfter
}
domainSslMap.Store(hostname, ri)
return
}
func CheckSSL(url string) (*time.Time, error) {
//client := &http.Client{
// Transport: &http.Transport{
// // 注意如果证书已过期,那么只有在关闭证书校验的情况下链接才能建立成功
// TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
// },
// // 10s 超时后认为服务挂了
// Timeout: 10 * time.Second,
//}
url = strings.ReplaceAll(url, "http://", "https://")
if !strings.HasPrefix(url, "https://") {
url = fmt.Sprintf("https://%s", url)
}
log.Println(url)
resp, err := client.Get(url)
if err != nil {
log.Errorln(err)
return nil, err
}
defer func() { _ = resp.Body.Close() }()
if resp.TLS == nil {
return nil, errors.New("未获取到SSL信息")
}
// 遍历所有证书
for _, cert := range resp.TLS.PeerCertificates {
if cert.Issuer.CommonName == "Kubernetes Ingress Controller Fake Certificate" {
return nil, errors.New("Kubernetes Ingress Controller Fake Certificate")
}
// 检测证书是否已经过期
if !cert.NotAfter.After(time.Now()) {
log.Warningln(fmt.Sprintf("Website [%s] certificate has expired: %s", url, cert.NotAfter.Local().Format("2006-01-02 15:04:05")))
}
return &cert.NotAfter, nil
}
return nil, err
}
type CheckSslResp struct {
Domain string
Error error
Success bool
NotAfter *time.Time
}