You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

286 lines
9.0 KiB

8 years ago
8 years ago
  1. package webhook
  2. import (
  3. "crypto/hmac"
  4. "crypto/sha1"
  5. "encoding/hex"
  6. "encoding/json"
  7. "fmt"
  8. "html"
  9. "io/ioutil"
  10. "net/http"
  11. "strings"
  12. "github.com/google/go-github/github"
  13. "github.com/matrix-org/gomatrix"
  14. "github.com/matrix-org/util"
  15. log "github.com/sirupsen/logrus"
  16. )
  17. // OnReceiveRequest processes incoming github webhook requests and returns a
  18. // matrix message to send, along with parsed repo information.
  19. // The secretToken, if supplied, will be used to verify the request is from
  20. // Github. If it isn't, an error is returned.
  21. func OnReceiveRequest(r *http.Request, secretToken string) (string, *github.Repository, *gomatrix.HTMLMessage, *util.JSONResponse) {
  22. // Verify the HMAC signature if NEB was configured with a secret token
  23. eventType := r.Header.Get("X-GitHub-Event")
  24. signatureSHA1 := r.Header.Get("X-Hub-Signature")
  25. content, err := ioutil.ReadAll(r.Body)
  26. if err != nil {
  27. log.WithError(err).Print("Failed to read Github webhook body")
  28. resErr := util.MessageResponse(400, "Failed to parse body")
  29. return "", nil, nil, &resErr
  30. }
  31. // Verify request if a secret token has been supplied.
  32. if secretToken != "" {
  33. sigHex := strings.Split(signatureSHA1, "=")[1]
  34. var sigBytes []byte
  35. sigBytes, err = hex.DecodeString(sigHex)
  36. if err != nil {
  37. log.WithError(err).WithField("X-Hub-Signature", sigHex).Print(
  38. "Failed to decode signature as hex.")
  39. resErr := util.MessageResponse(400, "Failed to decode signature")
  40. return "", nil, nil, &resErr
  41. }
  42. if !checkMAC([]byte(content), sigBytes, []byte(secretToken)) {
  43. log.WithFields(log.Fields{
  44. "X-Hub-Signature": signatureSHA1,
  45. }).Print("Received Github event which failed MAC check.")
  46. resErr := util.MessageResponse(403, "Bad signature")
  47. return "", nil, nil, &resErr
  48. }
  49. }
  50. log.WithFields(log.Fields{
  51. "event_type": eventType,
  52. "signature": signatureSHA1,
  53. }).Print("Received Github event")
  54. if eventType == "ping" {
  55. // Github will send a "ping" event when the webhook is first created. We need
  56. // to return a 200 in order for the webhook to be marked as "up" (this doesn't
  57. // affect delivery, just the tick/cross status flag).
  58. res := util.MessageResponse(200, "pong")
  59. return "", nil, nil, &res
  60. }
  61. htmlStr, repo, refinedType, err := parseGithubEvent(eventType, content)
  62. if err != nil {
  63. log.WithError(err).Print("Failed to parse github event")
  64. resErr := util.MessageResponse(500, "Failed to parse github event")
  65. return "", nil, nil, &resErr
  66. }
  67. msg := gomatrix.GetHTMLMessage("m.notice", htmlStr)
  68. return refinedType, repo, &msg, nil
  69. }
  70. // checkMAC reports whether messageMAC is a valid HMAC tag for message.
  71. func checkMAC(message, messageMAC, key []byte) bool {
  72. mac := hmac.New(sha1.New, key)
  73. mac.Write(message)
  74. expectedMAC := mac.Sum(nil)
  75. return hmac.Equal(messageMAC, expectedMAC)
  76. }
  77. // parseGithubEvent parses a github event type and JSON data and returns an explanatory
  78. // HTML string, the github repository and the refined event type, or an error.
  79. func parseGithubEvent(eventType string, data []byte) (string, *github.Repository, string, error) {
  80. if eventType == "pull_request" {
  81. var ev github.PullRequestEvent
  82. if err := json.Unmarshal(data, &ev); err != nil {
  83. return "", nil, eventType, err
  84. }
  85. refinedEventType := refineEventType(eventType, ev.Action)
  86. return pullRequestHTMLMessage(ev), ev.Repo, refinedEventType, nil
  87. } else if eventType == "issues" {
  88. var ev github.IssuesEvent
  89. if err := json.Unmarshal(data, &ev); err != nil {
  90. return "", nil, eventType, err
  91. }
  92. refinedEventType := refineEventType(eventType, ev.Action)
  93. return issueHTMLMessage(ev), ev.Repo, refinedEventType, nil
  94. } else if eventType == "push" {
  95. var ev github.PushEvent
  96. if err := json.Unmarshal(data, &ev); err != nil {
  97. return "", nil, eventType, err
  98. }
  99. // The 'push' event repository format is subtly different from normal, so munge the bits we need.
  100. fullName := *ev.Repo.Owner.Name + "/" + *ev.Repo.Name
  101. repo := github.Repository{
  102. Owner: &github.User{
  103. Login: ev.Repo.Owner.Name,
  104. },
  105. Name: ev.Repo.Name,
  106. FullName: &fullName,
  107. }
  108. return pushHTMLMessage(ev), &repo, eventType, nil
  109. } else if eventType == "issue_comment" {
  110. var ev github.IssueCommentEvent
  111. if err := json.Unmarshal(data, &ev); err != nil {
  112. return "", nil, eventType, err
  113. }
  114. return issueCommentHTMLMessage(ev), ev.Repo, eventType, nil
  115. } else if eventType == "pull_request_review_comment" {
  116. var ev github.PullRequestReviewCommentEvent
  117. if err := json.Unmarshal(data, &ev); err != nil {
  118. return "", nil, eventType, err
  119. }
  120. return prReviewCommentHTMLMessage(ev), ev.Repo, eventType, nil
  121. }
  122. return "", nil, eventType, fmt.Errorf("Unrecognized event type")
  123. }
  124. func refineEventType(eventType string, action *string) string {
  125. if action == nil {
  126. return eventType
  127. }
  128. a := *action
  129. if a == "assigned" || a == "unassigned" {
  130. return "assignments"
  131. } else if a == "milestoned" || a == "demilestoned" {
  132. return "milestones"
  133. } else if a == "labeled" || a == "unlabeled" {
  134. return "labels"
  135. }
  136. return eventType
  137. }
  138. func pullRequestHTMLMessage(p github.PullRequestEvent) string {
  139. var actionTarget string
  140. if p.PullRequest.Assignee != nil && p.PullRequest.Assignee.Login != nil {
  141. actionTarget = fmt.Sprintf(" to %s", *p.PullRequest.Assignee.Login)
  142. }
  143. return fmt.Sprintf(
  144. "[<u>%s</u>] %s %s <b>pull request #%d</b>: %s [%s]%s - %s",
  145. html.EscapeString(*p.Repo.FullName),
  146. html.EscapeString(*p.Sender.Login),
  147. html.EscapeString(*p.Action),
  148. *p.Number,
  149. html.EscapeString(*p.PullRequest.Title),
  150. html.EscapeString(*p.PullRequest.State),
  151. html.EscapeString(actionTarget),
  152. html.EscapeString(*p.PullRequest.HTMLURL),
  153. )
  154. }
  155. func issueHTMLMessage(p github.IssuesEvent) string {
  156. var actionTarget string
  157. if p.Issue.Assignee != nil && p.Issue.Assignee.Login != nil {
  158. actionTarget = fmt.Sprintf(" to %s", *p.Issue.Assignee.Login)
  159. }
  160. action := html.EscapeString(*p.Action)
  161. if p.Label != nil && (*p.Action == "labeled" || *p.Action == "unlabeled") {
  162. action = *p.Action + " [" + html.EscapeString(*p.Label.Name) + "] to"
  163. }
  164. return fmt.Sprintf(
  165. "[<u>%s</u>] %s %s <b>issue #%d</b>: %s [%s]%s - %s",
  166. html.EscapeString(*p.Repo.FullName),
  167. html.EscapeString(*p.Sender.Login),
  168. action,
  169. *p.Issue.Number,
  170. html.EscapeString(*p.Issue.Title),
  171. html.EscapeString(*p.Issue.State),
  172. html.EscapeString(actionTarget),
  173. html.EscapeString(*p.Issue.HTMLURL),
  174. )
  175. }
  176. func issueCommentHTMLMessage(p github.IssueCommentEvent) string {
  177. var kind string
  178. if p.Issue.PullRequestLinks == nil {
  179. kind = "issue"
  180. } else {
  181. kind = "pull request"
  182. }
  183. return fmt.Sprintf(
  184. "[<u>%s</u>] %s commented on %s's <b>%s #%d</b>: %s - %s",
  185. html.EscapeString(*p.Repo.FullName),
  186. html.EscapeString(*p.Comment.User.Login),
  187. html.EscapeString(*p.Issue.User.Login),
  188. kind,
  189. *p.Issue.Number,
  190. html.EscapeString(*p.Issue.Title),
  191. html.EscapeString(*p.Issue.HTMLURL),
  192. )
  193. }
  194. func prReviewCommentHTMLMessage(p github.PullRequestReviewCommentEvent) string {
  195. assignee := "None"
  196. if p.PullRequest.Assignee != nil {
  197. assignee = html.EscapeString(*p.PullRequest.Assignee.Login)
  198. }
  199. return fmt.Sprintf(
  200. "[<u>%s</u>] %s made a line comment on %s's <b>pull request #%d</b> (assignee: %s): %s - %s",
  201. html.EscapeString(*p.Repo.FullName),
  202. html.EscapeString(*p.Sender.Login),
  203. html.EscapeString(*p.PullRequest.User.Login),
  204. *p.PullRequest.Number,
  205. assignee,
  206. html.EscapeString(*p.PullRequest.Title),
  207. html.EscapeString(*p.Comment.HTMLURL),
  208. )
  209. }
  210. func pushHTMLMessage(p github.PushEvent) string {
  211. // /refs/heads/alice/branch-name => alice/branch-name
  212. branch := strings.Replace(*p.Ref, "refs/heads/", "", -1)
  213. // this branch was deleted, no HeadCommit object and deleted=true
  214. if p.HeadCommit == nil && p.Deleted != nil && *p.Deleted {
  215. return fmt.Sprintf(
  216. `[<u>%s</u>] %s <b><font color="red">deleted</font> %s</b>`,
  217. html.EscapeString(*p.Repo.FullName),
  218. html.EscapeString(*p.Pusher.Name),
  219. html.EscapeString(branch),
  220. )
  221. }
  222. if p.Commits != nil && len(p.Commits) > 1 {
  223. // multi-commit message
  224. // [<repo>] <username> pushed <num> commits to <branch>: <git.io link>
  225. // <up to 3 commits>
  226. var cList []string
  227. for _, c := range p.Commits {
  228. cList = append(cList, fmt.Sprintf(
  229. `%s: %s`,
  230. html.EscapeString(nameForAuthor(c.Author)),
  231. html.EscapeString(*c.Message),
  232. ))
  233. }
  234. return fmt.Sprintf(
  235. `[<u>%s</u>] %s pushed %d commits to <b>%s</b>: %s<br>%s`,
  236. html.EscapeString(*p.Repo.FullName),
  237. html.EscapeString(nameForAuthor(p.HeadCommit.Committer)),
  238. len(p.Commits),
  239. html.EscapeString(branch),
  240. html.EscapeString(*p.HeadCommit.URL),
  241. strings.Join(cList, "<br>"),
  242. )
  243. }
  244. // single commit message
  245. // [<repo>] <username> pushed to <branch>: <msg> - <git.io link>
  246. return fmt.Sprintf(
  247. `[<u>%s</u>] %s pushed to <b>%s</b>: %s - %s`,
  248. html.EscapeString(*p.Repo.FullName),
  249. html.EscapeString(nameForAuthor(p.HeadCommit.Committer)),
  250. html.EscapeString(branch),
  251. html.EscapeString(*p.HeadCommit.Message),
  252. html.EscapeString(*p.HeadCommit.URL),
  253. )
  254. }
  255. func nameForAuthor(a *github.CommitAuthor) string {
  256. if a == nil {
  257. return ""
  258. }
  259. if a.Login != nil { // prefer to use their GH username than the name they commited as
  260. return *a.Login
  261. }
  262. return *a.Name
  263. }