mirror of
https://github.com/Luzifer/promcertcheck.git
synced 2024-11-08 16:00:08 +00:00
Initial version
This commit is contained in:
commit
f109a5b41d
7 changed files with 568 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
certcheck
|
11
Makefile
Normal file
11
Makefile
Normal file
|
@ -0,0 +1,11 @@
|
|||
test:
|
||||
go generate
|
||||
go run *.go \
|
||||
--probe="https://www.cloudkeys.de/" \
|
||||
--probe="https://foo.hub.luzifer.io/" \
|
||||
--probe="https://registry.luzifer.io/" \
|
||||
--probe="https://blog.knut.me/" \
|
||||
--probe="https://gobuilder.me/" \
|
||||
--probe="https://pwd.luzifer.io/" \
|
||||
--probe="https://www.itpad.de/" \
|
||||
--probe="https://mondash.org/"
|
236
bindata.go
Normal file
236
bindata.go
Normal file
|
@ -0,0 +1,236 @@
|
|||
// Code generated by go-bindata.
|
||||
// sources:
|
||||
// display.html
|
||||
// DO NOT EDIT!
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"os"
|
||||
"time"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func bindataRead(data []byte, name string) ([]byte, error) {
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, gz)
|
||||
clErr := gz.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
if clErr != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type asset struct {
|
||||
bytes []byte
|
||||
info os.FileInfo
|
||||
}
|
||||
|
||||
type bindataFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
mode os.FileMode
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func (fi bindataFileInfo) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
func (fi bindataFileInfo) Size() int64 {
|
||||
return fi.size
|
||||
}
|
||||
func (fi bindataFileInfo) Mode() os.FileMode {
|
||||
return fi.mode
|
||||
}
|
||||
func (fi bindataFileInfo) ModTime() time.Time {
|
||||
return fi.modTime
|
||||
}
|
||||
func (fi bindataFileInfo) IsDir() bool {
|
||||
return false
|
||||
}
|
||||
func (fi bindataFileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _displayHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x9c\x56\x6d\x73\xdb\x36\x0c\xfe\xde\x5f\x81\xe8\xae\xd7\x24\x17\x49\x4b\xb2\xdc\xba\x4c\xf2\x5d\x96\xe5\xd6\x6c\x5d\xda\x25\xd9\xdb\xf5\xfa\x81\x16\x21\x8b\x0e\x45\xaa\x24\x65\xc7\xcb\xe5\xbf\x0f\x94\x64\x5b\xf1\x4b\xda\xee\x8b\x2d\x90\xc0\x03\xe0\x21\x40\x30\xd9\xf9\xe9\xdd\xf9\xed\x3f\xef\x2f\xa0\x70\xa5\x1c\xbc\x48\xfc\x1f\x48\xa6\x46\x69\x80\x2a\x18\xbc\x00\x48\x0a\x64\xdc\x7f\xd0\x67\x89\x8e\x41\x56\x30\x63\xd1\xa5\x41\xed\xf2\xf0\x75\xd0\xdf\x2a\x9c\xab\x42\xfc\x54\x8b\x49\x1a\xfc\x1d\xfe\x71\x16\x9e\xeb\xb2\x62\x4e\x0c\x25\x06\x90\x69\xe5\x50\x91\xdd\xe5\x45\x8a\x7c\x84\x4f\x2c\x15\x2b\x31\x0d\x26\x02\xa7\x95\x36\xae\xa7\x3c\x15\xdc\x15\x29\xc7\x89\xc8\x30\x6c\x84\x03\x10\x4a\x38\xc1\x64\x68\x33\x26\x31\x3d\x9c\x03\xed\x84\x21\xdc\x16\x08\x6c\xa8\x27\x08\xc7\xd0\x00\x3b\x36\xb2\xb0\x5f\xd6\xd6\xed\x13\x68\x89\x90\x0b\x63\x1d\x41\x80\x23\x55\x9f\xdb\x0f\xc0\xd4\x0c\x34\x89\xa6\x91\xe7\xbe\xc1\x1b\xb5\x36\xfb\x2c\x77\x68\xf6\xbd\x89\xc5\x16\x32\x0c\x3b\xaf\x4e\x38\x89\x83\x73\x34\x4e\xe4\x22\x63\x0e\x61\xc2\xa4\xe0\x94\xb5\x56\x60\xd0\xd6\xd2\xd9\x24\x6e\xb5\x5e\x2c\x03\xfd\x51\x6b\x67\x9d\x61\xd5\x12\x49\x0a\x75\x47\x16\x32\x0d\xac\x9b\x49\xb4\x05\x22\x31\x51\x18\xcc\xd3\x20\x8e\x4b\x76\x9f\x71\x15\x0d\xe7\x76\x5e\xa0\xe0\xe2\xc5\x42\x7c\x1c\x1d\x47\x27\x71\x66\xed\x72\x2d\x2a\x05\x69\x59\x1b\xf4\x5d\xbf\xb9\xfd\xed\xed\x09\xd8\x42\x94\x94\x39\x87\x6b\xb4\x95\x56\x3c\x1a\x5b\xc8\xb5\x81\xcb\x8b\xd7\x60\xeb\xca\x1f\x03\xe8\xbc\x53\x46\x89\x25\x51\x62\x1b\x83\x12\xb9\x60\xf0\xa9\x46\x23\xb0\x47\x84\x87\xfe\xeb\xec\xfa\xea\xf2\xea\xe7\xd3\x3e\x28\xd7\x68\xd5\x2b\x07\x53\x6d\xee\x40\xe4\x30\xd3\x35\xf8\x83\x6e\x0e\xa0\x62\x23\x22\x8c\xe0\x72\x21\xf1\x34\x8e\x9f\xc0\x7d\x20\x6d\xe9\x28\x22\xf8\xfe\x63\xbb\x4a\xeb\x36\x33\xa2\x72\x60\x4d\x96\x06\xbe\xde\x2c\x59\x69\x6b\xa3\x8e\x1f\x4f\x89\x2f\xe2\x13\xca\x6f\x42\x94\x7c\x17\x1d\x2d\xe5\x86\x8e\x31\xb1\x91\xc4\x2d\xcc\xd7\xa0\x9a\x36\xa5\xf8\x30\xfa\x96\x30\x3b\x69\x0b\x62\xb2\xf3\x01\x15\x17\xf9\xc7\x36\x9d\x24\x9e\x37\x51\x32\xd4\x7c\xd6\xe9\x70\x31\x81\x4c\x32\x6b\xd3\xc0\x97\x1c\x13\x0a\x4d\xb0\x88\xa8\xb7\x6b\xf4\x34\x80\xa6\x26\x28\x38\x14\xa3\xc2\x9d\x1e\x7d\x53\xdd\x7b\xa7\xa4\xd5\x1d\xed\xba\xc9\x62\x63\xd5\x97\x0c\x4b\x1e\x1e\x1e\x2d\x7c\xad\x6a\x54\x4c\xa1\x84\xe6\x37\xe4\x98\x33\x2a\xe1\x27\xba\x1b\xb4\x43\x9f\xa0\x50\xa3\x15\x3d\x80\xe7\x1b\xe3\x29\x68\x9b\xcd\xf3\x7e\x3c\x7f\x6b\x4e\x12\xc7\xe8\x8a\x99\x2b\xb6\x42\xf3\x1b\x52\x17\x88\x0a\xf9\x9a\x85\xb7\x31\xeb\x8b\x7e\xb9\x18\xbc\xd1\xd6\x51\xcb\x16\x03\x2f\x5c\x5a\x4b\x95\xbe\x10\xff\xf4\x39\x40\xad\x9c\x90\x8b\xb5\xeb\x26\x99\x46\x5c\x77\x13\x6f\xf2\xf3\xf0\xb2\xe9\xb5\x82\x1c\x1d\x78\x2e\xfc\x7d\xd4\x51\x02\x96\x3a\x0f\x39\xbc\x7c\xdc\x10\x1d\xd9\x51\x4f\x90\x66\x74\xe3\x98\xab\x2d\xa4\x29\x64\x4b\x86\xdf\xfd\xba\xd9\x6c\x6b\xb6\x84\x87\xf2\x39\xc4\x8b\xfb\x4a\xd0\xe6\x8d\xa6\x33\xdb\x0e\x3d\x67\x7e\xca\x8c\xda\x54\x04\x0b\x57\x74\x7b\x7e\x1e\x85\xd3\xf8\xe9\x75\xc2\x2a\x88\xef\xab\xed\x28\x7c\xf0\xf0\xd0\xf0\x0a\x8f\x8f\xc4\x3d\xdf\x84\xb2\xd0\xf3\x69\xf7\x0a\x34\x6a\xcf\x3a\xa2\xa9\x55\x6a\x75\x45\x23\xe9\xff\x80\x5c\x69\x77\xe6\xc7\xc5\x97\xda\xb6\xbc\xd3\x9f\x21\xea\x76\xf7\x9e\x31\xdb\x5a\x4b\xc4\x89\x2f\xa7\x35\x52\xc8\xc0\x77\xc1\xe0\xeb\xdb\x2c\xa7\x09\xb2\xe1\x0c\x12\xd6\xcd\xa3\xf9\x1d\x39\x12\xae\xa8\x87\xcd\xfd\xf8\xb6\xfe\x57\xe4\x68\xe2\xca\xe8\xd2\x57\x50\x56\x60\x76\x17\x0c\xde\x93\x78\x3e\x17\x81\x72\x9e\xa0\xb1\xfe\x0a\xf0\x79\xb2\xcf\x84\xb6\xb2\xb0\x72\xdf\x2d\x37\xfb\x1b\xcd\x20\x1a\xff\x4e\x07\x39\x83\x5d\x85\x19\x5a\xcb\xe8\xd3\x13\xb4\x18\xba\xaf\x2c\xfc\xc2\x26\xec\xa6\xbd\xf5\x2b\x59\x8f\x84\xb2\x7b\xcb\xe1\xd3\x1f\x07\x71\xcc\xc6\xec\x3e\x1a\x69\x3d\x92\xc8\x2a\x61\x9b\x6c\xfd\x5a\x2c\xc5\xd0\xc6\x63\x3f\x09\x67\x34\x16\x0e\x0f\xa3\xe3\x4e\xda\x3a\x16\x28\xb4\x4b\x95\xc9\x9a\xd3\x33\x45\x4a\xff\xba\xa8\x68\xf0\xf1\x79\x08\xb0\x3b\x44\xa9\xa7\x7b\x07\x40\xd1\x8a\x4e\x51\x50\xc5\x4f\x04\xaf\x99\x6c\xa6\x24\x0d\x61\x0b\x0a\x91\x93\xd9\x96\x80\xbf\xf4\xad\x30\x5e\x7d\x2a\xac\x86\x9c\xc4\xed\xbc\x4a\xe2\xf6\x7d\xf8\x5f\x00\x00\x00\xff\xff\x21\xaf\x29\x78\x30\x0a\x00\x00")
|
||||
|
||||
func displayHtmlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
_displayHtml,
|
||||
"display.html",
|
||||
)
|
||||
}
|
||||
|
||||
func displayHtml() (*asset, error) {
|
||||
bytes, err := displayHtmlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "display.html", size: 2608, mode: os.FileMode(420), modTime: time.Unix(1441373785, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Asset loads and returns the asset for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func Asset(name string) ([]byte, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.bytes, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
|
||||
// MustAsset is like Asset but panics when Asset would return an error.
|
||||
// It simplifies safe initialization of global variables.
|
||||
func MustAsset(name string) []byte {
|
||||
a, err := Asset(name)
|
||||
if (err != nil) {
|
||||
panic("asset: Asset(" + name + "): " + err.Error())
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// AssetInfo loads and returns the asset info for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func AssetInfo(name string) (os.FileInfo, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.info, nil
|
||||
}
|
||||
return nil, fmt.Errorf("AssetInfo %s not found", name)
|
||||
}
|
||||
|
||||
// AssetNames returns the names of the assets.
|
||||
func AssetNames() []string {
|
||||
names := make([]string, 0, len(_bindata))
|
||||
for name := range _bindata {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||
var _bindata = map[string]func() (*asset, error){
|
||||
"display.html": displayHtml,
|
||||
}
|
||||
|
||||
// AssetDir returns the file names below a certain
|
||||
// directory embedded in the file by go-bindata.
|
||||
// For example if you run go-bindata on data/... and data contains the
|
||||
// following hierarchy:
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
// then AssetDir("data") would return []string{"foo.txt", "img"}
|
||||
// AssetDir("data/img") would return []string{"a.png", "b.png"}
|
||||
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
|
||||
// AssetDir("") will return []string{"data"}.
|
||||
func AssetDir(name string) ([]string, error) {
|
||||
node := _bintree
|
||||
if len(name) != 0 {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
pathList := strings.Split(cannonicalName, "/")
|
||||
for _, p := range pathList {
|
||||
node = node.Children[p]
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.Func != nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
rv := make([]string, 0, len(node.Children))
|
||||
for childName := range node.Children {
|
||||
rv = append(rv, childName)
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
type bintree struct {
|
||||
Func func() (*asset, error)
|
||||
Children map[string]*bintree
|
||||
}
|
||||
var _bintree = &bintree{nil, map[string]*bintree{
|
||||
"display.html": &bintree{displayHtml, map[string]*bintree{
|
||||
}},
|
||||
}}
|
||||
|
||||
// RestoreAsset restores an asset under the given directory
|
||||
func RestoreAsset(dir, name string) error {
|
||||
data, err := Asset(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := AssetInfo(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreAssets restores an asset under the given directory recursively
|
||||
func RestoreAssets(dir, name string) error {
|
||||
children, err := AssetDir(name)
|
||||
// File
|
||||
if err != nil {
|
||||
return RestoreAsset(dir, name)
|
||||
}
|
||||
// Dir
|
||||
for _, child := range children {
|
||||
err = RestoreAssets(dir, filepath.Join(name, child))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _filePath(dir, name string) string {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
|
||||
}
|
||||
|
86
cert.go
Normal file
86
cert.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type probeResult uint
|
||||
|
||||
const (
|
||||
certificateOK probeResult = iota
|
||||
certificateNotFound
|
||||
certificateExpiresSoon
|
||||
certificateInvalid
|
||||
generalFailure
|
||||
)
|
||||
|
||||
func (p probeResult) String() string {
|
||||
switch p {
|
||||
case certificateOK:
|
||||
return "Certificate OK"
|
||||
case certificateExpiresSoon:
|
||||
return fmt.Sprintf("Certificate expires within %s", config.ExpireWarning)
|
||||
case certificateInvalid:
|
||||
return "Certificate invalid / intermediate certificates not present"
|
||||
case certificateNotFound:
|
||||
return "Did not find a certificate valid for this domain"
|
||||
case generalFailure:
|
||||
return "Something went wrong in the request"
|
||||
}
|
||||
|
||||
return "" // This does not happen.
|
||||
}
|
||||
|
||||
func checkCertificate(probeURL *url.URL) (probeResult, *x509.Certificate) {
|
||||
req, _ := http.NewRequest("HEAD", probeURL.String(), nil)
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("Mozilla/5.0 (compatible; PromCertcheck/%s; +https://github.com/Luzifer/promcertcheck)", version))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
switch err.(type) {
|
||||
case nil, redirectFoundError:
|
||||
default:
|
||||
if !strings.Contains(err.Error(), "Found a redirect.") {
|
||||
return generalFailure, nil
|
||||
}
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
intermediatePool := x509.NewCertPool()
|
||||
var verifyCert *x509.Certificate
|
||||
|
||||
for _, cert := range resp.TLS.PeerCertificates {
|
||||
wildHost := "*" + probeURL.Host[strings.Index(probeURL.Host, "."):]
|
||||
if !inSlice(cert.DNSNames, probeURL.Host) && !inSlice(cert.DNSNames, wildHost) {
|
||||
intermediatePool.AddCert(cert)
|
||||
continue
|
||||
}
|
||||
|
||||
verifyCert = cert
|
||||
}
|
||||
|
||||
if verifyCert == nil {
|
||||
return certificateNotFound, nil
|
||||
}
|
||||
|
||||
verificationResult := false
|
||||
if _, err := verifyCert.Verify(x509.VerifyOptions{
|
||||
Intermediates: intermediatePool,
|
||||
}); err == nil {
|
||||
verificationResult = true
|
||||
}
|
||||
|
||||
if !verificationResult {
|
||||
return certificateInvalid, verifyCert
|
||||
}
|
||||
|
||||
if verifyCert.NotAfter.Sub(time.Now()) < config.expireWarning {
|
||||
return certificateExpiresSoon, verifyCert
|
||||
}
|
||||
|
||||
return certificateOK, verifyCert
|
||||
}
|
66
display.html
Normal file
66
display.html
Normal file
|
@ -0,0 +1,66 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
||||
<title>Certificate validation results</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
||||
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row" style="height:20px"></div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Certificate validation results
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Host</th><th>Issuer</th><th>Valid until</th><th>Result</th>
|
||||
</tr>
|
||||
{% for host, res in results sorted %}
|
||||
{% if res.Status == certificateOK %}
|
||||
<tr>
|
||||
{% elif res.Status == certificateExpiresSoon %}
|
||||
<tr class="warning">
|
||||
{% else %}
|
||||
<tr class="danger">
|
||||
{% endif %}
|
||||
<td>{{ host }}</td>
|
||||
<td>{{ res.Certificate.Issuer.CommonName }}</td>
|
||||
<td>{{ res.Certificate.NotAfter }}</td>
|
||||
<td>{{ res.Status.String() }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<a href="https://github.com/Luzifer/promcertcheck">PromCertcheck {{ version }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
26
http.go
Normal file
26
http.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
//go:generate go-bindata display.html
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/flosch/pongo2"
|
||||
)
|
||||
|
||||
func httpHandler(res http.ResponseWriter, r *http.Request) {
|
||||
tplsrc, _ := Asset("display.html")
|
||||
|
||||
template, err := pongo2.FromString(string(tplsrc))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
template.ExecuteWriter(pongo2.Context{
|
||||
"results": probeMonitors,
|
||||
"certificateOK": certificateOK,
|
||||
"certificateExpiresSoon": certificateExpiresSoon,
|
||||
"version": version,
|
||||
}, res)
|
||||
}
|
142
main.go
Normal file
142
main.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/rconfig"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
var (
|
||||
config = struct {
|
||||
Debug bool `flag:"debug" default:"false" description:"Output debugging data"`
|
||||
ExpireWarning string `flag:"expire-warning" default:"744h" description:"When to warn about a soon expiring certificate"`
|
||||
Probes []string `flag:"probe" default:"" description:"URLs to check for certificate issues"`
|
||||
|
||||
expireWarning time.Duration
|
||||
}{}
|
||||
version = "dev"
|
||||
probeMonitors = map[string]*probeMonitor{}
|
||||
)
|
||||
|
||||
type probeMonitor struct {
|
||||
IsValid prometheus.Gauge
|
||||
Expires prometheus.Gauge
|
||||
Status probeResult
|
||||
Certificate *x509.Certificate
|
||||
}
|
||||
|
||||
type redirectFoundError struct{}
|
||||
|
||||
func (r redirectFoundError) Error() string {
|
||||
return "Found a redirect."
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
rconfig.Parse(&config)
|
||||
config.expireWarning, err = time.ParseDuration(config.ExpireWarning)
|
||||
if err != nil {
|
||||
log.Fatalf("You need to specify a valid expire-warning: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return redirectFoundError{}
|
||||
}
|
||||
|
||||
registerProbes()
|
||||
refreshCertificateStatus()
|
||||
|
||||
c := cron.New()
|
||||
c.AddFunc("0 0 * * * *", refreshCertificateStatus)
|
||||
c.Start()
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/metrics", prometheus.Handler())
|
||||
r.HandleFunc("/", httpHandler)
|
||||
http.ListenAndServe(":3000", r)
|
||||
}
|
||||
|
||||
func registerProbes() {
|
||||
for _, probe := range config.Probes {
|
||||
probeURL, _ := url.Parse(probe)
|
||||
|
||||
monitors := &probeMonitor{}
|
||||
monitors.Expires = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "certcheck_expires",
|
||||
Help: "Expiration date in unix timestamp (UTC)",
|
||||
ConstLabels: prometheus.Labels{
|
||||
"host": probeURL.Host,
|
||||
},
|
||||
})
|
||||
monitors.IsValid = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "certcheck_valid",
|
||||
Help: "Validity of the certificate (0/1)",
|
||||
ConstLabels: prometheus.Labels{
|
||||
"host": probeURL.Host,
|
||||
},
|
||||
})
|
||||
|
||||
prometheus.MustRegister(monitors.Expires)
|
||||
prometheus.MustRegister(monitors.IsValid)
|
||||
|
||||
probeMonitors[probeURL.Host] = monitors
|
||||
}
|
||||
}
|
||||
|
||||
func refreshCertificateStatus() {
|
||||
for _, probe := range config.Probes {
|
||||
probeURL, _ := url.Parse(probe)
|
||||
verificationResult, verifyCert := checkCertificate(probeURL)
|
||||
|
||||
if config.Debug {
|
||||
fmt.Printf("---\nProbe: %s\nResult: %s\n",
|
||||
probeURL.Host,
|
||||
verificationResult,
|
||||
)
|
||||
if verifyCert != nil {
|
||||
fmt.Printf("Version: %d\nSerial: %d\nSubject: %s\nExpires: %s\nIssuer: %s\nAlt Names: %s\n",
|
||||
verifyCert.Version,
|
||||
verifyCert.SerialNumber,
|
||||
verifyCert.Subject.CommonName,
|
||||
verifyCert.NotAfter,
|
||||
verifyCert.Issuer.CommonName,
|
||||
strings.Join(verifyCert.DNSNames, ", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
probeMonitors[probeURL.Host].Expires.Set(float64(verifyCert.NotAfter.UTC().Unix()))
|
||||
switch verificationResult {
|
||||
case certificateExpiresSoon, certificateOK:
|
||||
probeMonitors[probeURL.Host].IsValid.Set(1)
|
||||
case certificateInvalid, certificateNotFound:
|
||||
probeMonitors[probeURL.Host].IsValid.Set(0)
|
||||
default:
|
||||
probeMonitors[probeURL.Host].IsValid.Set(0)
|
||||
}
|
||||
probeMonitors[probeURL.Host].Status = verificationResult
|
||||
probeMonitors[probeURL.Host].Certificate = verifyCert
|
||||
}
|
||||
}
|
||||
|
||||
func inSlice(slice []string, needle string) bool {
|
||||
for _, i := range slice {
|
||||
if i == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
Loading…
Reference in a new issue