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
}
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 = `
域名证书过期详情
序号
地址
过期时间
检查时间
错误信息
{{range $i,$v := . }}
{{$i}}
{{$v.Host}}
{{$v.ExpiryTime}}
{{$v.LastUpdate}}
{{$v.Error}}
{{end}}
`