Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose event message headers, introduce a new way to read the body from an io.Reader #1955

Merged
merged 7 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 51 additions & 30 deletions github/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"hash"
"io"
"io/ioutil"
"mime"
"net/http"
Expand All @@ -31,14 +32,14 @@ const (
// sha256Prefix and sha512Prefix are provided for future compatibility.
sha256Prefix = "sha256"
sha512Prefix = "sha512"
// sha1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest.
sha1SignatureHeader = "X-Hub-Signature"
// sha256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest.
sha256SignatureHeader = "X-Hub-Signature-256"
// eventTypeHeader is the GitHub header key used to pass the event type.
eventTypeHeader = "X-Github-Event"
// deliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event.
deliveryIDHeader = "X-Github-Delivery"
// SHA1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest.
SHA1SignatureHeader = "X-Hub-Signature"
// SHA256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest.
SHA256SignatureHeader = "X-Hub-Signature-256"
// EventTypeHeader is the GitHub header key used to pass the event type.
EventTypeHeader = "X-Github-Event"
// DeliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event.
DeliveryIDHeader = "X-Github-Delivery"
)

var (
Expand Down Expand Up @@ -139,7 +140,7 @@ func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
return buf, hashFunc, nil
}

// ValidatePayload validates an incoming GitHub Webhook event request
// ValidatePayload validates an incoming GitHub Webhook event request body
// and returns the (JSON) payload.
// The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded".
// If the Content-Type is neither then an error is returned.
Expand All @@ -150,25 +151,19 @@ func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
// Example usage:
//
// func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// payload, err := github.ValidatePayload(r, s.webhookSecretKey)
// // read signature from request
// signature := ""
// payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey)
// if err != nil { ... }
// // Process payload...
// }
//
func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) {
func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) {
var body []byte // Raw body that GitHub uses to calculate the signature.

ct := r.Header.Get("Content-Type")

mediatype, _, err := mime.ParseMediaType(ct)
if err != nil {
mediatype = ""
}

switch mediatype {
switch contentType {
case "application/json":
var err error
if body, err = ioutil.ReadAll(r.Body); err != nil {
if body, err = ioutil.ReadAll(readable); err != nil {
return nil, err
}

Expand All @@ -182,7 +177,7 @@ func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err e
const payloadFormParam = "payload"

var err error
if body, err = ioutil.ReadAll(r.Body); err != nil {
if body, err = ioutil.ReadAll(readable); err != nil {
return nil, err
}

Expand All @@ -195,24 +190,50 @@ func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err e
payload = []byte(form.Get(payloadFormParam))

default:
return nil, fmt.Errorf("Webhook request has unsupported Content-Type %q", ct)
return nil, fmt.Errorf("Webhook request has unsupported Content-Type %q", contentType)
}

// Only validate the signature if a secret token exists. This is intended for
// local development only and all webhooks should ideally set up a secret token.
if len(secretToken) > 0 {
sig := r.Header.Get(sha256SignatureHeader)
if sig == "" {
sig = r.Header.Get(sha1SignatureHeader)
}
if err := ValidateSignature(sig, body, secretToken); err != nil {
if err := ValidateSignature(signature, body, secretToken); err != nil {
return nil, err
}
}

return payload, nil
}

// ValidatePayload validates an incoming GitHub Webhook event request
// and returns the (JSON) payload.
// The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded".
// If the Content-Type is neither then an error is returned.
// secretToken is the GitHub Webhook secret token.
// If your webhook does not contain a secret token, you can pass nil or an empty slice.
// This is intended for local development purposes only and all webhooks should ideally set up a secret token.
//
// Example usage:
//
// func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// payload, err := github.ValidatePayload(r, s.webhookSecretKey)
// if err != nil { ... }
// // Process payload...
// }
//
func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) {
signature := r.Header.Get(SHA256SignatureHeader)
if signature == "" {
signature = r.Header.Get(SHA1SignatureHeader)
}

contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return nil, err
}

return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken)
}

// ValidateSignature validates the signature for the given payload.
// signature is the GitHub hash signature delivered in the X-Hub-Signature header.
// payload is the JSON payload sent by GitHub Webhooks.
Expand All @@ -234,14 +255,14 @@ func ValidateSignature(signature string, payload, secretToken []byte) error {
//
// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/hooks/#webhook-headers
func WebHookType(r *http.Request) string {
return r.Header.Get(eventTypeHeader)
return r.Header.Get(EventTypeHeader)
}

// DeliveryID returns the unique delivery ID of webhook request r.
//
// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/hooks/#webhook-headers
func DeliveryID(r *http.Request) string {
return r.Header.Get(deliveryIDHeader)
return r.Header.Get(DeliveryIDHeader)
}

// ParseWebHook parses the event payload. For recognized event types, a
Expand Down
26 changes: 19 additions & 7 deletions github/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TestValidatePayload(t *testing.T) {
},
{
signature: "sha256=b1f8020f5b4cd42042f807dd939015c4a418bc1ff7f604dd55b0a19b5d953d9b",
signatureHeader: sha256SignatureHeader,
signatureHeader: SHA256SignatureHeader,
event: "ping",
wantEvent: "ping",
wantPayload: defaultBody,
Expand All @@ -89,7 +89,7 @@ func TestValidatePayload(t *testing.T) {
if test.signatureHeader != "" {
req.Header.Set(test.signatureHeader, test.signature)
} else {
req.Header.Set(sha1SignatureHeader, test.signature)
req.Header.Set(SHA1SignatureHeader, test.signature)
}
}
req.Header.Set("Content-Type", "application/json")
Expand Down Expand Up @@ -120,7 +120,7 @@ func TestValidatePayload_FormGet(t *testing.T) {
}
req.PostForm = form
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set(sha1SignatureHeader, signature)
req.Header.Set(SHA1SignatureHeader, signature)

got, err := ValidatePayload(req, secretKey)
if err != nil {
Expand All @@ -131,7 +131,7 @@ func TestValidatePayload_FormGet(t *testing.T) {
}

// check that if payload is invalid we get error
req.Header.Set(sha1SignatureHeader, "invalid signature")
req.Header.Set(SHA1SignatureHeader, "invalid signature")
if _, err = ValidatePayload(req, []byte{0}); err == nil {
t.Error("ValidatePayload = nil, want err")
}
Expand All @@ -150,7 +150,7 @@ func TestValidatePayload_FormPost(t *testing.T) {
t.Fatalf("NewRequest: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set(sha1SignatureHeader, signature)
req.Header.Set(SHA1SignatureHeader, signature)

got, err := ValidatePayload(req, secretKey)
if err != nil {
Expand All @@ -161,7 +161,7 @@ func TestValidatePayload_FormPost(t *testing.T) {
}

// check that if payload is invalid we get error
req.Header.Set(sha1SignatureHeader, "invalid signature")
req.Header.Set(SHA1SignatureHeader, "invalid signature")
if _, err = ValidatePayload(req, []byte{0}); err == nil {
t.Error("ValidatePayload = nil, want err")
}
Expand Down Expand Up @@ -477,6 +477,18 @@ func TestParseWebHook_BadMessageType(t *testing.T) {
}
}

func TestValidatePayloadFromBody_UnableToParseBody(t *testing.T) {
if _, err := ValidatePayloadFromBody("application/x-www-form-urlencoded", bytes.NewReader([]byte(`%`)), "sha1=", []byte{}); err == nil {
t.Errorf("ValidatePayloadFromBody returned nil; wanted error")
}
}

func TestValidatePayloadFromBody_UnsupportedContentType(t *testing.T) {
if _, err := ValidatePayloadFromBody("invalid", bytes.NewReader([]byte(`{}`)), "sha1=", []byte{}); err == nil {
t.Errorf("ValidatePayloadFromBody returned nil; wanted error")
}
}

func TestDeliveryID(t *testing.T) {
id := "8970a780-244e-11e7-91ca-da3aabcb9793"
req, err := http.NewRequest("POST", "http://localhost", nil)
Expand All @@ -494,7 +506,7 @@ func TestDeliveryID(t *testing.T) {
func TestWebHookType(t *testing.T) {
want := "yo"
req := &http.Request{
Header: http.Header{eventTypeHeader: []string{want}},
Header: http.Header{EventTypeHeader: []string{want}},
}
if got := WebHookType(req); got != want {
t.Errorf("WebHookType = %q, want %q", got, want)
Expand Down