Go1.10域名证书检查服务代码片段

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>`

猜你喜欢

转载自blog.csdn.net/fyxichen/article/details/80559148