Last active
September 24, 2024 14:40
-
-
Save rjz/b51dc03061dbcff1c521 to your computer and use it in GitHub Desktop.
Handle Github webhooks with golang
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
// Now available in package form at https://github.com/rjz/githubhook | |
package handler | |
// https://developer.github.com/webhooks/ | |
import ( | |
"crypto/hmac" | |
"crypto/sha1" | |
"encoding/hex" | |
"errors" | |
"io" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"strings" | |
) | |
// Replace with your hook's secret | |
const secret = "shhhhh!!" | |
func signBody(secret, body []byte) []byte { | |
computed := hmac.New(sha1.New, secret) | |
computed.Write(body) | |
return []byte(computed.Sum(nil)) | |
} | |
func verifySignature(secret []byte, signature string, body []byte) bool { | |
const signaturePrefix = "sha1=" | |
const signatureLength = 45 // len(SignaturePrefix) + len(hex(sha1)) | |
if len(signature) != signatureLength || !strings.HasPrefix(signature, signaturePrefix) { | |
return false | |
} | |
actual := make([]byte, 20) | |
hex.Decode(actual, []byte(signature[5:])) | |
return hmac.Equal(signBody(secret, body), actual) | |
} | |
type HookContext struct { | |
Signature string | |
Event string | |
Id string | |
Payload []byte | |
} | |
func ParseHook(secret []byte, req *http.Request) (*HookContext, error) { | |
hc := HookContext{} | |
if hc.Signature = req.Header.Get("x-hub-signature"); len(hc.Signature) == 0 { | |
return nil, errors.New("No signature!") | |
} | |
if hc.Event = req.Header.Get("x-github-event"); len(hc.Event) == 0 { | |
return nil, errors.New("No event!") | |
} | |
if hc.Id = req.Header.Get("x-github-delivery"); len(hc.Id) == 0 { | |
return nil, errors.New("No event Id!") | |
} | |
body, err := ioutil.ReadAll(req.Body) | |
if err != nil { | |
return nil, err | |
} | |
if !verifySignature(secret, hc.Signature, body) { | |
return nil, errors.New("Invalid signature") | |
} | |
hc.Payload = body | |
return &hc, nil | |
} | |
func Handler(w http.ResponseWriter, r *http.Request) { | |
hc, err := ParseHook([]byte(secret), r) | |
w.Header().Set("Content-type", "application/json") | |
if err != nil { | |
w.WriteHeader(http.StatusBadRequest) | |
log.Printf("Failed processing hook! ('%s')", err) | |
io.WriteString(w, "{}") | |
return | |
} | |
log.Printf("Received %s", hc.Event) | |
// parse `hc.Payload` or do additional processing here | |
w.WriteHeader(http.StatusOK) | |
io.WriteString(w, "{}") | |
return | |
} |
Cool. Thanks man
Thanks!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
👍. Moved to https://github.com/rjz/githubhook for the sake of
go get
.