Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add certificate provider #4

Merged
merged 3 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}