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{}) {}