github.phpd.cn/cilium/cilium@v1.6.12/proxylib/cassandra/cassandraparser.go (about)

     1  // Copyright 2018 Authors of Cilium
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cassandra
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/binary"
    20  	"fmt"
    21  	"regexp"
    22  	"strings"
    23  
    24  	. "github.com/cilium/cilium/proxylib/proxylib"
    25  
    26  	"github.com/cilium/proxy/go/cilium/api"
    27  	log "github.com/sirupsen/logrus"
    28  )
    29  
    30  //
    31  // Cassandra v3/v4 Parser
    32  //
    33  // Spec: https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v4.spec
    34  //
    35  
    36  // Current Cassandra parser supports filtering on messages where the opcode is 'query-like'
    37  // (i.e., opcode 'query', 'prepare', 'batch'.  In those scenarios, we match on query_action and query_table.
    38  // Examples:
    39  // query_action = 'select', query_table = 'system.*'
    40  // query_action = 'insert', query_table = 'attendance.daily_records'
    41  // query_action = 'select', query_table = 'deathstar.scrum_notes'
    42  // query_action = 'insert', query_table = 'covalent.foo'
    43  //
    44  // Batch requests are logged as invidual queries, but an entire batch request will be allowed
    45  // only if all requests are allowed.
    46  
    47  // Non-query client requests, including 'Options', 'Auth_Response', 'Startup', and 'Register'
    48  // are automatically allowed to simplify the policy language.
    49  
    50  // There are known changes in protocol v2 that are not compatible with this parser, see the
    51  // the "Changes from v2" in https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v3.spec
    52  
    53  type CassandraRule struct {
    54  	queryActionExact   string
    55  	tableRegexCompiled *regexp.Regexp
    56  }
    57  
    58  const cassHdrLen = 9
    59  const cassMaxLen = 268435456 // 256 MB, per spec
    60  
    61  const unknownPreparedQueryPath = "/unknown-prepared-query"
    62  
    63  func (rule *CassandraRule) Matches(data interface{}) bool {
    64  	// Cast 'data' to the type we give to 'Matches()'
    65  
    66  	path, ok := data.(string)
    67  	if !ok {
    68  		log.Warning("Matches() called with type other than string")
    69  		return false
    70  	}
    71  	log.Debugf("Policy Match test for '%s'", path)
    72  	regexStr := ""
    73  	if rule.tableRegexCompiled != nil {
    74  		regexStr = rule.tableRegexCompiled.String()
    75  	}
    76  
    77  	log.Debugf("Rule: action '%s', table '%s'", rule.queryActionExact, regexStr)
    78  	if path == unknownPreparedQueryPath {
    79  		log.Warning("Dropping execute for unknown prepared-id")
    80  		return false
    81  	}
    82  	parts := strings.Split(path, "/")
    83  	if len(parts) <= 2 {
    84  		// this is not a query-like request, just allow
    85  		return true
    86  	} else if len(parts) < 4 {
    87  		// should never happen unless we've messed up internally
    88  		// as path is either /<opcode> or /<opcode>/<action>/<table>
    89  		log.Errorf("Invalid parsed path: '%s'", path)
    90  		return false
    91  	}
    92  	if rule.queryActionExact != "" && rule.queryActionExact != parts[2] {
    93  		log.Debugf("CassandraRule: query_action mismatch %v, %s", rule.queryActionExact, parts[1])
    94  		return false
    95  	}
    96  	if len(parts[3]) > 0 &&
    97  		rule.tableRegexCompiled != nil &&
    98  		!rule.tableRegexCompiled.MatchString(parts[3]) {
    99  		log.Debugf("CassandraRule: table_regex mismatch '%v', '%s'", rule.tableRegexCompiled, parts[3])
   100  		return false
   101  	}
   102  
   103  	return true
   104  }
   105  
   106  // CassandraRuleParser parses protobuf L7 rules to enforcement objects
   107  // May panic
   108  func CassandraRuleParser(rule *cilium.PortNetworkPolicyRule) []L7NetworkPolicyRule {
   109  	var rules []L7NetworkPolicyRule
   110  	l7Rules := rule.GetL7Rules()
   111  	if l7Rules == nil {
   112  		return rules
   113  	}
   114  	for _, l7Rule := range l7Rules.GetL7Rules() {
   115  		var cr CassandraRule
   116  		for k, v := range l7Rule.Rule {
   117  			switch k {
   118  			case "query_action":
   119  				cr.queryActionExact = v
   120  			case "query_table":
   121  				if v != "" {
   122  					cr.tableRegexCompiled = regexp.MustCompile(v)
   123  				}
   124  			default:
   125  				ParseError(fmt.Sprintf("Unsupported key: %s", k), rule)
   126  			}
   127  		}
   128  		if len(cr.queryActionExact) > 0 {
   129  			// ensure this is a valid query action
   130  			res := queryActionMap[cr.queryActionExact]
   131  			if res == invalidAction {
   132  				ParseError(fmt.Sprintf("Unable to parse L7 cassandra rule with invalid query_action: '%s'", cr.queryActionExact), rule)
   133  			} else if res == actionNoTable && cr.tableRegexCompiled != nil {
   134  				ParseError(fmt.Sprintf("query_action '%s' is not compatible with a query_table match", cr.queryActionExact), rule)
   135  			}
   136  
   137  		}
   138  
   139  		log.Debugf("Parsed CassandraRule pair: %v", cr)
   140  		rules = append(rules, &cr)
   141  	}
   142  	return rules
   143  }
   144  
   145  type CassandraParserFactory struct{}
   146  
   147  var cassandraParserFactory *CassandraParserFactory
   148  
   149  func init() {
   150  	log.Debug("init(): Registering cassandraParserFactory")
   151  	RegisterParserFactory("cassandra", cassandraParserFactory)
   152  	RegisterL7RuleParser("cassandra", CassandraRuleParser)
   153  }
   154  
   155  type CassandraParser struct {
   156  	connection *Connection
   157  	inserted   bool
   158  	keyspace   string // stores current keyspace name from 'use' command
   159  
   160  	// stores prepared query string while
   161  	// waiting for 'prepared' reply from server
   162  	// with a prepared id.
   163  	// replies associated via stream-id
   164  	preparedQueryPathByStreamID map[uint16]string
   165  
   166  	// allowing us to enforce policy on query
   167  	// at the time of the execute command.
   168  	preparedQueryPathByPreparedID map[string]string // stores query string based on prepared-id,
   169  }
   170  
   171  func (pf *CassandraParserFactory) Create(connection *Connection) Parser {
   172  	log.Debugf("CassandraParserFactory: Create: %v", connection)
   173  
   174  	p := CassandraParser{connection: connection}
   175  	p.preparedQueryPathByStreamID = make(map[uint16]string)
   176  	p.preparedQueryPathByPreparedID = make(map[string]string)
   177  	return &p
   178  }
   179  
   180  func (p *CassandraParser) OnData(reply, endStream bool, dataArray [][]byte) (OpType, int) {
   181  
   182  	// inefficient, but simple for now
   183  	data := bytes.Join(dataArray, []byte{})
   184  
   185  	if len(data) < cassHdrLen {
   186  		// Partial header received, ask for more
   187  		needs := cassHdrLen - len(data)
   188  		log.Debugf("Did not receive full header, need %d more bytes", needs)
   189  		return MORE, needs
   190  	}
   191  
   192  	// full header available, read full request length
   193  	requestLen := binary.BigEndian.Uint32(data[5:9])
   194  	log.Debugf("Request length = %d", requestLen)
   195  	if requestLen > cassMaxLen {
   196  		log.Errorf("Request length of %d is greater than 256 MB", requestLen)
   197  		return ERROR, int(ERROR_INVALID_FRAME_LENGTH)
   198  	}
   199  
   200  	dataMissing := (cassHdrLen + int(requestLen)) - len(data)
   201  	if dataMissing > 0 {
   202  		// full header received, but only partial request
   203  
   204  		log.Debugf("Hdr received, but need %d more bytes of request", dataMissing)
   205  		return MORE, dataMissing
   206  	}
   207  
   208  	// we parse replies, but only to look for prepared-query-id responses
   209  	if reply {
   210  		if len(data) == 0 {
   211  			log.Debugf("ignoring zero length reply call to onData")
   212  			return NOP, 0
   213  
   214  		}
   215  		cassandraParseReply(p, data[0:(cassHdrLen+requestLen)])
   216  
   217  		log.Debugf("reply, passing %d bytes", (cassHdrLen + requestLen))
   218  		return PASS, (cassHdrLen + int(requestLen))
   219  	}
   220  
   221  	err, paths := cassandraParseRequest(p, data[0:(cassHdrLen+requestLen)])
   222  	if err != 0 {
   223  		log.Errorf("Parsing error %d", err)
   224  		return ERROR, int(err)
   225  	}
   226  
   227  	log.Debugf("Request paths = %s", paths)
   228  
   229  	matches := true
   230  	access_log_entry_type := cilium.EntryType_Request
   231  	unpreparedQuery := false
   232  
   233  	for i := 0; i < len(paths); i++ {
   234  		if strings.HasPrefix(paths[i], "/query/use/") ||
   235  			strings.HasPrefix(paths[i], "/batch/use/") ||
   236  			strings.HasPrefix(paths[i], "/prepare/use/") {
   237  			// do not count a "use" query as a deny
   238  			continue
   239  		}
   240  
   241  		if paths[i] == unknownPreparedQueryPath {
   242  			matches = false
   243  			unpreparedQuery = true
   244  			access_log_entry_type = cilium.EntryType_Denied
   245  			break
   246  		}
   247  
   248  		if !p.connection.Matches(paths[i]) {
   249  			matches = false
   250  			access_log_entry_type = cilium.EntryType_Denied
   251  			break
   252  		}
   253  	}
   254  
   255  	for i := 0; i < len(paths); i++ {
   256  		parts := strings.Split(paths[i], "/")
   257  		fields := map[string]string{}
   258  
   259  		if len(parts) >= 3 && parts[2] == "use" {
   260  			// do not log 'use' queries
   261  			continue
   262  		} else if len(parts) == 4 {
   263  			fields["query_action"] = parts[2]
   264  			fields["query_table"] = parts[3]
   265  		} else if unpreparedQuery == true {
   266  			fields["error"] = "unknown prepared query id"
   267  		} else {
   268  			// do not log non-query accesses
   269  			continue
   270  		}
   271  
   272  		p.connection.Log(access_log_entry_type,
   273  			&cilium.LogEntry_GenericL7{
   274  				GenericL7: &cilium.L7LogEntry{
   275  					Proto:  "cassandra",
   276  					Fields: fields,
   277  				},
   278  			})
   279  
   280  	}
   281  
   282  	if !matches {
   283  
   284  		// If we have already sent another error to the client,
   285  		// do not send unauthorized message
   286  		if !unpreparedQuery {
   287  			unauthMsg := make([]byte, len(unauthMsgBase))
   288  			copy(unauthMsg, unauthMsgBase)
   289  			// We want to use the same protocol and stream ID
   290  			// as the incoming request.
   291  			// update the protocol to match the request
   292  			unauthMsg[0] = 0x80 | (data[0] & 0x07)
   293  			// update the stream ID to match the request
   294  			unauthMsg[2] = data[2]
   295  			unauthMsg[3] = data[3]
   296  			p.connection.Inject(true, unauthMsg)
   297  		}
   298  		return DROP, int(cassHdrLen + requestLen)
   299  	}
   300  
   301  	return PASS, int(cassHdrLen + requestLen)
   302  }
   303  
   304  // A full response (header + body) to be used as an
   305  // "unauthorized" error to be sent to cassandra client as part of policy
   306  // deny.   Array must be updated to ensure that reply has
   307  // protocol version and stream-id that matches the request.
   308  
   309  var unauthMsgBase = []byte{
   310  	0x0,      // version (uint8) - must be set before injection
   311  	0x0,      // flags, (uint8)
   312  	0x0, 0x0, // stream-id (uint16) - must be set before injection
   313  	0x0,                 // opcode error (uint8)
   314  	0x0, 0x0, 0x0, 0x1a, // request length (uint32) - update if text changes
   315  	0x0, 0x0, 0x21, 0x00, // 'unauthorized error code' 0x2100 (uint32)
   316  	0x0, 0x14, // length of error msg (uint16)  - update if text changes
   317  	'R', 'e', 'q', 'u', 'e', 's', 't', ' ', 'U', 'n', 'a', 'u', 't', 'h', 'o', 'r', 'i', 'z', 'e', 'd',
   318  }
   319  
   320  // A full response (header + body) to be used as a
   321  // "unprepared" error to be sent to cassandra client if proxy
   322  // does not have the path for this prepare-query-id cached
   323  
   324  var unpreparedMsgBase = []byte{
   325  	0x0,      // version (uint8) - must be set before injection
   326  	0x0,      // flags, (uint8)
   327  	0x0, 0x0, // stream-id (uint16) - must be set before injection
   328  	0x0,                // opcode error (uint8)
   329  	0x0, 0x0, 0x0, 0x0, // request length (uint32) - must be set based on
   330  	// of length of prepared query id
   331  	0x0, 0x0, 0x25, 0x00, // 'unprepared error code' 0x2500 (uint32)
   332  	// must append [short bytes] array of prepared query id.
   333  }
   334  
   335  // create reply byte buffer with error code 'unprepared' with code 0x2500
   336  // followed by a [short bytes] indicating the unknown ID
   337  // must set stream-id of the response to match the request
   338  func createUnpreparedMsg(version byte, streamID []byte, preparedID string) []byte {
   339  
   340  	unpreparedMsg := make([]byte, len(unpreparedMsgBase))
   341  	copy(unpreparedMsg, unpreparedMsgBase)
   342  	unpreparedMsg[0] = 0x80 | version
   343  	unpreparedMsg[2] = streamID[0]
   344  	unpreparedMsg[3] = streamID[1]
   345  
   346  	idLen := len(preparedID)
   347  	idLenBytes := make([]byte, 2)
   348  	binary.BigEndian.PutUint16(idLenBytes, uint16(idLen))
   349  
   350  	reqLen := 4 + 2 + idLen
   351  	reqLenBytes := make([]byte, 4)
   352  	binary.BigEndian.PutUint32(reqLenBytes, uint32(reqLen))
   353  	unpreparedMsg[5] = reqLenBytes[0]
   354  	unpreparedMsg[6] = reqLenBytes[1]
   355  	unpreparedMsg[7] = reqLenBytes[2]
   356  	unpreparedMsg[8] = reqLenBytes[3]
   357  
   358  	res := append(unpreparedMsg, idLenBytes...)
   359  	return append(res, []byte(preparedID)...)
   360  }
   361  
   362  var opcodeMap = map[byte]string{
   363  	0x00: "error",
   364  	0x01: "startup",
   365  	0x02: "ready",
   366  	0x03: "authenticate",
   367  	0x05: "options",
   368  	0x06: "supported",
   369  	0x07: "query",
   370  	0x08: "result",
   371  	0x09: "prepare",
   372  	0x0A: "execute",
   373  	0x0B: "register",
   374  	0x0C: "event",
   375  	0x0D: "batch",
   376  	0x0E: "auth_challenge",
   377  	0x0F: "auth_response",
   378  	0x10: "auth_success",
   379  }
   380  
   381  // map to test whether a 'query_action' is valid or not
   382  
   383  const invalidAction = 0
   384  const actionWithTable = 1
   385  const actionNoTable = 2
   386  
   387  var queryActionMap = map[string]int{
   388  	"select":         actionWithTable,
   389  	"delete":         actionWithTable,
   390  	"insert":         actionWithTable,
   391  	"update":         actionWithTable,
   392  	"create-table":   actionWithTable,
   393  	"drop-table":     actionWithTable,
   394  	"alter-table":    actionWithTable,
   395  	"truncate-table": actionWithTable,
   396  
   397  	// these queries take a keyspace
   398  	// and match against query_table
   399  	"use":             actionWithTable,
   400  	"create-keyspace": actionWithTable,
   401  	"alter-keyspace":  actionWithTable,
   402  	"drop-keyspace":   actionWithTable,
   403  
   404  	"drop-index":               actionNoTable,
   405  	"create-index":             actionNoTable, // TODO: we could tie this to table if we want
   406  	"create-materialized-view": actionNoTable,
   407  	"drop-materialized-view":   actionNoTable,
   408  
   409  	// TODO: these admin ops could be bundled into meta roles
   410  	// (e.g., role-mgmt, permission-mgmt)
   411  	"create-role":       actionNoTable,
   412  	"alter-role":        actionNoTable,
   413  	"drop-role":         actionNoTable,
   414  	"grant-role":        actionNoTable,
   415  	"revoke-role":       actionNoTable,
   416  	"list-roles":        actionNoTable,
   417  	"grant-permission":  actionNoTable,
   418  	"revoke-permission": actionNoTable,
   419  	"list-permissions":  actionNoTable,
   420  	"create-user":       actionNoTable,
   421  	"alter-user":        actionNoTable,
   422  	"drop-user":         actionNoTable,
   423  	"list-users":        actionNoTable,
   424  
   425  	"create-function":  actionNoTable,
   426  	"drop-function":    actionNoTable,
   427  	"create-aggregate": actionNoTable,
   428  	"drop-aggregate":   actionNoTable,
   429  	"create-type":      actionNoTable,
   430  	"alter-type":       actionNoTable,
   431  	"drop-type":        actionNoTable,
   432  	"create-trigger":   actionNoTable,
   433  	"drop-trigger":     actionNoTable,
   434  }
   435  
   436  func parseQuery(p *CassandraParser, query string) (string, string) {
   437  	var action string
   438  	var table string
   439  
   440  	query = strings.TrimRight(query, ";")            // remove potential trailing ;
   441  	fields := strings.Fields(strings.ToLower(query)) // handles all whitespace
   442  
   443  	// we currently do not strip comments.  It seems like cqlsh does
   444  	// strip comments, but its not clear if that can be assumed of all clients
   445  	// It should not be possible to "spoof" the 'action' as this is assumed to be
   446  	// the first token (leaving no room for a comment to start), but it could potentially
   447  	// trick this parser into thinking we're accessing table X, when in fact the
   448  	// query accesses table Y, which would obviously be a security vulnerability
   449  	// As a result, we look at each token here, and if any of them match the comment
   450  	// characters for cassandra, we fail parsing.
   451  	for i := 0; i < len(fields); i++ {
   452  		if len(fields[i]) >= 2 &&
   453  			(fields[i][:2] == "--" ||
   454  				fields[i][:2] == "/*" ||
   455  				fields[i][:2] == "//") {
   456  
   457  			log.Warnf("Unable to safely parse query with comments '%s'", query)
   458  			return "", ""
   459  		}
   460  	}
   461  	if len(fields) < 2 {
   462  		goto invalidQuery
   463  	}
   464  
   465  	action = fields[0]
   466  	switch action {
   467  	case "select", "delete":
   468  		for i := 1; i < len(fields); i++ {
   469  			if fields[i] == "from" {
   470  				table = strings.ToLower(fields[i+1])
   471  			}
   472  		}
   473  		if len(table) == 0 {
   474  			log.Warnf("Unable to parse table name from query '%s'", query)
   475  			return "", ""
   476  		}
   477  	case "insert":
   478  		// INSERT into <table-name>
   479  		if len(fields) < 3 {
   480  			goto invalidQuery
   481  		}
   482  		table = strings.ToLower(fields[2])
   483  	case "update":
   484  		// UPDATE <table-name>
   485  		table = strings.ToLower(fields[1])
   486  	case "use":
   487  		p.keyspace = strings.Trim(fields[1], "\"\\'")
   488  		log.Debugf("Saving keyspace '%s'", p.keyspace)
   489  		table = p.keyspace
   490  	case "alter", "create", "drop", "truncate", "list":
   491  
   492  		action = strings.Join([]string{action, fields[1]}, "-")
   493  		if fields[1] == "table" || fields[1] == "keyspace" {
   494  
   495  			if len(fields) < 3 {
   496  				goto invalidQuery
   497  			}
   498  			table = fields[2]
   499  			if table == "if" {
   500  				if action == "create-table" {
   501  					if len(fields) < 6 {
   502  						goto invalidQuery
   503  					}
   504  					// handle optional "IF NOT EXISTS"
   505  					table = fields[5]
   506  				} else if action == "drop-table" || action == "drop-keyspace" {
   507  					if len(fields) < 5 {
   508  						goto invalidQuery
   509  					}
   510  					// handle optional "IF EXISTS"
   511  					table = fields[4]
   512  				}
   513  			}
   514  		}
   515  		if action == "truncate" && len(fields) == 2 {
   516  			// special case, truncate can just be passed table name
   517  			table = fields[1]
   518  		}
   519  		if fields[1] == "materialized" {
   520  			action = action + "-view"
   521  		} else if fields[1] == "custom" {
   522  			action = "create-index"
   523  		}
   524  	default:
   525  		goto invalidQuery
   526  	}
   527  
   528  	if len(table) > 0 && !strings.Contains(table, ".") && action != "use" {
   529  		table = p.keyspace + "." + table
   530  	}
   531  	return action, table
   532  
   533  invalidQuery:
   534  
   535  	log.Errorf("Unable to parse query: '%s'", query)
   536  	return "", ""
   537  }
   538  
   539  func cassandraParseRequest(p *CassandraParser, data []byte) (OpError, []string) {
   540  
   541  	direction := data[0] & 0x80 // top bit
   542  	if direction != 0 {
   543  		log.Errorf("Direction bit is 'reply', but we are trying to parse a request")
   544  		return ERROR_INVALID_FRAME_TYPE, nil
   545  	}
   546  
   547  	compressionFlag := data[1] & 0x01
   548  	if compressionFlag == 1 {
   549  		log.Errorf("Compression flag set, unable to parse request beyond the header")
   550  		return ERROR_INVALID_FRAME_TYPE, nil
   551  	}
   552  
   553  	opcode := data[4]
   554  	path := opcodeMap[opcode]
   555  
   556  	// parse query string from query/prepare/batch requests
   557  
   558  	// NOTE: parsing only prepare statements and passing all execute
   559  	// statements requires that we 'invalidate' all execute statements
   560  	// anytime policy changes, to ensure that no execute statements are
   561  	// allowed that correspond to prepared queries that would no longer
   562  	// be valid.   A better option might be to cache all prepared queries,
   563  	// mapping the execution ID to allow/deny each time policy is changed.
   564  	if opcode == 0x07 || opcode == 0x09 {
   565  		// query || prepare
   566  		queryLen := binary.BigEndian.Uint32(data[9:13])
   567  		endIndex := 13 + queryLen
   568  		query := string(data[13:endIndex])
   569  		action, table := parseQuery(p, query)
   570  
   571  		if action == "" {
   572  			return ERROR_INVALID_FRAME_TYPE, nil
   573  		}
   574  
   575  		path = "/" + path + "/" + action + "/" + table
   576  		if opcode == 0x09 {
   577  			// stash 'path' for this prepared query based on stream id
   578  			// rewrite 'opcode' portion of the path to be 'execute' rather than 'prepare'
   579  			streamID := binary.BigEndian.Uint16(data[2:4])
   580  			log.Debugf("Prepare query path '%s' with stream-id %d", path, streamID)
   581  			p.preparedQueryPathByStreamID[streamID] = strings.Replace(path, "prepare", "execute", 1)
   582  		}
   583  		return 0, []string{path}
   584  	} else if opcode == 0x0d {
   585  		// batch
   586  
   587  		numQueries := binary.BigEndian.Uint16(data[10:12])
   588  		paths := make([]string, numQueries)
   589  		log.Debugf("batch query count = %d", numQueries)
   590  		offset := 12
   591  		for i := 0; i < int(numQueries); i++ {
   592  			kind := data[offset]
   593  			if kind == 0 {
   594  				// full query string
   595  				queryLen := int(binary.BigEndian.Uint32(data[offset+1 : offset+5]))
   596  
   597  				query := string(data[offset+5 : offset+5+queryLen])
   598  				action, table := parseQuery(p, query)
   599  
   600  				if action == "" {
   601  					return ERROR_INVALID_FRAME_TYPE, nil
   602  				}
   603  				path = "/" + path + "/" + action + "/" + table
   604  				paths[i] = path
   605  				path = "batch" // reset for next item
   606  				offset = offset + 5 + queryLen
   607  				offset = readPastBatchValues(data, offset)
   608  			} else if kind == 1 {
   609  				// prepared query id
   610  
   611  				idLen := int(binary.BigEndian.Uint16(data[offset+1 : offset+3]))
   612  				preparedID := string(data[offset+3 : (offset + 3 + idLen)])
   613  				log.Debugf("Batch entry with prepared-id = '%s'", preparedID)
   614  				path := p.preparedQueryPathByPreparedID[preparedID]
   615  				if len(path) > 0 {
   616  					paths[i] = path
   617  				} else {
   618  					log.Warnf("No cached entry for prepared-id = '%s' in batch", preparedID)
   619  					unpreparedMsg := createUnpreparedMsg(data[0], data[2:4], preparedID)
   620  					p.connection.Inject(true, unpreparedMsg)
   621  					return 0, []string{unknownPreparedQueryPath}
   622  				}
   623  				offset = offset + 3 + idLen
   624  
   625  				offset = readPastBatchValues(data, offset)
   626  			} else {
   627  				log.Errorf("unexpected value of 'kind' in batch query: %d", kind)
   628  				return ERROR_INVALID_FRAME_TYPE, nil
   629  			}
   630  		}
   631  		return 0, paths
   632  	} else if opcode == 0x0a {
   633  		// execute
   634  
   635  		// parse out prepared query id, and then look up our
   636  		// cached query path for policy evaluation.
   637  		idLen := binary.BigEndian.Uint16(data[9:11])
   638  		preparedID := string(data[11:(11 + idLen)])
   639  		log.Debugf("Execute with prepared-id = '%s'", preparedID)
   640  		path := p.preparedQueryPathByPreparedID[preparedID]
   641  
   642  		if len(path) == 0 {
   643  			log.Warnf("No cached entry for prepared-id = '%s'", preparedID)
   644  			unpreparedMsg := createUnpreparedMsg(data[0], data[2:4], preparedID)
   645  			p.connection.Inject(true, unpreparedMsg)
   646  
   647  			// this path is special-cased in Matches() so that unknown
   648  			// prepared IDs are dropped if any rules are defined
   649  			return 0, []string{unknownPreparedQueryPath}
   650  		}
   651  
   652  		return 0, []string{path}
   653  	} else {
   654  		// other opcode, just return type of opcode
   655  
   656  		return 0, []string{"/" + path}
   657  	}
   658  
   659  }
   660  
   661  func readPastBatchValues(data []byte, initialOffset int) int {
   662  	numValues := int(binary.BigEndian.Uint16(data[initialOffset : initialOffset+2]))
   663  	offset := initialOffset + 2
   664  	for i := 0; i < numValues; i++ {
   665  		valueLen := int(binary.BigEndian.Uint32(data[offset : offset+4]))
   666  		// handle 'null' (-1) and 'not set' (-2) case, where 0 bytes follow
   667  		if valueLen >= 0 {
   668  			offset = offset + 4 + valueLen
   669  		}
   670  	}
   671  	return offset
   672  }
   673  
   674  // reply parsing is very basic, just focusing on parsing RESULT messages that
   675  // contain prepared query IDs so that we can later enforce policy on "execute" requests.
   676  func cassandraParseReply(p *CassandraParser, data []byte) {
   677  
   678  	direction := data[0] & 0x80 // top bit
   679  	if direction != 0x80 {
   680  		log.Errorf("Direction bit is 'request', but we are trying to parse a reply")
   681  		return
   682  	}
   683  
   684  	compressionFlag := data[1] & 0x01
   685  	if compressionFlag == 1 {
   686  		log.Errorf("Compression flag set, unable to parse reply beyond the header")
   687  		return
   688  	}
   689  
   690  	streamID := binary.BigEndian.Uint16(data[2:4])
   691  	log.Debugf("Reply with opcode %d and stream-id %d", data[4], streamID)
   692  	// if this is an opcode == RESULT message of type 'prepared', associate the prepared
   693  	// statement id with the full query string that was included in the
   694  	// associated PREPARE request.  The stream-id in this reply allows us to
   695  	// find the associated prepare query string.
   696  	if data[4] == 0x08 {
   697  		resultKind := binary.BigEndian.Uint32(data[9:13])
   698  		log.Debugf("resultKind = %d", resultKind)
   699  		if resultKind == 0x0004 {
   700  			idLen := binary.BigEndian.Uint16(data[13:15])
   701  			preparedID := string(data[15 : 15+idLen])
   702  			log.Debugf("Result with prepared-id = '%s' for stream-id %d", preparedID, streamID)
   703  			path := p.preparedQueryPathByStreamID[streamID]
   704  			if len(path) > 0 {
   705  				// found cached query path to associate with this preparedID
   706  				p.preparedQueryPathByPreparedID[preparedID] = path
   707  				log.Debugf("Associating query path '%s' with prepared-id %s as part of stream-id %d", path, preparedID, streamID)
   708  			} else {
   709  				log.Warnf("Unable to find prepared query path associated with stream-id %d", streamID)
   710  			}
   711  		}
   712  	}
   713  }