From ba85a96ab83db6706f33b4ab4dfbd7aa37a0e10a Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Wed, 3 Oct 2018 01:17:25 -0700 Subject: [PATCH] http webhook notifier - checkins --- core/database.go | 4 +- core/services.go | 2 +- handlers/handlers.go | 4 +- handlers/routes.go | 1 - notifiers/webhook.go | 185 +++++++++++++++++++++++++++++++++ notifiers/webhook_test.go | 104 ++++++++++++++++++ source/tmpl/form_notifier.html | 24 +++-- source/tmpl/form_service.html | 2 +- source/tmpl/form_user.html | 2 +- 9 files changed, 311 insertions(+), 17 deletions(-) create mode 100644 notifiers/webhook.go create mode 100644 notifiers/webhook_test.go diff --git a/core/database.go b/core/database.go index 02603d75..f8406c87 100644 --- a/core/database.go +++ b/core/database.go @@ -291,7 +291,7 @@ func (db *DbConfig) DropDatabase() error { func (db *DbConfig) CreateDatabase() error { utils.Log(1, "Creating Database Tables...") err := DbSession.CreateTable(&types.Checkin{}) - //err = DbSession.CreateTable(&types.CheckinHit{}) + err = DbSession.CreateTable(&types.CheckinHit{}) err = DbSession.CreateTable(¬ifier.Notification{}) err = DbSession.Table("core").CreateTable(&types.Core{}) err = DbSession.CreateTable(&types.Failure{}) @@ -317,7 +317,7 @@ func (db *DbConfig) MigrateDatabase() error { if tx.Error != nil { return tx.Error } - tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Checkin{}, ¬ifier.Notification{}).Table("core").AutoMigrate(&types.Core{}) + tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Checkin{}, &types.CheckinHit{}, ¬ifier.Notification{}).Table("core").AutoMigrate(&types.Core{}) if tx.Error != nil { tx.Rollback() utils.Log(3, fmt.Sprintf("Statup Database could not be migrated: %v", tx.Error)) diff --git a/core/services.go b/core/services.go index d8b899b9..600ef537 100644 --- a/core/services.go +++ b/core/services.go @@ -51,7 +51,7 @@ func SelectService(id int64) *Service { func (s *Service) Checkins() []*types.Checkin { var hits []*types.Checkin - servicesDB().Where("service = ?", s.Id).Scan(&hits) + servicesDB().Where("service = ?", s.Id).Find(&hits) return hits } diff --git a/handlers/handlers.go b/handlers/handlers.go index 92a30832..a1ac89db 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -89,8 +89,8 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap "js": func(html interface{}) template.JS { return template.JS(utils.ToString(html)) }, - "safe": func(html interface{}) template.HTML { - return template.HTML(utils.ToString(html)) + "safe": func(html string) template.HTML { + return template.HTML(html) }, "Auth": func() bool { return IsAuthenticated(r) diff --git a/handlers/routes.go b/handlers/routes.go index 58aeb820..7f957d53 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -47,7 +47,6 @@ func Router() *mux.Router { r.PathPrefix("/statup.png").Handler(http.FileServer(source.TmplBox.HTTPBox())) } r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(source.JsBox.HTTPBox()))) - //r.Handle("/charts/{id}.js", http.HandlerFunc(renderServiceChartHandler)) r.Handle("/charts.js", http.HandlerFunc(renderServiceChartsHandler)) r.Handle("/setup", http.HandlerFunc(setupHandler)).Methods("GET") r.Handle("/setup", http.HandlerFunc(processSetupHandler)).Methods("POST") diff --git a/notifiers/webhook.go b/notifiers/webhook.go new file mode 100644 index 00000000..c34a79f8 --- /dev/null +++ b/notifiers/webhook.go @@ -0,0 +1,185 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package notifiers + +import ( + "bytes" + "fmt" + "github.com/hunterlong/statup/core/notifier" + "github.com/hunterlong/statup/types" + "github.com/hunterlong/statup/utils" + "io/ioutil" + "net/http" + "strings" + "time" +) + +const ( + WEBHOOK_METHOD = "webhook" +) + +type Webhook struct { + *notifier.Notification +} + +var webhook = &Webhook{¬ifier.Notification{ + Method: WEBHOOK_METHOD, + Title: "HTTP Webhook", + Description: "Send a custom HTTP request to a specific URL with your own body, headers, and parameters", + Author: "Hunter Long", + AuthorUrl: "https://github.com/hunterlong", + Delay: time.Duration(1 * time.Second), + Form: []notifier.NotificationForm{{ + Type: "text", + Title: "HTTP Endpoint", + Placeholder: "http://webhookurl.com/JW2MCP4SKQP", + SmallText: "Insert the URL for your HTTP Requests", + DbField: "Host", + Required: true, + }, { + Type: "text", + Title: "HTTP Method", + Placeholder: "POST", + SmallText: "Choose a HTTP method for example: GET, POST, DELETE, or PATCH", + DbField: "Var1", + Required: true, + }, { + Type: "textarea", + Title: "HTTP Body", + Placeholder: `{"service_id": "%s.Id", "service_name": "%s.Name"}`, + SmallText: "Optional HTTP body for a POST request. You can insert variables into your body request.
%service.Id, %service.Name
%failure.Issue", + DbField: "Var2", + }, { + Type: "text", + Title: "Content Type", + Placeholder: `application/json`, + SmallText: "Optional content type for example: application/json or text/plain", + DbField: "api_key", + }, { + Type: "text", + Title: "Header", + Placeholder: "Authorization=Token12345", + SmallText: "Optional Headers for request use format: KEY=Value,Key=Value", + DbField: "api_secret", + }, + }}} + +// DEFINE YOUR NOTIFICATION HERE. +func init() { + err := notifier.AddNotifier(webhook) + if err != nil { + panic(err) + } +} + +// Send will send a HTTP Post to the Webhook API. It accepts type: string +func (w *Webhook) Send(msg interface{}) error { + message := msg.(string) + _, err := w.run(message) + return err +} + +func (w *Webhook) Select() *notifier.Notification { + return w.Notification +} + +func replaceBodyText(body string, s *types.Service, f *types.Failure) string { + if s != nil { + body = strings.Replace(body, "%service.Name", s.Name, -1) + body = strings.Replace(body, "%service.Id", utils.ToString(s.Id), -1) + } + if f != nil { + body = strings.Replace(body, "%failure.Issue", f.Issue, -1) + } + return body +} + +func (w *Webhook) run(body string) (*http.Response, error) { + utils.Log(1, fmt.Sprintf("sending body: '%v' to %v as a %v request", body, w.Host, w.Var1)) + client := new(http.Client) + client.Timeout = time.Duration(10 * time.Second) + var buf *bytes.Buffer + buf = bytes.NewBuffer(nil) + if w.Var2 != "" { + buf = bytes.NewBuffer([]byte(w.Var2)) + } + req, err := http.NewRequest(w.Var1, w.Host, buf) + if err != nil { + return nil, err + } + if w.ApiSecret != "" { + splitArray := strings.Split(w.ApiSecret, ",") + for _, a := range splitArray { + split := strings.Split(a, "=") + req.Header.Add(split[0], split[1]) + } + } + if w.ApiSecret != "" { + req.Header.Add("Content-Type", w.ApiSecret) + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + return resp, err +} + +func (w *Webhook) OnTest() error { + service := &types.Service{ + Id: 1, + Name: "Interpol - All The Rage Back Home", + Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU", + ExpectedStatus: 200, + Interval: 30, + Type: "http", + Method: "GET", + Timeout: 20, + LastStatusCode: 404, + Expected: "test example", + LastResponse: "this is an example response", + CreatedAt: time.Now().Add(-24 * time.Hour), + } + body := replaceBodyText(w.Var2, service, nil) + resp, err := w.run(body) + if err != nil { + return err + } + defer resp.Body.Close() + content, err := ioutil.ReadAll(resp.Body) + utils.Log(1, fmt.Sprintf("webhook notifier received: '%v'", string(content))) + return err +} + +// OnFailure will trigger failing service +func (w *Webhook) OnFailure(s *types.Service, f *types.Failure) { + msg := replaceBodyText(w.Var2, s, f) + webhook.AddQueue(msg) + w.Online = false +} + +// OnSuccess will trigger successful service +func (w *Webhook) OnSuccess(s *types.Service) { + if !w.Online { + msg := replaceBodyText(w.Var2, s, nil) + webhook.AddQueue(msg) + } + w.Online = true +} + +// OnSave triggers when this notifier has been saved +func (w *Webhook) OnSave() error { + return nil +} diff --git a/notifiers/webhook_test.go b/notifiers/webhook_test.go new file mode 100644 index 00000000..ef808728 --- /dev/null +++ b/notifiers/webhook_test.go @@ -0,0 +1,104 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package notifiers + +import ( + "github.com/hunterlong/statup/core/notifier" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +var ( + WEBHOOK_URL = "https://jsonplaceholder.typicode.com/posts" + webhookMessage = `{ "title": "%service.Id", "body": "%service.Name", "userId": 19999 }` + fullMsg string +) + +func init() { + webhook.Host = WEBHOOK_URL + webhook.Var1 = "POST" +} + +func TestWebhookNotifier(t *testing.T) { + t.Parallel() + currentCount = CountNotifiers() + + t.Run("Load Webhook", func(t *testing.T) { + webhook.Host = WEBHOOK_URL + webhook.Delay = time.Duration(100 * time.Millisecond) + err := notifier.AddNotifier(webhook) + assert.Nil(t, err) + assert.Equal(t, "Hunter Long", webhook.Author) + assert.Equal(t, WEBHOOK_URL, webhook.Host) + }) + + t.Run("Load Webhook Notifier", func(t *testing.T) { + notifier.Load() + }) + + t.Run("Webhook Notifier Tester", func(t *testing.T) { + assert.True(t, webhook.CanTest()) + }) + + t.Run("Webhook Replace Body Text", func(t *testing.T) { + fullMsg = replaceBodyText(webhookMessage, TestService, TestFailure) + assert.Equal(t, 78, len(fullMsg)) + }) + + t.Run("Webhook Within Limits", func(t *testing.T) { + ok, err := webhook.WithinLimits() + assert.Nil(t, err) + assert.True(t, ok) + }) + + t.Run("Webhook OnFailure", func(t *testing.T) { + webhook.OnFailure(TestService, TestFailure) + assert.Len(t, webhook.Queue, 1) + }) + + t.Run("Webhook Check Offline", func(t *testing.T) { + assert.False(t, webhook.Online) + }) + + t.Run("Webhook OnSuccess", func(t *testing.T) { + webhook.OnSuccess(TestService) + assert.Len(t, webhook.Queue, 2) + }) + + t.Run("Webhook Check Back Online", func(t *testing.T) { + assert.True(t, webhook.Online) + }) + + t.Run("Webhook OnSuccess Again", func(t *testing.T) { + webhook.OnSuccess(TestService) + assert.Len(t, webhook.Queue, 2) + }) + + t.Run("Webhook Send", func(t *testing.T) { + err := webhook.Send(fullMsg) + assert.Nil(t, err) + assert.Len(t, webhook.Queue, 2) + }) + + t.Run("Webhook Queue", func(t *testing.T) { + go notifier.Queue(webhook) + time.Sleep(5 * time.Second) + assert.Equal(t, WEBHOOK_URL, webhook.Host) + assert.Equal(t, 1, len(webhook.Queue)) + }) + +} diff --git a/source/tmpl/form_notifier.html b/source/tmpl/form_notifier.html index 9daf56f0..c65e66da 100644 --- a/source/tmpl/form_notifier.html +++ b/source/tmpl/form_notifier.html @@ -2,13 +2,19 @@ {{$n := .Select}}
{{if $n.Title}}

{{$n.Title}}

{{end}} -{{if $n.Description}}

{{safe $n.Description}}

{{end}} +{{if $n.Description}}

{{$n.Description}}

{{end}} -{{range .Form}} +{{range $n.Form}}
- - - {{if .SmallText}}{{safe .SmallText}}{{end}} + + {{if eq .Type "textarea"}} + + {{else}} + + {{end}} + {{if .SmallText}} + {{safe .SmallText}} + {{end}}
{{end}} @@ -26,10 +32,10 @@
- - - - + + + +
diff --git a/source/tmpl/form_service.html b/source/tmpl/form_service.html index c796c305..61f05e41 100644 --- a/source/tmpl/form_service.html +++ b/source/tmpl/form_service.html @@ -1,5 +1,5 @@ {{define "form_service"}} - +
diff --git a/source/tmpl/form_user.html b/source/tmpl/form_user.html index c233614a..b14a20d2 100644 --- a/source/tmpl/form_user.html +++ b/source/tmpl/form_user.html @@ -1,5 +1,5 @@ {{define "form_user"}} - +