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 }