diff --git a/CHANGELOG.md b/CHANGELOG.md index e2e51326..9e8b9f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.90.41 (05-20-2020) +- Added TLS Client Cert/Key feature for HTTP and TCP/UDP services +- Replaced environment variable ADMIN_PASS to ADMIN_PASSWORD. + # 0.90.40 (05-18-2020) - Fixed issues with MySQL and Postgres taking forever to insert sample data (now run in bulk) - Removed API Authentication for /api/logout route diff --git a/Makefile b/Makefile index 30b9ac1f..d9f3c8be 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/cli.go b/cmd/cli.go index ffbed998..083b55dd 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -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 } diff --git a/dev/docker-compose.full.yml b/dev/docker-compose.full.yml index 6c326ecb..740c75b8 100644 --- a/dev/docker-compose.full.yml +++ b/dev/docker-compose.full.yml @@ -36,7 +36,7 @@ services: DOMAIN: http://localhost:8080 DESCRIPTION: This is a dev environment on SQLite! ADMIN_USER: admin - ADMIN_PASS: admin + ADMIN_PASSWORD: admin SAMPLE_DATA: 'false' ports: - 8080:8080 @@ -69,7 +69,7 @@ services: DOMAIN: http://localhost:8081 DESCRIPTION: This is a dev environment on MySQL! ADMIN_USER: admin - ADMIN_PASS: admin + ADMIN_PASSWORD: admin SAMPLE_DATA: 'false' ports: - 8081:8080 @@ -105,7 +105,7 @@ services: DOMAIN: http://localhost:8082 DESCRIPTION: This is a dev environment on Postgres! ADMIN_USER: admin - ADMIN_PASS: admin + ADMIN_PASSWORD: admin SAMPLE_DATA: 'false' ports: - 8082:8080 @@ -141,7 +141,7 @@ services: DOMAIN: http://localhost:8083 DESCRIPTION: This is a dev environment on MariaDB! ADMIN_USER: admin - ADMIN_PASS: admin + ADMIN_PASSWORD: admin SAMPLE_DATA: 'false' ports: - 8083:8080 diff --git a/dev/docker-compose.lite.yml b/dev/docker-compose.lite.yml index 65c47a68..75e48a54 100644 --- a/dev/docker-compose.lite.yml +++ b/dev/docker-compose.lite.yml @@ -24,7 +24,7 @@ services: DOMAIN: http://localhost:8585 DESCRIPTION: This is a dev environment with auto reloading! ADMIN_USER: admin - ADMIN_PASS: admin + ADMIN_PASSWORD: admin PORT: 8585 ports: - 8888:8888 diff --git a/dev/pwd-stack.yml b/dev/pwd-stack.yml index 602f5612..71c2a179 100644 --- a/dev/pwd-stack.yml +++ b/dev/pwd-stack.yml @@ -18,7 +18,7 @@ services: DOMAIN: http://localhost:8080 DESCRIPTION: This is a dev environment on SQLite! ADMIN_USER: admin - ADMIN_PASS: admin + ADMIN_PASSWORD: admin postgres: hostname: postgres diff --git a/frontend/src/forms/Service.vue b/frontend/src/forms/Service.vue index 1dfb926c..e3f2a267 100644 --- a/frontend/src/forms/Service.vue +++ b/frontend/src/forms/Service.vue @@ -156,6 +156,41 @@ +
+ +
+ + + + + +
+
+ +
+ +
+ + Absolute path to TLS Client Certificate file or in PEM format +
+
+ +
+ +
+ + Absolute path to TLS Client Key file or in PEM format +
+
+ +
+ +
+ + Absolute path to Root CA file or in PEM format (optional) +
+
+ @@ -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) diff --git a/handlers/oauth.go b/handlers/oauth.go index 09ceb8d0..ce9158bf 100644 --- a/handlers/oauth.go +++ b/handlers/oauth.go @@ -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 } diff --git a/notifiers/discord.go b/notifiers/discord.go index 988c7734..4d9ca75a 100644 --- a/notifiers/discord.go +++ b/notifiers/discord.go @@ -39,7 +39,7 @@ var Discorder = &discord{¬ifications.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 } diff --git a/notifiers/line_notify.go b/notifiers/line_notify.go index f6955d92..549449b9 100644 --- a/notifiers/line_notify.go +++ b/notifiers/line_notify.go @@ -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 } diff --git a/notifiers/mobile.go b/notifiers/mobile.go index edaadb9d..8ade1a8e 100644 --- a/notifiers/mobile.go +++ b/notifiers/mobile.go @@ -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 } diff --git a/notifiers/pushover.go b/notifiers/pushover.go index ae85eb13..6e93b8e1 100644 --- a/notifiers/pushover.go +++ b/notifiers/pushover.go @@ -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 } diff --git a/notifiers/slack.go b/notifiers/slack.go index 2eb776cb..8687b481 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -50,7 +50,7 @@ var slacker = &slack{¬ifications.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 } diff --git a/notifiers/telegram.go b/notifiers/telegram.go index 0be97e96..214150e3 100644 --- a/notifiers/telegram.go +++ b/notifiers/telegram.go @@ -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 { diff --git a/notifiers/twilio.go b/notifiers/twilio.go index e3ec3aaf..ef7bec7e 100644 --- a/notifiers/twilio.go +++ b/notifiers/twilio.go @@ -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) diff --git a/types/configs/load.go b/types/configs/load.go index 6bb6d79d..335cc01b 100644 --- a/types/configs/load.go +++ b/types/configs/load.go @@ -62,7 +62,7 @@ func LoadConfigFile(configFile string) (*DbConfig, error) { Domain: p.GetString("DOMAIN"), Email: p.GetString("EMAIL"), Username: p.GetString("ADMIN_USER"), - Password: p.GetString("ADMIN_PASS"), + Password: p.GetString("ADMIN_PASSWORD"), Location: utils.Directory, SqlFile: p.GetString("SQL_FILE"), } diff --git a/types/services/methods.go b/types/services/methods.go index a2e5e84a..471f58f9 100644 --- a/types/services/methods.go +++ b/types/services/methods.go @@ -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 } diff --git a/types/services/routine.go b/types/services/routine.go index c7956e76..43aa2c7d 100644 --- a/types/services/routine.go +++ b/types/services/routine.go @@ -2,6 +2,7 @@ package services import ( "bytes" + "crypto/tls" "fmt" "google.golang.org/grpc" "net" @@ -181,7 +182,18 @@ func CheckTcp(s *Service, record bool) *Service { domain = fmt.Sprintf("[%v]:%v", s.Domain, s.Port) } } - conn, err := net.DialTimeout(s.Type, domain, time.Duration(s.Timeout)*time.Second) + + tlsConfig, err := s.LoadTLSCert() + if err != nil { + log.Errorln(err) + } + + dialer := &net.Dialer{ + KeepAlive: time.Duration(s.Timeout) * time.Second, + Timeout: time.Duration(s.Timeout) * time.Second, + } + + conn, err := tls.DialWithDialer(dialer, s.Type, domain, tlsConfig) if err != nil { if record { recordFailure(s, fmt.Sprintf("Dial Error %v", err)) @@ -258,8 +270,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)) diff --git a/types/services/struct.go b/types/services/struct.go index bdd17086..88406f21 100644 --- a/types/services/struct.go +++ b/types/services/struct.go @@ -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"` diff --git a/utils/file.go b/utils/file.go index 07b77338..1019b6fd 100644 --- a/utils/file.go +++ b/utils/file.go @@ -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 { diff --git a/utils/utils.go b/utils/utils.go index 2085b8f2..2d6dce27 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -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, diff --git a/version.txt b/version.txt index 1d535ecf..fb82a5d2 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.90.40 +0.90.41