github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/redis_signals.go (about)

     1  package gateway
     2  
     3  import (
     4  	"crypto"
     5  	"crypto/sha256"
     6  	"encoding/base64"
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/go-redis/redis"
    15  
    16  	"github.com/TykTechnologies/goverify"
    17  	"github.com/TykTechnologies/tyk/config"
    18  	"github.com/TykTechnologies/tyk/storage"
    19  )
    20  
    21  type NotificationCommand string
    22  
    23  const (
    24  	RedisPubSubChannel = "tyk.cluster.notifications"
    25  
    26  	NoticeApiUpdated             NotificationCommand = "ApiUpdated"
    27  	NoticeApiRemoved             NotificationCommand = "ApiRemoved"
    28  	NoticeApiAdded               NotificationCommand = "ApiAdded"
    29  	NoticeGroupReload            NotificationCommand = "GroupReload"
    30  	NoticePolicyChanged          NotificationCommand = "PolicyChanged"
    31  	NoticeConfigUpdate           NotificationCommand = "NoticeConfigUpdated"
    32  	NoticeDashboardZeroConf      NotificationCommand = "NoticeDashboardZeroConf"
    33  	NoticeDashboardConfigRequest NotificationCommand = "NoticeDashboardConfigRequest"
    34  	NoticeGatewayConfigResponse  NotificationCommand = "NoticeGatewayConfigResponse"
    35  	NoticeGatewayDRLNotification NotificationCommand = "NoticeGatewayDRLNotification"
    36  	NoticeGatewayLENotification  NotificationCommand = "NoticeGatewayLENotification"
    37  	KeySpaceUpdateNotification   NotificationCommand = "KeySpaceUpdateNotification"
    38  )
    39  
    40  // Notification is a type that encodes a message published to a pub sub channel (shared between implementations)
    41  type Notification struct {
    42  	Command       NotificationCommand `json:"command"`
    43  	Payload       string              `json:"payload"`
    44  	Signature     string              `json:"signature"`
    45  	SignatureAlgo crypto.Hash         `json:"algorithm"`
    46  }
    47  
    48  func (n *Notification) Sign() {
    49  	n.SignatureAlgo = crypto.SHA256
    50  	hash := sha256.Sum256([]byte(string(n.Command) + n.Payload + config.Global().NodeSecret))
    51  	n.Signature = hex.EncodeToString(hash[:])
    52  }
    53  
    54  func startPubSubLoop() {
    55  	cacheStore := storage.RedisCluster{}
    56  	cacheStore.Connect()
    57  	// On message, synchronise
    58  	for {
    59  		err := cacheStore.StartPubSubHandler(RedisPubSubChannel, func(v interface{}) {
    60  			handleRedisEvent(v, nil, nil)
    61  		})
    62  		if err != nil {
    63  			pubSubLog.WithField("err", err).Error("Connection to Redis failed, reconnect in 10s")
    64  
    65  			time.Sleep(10 * time.Second)
    66  			pubSubLog.Warning("Reconnecting")
    67  		}
    68  	}
    69  }
    70  
    71  func handleRedisEvent(v interface{}, handled func(NotificationCommand), reloaded func()) {
    72  	message, ok := v.(*redis.Message)
    73  	if !ok {
    74  		return
    75  	}
    76  	notif := Notification{}
    77  	if err := json.Unmarshal([]byte(message.Payload), &notif); err != nil {
    78  		pubSubLog.Error("Unmarshalling message body failed, malformed: ", err)
    79  		return
    80  	}
    81  
    82  	// Add messages to ignore here
    83  	switch notif.Command {
    84  	case NoticeGatewayConfigResponse:
    85  		return
    86  	}
    87  
    88  	// Check for a signature, if not signature found, handle
    89  	if !isPayloadSignatureValid(notif) {
    90  		pubSubLog.Error("Payload signature is invalid!")
    91  		return
    92  	}
    93  
    94  	switch notif.Command {
    95  	case NoticeDashboardZeroConf:
    96  		handleDashboardZeroConfMessage(notif.Payload)
    97  	case NoticeConfigUpdate:
    98  		handleNewConfiguration(notif.Payload)
    99  	case NoticeDashboardConfigRequest:
   100  		handleSendMiniConfig(notif.Payload)
   101  	case NoticeGatewayDRLNotification:
   102  		if config.Global().ManagementNode {
   103  			// DRL is not initialized, going through would
   104  			// be mostly harmless but would flood the log
   105  			// with warnings since DRLManager.Ready == false
   106  			return
   107  		}
   108  		onServerStatusReceivedHandler(notif.Payload)
   109  	case NoticeGatewayLENotification:
   110  		onLESSLStatusReceivedHandler(notif.Payload)
   111  	case NoticeApiUpdated, NoticeApiRemoved, NoticeApiAdded, NoticePolicyChanged, NoticeGroupReload:
   112  		pubSubLog.Info("Reloading endpoints")
   113  		reloadURLStructure(reloaded)
   114  	case KeySpaceUpdateNotification:
   115  		handleKeySpaceEventCacheFlush(notif.Payload)
   116  	default:
   117  		pubSubLog.Warnf("Unknown notification command: %q", notif.Command)
   118  		return
   119  	}
   120  	if handled != nil {
   121  		// went through. all others shoul have returned early.
   122  		handled(notif.Command)
   123  	}
   124  }
   125  
   126  func handleKeySpaceEventCacheFlush(payload string) {
   127  
   128  	keys := strings.Split(payload, ",")
   129  
   130  	for _, key := range keys {
   131  		splitKeys := strings.Split(key, ":")
   132  		if len(splitKeys) > 1 {
   133  			key = splitKeys[0]
   134  		}
   135  
   136  		RPCGlobalCache.Delete("apikey-" + key)
   137  		SessionCache.Delete(key)
   138  	}
   139  }
   140  
   141  var redisInsecureWarn sync.Once
   142  var notificationVerifier goverify.Verifier
   143  
   144  func isPayloadSignatureValid(notification Notification) bool {
   145  	if config.Global().AllowInsecureConfigs {
   146  		return true
   147  	}
   148  
   149  	switch notification.SignatureAlgo {
   150  	case crypto.SHA256:
   151  		hash := sha256.Sum256([]byte(string(notification.Command) + notification.Payload + config.Global().NodeSecret))
   152  		expectedSignature := hex.EncodeToString(hash[:])
   153  
   154  		if expectedSignature == notification.Signature {
   155  			return true
   156  		} else {
   157  			pubSubLog.Error("Notification signer: Failed verifying pub sub signature using node_secret: ")
   158  			return false
   159  		}
   160  	default:
   161  		if config.Global().PublicKeyPath != "" && notificationVerifier == nil {
   162  			var err error
   163  
   164  			notificationVerifier, err = goverify.LoadPublicKeyFromFile(config.Global().PublicKeyPath)
   165  			if err != nil {
   166  
   167  				pubSubLog.Error("Notification signer: Failed loading private key from path: ", err)
   168  				return false
   169  			}
   170  		}
   171  
   172  		if notificationVerifier != nil {
   173  
   174  			signed, err := base64.StdEncoding.DecodeString(notification.Signature)
   175  			if err != nil {
   176  
   177  				pubSubLog.Error("Failed to decode signature: ", err)
   178  				return false
   179  			}
   180  
   181  			if err := notificationVerifier.Verify([]byte(notification.Payload), signed); err != nil {
   182  
   183  				pubSubLog.Error("Could not verify notification: ", err, ": ", notification)
   184  
   185  				return false
   186  			}
   187  
   188  			return true
   189  		}
   190  	}
   191  
   192  	return false
   193  }
   194  
   195  // RedisNotifier will use redis pub/sub channels to send notifications
   196  type RedisNotifier struct {
   197  	store   *storage.RedisCluster
   198  	channel string
   199  }
   200  
   201  // Notify will send a notification to a channel
   202  func (r *RedisNotifier) Notify(notif interface{}) bool {
   203  	if n, ok := notif.(Notification); ok {
   204  		n.Sign()
   205  		notif = n
   206  	}
   207  
   208  	toSend, err := json.Marshal(notif)
   209  
   210  	if err != nil {
   211  
   212  		pubSubLog.Error("Problem marshalling notification: ", err)
   213  		return false
   214  	}
   215  
   216  	// pubSubLog.Debug("Sending notification", notif)
   217  
   218  	if err := r.store.Publish(r.channel, string(toSend)); err != nil {
   219  
   220  		pubSubLog.Error("Could not send notification: ", err)
   221  		return false
   222  	}
   223  
   224  	return true
   225  }
   226  
   227  type dashboardConfigPayload struct {
   228  	DashboardConfig struct {
   229  		Hostname string
   230  		Port     int
   231  		UseTLS   bool
   232  	}
   233  	TimeStamp int64
   234  }
   235  
   236  func createConnectionStringFromDashboardObject(config dashboardConfigPayload) string {
   237  
   238  	hostname := "http://"
   239  
   240  	if config.DashboardConfig.UseTLS {
   241  		hostname = "https://"
   242  	}
   243  
   244  	hostname += config.DashboardConfig.Hostname
   245  
   246  	if config.DashboardConfig.Port != 0 {
   247  
   248  		hostname = strings.TrimRight(hostname, "/")
   249  		hostname += ":" + strconv.Itoa(config.DashboardConfig.Port)
   250  	}
   251  
   252  	return hostname
   253  }
   254  
   255  func handleDashboardZeroConfMessage(payload string) {
   256  	// Decode the configuration from the payload
   257  	dashPayload := dashboardConfigPayload{}
   258  
   259  	if err := json.Unmarshal([]byte(payload), &dashPayload); err != nil {
   260  
   261  		pubSubLog.Error("Failed to decode dashboard zeroconf payload")
   262  		return
   263  	}
   264  
   265  	globalConf := config.Global()
   266  
   267  	if !globalConf.UseDBAppConfigs || globalConf.DisableDashboardZeroConf {
   268  		return
   269  	}
   270  
   271  	hostname := createConnectionStringFromDashboardObject(dashPayload)
   272  	setHostname := false
   273  
   274  	if globalConf.DBAppConfOptions.ConnectionString == "" {
   275  		globalConf.DBAppConfOptions.ConnectionString = hostname
   276  		setHostname = true
   277  	}
   278  
   279  	if globalConf.Policies.PolicyConnectionString == "" {
   280  		globalConf.Policies.PolicyConnectionString = hostname
   281  		setHostname = true
   282  	}
   283  
   284  	if setHostname {
   285  		config.SetGlobal(globalConf)
   286  		pubSubLog.Info("Hostname set with dashboard zeroconf signal")
   287  	}
   288  }