Skip to content

Commit

Permalink
webhook: Add basic handler
Browse files Browse the repository at this point in the history
- Add tests for webhook handler
- Remove Echo endpoint
  • Loading branch information
nkprince007 committed May 24, 2018
1 parent 98780dd commit 3a10711
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
listen
coverage.out

# ignore vendored dependencies from go dep tool
vendor
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ run: build
MICRO_SERVER_ADDRESS=:8000 ./listen

test:
go test -v ./... -cover
go test -v ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out

docker:
docker build . -t registry.gitlab.com/nkprince007/listen:latest
106 changes: 92 additions & 14 deletions handler/handler.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,107 @@
package handler

import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"io/ioutil"
"net/http"
"time"
"os"
)

// Echo receives whatever was sent and posts it back
func Echo(w http.ResponseWriter, r *http.Request) {
// decode the incoming request as json
var request map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), 500)
type Provider string

const (
GitHub Provider = "github"
GitLab Provider = "gitlab"
None Provider = ""
)

type response struct {
Description string `json:"description"`
Status bool `json:"status"`
}

func encodeResponse(desc string, status bool) []byte {
rsp := &response{desc, status}
enc, _ := json.Marshal(rsp)
return enc
}

func recognizeWebhook(h http.Header) (string, Provider) {
event := h.Get("X-GitHub-Event")
if len(event) > 0 {
return event, GitHub
}

event = h.Get("X-Gitlab-Event")
if len(event) > 0 {
return event, GitLab
}

return "", None
}

func handleGitHub(r *http.Request, event string) {

}

func handleGitLab(r *http.Request, event string) {

}

// Capture accepts webhooks and multicasts events.
func Capture(w http.ResponseWriter, r *http.Request) {
// verify whether it is a POST request
if r.Method != "POST" {
w.WriteHeader(405)
w.Write(encodeResponse("only POST requests allowed", false))
return
}

response := map[string]interface{}{
"payload": request,
"processedAt": time.Now().Format(time.RFC1123),
"status": "received",
event, provider := recognizeWebhook(r.Header)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(500)
w.Write(encodeResponse("could not decode request body", false))
return
}

// encode and write the response as json
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, err.Error(), 500)
switch provider {
case GitHub:
requestSign := r.Header.Get("X-Hub-Signature")
if githubSecret, ok := os.LookupEnv("GITHUB_SECRET"); ok {
hash := hmac.New(sha1.New, []byte(githubSecret))
hash.Write(body)
generatedSign := hex.EncodeToString(hash.Sum(nil))

if requestSign[5:] != generatedSign {
w.WriteHeader(403)
w.Write(encodeResponse("signature not matched", false))
return
}
}

handleGitHub(r, event)
case GitLab:
requestSign := r.Header.Get("X-Gitlab-Token")
if gitlabSecret, ok := os.LookupEnv("GITLAB_SECRET"); ok {
if requestSign != gitlabSecret {
w.WriteHeader(403)
w.Write(encodeResponse("signature not matched", false))
return
}
}

handleGitLab(r, event)
default:
// no provider matched
w.WriteHeader(400)
w.Write(encodeResponse("no matching providers implemented", false))
return
}

w.WriteHeader(200)
w.Write(encodeResponse("webhook accepted", true))
}
216 changes: 216 additions & 0 deletions handler/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package handler_test

import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"net/http"
"net/http/httptest"
"os"
"syscall"
"testing"

"gitlab.com/nkprince007/listen/handler"
)

func generateGhSign(secret string, body []byte) string {
hash := hmac.New(sha1.New, []byte(secret))
hash.Write(body)
return hex.EncodeToString(hash.Sum(nil))
}

func TestCaptureWrongMethod(t *testing.T) {
var ts = httptest.NewServer(http.HandlerFunc(handler.Capture))
defer ts.Close()

res, err := http.Get(ts.URL)
if err != nil {
t.Error(err)
}

if res.StatusCode != http.StatusMethodNotAllowed {
t.Error(res.Status)
t.Error("methods other than post should be rejected")
}
}

func TestNoMatchingProvider(t *testing.T) {
var ts = httptest.NewServer(http.HandlerFunc(handler.Capture))
defer ts.Close()

// no explicit X-*-Event header has been set on request
data := bytes.NewBuffer([]byte("{}"))
res, err := http.Post(ts.URL, "application/json", data)
if err != nil {
t.Error(err)
}

if res.StatusCode != http.StatusBadRequest {
t.Error(res.Status)
t.Error("matching provider should not be found")
}
}

