mirror of https://github.com/matrix-org/go-neb.git
Browse Source
Merge pull request #121 from matrix-org/kegan/travis
Merge pull request #121 from matrix-org/kegan/travis
Implement Travis-CI Servicekegan/text-logging
Kegsay
8 years ago
committed by
GitHub
4 changed files with 596 additions and 0 deletions
-
1src/github.com/matrix-org/go-neb/goneb.go
-
276src/github.com/matrix-org/go-neb/services/travisci/travisci.go
-
210src/github.com/matrix-org/go-neb/services/travisci/travisci_test.go
-
109src/github.com/matrix-org/go-neb/services/travisci/verify.go
@ -0,0 +1,276 @@ |
|||
// Package travisci implements a Service capable of processing webhooks from Travis-CI.
|
|||
package travisci |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"fmt" |
|||
"net/http" |
|||
"regexp" |
|||
"strconv" |
|||
"strings" |
|||
"time" |
|||
|
|||
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/types" |
|||
) |
|||
|
|||
// 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}`) |
|||
|
|||
// 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
|
|||
// 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 |
|||
// 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
|
|||
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"` |
|||
} |
|||
|
|||
// The payload from Travis-CI
|
|||
type webhookNotification struct { |
|||
ID int `json:"id"` |
|||
Number string `json:"number"` |
|||
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"` |
|||
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"` |
|||
} |
|||
|
|||
// 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["commit_message"] = n.Message |
|||
subjAndMsg := strings.SplitN(n.Message, "\n", 2) |
|||
t["commit_subject"] = subjAndMsg[0] |
|||
if n.Status != nil { |
|||
t["result"] = strconv.Itoa(*n.Status) |
|||
} |
|||
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
|
|||
// 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["elapsed_time"] = t["duration"] |
|||
} |
|||
} |
|||
|
|||
t["compare_url"] = n.CompareURL |
|||
t["build_url"] = n.BuildURL |
|||
return t |
|||
} |
|||
|
|||
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) |
|||
} |
|||
return out |
|||
} |
|||
|
|||
// OnReceiveWebhook receives requests from Travis-CI and possibly sends requests to Matrix as a result.
|
|||
//
|
|||
// 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://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) { |
|||
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([]byte(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.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.OwnerName == "" || notif.Repository.Name == "" { |
|||
log.WithField("repo", notif.Repository).Error("Travis-CI webhook missing repository fields") |
|||
w.WriteHeader(400) |
|||
return |
|||
} |
|||
whForRepo := notif.Repository.OwnerName + "/" + notif.Repository.Name |
|||
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 |
|||
} |
|||
msg := matrix.TextMessage{ |
|||
Body: outputForTemplate(repoData.Template, tmplData), |
|||
MsgType: "m.notice", |
|||
} |
|||
|
|||
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.") |
|||
} |
|||
} |
|||
} |
|||
w.WriteHeader(200) |
|||
} |
|||
|
|||
// 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{ |
|||
DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType), |
|||
WebhookEndpointURL: webhookEndpointURL, |
|||
} |
|||
}) |
|||
} |
@ -0,0 +1,210 @@ |
|||
package travisci |
|||
|
|||
import ( |
|||
"bytes" |
|||
"encoding/json" |
|||
"fmt" |
|||
"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----- |
|||
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#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 { |
|||
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.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.TextMessage |
|||
if err := json.NewDecoder(req.Body).Decode(&msg); err != nil { |
|||
return nil, fmt.Errorf("Failed to decode request JSON: %s", 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.TextMessage{} // 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 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( |
|||
`{ |
|||
"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) |
|||
} |
@ -0,0 +1,109 @@ |
|||
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.
|
|||
// .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(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. |
|||
*/ |
|||
sig, err := base64.StdEncoding.DecodeString(sigHeader) |
|||
if err != nil { |
|||
return fmt.Errorf("verifyOrigin: Failed to decode signature as base64: %s", err) |
|||
} |
|||
|
|||
if err := loadPublicKeys(); err != nil { |
|||
return fmt.Errorf("verifyOrigin: Failed to cache Travis public keys: %s", 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
|
|||
} |
|||
} |
|||
return fmt.Errorf("verifyOrigin: Signature verification failed: %s", verifyErr) |
|||
} |
|||
|
|||
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) { |
|||
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: %s", key) |
|||
} |
|||
return |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue