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.

293 lines
9.2 KiB

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