-
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.
- Add tests for webhook handler - Remove Echo endpoint
- Loading branch information
1 parent
98780dd
commit 3a10711
Showing
5 changed files
with
312 additions
and
16 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 |
---|---|---|
@@ -1,4 +1,5 @@ | ||
listen | ||
coverage.out | ||
|
||
# ignore vendored dependencies from go dep tool | ||
vendor |
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
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,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)) | ||
} |
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,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") | ||
} | ||
} |
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