func TestGitHubSignatureVerification(t *testing.T) {
oldGhSecret := os.Getenv("GITHUB_SECRET")
os.Setenv("GITHUB_SECRET", "secret")

defer func() {
if oldGhSecret != "" {
os.Setenv("GITHUB_SECRET", oldGhSecret)
}
}()

reqBody := []byte("{}")
sign := generateGhSign("secret", reqBody)

var ts = httptest.NewServer(http.HandlerFunc(handler.Capture))
defer ts.Close()

data := bytes.NewBuffer(reqBody)

// pass signature verification
req, err := http.NewRequest("POST", ts.URL, data)
if err != nil {
t.Error(err)
}
req.Header.Add("X-GitHub-Event", "ping")
req.Header.Add("X-Hub-Signature", "sha1="+sign)

res, err := ts.Client().Do(req)
if err != nil {
t.Error(err)
}

if res.StatusCode != http.StatusOK {
t.Error(res.Status)
t.Error("github signature verification success, but still rejected")
}

// failed signature verification
req, err = http.NewRequest("POST", ts.URL, data)
if err != nil {
t.Error(err)
}
req.Header.Add("X-GitHub-Event", "ping")
req.Header.Add("X-Hub-Signature", "sha1=somethingelse")

res, err = ts.Client().Do(req)
if err != nil {
t.Error(err)
}

if res.StatusCode != http.StatusForbidden {
t.Error(res.Status)
t.Error("github signature match failed, but still forwarded")
}
}

func TestGitLabSignatureVerification(t *testing.T) {
oldGlSecret := os.Getenv("GITLAB_SECRET")
os.Setenv("GITLAB_SECRET", "secret")

defer func() {
if oldGlSecret != "" {
os.Setenv("GITLAB_SECRET", oldGlSecret)
}
}()

reqBody := []byte("{}")
var ts = httptest.NewServer(http.HandlerFunc(handler.Capture))
defer ts.Close()

data := bytes.NewBuffer(reqBody)

// pass signature verification
req, err := http.NewRequest("POST", ts.URL, data)
if err != nil {
t.Error(err)
}
req.Header.Add("X-Gitlab-Event", "ping")
req.Header.Add("X-Gitlab-Token", "secret")

res, err := ts.Client().Do(req)
if err != nil {
t.Error(err)
}

if res.StatusCode != http.StatusOK {
t.Error(res.Status)
t.Error("gitlab signature verification success, but still rejected")
}

// failed signature verification
req, err = http.NewRequest("POST", ts.URL, data)
if err != nil {
t.Error(err)
}
req.Header.Add("X-Gitlab-Event", "ping")
req.Header.Add("X-Gitlab-Token", "somethingelse")

res, err = ts.Client().Do(req)
if err != nil {
t.Error(err)
}

if res.StatusCode != http.StatusForbidden {
t.Error(res.Status)
t.Error("gitlab signature match failed, but still forwarded")
}
}

func TestNoSignatureVerification(t *testing.T) {
oldGhSecret := os.Getenv("GITHUB_SECRET")
oldGlSecret := os.Getenv("GITLAB_SECRET")
syscall.Unsetenv("GITHUB_SECRET")
syscall.Unsetenv("GITLAB_SECRET")

defer func() {
if oldGhSecret != "" {
os.Setenv("GITHUB_SECRET", oldGhSecret)
}
if oldGlSecret != "" {
os.Setenv("GITLAB_SECRET", oldGlSecret)
}
}()

var ts = httptest.NewServer(http.HandlerFunc(handler.Capture))
defer ts.Close()

data := bytes.NewBuffer([]byte("{}"))

// GitHub
req, err := http.NewRequest("POST", ts.URL, data)
if err != nil {
t.Error(err)
}
req.Header.Add("X-GitHub-Event", "ping")

res, err := ts.Client().Do(req)
if err != nil {
t.Error(err)
}

if res.StatusCode != http.StatusOK {
t.Error(res.Status)
t.Error("github signature verification disabled, but still rejected")
}

// GitLab
req, err = http.NewRequest("POST", ts.URL, data)
if err != nil {
t.Error(err)
}
req.Header.Add("X-Gitlab-Event", "ping")

res, err = ts.Client().Do(req)
if err != nil {
t.Error(err)
}

if res.StatusCode != http.StatusOK {
t.Error(res.Status)
t.Error("gitlab signature verification disabled, but still rejected")
}
}
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func main() {
)

// register call handler
service.HandleFunc("/", handler.Echo)
service.HandleFunc("/", handler.Capture)

// initialise service
if err := service.Init(); err != nil {
Expand Down

0 comments on commit 3a10711

Please sign in to comment.