8000 In the beginning · rjz/githubhook@7565c5e · GitHub
[go: up one dir, main page]

Skip to content

Commit 7565c5e

Browse files
committed
In the beginning
0 parents  commit 7565c5e

File tree

5 files changed

+199
-0
lines changed

5 files changed

+199
-0
lines changed

CONTRIBUTING.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Contributing
2+
===============================================================================
3+
4+
Have something to add? Feature requests, bug reports, and contributions are
5+
enormously welcome!
6+
7+
1. Fork this repo
8+
2. Update the tests and implement the change
9+
3. Submit a [pull request][github-pull-request]
10+
11+
(hint: following the conventions in the [the code review
12+
checklist][code-review-checklist] will expedite review and merge)
13+
14+
[github-pull-request]: help.github.com/pull-requests/
15+
[code-review-checklist]: https://github.com/rjz/code-review-checklist
16+

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// MIT License
2+
3+
Copyright (C) RJ Zaworski <rj@rjzaworski.com>
4+
5+
Permission is hereby 8000 granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
githubhook
2+
===============================================
3+
4+
Golang parser for [github webhooks][gh-webhook]. Not a server, though it could
5+
be integrated with one.
6+
7+
Installation
8+
-----------------------------------------------
9+
10+
$ go get github.com/rjz/githubhook
11+
12+
Usage
13+
-----------------------------------------------
14+
15+
Given an incoming `*http.Request` representing a webhook signed with a `secret`,
16+
use `githubhook` to validate and parse its content:
17+
18+
secret := []byte("don't tell!")
19+
hook, err := githubhook.Parse(secret, req)
20+
21+
[gh-webhook]: https://developer.github.com/webhooks/

webhook.go

Lines changed: 69 additions & 0 deletions
< 8000 th scope="col">Original file line number
Diff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package webhook
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha1"
6+
"encoding/hex"
7+
"errors"
8+
"io/ioutil"
9+
"net/http"
10+
"strings"
11+
)
12+
13+
type Hook struct {
14+
Signature string
15+
Event string
16+
Id string
17+
Payload []byte
18+
}
19+
20+
func signBody(secret, body []byte) []byte {
21+
computed := hmac.New(sha1.New, secret)
22+
computed.Write(body)
23+
return []byte(computed.Sum(nil))
24+
}
25+
26+
func verifySignature(secret []byte, signature string, body []byte) bool {
27+
28+
const signaturePrefix = "sha1="
29+
const signatureLength = 45 // len(SignaturePrefix) + len(hex(sha1))
30+
31+
if len(signature) != signatureLength || !strings.HasPrefix(signature, signaturePrefix) {
32+
return false
33+
}
34+
35+
actual := make([]byte, 20)
36+
hex.Decode(actual, []byte(signature[5:]))
37+
38+
return hmac.Equal(signBody(secret, body), actual)
39+
}
40+
41+
func Parse(secret []byte, req *http.Request) (*Hook, error) {
42+
hook := Hook{}
43+
44+
if hook.Signature = req.Header.Get("x-hub-signature"); len(hook.Signature) == 0 {
45+
return nil, errors.New("No signature!")
46+
}
47+
48+
if hook.Event = req.Header.Get("x-github-event"); len(hook.Event) == 0 {
49+
return nil, errors.New("No event!")
50+
}
51+
52+
if hook.Id = req.Header.Get("x-github-delivery"); len(hook.Id) == 0 {
53+
return nil, errors.New("No event Id!")
54+
}
55+
56+
body, err := ioutil.ReadAll(req.Body)
57+
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
if !verifySignature(secret, hook.Signature, body) {
63+
return nil, errors.New("Invalid signature")
64+
}
65+
66+
hook.Payload = body
67+
68+
return &hook, nil
69+
}

webhook_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package webhook
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha1"
6+
"encoding/hex"
7+
"fmt"
8+
"net/http"
9+
"strings"
10+
"testing"
11+
)
12+
13+
const testSecret = "foobar"
14+
15+
func expectErrorMessage(t *testing.T, msg string, err error) {
16+
if err == nil || err.Error() != msg {
17+
t.Error(fmt.Sprintf("Expected '%s', got %s", msg, err))
18+
}
19+
}
20+
21+
func expectParseError(t *testing.T, msg string, r *http.Request) {
22+
_, err := Parse([]byte(testSecret), r)
23+
expectErrorMessage(t, msg, err)
24+
}
25+
26+
func signature(body string) string {
27+
dst := make([]byte, 40)
28+
computed := hmac.New(sha1.New, []byte(testSecret))
29+
computed.Write([]byte(body))
30+
hex.Encode(dst, computed.Sum(nil))
31+
return "sha1=" + string(dst)
32+
}
33+
34+
func TestMissingSignature(t *testing.T) {
35+
r, _ := http.NewRequest("GET", "/path", nil)
36+
expectParseError(t, "No signature!", r)
37+
}
38+
39+
func TestMissingEvent(t *testing.T) {
40+
r, _ := http.NewRequest("GET", "/path", nil)
41+
r.Header.Add("x-hub-signature", "bogus signature")
42+
expectParseError(t, "No event!", r)
43+
}
44+
45+
func TestMissingEventId(t *testing.T) {
46+
r, _ := http.NewRequest("GET", "/path", nil)
47+
r.Header.Add("x-hub-signature", "bogus signature")
48+
r.Header.Add("x-github-event", "bogus event")
49+
expectParseError(t, "No event Id!", r)
50+
}
51+
52+
func TestInvalidSignature(t *testing.T) {
53+
r, _ := http.NewRequest("GET", "/path", strings.NewReader("..."))
54+
r.Header.Add("x-hub-signature", "bogus signature")
55+
r.Header.Add("x-github-event", "bogus event")
56+
r.Header.Add("x-github-delivery", "bogus id")
57+
expectParseError(t, "Invalid signature", r)
58+
}
59+
60+
func TestValidSignature(t *testing.T) {
61+
62+
body := "{}"
63+
64+
r, _ := http.NewRequest("GET", "/path", strings.NewReader(body))
65+
r.Header.Add("x-hub-signature", signature(body))
66+
r.Header.Add("x-github-event", "bogus event")
67+
r.Header.Add("x-github-delivery", "bogus id")
68+
69+
if _, err := Parse([]byte(testSecret), r); err != nil {
70+
t.Error(fmt.Sprintf("Unexpected error '%s'", err))
71+
}
72+
}

0 commit comments

Comments
 (0)
0