github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/lambda/target/webhook.go (about)

     1  // Copyright (c) 2015-2023 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package target
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"crypto/tls"
    24  	"encoding/json"
    25  	"errors"
    26  	"net/http"
    27  	"strings"
    28  	"sync/atomic"
    29  	"syscall"
    30  	"time"
    31  
    32  	"github.com/minio/minio/internal/config/lambda/event"
    33  	xhttp "github.com/minio/minio/internal/http"
    34  	"github.com/minio/minio/internal/logger"
    35  	"github.com/minio/pkg/v2/certs"
    36  	xnet "github.com/minio/pkg/v2/net"
    37  )
    38  
    39  // Webhook constants
    40  const (
    41  	WebhookEndpoint   = "endpoint"
    42  	WebhookAuthToken  = "auth_token"
    43  	WebhookClientCert = "client_cert"
    44  	WebhookClientKey  = "client_key"
    45  
    46  	EnvWebhookEnable     = "MINIO_LAMBDA_WEBHOOK_ENABLE"
    47  	EnvWebhookEndpoint   = "MINIO_LAMBDA_WEBHOOK_ENDPOINT"
    48  	EnvWebhookAuthToken  = "MINIO_LAMBDA_WEBHOOK_AUTH_TOKEN"
    49  	EnvWebhookClientCert = "MINIO_LAMBDA_WEBHOOK_CLIENT_CERT"
    50  	EnvWebhookClientKey  = "MINIO_LAMBDA_WEBHOOK_CLIENT_KEY"
    51  )
    52  
    53  // WebhookArgs - Webhook target arguments.
    54  type WebhookArgs struct {
    55  	Enable     bool            `json:"enable"`
    56  	Endpoint   xnet.URL        `json:"endpoint"`
    57  	AuthToken  string          `json:"authToken"`
    58  	Transport  *http.Transport `json:"-"`
    59  	ClientCert string          `json:"clientCert"`
    60  	ClientKey  string          `json:"clientKey"`
    61  }
    62  
    63  // Validate WebhookArgs fields
    64  func (w WebhookArgs) Validate() error {
    65  	if !w.Enable {
    66  		return nil
    67  	}
    68  	if w.Endpoint.IsEmpty() {
    69  		return errors.New("endpoint empty")
    70  	}
    71  	if w.ClientCert != "" && w.ClientKey == "" || w.ClientCert == "" && w.ClientKey != "" {
    72  		return errors.New("cert and key must be specified as a pair")
    73  	}
    74  	return nil
    75  }
    76  
    77  // WebhookTarget - Webhook target.
    78  type WebhookTarget struct {
    79  	activeRequests int64
    80  	totalRequests  int64
    81  	failedRequests int64
    82  
    83  	lazyInit lazyInit
    84  
    85  	id         event.TargetID
    86  	args       WebhookArgs
    87  	transport  *http.Transport
    88  	httpClient *http.Client
    89  	loggerOnce logger.LogOnce
    90  	cancel     context.CancelFunc
    91  	cancelCh   <-chan struct{}
    92  }
    93  
    94  // ID - returns target ID.
    95  func (target *WebhookTarget) ID() event.TargetID {
    96  	return target.id
    97  }
    98  
    99  // IsActive - Return true if target is up and active
   100  func (target *WebhookTarget) IsActive() (bool, error) {
   101  	if err := target.init(); err != nil {
   102  		return false, err
   103  	}
   104  	return target.isActive()
   105  }
   106  
   107  // errNotConnected - indicates that the target connection is not active.
   108  var errNotConnected = errors.New("not connected to target server/service")
   109  
   110  func (target *WebhookTarget) isActive() (bool, error) {
   111  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   112  	defer cancel()
   113  
   114  	req, err := http.NewRequestWithContext(ctx, http.MethodHead, target.args.Endpoint.String(), nil)
   115  	if err != nil {
   116  		if xnet.IsNetworkOrHostDown(err, false) {
   117  			return false, errNotConnected
   118  		}
   119  		return false, err
   120  	}
   121  	tokens := strings.Fields(target.args.AuthToken)
   122  	switch len(tokens) {
   123  	case 2:
   124  		req.Header.Set("Authorization", target.args.AuthToken)
   125  	case 1:
   126  		req.Header.Set("Authorization", "Bearer "+target.args.AuthToken)
   127  	}
   128  
   129  	resp, err := target.httpClient.Do(req)
   130  	if err != nil {
   131  		if xnet.IsNetworkOrHostDown(err, true) {
   132  			return false, errNotConnected
   133  		}
   134  		return false, err
   135  	}
   136  	xhttp.DrainBody(resp.Body)
   137  	// No network failure i.e response from the target means its up
   138  	return true, nil
   139  }
   140  
   141  // Stat - returns lamdba webhook target statistics such as
   142  // current calls in progress, successfully completed functions
   143  // failed functions.
   144  func (target *WebhookTarget) Stat() event.TargetStat {
   145  	return event.TargetStat{
   146  		ID:             target.id,
   147  		ActiveRequests: atomic.LoadInt64(&target.activeRequests),
   148  		TotalRequests:  atomic.LoadInt64(&target.totalRequests),
   149  		FailedRequests: atomic.LoadInt64(&target.failedRequests),
   150  	}
   151  }
   152  
   153  // Send - sends an event to the webhook.
   154  func (target *WebhookTarget) Send(eventData event.Event) (resp *http.Response, err error) {
   155  	atomic.AddInt64(&target.activeRequests, 1)
   156  	defer atomic.AddInt64(&target.activeRequests, -1)
   157  
   158  	atomic.AddInt64(&target.totalRequests, 1)
   159  	defer func() {
   160  		if err != nil {
   161  			atomic.AddInt64(&target.failedRequests, 1)
   162  		}
   163  	}()
   164  
   165  	if err = target.init(); err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	data, err := json.Marshal(eventData)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	req, err := http.NewRequest(http.MethodPost, target.args.Endpoint.String(), bytes.NewReader(data))
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	// Verify if the authToken already contains
   180  	// <Key> <Token> like format, if this is
   181  	// already present we can blindly use the
   182  	// authToken as is instead of adding 'Bearer'
   183  	tokens := strings.Fields(target.args.AuthToken)
   184  	switch len(tokens) {
   185  	case 2:
   186  		req.Header.Set("Authorization", target.args.AuthToken)
   187  	case 1:
   188  		req.Header.Set("Authorization", "Bearer "+target.args.AuthToken)
   189  	}
   190  
   191  	req.Header.Set("Content-Type", "application/json")
   192  
   193  	return target.httpClient.Do(req)
   194  }
   195  
   196  // Close the target. Will cancel all active requests.
   197  func (target *WebhookTarget) Close() error {
   198  	target.cancel()
   199  	return nil
   200  }
   201  
   202  func (target *WebhookTarget) init() error {
   203  	return target.lazyInit.Do(target.initWebhook)
   204  }
   205  
   206  // Only called from init()
   207  func (target *WebhookTarget) initWebhook() error {
   208  	args := target.args
   209  	transport := target.transport
   210  
   211  	if args.ClientCert != "" && args.ClientKey != "" {
   212  		manager, err := certs.NewManager(context.Background(), args.ClientCert, args.ClientKey, tls.LoadX509KeyPair)
   213  		if err != nil {
   214  			return err
   215  		}
   216  		manager.ReloadOnSignal(syscall.SIGHUP) // allow reloads upon SIGHUP
   217  		transport.TLSClientConfig.GetClientCertificate = manager.GetClientCertificate
   218  	}
   219  	target.httpClient = &http.Client{Transport: transport}
   220  
   221  	yes, err := target.isActive()
   222  	if err != nil {
   223  		return err
   224  	}
   225  	if !yes {
   226  		return errNotConnected
   227  	}
   228  
   229  	return nil
   230  }
   231  
   232  // NewWebhookTarget - creates new Webhook target.
   233  func NewWebhookTarget(ctx context.Context, id string, args WebhookArgs, loggerOnce logger.LogOnce, transport *http.Transport) (*WebhookTarget, error) {
   234  	ctx, cancel := context.WithCancel(ctx)
   235  
   236  	target := &WebhookTarget{
   237  		id:         event.TargetID{ID: id, Name: "webhook"},
   238  		args:       args,
   239  		loggerOnce: loggerOnce,
   240  		transport:  transport,
   241  		cancel:     cancel,
   242  		cancelCh:   ctx.Done(),
   243  	}
   244  
   245  	return target, nil
   246  }