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 }