Skip to content

Commit

Permalink
Add certificate provider (#4)
Browse files Browse the repository at this point in the history
* add certificate provider

* lint: suppress warn and simplify

* rename service file name
  • Loading branch information
umputun authored Jun 22, 2022
1 parent 8372300 commit 384634b
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 21 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,32 @@ This provider parses the nginx's response and returns the following:

All the values are parsed directly from the response except `change_handled` which is a difference between two subsequent `handled` values.

#### certificate provider

Checks if certificate expired or going to expire in the next 5 days.

Request examples:
- `foo:cert://example.com` - check if certificate is ok for https://example.com
- `bar:cert://umputun.com` - check if certificate is ok for https://umputun.com


- Response example:

```json
{
"cert": {
"name": "bar",
"status_code": 200,
"response_time": 44,
"body": {
"days_left": 73,,
"expire": "2022-09-03T16:31:52Z",
"status": "ok"
}
}
}
```

## API

- `GET /status` - returns server status in JSON format
Expand Down
11 changes: 6 additions & 5 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ func main() {
}

providers := external.Providers{
HTTP: &external.HTTPProvider{Client: http.Client{Timeout: opts.TimeOut}},
Mongo: &external.MongoProvider{TimeOut: opts.TimeOut},
Docker: &external.DockerProvider{TimeOut: opts.TimeOut},
Program: &external.ProgramProvider{TimeOut: opts.TimeOut, WithShell: true},
Nginx: &external.NginxProvider{TimeOut: opts.TimeOut},
HTTP: &external.HTTPProvider{Client: http.Client{Timeout: opts.TimeOut}},
Mongo: &external.MongoProvider{TimeOut: opts.TimeOut},
Docker: &external.DockerProvider{TimeOut: opts.TimeOut},
Program: &external.ProgramProvider{TimeOut: opts.TimeOut, WithShell: true},
Nginx: &external.NginxProvider{TimeOut: opts.TimeOut},
Certificate: &external.CertificateProvider{TimeOut: opts.TimeOut},
}

srv := server.Rest{
Expand Down
59 changes: 59 additions & 0 deletions app/status/external/certificate_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package external

import (
"crypto/tls"
"fmt"
"strings"
"time"

"github.com/pkg/errors"
)

// CertificateProvider is a status provider that check SSL certificate
type CertificateProvider struct {
TimeOut time.Duration
}

// Status url looks like: cert://example.com. It will try to get SSL certificate and check if it is valid and not going to expire soon
func (c *CertificateProvider) Status(req Request) (*Response, error) {
st := time.Now()
addr := strings.TrimPrefix(req.URL, "cert://") + ":443"
conn, err := tls.Dial("tcp", addr, &tls.Config{}) //nolint:gosec // we don't care about cert version
if err != nil {
return nil, errors.Wrapf(err, "failed to connect to %s", addr)
}
if err = conn.Handshake(); err != nil {
return nil, errors.Wrapf(err, "failed to handshake with %s", addr)
}
defer conn.Close() // nolint

certs := conn.ConnectionState().PeerCertificates
earlierCert := time.Date(2150, 1, 1, 0, 0, 0, 0, time.UTC)
for _, cert := range certs {
if cert.NotAfter.Before(earlierCert) {
earlierCert = cert.NotAfter
}
}

daysLeft := int(time.Until(earlierCert).Hours() / 24)
body := map[string]interface{}{
"expire": earlierCert.Format(time.RFC3339),
"days_left": daysLeft,
"host": strings.Replace(req.URL, "cert://", "https://", 1),
"status": "ok",
}
if daysLeft < 5 {
body["status"] = fmt.Sprintf("expiring soon, in %d days", daysLeft)
}
if earlierCert.Before(time.Now()) {
body["status"] = "expired"
}

result := Response{
Name: req.Name,
StatusCode: 200,
Body: body,
ResponseTime: time.Since(st).Milliseconds(),
}
return &result, nil
}
31 changes: 31 additions & 0 deletions app/status/external/certificate_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package external

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCertificateProvider_Status(t *testing.T) {
cp := CertificateProvider{TimeOut: time.Minute}
resp, err := cp.Status(Request{Name: "test", URL: "cert://umputun.com"})
require.NoError(t, err)
t.Logf("%+v", resp)
assert.Equal(t, "test", resp.Name)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "ok", resp.Body["status"])
assert.Equal(t, "https://umputun.com", resp.Body["host"])

exp, err := time.Parse(time.RFC3339, resp.Body[`expire`].(string))
require.NoError(t, err)
assert.True(t, exp.After(time.Now().Add(5*24*time.Hour)))
t.Logf("expire: %+v", exp)
}

func TestCertificateProvider_StatusFailed(t *testing.T) {
cp := CertificateProvider{TimeOut: time.Minute}
_, err := cp.Status(Request{Name: "test", URL: "cert://127.0.0.1"})
require.Error(t, err)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ type Service struct {

// Providers is a list of StatusProvider
type Providers struct {
HTTP StatusProvider
Mongo StatusProvider
Docker StatusProvider
Program StatusProvider
Nginx StatusProvider
HTTP StatusProvider
Mongo StatusProvider
Docker StatusProvider
Program StatusProvider
Nginx StatusProvider
Certificate StatusProvider
}

// StatusProvider is an interface for getting status from external services
Expand Down Expand Up @@ -101,6 +102,8 @@ func (s *Service) Status() []Response {
resp, err = s.providers.Program.Status(r)
case strings.HasPrefix(r.URL, "nginx://"):
resp, err = s.providers.Nginx.Status(r)
case strings.HasPrefix(r.URL, "cert://"):
resp, err = s.providers.Certificate.Status(r)

default:
log.Printf("[WARN] unsupported protocol for service, %s %s", r.Name, r.URL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,16 @@ func TestService_Status(t *testing.T) {
pn := &StatusProviderMock{StatusFunc: func(r Request) (*Response, error) {
return &Response{StatusCode: 203, Name: "nginx"}, nil
}}
pc := &StatusProviderMock{StatusFunc: func(r Request) (*Response, error) {
return &Response{StatusCode: 204, Name: "cert"}, nil
}}

s := NewService(Providers{ph, pm, pd, pp, pn}, 4,
s := NewService(Providers{ph, pm, pd, pp, pn, pc}, 4,
"s1:http://127.0.0.1/ping", "s2:docker:///var/blah", "s3:mongodb://127.0.0.1:27017",
"s4:program://ls?arg=1", "bad:bad")
"s4:program://ls?arg=1", "s5:cert://umputun.com", "bad:bad")

res := s.Status()
require.Equal(t, 5, len(res))
require.Equal(t, 6, len(res))
assert.Equal(t, 1, len(ph.StatusCalls()))
assert.Equal(t, Request{Name: "s1", URL: "http://127.0.0.1/ping"}, ph.StatusCalls()[0].Req)

Expand All @@ -75,15 +78,19 @@ func TestService_Status(t *testing.T) {
assert.Equal(t, "bad", res[0].Name)
assert.Equal(t, 500, res[0].StatusCode)

assert.Equal(t, "docker", res[1].Name)
assert.Equal(t, 202, res[1].StatusCode)
assert.Equal(t, "cert", res[1].Name)
assert.Equal(t, 204, res[1].StatusCode)

assert.Equal(t, "docker", res[2].Name)
assert.Equal(t, 202, res[2].StatusCode)

assert.Equal(t, "http", res[3].Name)
assert.Equal(t, 200, res[3].StatusCode)

assert.Equal(t, "http", res[2].Name)
assert.Equal(t, 200, res[2].StatusCode)
assert.Equal(t, "mongo", res[4].Name)
assert.Equal(t, 201, res[4].StatusCode)

assert.Equal(t, "mongo", res[3].Name)
assert.Equal(t, 201, res[3].StatusCode)
assert.Equal(t, "program", res[5].Name)
assert.Equal(t, 203, res[5].StatusCode)

assert.Equal(t, "program", res[4].Name)
assert.Equal(t, 203, res[4].StatusCode)
}

0 comments on commit 384634b

Please sign in to comment.