From 2b72d94f04dea88f1faecb6ba871407ba15802ab Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Sat, 20 Jan 2018 00:56:12 -0700
Subject: [PATCH] Add a circuit breaker for validating access tokens

Adds #46
---
 config.sample.yaml                            |  2 +
 .../matrix-media-repo/client/r0/download.go   |  3 +-
 .../client/r0/preview_url.go                  |  3 +-
 .../matrix-media-repo/client/r0/upload.go     |  3 +-
 .../matrix-media-repo/config/config.go        |  1 +
 .../matrix-media-repo/matrix/matrix.go        | 64 +++++++++++++++++++
 .../matrix-media-repo/util/matrix.go          | 28 --------
 vendor/manifest                               | 18 ++++++
 8 files changed, 91 insertions(+), 31 deletions(-)
 create mode 100644 src/github.com/turt2live/matrix-media-repo/matrix/matrix.go
 delete mode 100644 src/github.com/turt2live/matrix-media-repo/util/matrix.go

diff --git a/config.sample.yaml b/config.sample.yaml
index 86282651..19e28979 100644
--- a/config.sample.yaml
+++ b/config.sample.yaml
@@ -18,6 +18,8 @@ homeservers:
   - name: t2bot.io # This should match the Host header given to the media repo
     downloadRequiresAuth: false # Set to true to require auth on downloads and identicons
     csApi: "https://t2bot.io/" # The base URL to where the homeserver can actually be reached
+    backoffAt: 10 # The number of consecutive failures in calling this homeserver before the
+                  # media repository will start backing off. This defaults to 10 if not given.
 
 # The file upload settings for the media repository
 uploads:
