github.com/Jeffail/benthos/v3@v3.65.0/lib/input/reader/amqp_1.go (about) 1 package reader 2 3 import ( 4 "context" 5 "crypto/tls" 6 "errors" 7 "fmt" 8 "math/rand" 9 "sync" 10 "time" 11 12 "github.com/Azure/go-amqp" 13 "github.com/Jeffail/benthos/v3/lib/log" 14 "github.com/Jeffail/benthos/v3/lib/message" 15 "github.com/Jeffail/benthos/v3/lib/metrics" 16 "github.com/Jeffail/benthos/v3/lib/types" 17 "github.com/Jeffail/benthos/v3/lib/util/amqp/sasl" 18 btls "github.com/Jeffail/benthos/v3/lib/util/tls" 19 ) 20 21 // AMQP1Config contains configuration for the AMQP1 input type. 22 type AMQP1Config struct { 23 URL string `json:"url" yaml:"url"` 24 SourceAddress string `json:"source_address" yaml:"source_address"` 25 AzureRenewLock bool `json:"azure_renew_lock" yaml:"azure_renew_lock"` 26 TLS btls.Config `json:"tls" yaml:"tls"` 27 SASL sasl.Config `json:"sasl" yaml:"sasl"` 28 } 29 30 // NewAMQP1Config creates a new AMQP1Config with default values. 31 func NewAMQP1Config() AMQP1Config { 32 return AMQP1Config{ 33 URL: "", 34 SourceAddress: "", 35 TLS: btls.NewConfig(), 36 SASL: sasl.NewConfig(), 37 } 38 } 39 40 //------------------------------------------------------------------------------ 41 42 type amqp1Conn struct { 43 client *amqp.Client 44 session *amqp.Session 45 receiver *amqp.Receiver 46 renewLockReceiver *amqp.Receiver 47 renewLockSender *amqp.Sender 48 49 log log.Modular 50 lockRenewAddressPrefix string 51 } 52 53 func (c *amqp1Conn) Close(ctx context.Context) { 54 if c.renewLockSender != nil { 55 if err := c.renewLockSender.Close(ctx); err != nil { 56 c.log.Errorf("Failed to cleanly close renew lock sender: %v\n", err) 57 } 58 } 59 if c.renewLockReceiver != nil { 60 if err := c.renewLockReceiver.Close(ctx); err != nil { 61 c.log.Errorf("Failed to cleanly close renew lock receiver: %v\n", err) 62 } 63 } 64 if c.receiver != nil { 65 if err := c.receiver.Close(ctx); err != nil { 66 c.log.Errorf("Failed to cleanly close receiver: %v\n", err) 67 } 68 } 69 if c.session != nil { 70 if err := c.session.Close(ctx); err != nil { 71 c.log.Errorf("Failed to cleanly close session: %v\n", err) 72 } 73 } 74 if c.client != nil { 75 if err := c.client.Close(); err != nil { 76 c.log.Errorf("Failed to cleanly close client: %v\n", err) 77 } 78 } 79 } 80 81 //------------------------------------------------------------------------------ 82 83 // AMQP1 is an input type that reads messages via the AMQP 1.0 protocol. 84 type AMQP1 struct { 85 tlsConf *tls.Config 86 87 conf AMQP1Config 88 stats metrics.Type 89 log log.Modular 90 91 m sync.RWMutex 92 conn *amqp1Conn 93 } 94 95 // NewAMQP1 creates a new AMQP1 input type. 96 func NewAMQP1(conf AMQP1Config, log log.Modular, stats metrics.Type) (*AMQP1, error) { 97 a := AMQP1{ 98 conf: conf, 99 stats: stats, 100 log: log, 101 } 102 if conf.TLS.Enabled { 103 var err error 104 if a.tlsConf, err = conf.TLS.Get(); err != nil { 105 return nil, err 106 } 107 } 108 return &a, nil 109 } 110 111 //------------------------------------------------------------------------------ 112 113 // ConnectWithContext establishes a connection to an AMQP1 server. 114 func (a *AMQP1) ConnectWithContext(ctx context.Context) error { 115 a.m.Lock() 116 defer a.m.Unlock() 117 118 if a.conn != nil { 119 return nil 120 } 121 122 conn := &amqp1Conn{ 123 log: a.log, 124 lockRenewAddressPrefix: randomString(15), 125 } 126 127 opts, err := a.conf.SASL.ToOptFns() 128 if err != nil { 129 return err 130 } 131 if a.conf.TLS.Enabled { 132 opts = append(opts, amqp.ConnTLS(true), amqp.ConnTLSConfig(a.tlsConf)) 133 } 134 135 // Create client 136 if conn.client, err = amqp.Dial(a.conf.URL, opts...); err != nil { 137 return err 138 } 139 140 // Open a session 141 if conn.session, err = conn.client.NewSession(); err != nil { 142 conn.Close(ctx) 143 return err 144 } 145 146 // Create a receiver 147 if conn.receiver, err = conn.session.NewReceiver( 148 amqp.LinkSourceAddress(a.conf.SourceAddress), 149 amqp.LinkCredit(10), 150 ); err != nil { 151 conn.Close(ctx) 152 return err 153 } 154 155 if a.conf.AzureRenewLock { 156 managementAddress := a.conf.SourceAddress + "/$management" 157 158 conn.renewLockSender, err = conn.session.NewSender( 159 amqp.LinkSourceAddress(conn.lockRenewAddressPrefix+lockRenewRequestSuffix), 160 amqp.LinkTargetAddress(managementAddress), 161 ) 162 if err != nil { 163 conn.Close(ctx) 164 return err 165 } 166 conn.renewLockReceiver, err = conn.session.NewReceiver( 167 amqp.LinkSourceAddress(managementAddress), 168 amqp.LinkTargetAddress(conn.lockRenewAddressPrefix+lockRenewResponseSuffix), 169 ) 170 if err != nil { 171 conn.Close(ctx) 172 return err 173 } 174 } 175 176 a.conn = conn 177 a.log.Infof("Receiving AMQP 1.0 messages from source: %v\n", a.conf.SourceAddress) 178 return nil 179 } 180 181 // disconnect safely closes a connection to an AMQP1 server. 182 func (a *AMQP1) disconnect(ctx context.Context) error { 183 a.m.Lock() 184 defer a.m.Unlock() 185 186 if a.conn != nil { 187 a.conn.Close(ctx) 188 } 189 a.conn = nil 190 return nil 191 } 192 193 //------------------------------------------------------------------------------ 194 195 // ReadWithContext a new AMQP1 message. 196 func (a *AMQP1) ReadWithContext(ctx context.Context) (types.Message, AsyncAckFn, error) { 197 a.m.RLock() 198 conn := a.conn 199 a.m.RUnlock() 200 201 if conn == nil { 202 return nil, nil, types.ErrNotConnected 203 } 204 205 // Receive next message 206 amqpMsg, err := conn.receiver.Receive(ctx) 207 if err != nil { 208 if err == amqp.ErrTimeout { 209 err = types.ErrTimeout 210 } else { 211 if dErr, isDetachError := err.(*amqp.DetachError); isDetachError && dErr.RemoteError != nil { 212 a.log.Errorf("Lost connection due to: %v\n", dErr.RemoteError) 213 } else { 214 a.log.Errorf("Lost connection due to: %v\n", err) 215 } 216 a.disconnect(ctx) 217 err = types.ErrNotConnected 218 } 219 return nil, nil, err 220 } 221 222 msg := message.New(nil) 223 224 part := message.NewPart(amqpMsg.GetData()) 225 226 if amqpMsg.Properties != nil { 227 setMetadata(part, "amqp_content_type", amqpMsg.Properties.ContentType) 228 setMetadata(part, "amqp_content_encoding", amqpMsg.Properties.ContentEncoding) 229 setMetadata(part, "amqp_creation_time", amqpMsg.Properties.CreationTime) 230 } 231 if amqpMsg.Annotations != nil { 232 for k, v := range amqpMsg.Annotations { 233 keyStr, keyIsStr := k.(string) 234 valStr, valIsStr := v.(string) 235 if keyIsStr && valIsStr { 236 setMetadata(part, keyStr, valStr) 237 } 238 } 239 } 240 241 msg.Append(part) 242 243 var done chan struct{} 244 if a.conf.AzureRenewLock { 245 done = a.startRenewJob(amqpMsg) 246 } 247 248 return msg, func(ctx context.Context, res types.Response) error { 249 if done != nil { 250 close(done) 251 done = nil 252 } 253 254 // TODO: These methods were moved in v0.16.0, but nacking seems broken 255 // (integration tests fail) 256 if res.Error() != nil { 257 return conn.receiver.ModifyMessage(ctx, amqpMsg, true, false, amqpMsg.Annotations) 258 } 259 return conn.receiver.AcceptMessage(ctx, amqpMsg) 260 }, nil 261 } 262 263 // CloseAsync shuts down the AMQP1 input and stops processing requests. 264 func (a *AMQP1) CloseAsync() { 265 a.disconnect(context.Background()) 266 } 267 268 // WaitForClose blocks until the AMQP1 input has closed down. 269 func (a *AMQP1) WaitForClose(timeout time.Duration) error { 270 return nil 271 } 272 273 //------------------------------------------------------------------------------ 274 275 const ( 276 lockRenewResponseSuffix = "-response" 277 lockRenewRequestSuffix = "-request" 278 ) 279 280 const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 281 282 var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) 283 284 func randomString(n int) string { 285 b := make([]byte, n) 286 for i := range b { 287 b[i] = letterBytes[seededRand.Intn(len(letterBytes))] 288 } 289 return string(b) 290 } 291 292 func (a *AMQP1) startRenewJob(amqpMsg *amqp.Message) chan struct{} { 293 done := make(chan struct{}) 294 go func() { 295 ctx := context.Background() 296 297 lockedUntil, ok := amqpMsg.Annotations["x-opt-locked-until"].(time.Time) 298 if !ok { 299 a.log.Errorln("Missing x-opt-locked-until annotation in received message") 300 return 301 } 302 303 for { 304 select { 305 case <-done: 306 return 307 case <-time.After(time.Until(lockedUntil) / 10 * 9): 308 var err error 309 lockedUntil, err = a.renewWithContext(ctx, amqpMsg) 310 if err != nil { 311 a.log.Errorf("Unable to renew lock err: %v", err) 312 return 313 } 314 315 a.log.Tracef("Renewed lock until %v", lockedUntil) 316 } 317 } 318 }() 319 return done 320 } 321 322 func uuidFromLockTokenBytes(bytes []byte) (*amqp.UUID, error) { 323 if len(bytes) != 16 { 324 return nil, fmt.Errorf("invalid lock token, token was not 16 bytes long") 325 } 326 327 var swapIndex = func(indexOne, indexTwo int, array *[16]byte) { 328 array[indexOne], array[indexTwo] = array[indexTwo], array[indexOne] 329 } 330 331 // Get lock token from the deliveryTag 332 var lockTokenBytes [16]byte 333 copy(lockTokenBytes[:], bytes[:16]) 334 // translate from .net guid byte serialisation format to amqp rfc standard 335 swapIndex(0, 3, &lockTokenBytes) 336 swapIndex(1, 2, &lockTokenBytes) 337 swapIndex(4, 5, &lockTokenBytes) 338 swapIndex(6, 7, &lockTokenBytes) 339 amqpUUID := amqp.UUID(lockTokenBytes) 340 341 return &amqpUUID, nil 342 } 343 344 func (a *AMQP1) renewWithContext(ctx context.Context, msg *amqp.Message) (time.Time, error) { 345 a.m.RLock() 346 conn := a.conn 347 a.m.RUnlock() 348 349 if conn == nil { 350 return time.Time{}, types.ErrNotConnected 351 } 352 353 lockToken, err := uuidFromLockTokenBytes(msg.DeliveryTag) 354 if err != nil { 355 return time.Time{}, err 356 } 357 358 replyTo := conn.lockRenewAddressPrefix + lockRenewResponseSuffix 359 renewMsg := &amqp.Message{ 360 Properties: &amqp.MessageProperties{ 361 MessageID: msg.Properties.MessageID, 362 ReplyTo: &replyTo, 363 }, 364 ApplicationProperties: map[string]interface{}{ 365 "operation": "com.microsoft:renew-lock", 366 }, 367 Value: map[string]interface{}{ 368 "lock-tokens": []amqp.UUID{*lockToken}, 369 }, 370 } 371 372 err = conn.renewLockSender.Send(ctx, renewMsg) 373 if err != nil { 374 return time.Time{}, err 375 } 376 377 result, err := conn.renewLockReceiver.Receive(ctx) 378 if err != nil { 379 return time.Time{}, err 380 } 381 if statusCode, ok := result.ApplicationProperties["statusCode"].(int32); !ok || statusCode != 200 { 382 return time.Time{}, fmt.Errorf("unsuccessful status code %d, message %s", statusCode, result.ApplicationProperties["statusDescription"]) 383 } 384 385 values, ok := result.Value.(map[string]interface{}) 386 if !ok { 387 return time.Time{}, errors.New("missing value in response message") 388 } 389 390 expirations, ok := values["expirations"].([]time.Time) 391 if !ok || len(expirations) != 1 { 392 return time.Time{}, errors.New("missing expirations filed in response message values") 393 } 394 395 return expirations[0], nil 396 }