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), ¬if); 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 }