mTLS service check (http)

pull/598/head
hunterlong 2020-05-19 23:41:50 -07:00
parent 47e8039761
commit 4618d56c18
16 changed files with 129 additions and 17 deletions

View File

@ -322,5 +322,13 @@ postman: clean compile
newman run -e dev/postman_environment.json dev/postman.json
killall statping
certs:
openssl req -newkey rsa:2048 \
-new -nodes -x509 \
-days 3650 \
-out cert.pem \
-keyout key.pem \
-subj "/C=US/ST=California/L=Santa Monica/O=Statping/OU=Development/CN=localhost"
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
.SILENT: travis_s3_creds

View File

@ -285,7 +285,7 @@ func runOnce() error {
func checkGithubUpdates() (githubResponse, error) {
url := "https://api.github.com/repos/statping/statping/releases/latest"
contents, _, err := utils.HttpRequest(url, "GET", nil, nil, nil, time.Duration(2*time.Second), true)
contents, _, err := utils.HttpRequest(url, "GET", nil, nil, nil, time.Duration(2*time.Second), true, nil)
if err != nil {
return githubResponse{}, err
}

View File

@ -156,6 +156,41 @@
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Use TLS Certificate</label>
<div class="col-8 mt-1">
<span @click="service.use_tls = !!service.use_tls" class="switch float-left">
<input v-model="service.use_tls" type="checkbox" name="verify_ssl-option" class="switch" id="switch-use-tls" v-bind:checked="service.use_tls">
<label for="switch-use-tls" v-if="service.use_tls">Custom TLS Certificates for mTLS services</label>
<label for="switch-use-tls" v-if="!service.use_tls">Ignore TLS Certificates</label>
</span>
</div>
</div>
<div v-if="service.use_tls" class="form-group row">
<label for="service_tls_cert" class="col-sm-4 col-form-label">TLS Client Certificate</label>
<div class="col-sm-8">
<textarea v-model="service.tls_cert" name="tls_cert" class="form-control" id="service_tls_cert"></textarea>
<small class="form-text text-muted">Absolute path to TLS Client Certificate file or in PEM format</small>
</div>
</div>
<div v-if="service.use_tls" class="form-group row">
<label for="service_tls_cert_key" class="col-sm-4 col-form-label">TLS Client Key</label>
<div class="col-sm-8">
<textarea v-model="service.tls_cert_key" name="tls_cert_key" class="form-control" id="service_tls_cert_key"></textarea>
<small class="form-text text-muted">Absolute path to TLS Client Key file or in PEM format</small>
</div>
</div>
<div v-if="service.use_tls" class="form-group row">
<label for="service_tls_cert_chain" class="col-sm-4 col-form-label">Root CA</label>
<div class="col-sm-8">
<textarea v-model="service.tls_cert_root" name="tls_cert_key" class="form-control" id="service_tls_cert_chain"></textarea>
<small class="form-text text-muted">Absolute path to Root CA file or in PEM format (optional)</small>
</div>
</div>
</div>
</div>
@ -235,6 +270,10 @@
notify_all_changes: true,
notify_after: 2,
public: true,
use_tls: false,
tls_cert: "",
tls_cert_key: "",
tls_cert_root: "",
},
groups: [],
}
@ -247,12 +286,15 @@
watch: {
in_service () {
this.service = this.in_service
if (this.service.tls_cert) {
this.service.use_tls = true
}
}
},
async mounted () {
if (!this.$store.getters.groups) {
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
}
},
methods: {
@ -289,6 +331,7 @@
delete s.last_success
delete s.latency
delete s.online_24_hours
delete s.use_tls
s.check_interval = parseInt(s.check_interval)
s.timeout = parseInt(s.timeout)
s.port = parseInt(s.port)

View File

@ -143,7 +143,7 @@ func slackOAuth(r *http.Request) (*oAuth, error) {
// slackIdentity will query the Slack API to fetch the users ID, username, and email address.
func (a *oAuth) slackIdentity() (*oAuth, error) {
url := fmt.Sprintf("https://slack.com/api/users.identity?token=%s", a.Token)
out, resp, err := utils.HttpRequest(url, "GET", "application/x-www-form-urlencoded", nil, nil, 10*time.Second, true)
out, resp, err := utils.HttpRequest(url, "GET", "application/x-www-form-urlencoded", nil, nil, 10*time.Second, true, nil)
if err != nil {
return a, err
}

View File

@ -39,7 +39,7 @@ var Discorder = &discord{&notifications.Notification{
// Send will send a HTTP Post to the discord API. It accepts type: []byte
func (d *discord) sendRequest(msg string) error {
_, _, err := utils.HttpRequest(Discorder.GetValue("host"), "POST", "application/json", nil, strings.NewReader(msg), time.Duration(10*time.Second), true)
_, _, err := utils.HttpRequest(Discorder.GetValue("host"), "POST", "application/json", nil, strings.NewReader(msg), time.Duration(10*time.Second), true, nil)
return err
}
@ -63,7 +63,7 @@ func (d *discord) OnSuccess(s *services.Service) error {
func (d *discord) OnTest() (string, error) {
outError := errors.New("Incorrect discord URL, please confirm URL is correct")
message := `{"content": "Testing the discord notifier"}`
contents, _, err := utils.HttpRequest(Discorder.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(message)), time.Duration(10*time.Second), true)
contents, _, err := utils.HttpRequest(Discorder.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(message)), time.Duration(10*time.Second), true, nil)
if string(contents) == "" {
return "", nil
}

View File

@ -47,7 +47,7 @@ func (l *lineNotifier) sendMessage(message string) (string, error) {
v := url.Values{}
v.Set("message", message)
headers := []string{fmt.Sprintf("Authorization=Bearer %v", l.ApiSecret)}
content, _, err := utils.HttpRequest("https://notify-api.line.me/api/notify", "POST", "application/x-www-form-urlencoded", headers, strings.NewReader(v.Encode()), time.Duration(10*time.Second), true)
content, _, err := utils.HttpRequest("https://notify-api.line.me/api/notify", "POST", "application/x-www-form-urlencoded", headers, strings.NewReader(v.Encode()), time.Duration(10*time.Second), true, nil)
return string(content), err
}

View File

@ -142,7 +142,7 @@ func pushRequest(msg *pushArray) ([]byte, error) {
return nil, err
}
url := "https://push.statping.com/api/push"
body, _, err = utils.HttpRequest(url, "POST", "application/json", nil, bytes.NewBuffer(body), time.Duration(20*time.Second), true)
body, _, err = utils.HttpRequest(url, "POST", "application/json", nil, bytes.NewBuffer(body), time.Duration(20*time.Second), true, nil)
return body, err
}

View File

@ -59,7 +59,7 @@ func (t *pushover) sendMessage(message string) (string, error) {
v.Set("message", message)
rb := strings.NewReader(v.Encode())
content, _, err := utils.HttpRequest(pushoverUrl, "POST", "application/x-www-form-urlencoded", nil, rb, time.Duration(10*time.Second), true)
content, _, err := utils.HttpRequest(pushoverUrl, "POST", "application/x-www-form-urlencoded", nil, rb, time.Duration(10*time.Second), true, nil)
if err != nil {
return "", err
}

View File

@ -50,7 +50,7 @@ var slacker = &slack{&notifications.Notification{
// Send will send a HTTP Post to the slack webhooker API. It accepts type: string
func (s *slack) sendSlack(msg string) error {
_, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, strings.NewReader(msg), time.Duration(10*time.Second), true)
_, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, strings.NewReader(msg), time.Duration(10*time.Second), true, nil)
if err != nil {
return err
}
@ -60,7 +60,7 @@ func (s *slack) sendSlack(msg string) error {
func (s *slack) OnTest() (string, error) {
testMsg := ReplaceVars(failingTemplate, exampleService, exampleFailure)
contents, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(testMsg)), time.Duration(10*time.Second), true)
contents, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(testMsg)), time.Duration(10*time.Second), true, nil)
if err != nil {
return "", err
}

View File

@ -59,7 +59,7 @@ func (t *telegram) sendMessage(message string) (string, error) {
v.Set("text", message)
rb := *strings.NewReader(v.Encode())
contents, _, err := utils.HttpRequest(apiEndpoint, "GET", "application/x-www-form-urlencoded", nil, &rb, time.Duration(10*time.Second), true)
contents, _, err := utils.HttpRequest(apiEndpoint, "GET", "application/x-www-form-urlencoded", nil, &rb, time.Duration(10*time.Second), true, nil)
success, _ := telegramSuccess(contents)
if !success {

View File

@ -72,7 +72,7 @@ func (t *twilio) sendMessage(message string) (string, error) {
authHeader := utils.Base64(fmt.Sprintf("%s:%s", t.ApiKey, t.ApiSecret))
contents, _, err := utils.HttpRequest(twilioUrl, "POST", "application/x-www-form-urlencoded", []string{"Authorization=Basic " + authHeader}, rb, 10*time.Second, true)
contents, _, err := utils.HttpRequest(twilioUrl, "POST", "application/x-www-form-urlencoded", []string{"Authorization=Basic " + authHeader}, rb, 10*time.Second, true, nil)
success, _ := twilioSuccess(contents)
if !success {
errorOut := twilioError(contents)

View File

@ -2,13 +2,16 @@ package services
import (
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"github.com/statping/statping/types"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/hits"
"github.com/statping/statping/utils"
"io/ioutil"
"sort"
"strconv"
"time"
@ -16,6 +19,43 @@ import (
const limitedFailures = 25
func (s *Service) LoadTLSCert() (*tls.Config, error) {
if !s.TLSCert.Valid && !s.TLSCertKey.Valid {
return nil, nil
}
// load TLS cert and key from file path or PEM format
var cert tls.Certificate
var err error
tlsCertExtension := utils.FileExtension(s.TLSCert.String)
tlsCertKeyExtension := utils.FileExtension(s.TLSCertKey.String)
if tlsCertExtension == "" && tlsCertKeyExtension == "" {
cert, err = tls.X509KeyPair([]byte(s.TLSCert.String), []byte(s.TLSCertKey.String))
} else {
cert, err = tls.LoadX509KeyPair(s.TLSCert.String, s.TLSCertKey.String)
}
if err != nil {
return nil, errors.Wrap(err, "issue loading X509KeyPair")
}
// create Root CA pool or use Root CA provided
chainFile := s.TLSCert.String
if s.TLSCertRoot.String != "" {
chainFile = s.TLSCertRoot.String
}
caCert, err := ioutil.ReadFile(chainFile)
if err != nil {
return nil, errors.Wrap(err, "issue reading cert file: "+chainFile)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
return &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{cert},
}, nil
}
func (s *Service) Duration() time.Duration {
return time.Duration(s.Interval) * time.Second
}

View File

@ -258,8 +258,12 @@ func CheckHttp(s *Service, record bool) *Service {
contentType = "application/json"
}
content, res, err = utils.HttpRequest(s.Domain, s.Method, contentType,
headers, data, timeout, s.VerifySSL.Bool)
customTLS, err := s.LoadTLSCert()
if err != nil {
log.Errorln(err)
}
content, res, err = utils.HttpRequest(s.Domain, s.Method, contentType, headers, data, timeout, s.VerifySSL.Bool, customTLS)
if err != nil {
if record {
recordFailure(s, fmt.Sprintf("HTTP Error %v", err))

View File

@ -37,6 +37,9 @@ type Service struct {
VerifySSL null.NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin" yaml:"verify_ssl"`
Public null.NullBool `gorm:"default:true;column:public" json:"public" yaml:"public"`
GroupId int `gorm:"default:0;column:group_id" json:"group_id" yaml:"group_id"`
TLSCert null.NullString `gorm:"column:tls_cert" json:"tls_cert" scope:"user,admin" yaml:"tls_cert"`
TLSCertKey null.NullString `gorm:"column:tls_cert_key" json:"tls_cert_key" scope:"user,admin" yaml:"tls_cert_key"`
TLSCertRoot null.NullString `gorm:"column:tls_cert_root" json:"tls_cert_root" scope:"user,admin" yaml:"tls_cert_root"`
Headers null.NullString `gorm:"column:headers" json:"headers" scope:"user,admin" yaml:"headers"`
Permalink null.NullString `gorm:"column:permalink;unique;" json:"permalink" yaml:"permalink"`
Redirect null.NullBool `gorm:"default:false;column:redirect" json:"redirect" scope:"user,admin" yaml:"redirect"`

View File

@ -3,6 +3,7 @@ package utils
import (
"io/ioutil"
"os"
"strings"
)
// DeleteDirectory will attempt to delete a directory and all contents inside
@ -30,6 +31,15 @@ func FolderExists(folder string) bool {
return false
}
// FileExtension returns the file extension based on a file path
func FileExtension(path string) string {
s := strings.Split(path, ".")
if len(s) == 0 {
return ""
}
return s[len(s)-1]
}
// FileExists returns true if a file exists
// exists := FileExists("assets/css/base.css")
func FileExists(name string) bool {

View File

@ -197,7 +197,7 @@ func DurationReadable(d time.Duration) string {
// // body - The body or form data to send with HTTP request
// // timeout - Specific duration to timeout on. time.Duration(30 * time.Seconds)
// // You can use a HTTP Proxy if you HTTP_PROXY environment variable
func HttpRequest(url, method string, content interface{}, headers []string, body io.Reader, timeout time.Duration, verifySSL bool) ([]byte, *http.Response, error) {
func HttpRequest(url, method string, content interface{}, headers []string, body io.Reader, timeout time.Duration, verifySSL bool, customTLS *tls.Config) ([]byte, *http.Response, error) {
var err error
var req *http.Request
t1 := Now()
@ -247,6 +247,10 @@ func HttpRequest(url, method string, content interface{}, headers []string, body
return dialer.DialContext(ctx, network, addr)
},
}
if customTLS != nil {
transport.TLSClientConfig.RootCAs = customTLS.RootCAs
transport.TLSClientConfig.Certificates = customTLS.Certificates
}
client := &http.Client{
Transport: transport,
Timeout: timeout,