package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"os"
"sort"
"strings"
"sync"
"time"
)
var (
interval int
listen, cpath string
domainManager = &Domains{maps: make(map[string]Domain), runc: make(chan string)}
temp, _ = template.New("index.html").Parse(index)
)
func init() {
flag.StringVar(&listen, "l", ":1789", "指定监听的地址端口")
flag.StringVar(&cpath, "c", "config.json", "指定domains配置文件")
flag.IntVar(&interval, "i", 24, "指定检查间隔,单位:小时")
flag.Parse()
buf, err := ioutil.ReadFile(cpath)
if err != nil {
fmt.Printf("Read config error:%s\n", err.Error())
return
}
var domains []string
err = json.Unmarshal(buf, &domains)
if err != nil {
fmt.Printf("Unmarshal error:%s\n", err.Error())
return
}
//设置全局请求超时间
http.DefaultClient.Timeout = 15 * time.Second
//启动后台检查更新服务
go domainManager.Run(context.Background(), time.Duration(interval)*time.Hour)
for _, domain := range domains {
if err = domainManager.AddDomain(domain, true); err != nil {
fmt.Println(err.Error())
}
}
}
func main() {
err := http.ListenAndServe(listen, domainManager)
if err != nil {
fmt.Printf("Listen server error:%s\n", err.Error())
}
}
type Domain struct {
Host string `json:"host,omitempty"`
ExpiryTime time.Time `json:"expiry_time,omitempty"`
LastUpdate time.Time
Error string
}
type DomainSort []Domain
func (ds DomainSort) Len() int {
return len(ds)
}
func (ds DomainSort) Less(i, j int) bool {
return ds[i].ExpiryTime.Before(ds[j].ExpiryTime)
}
func (ds DomainSort) Swap(i, j int) {
ds[i], ds[j] = ds[j], ds[i]
}
type Domains struct {
lock sync.RWMutex
runc chan string
maps map[string]Domain
}
//控制并发数查询数:20
func (ds *Domains) update() {
var block = make(chan struct{}, 20)
for domain := range ds.runc {
block <- struct{}{}
go func(domain string) {
ds.updateExpriyTime(domain)
<-block
}(domain)
}
}
//更新主机证书过期时间
func (ds *Domains) updateExpriyTime(urlStr string) {
resp, err := http.Get(urlStr)
domain := Domain{Host: urlStr, LastUpdate: time.Now(), Error: ""}
if err == nil {
if info := resp.TLS; info != nil {
if len(info.PeerCertificates) > 0 {
domain.ExpiryTime = info.PeerCertificates[0].NotAfter
}
} else {
domain.Error = "证书请求检查错误"
}
resp.Body.Close()
} else {
if e, ok := err.(*url.Error); ok && e.Timeout() {
domain.Error = "网络连接超时"
} else {
domain.Error = err.Error()
}
}
ds.lock.Lock()
ds.maps[urlStr] = domain
ds.lock.Unlock()
}
func (ds *Domains) Run(ctx context.Context, interval time.Duration) {
timer := time.NewTimer(interval)
go ds.update()
for {
select {
case <-timer.C:
ds.lock.RLock()
var domains = make([]string, 0, len(ds.maps))
for domain, _ := range ds.maps {
domains = append(domains, domain)
}
ds.lock.RUnlock()
for _, domain := range domains {
ds.runc <- domain
}
timer.Reset(interval)
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return
}
}
}
func (ds *Domains) AddDomain(domain string, backend bool) error {
if !strings.HasPrefix(domain, "https://") {
return fmt.Errorf("%s 必须是https地址", domain)
}
ds.lock.RLock()
_, ok := ds.maps[domain]
ds.lock.RUnlock()
if ok {
return fmt.Errorf("%s already is exist", domain)
}
//异步检查更新
if backend {
ds.runc <- domain
} else {
ds.updateExpriyTime(domain)
}
return nil
}
func (ds *Domains) DelDomain(domains ...string) {
ds.lock.Lock()
for _, domain := range domains {
delete(ds.maps, domain)
}
ds.lock.Unlock()
}
//返回按过期时间排序的列表
func (ds *Domains) ToSlice() DomainSort {
ds.lock.Lock()
hosts := make(DomainSort, len(ds.maps))
var idx int = 0
for _, v := range ds.maps {
hosts[idx] = v
idx++
}
ds.lock.Unlock()
sort.Sort(hosts)
return hosts
}
//同步所有域名到磁盘,做持久化
func (ds *Domains) Todisk(path string) error {
File, err := os.Create(path + ".swap")
if err != nil {
return err
}
ds.lock.RLock()
defer ds.lock.RUnlock()
var domains = make([]string, 0, len(ds.maps))
enc := json.NewEncoder(File)
enc.SetIndent("", "\t")
for domain := range ds.maps {
domains = append(domains, domain)
}
err = enc.Encode(domains)
File.Close()
if err == nil {
os.Remove(path)
err = os.Rename(path+".swap", path)
}
return err
}
type manager struct {
Action string `json:"action"`
Domains []string `json:"domains"`
}
func (ds *Domains) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Printf("RemoteIP:%s\tURI:%s\n", r.RemoteAddr, r.RequestURI)
d := ds.ToSlice()
switch r.URL.Path {
default:
err := temp.Execute(w, d)
if err != nil {
fmt.Printf("Execute data error:%s\n", err.Error())
}
case "/api/list":
buf, _ := json.MarshalIndent(d, "", "\t")
w.Write(buf)
case "/api/manage":
if r.ContentLength > 2<<20 || r.ContentLength == 0 {
fmt.Fprintf(w, "无效的请求消息")
return
}
buf, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var manage = manager{}
err = json.Unmarshal(buf, &manage)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
switch manage.Action {
case "add":
for _, domain := range manage.Domains {
ds.AddDomain(domain, false)
}
case "del":
ds.DelDomain(manage.Domains...)
default:
return
}
err = ds.Todisk(cpath)
if err != nil {
http.Error(w, err.Error(), 500)
}
}
}
const index = `
<html>
<meta charset="utf-8" />
<title>域名证书过期详情</title>
<head>
<link href="https://magicbox.bkclouds.cc/static_api/v3/assets/bootstrap-3.3.4/css/bootstrap.min.css" rel="stylesheet">
<link href="https://magicbox.bkclouds.cc/static_api/v3/bk/css/bk.css" rel="stylesheet">
</head>
<body>
<table class="table table-out-bordered table-bordered table-hover">
<thead>
<tr>
<th style="width: 7%">序号</th>
<th style="width:20%;">地址</th>
<th>过期时间</th>
<th>检查时间</th>
<th>错误信息</th>
</tr>
</thead>
<tbody>
{{range $i,$v := . }} <tr>
<td>{{$i}}</td>
<td>{{$v.Host}}</td>
<td>{{$v.ExpiryTime}}</td>
<td>{{$v.LastUpdate}}</td>
<td>{{$v.Error}}</td>
</tr>
{{end}}
</tbody>
</table>
</body>
</html>`
Go1.10域名证书检查服务代码片段
最新推荐文章于 2023-12-25 22:15:00 发布