From cbc30321a42c6eadfe5d010ea53dd2187e11f0db Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 31 Jan 2017 13:57:02 +0000 Subject: [PATCH] Use github.com/matrix-org/util --- .../matrix-org/go-neb/api/handlers/auth.go | 50 ++--- .../matrix-org/go-neb/api/handlers/client.go | 12 +- .../go-neb/api/handlers/heartbeat.go | 4 +- .../matrix-org/go-neb/api/handlers/service.go | 36 ++-- src/github.com/matrix-org/go-neb/goneb.go | 22 +- .../matrix-org/go-neb/server/server_test.go | 28 --- .../go-neb/services/github/github_webhook.go | 35 ++- .../go-neb/services/github/webhook/webhook.go | 14 +- .../go-neb/services/jira/webhook/webhook.go | 26 +-- src/github.com/matrix-org/go-neb/util/util.go | 35 --- vendor/manifest | 20 +- vendor/src/github.com/matrix-org/util/LICENSE | 201 ++++++++++++++++++ .../src/github.com/matrix-org/util/README.md | 7 + .../src/github.com/matrix-org/util/error.go | 6 +- .../matrix-org/util/hooks/install.sh | 5 + .../matrix-org/util/hooks/pre-commit | 9 + .../src/github.com/matrix-org/util/json.go | 86 ++++---- .../github.com/matrix-org/util/json_test.go | 99 +++++++++ 18 files changed, 498 insertions(+), 197 deletions(-) delete mode 100644 src/github.com/matrix-org/go-neb/server/server_test.go delete mode 100644 src/github.com/matrix-org/go-neb/util/util.go create mode 100644 vendor/src/github.com/matrix-org/util/LICENSE create mode 100644 vendor/src/github.com/matrix-org/util/README.md rename src/github.com/matrix-org/go-neb/errors/errors.go => vendor/src/github.com/matrix-org/util/error.go (91%) create mode 100644 vendor/src/github.com/matrix-org/util/hooks/install.sh create mode 100644 vendor/src/github.com/matrix-org/util/hooks/pre-commit rename src/github.com/matrix-org/go-neb/server/server.go => vendor/src/github.com/matrix-org/util/json.go (61%) create mode 100644 vendor/src/github.com/matrix-org/util/json_test.go diff --git a/src/github.com/matrix-org/go-neb/api/handlers/auth.go b/src/github.com/matrix-org/go-neb/api/handlers/auth.go index 4179397..e056a1f 100644 --- a/src/github.com/matrix-org/go-neb/api/handlers/auth.go +++ b/src/github.com/matrix-org/go-neb/api/handlers/auth.go @@ -10,9 +10,9 @@ import ( log "github.com/Sirupsen/logrus" "github.com/matrix-org/go-neb/api" "github.com/matrix-org/go-neb/database" - "github.com/matrix-org/go-neb/errors" "github.com/matrix-org/go-neb/metrics" "github.com/matrix-org/go-neb/types" + "github.com/matrix-org/util" ) // RequestAuthSession represents an HTTP handler capable of processing /admin/requestAuthSession requests. @@ -40,13 +40,13 @@ type RequestAuthSession struct { // { // // AuthRealm-specific information // } -func (h *RequestAuthSession) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { +func (h *RequestAuthSession) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) { if req.Method != "POST" { - return nil, &errors.HTTPError{nil, "Unsupported Method", 405} + return nil, &util.HTTPError{nil, "Unsupported Method", 405} } var body api.RequestAuthSessionRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} + return nil, &util.HTTPError{err, "Error parsing request JSON", 400} } log.WithFields(log.Fields{ "realm_id": body.RealmID, @@ -54,17 +54,17 @@ func (h *RequestAuthSession) OnIncomingRequest(req *http.Request) (interface{}, }).Print("Incoming auth session request") if err := body.Check(); err != nil { - return nil, &errors.HTTPError{err, err.Error(), 400} + return nil, &util.HTTPError{err, err.Error(), 400} } realm, err := h.Db.LoadAuthRealm(body.RealmID) if err != nil { - return nil, &errors.HTTPError{err, "Unknown RealmID", 400} + return nil, &util.HTTPError{err, "Unknown RealmID", 400} } response := realm.RequestAuthSession(body.UserID, body.Config) if response == nil { - return nil, &errors.HTTPError{nil, "Failed to request auth session", 500} + return nil, &util.HTTPError{nil, "Failed to request auth session", 500} } metrics.IncrementAuthSession(realm.Type()) @@ -90,16 +90,16 @@ type RemoveAuthSession struct { // Response: // HTTP/1.1 200 OK // {} -func (h *RemoveAuthSession) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { +func (h *RemoveAuthSession) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) { if req.Method != "POST" { - return nil, &errors.HTTPError{nil, "Unsupported Method", 405} + return nil, &util.HTTPError{nil, "Unsupported Method", 405} } var body struct { RealmID string UserID string } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} + return nil, &util.HTTPError{err, "Error parsing request JSON", 400} } log.WithFields(log.Fields{ "realm_id": body.RealmID, @@ -107,16 +107,16 @@ func (h *RemoveAuthSession) OnIncomingRequest(req *http.Request) (interface{}, * }).Print("Incoming remove auth session request") if body.UserID == "" || body.RealmID == "" { - return nil, &errors.HTTPError{nil, `Must supply a "UserID", a "RealmID"`, 400} + return nil, &util.HTTPError{nil, `Must supply a "UserID", a "RealmID"`, 400} } _, err := h.Db.LoadAuthRealm(body.RealmID) if err != nil { - return nil, &errors.HTTPError{err, "Unknown RealmID", 400} + return nil, &util.HTTPError{err, "Unknown RealmID", 400} } if err := h.Db.RemoveAuthSession(body.RealmID, body.UserID); err != nil { - return nil, &errors.HTTPError{err, "Failed to remove auth session", 500} + return nil, &util.HTTPError{err, "Failed to remove auth session", 500} } return []byte(`{}`), nil @@ -186,31 +186,31 @@ type ConfigureAuthRealm struct { // // New auth realm config information // }, // } -func (h *ConfigureAuthRealm) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { +func (h *ConfigureAuthRealm) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) { if req.Method != "POST" { - return nil, &errors.HTTPError{nil, "Unsupported Method", 405} + return nil, &util.HTTPError{nil, "Unsupported Method", 405} } var body api.ConfigureAuthRealmRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} + return nil, &util.HTTPError{err, "Error parsing request JSON", 400} } if err := body.Check(); err != nil { - return nil, &errors.HTTPError{err, err.Error(), 400} + return nil, &util.HTTPError{err, err.Error(), 400} } realm, err := types.CreateAuthRealm(body.ID, body.Type, body.Config) if err != nil { - return nil, &errors.HTTPError{err, "Error parsing config JSON", 400} + return nil, &util.HTTPError{err, "Error parsing config JSON", 400} } if err = realm.Register(); err != nil { - return nil, &errors.HTTPError{err, "Error registering auth realm", 400} + return nil, &util.HTTPError{err, "Error registering auth realm", 400} } oldRealm, err := h.Db.StoreAuthRealm(realm) if err != nil { - return nil, &errors.HTTPError{err, "Error storing realm", 500} + return nil, &util.HTTPError{err, "Error storing realm", 500} } return &struct { @@ -252,25 +252,25 @@ type GetSession struct { // { // "Authenticated": false // } -func (h *GetSession) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { +func (h *GetSession) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) { if req.Method != "POST" { - return nil, &errors.HTTPError{nil, "Unsupported Method", 405} + return nil, &util.HTTPError{nil, "Unsupported Method", 405} } var body struct { RealmID string UserID string } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} + return nil, &util.HTTPError{err, "Error parsing request JSON", 400} } if body.RealmID == "" || body.UserID == "" { - return nil, &errors.HTTPError{nil, `Must supply a "RealmID" and "UserID"`, 400} + return nil, &util.HTTPError{nil, `Must supply a "RealmID" and "UserID"`, 400} } session, err := h.Db.LoadAuthSessionByUser(body.RealmID, body.UserID) if err != nil && err != sql.ErrNoRows { - return nil, &errors.HTTPError{err, `Failed to load session`, 500} + return nil, &util.HTTPError{err, `Failed to load session`, 500} } if err == sql.ErrNoRows { return &struct { diff --git a/src/github.com/matrix-org/go-neb/api/handlers/client.go b/src/github.com/matrix-org/go-neb/api/handlers/client.go index a814dd3..5327ae8 100644 --- a/src/github.com/matrix-org/go-neb/api/handlers/client.go +++ b/src/github.com/matrix-org/go-neb/api/handlers/client.go @@ -6,7 +6,7 @@ import ( "github.com/matrix-org/go-neb/api" "github.com/matrix-org/go-neb/clients" - "github.com/matrix-org/go-neb/errors" + "github.com/matrix-org/util" ) // ConfigureClient represents an HTTP handler capable of processing /admin/configureClient requests. @@ -39,23 +39,23 @@ type ConfigureClient struct { // // The new api.ClientConfig // } // } -func (s *ConfigureClient) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { +func (s *ConfigureClient) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) { if req.Method != "POST" { - return nil, &errors.HTTPError{nil, "Unsupported Method", 405} + return nil, &util.HTTPError{nil, "Unsupported Method", 405} } var body api.ClientConfig if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} + return nil, &util.HTTPError{err, "Error parsing request JSON", 400} } if err := body.Check(); err != nil { - return nil, &errors.HTTPError{err, "Error parsing client config", 400} + return nil, &util.HTTPError{err, "Error parsing client config", 400} } oldClient, err := s.Clients.Update(body) if err != nil { - return nil, &errors.HTTPError{err, "Error storing token", 500} + return nil, &util.HTTPError{err, "Error storing token", 500} } return &struct { diff --git a/src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go b/src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go index 1917971..484814b 100644 --- a/src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go +++ b/src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go @@ -11,7 +11,7 @@ package handlers import ( "net/http" - "github.com/matrix-org/go-neb/errors" + "github.com/matrix-org/util" ) // Heartbeat implements the heartbeat API @@ -26,6 +26,6 @@ type Heartbeat struct{} // Response: // HTTP/1.1 200 OK // {} -func (*Heartbeat) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { +func (*Heartbeat) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) { return &struct{}{}, nil } diff --git a/src/github.com/matrix-org/go-neb/api/handlers/service.go b/src/github.com/matrix-org/go-neb/api/handlers/service.go index 6acb777..6cd3fd4 100644 --- a/src/github.com/matrix-org/go-neb/api/handlers/service.go +++ b/src/github.com/matrix-org/go-neb/api/handlers/service.go @@ -11,12 +11,12 @@ import ( "github.com/matrix-org/go-neb/api" "github.com/matrix-org/go-neb/clients" "github.com/matrix-org/go-neb/database" - "github.com/matrix-org/go-neb/errors" "github.com/matrix-org/go-neb/matrix" "github.com/matrix-org/go-neb/metrics" "github.com/matrix-org/go-neb/polling" "github.com/matrix-org/go-neb/types" "github.com/matrix-org/gomatrix" + "github.com/matrix-org/util" ) // ConfigureService represents an HTTP handler which can process /admin/configureService requests. @@ -76,9 +76,9 @@ func (s *ConfigureService) getMutexForServiceID(serviceID string) *sync.Mutex { // // new service-specific config information // }, // } -func (s *ConfigureService) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { +func (s *ConfigureService) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) { if req.Method != "POST" { - return nil, &errors.HTTPError{nil, "Unsupported Method", 405} + return nil, &util.HTTPError{nil, "Unsupported Method", 405} } service, httpErr := s.createService(req) @@ -98,25 +98,25 @@ func (s *ConfigureService) OnIncomingRequest(req *http.Request) (interface{}, *e old, err := s.db.LoadService(service.ServiceID()) if err != nil && err != sql.ErrNoRows { - return nil, &errors.HTTPError{err, "Error loading old service", 500} + return nil, &util.HTTPError{err, "Error loading old service", 500} } client, err := s.clients.Client(service.ServiceUserID()) if err != nil { - return nil, &errors.HTTPError{err, "Unknown matrix client", 400} + return nil, &util.HTTPError{err, "Unknown matrix client", 400} } if err := checkClientForService(service, client); err != nil { - return nil, &errors.HTTPError{err, err.Error(), 400} + return nil, &util.HTTPError{err, err.Error(), 400} } if err = service.Register(old, client); err != nil { - return nil, &errors.HTTPError{err, "Failed to register service: " + err.Error(), 500} + return nil, &util.HTTPError{err, "Failed to register service: " + err.Error(), 500} } oldService, err := s.db.StoreService(service) if err != nil { - return nil, &errors.HTTPError{err, "Error storing service", 500} + return nil, &util.HTTPError{err, "Error storing service", 500} } // Start any polling NOW because they may decide to stop it in PostRegister, and we want to make @@ -141,19 +141,19 @@ func (s *ConfigureService) OnIncomingRequest(req *http.Request) (interface{}, *e }{service.ServiceID(), service.ServiceType(), oldService, service}, nil } -func (s *ConfigureService) createService(req *http.Request) (types.Service, *errors.HTTPError) { +func (s *ConfigureService) createService(req *http.Request) (types.Service, *util.HTTPError) { var body api.ConfigureServiceRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} + return nil, &util.HTTPError{err, "Error parsing request JSON", 400} } if err := body.Check(); err != nil { - return nil, &errors.HTTPError{err, err.Error(), 400} + return nil, &util.HTTPError{err, err.Error(), 400} } service, err := types.CreateService(body.ID, body.Type, body.UserID, body.Config) if err != nil { - return nil, &errors.HTTPError{err, "Error parsing config JSON", 400} + return nil, &util.HTTPError{err, "Error parsing config JSON", 400} } return service, nil } @@ -182,27 +182,27 @@ type GetService struct { // // service-specific config information // } // } -func (h *GetService) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { +func (h *GetService) OnIncomingRequest(req *http.Request) (interface{}, *util.HTTPError) { if req.Method != "POST" { - return nil, &errors.HTTPError{nil, "Unsupported Method", 405} + return nil, &util.HTTPError{nil, "Unsupported Method", 405} } var body struct { ID string } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} + return nil, &util.HTTPError{err, "Error parsing request JSON", 400} } if body.ID == "" { - return nil, &errors.HTTPError{nil, `Must supply a "ID"`, 400} + return nil, &util.HTTPError{nil, `Must supply a "ID"`, 400} } srv, err := h.Db.LoadService(body.ID) if err != nil { if err == sql.ErrNoRows { - return nil, &errors.HTTPError{err, `Service not found`, 404} + return nil, &util.HTTPError{err, `Service not found`, 404} } - return nil, &errors.HTTPError{err, `Failed to load service`, 500} + return nil, &util.HTTPError{err, `Failed to load service`, 500} } return &struct { diff --git a/src/github.com/matrix-org/go-neb/goneb.go b/src/github.com/matrix-org/go-neb/goneb.go index b4bd79d..ada5cb9 100644 --- a/src/github.com/matrix-org/go-neb/goneb.go +++ b/src/github.com/matrix-org/go-neb/goneb.go @@ -19,7 +19,6 @@ import ( "github.com/matrix-org/go-neb/polling" _ "github.com/matrix-org/go-neb/realms/github" _ "github.com/matrix-org/go-neb/realms/jira" - "github.com/matrix-org/go-neb/server" _ "github.com/matrix-org/go-neb/services/echo" _ "github.com/matrix-org/go-neb/services/giphy" _ "github.com/matrix-org/go-neb/services/github" @@ -29,6 +28,7 @@ import ( _ "github.com/matrix-org/go-neb/services/slackapi" _ "github.com/matrix-org/go-neb/services/travisci" "github.com/matrix-org/go-neb/types" + "github.com/matrix-org/util" _ "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus" yaml "gopkg.in/yaml.v2" @@ -175,11 +175,11 @@ func setup(e envVars, mux *http.ServeMux, matrixClient *http.Client) { // Handle non-admin paths for normal NEB functioning mux.Handle("/metrics", prometheus.Handler()) - mux.Handle("/test", prometheus.InstrumentHandler("test", server.MakeJSONAPI(&handlers.Heartbeat{}))) + mux.Handle("/test", prometheus.InstrumentHandler("test", util.MakeJSONAPI(&handlers.Heartbeat{}))) wh := handlers.NewWebhook(db, clients) - mux.HandleFunc("/services/hooks/", prometheus.InstrumentHandlerFunc("webhookHandler", server.Protect(wh.Handle))) + mux.HandleFunc("/services/hooks/", prometheus.InstrumentHandlerFunc("webhookHandler", util.Protect(wh.Handle))) rh := &handlers.RealmRedirect{db} - mux.HandleFunc("/realms/redirects/", prometheus.InstrumentHandlerFunc("realmRedirectHandler", server.Protect(rh.Handle))) + mux.HandleFunc("/realms/redirects/", prometheus.InstrumentHandlerFunc("realmRedirectHandler", util.Protect(rh.Handle))) // Read exclusively from the config file if one was supplied. // Otherwise, add HTTP listeners for new Services/Sessions/Clients/etc. @@ -190,13 +190,13 @@ func setup(e envVars, mux *http.ServeMux, matrixClient *http.Client) { log.Info("Inserted ", len(cfg.Services), " services") } else { - mux.Handle("/admin/getService", prometheus.InstrumentHandler("getService", server.MakeJSONAPI(&handlers.GetService{db}))) - mux.Handle("/admin/getSession", prometheus.InstrumentHandler("getSession", server.MakeJSONAPI(&handlers.GetSession{db}))) - mux.Handle("/admin/configureClient", prometheus.InstrumentHandler("configureClient", server.MakeJSONAPI(&handlers.ConfigureClient{clients}))) - mux.Handle("/admin/configureService", prometheus.InstrumentHandler("configureService", server.MakeJSONAPI(handlers.NewConfigureService(db, clients)))) - mux.Handle("/admin/configureAuthRealm", prometheus.InstrumentHandler("configureAuthRealm", server.MakeJSONAPI(&handlers.ConfigureAuthRealm{db}))) - mux.Handle("/admin/requestAuthSession", prometheus.InstrumentHandler("requestAuthSession", server.MakeJSONAPI(&handlers.RequestAuthSession{db}))) - mux.Handle("/admin/removeAuthSession", prometheus.InstrumentHandler("removeAuthSession", server.MakeJSONAPI(&handlers.RemoveAuthSession{db}))) + mux.Handle("/admin/getService", prometheus.InstrumentHandler("getService", util.MakeJSONAPI(&handlers.GetService{db}))) + mux.Handle("/admin/getSession", prometheus.InstrumentHandler("getSession", util.MakeJSONAPI(&handlers.GetSession{db}))) + mux.Handle("/admin/configureClient", prometheus.InstrumentHandler("configureClient", util.MakeJSONAPI(&handlers.ConfigureClient{clients}))) + mux.Handle("/admin/configureService", prometheus.InstrumentHandler("configureService", util.MakeJSONAPI(handlers.NewConfigureService(db, clients)))) + mux.Handle("/admin/configureAuthRealm", prometheus.InstrumentHandler("configureAuthRealm", util.MakeJSONAPI(&handlers.ConfigureAuthRealm{db}))) + mux.Handle("/admin/requestAuthSession", prometheus.InstrumentHandler("requestAuthSession", util.MakeJSONAPI(&handlers.RequestAuthSession{db}))) + mux.Handle("/admin/removeAuthSession", prometheus.InstrumentHandler("removeAuthSession", util.MakeJSONAPI(&handlers.RemoveAuthSession{db}))) } polling.SetClients(clients) if err := polling.Start(); err != nil { diff --git a/src/github.com/matrix-org/go-neb/server/server_test.go b/src/github.com/matrix-org/go-neb/server/server_test.go deleted file mode 100644 index b9afb12..0000000 --- a/src/github.com/matrix-org/go-neb/server/server_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package server - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestProtect(t *testing.T) { - mockWriter := httptest.NewRecorder() - mockReq, _ := http.NewRequest("GET", "http://example.com/foo", nil) - h := Protect(func(w http.ResponseWriter, req *http.Request) { - panic("oh noes!") - }) - - h(mockWriter, mockReq) - - expectCode := 500 - if mockWriter.Code != expectCode { - t.Errorf("TestProtect wanted HTTP status %d, got %d", expectCode, mockWriter.Code) - } - - expectBody := `{"message":"Internal Server Error"}` - actualBody := mockWriter.Body.String() - if actualBody != expectBody { - t.Errorf("TestProtect wanted body %s, got %s", expectBody, actualBody) - } -} diff --git a/src/github.com/matrix-org/go-neb/services/github/github_webhook.go b/src/github.com/matrix-org/go-neb/services/github/github_webhook.go index c31d918..fdf36e6 100644 --- a/src/github.com/matrix-org/go-neb/services/github/github_webhook.go +++ b/src/github.com/matrix-org/go-neb/services/github/github_webhook.go @@ -12,7 +12,6 @@ import ( "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" "github.com/matrix-org/gomatrix" ) @@ -178,7 +177,7 @@ func (s *WebhookService) Register(oldService types.Service, client *gomatrix.Cli reposForWebhooks := s.repoList() // Add hooks for the newly added repos but don't remove hooks for the removed repos: we'll clean those out later - newRepos, removedRepos := util.Difference(reposForWebhooks, oldRepos) + newRepos, removedRepos := difference(reposForWebhooks, oldRepos) if len(reposForWebhooks) == 0 && len(removedRepos) == 0 { // The user didn't specify any webhooks. This may be a bug or it may be // a conscious decision to remove all webhooks for this service. Figure out @@ -224,7 +223,7 @@ func (s *WebhookService) PostRegister(oldService types.Service) { newRepos := s.repoList() // Register() handled adding the new repos, we just want to clean up after ourselves - _, removedRepos := util.Difference(newRepos, oldRepos) + _, removedRepos := difference(newRepos, oldRepos) for _, r := range removedRepos { segs := strings.Split(r, "/") if err := s.deleteHook(segs[0], segs[1]); err != nil { @@ -398,6 +397,36 @@ func sameRepos(a *WebhookService, b *WebhookService) bool { return true } +// difference returns the elements that are only in the first list and +// the elements that are only in the second. As a side-effect this sorts +// the input lists in-place. +func difference(a, b []string) (onlyA, onlyB []string) { + sort.Strings(a) + sort.Strings(b) + for { + if len(b) == 0 { + onlyA = append(onlyA, a...) + return + } + if len(a) == 0 { + onlyB = append(onlyB, b...) + return + } + xA := a[0] + xB := b[0] + if xA < xB { + onlyA = append(onlyA, xA) + a = a[1:] + } else if xA > xB { + onlyB = append(onlyB, xB) + b = b[1:] + } else { + a = a[1:] + b = b[1:] + } + } +} + func (s *WebhookService) githubClientFor(userID string, allowUnauth bool) *gogithub.Client { token, err := getTokenForUser(s.RealmID, userID) if err != nil { diff --git a/src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go b/src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go index 467d033..ed510d4 100644 --- a/src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go +++ b/src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go @@ -13,22 +13,22 @@ import ( log "github.com/Sirupsen/logrus" "github.com/google/go-github/github" - "github.com/matrix-org/go-neb/errors" "github.com/matrix-org/gomatrix" + "github.com/matrix-org/util" ) // OnReceiveRequest processes incoming github webhook requests and returns a // matrix message to send, along with parsed repo information. // The secretToken, if supplied, will be used to verify the request is from // Github. If it isn't, an error is returned. -func OnReceiveRequest(r *http.Request, secretToken string) (string, *github.Repository, *gomatrix.HTMLMessage, *errors.HTTPError) { +func OnReceiveRequest(r *http.Request, secretToken string) (string, *github.Repository, *gomatrix.HTMLMessage, *util.HTTPError) { // Verify the HMAC signature if NEB was configured with a secret token eventType := r.Header.Get("X-GitHub-Event") signatureSHA1 := r.Header.Get("X-Hub-Signature") content, err := ioutil.ReadAll(r.Body) if err != nil { log.WithError(err).Print("Failed to read Github webhook body") - return "", nil, nil, &errors.HTTPError{nil, "Failed to parse body", 400} + return "", nil, nil, &util.HTTPError{nil, "Failed to parse body", 400} } // Verify request if a secret token has been supplied. if secretToken != "" { @@ -38,14 +38,14 @@ func OnReceiveRequest(r *http.Request, secretToken string) (string, *github.Repo if err != nil { log.WithError(err).WithField("X-Hub-Signature", sigHex).Print( "Failed to decode signature as hex.") - return "", nil, nil, &errors.HTTPError{nil, "Failed to decode signature", 400} + return "", nil, nil, &util.HTTPError{nil, "Failed to decode signature", 400} } if !checkMAC([]byte(content), sigBytes, []byte(secretToken)) { log.WithFields(log.Fields{ "X-Hub-Signature": signatureSHA1, }).Print("Received Github event which failed MAC check.") - return "", nil, nil, &errors.HTTPError{nil, "Bad signature", 403} + return "", nil, nil, &util.HTTPError{nil, "Bad signature", 403} } } @@ -58,13 +58,13 @@ func OnReceiveRequest(r *http.Request, secretToken string) (string, *github.Repo // Github will send a "ping" event when the webhook is first created. We need // to return a 200 in order for the webhook to be marked as "up" (this doesn't // affect delivery, just the tick/cross status flag). - return "", nil, nil, &errors.HTTPError{nil, "pong", 200} + return "", nil, nil, &util.HTTPError{nil, "pong", 200} } htmlStr, repo, refinedType, err := parseGithubEvent(eventType, content) if err != nil { log.WithError(err).Print("Failed to parse github event") - return "", nil, nil, &errors.HTTPError{nil, "Failed to parse github event", 500} + return "", nil, nil, &util.HTTPError{nil, "Failed to parse github event", 500} } msg := gomatrix.GetHTMLMessage("m.notice", htmlStr) diff --git a/src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go b/src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go index 04293b9..4aa45a9 100644 --- a/src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go +++ b/src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go @@ -9,8 +9,8 @@ import ( log "github.com/Sirupsen/logrus" gojira "github.com/andygrunwald/go-jira" "github.com/matrix-org/go-neb/database" - "github.com/matrix-org/go-neb/errors" "github.com/matrix-org/go-neb/realms/jira" + "github.com/matrix-org/util" ) type jiraWebhook struct { @@ -101,17 +101,17 @@ func RegisterHook(jrealm *jira.Realm, projects []string, userID, webhookEndpoint // OnReceiveRequest is called when JIRA hits NEB with an update. // Returns the project key and webhook event, or an error. -func OnReceiveRequest(req *http.Request) (string, *Event, *errors.HTTPError) { +func OnReceiveRequest(req *http.Request) (string, *Event, *util.HTTPError) { // extract the JIRA webhook event JSON defer req.Body.Close() var whe Event err := json.NewDecoder(req.Body).Decode(&whe) if err != nil { - return "", nil, &errors.HTTPError{err, "Failed to parse request JSON", 400} + return "", nil, &util.HTTPError{err, "Failed to parse request JSON", 400} } if err != nil { - return "", nil, &errors.HTTPError{err, "Failed to parse JIRA URL", 400} + return "", nil, &util.HTTPError{err, "Failed to parse JIRA URL", 400} } projKey := strings.Split(whe.Issue.Key, "-")[0] projKey = strings.ToUpper(projKey) @@ -153,18 +153,18 @@ func createWebhook(jrealm *jira.Realm, webhookEndpointURL, userID string) error return err } -func getWebhook(cli *gojira.Client, webhookEndpointURL string) (*jiraWebhook, *errors.HTTPError) { +func getWebhook(cli *gojira.Client, webhookEndpointURL string) (*jiraWebhook, *util.HTTPError) { req, err := cli.NewRequest("GET", "rest/webhooks/1.0/webhook", nil) if err != nil { - return nil, &errors.HTTPError{err, "Failed to prepare webhook request", 500} + return nil, &util.HTTPError{err, "Failed to prepare webhook request", 500} } var webhookList []jiraWebhook res, err := cli.Do(req, &webhookList) if err != nil { - return nil, &errors.HTTPError{err, "Failed to query webhooks", 502} + return nil, &util.HTTPError{err, "Failed to query webhooks", 502} } if res.StatusCode < 200 || res.StatusCode >= 300 { - return nil, &errors.HTTPError{ + return nil, &util.HTTPError{ err, fmt.Sprintf("Querying webhook returned HTTP %d", res.StatusCode), 403, @@ -181,23 +181,23 @@ func getWebhook(cli *gojira.Client, webhookEndpointURL string) (*jiraWebhook, *e return nebWH, nil } -func checkProjectsArePublic(jrealm *jira.Realm, projects []string, userID string) *errors.HTTPError { +func checkProjectsArePublic(jrealm *jira.Realm, projects []string, userID string) *util.HTTPError { publicCli, err := jrealm.JIRAClient("", true) if err != nil { - return &errors.HTTPError{err, "Cannot create public JIRA client", 500} + return &util.HTTPError{err, "Cannot create public JIRA client", 500} } for _, projectKey := range projects { // check you can query this project with a public client req, err := publicCli.NewRequest("GET", "rest/api/2/project/"+projectKey, nil) if err != nil { - return &errors.HTTPError{err, "Failed to create project URL", 500} + return &util.HTTPError{err, "Failed to create project URL", 500} } res, err := publicCli.Do(req, nil) if err != nil { - return &errors.HTTPError{err, fmt.Sprintf("Failed to query project %s", projectKey), 500} + return &util.HTTPError{err, fmt.Sprintf("Failed to query project %s", projectKey), 500} } if res.StatusCode < 200 || res.StatusCode >= 300 { - return &errors.HTTPError{err, fmt.Sprintf("Project %s is not public. (HTTP %d)", projectKey, res.StatusCode), 403} + return &util.HTTPError{err, fmt.Sprintf("Project %s is not public. (HTTP %d)", projectKey, res.StatusCode), 403} } } return nil diff --git a/src/github.com/matrix-org/go-neb/util/util.go b/src/github.com/matrix-org/go-neb/util/util.go deleted file mode 100644 index d8b59c7..0000000 --- a/src/github.com/matrix-org/go-neb/util/util.go +++ /dev/null @@ -1,35 +0,0 @@ -package util - -import ( - "sort" -) - -// Difference returns the elements that are only in the first list and -// the elements that are only in the second. As a side-effect this sorts -// the input lists in-place. -func Difference(a, b []string) (onlyA, onlyB []string) { - sort.Strings(a) - sort.Strings(b) - for { - if len(b) == 0 { - onlyA = append(onlyA, a...) - return - } - if len(a) == 0 { - onlyB = append(onlyB, b...) - return - } - xA := a[0] - xB := b[0] - if xA < xB { - onlyA = append(onlyA, xA) - a = a[1:] - } else if xA > xB { - onlyB = append(onlyB, xB) - b = b[1:] - } else { - a = a[1:] - b = b[1:] - } - } -} diff --git a/vendor/manifest b/vendor/manifest index 038f2a8..4eda44e 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -141,6 +141,12 @@ "revision": "e66d1ef529b7851262b49dc42a26ff1f1d1d9e4d", "branch": "master" }, + { + "importpath": "github.com/matrix-org/util", + "repository": "https://github.com/matrix-org/util", + "revision": "9b44af331cdd83d702e4f16433e47341d983c23b", + "branch": "master" + }, { "importpath": "github.com/mattn/go-shellwords", "repository": "https://github.com/mattn/go-shellwords", @@ -218,13 +224,6 @@ "revision": "abf152e5f3e97f2fafac028d2cc06c1feb87ffa5", "branch": "master" }, - { - "importpath": "github.com/syndtr/goleveldb/leveldb", - "repository": "https://github.com/syndtr/goleveldb", - "revision": "6b4daa5362b502898ddf367c5c11deb9e7a5c727", - "branch": "master", - "path": "/leveldb" - }, { "importpath": "github.com/russross/blackfriday", "repository": "https://github.com/russross/blackfriday", @@ -237,6 +236,13 @@ "revision": "10ef21a441db47d8b13ebcc5fd2310f636973c77", "branch": "master" }, + { + "importpath": "github.com/syndtr/goleveldb/leveldb", + "repository": "https://github.com/syndtr/goleveldb", + "revision": "6b4daa5362b502898ddf367c5c11deb9e7a5c727", + "branch": "master", + "path": "/leveldb" + }, { "importpath": "golang.org/x/net/context", "repository": "https://go.googlesource.com/net", diff --git a/vendor/src/github.com/matrix-org/util/LICENSE b/vendor/src/github.com/matrix-org/util/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/src/github.com/matrix-org/util/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/src/github.com/matrix-org/util/README.md b/vendor/src/github.com/matrix-org/util/README.md new file mode 100644 index 0000000..319e4b5 --- /dev/null +++ b/vendor/src/github.com/matrix-org/util/README.md @@ -0,0 +1,7 @@ +# util + +[![GoDoc](https://godoc.org/github.com/matrix-org/util?status.svg)](https://godoc.org/github.com/matrix-org/util) +[![Build Status](https://travis-ci.org/matrix-org/util.svg?branch=master)](https://travis-ci.org/matrix-org/util) +[![Coverage Status](https://coveralls.io/repos/github/matrix-org/util/badge.svg)](https://coveralls.io/github/matrix-org/util) + +A loose collection of Golang functions that we use at matrix.org diff --git a/src/github.com/matrix-org/go-neb/errors/errors.go b/vendor/src/github.com/matrix-org/util/error.go similarity index 91% rename from src/github.com/matrix-org/go-neb/errors/errors.go rename to vendor/src/github.com/matrix-org/util/error.go index 9bf33d8..9d40c57 100644 --- a/src/github.com/matrix-org/go-neb/errors/errors.go +++ b/vendor/src/github.com/matrix-org/util/error.go @@ -1,8 +1,6 @@ -package errors +package util -import ( - "fmt" -) +import "fmt" // HTTPError An HTTP Error response, which may wrap an underlying native Go Error. type HTTPError struct { diff --git a/vendor/src/github.com/matrix-org/util/hooks/install.sh b/vendor/src/github.com/matrix-org/util/hooks/install.sh new file mode 100644 index 0000000..f8aa331 --- /dev/null +++ b/vendor/src/github.com/matrix-org/util/hooks/install.sh @@ -0,0 +1,5 @@ +#! /bin/bash + +DOT_GIT="$(dirname $0)/../.git" + +ln -s "../../hooks/pre-commit" "$DOT_GIT/hooks/pre-commit" \ No newline at end of file diff --git a/vendor/src/github.com/matrix-org/util/hooks/pre-commit b/vendor/src/github.com/matrix-org/util/hooks/pre-commit new file mode 100644 index 0000000..41df674 --- /dev/null +++ b/vendor/src/github.com/matrix-org/util/hooks/pre-commit @@ -0,0 +1,9 @@ +#! /bin/bash + +set -eu + +golint +go fmt +go tool vet --all --shadow . +gocyclo -over 12 . +go test -timeout 5s -test.v diff --git a/src/github.com/matrix-org/go-neb/server/server.go b/vendor/src/github.com/matrix-org/util/json.go similarity index 61% rename from src/github.com/matrix-org/go-neb/server/server.go rename to vendor/src/github.com/matrix-org/util/json.go index b240914..4735bf5 100644 --- a/src/github.com/matrix-org/go-neb/server/server.go +++ b/vendor/src/github.com/matrix-org/util/json.go @@ -1,21 +1,29 @@ -// Package server contains building blocks for REST APIs. -package server +package util import ( + "context" "encoding/json" - log "github.com/Sirupsen/logrus" - "github.com/matrix-org/go-neb/errors" + "fmt" + "math/rand" "net/http" "runtime/debug" + + log "github.com/Sirupsen/logrus" ) +// ContextKeys is a type alias for string to namespace Context keys per-package. +type ContextKeys string + +// CtxValueLogger is the key to extract the logrus Logger. +const CtxValueLogger = ContextKeys("logger") + // JSONRequestHandler represents an interface that must be satisfied in order to respond to incoming // HTTP requests with JSON. The interface returned will be marshalled into JSON to be sent to the client, // unless the interface is []byte in which case the bytes are sent to the client unchanged. // If an error is returned, a JSON error response will also be returned, unless the error code // is a 302 REDIRECT in which case a redirect is sent based on the Message field. type JSONRequestHandler interface { - OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) + OnIncomingRequest(req *http.Request) (interface{}, *HTTPError) } // JSONError represents a JSON API error response @@ -23,34 +31,21 @@ type JSONError struct { Message string `json:"message"` } -// WithCORSOptions intercepts all OPTIONS requests and responds with CORS headers. The request handler -// is not invoked when this happens. -func WithCORSOptions(handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if req.Method == "OPTIONS" { - SetCORSHeaders(w) - return - } - handler(w, req) - } -} - // Protect panicking HTTP requests from taking down the entire process, and log them using // the correct logger, returning a 500 with a JSON response rather than abruptly closing the -// connection. +// connection. The http.Request MUST have a CtxValueLogger. func Protect(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { defer func() { if r := recover(); r != nil { - log.WithFields(log.Fields{ - "panic": r, - "method": req.Method, - "url": req.URL, + logger := req.Context().Value(CtxValueLogger).(*log.Entry) + logger.WithFields(log.Fields{ + "panic": r, }).Errorf( "Request panicked!\n%s", debug.Stack(), ) jsonErrorResponse( - w, req, &errors.HTTPError{nil, "Internal Server Error", 500}, + w, req, &HTTPError{nil, "Internal Server Error", 500}, ) } }() @@ -59,12 +54,19 @@ func Protect(handler http.HandlerFunc) http.HandlerFunc { } // MakeJSONAPI creates an HTTP handler which always responds to incoming requests with JSON responses. +// Incoming http.Requests will have a logger (with a request ID/method/path logged) attached to the Context. +// This can be accessed via the const CtxValueLogger. The type of the logger is *log.Entry from github.com/Sirupsen/logrus func MakeJSONAPI(handler JSONRequestHandler) http.HandlerFunc { return Protect(func(w http.ResponseWriter, req *http.Request) { - logger := log.WithFields(log.Fields{ - "method": req.Method, - "url": req.URL, - }) + // Set a Logger on the context + ctx := context.WithValue(req.Context(), CtxValueLogger, log.WithFields(log.Fields{ + "req.method": req.Method, + "req.path": req.URL.Path, + "req.id": RandomString(12), + })) + req = req.WithContext(ctx) + + logger := req.Context().Value(CtxValueLogger).(*log.Entry) logger.Print("Incoming request") res, httpErr := handler.OnIncomingRequest(req) @@ -85,28 +87,25 @@ func MakeJSONAPI(handler JSONRequestHandler) http.HandlerFunc { if !ok { r, err := json.Marshal(res) if err != nil { - jsonErrorResponse(w, req, &errors.HTTPError{nil, "Failed to serialise response as JSON", 500}) + jsonErrorResponse(w, req, &HTTPError{nil, "Failed to serialise response as JSON", 500}) return } - logger.Print("<<< Returning response ", string(r)) resBytes = r } + logger.Print(fmt.Sprintf("Responding (%d bytes)", len(resBytes))) w.Write(resBytes) }) } -func jsonErrorResponse(w http.ResponseWriter, req *http.Request, httpErr *errors.HTTPError) { +func jsonErrorResponse(w http.ResponseWriter, req *http.Request, httpErr *HTTPError) { + logger := req.Context().Value(CtxValueLogger).(*log.Entry) if httpErr.Code == 302 { - log.WithField("err", httpErr.Error()).Print("Redirecting") + logger.WithField("err", httpErr.Error()).Print("Redirecting") http.Redirect(w, req, httpErr.Message, 302) return } - - log.WithField("err", httpErr.Error()).Print("Request failed") - log.WithFields(log.Fields{ - "url": req.URL, - "code": httpErr.Code, - "message": httpErr.Message, + logger.WithFields(log.Fields{ + log.ErrorKey: httpErr, }).Print("Responding with error") w.WriteHeader(httpErr.Code) // Set response code @@ -117,7 +116,7 @@ func jsonErrorResponse(w http.ResponseWriter, req *http.Request, httpErr *errors if err != nil { // We should never fail to marshal the JSON error response, but in this event just skip // marshalling altogether - log.Warn("Failed to marshal error response") + logger.Warn("Failed to marshal error response") w.Write([]byte(`{}`)) return } @@ -130,3 +129,14 @@ func SetCORSHeaders(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") } + +const alphanumerics = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +// RandomString generates a pseudo-random string of length n. +func RandomString(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = alphanumerics[rand.Int63()%int64(len(alphanumerics))] + } + return string(b) +} diff --git a/vendor/src/github.com/matrix-org/util/json_test.go b/vendor/src/github.com/matrix-org/util/json_test.go new file mode 100644 index 0000000..203fa70 --- /dev/null +++ b/vendor/src/github.com/matrix-org/util/json_test.go @@ -0,0 +1,99 @@ +package util + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + log "github.com/Sirupsen/logrus" +) + +type MockJSONRequestHandler struct { + handler func(req *http.Request) (interface{}, *HTTPError) +} + +func (h *MockJSONRequestHandler) OnIncomingRequest(req *http.Request) (interface{}, *HTTPError) { + return h.handler(req) +} + +type MockResponse struct { + Foo string `json:"foo"` +} + +func TestMakeJSONAPI(t *testing.T) { + log.SetLevel(log.PanicLevel) // suppress logs in test output + tests := []struct { + Return interface{} + Err *HTTPError + ExpectCode int + ExpectJSON string + }{ + {nil, &HTTPError{nil, "Everything is broken", 500}, 500, `{"message":"Everything is broken"}`}, // Error return values + {nil, &HTTPError{nil, "Not here", 404}, 404, `{"message":"Not here"}`}, // With different status codes + {&MockResponse{"yep"}, nil, 200, `{"foo":"yep"}`}, // Success return values + {[]MockResponse{{"yep"}, {"narp"}}, nil, 200, `[{"foo":"yep"},{"foo":"narp"}]`}, // Top-level array success values + {[]byte(`actually bytes`), nil, 200, `actually bytes`}, // raw []byte escape hatch + {func(cannotBe, marshalled string) {}, nil, 500, `{"message":"Failed to serialise response as JSON"}`}, // impossible marshal + } + + for _, tst := range tests { + mock := MockJSONRequestHandler{func(req *http.Request) (interface{}, *HTTPError) { + return tst.Return, tst.Err + }} + mockReq, _ := http.NewRequest("GET", "http://example.com/foo", nil) + mockWriter := httptest.NewRecorder() + handlerFunc := MakeJSONAPI(&mock) + handlerFunc(mockWriter, mockReq) + if mockWriter.Code != tst.ExpectCode { + t.Errorf("TestMakeJSONAPI wanted HTTP status %d, got %d", tst.ExpectCode, mockWriter.Code) + } + actualBody := mockWriter.Body.String() + if actualBody != tst.ExpectJSON { + t.Errorf("TestMakeJSONAPI wanted body '%s', got '%s'", tst.ExpectJSON, actualBody) + } + } +} + +func TestMakeJSONAPIRedirect(t *testing.T) { + log.SetLevel(log.PanicLevel) // suppress logs in test output + mock := MockJSONRequestHandler{func(req *http.Request) (interface{}, *HTTPError) { + return nil, &HTTPError{nil, "https://matrix.org", 302} + }} + mockReq, _ := http.NewRequest("GET", "http://example.com/foo", nil) + mockWriter := httptest.NewRecorder() + handlerFunc := MakeJSONAPI(&mock) + handlerFunc(mockWriter, mockReq) + if mockWriter.Code != 302 { + t.Errorf("TestMakeJSONAPIRedirect wanted HTTP status 302, got %d", mockWriter.Code) + } + location := mockWriter.Header().Get("Location") + if location != "https://matrix.org" { + t.Errorf("TestMakeJSONAPIRedirect wanted Location header 'https://matrix.org', got '%s'", location) + } +} + +func TestProtect(t *testing.T) { + log.SetLevel(log.PanicLevel) // suppress logs in test output + mockWriter := httptest.NewRecorder() + mockReq, _ := http.NewRequest("GET", "http://example.com/foo", nil) + mockReq = mockReq.WithContext( + context.WithValue(mockReq.Context(), CtxValueLogger, log.WithField("test", "yep")), + ) + h := Protect(func(w http.ResponseWriter, req *http.Request) { + panic("oh noes!") + }) + + h(mockWriter, mockReq) + + expectCode := 500 + if mockWriter.Code != expectCode { + t.Errorf("TestProtect wanted HTTP status %d, got %d", expectCode, mockWriter.Code) + } + + expectBody := `{"message":"Internal Server Error"}` + actualBody := mockWriter.Body.String() + if actualBody != expectBody { + t.Errorf("TestProtect wanted body %s, got %s", expectBody, actualBody) + } +}