From ff8b4c41d078809412ef0425dcb5b21e525d05ff Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Nov 2016 09:55:30 +0000 Subject: [PATCH 1/9] Add stub travis-ci service --- .../go-neb/services/travisci/travisci.go | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/github.com/matrix-org/go-neb/services/travisci/travisci.go diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go new file mode 100644 index 0000000..87d8bc6 --- /dev/null +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -0,0 +1,99 @@ +package travisci + +import ( + "fmt" + "net/http" + "sort" + "strings" + + log "github.com/Sirupsen/logrus" + "github.com/matrix-org/go-neb/database" + "github.com/matrix-org/go-neb/matrix" + "github.com/matrix-org/go-neb/services/github/client" + "github.com/matrix-org/go-neb/services/github/webhook" + "github.com/matrix-org/go-neb/types" + "github.com/matrix-org/go-neb/util" +) + +// ServiceType of the Travis-CI service. +const ServiceType = "travis-ci" + +// Service contains the Config fields for the Travis-CI service. +// +// This service will send notifications into a Matrix room when Travis-CI sends +// webhook events to it. It requires a public domain which Travis-CI can reach. +// Notices will be sent as the service user ID. +// +// Example JSON request: +// { +// rooms: { +// "!ewfug483gsfe:localhost": { +// repos: { +// "matrix-org/go-neb": { +// template: "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}\nBuild details : %{build_url}" +// } +// } +// } +// } +// } +type Service struct { + types.DefaultService + webhookEndpointURL string + // A map from Matrix room ID to Github-style owner/repo repositories. + Rooms map[string]struct { + // A map of "owner/repo" to configuration information + Repos map[string]struct { + // The template string to use when creating notifications. + // + // This is identical to the format of Slack Notifications for Travis-CI: + // https://docs.travis-ci.com/user/notifications#Customizing-slack-notifications + // + // The following variables are available: + // repository_slug: your GitHub repo identifier (like svenfuchs/minimal) + // repository_name: the slug without the username + // build_number: build number + // build_id: build id + // branch: branch build name + // commit: shortened commit SHA + // author: commit author name + // commit_message: commit message of build + // commit_subject: first line of the commit message + // result: result of build + // message: Travis CI message to the build + // duration: total duration of all builds in the matrix + // elapsed_time: time between build start and finish + // compare_url: commit change view URL + // build_url: URL of the build detail + Template string `json:"template"` + } `json:"repos"` + } `json:"rooms"` +} + +// OnReceiveWebhook receives requests from Travis-CI and possibly sends requests to Matrix as a result. +// +// If the "repository.url" matches a known Github repository, a notification will be formed from the +// template for that repository and a notice will be sent to Matrix. +// +// Go-NEB cannot register with Travis-CI for webhooks automatically. The user must manually add the +// webhook endpoint URL to their .travis.yml file: +// notifications: +// webhooks: http://your-domain.com/notifications +// +// See https://docs.travis-ci.com/user/notifications#Webhook-notifications for more information. +func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { + return +} + +// Register makes sure the Config information supplied is valid. +func (s *Service) Register(oldService types.Service, client *matrix.Client) error { + return nil +} + +func init() { + types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service { + return &Service{ + DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType), + webhookEndpointURL: webhookEndpointURL, + } + }) +} From 2b3cbba2e63992de676a2a6fe004b51caa09e5f6 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Nov 2016 10:36:47 +0000 Subject: [PATCH 2/9] Begin to flesh out the Travis-CI service --- .../go-neb/services/travisci/travisci.go | 84 +++++++++++++++++-- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go index 87d8bc6..87658fd 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -3,21 +3,25 @@ package travisci import ( "fmt" "net/http" - "sort" - "strings" + "regexp" log "github.com/Sirupsen/logrus" "github.com/matrix-org/go-neb/database" "github.com/matrix-org/go-neb/matrix" - "github.com/matrix-org/go-neb/services/github/client" - "github.com/matrix-org/go-neb/services/github/webhook" "github.com/matrix-org/go-neb/types" - "github.com/matrix-org/go-neb/util" ) // ServiceType of the Travis-CI service. const ServiceType = "travis-ci" +// DefaultTemplate contains the template that will be used if none is supplied. +// This matches the default mentioned at: https://docs.travis-ci.com/user/notifications#Customizing-slack-notifications +const DefaultTemplate = (`%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message} + Change view : %{compare_url} + Build details : %{build_url}`) + +var ownerRepoRegex = regexp.MustCompile(`^([A-z0-9-_.]+)/([A-z0-9-_.]+)$`) + // Service contains the Config fields for the Travis-CI service. // // This service will send notifications into a Matrix room when Travis-CI sends @@ -69,6 +73,50 @@ type Service struct { } `json:"rooms"` } +// The payload from Travis-CI +type webhookNotification struct { + Number string `json:"number"` + Status string `json:"status"` + StartedAt string `json:"started_at"` + FinishedAt string `json:"finished_at"` + StatusMessage string `json:"status_message"` + Commit string `json:"commit"` + Branch string `json:"branch"` + Message string `json:"message"` + CompareURL string `json:"compare_url"` + CommittedAt string `json:"committed_at"` + CommitterName string `json:"committer_name"` + CommitterEmail string `json:"committer_email"` + AuthorName string `json:"author_name"` + AuthorEmail string `json:"author_email"` + Type string `json:"type"` + BuildURL string `json:"build_url"` + Repository struct { + Name string `json:"name"` + OwnerName string `json:"owner_name"` + URL string `json:"url"` + } `json:"repository"` +} + +// The template variables a user can use in their messages +type notificationTemplate struct { + RepositorySlug string + RepositoryName string + BuildNumber string + BuildID string + Branch string + Commit string + Author string + CommitMessage string + CommitSubject string + Result string + Message string + Duration string + ElapsedTime string + CompareURL string + BuildURL string +} + // OnReceiveWebhook receives requests from Travis-CI and possibly sends requests to Matrix as a result. // // If the "repository.url" matches a known Github repository, a notification will be formed from the @@ -86,9 +134,35 @@ func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli // Register makes sure the Config information supplied is valid. func (s *Service) Register(oldService types.Service, client *matrix.Client) error { + for _, roomData := range s.Rooms { + for repo := range roomData.Repos { + match := ownerRepoRegex.FindStringSubmatch(repo) + if len(match) == 0 { + return fmt.Errorf("Repository '%s' is not a valid repository name.", repo) + } + } + } return nil } +// PostRegister deletes this service if there are no registered repos. +func (s *Service) PostRegister(oldService types.Service) { + for _, roomData := range s.Rooms { + for _ = range roomData.Repos { + return // at least 1 repo exists + } + } + // Delete this service since no repos are configured + logger := log.WithFields(log.Fields{ + "service_type": s.ServiceType(), + "service_id": s.ServiceID(), + }) + logger.Info("Removing service as no repositories are registered.") + if err := database.GetServiceDB().DeleteService(s.ServiceID()); err != nil { + logger.WithError(err).Error("Failed to delete service") + } +} + func init() { types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service { return &Service{ From 6d297f63ad004161151ced4a488ba8e451df2acd Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Nov 2016 11:47:15 +0000 Subject: [PATCH 3/9] Process Travis webhook events --- .../go-neb/services/travisci/travisci.go | 116 +++++++++++++++--- 1 file changed, 99 insertions(+), 17 deletions(-) diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go index 87658fd..3660940 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -1,9 +1,13 @@ package travisci import ( + "encoding/json" "fmt" "net/http" "regexp" + "strconv" + "strings" + "time" log "github.com/Sirupsen/logrus" "github.com/matrix-org/go-neb/database" @@ -75,22 +79,23 @@ type Service struct { // The payload from Travis-CI type webhookNotification struct { - Number string `json:"number"` - Status string `json:"status"` - StartedAt string `json:"started_at"` - FinishedAt string `json:"finished_at"` - StatusMessage string `json:"status_message"` - Commit string `json:"commit"` - Branch string `json:"branch"` - Message string `json:"message"` - CompareURL string `json:"compare_url"` - CommittedAt string `json:"committed_at"` - CommitterName string `json:"committer_name"` - CommitterEmail string `json:"committer_email"` - AuthorName string `json:"author_name"` - AuthorEmail string `json:"author_email"` - Type string `json:"type"` - BuildURL string `json:"build_url"` + ID int `json:"id"` + Number string `json:"number"` + Status *string `json:"status"` + StartedAt *string `json:"started_at"` + FinishedAt *string `json:"finished_at"` + StatusMessage string `json:"status_message"` + Commit string `json:"commit"` + Branch string `json:"branch"` + Message string `json:"message"` + CompareURL string `json:"compare_url"` + CommittedAt string `json:"committed_at"` + CommitterName string `json:"committer_name"` + CommitterEmail string `json:"committer_email"` + AuthorName string `json:"author_name"` + AuthorEmail string `json:"author_email"` + Type string `json:"type"` + BuildURL string `json:"build_url"` Repository struct { Name string `json:"name"` OwnerName string `json:"owner_name"` @@ -117,6 +122,50 @@ type notificationTemplate struct { BuildURL string } +func notifToTemplate(n webhookNotification) (t notificationTemplate) { + t.RepositorySlug = n.Repository.OwnerName + "/" + n.Repository.Name + t.RepositoryName = n.Repository.Name + t.BuildNumber = n.Number + t.BuildID = strconv.Itoa(n.ID) + t.Branch = n.Branch + t.Commit = n.Commit + t.Author = n.CommitterName // author: commit author name + // commit_message: commit message of build + // commit_subject: first line of the commit message + t.CommitMessage = n.Message + subjAndMsg := strings.SplitN(n.Message, "\n", 2) + t.CommitSubject = subjAndMsg[0] + if n.Status != nil { + t.Result = *n.Status + } + t.Message = n.StatusMessage + + if n.StartedAt != nil && n.FinishedAt != nil { + // duration: total duration of all builds in the matrix -- TODO + // elapsed_time: time between build start and finish + // Example from docs: "2011-11-11T11: 11: 11Z" + start, err := time.Parse("2006-01-02T15: 04: 05Z", *n.StartedAt) + finish, err2 := time.Parse("2006-01-02T15: 04: 05Z", *n.FinishedAt) + if err != nil || err2 != nil { + log.WithFields(log.Fields{ + "started_at": *n.StartedAt, + "finished_at": *n.FinishedAt, + }).Warn("Failed to parse Travis-CI start/finish times.") + } else { + t.Duration = finish.Sub(start).String() + t.ElapsedTime = t.Duration + } + } + + t.CompareURL = n.CompareURL + t.BuildURL = n.BuildURL + return +} + +func travisToGoTemplate(travisTmpl string) (goTmpl string) { + return +} + // OnReceiveWebhook receives requests from Travis-CI and possibly sends requests to Matrix as a result. // // If the "repository.url" matches a known Github repository, a notification will be formed from the @@ -129,7 +178,40 @@ type notificationTemplate struct { // // See https://docs.travis-ci.com/user/notifications#Webhook-notifications for more information. func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { - return + var notif webhookNotification + if err := json.NewDecoder(req.Body).Decode(¬if); err != nil { + w.WriteHeader(400) + return + } + if notif.Repository.URL == "" || notif.Repository.OwnerName == "" || notif.Repository.Name == "" { + w.WriteHeader(400) + return + } + whForRepo := notif.Repository.OwnerName + "/" + notif.Repository.Name + // TODO tmplData := notifToTemplate(notif) + + logger := log.WithFields(log.Fields{ + "repo": whForRepo, + }) + + for roomID, roomData := range s.Rooms { + for ownerRepo, repoData := range roomData.Repos { + if ownerRepo != whForRepo { + continue + } + tmpl := travisToGoTemplate(repoData.Template) + msg := tmpl // TODO + + logger.WithFields(log.Fields{ + "msg": msg, + "room_id": roomID, + }).Print("Sending Travis-CI notification to room") + if _, e := cli.SendMessageEvent(roomID, "m.room.message", msg); e != nil { + logger.WithError(e).WithField("room_id", roomID).Print( + "Failed to send Travis-CI notification to room.") + } + } + } } // Register makes sure the Config information supplied is valid. From 5bf126473e38764b355627f4efa555420977a83d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Nov 2016 14:37:59 +0000 Subject: [PATCH 4/9] Verify incoming Travis-CI webhook requests --- .../go-neb/services/travisci/travisci.go | 23 ++++- .../go-neb/services/travisci/verify.go | 95 +++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/github.com/matrix-org/go-neb/services/travisci/verify.go diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go index 3660940..652a458 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -3,6 +3,7 @@ package travisci import ( "encoding/json" "fmt" + "io/ioutil" "net/http" "regexp" "strconv" @@ -24,8 +25,11 @@ const DefaultTemplate = (`%{repository}#%{build_number} (%{branch} - %{commit} : Change view : %{compare_url} Build details : %{build_url}`) +// Matches 'owner/repo' var ownerRepoRegex = regexp.MustCompile(`^([A-z0-9-_.]+)/([A-z0-9-_.]+)$`) +var httpClient = &http.Client{} + // Service contains the Config fields for the Travis-CI service. // // This service will send notifications into a Matrix room when Travis-CI sends @@ -81,7 +85,7 @@ type Service struct { type webhookNotification struct { ID int `json:"id"` Number string `json:"number"` - Status *string `json:"status"` + Status *string `json:"status"` // 0 (success) or 1 (incomplete/fail). StartedAt *string `json:"started_at"` FinishedAt *string `json:"finished_at"` StatusMessage string `json:"status_message"` @@ -178,8 +182,23 @@ func travisToGoTemplate(travisTmpl string) (goTmpl string) { // // See https://docs.travis-ci.com/user/notifications#Webhook-notifications for more information. func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { + payload, err := ioutil.ReadAll(req.Body) + if err != nil { + log.WithError(err).Error("Failed to read incoming Travis-CI webhook") + w.WriteHeader(500) + return + } + if err := verifyOrigin(req.Host, payload, req.Header.Get("Signature")); err != nil { + log.WithFields(log.Fields{ + "Signature": req.Header.Get("Signature"), + log.ErrorKey: err, + }).Warn("Received unauthorised Travis-CI webhook request.") + w.WriteHeader(403) + return + } + var notif webhookNotification - if err := json.NewDecoder(req.Body).Decode(¬if); err != nil { + if err := json.Unmarshal(payload, ¬if); err != nil { w.WriteHeader(400) return } diff --git a/src/github.com/matrix-org/go-neb/services/travisci/verify.go b/src/github.com/matrix-org/go-neb/services/travisci/verify.go new file mode 100644 index 0000000..9454c2b --- /dev/null +++ b/src/github.com/matrix-org/go-neb/services/travisci/verify.go @@ -0,0 +1,95 @@ +package travisci + +import ( + "crypto" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "net/http" + "strings" +) + +// Host => Public Key. +// Travis has a .com and .org with different public keys. +var travisPublicKeyMap = map[string]*rsa.PublicKey{ + "api.travis-ci.org": nil, + "api.travis-ci.com": nil, +} + +func verifyOrigin(host string, payload []byte, sigHeader string) error { + /* + From: https://docs.travis-ci.com/user/notifications#Verifying-Webhook-requests + 1. Pick up the payload data from the HTTP request’s body. + 2. Obtain the Signature header value, and base64-decode it. + 3. Obtain the public key corresponding to the private key that signed the payload. + This is available at the /config endpoint’s config.notifications.webhook.public_key on + the relevant API server. (e.g., https://api.travis-ci.org/config) + 4. Verify the signature using the public key and SHA1 digest. + */ + // 2. Get signature bytes + sig, err := base64.StdEncoding.DecodeString(sigHeader) + if err != nil { + return err + } + + // 3. Get public key + pubKey, validURL := travisPublicKeyMap[host] + if !validURL { + return fmt.Errorf("Invalid host: %s", host) + } + if pubKey == nil { + pemPubKey, err := fetchPEMPublicKey("https://" + host + "/config") + if err != nil { + return err + } + block, _ := pem.Decode([]byte(pemPubKey)) + if block == nil { + return fmt.Errorf("public_key at %s doesn't have a valid PEM block", host) + } + + k, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return err + } + pubKey = k.(*rsa.PublicKey) + travisPublicKeyMap[host] = pubKey + } + + // 4. Verify with SHA1 + h := sha1.New() + h.Write(payload) + digest := h.Sum(nil) + return rsa.VerifyPKCS1v15(pubKey, crypto.SHA1, digest, sig) +} + +func fetchPEMPublicKey(travisURL string) (key string, err error) { + var res *http.Response + res, err = httpClient.Get(travisURL) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return + } + configStruct := struct { + Config struct { + Notifications struct { + Webhook struct { + PublicKey string `json:"public_key"` + } `json:"webhook"` + } `json:"notifications"` + } `json:"config"` + }{} + if err = json.NewDecoder(res.Body).Decode(&configStruct); err != nil { + return + } + key = configStruct.Config.Notifications.Webhook.PublicKey + if key == "" || strings.HasPrefix(key, "-----BEGIN PUBLIC KEY-----") { + err = fmt.Errorf("Couldn't fetch Travis-CI public key. Missing or malformed key.") + } + return +} From 4825c5c4ef982cfdec97cf9ccfb9584f48118be1 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Nov 2016 16:46:02 +0000 Subject: [PATCH 5/9] Begin to add Travis-CI Service tests Currently this verifies the signature logic, but not the templating logic which isn't implemented yet. --- .../go-neb/services/travisci/travisci.go | 29 ++- .../go-neb/services/travisci/travisci_test.go | 178 ++++++++++++++++++ .../go-neb/services/travisci/verify.go | 70 ++++--- 3 files changed, 239 insertions(+), 38 deletions(-) create mode 100644 src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go index 652a458..fa82a5a 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -3,7 +3,6 @@ package travisci import ( "encoding/json" "fmt" - "io/ioutil" "net/http" "regexp" "strconv" @@ -85,7 +84,7 @@ type Service struct { type webhookNotification struct { ID int `json:"id"` Number string `json:"number"` - Status *string `json:"status"` // 0 (success) or 1 (incomplete/fail). + Status *int `json:"status"` // 0 (success) or 1 (incomplete/fail). StartedAt *string `json:"started_at"` FinishedAt *string `json:"finished_at"` StatusMessage string `json:"status_message"` @@ -110,6 +109,7 @@ type webhookNotification struct { // The template variables a user can use in their messages type notificationTemplate struct { RepositorySlug string + Repository string // Deprecated: alias for RepositorySlug RepositoryName string BuildNumber string BuildID string @@ -128,6 +128,7 @@ type notificationTemplate struct { func notifToTemplate(n webhookNotification) (t notificationTemplate) { t.RepositorySlug = n.Repository.OwnerName + "/" + n.Repository.Name + t.Repository = t.RepositorySlug t.RepositoryName = n.Repository.Name t.BuildNumber = n.Number t.BuildID = strconv.Itoa(n.ID) @@ -140,7 +141,7 @@ func notifToTemplate(n webhookNotification) (t notificationTemplate) { subjAndMsg := strings.SplitN(n.Message, "\n", 2) t.CommitSubject = subjAndMsg[0] if n.Status != nil { - t.Result = *n.Status + t.Result = strconv.Itoa(*n.Status) } t.Message = n.StatusMessage @@ -182,13 +183,18 @@ func travisToGoTemplate(travisTmpl string) (goTmpl string) { // // See https://docs.travis-ci.com/user/notifications#Webhook-notifications for more information. func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { - payload, err := ioutil.ReadAll(req.Body) - if err != nil { - log.WithError(err).Error("Failed to read incoming Travis-CI webhook") - w.WriteHeader(500) + if err := req.ParseForm(); err != nil { + log.WithError(err).Error("Failed to read incoming Travis-CI webhook form") + w.WriteHeader(400) + return + } + payload := req.PostFormValue("payload") + if payload == "" { + log.Error("Travis-CI webhook is missing payload= form value") + w.WriteHeader(400) return } - if err := verifyOrigin(req.Host, payload, req.Header.Get("Signature")); err != nil { + if err := verifyOrigin([]byte(payload), req.Header.Get("Signature")); err != nil { log.WithFields(log.Fields{ "Signature": req.Header.Get("Signature"), log.ErrorKey: err, @@ -198,11 +204,13 @@ func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli } var notif webhookNotification - if err := json.Unmarshal(payload, ¬if); err != nil { + if err := json.Unmarshal([]byte(payload), ¬if); err != nil { + log.WithError(err).Error("Travis-CI webhook received an invalid JSON payload=") w.WriteHeader(400) return } - if notif.Repository.URL == "" || notif.Repository.OwnerName == "" || notif.Repository.Name == "" { + if notif.Repository.OwnerName == "" || notif.Repository.Name == "" { + log.WithField("repo", notif.Repository).Error("Travis-CI webhook missing repository fields") w.WriteHeader(400) return } @@ -231,6 +239,7 @@ func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli } } } + w.WriteHeader(200) } // Register makes sure the Config information supplied is valid. diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go new file mode 100644 index 0000000..1848a81 --- /dev/null +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go @@ -0,0 +1,178 @@ +package travisci + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/matrix-org/go-neb/database" + "github.com/matrix-org/go-neb/matrix" + "github.com/matrix-org/go-neb/types" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +const travisOrgPEMPublicKey = (`-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtjdLkS+FP+0fPC09j25 +y/PiuYDDivIT86COVedvlElk99BBYTrqNaJybxjXbIZ1Q6xFNhOY+iTcBr4E1zJu +tizF3Xi0V9tOuP/M8Wn4Y/1lCWbQKlWrNQuqNBmhovF4K3mDCYswVbpgTmp+JQYu +Bm9QMdieZMNry5s6aiMA9aSjDlNyedvSENYo18F+NYg1J0C0JiPYTxheCb4optr1 +5xNzFKhAkuGs4XTOA5C7Q06GCKtDNf44s/CVE30KODUxBi0MCKaxiXw/yy55zxX2 +/YdGphIyQiA5iO1986ZmZCLLW8udz9uhW5jUr3Jlp9LbmphAC61bVSf4ou2YsJaN +0QIDAQAB +-----END PUBLIC KEY-----`) + +const travisComPEMPublicKey = (`-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnQU2j9lnRtyuW36arNOc +dzCzyKVirLUi3/aLh6UfnTVXzTnx8eHUnBn1ZeQl7Eh3J3qqdbIKl6npS27ONzCy +3PIcfjpLPaVyGagIL8c8XgDEvB45AesC0osVP5gkXQkPUM3B2rrUmp1AZzG+Fuo0 +SAeNnS71gN63U3brL9fN/MTCXJJ6TvMt3GrcJUq5uq56qNeJTsiowK6eiiWFUSfh +e1qapOdMFmcEs9J/R1XQ/scxbAnLcWfl8lqH/MjMdCMe0j3X2ZYMTqOHsb3cQGSS +dMPwZGeLWV+OaxjJ7TrJ+riqMANOgqCBGpvWUnUfo046ACOx7p6u4fFc3aRiuqYK +VQIDAQAB +-----END PUBLIC KEY-----`) + +const exampleSignature = ("pW0CDpmcAeWw3qf2Ufx8UvzfrZRUpYx30HBl9nJcDkh2v9FrF1GjJVsrcqx7ly0FPjb7dkfMJ/d0Q3JpDb1EL4p509cN4Vy8+HpfINw35Wg6JqzOQqTa" + + "TidwoDLXo0NgL78zfiL3dra7ZwOGTA+LmnLSuNp38ROxn70u26uqJzWprGSdVNbRu1LkF1QKLa61NZegfxK7RZn1PlIsznWIyTS0qj81mg2sXMDLH1J4" + + "pHxjEpzydjSb5b8tCjrN+vFLDdAtP5RjU8NwvQM4LRRGbLDIlRsO77HDwfXrPgUE3DjPIqVpHhMcCusygp0ClH2b1J1O3LkhxSS9ol5w99Hkpg==") +const exampleBody = ("payload=%7B%22id%22%3A176075135%2C%22repository%22%3A%7B%22id%22%3A6461770%2C%22name%22%3A%22flow-jsdoc%22%2C%22owner_" + + "name%22%3A%22Kegsay%22%2C%22url%22%3Anull%7D%2C%22number%22%3A%2218%22%2C%22config%22%3A%7B%22notifications%22%3A%7B%22web" + + "hooks%22%3A%5B%22http%3A%2F%2F7abbe705.ngrok.io%22%5D%7D%2C%22language%22%3A%22node_js%22%2C%22node_js%22%3A%5B%224.1%22%5D%2C%22.resu" + + "lt%22%3A%22configured%22%2C%22group%22%3A%22stable%22%2C%22dist%22%3A%22precise%22%7D%2C%22status%22%3A0%2C%22result%22%3A0%2C%22status_" + + "message%22%3A%22Passed%22%2C%22result_message%22%3A%22Passed%22%2C%22started_at%22%3A%222016-11-15T15%3A10%3A22Z%22%2C%22finished_" + + "at%22%3A%222016-11-15T15%3A10%3A54Z%22%2C%22duration%22%3A32%2C%22build_url%22%3A%22https%3A%2F%2Ftravis-ci.org%2FKegsay%2Fflow-js" + + "doc%2Fbuilds%2F176075135%22%2C%22commit_id%22%3A50222535%2C%22commit%22%3A%223a092c3a6032ebb50384c99b445f947e9ce86e2a%22%2C%22base_com" + + "mit%22%3Anull%2C%22head_commit%22%3Anull%2C%22branch%22%3A%22master%22%2C%22message%22%3A%22Test+Travis+webhook+support%22%2C%22compare_" + + "url%22%3A%22https%3A%2F%2Fgithub.com%2FKegsay%2Fflow-jsdoc%2Fcompare%2F9f9d459ba082...3a092c3a6032%22%2C%22committed_at%22%3A%222016-1" + + "1-15T15%3A08%3A16Z%22%2C%22author_name%22%3A%22Kegan+Dougal%22%2C%22author_email%22%3A%22kegan%40matrix.org%22%2C%22committer_" + + "name%22%3A%22Kegan+Dougal%22%2C%22committer_email%22%3A%22kegan%40matrix.org%22%2C%22matrix%22%3A%5B%7B%22id%22%3A176075137%2C%22reposit" + + "ory_id%22%3A6461770%2C%22parent_id%22%3A176075135%2C%22number%22%3A%2218.1%22%2C%22state%22%3A%22finished%22%2C%22config%22%3A%7B%22notifi" + + "cations%22%3A%7B%22webhooks%22%3A%5B%22http%3A%2F%2F7abbe705.ngrok.io%22%5D%7D%2C%22language%22%3A%22node_js%22%2C%22node_" + + "js%22%3A%224.1%22%2C%22.result%22%3A%22configured%22%2C%22group%22%3A%22stable%22%2C%22dist%22%3A%22precise%22%2C%22os%22%3A%22li" + + "nux%22%7D%2C%22status%22%3A0%2C%22result%22%3A0%2C%22commit%22%3A%223a092c3a6032ebb50384c99b445f947e9ce86e2a%22%2C%22branch%22%3A%22mas" + + "ter%22%2C%22message%22%3A%22Test+Travis+webhook+support%22%2C%22compare_url%22%3A%22https%3A%2F%2Fgithub.com%2FKegsay%2Fflow-jsdoc%2Fcomp" + + "are%2F9f9d459ba082...3a092c3a6032%22%2C%22started_at%22%3A%222016-11-15T15%3A10%3A22Z%22%2C%22finished_at%22%3A%222016-11-" + + "15T15%3A10%3A54Z%22%2C%22committed_at%22%3A%222016-11-15T15%3A08%3A16Z%22%2C%22author_name%22%3A%22Kegan+Dougal%22%2C%22author_ema" + + "il%22%3A%22kegan%40matrix.org%22%2C%22committer_name%22%3A%22Kegan+Dougal%22%2C%22committer_email%22%3A%22kegan%40matrix.org%22%2C%22allow_f" + + "ailure%22%3Afalse%7D%5D%2C%22type%22%3A%22push%22%2C%22state%22%3A%22passed%22%2C%22pull_request%22%3Afalse%2C%22pull_request_number%22%3Anu" + + "ll%2C%22pull_request_title%22%3Anull%2C%22tag%22%3Anull%7D") + +var travisTests = []struct { + Signature string + ValidSignature bool + Body string + Template string + ExpectedOutput string +}{ + { + exampleSignature, true, exampleBody, + "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}", + "Kegsay/flow-jsdoc#22 master - 3a092c3a6032ebb50384c99b445f947e9ce86e2a : Kegan Dougal: Test Travis webhook support", + }, +} + +type MockTransport struct { + roundTrip func(*http.Request) (*http.Response, error) +} + +func (t MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return t.roundTrip(req) +} + +func TestTravisCI(t *testing.T) { + database.SetServiceDB(&database.NopStorage{}) + + // When the service tries to get Travis' public key, return the constant + urlToKey := make(map[string]string) + urlToKey["https://api.travis-ci.org/config"] = travisOrgPEMPublicKey + urlToKey["https://api.travis-ci.com/config"] = travisComPEMPublicKey + travisTransport := struct{ MockTransport }{} + travisTransport.roundTrip = func(req *http.Request) (*http.Response, error) { + if key := urlToKey[req.URL.String()]; key != "" { + escKey, _ := json.Marshal(key) + return &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString( + `{"config":{"notifications":{"webhook":{"public_key":` + string(escKey) + `}}}}`, + )), + }, nil + } + return nil, fmt.Errorf("Unhandled URL %s", req.URL.String()) + } + // clobber the http client that the service uses to talk to Travis + httpClient = &http.Client{Transport: travisTransport} + + // Intercept message sending to Matrix and mock responses + msgs := []matrix.HTMLMessage{} + matrixTrans := struct{ MockTransport }{} + matrixTrans.roundTrip = func(req *http.Request) (*http.Response, error) { + if !strings.Contains(req.URL.String(), "/send/m.room.message") { + return nil, fmt.Errorf("Unhandled URL: %s", req.URL.String()) + } + var msg matrix.HTMLMessage + if err := json.NewDecoder(req.Body).Decode(&msg); err != nil { + return nil, err + } + msgs = append(msgs, msg) + return &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString(`{"event_id":"$yup:event"}`)), + }, nil + } + u, _ := url.Parse("https://hyrule") + matrixCli := matrix.NewClient(&http.Client{Transport: matrixTrans}, u, "its_a_secret", "@travisci:hyrule") + + // BEGIN running the Travis-CI table tests + // --------------------------------------- + for _, test := range travisTests { + msgs = []matrix.HTMLMessage{} // reset sent messages + mockWriter := httptest.NewRecorder() + travis := makeService(t, test.Template) + if travis == nil { + t.Error("TestTravisCI Failed to create service") + continue + } + if err := travis.Register(nil, matrixCli); err != nil { + t.Errorf("TestTravisCI Failed to Register(): %s", err) + continue + } + req, err := http.NewRequest( + "POST", "https://neb.endpoint/travis-ci-service", bytes.NewBufferString(test.Body), + ) + if err != nil { + t.Errorf("TestTravisCI Failed to create webhook request: %s", err) + continue + } + req.Header.Set("Signature", test.Signature) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + travis.OnReceiveWebhook(mockWriter, req, matrixCli) + + if mockWriter.Code != 200 { + t.Errorf("TestTravisCI OnReceiveWebhook want code %d, got %d", 200, mockWriter.Code) + } + } +} + +func makeService(t *testing.T, template string) *Service { + srv, err := types.CreateService("id", ServiceType, "@travisci:hyrule", []byte( + `{ + "rooms":{ + "!ewfug483gsfe:localhost": { + "repos": { + "Kegsay/flow-jsdoc": { + "template": "`+template+`" + } + } + } + } + }`, + )) + if err != nil { + t.Error("Failed to create Travis-CI service: ", err) + return nil + } + return srv.(*Service) +} diff --git a/src/github.com/matrix-org/go-neb/services/travisci/verify.go b/src/github.com/matrix-org/go-neb/services/travisci/verify.go index 9454c2b..dfde078 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/verify.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/verify.go @@ -15,12 +15,13 @@ import ( // Host => Public Key. // Travis has a .com and .org with different public keys. +// .org is the public one and is one we will try first, then .com var travisPublicKeyMap = map[string]*rsa.PublicKey{ "api.travis-ci.org": nil, "api.travis-ci.com": nil, } -func verifyOrigin(host string, payload []byte, sigHeader string) error { +func verifyOrigin(payload []byte, sigHeader string) error { /* From: https://docs.travis-ci.com/user/notifications#Verifying-Webhook-requests 1. Pick up the payload data from the HTTP request’s body. @@ -30,40 +31,53 @@ func verifyOrigin(host string, payload []byte, sigHeader string) error { the relevant API server. (e.g., https://api.travis-ci.org/config) 4. Verify the signature using the public key and SHA1 digest. */ - // 2. Get signature bytes sig, err := base64.StdEncoding.DecodeString(sigHeader) if err != nil { - return err + return fmt.Errorf("verifyOrigin: Failed to decode signature as base64: %s", err) } - // 3. Get public key - pubKey, validURL := travisPublicKeyMap[host] - if !validURL { - return fmt.Errorf("Invalid host: %s", host) + if err := loadPublicKeys(); err != nil { + return fmt.Errorf("verifyOrigin: Failed to cache Travis public keys: %s", err) } - if pubKey == nil { - pemPubKey, err := fetchPEMPublicKey("https://" + host + "/config") - if err != nil { - return err - } - block, _ := pem.Decode([]byte(pemPubKey)) - if block == nil { - return fmt.Errorf("public_key at %s doesn't have a valid PEM block", host) - } - k, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return err + // 4. Verify with SHA1 + // NB: We don't know who sent this request (no Referer header or anything) so we need to try + // both public keys at both endpoints. We use the .org one first since it's more popular. + var verifyErr error + for _, host := range []string{"api.travis-ci.org", "api.travis-ci.com"} { + h := sha1.New() + h.Write(payload) + digest := h.Sum(nil) + verifyErr = rsa.VerifyPKCS1v15(travisPublicKeyMap[host], crypto.SHA1, digest, sig) + if verifyErr == nil { + return nil // Valid for this key } - pubKey = k.(*rsa.PublicKey) - travisPublicKeyMap[host] = pubKey } + return fmt.Errorf("verifyOrigin: Signature verification failed: %s", verifyErr) +} - // 4. Verify with SHA1 - h := sha1.New() - h.Write(payload) - digest := h.Sum(nil) - return rsa.VerifyPKCS1v15(pubKey, crypto.SHA1, digest, sig) +func loadPublicKeys() error { + for _, host := range []string{"api.travis-ci.com", "api.travis-ci.org"} { + pubKey := travisPublicKeyMap[host] + if pubKey == nil { + pemPubKey, err := fetchPEMPublicKey("https://" + host + "/config") + if err != nil { + return err + } + block, _ := pem.Decode([]byte(pemPubKey)) + if block == nil { + return fmt.Errorf("public_key at %s doesn't have a valid PEM block", host) + } + + k, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return err + } + pubKey = k.(*rsa.PublicKey) + travisPublicKeyMap[host] = pubKey + } + } + return nil } func fetchPEMPublicKey(travisURL string) (key string, err error) { @@ -88,8 +102,8 @@ func fetchPEMPublicKey(travisURL string) (key string, err error) { return } key = configStruct.Config.Notifications.Webhook.PublicKey - if key == "" || strings.HasPrefix(key, "-----BEGIN PUBLIC KEY-----") { - err = fmt.Errorf("Couldn't fetch Travis-CI public key. Missing or malformed key.") + if key == "" || !strings.HasPrefix(key, "-----BEGIN PUBLIC KEY-----") { + err = fmt.Errorf("Couldn't fetch Travis-CI public key. Missing or malformed key: %s", key) } return } From 3a8b4a6aca4de0361e5bc301cc4afd6300d616f1 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 16 Nov 2016 11:51:58 +0000 Subject: [PATCH 6/9] Add working test and implement template parsing --- .../go-neb/services/travisci/travisci.go | 74 ++++++++----------- .../go-neb/services/travisci/travisci_test.go | 24 ++++-- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go index fa82a5a..7391268 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -106,44 +106,26 @@ type webhookNotification struct { } `json:"repository"` } -// The template variables a user can use in their messages -type notificationTemplate struct { - RepositorySlug string - Repository string // Deprecated: alias for RepositorySlug - RepositoryName string - BuildNumber string - BuildID string - Branch string - Commit string - Author string - CommitMessage string - CommitSubject string - Result string - Message string - Duration string - ElapsedTime string - CompareURL string - BuildURL string -} - -func notifToTemplate(n webhookNotification) (t notificationTemplate) { - t.RepositorySlug = n.Repository.OwnerName + "/" + n.Repository.Name - t.Repository = t.RepositorySlug - t.RepositoryName = n.Repository.Name - t.BuildNumber = n.Number - t.BuildID = strconv.Itoa(n.ID) - t.Branch = n.Branch - t.Commit = n.Commit - t.Author = n.CommitterName // author: commit author name +// Converts a webhook notification into a map of template var name to value +func notifToTemplate(n webhookNotification) map[string]string { + t := make(map[string]string) + t["repository_slug"] = n.Repository.OwnerName + "/" + n.Repository.Name + t["repository"] = t["repository_slug"] // Deprecated form but still used everywhere in people's templates + t["repository_name"] = n.Repository.Name + t["build_number"] = n.Number + t["build_id"] = strconv.Itoa(n.ID) + t["branch"] = n.Branch + t["commit"] = n.Commit + t["author"] = n.CommitterName // author: commit author name // commit_message: commit message of build // commit_subject: first line of the commit message - t.CommitMessage = n.Message + t["commit_message"] = n.Message subjAndMsg := strings.SplitN(n.Message, "\n", 2) - t.CommitSubject = subjAndMsg[0] + t["commit_subject"] = subjAndMsg[0] if n.Status != nil { - t.Result = strconv.Itoa(*n.Status) + t["result"] = strconv.Itoa(*n.Status) } - t.Message = n.StatusMessage + t["message"] = n.StatusMessage if n.StartedAt != nil && n.FinishedAt != nil { // duration: total duration of all builds in the matrix -- TODO @@ -157,18 +139,22 @@ func notifToTemplate(n webhookNotification) (t notificationTemplate) { "finished_at": *n.FinishedAt, }).Warn("Failed to parse Travis-CI start/finish times.") } else { - t.Duration = finish.Sub(start).String() - t.ElapsedTime = t.Duration + t["duration"] = finish.Sub(start).String() + t["elapsed_time"] = t["duration"] } } - t.CompareURL = n.CompareURL - t.BuildURL = n.BuildURL - return + t["compare_url"] = n.CompareURL + t["build_url"] = n.BuildURL + return t } -func travisToGoTemplate(travisTmpl string) (goTmpl string) { - return +func outputForTemplate(travisTmpl string, tmpl map[string]string) (out string) { + out = travisTmpl + for tmplVar, tmplValue := range tmpl { + out = strings.Replace(out, "%{"+tmplVar+"}", tmplValue, -1) + } + return out } // OnReceiveWebhook receives requests from Travis-CI and possibly sends requests to Matrix as a result. @@ -215,7 +201,7 @@ func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli return } whForRepo := notif.Repository.OwnerName + "/" + notif.Repository.Name - // TODO tmplData := notifToTemplate(notif) + tmplData := notifToTemplate(notif) logger := log.WithFields(log.Fields{ "repo": whForRepo, @@ -226,8 +212,10 @@ func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli if ownerRepo != whForRepo { continue } - tmpl := travisToGoTemplate(repoData.Template) - msg := tmpl // TODO + msg := matrix.TextMessage{ + Body: outputForTemplate(repoData.Template, tmplData), + MsgType: "m.notice", + } logger.WithFields(log.Fields{ "msg": msg, diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go index 1848a81..0affa2f 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go @@ -4,15 +4,16 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/matrix-org/go-neb/database" - "github.com/matrix-org/go-neb/matrix" - "github.com/matrix-org/go-neb/types" "io/ioutil" "net/http" "net/http/httptest" "net/url" "strings" "testing" + + "github.com/matrix-org/go-neb/database" + "github.com/matrix-org/go-neb/matrix" + "github.com/matrix-org/go-neb/types" ) const travisOrgPEMPublicKey = (`-----BEGIN PUBLIC KEY----- @@ -70,7 +71,7 @@ var travisTests = []struct { { exampleSignature, true, exampleBody, "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}", - "Kegsay/flow-jsdoc#22 master - 3a092c3a6032ebb50384c99b445f947e9ce86e2a : Kegan Dougal: Test Travis webhook support", + "Kegsay/flow-jsdoc#18 (master - 3a092c3a6032ebb50384c99b445f947e9ce86e2a : Kegan Dougal): Passed", }, } @@ -106,15 +107,15 @@ func TestTravisCI(t *testing.T) { httpClient = &http.Client{Transport: travisTransport} // Intercept message sending to Matrix and mock responses - msgs := []matrix.HTMLMessage{} + msgs := []matrix.TextMessage{} matrixTrans := struct{ MockTransport }{} matrixTrans.roundTrip = func(req *http.Request) (*http.Response, error) { if !strings.Contains(req.URL.String(), "/send/m.room.message") { return nil, fmt.Errorf("Unhandled URL: %s", req.URL.String()) } - var msg matrix.HTMLMessage + var msg matrix.TextMessage if err := json.NewDecoder(req.Body).Decode(&msg); err != nil { - return nil, err + return nil, fmt.Errorf("Failed to decode request JSON: %s", err) } msgs = append(msgs, msg) return &http.Response{ @@ -128,7 +129,7 @@ func TestTravisCI(t *testing.T) { // BEGIN running the Travis-CI table tests // --------------------------------------- for _, test := range travisTests { - msgs = []matrix.HTMLMessage{} // reset sent messages + msgs = []matrix.TextMessage{} // reset sent messages mockWriter := httptest.NewRecorder() travis := makeService(t, test.Template) if travis == nil { @@ -153,6 +154,13 @@ func TestTravisCI(t *testing.T) { if mockWriter.Code != 200 { t.Errorf("TestTravisCI OnReceiveWebhook want code %d, got %d", 200, mockWriter.Code) } + if len(msgs) != 1 { + t.Errorf("TestTravisCI want sent messages %d, got %d ", 1, len(msgs)) + continue + } + if msgs[0].Body != test.ExpectedOutput { + t.Errorf("TestTravisCI want matrix body '%s', got '%s'", test.ExpectedOutput, msgs[0].Body) + } } } From 7f8c36ba89534bdcf517076a4da0e9afd0c131b3 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 16 Nov 2016 13:09:17 +0000 Subject: [PATCH 7/9] Add a few more tests --- .../go-neb/services/travisci/travisci.go | 2 +- .../go-neb/services/travisci/travisci_test.go | 42 +++++++++++++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go index 7391268..9fc4fed 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -125,7 +125,7 @@ func notifToTemplate(n webhookNotification) map[string]string { if n.Status != nil { t["result"] = strconv.Itoa(*n.Status) } - t["message"] = n.StatusMessage + t["message"] = n.StatusMessage // message: Travis CI message to the build if n.StartedAt != nil && n.FinishedAt != nil { // duration: total duration of all builds in the matrix -- TODO diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go index 0affa2f..81d17b7 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go @@ -73,6 +73,17 @@ var travisTests = []struct { "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}", "Kegsay/flow-jsdoc#18 (master - 3a092c3a6032ebb50384c99b445f947e9ce86e2a : Kegan Dougal): Passed", }, + { + "obviously_invalid_signature", false, exampleBody, + "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}", + "Kegsay/flow-jsdoc#18 (master - 3a092c3a6032ebb50384c99b445f947e9ce86e2a : Kegan Dougal): Passed", + }, + { + // Payload is valid but doesn't match signature now + exampleSignature, false, strings.TrimSuffix(exampleBody, "%7D") + "%2C%22EXTRA_KEY%22%3Anull%7D", + "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}", + "Kegsay/flow-jsdoc#18 (master - 3a092c3a6032ebb50384c99b445f947e9ce86e2a : Kegan Dougal): Passed", + }, } type MockTransport struct { @@ -151,19 +162,32 @@ func TestTravisCI(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") travis.OnReceiveWebhook(mockWriter, req, matrixCli) - if mockWriter.Code != 200 { - t.Errorf("TestTravisCI OnReceiveWebhook want code %d, got %d", 200, mockWriter.Code) - } - if len(msgs) != 1 { - t.Errorf("TestTravisCI want sent messages %d, got %d ", 1, len(msgs)) - continue - } - if msgs[0].Body != test.ExpectedOutput { - t.Errorf("TestTravisCI want matrix body '%s', got '%s'", test.ExpectedOutput, msgs[0].Body) + if test.ValidSignature { + if !assertResponse(t, mockWriter, msgs, 200, 1) { + continue + } + + if msgs[0].Body != test.ExpectedOutput { + t.Errorf("TestTravisCI want matrix body '%s', got '%s'", test.ExpectedOutput, msgs[0].Body) + } + } else { + assertResponse(t, mockWriter, msgs, 403, 0) } } } +func assertResponse(t *testing.T, w *httptest.ResponseRecorder, msgs []matrix.TextMessage, expectCode int, expectMsgLength int) bool { + if w.Code != expectCode { + t.Errorf("TestTravisCI OnReceiveWebhook want HTTP code %d, got %d", expectCode, w.Code) + return false + } + if len(msgs) != expectMsgLength { + t.Errorf("TestTravisCI want %d sent messages, got %d ", expectMsgLength, len(msgs)) + return false + } + return true +} + func makeService(t *testing.T, template string) *Service { srv, err := types.CreateService("id", ServiceType, "@travisci:hyrule", []byte( `{ From be52bdedf11f181d6532f2ee6c8023612bdbe31b Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 16 Nov 2016 13:21:39 +0000 Subject: [PATCH 8/9] Clarify docs --- .../matrix-org/go-neb/services/travisci/travisci.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go index 9fc4fed..ba78c59 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -1,3 +1,4 @@ +// Package travisci implements a Service capable of processing webhooks from Travis-CI. package travisci import ( @@ -49,7 +50,8 @@ var httpClient = &http.Client{} // } type Service struct { types.DefaultService - webhookEndpointURL string + // The URL which should be added to .travis.yml - Populated by Go-NEB after Service registration. + WebhookEndpointURL string // A map from Matrix room ID to Github-style owner/repo repositories. Rooms map[string]struct { // A map of "owner/repo" to configuration information @@ -159,13 +161,13 @@ func outputForTemplate(travisTmpl string, tmpl map[string]string) (out string) { // OnReceiveWebhook receives requests from Travis-CI and possibly sends requests to Matrix as a result. // -// If the "repository.url" matches a known Github repository, a notification will be formed from the +// If the repository matches a known Github repository, a notification will be formed from the // template for that repository and a notice will be sent to Matrix. // // Go-NEB cannot register with Travis-CI for webhooks automatically. The user must manually add the // webhook endpoint URL to their .travis.yml file: // notifications: -// webhooks: http://your-domain.com/notifications +// webhooks: http://go-neb-endpoint.com/notifications // // See https://docs.travis-ci.com/user/notifications#Webhook-notifications for more information. func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { @@ -265,7 +267,7 @@ func init() { types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service { return &Service{ DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType), - webhookEndpointURL: webhookEndpointURL, + WebhookEndpointURL: webhookEndpointURL, } }) } From 1d23f6bd94e637e36f6992ae2fe7eb8b3aeae214 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 16 Nov 2016 13:42:20 +0000 Subject: [PATCH 9/9] Actually use the default and enable travisci service --- src/github.com/matrix-org/go-neb/goneb.go | 1 + src/github.com/matrix-org/go-neb/services/travisci/travisci.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/github.com/matrix-org/go-neb/goneb.go b/src/github.com/matrix-org/go-neb/goneb.go index d881aca..79ac3e4 100644 --- a/src/github.com/matrix-org/go-neb/goneb.go +++ b/src/github.com/matrix-org/go-neb/goneb.go @@ -26,6 +26,7 @@ import ( _ "github.com/matrix-org/go-neb/services/guggy" _ "github.com/matrix-org/go-neb/services/jira" _ "github.com/matrix-org/go-neb/services/rssbot" + _ "github.com/matrix-org/go-neb/services/travisci" "github.com/matrix-org/go-neb/types" _ "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus" diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go index ba78c59..8185794 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -152,6 +152,9 @@ func notifToTemplate(n webhookNotification) map[string]string { } func outputForTemplate(travisTmpl string, tmpl map[string]string) (out string) { + if travisTmpl == "" { + travisTmpl = DefaultTemplate + } out = travisTmpl for tmplVar, tmplValue := range tmpl { out = strings.Replace(out, "%{"+tmplVar+"}", tmplValue, -1)