forked from src-d/github-reminder
/
handler.go
185 lines (167 loc) · 5.5 KB
/
handler.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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// Package handler provides an http.Handler serving the endpoings of the github-reminder app.
package handler
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"github.com/google/go-github/github"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/src-d/github-reminder/reminder"
)
type server struct {
appID int
key []byte
secret []byte
transport http.RoundTripper
}
// New returns a new http.Handler serving github-reminder endpoints.
// key should contain the app's private key for authentication.
// secret can be empty or contain the application's secret used for hook authentication.
// You can read more about secret's here: https://developer.github.com/webhooks/#delivery-headers.
func New(appID int, key, secret []byte, transport http.RoundTripper) (http.Handler, error) {
if transport == nil {
transport = http.DefaultTransport
}
s := &server{appID, key, secret, transport}
r := mux.NewRouter()
r.HandleFunc("/hook", s.hookHandler)
r.HandleFunc("/cron", s.cronHandler)
return r, nil
}
func (s *server) cronHandler(w http.ResponseWriter, r *http.Request) {
client, err := reminder.NewApplicationClient(s.appID, s.key, s.transport)
if err != nil {
logrus.Errorf("could not create authenticated client: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
ctx := r.Context()
instIDs, err := client.Installations(ctx)
if err != nil {
logrus.Errorf("could not fetch installations: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
for _, instID := range instIDs {
client, err := reminder.NewInstallationClient(s.appID, instID, s.key, s.transport)
if err != nil {
logrus.Errorf("could not create authenticated client: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
if err = client.UpdateInstallation(ctx); err != nil {
logrus.Errorf("could not update installation: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}
}
func (s *server) hookHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
logrus.Warnf("could not read body: %v", err)
http.Error(w, "could not read body", http.StatusInternalServerError)
return
}
if err := checkSignature(r.Header.Get("X-Hub-Signature"), body, s.secret); err != nil {
logrus.Warnf("bad signature: %v", err)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
inst, owner, name, issue, err := extractIssueInfo(r.Header.Get("X-Github-Event"), body)
if err != nil {
logrus.Warnf("could not extract issue info: %v", err)
http.Error(w, "bad request", http.StatusBadRequest)
return
}
client, err := reminder.NewInstallationClient(s.appID, inst, s.key, s.transport)
if err != nil {
logrus.Errorf("could not create authenticated client: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
if issue == 0 {
logrus.Infof("updating repository %s/%s", owner, name)
err = client.UpdateRepo(r.Context(), owner, name)
} else {
logrus.Infof("updating issue %s/%s#%d", owner, name, issue)
err = client.UpdateIssue(r.Context(), owner, name, issue)
}
if err != nil {
logrus.Errorf("could not update issue: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}
func extractIssueInfo(kind string, body []byte) (inst int, owner, name string, issue int, err error) {
switch kind {
case "issue_comment":
var data github.IssueCommentEvent
if err := json.Unmarshal(body, &data); err != nil {
return 0, "", "", 0, errors.Wrap(err, "could not decode issue comment event")
}
repo := data.GetIssue().GetRepository()
if data.GetIssue().Repository == nil {
repo = data.GetRepo()
}
return data.GetInstallation().GetID(),
repo.GetOwner().GetLogin(),
repo.GetName(),
data.GetIssue().GetNumber(),
nil
case "issues":
var data github.IssuesEvent
if err := json.Unmarshal(body, &data); err != nil {
return 0, "", "", 0, errors.Wrap(err, "could not decode issue event")
}
repo := data.GetIssue().GetRepository()
if data.GetIssue().Repository == nil {
repo = data.GetRepo()
}
return data.GetInstallation().GetID(),
repo.GetOwner().GetLogin(),
repo.GetName(),
data.GetIssue().GetNumber(),
nil
case "pull_request":
var data github.PullRequestEvent
if err := json.Unmarshal(body, &data); err != nil {
return 0, "", "", 0, errors.Wrap(err, "could not decode pull request event")
}
return data.GetInstallation().GetID(),
data.GetPullRequest().GetHead().GetRepo().GetOwner().GetLogin(),
data.GetPullRequest().GetHead().GetRepo().GetName(),
data.GetPullRequest().GetNumber(),
nil
case "label":
var data github.LabelEvent
if err := json.Unmarshal(body, &data); err != nil {
return 0, "", "", 0, errors.Wrap(err, "could not decode label event")
}
return data.GetInstallation().GetID(),
data.GetRepo().GetOwner().GetLogin(),
data.GetRepo().GetName(),
0,
nil
}
return 0, "", "", 0, errors.Errorf("unkown event type %s", kind)
}
func checkSignature(got string, body, secret []byte) error {
if secret == nil {
return nil
}
if !strings.HasPrefix(got, "sha1=") {
return errors.Errorf("unknown hashing algorithm")
}
got = got[5:]
mac := hmac.New(sha1.New, secret)
mac.Write(body)
if wants := hex.EncodeToString(mac.Sum(nil)); wants != got {
return errors.Errorf("wrong signature")
}
return nil
}