storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/event/target/webhook.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2018 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package target
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/tls"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"net/http"
    29  	"net/url"
    30  	"os"
    31  	"path/filepath"
    32  	"time"
    33  
    34  	"storj.io/minio/pkg/certs"
    35  	"storj.io/minio/pkg/event"
    36  	xnet "storj.io/minio/pkg/net"
    37  )
    38  
    39  // Webhook constants
    40  const (
    41  	WebhookEndpoint   = "endpoint"
    42  	WebhookAuthToken  = "auth_token"
    43  	WebhookQueueDir   = "queue_dir"
    44  	WebhookQueueLimit = "queue_limit"
    45  	WebhookClientCert = "client_cert"
    46  	WebhookClientKey  = "client_key"
    47  
    48  	EnvWebhookEnable     = "MINIO_NOTIFY_WEBHOOK_ENABLE"
    49  	EnvWebhookEndpoint   = "MINIO_NOTIFY_WEBHOOK_ENDPOINT"
    50  	EnvWebhookAuthToken  = "MINIO_NOTIFY_WEBHOOK_AUTH_TOKEN"
    51  	EnvWebhookQueueDir   = "MINIO_NOTIFY_WEBHOOK_QUEUE_DIR"
    52  	EnvWebhookQueueLimit = "MINIO_NOTIFY_WEBHOOK_QUEUE_LIMIT"
    53  	EnvWebhookClientCert = "MINIO_NOTIFY_WEBHOOK_CLIENT_CERT"
    54  	EnvWebhookClientKey  = "MINIO_NOTIFY_WEBHOOK_CLIENT_KEY"
    55  )
    56  
    57  // WebhookArgs - Webhook target arguments.
    58  type WebhookArgs struct {
    59  	Enable     bool            `json:"enable"`
    60  	Endpoint   xnet.URL        `json:"endpoint"`
    61  	AuthToken  string          `json:"authToken"`
    62  	Transport  *http.Transport `json:"-"`
    63  	QueueDir   string          `json:"queueDir"`
    64  	QueueLimit uint64          `json:"queueLimit"`
    65  	ClientCert string          `json:"clientCert"`
    66  	ClientKey  string          `json:"clientKey"`
    67  }
    68  
    69  // Validate WebhookArgs fields
    70  func (w WebhookArgs) Validate() error {
    71  	if !w.Enable {
    72  		return nil
    73  	}
    74  	if w.Endpoint.IsEmpty() {
    75  		return errors.New("endpoint empty")
    76  	}
    77  	if w.QueueDir != "" {
    78  		if !filepath.IsAbs(w.QueueDir) {
    79  			return errors.New("queueDir path should be absolute")
    80  		}
    81  	}
    82  	if w.ClientCert != "" && w.ClientKey == "" || w.ClientCert == "" && w.ClientKey != "" {
    83  		return errors.New("cert and key must be specified as a pair")
    84  	}
    85  	return nil
    86  }
    87  
    88  // WebhookTarget - Webhook target.
    89  type WebhookTarget struct {
    90  	id         event.TargetID
    91  	args       WebhookArgs
    92  	httpClient *http.Client
    93  	store      Store
    94  	loggerOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{})
    95  }
    96  
    97  // ID - returns target ID.
    98  func (target WebhookTarget) ID() event.TargetID {
    99  	return target.id
   100  }
   101  
   102  // HasQueueStore - Checks if the queueStore has been configured for the target
   103  func (target *WebhookTarget) HasQueueStore() bool {
   104  	return target.store != nil
   105  }
   106  
   107  // IsActive - Return true if target is up and active
   108  func (target *WebhookTarget) IsActive() (bool, error) {
   109  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   110  	defer cancel()
   111  
   112  	req, err := http.NewRequestWithContext(ctx, http.MethodHead, target.args.Endpoint.String(), nil)
   113  	if err != nil {
   114  		if xnet.IsNetworkOrHostDown(err, false) {
   115  			return false, errNotConnected
   116  		}
   117  		return false, err
   118  	}
   119  
   120  	resp, err := target.httpClient.Do(req)
   121  	if err != nil {
   122  		if xnet.IsNetworkOrHostDown(err, false) || errors.Is(err, context.DeadlineExceeded) {
   123  			return false, errNotConnected
   124  		}
   125  		return false, err
   126  	}
   127  	io.Copy(ioutil.Discard, resp.Body)
   128  	resp.Body.Close()
   129  	// No network failure i.e response from the target means its up
   130  	return true, nil
   131  }
   132  
   133  // Save - saves the events to the store if queuestore is configured, which will be replayed when the wenhook connection is active.
   134  func (target *WebhookTarget) Save(eventData event.Event) error {
   135  	if target.store != nil {
   136  		return target.store.Put(eventData)
   137  	}
   138  	err := target.send(eventData)
   139  	if err != nil {
   140  		if xnet.IsNetworkOrHostDown(err, false) {
   141  			return errNotConnected
   142  		}
   143  	}
   144  	return err
   145  }
   146  
   147  // send - sends an event to the webhook.
   148  func (target *WebhookTarget) send(eventData event.Event) error {
   149  	objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	key := eventData.S3.Bucket.Name + "/" + objectName
   154  
   155  	data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}})
   156  	if err != nil {
   157  		return err
   158  	}
   159  
   160  	req, err := http.NewRequest("POST", target.args.Endpoint.String(), bytes.NewReader(data))
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	if target.args.AuthToken != "" {
   166  		req.Header.Set("Authorization", "Bearer "+target.args.AuthToken)
   167  	}
   168  
   169  	req.Header.Set("Content-Type", "application/json")
   170  
   171  	resp, err := target.httpClient.Do(req)
   172  	if err != nil {
   173  		target.Close()
   174  		return err
   175  	}
   176  	defer resp.Body.Close()
   177  	io.Copy(ioutil.Discard, resp.Body)
   178  
   179  	if resp.StatusCode < 200 || resp.StatusCode > 299 {
   180  		target.Close()
   181  		return fmt.Errorf("sending event failed with %v", resp.Status)
   182  	}
   183  
   184  	return nil
   185  }
   186  
   187  // Send - reads an event from store and sends it to webhook.
   188  func (target *WebhookTarget) Send(eventKey string) error {
   189  	eventData, eErr := target.store.Get(eventKey)
   190  	if eErr != nil {
   191  		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
   192  		// Such events will not exist and would've been already been sent successfully.
   193  		if os.IsNotExist(eErr) {
   194  			return nil
   195  		}
   196  		return eErr
   197  	}
   198  
   199  	if err := target.send(eventData); err != nil {
   200  		if xnet.IsNetworkOrHostDown(err, false) {
   201  			return errNotConnected
   202  		}
   203  		return err
   204  	}
   205  
   206  	// Delete the event from store.
   207  	return target.store.Del(eventKey)
   208  }
   209  
   210  // Close - does nothing and available for interface compatibility.
   211  func (target *WebhookTarget) Close() error {
   212  	// Close idle connection with "keep-alive" states
   213  	target.httpClient.CloseIdleConnections()
   214  	return nil
   215  }
   216  
   217  // NewWebhookTarget - creates new Webhook target.
   218  func NewWebhookTarget(ctx context.Context, id string, args WebhookArgs, loggerOnce func(ctx context.Context, err error, id interface{}, kind ...interface{}), transport *http.Transport, test bool) (*WebhookTarget, error) {
   219  	var store Store
   220  	target := &WebhookTarget{
   221  		id:         event.TargetID{ID: id, Name: "webhook"},
   222  		args:       args,
   223  		loggerOnce: loggerOnce,
   224  	}
   225  
   226  	if target.args.ClientCert != "" && target.args.ClientKey != "" {
   227  		manager, err := certs.NewManager(ctx, target.args.ClientCert, target.args.ClientKey, tls.LoadX509KeyPair)
   228  		if err != nil {
   229  			return target, err
   230  		}
   231  		transport.TLSClientConfig.GetClientCertificate = manager.GetClientCertificate
   232  	}
   233  	target.httpClient = &http.Client{Transport: transport}
   234  
   235  	if args.QueueDir != "" {
   236  		queueDir := filepath.Join(args.QueueDir, storePrefix+"-webhook-"+id)
   237  		store = NewQueueStore(queueDir, args.QueueLimit)
   238  		if err := store.Open(); err != nil {
   239  			target.loggerOnce(context.Background(), err, target.ID())
   240  			return target, err
   241  		}
   242  		target.store = store
   243  	}
   244  
   245  	_, err := target.IsActive()
   246  	if err != nil {
   247  		if target.store == nil || err != errNotConnected {
   248  			target.loggerOnce(ctx, err, target.ID())
   249  			return target, err
   250  		}
   251  	}
   252  
   253  	if target.store != nil && !test {
   254  		// Replays the events from the store.
   255  		eventKeyCh := replayEvents(target.store, ctx.Done(), target.loggerOnce, target.ID())
   256  		// Start replaying events from the store.
   257  		go sendEvents(target, eventKeyCh, ctx.Done(), target.loggerOnce)
   258  	}
   259  
   260  	return target, nil
   261  }