Skip to content

Commit 64e0568

Browse files
committed
Create messages.go
1 parent 0a447f1 commit 64e0568

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed

bitbucket/messages.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package bitbucket
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha1"
6+
"crypto/sha256"
7+
"crypto/sha512"
8+
"encoding/hex"
9+
"encoding/json"
10+
"errors"
11+
"fmt"
12+
"hash"
13+
"io/ioutil"
14+
"net/http"
15+
"net/url"
16+
"strings"
17+
)
18+
19+
const (
20+
// sha1Prefix is the prefix used by Bitbucket Server before the HMAC hexdigest.
21+
sha1Prefix = "sha1"
22+
// sha256Prefix and sha512Prefix are provided for future compatibility.
23+
sha256Prefix = "sha256"
24+
sha512Prefix = "sha512"
25+
// signatureHeader is the Bitbucket Server header key used to pass the HMAC hexdigest.
26+
signatureHeader = "X-Hub-Signature"
27+
// eventKeyHeader is the Bitbucket Server header key used to pass the event type.
28+
eventKeyHeader = "X-Event-Key"
29+
// requestIDHeader is the Bitbucket Server header key used to pass the unique ID for the webhook event.
30+
requestIDHeader = "X-Request-Id"
31+
// payloadFormParam is the name of the form parameter that the JSON payload
32+
// will be in if a webhook has its content type set to application/x-www-form-urlencoded.
33+
payloadFormParam = "payload"
34+
)
35+
36+
// genMAC generates the HMAC signature for a message provided the secret key
37+
// and hashFunc.
38+
func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte {
39+
mac := hmac.New(hashFunc, key)
40+
mac.Write(message)
41+
return mac.Sum(nil)
42+
}
43+
44+
// checkMAC reports whether messageMAC is a valid HMAC tag for message.
45+
func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool {
46+
expectedMAC := genMAC(message, key, hashFunc)
47+
return hmac.Equal(messageMAC, expectedMAC)
48+
}
49+
50+
// messageMAC returns the hex-decoded HMAC tag from the signature and its
51+
// corresponding hash function.
52+
func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
53+
if signature == "" {
54+
return nil, nil, errors.New("missing signature")
55+
}
56+
sigParts := strings.SplitN(signature, "=", 2)
57+
if len(sigParts) != 2 {
58+
return nil, nil, fmt.Errorf("error parsing signature %q", signature)
59+
}
60+
61+
var hashFunc func() hash.Hash
62+
switch sigParts[0] {
63+
case sha1Prefix:
64+
hashFunc = sha1.New
65+
case sha256Prefix:
66+
hashFunc = sha256.New
67+
case sha512Prefix:
68+
hashFunc = sha512.New
69+
default:
70+
return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0])
71+
}
72+
73+
buf, err := hex.DecodeString(sigParts[1])
74+
if err != nil {
75+
return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err)
76+
}
77+
return buf, hashFunc, nil
78+
}
79+
80+
// ValidatePayload validates an incoming Bitbucket Server Webhook event request
81+
// and returns the (JSON) payload.
82+
// The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded".
83+
// If the Content-Type is neither then an error is returned.
84+
// secretToken is the Bitbucket Server Webhook secret token.
85+
// If your webhook does not contain a secret token, you can pass nil or an empty slice.
86+
// This is intended for local development purposes only and all webhooks should ideally set up a secret token.
87+
//
88+
// Example usage:
89+
//
90+
// func (s *BitbucketEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
91+
// payload, err := bitbucket.ValidatePayload(r, s.webhookSecretKey)
92+
// if err != nil { ... }
93+
// // Process payload...
94+
// }
95+
//
96+
func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) {
97+
var body []byte // Raw body that Bitbucket Server uses to calculate the signature.
98+
99+
switch ct := r.Header.Get("Content-Type"); ct {
100+
case "application/json":
101+
var err error
102+
if body, err = ioutil.ReadAll(r.Body); err != nil {
103+
return nil, err
104+
}
105+
106+
// If the content type is application/json,
107+
// the JSON payload is just the original body.
108+
payload = body
109+
110+
case "application/x-www-form-urlencoded":
111+
var err error
112+
if body, err = ioutil.ReadAll(r.Body); err != nil {
113+
return nil, err
114+
}
115+
116+
// If the content type is application/x-www-form-urlencoded,
117+
// the JSON payload will be under the "payload" form param.
118+
form, err := url.ParseQuery(string(body))
119+
if err != nil {
120+
return nil, err
121+
}
122+
payload = []byte(form.Get(payloadFormParam))
123+
124+
default:
125+
return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", ct)
126+
}
127+
128+
// Only validate the signature if a secret token exists. This is intended for
129+
// local development only and all webhooks should ideally set up a secret token.
130+
if len(secretToken) > 0 {
131+
sig := r.Header.Get(signatureHeader)
132+
if err := ValidateSignature(sig, body, secretToken); err != nil {
133+
return nil, err
134+
}
135+
}
136+
137+
return payload, nil
138+
}
139+
140+
// ValidateSignature validates the signature for the given payload.
141+
// signature is the Bitbucket Server hash signature delivered in the X-Hub-Signature header.
142+
// payload is the JSON payload sent by Bitbucket Server Webhooks.
143+
// secretToken is the Bitbucket Server Webhook secret token.
144+
//
145+
// Doc: https://confluence.atlassian.com/bitbucketserver070/managing-webhooks-in-bitbucket-server-996644364.html#ManagingwebhooksinBitbucketServer-webhooksecretsWebhooksecrets
146+
func ValidateSignature(signature string, payload, secretToken []byte) error {
147+
messageMAC, hashFunc, err := messageMAC(signature)
148+
if err != nil {
149+
return err
150+
}
151+
if !checkMAC(payload, messageMAC, secretToken, hashFunc) {
152+
return errors.New("payload signature check failed")
153+
}
154+
return nil
155+
}
156+
157+
// WebHookType returns the event key of webhook request r.
158+
//
159+
// Doc: https://confluence.atlassian.com/bitbucketserver070/event-payload-996644369.html#Eventpayload-HTTPheaders
160+
func WebHookType(r *http.Request) string {
161+
return r.Header.Get(eventKeyHeader)
162+
}
163+
164+
// RequestID returns the unique UUID for each webhook request r.
165+
//
166+
// Doc: https://confluence.atlassian.com/bitbucketserver070/event-payload-996644369.html#Eventpayload-HTTPheaders
167+
func RequestID(r *http.Request) string {
168+
return r.Header.Get(requestIDHeader)
169+
}
170+
171+
// ParseWebHook parses the event payload. An error will be returned for unrecognized
172+
// event types.
173+
//
174+
// Example usage:
175+
//
176+
// func (s *BitbucketEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
177+
// payload, err := bitbucket.ValidatePayload(r, s.webhookSecretKey)
178+
// if err != nil { ... }
179+
// event, err := bitbucket.ParseWebHook(bitbucket.WebHookType(r), payload)
180+
// if err != nil { ... }
181+
// switch event := event.(type) {
182+
// case *bitbucket.RepositoryForkedEvent:
183+
// processRepositoryForkedEvent(event)
184+
// case *bitbucket.PullRequestModifiedEvent:
185+
// processPullRequestModifiedEvent(event)
186+
// ...
187+
// }
188+
// }
189+
//
190+
func ParseWebHook(eventKey string, payload []byte) (interface{}, error) {
191+
var event interface{}
192+
193+
switch eventKey {
194+
case EventKeyRepositoryPush:
195+
event = &PushEvent{}
196+
case EventKeyRepositoryModified:
197+
event = &RepositoryModifiedEvent{}
198+
case EventKeyRepositoryForked:
199+
event = &RepositoryForkedEvent{}
200+
case EventKeyPullRequestOpened:
201+
event = &PullRequestOpenedEvent{}
202+
case EventKeyPullRequestReviewersUpdated:
203+
event = &PullRequestReviewerEvent{}
204+
case EventKeyPullRequestModified:
205+
event = &PullRequestModifiedEvent{}
206+
case EventKeyPullRequestBranchUpdated:
207+
event = &PullRequestBranchUpdatedEvent{}
208+
case EventKeyPullRequestApproved:
209+
event = &PullRequestApprovedEvent{}
210+
case EventKeyPullRequestUnapproved:
211+
event = &PullRequestUnapprovedEvent{}
212+
case EventKeyPullRequestNeedsWork:
213+
event = &PullRequestNeedsWorkEvent{}
214+
case EventKeyPullRequestMerged:
215+
event = &PullRequestMergedEvent{}
216+
case EventKeyPullRequestDeclined:
217+
event = &PullRequestDeclinedEvent{}
218+
case EventKeyPullRequestDeleted:
219+
event = &PullRequestDeletedEvent{}
220+
221+
default:
222+
return nil, fmt.Errorf("unknown X-Event-Key in message: %v", eventKey)
223+
}
224+
225+
err := json.Unmarshal(payload, event)
226+
if err != nil {
227+
return nil, err
228+
}
229+
230+
return event, nil
231+
}

0 commit comments

Comments
 (0)