Golang
主页 > 脚本 > Golang >

怎么使用工具自动监测SSL证书有效期并发送提醒邮件

2024-10-02 | 佚名 | 点击:

自从云厂商的免费ssl证书改成3个月,而且证书数量还是20个之后,自己网站的ssl证书就换成了其它免费方案。但是免费方案不会提醒证书过期,所以写个工具每天定时查询证书剩余有效天数,如果证书即将过期,就发送邮件提醒。

基本实现

最基本的代码功能就是检测网站ssl证书的有效天数,可以用命令行传参的方式指定网站域名。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

package main

 

import (

    "crypto/tls"

    "flag"

    "fmt"

    "net"

    "os"

    "sync"

    "time"

)

 

var (

    port int

    wg   sync.WaitGroup

)

 

func checkssl(domain string, port int) {

    defer wg.Done()

    host := fmt.Sprintf("%s:%d", domain, port)

    conn, err := tls.DialWithDialer(&net.Dialer{

        Timeout:  time.Second * 5,

        Deadline: time.Now().Add(time.Second * 5),

    }, "tcp", host, &tls.Config{InsecureSkipVerify: true})

    if err != nil {

        fmt.Println(err)

        return

    }

    defer conn.Close()

 

    stats := conn.ConnectionState()

    certs := stats.PeerCertificates[0]

    localtz, _ := time.LoadLocation("Asia/Shanghai")

    issueTime := certs.NotBefore.In(localtz)

    expireTime := certs.NotAfter.In(localtz)

 

    today := time.Now().In(localtz)

    dayLeft := int(expireTime.Sub(today).Hours() / 24)

    fmt.Printf("%s, issue time: %v, expire time: %v, days left: %v\n", domain, issueTime, expireTime, dayLeft)

}

 

func main() {

    flag.IntVar(&port, "p", 443, "port, example: ./checkssl -p 1443 <domain name>")

    flag.Parse()

    positionArgs := flag.Args()

    if len(positionArgs) == 0 {

        fmt.Println("Error: Missing domain name")

        fmt.Println("Usage: ./checkssl <domain name>")

        os.Exit(1)

    }

 

    wg.Add(len(positionArgs))

    for _, arg := range positionArgs {

        go checkssl(arg, port)

    }

    wg.Wait()

}

使用示例

1

2

3

4

5

6

7

8

9

# 1. 编译

go build

# 2. 命令行传参的方式指定域名

./check-ssl baidu.com ithome.com qq.com

 

# 输出

baidu.com, issue time: 2024-01-30 08:00:00 +0800 CST, expire time: 2025-03-02 07:59:59 +0800 CST, days left: 187

ithome.com, issue time: 2024-01-22 08:00:00 +0800 CST, expire time: 2025-02-22 07:59:59 +0800 CST, days left: 179

qq.com, issue time: 2024-06-04 08:00:00 +0800 CST, expire time: 2025-06-11 07:59:59 +0800 CST, days left: 288

完善功能

需要完善的功能主要是发送邮件,这里使用SMTP协议来发送邮件。如果跟我一样用的是163邮箱,则需要先去获取一个SMTP的授权码。

因为需要配置SMTP的连接信息,所以改成了用文件来传入配置,也方便后期修改。配置文件config.yaml示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

domains:

  - baidu.com

  - qq.com

 

email:

  smtp:

    host: "smtp.163.com"  # smtp服务器的地址

    port: 465  # 因为云服务器屏蔽了25端口, 只能使用tls加密的465端口

    from: ""   # 发送方邮箱

    token: ""  # 授权码

  sendto:

    - "qq@qq.com"  # 接收方的邮箱地址

  expire: 7  # 证书剩余有效天数, 小于7天时发送邮件提醒

读取配置的代码文件config.go,使用viper来读取配置文件。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

package main

 

import "github.com/spf13/viper"

 

var (

    v *viper.Viper

)

 

type SMTPServer struct {

    Host  string

    Port  int

    Token string

    From  string

}

 

func initViper() {

    v = viper.New()

    v.AddConfigPath(".")

    v.SetConfigType("yaml")

    v.SetConfigFile(configfile)

    err := v.ReadInConfig()

    if err != nil {

        panic(err)

    }

}

 

