From a65fe73a02a9b89ee470e7894cb55eed7347a9d1 Mon Sep 17 00:00:00 2001 From: thomae <4493560+thomae@users.noreply.github.com> Date: Fri, 26 Feb 2021 08:05:48 +0100 Subject: [PATCH 1/6] Add generic Webhook (HTTP POST callback) as notifier --- README.md | 1 + cmd/root.go | 22 ++++++++ nanny.toml | 7 +++ pkg/notifier/webhook.go | 122 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 pkg/notifier/webhook.go diff --git a/README.md b/README.md index 2c3ce09..603a0bc 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Nanny can notify you via these channels (for now): * sentry * sms (twilio) * slack (webhook) +* generic webhook (HTTP POST callback) * xmpp (jabber) ## Example diff --git a/cmd/root.go b/cmd/root.go index bd20a8b..7817cba 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -31,6 +31,7 @@ type Config struct { Sentry Sentry Twilio Twilio Slack Slack + Webhook Webhook Xmpp Xmpp } @@ -75,6 +76,15 @@ type Slack struct { WebhookURL string } +// Webhook config. +type Webhook struct { + Enabled bool + WebhookURL string + WebhookURLAllClear string `mapstructure:"webhookURL_all_clear"` + WebhookSecret string + AllowInsecureTLS bool +} + // Xmpp config. type Xmpp struct { Enabled bool @@ -233,6 +243,18 @@ func makeNotifiers() (map[string]notifier.Notifier, error) { } notifiers["slack"] = slackNotifier } + if config.Webhook.Enabled { + webhookNotifier, err := notifier.NewWebhook( + config.Webhook.WebhookURL, + config.Webhook.WebhookURLAllClear, + config.Webhook.WebhookSecret, + config.Webhook.AllowInsecureTLS, + ) + if err != nil { + return nil, errors.Wrap(err, "unable to create webhook notifier") + } + notifiers["webhook"] = webhookNotifier + } if config.Xmpp.Enabled { xmppNotifier, err := notifier.NewXmpp( config.Xmpp.To, diff --git a/nanny.toml b/nanny.toml index c073b1d..521c5bf 100644 --- a/nanny.toml +++ b/nanny.toml @@ -35,6 +35,13 @@ to="" enabled=false webhookURL="" +[webhook] +enabled=false +webhookURL="https://webhook.somewhere.com/webhook" +webhookURL_all_clear="https://webhook.somewhere.com/webhook" +webhookSecret="" +allowInsecureTLS=false + [xmpp] enabled=false to=["someone@xmpp.somewhere.com"] diff --git a/pkg/notifier/webhook.go b/pkg/notifier/webhook.go new file mode 100644 index 0000000..95e0f65 --- /dev/null +++ b/pkg/notifier/webhook.go @@ -0,0 +1,122 @@ +package notifier + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "crypto/tls" + "encoding/hex" + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/pkg/errors" +) + +type webhookNotifier struct { + WebhookURL string + WebhookURLAllClear string + WebhookSecret string + AllowInsecureTLS bool +} + +func ComputeHmacSha256(secret string, payload []byte) string { + key := []byte(secret) + h := hmac.New(sha256.New, key) + h.Write([]byte(payload)) + return hex.EncodeToString(h.Sum(nil)) +} + +// NewWebhook creates a new webhook notifier from the supplied configuration. +func NewWebhook(WebhookURL string, + WebhookURLAllClear string, + WebhookSecret string, + AllowInsecureTLS bool) (Notifier, error) { + + if WebhookURL == "" { + return nil, errors.New("Unable to initialize webhook: webhookURL is empty") + } + if WebhookURLAllClear == "" { + return nil, errors.New("Unable to initialize webhook: webhookURL_all_clear is empty") + } + + return &webhookNotifier{ + WebhookURL, + WebhookURLAllClear, + WebhookSecret, + AllowInsecureTLS, + }, nil +} + +// Notify implements the Notifier interface for webhook. +func (w *webhookNotifier) Notify(msg Message) error { + postBody, _ := json.Marshal(map[string]interface{}{ + "message": msg.Format(), + "meta": msg.Meta, + }) + request, err := http.NewRequest("POST", w.WebhookURL, bytes.NewBuffer(postBody)) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-Program", msg.Program) + + if w.WebhookSecret != "" { + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + payload := append([]byte(timestamp), postBody...) + signature := ComputeHmacSha256(w.WebhookSecret, payload) + + request.Header.Set("X-Timestamp", timestamp) + request.Header.Set("X-HMAC-SHA256", signature) + } + + client := &http.Client{Timeout: time.Second * 10} + if w.AllowInsecureTLS { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client = &http.Client{Transport: transport, Timeout: time.Second * 10} + } + _, err = client.Do(request) + if err != nil { + return errors.Wrap(err, "unable to notify via webhook") + } + + return nil +} + +// NotifyAllClear implements the Notifier interface for webhook. +func (w *webhookNotifier) NotifyAllClear(msg Message) error { + postBody, _ := json.Marshal(map[string]interface{}{ + "message": msg.FormatAllClear(), + "meta": msg.Meta, + }) + request, err := http.NewRequest("POST", w.WebhookURLAllClear, bytes.NewBuffer(postBody)) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-Program", msg.Program) + + if w.WebhookSecret != "" { + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + payload := append([]byte(timestamp), postBody...) + signature := ComputeHmacSha256(w.WebhookSecret, payload) + + request.Header.Set("X-Timestamp", timestamp) + request.Header.Set("X-HMAC-SHA256", signature) + } + + client := &http.Client{Timeout: time.Second * 10} + if w.AllowInsecureTLS { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client = &http.Client{Transport: transport, Timeout: time.Second * 10} + } + _, err = client.Do(request) + if err != nil { + return errors.Wrap(err, "unable to notify via webhook") + } + + return nil +} + +func (w *webhookNotifier) String() string { + return "webhook" +} From 840fffdc452e56a02e2593b5e7b4ac2346382c4c Mon Sep 17 00:00:00 2001 From: thomae <4493560+thomae@users.noreply.github.com> Date: Fri, 26 Feb 2021 08:06:07 +0100 Subject: [PATCH 2/6] Add webhook receiver as an example --- receiver_examples/webhook_receiver_example.go | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 receiver_examples/webhook_receiver_example.go diff --git a/receiver_examples/webhook_receiver_example.go b/receiver_examples/webhook_receiver_example.go new file mode 100644 index 0000000..4500b17 --- /dev/null +++ b/receiver_examples/webhook_receiver_example.go @@ -0,0 +1,88 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "io/ioutil" + "log" + "math" + "net/http" + "strconv" + "time" +) + +func ComputeHmacSha256(secret string, message []byte) string { + key := []byte(secret) + h := hmac.New(sha256.New, key) + h.Write(message) + return hex.EncodeToString(h.Sum(nil)) +} + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + webhookSecret := "" + + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", 405) + return + } + + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + http.Error(w, "Unsupported Media Type", 415) + return + } + + sendingProgram := r.Header.Get("X-Program") + log.Println("X-Program: " + sendingProgram) + + currentTimestamp := strconv.FormatInt(time.Now().Unix(), 10) + requestTimestamp := r.Header.Get("X-Timestamp") + hmacSHA256 := r.Header.Get("X-HMAC-SHA256") + + body, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + http.Error(w, "Internal Server Error", 500) + return + } + + if requestTimestamp != "" && hmacSHA256 != "" { + payload := append([]byte(requestTimestamp), body...) + signature := ComputeHmacSha256(webhookSecret, payload) + log.Println("X-Timestamp: " + requestTimestamp) + log.Println("Current timestamp: " + currentTimestamp) + log.Println("X-HMAC-SHA256: " + hmacSHA256) + log.Println("Calculated signature: " + signature) + if hmacSHA256 == signature { + log.Println("Signature is correct") + } else { + log.Println("Signature is not correct") + http.Error(w, "Unauthorized", 401) + return + } + currentTimestampInt, err := strconv.Atoi(currentTimestamp) + if err != nil { + http.Error(w, "Internal Server Error", 500) + return + } + requestTimestampInt, err := strconv.Atoi(requestTimestamp) + if err != nil { + http.Error(w, "Bad Request", 400) + return + } + if int(math.Abs(float64(currentTimestampInt-requestTimestampInt))) > 10 { + log.Println("Timestamp is older than 10 seconds") + http.Error(w, "Bad Request", 400) + return + } else { + log.Println("Timestamp is not older than 10 seconds") + } + } + log.Println(string(body)) + log.Println("--------------------------------------------------------------------------------------") + }) + + http.ListenAndServe(":8081", nil) +} From 1d56072553cc45eb6b336e3f5dc73835697e4f50 Mon Sep 17 00:00:00 2001 From: thomae <4493560+thomae@users.noreply.github.com> Date: Fri, 26 Feb 2021 09:40:15 +0100 Subject: [PATCH 3/6] Change configuration names to snake_case, add configurable request timeout, reuse TCP connections --- cmd/root.go | 10 ++++++---- nanny.toml | 9 +++++---- pkg/notifier/webhook.go | 31 +++++++++++++------------------ 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 7817cba..8672e81 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -79,10 +79,11 @@ type Slack struct { // Webhook config. type Webhook struct { Enabled bool - WebhookURL string - WebhookURLAllClear string `mapstructure:"webhookURL_all_clear"` - WebhookSecret string - AllowInsecureTLS bool + WebhookURL string `mapstructure:"webhook_url"` + WebhookURLAllClear string `mapstructure:"webhook_url_all_clear"` + WebhookSecret string `mapstructure:"webhook_secret"` + RequestTimeout time.Duration `mapstructure:"request_timeout"` + AllowInsecureTLS bool `mapstructure:"allow_insecure_tls"` } // Xmpp config. @@ -248,6 +249,7 @@ func makeNotifiers() (map[string]notifier.Notifier, error) { config.Webhook.WebhookURL, config.Webhook.WebhookURLAllClear, config.Webhook.WebhookSecret, + config.Webhook.RequestTimeout, config.Webhook.AllowInsecureTLS, ) if err != nil { diff --git a/nanny.toml b/nanny.toml index 521c5bf..96b4185 100644 --- a/nanny.toml +++ b/nanny.toml @@ -37,10 +37,11 @@ webhookURL="" [webhook] enabled=false -webhookURL="https://webhook.somewhere.com/webhook" -webhookURL_all_clear="https://webhook.somewhere.com/webhook" -webhookSecret="" -allowInsecureTLS=false +webhook_url="https://webhook.somewhere.com/webhook" +webhook_url_all_clear="https://webhook.somewhere.com/webhook" +webhook_secret="" +request_timeout=10 +allow_insecure_tls=false [xmpp] enabled=false diff --git a/pkg/notifier/webhook.go b/pkg/notifier/webhook.go index 95e0f65..c623601 100644 --- a/pkg/notifier/webhook.go +++ b/pkg/notifier/webhook.go @@ -18,7 +18,7 @@ type webhookNotifier struct { WebhookURL string WebhookURLAllClear string WebhookSecret string - AllowInsecureTLS bool + httpClient *http.Client } func ComputeHmacSha256(secret string, payload []byte) string { @@ -32,6 +32,7 @@ func ComputeHmacSha256(secret string, payload []byte) string { func NewWebhook(WebhookURL string, WebhookURLAllClear string, WebhookSecret string, + RequestTimeout time.Duration, AllowInsecureTLS bool) (Notifier, error) { if WebhookURL == "" { @@ -41,11 +42,19 @@ func NewWebhook(WebhookURL string, return nil, errors.New("Unable to initialize webhook: webhookURL_all_clear is empty") } + httpClient := &http.Client{Timeout: time.Second * RequestTimeout} + if AllowInsecureTLS { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + httpClient = &http.Client{Transport: transport, Timeout: time.Second * RequestTimeout} + } + return &webhookNotifier{ WebhookURL, WebhookURLAllClear, WebhookSecret, - AllowInsecureTLS, + httpClient, }, nil } @@ -68,14 +77,7 @@ func (w *webhookNotifier) Notify(msg Message) error { request.Header.Set("X-HMAC-SHA256", signature) } - client := &http.Client{Timeout: time.Second * 10} - if w.AllowInsecureTLS { - transport := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - client = &http.Client{Transport: transport, Timeout: time.Second * 10} - } - _, err = client.Do(request) + _, err = w.httpClient.Do(request) if err != nil { return errors.Wrap(err, "unable to notify via webhook") } @@ -102,14 +104,7 @@ func (w *webhookNotifier) NotifyAllClear(msg Message) error { request.Header.Set("X-HMAC-SHA256", signature) } - client := &http.Client{Timeout: time.Second * 10} - if w.AllowInsecureTLS { - transport := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - client = &http.Client{Transport: transport, Timeout: time.Second * 10} - } - _, err = client.Do(request) + _, err = w.httpClient.Do(request) if err != nil { return errors.Wrap(err, "unable to notify via webhook") } From c1546e1aa55d07bd724a13e92c83a0e675e0c79e Mon Sep 17 00:00:00 2001 From: thomae <4493560+thomae@users.noreply.github.com> Date: Fri, 26 Feb 2021 09:54:15 +0100 Subject: [PATCH 4/6] Include unit in request timeout config --- nanny.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nanny.toml b/nanny.toml index 96b4185..cca901f 100644 --- a/nanny.toml +++ b/nanny.toml @@ -40,7 +40,7 @@ enabled=false webhook_url="https://webhook.somewhere.com/webhook" webhook_url_all_clear="https://webhook.somewhere.com/webhook" webhook_secret="" -request_timeout=10 +request_timeout=10s allow_insecure_tls=false [xmpp] From e64d5b402c0e637ccc101648da8bbac795b78dbb Mon Sep 17 00:00:00 2001 From: thomae <4493560+thomae@users.noreply.github.com> Date: Fri, 26 Feb 2021 10:04:46 +0100 Subject: [PATCH 5/6] Define request timeout as a string --- nanny.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nanny.toml b/nanny.toml index cca901f..8e8cf33 100644 --- a/nanny.toml +++ b/nanny.toml @@ -40,7 +40,7 @@ enabled=false webhook_url="https://webhook.somewhere.com/webhook" webhook_url_all_clear="https://webhook.somewhere.com/webhook" webhook_secret="" -request_timeout=10s +request_timeout="10s" allow_insecure_tls=false [xmpp] From 70ebecab6adf25da3d65dbc3ebe9d4642a958de5 Mon Sep 17 00:00:00 2001 From: thomae <4493560+thomae@users.noreply.github.com> Date: Fri, 26 Feb 2021 10:33:39 +0100 Subject: [PATCH 6/6] Bump version number --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 17b2ccd..6f2743d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.3 +0.4.4