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  }