-
Notifications
You must be signed in to change notification settings - Fork 1
/
GithubWebhookHttpHandler.go
163 lines (140 loc) · 5.42 KB
/
GithubWebhookHttpHandler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package lib
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/mbaynton/SimplePuppetProvisioner/lib/genericexec"
"github.com/oliveagle/jsonpath"
"io"
"io/ioutil"
"log"
"net/http"
)
type GithubWebhookHttpHandler struct {
webhookConfig *WebhooksConfig
execManager genericexec.GenericExecManagerInterface
log *log.Logger
}
type WebhooksConfig struct {
Secret string
EnableStandardR10kListener bool
R10kExecutable string
Listeners []ExecListener
}
type ExecListener struct {
Event string
Secret string
ExecConfig genericexec.GenericExecConfig
}
type jsonTemplateGetter struct {
jsonData *interface{}
}
func NewGithubWebhookHttpHandler(config *WebhooksConfig, execManager genericexec.GenericExecManagerInterface, log *log.Logger) *GithubWebhookHttpHandler {
webhookHandler := GithubWebhookHttpHandler{
webhookConfig: config,
execManager: execManager,
log: log,
}
if webhookHandler.webhookConfig.R10kExecutable == "" {
webhookHandler.webhookConfig.R10kExecutable = "/opt/puppetlabs/puppet/bin/r10k"
}
return &webhookHandler
}
func SetWebhookExecTaskConfigMap(config *WebhooksConfig, configMap map[string]genericexec.GenericExecConfig) {
for _, listener := range config.Listeners {
configMap[listener.ExecConfig.Name] = listener.ExecConfig
}
}
func (ctx *GithubWebhookHttpHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost {
response.WriteHeader(http.StatusMethodNotAllowed)
response.Write([]byte("This listener accepts only HTTP POST method requests."))
return
}
eventType := request.Header.Get("X-GitHub-Event")
if eventType == "" {
response.WriteHeader(http.StatusBadRequest)
response.Write([]byte("This listener accepts only requests compliant with the GitHub webhook API, including the X-GitHub-Event header. See https://developer.github.com/webhooks/."))
return
}
maxRequestBodyBytes := int64(1024 * 1024 * 2)
body, err := ioutil.ReadAll(io.LimitReader(request.Body, maxRequestBodyBytes))
if err != nil {
response.WriteHeader(http.StatusInternalServerError)
response.Write([]byte(fmt.Sprintf("Error reading request body: %v", err.Error())))
ctx.log.Printf("Error reading webhook request body for %s event: %v", eventType, err)
return
}
if int64(len(body)) == maxRequestBodyBytes {
response.WriteHeader(http.StatusRequestEntityTooLarge)
response.Write([]byte(fmt.Sprintf("Request body must be less than %d bytes.", maxRequestBodyBytes)))
ctx.log.Printf("Not processing webhook request json data of more than %d bytes for %s event.", maxRequestBodyBytes, eventType)
return
}
secret := ctx.webhookConfig.Secret
if secret != "" {
// Verify HMAC.
actualSignature := request.Header.Get("X-Hub-Signature")
if actualSignature == "" {
response.WriteHeader(http.StatusUnauthorized)
response.Write([]byte("This listener has a signing secret configured, but the request lacked a signature. Be sure the secret is also set in your webhook configuration on GitHub."))
ctx.log.Printf("Not processing webhook request for %s event: Missing X-Hub-Signature header.", eventType)
return
}
expectedSignature := ctx.computeExpectedSignature(body)
if !hmac.Equal([]byte(expectedSignature), []byte(actualSignature)) {
response.WriteHeader(http.StatusForbidden)
response.Write([]byte("HMAC signature verification failed. Ensure the secret configured on this listener and the secret configured for the webhook on GitHub match."))
ctx.log.Printf("Not processing webhook request for %s event: HMAC signature verification failure.", eventType)
return
}
}
var bodyJson interface{}
err = json.Unmarshal(body, &bodyJson)
if err != nil {
response.WriteHeader(http.StatusBadRequest)
response.Write([]byte("The request body was not valid JSON."))
ctx.log.Printf("Not processing webhook request for %s event: Request body was invalid JSON: %v", eventType, err)
return
}
templateGetter := jsonTemplateGetter{jsonData: &bodyJson}
matchedListeners := 0
for _, listener := range ctx.webhookConfig.Listeners {
// Does the listener match the event?
if listener.Event != "" && listener.Event == eventType {
ctx.execManager.RunTask(listener.ExecConfig.Name, templateGetter)
matchedListeners++
}
}
ctx.log.Printf("%d listener(s) matched incoming GitHub Webhook %s event.", matchedListeners, eventType)
response.WriteHeader(http.StatusOK)
response.Write([]byte(fmt.Sprintf("%d listeners matched.", matchedListeners)))
}
// Precondition: ctx.webhookConfig.Secret is set.
func (ctx *GithubWebhookHttpHandler) computeExpectedSignature(body []byte) string {
secret := ctx.webhookConfig.Secret
mac := hmac.New(sha1.New, []byte(secret))
mac.Write(body)
expectedSignature := fmt.Sprintf("sha1=%s", hex.EncodeToString(mac.Sum(nil)))
return expectedSignature
}
func (ctx jsonTemplateGetter) Get(path string) string {
res, err := jsonpath.JsonPathLookup(*ctx.jsonData, path)
if err != nil {
return ""
}
return fmt.Sprintf("%v", res)
}
func StandardR10kListenerConfig(config *WebhooksConfig) ExecListener {
return ExecListener{
Event: "push",
ExecConfig: genericexec.GenericExecConfig{
Name: "R10k sync",
Command: config.R10kExecutable,
Args: []string{"deploy", "environment", "--puppetfile"},
// TODO: add a nice success and error message, possibly with templatized if's for single / multiple commits in the push
},
}
}