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 }