-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Christopher Najewicz
committed
Jan 2, 2020
1 parent
bef2a64
commit bbc9376
Showing
7 changed files
with
320 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,4 +12,4 @@ | |
*.out | ||
|
||
# Dependency directories (remove the comment below to include it) | ||
# vendor/ | ||
vendor/ |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Gopkg.toml example | ||
# | ||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html | ||
# for detailed Gopkg.toml documentation. | ||
# | ||
# required = ["github.com/user/thing/cmd/thing"] | ||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] | ||
# | ||
# [[constraint]] | ||
# name = "github.com/user/project" | ||
# version = "1.0.0" | ||
# | ||
# [[constraint]] | ||
# name = "github.com/user/project2" | ||
# branch = "dev" | ||
# source = "github.com/myfork/project2" | ||
# | ||
# [[override]] | ||
# name = "github.com/x/y" | ||
# version = "2.4.0" | ||
# | ||
# [prune] | ||
# non-go = false | ||
# go-tests = true | ||
# unused-packages = true | ||
|
||
|
||
[[constraint]] | ||
name = "github.com/stretchr/testify" | ||
version = "1.4.0" | ||
|
||
[prune] | ||
go-tests = true | ||
unused-packages = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
project:=$(shell basename $(shell pwd)) | ||
commit:=$(shell git rev-parse --short HEAD) | ||
importpath:=github.com/chiefy/$(project) | ||
ts:=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') | ||
|
||
$(GOPATH)/bin/dep: | ||
@curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh | ||
|
||
vendor: $(GOPATH)/bin/dep | ||
@dep ensure | ||
|
||
test: vendor | ||
@go test ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,44 @@ | ||
# go-slack-utils | ||
Some wrappers for incoming webhooks, Block Kit UI and message authentication for Slack | ||
|
||
## What this is? | ||
|
||
This is a general purpose utility library for using Slack's Block Kit UI, with Go structs corresponding to the blocks used to create UI elements. Also included is middleware for validing Slack requests using HMAC-256 and the Slack secret signing key. | ||
|
||
|
||
## What this is not? | ||
|
||
A Slack API wrapper. There's plenty of those out there. | ||
|
||
|
||
## Installation | ||
|
||
``` | ||
go get -u github.com/chiefy/go-slack-utils | ||
``` | ||
|
||
## Usage | ||
|
||
### Middleware | ||
|
||
``` | ||
func main() { | ||
r := mux.NewRouter() | ||
r.HandleFunc("/command", MySlashCommandHandler).Methods(http.MethodPost) | ||
r.Use(middleware.ValidateTimestamp) | ||
// It's up to you on how you configure injection of the slack signing secret | ||
signingSecret := os.Getenv("SLACK_SIGNING_SECRET") | ||
// Generate the validation middleware by injecting the secret | ||
validateReq := middleware.ValidateSlackRequest(signingSecret) | ||
r.Use(validateReq) | ||
srv := &http.Server{ | ||
Handler: r, | ||
Addr: "127.0.0.1:" + os.Getenv("PORT"), | ||
WriteTimeout: 15 * time.Second, | ||
ReadTimeout: 15 * time.Second, | ||
} | ||
log.Fatal(srv.ListenAndServe()) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package middleware | ||
|
||
import ( | ||
"bytes" | ||
"crypto/hmac" | ||
"crypto/sha256" | ||
"encoding/hex" | ||
"fmt" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"strconv" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const ( | ||
slackSigHeader = "X-Slack-Signature" | ||
slackSigHeaderTimestamp = "X-Slack-Request-Timestamp" | ||
// RequestTTL asserts incoming requests must have a timestamp within this duration from server's time | ||
RequestTTL = "1m" | ||
) | ||
|
||
// ValidateTimestamp asserts that the incoming request's timestamp is within a reasonable timespan from current time | ||
func ValidateTimestamp(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
if !validateTimestamp(timestampFromRequest(r)) { | ||
log.Println("invalid timestamp") | ||
http.Error(w, "Invalid Timestamp", http.StatusBadRequest) | ||
} else { | ||
next.ServeHTTP(w, r) | ||
} | ||
}) | ||
} | ||
|
||
// timestampFromRequest gets the unix epoch timestamp on the http request | ||
func timestampFromRequest(r *http.Request) int64 { | ||
ts, err := strconv.ParseInt(r.Header.Get(slackSigHeaderTimestamp), 10, 64) | ||
if err != nil { | ||
log.Println("could not get timestamp", err) | ||
ts = 0 | ||
} | ||
return ts | ||
} | ||
|
||
// validateTimestamp asserts that the request's timestamp is within a reasonable timeframe compared with server's current time | ||
func validateTimestamp(ts int64) bool { | ||
abs := func(a time.Duration) time.Duration { | ||
if a >= 0 { | ||
return a | ||
} | ||
return -a | ||
} | ||
m, _ := time.ParseDuration(RequestTTL) | ||
d := abs(time.Since(time.Unix(ts, 0))) | ||
return d < m | ||
} | ||
|
||
// ValidateSlackRequest validates a request's signature is signed by the provided slack secret token | ||
func ValidateSlackRequest(signingSecretToken string) func(http.Handler) http.Handler { | ||
return func(next http.Handler) http.Handler { | ||
f := func(w http.ResponseWriter, r *http.Request) { | ||
bodyData, err := ioutil.ReadAll(r.Body) | ||
if err != nil { | ||
log.Printf("bad request body %s", err) | ||
http.Error(w, "Bad Request", http.StatusBadRequest) | ||
return | ||
} | ||
incomingSig := []byte(fmt.Sprintf("v0:%d:%s", timestampFromRequest(r), string(bodyData))) | ||
slackSig, _ := hex.DecodeString(strings.TrimPrefix(r.Header.Get(slackSigHeader), "v0=")) | ||
if !validMAC(incomingSig, slackSig, []byte(signingSecretToken)) { | ||
log.Println("HMAC error - signatures did not match") | ||
http.Error(w, "Forbidden", http.StatusForbidden) | ||
} else { | ||
r.Body.Close() | ||
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyData)) | ||
next.ServeHTTP(w, r) | ||
} | ||
} | ||
return http.HandlerFunc(f) | ||
} | ||
} | ||
|
||
// validMAC reports whether messageMAC is a valid HMAC tag for message. | ||
func validMAC(message, messageMAC, key []byte) bool { | ||
mac := hmac.New(sha256.New, key) | ||
mac.Write(message) | ||
expectedMAC := mac.Sum(nil) | ||
return hmac.Equal(messageMAC, expectedMAC) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package middleware | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"github.com/stretchr/testify/assert" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"strconv" | ||
"testing" | ||
"time" | ||
) | ||
|
||
const ( | ||
applicationJSON = "application/json" | ||
) | ||
|
||
// GetTestHandler returns a http.HandlerFunc for testing http middleware | ||
func GetTestHandler() http.HandlerFunc { | ||
fn := func(rw http.ResponseWriter, req *http.Request) { | ||
return | ||
} | ||
return http.HandlerFunc(fn) | ||
} | ||
|
||
func TestValidateTimestampHandler(t *testing.T) { | ||
assert := assert.New(t) | ||
|
||
tenMinutesAgo, _ := time.ParseDuration("-10m") | ||
justOverMinute, _ := time.ParseDuration(RequestTTL) | ||
justOverMinute = justOverMinute + (time.Second * 10) | ||
|
||
tests := []struct { | ||
description string | ||
url string | ||
expectedBody string | ||
expectedCode int | ||
timestamp int64 | ||
}{ | ||
{ | ||
description: "invalid timestamp header", | ||
url: "/", | ||
expectedBody: "Invalid Timestamp\n", | ||
expectedCode: 400, | ||
timestamp: 0, | ||
}, | ||
{ | ||
description: "ten minutes ago", | ||
url: "/", | ||
expectedBody: "Invalid Timestamp\n", | ||
expectedCode: 400, | ||
timestamp: time.Now().Add(tenMinutesAgo).Unix(), | ||
}, | ||
{ | ||
description: "three minutes in the future", | ||
url: "/", | ||
expectedBody: "Invalid Timestamp\n", | ||
expectedCode: 400, | ||
timestamp: time.Now().Add(justOverMinute).Unix(), | ||
}, | ||
{ | ||
description: "valid timestamp", | ||
url: "/", | ||
expectedBody: "", | ||
expectedCode: 200, | ||
timestamp: time.Now().Unix(), | ||
}, | ||
} | ||
|
||
ts := httptest.NewServer(ValidateTimestamp(GetTestHandler())) | ||
defer ts.Close() | ||
|
||
for _, tc := range tests { | ||
j, _ := json.Marshal("") | ||
|
||
req, err := http.NewRequest("POST", ts.URL, bytes.NewBuffer(j)) | ||
assert.NoError(err) | ||
|
||
req.Header.Set("Content-Type", applicationJSON) | ||
if tc.timestamp != 0 { | ||
req.Header.Set(slackSigHeaderTimestamp, strconv.Itoa(int(tc.timestamp))) | ||
} | ||
|
||
res, err := http.DefaultClient.Do(req) | ||
assert.NoError(err) | ||
|
||
if res != nil { | ||
defer res.Body.Close() | ||
} | ||
|
||
b, err := ioutil.ReadAll(res.Body) | ||
assert.NoError(err) | ||
|
||
assert.Equal(tc.expectedCode, res.StatusCode, tc.description) | ||
assert.Equal(tc.expectedBody, string(b), tc.description) | ||
} | ||
} |