diff --git a/src/github.com/turt2live/matrix-media-repo/client/r0/download.go b/src/github.com/turt2live/matrix-media-repo/client/r0/download.go
index 1391151f..c9c3ea5a 100644
--- a/src/github.com/turt2live/matrix-media-repo/client/r0/download.go
+++ b/src/github.com/turt2live/matrix-media-repo/client/r0/download.go
@@ -7,6 +7,7 @@ import (
 	"github.com/gorilla/mux"
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/client"
+	"github.com/turt2live/matrix-media-repo/matrix"
 	"github.com/turt2live/matrix-media-repo/media_cache"
 	"github.com/turt2live/matrix-media-repo/util"
 	"github.com/turt2live/matrix-media-repo/util/errs"
@@ -68,7 +69,7 @@ func ValidateUserCanDownload(r *http.Request, log *logrus.Entry) (bool) {
 	}
 
 	accessToken := util.GetAccessTokenFromRequest(r)
-	userId, err := util.GetUserIdFromToken(r.Context(), r.Host, accessToken)
+	userId, err := matrix.GetUserIdFromToken(r.Context(), r.Host, accessToken)
 	if err != nil {
 		log.Error("Error verifying token: " + err.Error())
 	}
diff --git a/src/github.com/turt2live/matrix-media-repo/client/r0/preview_url.go b/src/github.com/turt2live/matrix-media-repo/client/r0/preview_url.go
index b671fede..a327abd4 100644
--- a/src/github.com/turt2live/matrix-media-repo/client/r0/preview_url.go
+++ b/src/github.com/turt2live/matrix-media-repo/client/r0/preview_url.go
@@ -8,6 +8,7 @@ import (
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/client"
 	"github.com/turt2live/matrix-media-repo/config"
+	"github.com/turt2live/matrix-media-repo/matrix"
 	"github.com/turt2live/matrix-media-repo/services/url_service"
 	"github.com/turt2live/matrix-media-repo/util"
 	"github.com/turt2live/matrix-media-repo/util/errs"
@@ -32,7 +33,7 @@ func PreviewUrl(w http.ResponseWriter, r *http.Request, log *logrus.Entry) inter
 	}
 
 	accessToken := util.GetAccessTokenFromRequest(r)
-	userId, err := util.GetUserIdFromToken(r.Context(), r.Host, accessToken)
+	userId, err := matrix.GetUserIdFromToken(r.Context(), r.Host, accessToken)
 	if err != nil || userId == "" {
 		if err != nil {
 			log.Error("Error verifying token: " + err.Error())
diff --git a/src/github.com/turt2live/matrix-media-repo/client/r0/upload.go b/src/github.com/turt2live/matrix-media-repo/client/r0/upload.go
index 5aff4d06..6f2fc58b 100644
--- a/src/github.com/turt2live/matrix-media-repo/client/r0/upload.go
+++ b/src/github.com/turt2live/matrix-media-repo/client/r0/upload.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/client"
+	"github.com/turt2live/matrix-media-repo/matrix"
 	"github.com/turt2live/matrix-media-repo/services/media_service"
 	"github.com/turt2live/matrix-media-repo/util"
 )
@@ -17,7 +18,7 @@ type MediaUploadedResponse struct {
 
 func UploadMedia(w http.ResponseWriter, r *http.Request, log *logrus.Entry) interface{} {
 	accessToken := util.GetAccessTokenFromRequest(r)
-	userId, err := util.GetUserIdFromToken(r.Context(), r.Host, accessToken)
+	userId, err := matrix.GetUserIdFromToken(r.Context(), r.Host, accessToken)
 	if err != nil || userId == "" {
 		if err != nil {
 			log.Error("Error verifying token: " + err.Error())
diff --git a/src/github.com/turt2live/matrix-media-repo/config/config.go b/src/github.com/turt2live/matrix-media-repo/config/config.go
index 25b92208..a64d4d28 100644
--- a/src/github.com/turt2live/matrix-media-repo/config/config.go
+++ b/src/github.com/turt2live/matrix-media-repo/config/config.go
@@ -18,6 +18,7 @@ type HomeserverConfig struct {
 	Name                 string `yaml:"name"`
 	DownloadRequiresAuth bool   `yaml:"downloadRequiresAuth"`
 	ClientServerApi      string `yaml:"csApi"`
+	BackoffAt            int    `yaml:"backoffAt"`
 }
 
 type GeneralConfig struct {
diff --git a/src/github.com/turt2live/matrix-media-repo/matrix/matrix.go b/src/github.com/turt2live/matrix-media-repo/matrix/matrix.go
new file mode 100644
index 00000000..df6f3918
--- /dev/null
+++ b/src/github.com/turt2live/matrix-media-repo/matrix/matrix.go
@@ -0,0 +1,64 @@
+package matrix
+
+import (
+	"context"
+	"time"
+
+	"github.com/matrix-org/gomatrix"
+	"github.com/rubyist/circuitbreaker"
+	"github.com/turt2live/matrix-media-repo/util"
+)
+
+type userIdResponse struct {
+	UserId string `json:"user_id"`
+}
+
+var breakers = make(map[string]*circuit.Breaker)
+
+func GetUserIdFromToken(ctx context.Context, serverName string, accessToken string) (string, error) {
+	hs := util.GetHomeserverConfig(serverName)
+
+	cb, hasCb := breakers[hs.Name]
+	if !hasCb {
+		backoffAt := int64(hs.BackoffAt)
+		if backoffAt <= 0 {
+			backoffAt = 10 // default to 10 for those who don't have this set
+		}
+		cb = circuit.NewConsecutiveBreaker(backoffAt)
+		breakers[hs.Name] = cb
+	}
+
+	userId := ""
+	var replyError error
+	err := cb.CallContext(ctx, func() error {
+		mtxClient, err := gomatrix.NewClient(hs.ClientServerApi, "", accessToken)
+		if err != nil {
+			return err
+		}
+
+		response := &userIdResponse{}
+		url := mtxClient.BuildURL("/account/whoami")
+		_, err = mtxClient.MakeRequest("GET", url, nil, response)
+		if err != nil {
+			// Unknown token errors should be filtered out explicitly to ensure we don't break on bad requests
+			if httpErr, ok := err.(gomatrix.HTTPError); ok {
+				if respErr, ok := httpErr.WrappedError.(gomatrix.RespError); ok {
+					if respErr.ErrCode == "M_UNKNOWN_TOKEN" {
+						replyError = err // we still want to send the error to the caller though
+						return nil
+					}
+				}
+			}
+			return err
+		}
+
+		userId = response.UserId
+		return nil
+	}, 1*time.Minute)
+
+	if replyError != nil {
+		err = replyError
+	}
+
+	return userId, err
+}
diff --git a/src/github.com/turt2live/matrix-media-repo/util/matrix.go b/src/github.com/turt2live/matrix-media-repo/util/matrix.go
deleted file mode 100644
index 3e20be37..00000000
--- a/src/github.com/turt2live/matrix-media-repo/util/matrix.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package util
-
-import (
-	"context"
-
-	"github.com/matrix-org/gomatrix"
-)
-
-type userIdResponse struct {
-	UserId string `json:"user_id"`
-}
-
-func GetUserIdFromToken(ctx context.Context, serverName string, accessToken string) (string, error) {
-	hs := GetHomeserverConfig(serverName)
-	mtxClient, err := gomatrix.NewClient(hs.ClientServerApi, "", accessToken)
-	if err != nil {
-		return "", err
-	}
-
-	response := &userIdResponse{}
-	url := mtxClient.BuildURL("/account/whoami")
-	_, err = mtxClient.MakeRequest("GET", url, nil, response)
-	if err != nil {
-		return "", err
-	}
-
-	return response.UserId, nil
-}
diff --git a/vendor/manifest b/vendor/manifest
index eb215136..f7ad6172 100644
--- a/vendor/manifest
+++ b/vendor/manifest
@@ -31,6 +31,12 @@
 			"revision": "349dd0209470eabd9514242c688c403c0926d266",
 			"branch": "master"
 		},
+		{
+			"importpath": "github.com/cenk/backoff",
+			"repository": "https://github.com/cenk/backoff",
+			"revision": "2ea60e5f094469f9e65adb9cd103795b73ae743e",
+			"branch": "master"
+		},
 		{
 			"importpath": "github.com/cupcake/sigil",
 			"repository": "https://github.com/cupcake/sigil",
@@ -56,6 +62,12 @@
 			"branch": "master",
 			"path": "/opengraph"
 		},
+		{
+			"importpath": "github.com/facebookgo/clock",
+			"repository": "https://github.com/facebookgo/clock",
+			"revision": "600d898af40aa09a7a93ecb9265d87b0504b6f03",
+			"branch": "master"
+		},
 		{
 			"importpath": "github.com/gorilla/mux",
 			"repository": "https://github.com/gorilla/mux",
@@ -134,6 +146,12 @@
 			"revision": "3bcf86f879c771238f8a67832a1af71308801a47",
 			"branch": "master"
 		},
+		{
+			"importpath": "github.com/rubyist/circuitbreaker",
+			"repository": "https://github.com/rubyist/circuitbreaker",
+			"revision": "2074adba5ddc7d5f7559448a9c3066573521c5bf",
+			"branch": "master"
+		},
 		{
 			"importpath": "github.com/rwcarlsen/goexif",
 			"repository": "https://github.com/rwcarlsen/goexif",
-- 
GitLab