aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitch Riedstra <mitch@riedstra.us>2021-01-24 10:31:53 -0500
committerMitch Riedstra <mitch@riedstra.us>2021-01-24 10:31:53 -0500
commita4554be33914fd7cd77eea3326a747078bbe4c50 (patch)
tree5e97506f384344662f9d3e55a6f83f88c76d6217
downloadcheckup-a4554be33914fd7cd77eea3326a747078bbe4c50.tar.gz
checkup-a4554be33914fd7cd77eea3326a747078bbe4c50.tar.xz
initial
-rw-r--r--.gitignore2
-rw-r--r--cert.go41
-rw-r--r--cmd/main/main.go113
-rw-r--r--go.mod8
-rw-r--r--go.sum6
-rw-r--r--http.go23
-rw-r--r--load.go1
-rw-r--r--sample-config.yml17
-rw-r--r--webhook.go52
9 files changed, 263 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..db9a660
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+cmd/main/main
+config.yml
diff --git a/cert.go b/cert.go
new file mode 100644
index 0000000..2bf3cde
--- /dev/null
+++ b/cert.go
@@ -0,0 +1,41 @@
+package checkup
+
+import (
+ "bytes"
+ "crypto/tls"
+ "fmt"
+ "time"
+)
+
+func CertExpiresSoon(hostname, port string, window time.Duration) (*tls.Conn, error) {
+ conn, err := tls.Dial("tcp", hostname+":"+port,
+ &tls.Config{ServerName: hostname})
+
+ if err != nil {
+ return conn, err
+ }
+
+ t := time.Now().Add(window)
+
+ cert := conn.ConnectionState().PeerCertificates[0]
+
+ if t.After(cert.NotAfter) {
+ return conn, fmt.Errorf("Cert expires soon: %s ( %d days )",
+ cert.NotAfter, (cert.NotAfter.Unix()-time.Now().Unix())/(60*60*24))
+ }
+
+ return conn, err
+}
+
+func CertInfo(conn *tls.Conn) string {
+ buf := &bytes.Buffer{}
+ cs := conn.ConnectionState()
+ certs := cs.PeerCertificates
+ for _, cert := range certs {
+ fmt.Fprintf(buf, "Permitted: %v\n", cert.DNSNames)
+ fmt.Fprintf(buf, "Permitted: %v\n", cert.PermittedDNSDomains)
+ fmt.Fprintf(buf, "Not before: %v\n", cert.NotBefore)
+ fmt.Fprintf(buf, "Not after: %v\n", cert.NotAfter)
+ }
+ return string(buf.Bytes())
+}
diff --git a/cmd/main/main.go b/cmd/main/main.go
new file mode 100644
index 0000000..aa37a7f
--- /dev/null
+++ b/cmd/main/main.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "gopkg.in/yaml.v3"
+ "os"
+ "time"
+
+ "riedstra.dev/go/checkup"
+)
+
+type Config struct {
+ RocketChatURL string `yaml:"RocketChatURL"`
+ DiscordURL string `yaml:"DiscordURL"`
+ DefaultCertPort string `yaml:"DefaultCertPort"`
+ CertWindow int `yaml:"CertWindow"`
+ CheckCerts map[string]*string `yaml:"CheckCerts"`
+ ExpectedStatusCode int `yaml:"ExpectedStatusCode"`
+ StatusChecks map[string]*int `yaml:"StatusChecks"`
+}
+
+func ReadConfig(fn string) (*Config, error) {
+ fh, err := os.Open(fn)
+ if err != nil {
+ return nil, err
+ }
+
+ dec := yaml.NewDecoder(fh)
+ conf := &Config{}
+ err = dec.Decode(conf)
+ return conf, err
+}
+
+func notify(conf *Config, b []byte) {
+ if len(b) >= 1 {
+
+ if conf.RocketChatURL != "" {
+ err := checkup.SendRocketChatAlert(conf.RocketChatURL, string(b))
+ if err != nil {
+ fmt.Fprintln(os.Stderr, string(b))
+ fmt.Fprintf(os.Stderr,
+ "When sending webhook for rocketchat: %s\n", err)
+ }
+
+ }
+ if conf.DiscordURL != "" {
+ err := checkup.SendDiscordAlert(conf.DiscordURL, string(b))
+ if err != nil {
+ fmt.Fprintln(os.Stderr, string(b))
+ fmt.Fprintf(os.Stderr,
+ "When sending webhook for discord: %s\n", err)
+ }
+ }
+ }
+}
+
+func checkCerts(conf *Config) {
+ msg := &bytes.Buffer{}
+ for host, port := range conf.CheckCerts {
+ if port == nil {
+ port = &conf.DefaultCertPort
+ }
+
+ conn, err := checkup.CertExpiresSoon(
+ host,
+ *port,
+ time.Duration(conf.CertWindow)*time.Hour*24)
+ if err != nil {
+ fmt.Fprintf(msg, "%s:%s --> %s\n", host, *port, err)
+ continue
+ }
+ conn.Close()
+ }
+
+ notify(conf, msg.Bytes())
+}
+
+func checkStatus(conf *Config) {
+ msg := &bytes.Buffer{}
+
+ for url, code := range conf.StatusChecks {
+ if code == nil {
+ code = &conf.ExpectedStatusCode
+ }
+ err := checkup.HttpStatusOK(url, *code)
+ if err != nil {
+ fmt.Fprintf(msg, "Checking: %s, %v\n",
+ url, err)
+ }
+ }
+
+ notify(conf, msg.Bytes())
+}
+
+func main() {
+ fl := flag.NewFlagSet("checkup", flag.ExitOnError)
+
+ confFn := fl.String("c", "config.yml", "Configuration file path")
+
+ _ = fl.Parse(os.Args[1:])
+
+ conf, err := ReadConfig(*confFn)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ checkCerts(conf)
+ checkStatus(conf)
+
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..2614840
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,8 @@
+module riedstra.dev/go/checkup
+
+go 1.15
+
+require (
+ golang.org/x/sys v0.0.0-20210123231150-1d476976d117
+ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..eeb2d96
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,6 @@
+golang.org/x/sys v0.0.0-20210123231150-1d476976d117 h1:M1sK0uTIn2x3HD5sySUPBg7ml5hmlQ/t7n7cIM6My9w=
+golang.org/x/sys v0.0.0-20210123231150-1d476976d117/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/http.go b/http.go
new file mode 100644
index 0000000..f3a910f
--- /dev/null
+++ b/http.go
@@ -0,0 +1,23 @@
+package checkup
+
+import (
+ "fmt"
+ "net/http"
+)
+
+func HttpStatusOK(url string, status int) error {
+ resp, err := http.Get(url)
+
+ if err != nil {
+ return err
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != status {
+ return fmt.Errorf("Bad status code, expected %d got: %d",
+ status, resp.StatusCode)
+ }
+
+ return nil
+}
diff --git a/load.go b/load.go
new file mode 100644
index 0000000..39e534d
--- /dev/null
+++ b/load.go
@@ -0,0 +1 @@
+package checkup
diff --git a/sample-config.yml b/sample-config.yml
new file mode 100644
index 0000000..dadbc0a
--- /dev/null
+++ b/sample-config.yml
@@ -0,0 +1,17 @@
+---
+RocketChatURL: changeme
+DiscordURL: changeme
+
+DefaultCertPort: '443'
+# In Days
+CertWindow: 15
+CheckCerts:
+ 'riedstra.dev':
+ 'git.riedstra.dev':
+ 'dispatch.riedstra.dev':
+
+ExpectedStatusCode: 200
+StatusChecks:
+ 'https://riedstra.dev':
+ 'https://git.riedstra.dev':
+ 'https://dispatch.riedstra.dev':
diff --git a/webhook.go b/webhook.go
new file mode 100644
index 0000000..897a249
--- /dev/null
+++ b/webhook.go
@@ -0,0 +1,52 @@
+package checkup
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+)
+
+func SendDiscordAlert(hookURL string, message string) error {
+ return SendWebhook(hookURL,
+ struct {
+ Content string `json:"content"`
+ }{
+ Content: message,
+ }, http.StatusNoContent)
+
+}
+
+func SendRocketChatAlert(hookURL string, message string) error {
+ return SendWebhook(hookURL,
+ struct {
+ Text string `json:"text"`
+ }{
+ Text: message,
+ }, http.StatusOK)
+}
+
+func SendWebhook(hookURL string, msg interface{}, code int) error {
+ buf := &bytes.Buffer{}
+ enc := json.NewEncoder(buf)
+
+ err := enc.Encode(msg)
+ if err != nil {
+ return err
+ }
+
+ resp, err := http.Post(hookURL, "application/json", buf)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != code {
+ bod, err := ioutil.ReadAll(resp.Body)
+ return fmt.Errorf("Bad status code: %d, expected %d : %s (Read errs: %s)",
+ resp.StatusCode, http.StatusOK, bod, err)
+ }
+
+ return nil
+}