youtubebeat/vendor/github.com/elastic/beats/packetbeat/protos/amqp/amqp.go

552 lines
15 KiB
Go

// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package amqp
import (
"strconv"
"strings"
"time"
"github.com/elastic/beats/libbeat/beat"
"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/libbeat/logp"
"github.com/elastic/beats/libbeat/monitoring"
"github.com/elastic/beats/packetbeat/protos"
"github.com/elastic/beats/packetbeat/protos/tcp"
)
var (
debugf = logp.MakeDebug("amqp")
detailedf = logp.MakeDebug("amqpdetailed")
)
type amqpPlugin struct {
ports []int
sendRequest bool
sendResponse bool
maxBodyLength int
parseHeaders bool
parseArguments bool
hideConnectionInformation bool
transactions *common.Cache
transactionTimeout time.Duration
results protos.Reporter
//map containing functions associated with different method numbers
methodMap map[codeClass]map[codeMethod]amqpMethod
}
var (
unmatchedRequests = monitoring.NewInt(nil, "amqp.unmatched_requests")
unmatchedResponses = monitoring.NewInt(nil, "amqp.unmatched_responses")
)
func init() {
protos.Register("amqp", New)
}
func New(
testMode bool,
results protos.Reporter,
cfg *common.Config,
) (protos.Plugin, error) {
p := &amqpPlugin{}
config := defaultConfig
if !testMode {
if err := cfg.Unpack(&config); err != nil {
return nil, err
}
}
if err := p.init(results, &config); err != nil {
return nil, err
}
return p, nil
}
func (amqp *amqpPlugin) init(results protos.Reporter, config *amqpConfig) error {
amqp.initMethodMap()
amqp.setFromConfig(config)
if amqp.hideConnectionInformation == false {
amqp.addConnectionMethods()
}
amqp.transactions = common.NewCache(
amqp.transactionTimeout,
protos.DefaultTransactionHashSize)
amqp.transactions.StartJanitor(amqp.transactionTimeout)
amqp.results = results
return nil
}
func (amqp *amqpPlugin) initMethodMap() {
amqp.methodMap = map[codeClass]map[codeMethod]amqpMethod{
connectionCode: {
connectionClose: connectionCloseMethod,
connectionCloseOk: okMethod,
},
channelCode: {
channelClose: channelCloseMethod,
channelCloseOk: okMethod,
},
exchangeCode: {
exchangeDeclare: exchangeDeclareMethod,
exchangeDeclareOk: okMethod,
exchangeDelete: exchangeDeleteMethod,
exchangeDeleteOk: okMethod,
exchangeBind: exchangeBindMethod,
exchangeBindOk: okMethod,
exchangeUnbind: exchangeUnbindMethod,
exchangeUnbindOk: okMethod,
},
queueCode: {
queueDeclare: queueDeclareMethod,
queueDeclareOk: queueDeclareOkMethod,
queueBind: queueBindMethod,
queueBindOk: okMethod,
queueUnbind: queueUnbindMethod,
queueUnbindOk: okMethod,
queuePurge: queuePurgeMethod,
queuePurgeOk: queuePurgeOkMethod,
queueDelete: queueDeleteMethod,
queueDeleteOk: queueDeleteOkMethod,
},
basicCode: {
basicConsume: basicConsumeMethod,
basicConsumeOk: basicConsumeOkMethod,
basicCancel: basicCancelMethod,
basicCancelOk: basicCancelOkMethod,
basicPublish: basicPublishMethod,
basicReturn: basicReturnMethod,
basicDeliver: basicDeliverMethod,
basicGet: basicGetMethod,
basicGetOk: basicGetOkMethod,
basicGetEmpty: basicGetEmptyMethod,
basicAck: basicAckMethod,
basicReject: basicRejectMethod,
basicRecover: basicRecoverMethod,
basicRecoverOk: okMethod,
basicNack: basicNackMethod,
},
txCode: {
txSelect: txSelectMethod,
txSelectOk: okMethod,
txCommit: txCommitMethod,
txCommitOk: okMethod,
txRollback: txRollbackMethod,
txRollbackOk: okMethod,
},
}
}
func (amqp *amqpPlugin) GetPorts() []int {
return amqp.ports
}
func (amqp *amqpPlugin) setFromConfig(config *amqpConfig) {
amqp.ports = config.Ports
amqp.sendRequest = config.SendRequest
amqp.sendResponse = config.SendResponse
amqp.maxBodyLength = config.MaxBodyLength
amqp.parseHeaders = config.ParseHeaders
amqp.parseArguments = config.ParseArguments
amqp.hideConnectionInformation = config.HideConnectionInformation
amqp.transactionTimeout = config.TransactionTimeout
}
func (amqp *amqpPlugin) addConnectionMethods() {
amqp.methodMap[connectionCode][connectionStart] = connectionStartMethod
amqp.methodMap[connectionCode][connectionStartOk] = connectionStartOkMethod
amqp.methodMap[connectionCode][connectionTune] = connectionTuneMethod
amqp.methodMap[connectionCode][connectionTuneOk] = connectionTuneOkMethod
amqp.methodMap[connectionCode][connectionOpen] = connectionOpenMethod
amqp.methodMap[connectionCode][connectionOpenOk] = okMethod
amqp.methodMap[channelCode][channelOpen] = channelOpenMethod
amqp.methodMap[channelCode][channelOpenOk] = okMethod
amqp.methodMap[channelCode][channelFlow] = channelFlowMethod
amqp.methodMap[channelCode][channelFlowOk] = channelFlowOkMethod
amqp.methodMap[basicCode][basicQos] = basicQosMethod
amqp.methodMap[basicCode][basicQosOk] = okMethod
}
func (amqp *amqpPlugin) ConnectionTimeout() time.Duration {
return amqp.transactionTimeout
}
func (amqp *amqpPlugin) Parse(pkt *protos.Packet, tcptuple *common.TCPTuple,
dir uint8, private protos.ProtocolData) protos.ProtocolData {
defer logp.Recover("ParseAmqp exception")
detailedf("Parse method triggered")
priv := amqpPrivateData{}
if private != nil {
var ok bool
priv, ok = private.(amqpPrivateData)
if !ok {
priv = amqpPrivateData{}
}
}
if priv.data[dir] == nil {
priv.data[dir] = &amqpStream{
data: pkt.Payload,
message: &amqpMessage{ts: pkt.Ts},
}
} else {
// concatenate databytes
priv.data[dir].data = append(priv.data[dir].data, pkt.Payload...)
if len(priv.data[dir].data) > tcp.TCPMaxDataInStream {
debugf("Stream data too large, dropping TCP stream")
priv.data[dir] = nil
return priv
}
}
stream := priv.data[dir]
for len(stream.data) > 0 {
if stream.message == nil {
stream.message = &amqpMessage{ts: pkt.Ts}
}
ok, complete := amqp.amqpMessageParser(stream)
if !ok {
// drop this tcp stream. Will retry parsing with the next
// segment in it
priv.data[dir] = nil
return priv
}
if !complete {
break
}
amqp.handleAmqp(stream.message, tcptuple, dir)
}
return priv
}
func (amqp *amqpPlugin) GapInStream(tcptuple *common.TCPTuple, dir uint8,
nbytes int, private protos.ProtocolData) (priv protos.ProtocolData, drop bool) {
detailedf("GapInStream called")
return private, true
}
func (amqp *amqpPlugin) ReceivedFin(tcptuple *common.TCPTuple, dir uint8,
private protos.ProtocolData) protos.ProtocolData {
return private
}
func (amqp *amqpPlugin) handleAmqpRequest(msg *amqpMessage) {
// Add it to the HT
tuple := msg.tcpTuple
trans := amqp.getTransaction(tuple.Hashable())
if trans != nil {
if trans.amqp != nil {
debugf("Two requests without a Response. Dropping old request: %s", trans.amqp)
unmatchedRequests.Add(1)
}
} else {
trans = &amqpTransaction{tuple: tuple}
amqp.transactions.Put(tuple.Hashable(), trans)
}
trans.ts = msg.ts
trans.src, trans.dst = common.MakeEndpointPair(msg.tcpTuple.BaseTuple, msg.cmdlineTuple)
if msg.direction == tcp.TCPDirectionReverse {
trans.src, trans.dst = trans.dst, trans.src
}
trans.method = msg.method
// get the righ request
if len(msg.request) > 0 {
trans.request = strings.Join([]string{msg.method, msg.request}, " ")
} else {
trans.request = msg.method
}
//length = message + 4 bytes header + frame end octet
trans.bytesIn = msg.bodySize + 12
if msg.fields != nil {
trans.amqp = msg.fields
} else {
trans.amqp = common.MapStr{}
}
//if error or exception, publish it now. sometimes client or server never send
//an ack message and the error is lost. Also, if nowait flag set, don't expect
//any response and publish
if isAsynchronous(trans) {
amqp.publishTransaction(trans)
debugf("Amqp transaction completed")
amqp.transactions.Delete(trans.tuple.Hashable())
return
}
if trans.timer != nil {
trans.timer.Stop()
}
trans.timer = time.AfterFunc(transactionTimeout, func() { amqp.expireTransaction(trans) })
}
func (amqp *amqpPlugin) handleAmqpResponse(msg *amqpMessage) {
tuple := msg.tcpTuple
trans := amqp.getTransaction(tuple.Hashable())
if trans == nil || trans.amqp == nil {
debugf("Response from unknown transaction. Ignoring.")
unmatchedResponses.Add(1)
return
}
//length = message + 4 bytes class/method + frame end octet + header
trans.bytesOut = msg.bodySize + 12
//merge the both fields from request and response
trans.amqp.Update(msg.fields)
trans.response = common.OK_STATUS
if msg.method == "basic.get-empty" {
trans.method = "basic.get-empty"
}
trans.responseTime = int32(msg.ts.Sub(trans.ts).Nanoseconds() / 1e6)
trans.notes = msg.notes
amqp.publishTransaction(trans)
debugf("Amqp transaction completed")
// remove from map
amqp.transactions.Delete(trans.tuple.Hashable())
if trans.timer != nil {
trans.timer.Stop()
}
}
func (amqp *amqpPlugin) expireTransaction(trans *amqpTransaction) {
debugf("Transaction expired")
//possibility of a connection.close or channel.close method that didn't get an
//ok answer. Let's publish it.
if isCloseError(trans) {
trans.notes = append(trans.notes, "Close-ok method not received by sender")
amqp.publishTransaction(trans)
}
// remove from map
amqp.transactions.Delete(trans.tuple.Hashable())
}
//This method handles published messages from clients. Being an async
//process, the method, header and body frames are regrouped in one transaction
func (amqp *amqpPlugin) handlePublishing(client *amqpMessage) {
tuple := client.tcpTuple
trans := amqp.getTransaction(tuple.Hashable())
if trans == nil {
trans = &amqpTransaction{tuple: tuple}
amqp.transactions.Put(client.tcpTuple.Hashable(), trans)
}
trans.ts = client.ts
trans.src, trans.dst = common.MakeEndpointPair(client.tcpTuple.BaseTuple, client.cmdlineTuple)
trans.method = client.method
//for publishing and delivering, bytes in and out represent the length of the
//message itself
trans.bytesIn = client.bodySize
if client.bodySize > uint64(amqp.maxBodyLength) {
trans.body = client.body[:amqp.maxBodyLength]
} else {
trans.body = client.body
}
trans.toString = isStringable(client)
trans.amqp = client.fields
amqp.publishTransaction(trans)
debugf("Amqp transaction completed")
//delete trans from map
amqp.transactions.Delete(trans.tuple.Hashable())
}
//This method handles delivered messages via basic.deliver and basic.get-ok AND
//returned messages to clients. Being an async process, the method, header and
//body frames are regrouped in one transaction
func (amqp *amqpPlugin) handleDelivering(server *amqpMessage) {
tuple := server.tcpTuple
trans := amqp.getTransaction(tuple.Hashable())
if trans == nil {
trans = &amqpTransaction{tuple: tuple}
amqp.transactions.Put(server.tcpTuple.Hashable(), trans)
}
trans.ts = server.ts
trans.src, trans.dst = common.MakeEndpointPair(server.tcpTuple.BaseTuple, server.cmdlineTuple)
//for publishing and delivering, bytes in and out represent the length of the
//message itself
trans.bytesOut = server.bodySize
if server.bodySize > uint64(amqp.maxBodyLength) {
trans.body = server.body[:amqp.maxBodyLength]
} else {
trans.body = server.body
}
trans.toString = isStringable(server)
if server.method == "basic.get-ok" {
trans.method = "basic.get"
} else {
trans.method = server.method
}
trans.amqp = server.fields
amqp.publishTransaction(trans)
debugf("Amqp transaction completed")
//delete trans from map
amqp.transactions.Delete(trans.tuple.Hashable())
}
func (amqp *amqpPlugin) publishTransaction(t *amqpTransaction) {
if amqp.results == nil {
return
}
fields := common.MapStr{}
fields["type"] = "amqp"
fields["method"] = t.method
if isError(t) {
fields["status"] = common.ERROR_STATUS
} else {
fields["status"] = common.OK_STATUS
}
fields["responsetime"] = t.responseTime
fields["amqp"] = t.amqp
fields["bytes_out"] = t.bytesOut
fields["bytes_in"] = t.bytesIn
fields["src"] = &t.src
fields["dst"] = &t.dst
//let's try to convert request/response to a readable format
if amqp.sendRequest {
if t.method == "basic.publish" {
if t.toString {
if uint64(len(t.body)) < t.bytesIn {
fields["request"] = string(t.body) + " [...]"
} else {
fields["request"] = string(t.body)
}
} else {
if uint64(len(t.body)) < t.bytesIn {
fields["request"] = bodyToString(t.body) + " [...]"
} else {
fields["request"] = bodyToString(t.body)
}
}
} else {
fields["request"] = t.request
}
}
if amqp.sendResponse {
if t.method == "basic.deliver" || t.method == "basic.return" ||
t.method == "basic.get" {
if t.toString {
if uint64(len(t.body)) < t.bytesOut {
fields["response"] = string(t.body) + " [...]"
} else {
fields["response"] = string(t.body)
}
} else {
if uint64(len(t.body)) < t.bytesOut {
fields["response"] = bodyToString(t.body) + " [...]"
} else {
fields["response"] = bodyToString(t.body)
}
}
} else {
fields["response"] = t.response
}
}
if len(t.notes) > 0 {
fields["notes"] = t.notes
}
amqp.results(beat.Event{
Timestamp: t.ts,
Fields: fields,
})
}
//function to check if method is async or not
func isAsynchronous(trans *amqpTransaction) bool {
if val, ok := trans.amqp["no-wait"]; ok && val == true {
return true
}
return trans.method == "basic.reject" ||
trans.method == "basic.ack" ||
trans.method == "basic.nack"
}
//function to convert a body slice into a readable format
func bodyToString(data []byte) string {
ret := make([]string, len(data))
for i, c := range data {
ret[i] = strconv.Itoa(int(c))
}
return strings.Join(ret, " ")
}
//function used to check if a body message can be converted to readable string
func isStringable(m *amqpMessage) bool {
stringable := false
if contentEncoding, ok := m.fields["content-encoding"].(string); ok &&
contentEncoding != "" {
return false
}
if contentType, ok := m.fields["content-type"].(string); ok {
stringable = strings.Contains(contentType, "text") ||
strings.Contains(contentType, "json")
}
return stringable
}
func (amqp *amqpPlugin) getTransaction(k common.HashableTCPTuple) *amqpTransaction {
v := amqp.transactions.Get(k)
if v != nil {
return v.(*amqpTransaction)
}
return nil
}
func isError(t *amqpTransaction) bool {
return t.method == "basic.return" || t.method == "basic.reject" ||
isCloseError(t)
}
func isCloseError(t *amqpTransaction) bool {
return (t.method == "connection.close" || t.method == "channel.close") &&
getReplyCode(t.amqp) >= 300
}
func getReplyCode(m common.MapStr) uint16 {
code, _ := m["reply-code"].(uint16)
return code
}