github.com/livekit/protocol@v1.16.1-0.20240517185851-47e4c6bba773/webhook/url_notifier.go (about) 1 // Copyright 2023 LiveKit, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package webhook 16 17 import ( 18 "bytes" 19 "crypto/sha256" 20 "encoding/base64" 21 "sync" 22 "time" 23 24 "github.com/frostbyte73/core" 25 "github.com/hashicorp/go-retryablehttp" 26 "go.uber.org/atomic" 27 "google.golang.org/protobuf/encoding/protojson" 28 29 "github.com/livekit/protocol/auth" 30 "github.com/livekit/protocol/livekit" 31 "github.com/livekit/protocol/logger" 32 ) 33 34 type URLNotifierParams struct { 35 Logger logger.Logger 36 QueueSize int 37 URL string 38 APIKey string 39 APISecret string 40 } 41 42 const defaultQueueSize = 100 43 44 // URLNotifier is a QueuedNotifier that sends a POST request to a Webhook URL. 45 // It will retry on failure, and will drop events if notification fall too far behind 46 type URLNotifier struct { 47 mu sync.RWMutex 48 params URLNotifierParams 49 client *retryablehttp.Client 50 dropped atomic.Int32 51 worker core.QueueWorker 52 } 53 54 func NewURLNotifier(params URLNotifierParams) *URLNotifier { 55 if params.QueueSize == 0 { 56 params.QueueSize = defaultQueueSize 57 } 58 if params.Logger == nil { 59 params.Logger = logger.GetLogger() 60 } 61 62 n := &URLNotifier{ 63 params: params, 64 client: retryablehttp.NewClient(), 65 } 66 n.client.Logger = &logAdapter{} 67 n.worker = core.NewQueueWorker(core.QueueWorkerParams{ 68 QueueSize: params.QueueSize, 69 DropWhenFull: true, 70 OnDropped: func() { n.dropped.Inc() }, 71 }) 72 return n 73 } 74 75 func (n *URLNotifier) SetKeys(apiKey, apiSecret string) { 76 n.mu.Lock() 77 defer n.mu.Unlock() 78 n.params.APIKey = apiKey 79 n.params.APISecret = apiSecret 80 } 81 82 func (n *URLNotifier) QueueNotify(event *livekit.WebhookEvent) error { 83 n.worker.Submit(func() { 84 if err := n.send(event); err != nil { 85 n.params.Logger.Warnw("failed to send webhook", err, "url", n.params.URL, "event", event.Event) 86 n.dropped.Add(event.NumDropped + 1) 87 } else { 88 n.params.Logger.Infow("sent webhook", "url", n.params.URL, "event", event.Event, "eventDetails", logger.Proto(event)) 89 } 90 }) 91 return nil 92 } 93 94 func (n *URLNotifier) Stop(force bool) { 95 if force { 96 n.worker.Kill() 97 } else { 98 n.worker.Drain() 99 } 100 } 101 102 func (n *URLNotifier) send(event *livekit.WebhookEvent) error { 103 // set dropped count 104 event.NumDropped = n.dropped.Swap(0) 105 encoded, err := protojson.Marshal(event) 106 if err != nil { 107 return err 108 } 109 // sign payload 110 sum := sha256.Sum256(encoded) 111 b64 := base64.StdEncoding.EncodeToString(sum[:]) 112 113 n.mu.RLock() 114 apiKey := n.params.APIKey 115 apiSecret := n.params.APISecret 116 n.mu.RUnlock() 117 118 at := auth.NewAccessToken(apiKey, apiSecret). 119 SetValidFor(5 * time.Minute). 120 SetSha256(b64) 121 token, err := at.ToJWT() 122 if err != nil { 123 return err 124 } 125 r, err := retryablehttp.NewRequest("POST", n.params.URL, bytes.NewReader(encoded)) 126 if err != nil { 127 // ignore and continue 128 return err 129 } 130 r.Header.Set(authHeader, token) 131 // use a custom mime type to ensure signature is checked prior to parsing 132 r.Header.Set("content-type", "application/webhook+json") 133 res, err := n.client.Do(r) 134 if err != nil { 135 return err 136 } 137 _ = res.Body.Close() 138 return nil 139 } 140 141 type logAdapter struct{} 142 143 func (l *logAdapter) Printf(string, ...interface{}) {}