package main
import (
"crypto/tls"
"flag"
"fmt"
"net"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/dustin/go-humanize"
)
func main() {
warning_period := flag.Int("warn", 7, "warning period in days")
timeout := flag.Duration("timeout", 2*time.Second, "timeout for connection")
concurrency := flag.Int("c", 128, "number of concurrent checks")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] <host:port>...\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
// endpoints to check
endpoints := flag.Args()
dialer := &net.Dialer{
Timeout: *timeout,
}
warn_if_expired_at := time.Now().AddDate(0, 0, *warning_period)
checker := NewSimpleHostChecker(dialer, warn_if_expired_at)
os.Exit(RunChecks(checker, endpoints, *concurrency))
}
type HostChecker interface {
CheckHost(host string) (bool, error)
}
func RunChecks(
checker HostChecker,
endpoints []string,
concurrency int,
) (exitcode int) {
// semaphore to limit concurrency to a reasonable number
semaphore := make(chan struct{}, concurrency)
wg := new(sync.WaitGroup)
wg.Add(len(endpoints))
for _, i := range endpoints {
// sleep 1ms to avoid hitting DNS resolver limits
time.Sleep(time.Millisecond)
// acquire semaphore
semaphore <- struct{}{}
go func(i string) {
// mark as done when we're finished
defer wg.Done()
// release semaphore
defer func() { <-semaphore }()
expires_soon, err := checker.CheckHost(i)
if err != nil {
fmt.Printf("can't check %s: %s\n", i, err)
exitcode = 1
} else if expires_soon {
exitcode = 1
}
}(i)
}
wg.Wait()
return exitcode
}
var addrOverride = regexp.MustCompile(`^([^:]+):(((\[[0-9a-f:]+\])|([^:]+)):\d+)$`)
type SimpleHostChecker struct {
dialer *net.Dialer
warn_if_expired_at time.Time
}
func NewSimpleHostChecker(
dialer *net.Dialer,
warn_if_expired_at time.Time,
) *SimpleHostChecker {
return &SimpleHostChecker{
dialer: dialer,
warn_if_expired_at: warn_if_expired_at,
}
}
func expiresInOrExpired(t time.Time) string {
if time.Now().After(t) {
return "expired"
} else {
return "expires in"
}
}
func (c *SimpleHostChecker) CheckHost(
host string,
) (expires_soon bool, err error) {
config := tls.Config{
// we still want to get connection even if the cert is expired, or if
// the hostname doesn't match
InsecureSkipVerify: true,
}
// custom address parsing to allow default port and address override
if !strings.Contains(host, ":") {
host = host + ":443"
} else if match := addrOverride.FindStringSubmatch(host); match != nil {
config.ServerName = match[1]
host = match[2]
}
// make a connection to get the certificate
conn, err := tls.DialWithDialer(c.dialer, "tcp", host, &config)
if err != nil {
return
}
conn.Close()
// check all certificates in the chain for expiration
for _, cert := range conn.ConnectionState().PeerCertificates {
if c.warn_if_expired_at.After(cert.NotAfter) {
expires_soon = true
fmt.Printf("Certificate for %s (%s) %s %s.\n",
host, cert.Subject.CommonName,
expiresInOrExpired(cert.NotAfter),
humanize.Time(cert.NotAfter))
}
}
// TODO: validate hostname and chain of trust
return
}