type configer struct{}

 

func NewConfiger() configer {

    if v == nil {

        initViper()

    }

    return configer{}

}

 

func (c configer) GetSMTPServer() SMTPServer {

    return SMTPServer{

        Host:  v.GetString("email.smtp.host"),

        Port:  v.GetInt("email.smtp.port"),

        Token: v.GetString("email.smtp.token"),

        From:  v.GetString("email.smtp.from"),

    }

}

 

func (c configer) GetDomains() []string {

    return v.GetStringSlice("domains")

}

 

func (c configer) GetSendTos() []string {

    return v.GetStringSlice("email.sendto")

}

 

func (c configer) GetExpiry() int {

    return v.GetInt("email.expire")

}

发送邮件的相关代码文件:notify.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

package main

 

import (

    "crypto/tls"

    "fmt"

    "net/smtp"

 

    "github.com/jordan-wright/email"

)

 

type Postman struct {

    SmtpServer SMTPServer

    SendTos    []string

}

 

func (p Postman) SendEmail(domain string, dayleft int) {

    auth := smtp.PlainAuth("", p.SmtpServer.From, p.SmtpServer.Token, p.SmtpServer.Host)

    e := &email.Email{

        To:      p.SendTos,

        From:    fmt.Sprintf("YXHYW <%s>", p.SmtpServer.From),

        Subject: fmt.Sprintf("域名 %s SSL证书过期提醒", domain),

        Text:    []byte(fmt.Sprintf("域名 %s 的SSL证书即将过期, 剩余有效期 %d 天", domain, dayleft)),

    }

    // err := e.Send(fmt.Sprintf("%s:%d", p.SmtpServer.Host, p.SmtpServer.Port), auth)

    addr := fmt.Sprintf("%s:%d", p.SmtpServer.Host, p.SmtpServer.Port)

    fmt.Println("SMTP Server addr: ", addr)

    err := e.SendWithTLS(addr, auth, &tls.Config{

        InsecureSkipVerify: false,

        ServerName:         p.SmtpServer.Host,

    })

    if err != nil {

        fmt.Printf("Send email failed, %v\n", err)

    }

}

主体代码文件main.go,主要修改地方:检测到证书即将过期后,调用发送邮件的相关方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

package main

 

import (

    "crypto/tls"

    "flag"

    "fmt"

    "net"

    "sync"

    "time"

)

 

var (

    port       int

    configfile string

    wg         sync.WaitGroup

    c          configer = NewConfiger()

)

 

func checkssl(domain string, port int) {

    defer wg.Done()

    host := fmt.Sprintf("%s:%d", domain, port)

    conn, err := tls.DialWithDialer(&net.Dialer{

        Timeout:  time.Second * 5,

        Deadline: time.Now().Add(time.Second * 5),

    }, "tcp", host, &tls.Config{InsecureSkipVerify: true})

    if err != nil {

        fmt.Println(err)

        return

    }

    defer conn.Close()

 

    stats := conn.ConnectionState()

    certs := stats.PeerCertificates[0]

    localtz, _ := time.LoadLocation("Asia/Shanghai")

    issueTime := certs.NotBefore.In(localtz)

    expireTime := certs.NotAfter.In(localtz)

 

    today := time.Now().In(localtz)

    dayLeft := int(expireTime.Sub(today).Hours() / 24)

    fmt.Printf("%s, issue time: %v, expire time: %v, days left: %v\n", domain, issueTime, expireTime, dayLeft)

 

    // c := NewConfiger()

    if dayLeft < c.GetExpiry() {

        p := Postman{SmtpServer: c.GetSMTPServer(), SendTos: c.GetSendTos()}

        p.SendEmail(domain, dayLeft)

    }

}

 

func main() {

    flag.IntVar(&port, "p", 443, "port, example: ./check-ssl -p 1443 <domain name>")

    flag.StringVar(&configfile, "c", "config.yaml", "config file")

    flag.Parse()

 

    conf := NewConfiger()

    domains := conf.GetDomains()

 

    wg.Add(len(domains))

    for _, arg := range domains {

        go checkssl(arg, port)

    }

    wg.Wait()

}

本地测试通过后,可以配到服务器的crontab中每天执行。

原文链接:
相关文章
最新更新