Skip to content

Commit

Permalink
Merge pull request #38 from thomae/webhook
Browse files Browse the repository at this point in the history
Add generic Webhook (HTTP POST callback) as notifier
  • Loading branch information
PhilipSchmid authored Feb 26, 2021
2 parents 1a175bf + 70ebeca commit a5eab4d
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.3
0.4.4
24 changes: 24 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Config struct {
Sentry Sentry
Twilio Twilio
Slack Slack
Webhook Webhook
Xmpp Xmpp
}

Expand Down Expand Up @@ -75,6 +76,16 @@ type Slack struct {
WebhookURL string
}

// Webhook config.
type Webhook struct {
Enabled 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.
type Xmpp struct {
Enabled bool
Expand Down Expand Up @@ -233,6 +244,19 @@ 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.RequestTimeout,
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,
Expand Down
8 changes: 8 additions & 0 deletions nanny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ to=""
enabled=false
webhookURL=""

[webhook]
enabled=false
webhook_url="https://webhook.somewhere.com/webhook"
webhook_url_all_clear="https://webhook.somewhere.com/webhook"
webhook_secret=""
request_timeout="10s"
allow_insecure_tls=false

[xmpp]
enabled=false
to=["[email protected]"]
Expand Down
117 changes: 117 additions & 0 deletions pkg/notifier/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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
httpClient *http.Client
}

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,
RequestTimeout time.Duration,
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")
}

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,
httpClient,
}, 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)
}

_, err = w.httpClient.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)
}

_, err = w.httpClient.Do(request)
if err != nil {
return errors.Wrap(err, "unable to notify via webhook")
}

return nil
}

func (w *webhookNotifier) String() string {
return "webhook"
}
88 changes: 88 additions & 0 deletions receiver_examples/webhook_receiver_example.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit a5eab4d

Please sign in to comment.