github.com/kubearmor/cilium@v1.6.12/proxylib/memcached/text/parser.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  // text memcache protocol parser based on https://github.com/memcached/memcached/blob/master/doc/protocol.txt
    16  
    17  package text
    18  
    19  import (
    20  	"bytes"
    21  	"strconv"
    22  
    23  	"github.com/cilium/cilium/proxylib/memcached/meta"
    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  // ParserFactory implements proxylib.ParserFactory
    31  type ParserFactory struct{}
    32  
    33  // Create creates memcached parser
    34  func (p *ParserFactory) Create(connection *proxylib.Connection) proxylib.Parser {
    35  	log.Debugf("ParserFactory: Create: %v", connection)
    36  	return &Parser{connection: connection, replyQueue: make([]*replyIntent, 0)}
    37  }
    38  
    39  // compile time check for interface implementation
    40  var _ proxylib.ParserFactory = &ParserFactory{}
    41  
    42  // ParserFactoryInstance creates text parser for unified memcached parser
    43  var ParserFactoryInstance *ParserFactory
    44  
    45  // Parser implements proxylib.Parser
    46  type Parser struct {
    47  	connection *proxylib.Connection
    48  
    49  	replyQueue []*replyIntent
    50  
    51  	//set to true when watch command is observed
    52  	watching bool
    53  }
    54  
    55  type replyIntent struct {
    56  	command []byte
    57  	denied  bool
    58  }
    59  
    60  var _ proxylib.Parser = &Parser{}
    61  
    62  // consts indicating number of tokens in memcache command that indicates noreply command
    63  const (
    64  	casWithNoreplyFields     = 7
    65  	storageWithNoreplyFields = 6
    66  	deleteWithNoreplyFields  = 3
    67  	incrWithNoreplyFields    = 4
    68  	touchWithNoreplyFields   = 4
    69  )
    70  
    71  // OnData parses text memcached data
    72  func (p *Parser) OnData(reply, endStream bool, dataBuffers [][]byte) (proxylib.OpType, int) {
    73  	if reply {
    74  		injected := p.injectFromQueue()
    75  		if injected > 0 {
    76  			return proxylib.INJECT, injected
    77  		}
    78  		if len(dataBuffers) == 0 {
    79  			return proxylib.NOP, 0
    80  		}
    81  	}
    82  
    83  	// TODO: don't copy data to new slices
    84  	data := (bytes.Join(dataBuffers, []byte{}))
    85  	log.Debugf("Data length: %d", len(data))
    86  
    87  	linefeed := bytes.Index(data, []byte("\r\n"))
    88  	if linefeed < 0 {
    89  		log.Debugf("Did not receive full first line")
    90  		if len(data) > 0 && data[len(data)-1] == '\r' {
    91  			return proxylib.MORE, 1
    92  		}
    93  		return proxylib.MORE, 2
    94  	}
    95  
    96  	// TODO: iterate over data without copying it to new slices
    97  	// Tokenizing in memcached is done by spaces: https://github.com/memcached/memcached/blob/master/memcached.c#L2978
    98  	tokens := bytes.Fields(data[:linefeed])
    99  
   100  	if !reply {
   101  		meta := meta.MemcacheMeta{
   102  			Command: string(tokens[0]),
   103  		}
   104  		command := tokens[0]
   105  
   106  		frameLength := linefeed + 2
   107  		hasNoreply := false
   108  		switch {
   109  		case p.isCommandRetrieval(command):
   110  			// get, gets, gat, gats
   111  			if bytes.HasPrefix(command, []byte("get")) {
   112  				meta.Keys = tokens[1:]
   113  			} else if bytes.HasPrefix(command, []byte("gat")) {
   114  				meta.Keys = tokens[2:]
   115  			}
   116  		case p.isCommandStorage(command):
   117  			// storage commands
   118  			meta.Keys = tokens[1:2]
   119  			nBytes, err := strconv.Atoi(string(tokens[4]))
   120  			if err != nil {
   121  				log.Error("Failed to parse storage payload length")
   122  				return proxylib.ERROR, 0
   123  			}
   124  			// 2 additional bytes for terminating linefeed
   125  			frameLength += nBytes + 2
   126  
   127  			if command[0] == 'c' { //storage command is "cas"
   128  				hasNoreply = len(tokens) == casWithNoreplyFields
   129  			} else {
   130  				hasNoreply = len(tokens) == storageWithNoreplyFields
   131  			}
   132  		case p.isCommandDelete(command):
   133  			meta.Keys = tokens[1:2]
   134  			hasNoreply = len(tokens) == deleteWithNoreplyFields
   135  		case p.isCommandIncrDecr(command):
   136  			meta.Keys = tokens[1:2]
   137  			hasNoreply = len(tokens) == incrWithNoreplyFields
   138  		case bytes.Equal(command, []byte("touch")):
   139  			meta.Keys = tokens[1:2]
   140  			hasNoreply = len(tokens) == touchWithNoreplyFields
   141  		case bytes.Equal(command, []byte("slabs")),
   142  			bytes.Equal(command, []byte("lru")),
   143  			bytes.Equal(command, []byte("lru_crawler")),
   144  			bytes.Equal(command, []byte("stats")),
   145  			bytes.Equal(command, []byte("version")),
   146  			bytes.Equal(command, []byte("misbehave")):
   147  
   148  			meta.Keys = [][]byte{}
   149  		case bytes.Equal(command, []byte("flush_all")),
   150  			bytes.Equal(command, []byte("cache_memlimit")):
   151  			meta.Keys = [][]byte{}
   152  			hasNoreply = bytes.Equal(tokens[len(tokens)-1], []byte("noreply"))
   153  		case bytes.Equal(command, []byte("quit")):
   154  			meta.Keys = [][]byte{}
   155  			hasNoreply = true
   156  		case bytes.Equal(command, []byte("watch")):
   157  			meta.Keys = [][]byte{}
   158  			p.watching = true
   159  		default:
   160  			log.Error("Could not parse text memcache frame")
   161  			return proxylib.ERROR, 0
   162  		}
   163  		logEntry := &cilium.LogEntry_GenericL7{
   164  			GenericL7: &cilium.L7LogEntry{
   165  				Proto: "textmemcached",
   166  				Fields: map[string]string{
   167  					"command": meta.Command,
   168  					"keys":    string(bytes.Join(meta.Keys, []byte(", "))),
   169  				},
   170  			},
   171  		}
   172  
   173  		r := &replyIntent{
   174  			command: command,
   175  		}
   176  
   177  		matches := p.connection.Matches(meta)
   178  
   179  		if matches {
   180  			r.denied = false
   181  			if !hasNoreply {
   182  				p.replyQueue = append(p.replyQueue, r)
   183  			}
   184  			p.connection.Log(cilium.EntryType_Request, logEntry)
   185  			return proxylib.PASS, frameLength
   186  		}
   187  
   188  		r.denied = true
   189  		if !hasNoreply {
   190  			if len(p.replyQueue) == 0 {
   191  				p.injectDeniedMessage()
   192  			} else {
   193  				p.replyQueue = append(p.replyQueue, r)
   194  			}
   195  		}
   196  		p.connection.Log(cilium.EntryType_Denied, logEntry)
   197  		return proxylib.DROP, frameLength
   198  	}
   199  
   200  	//reply
   201  	log.Debugf("reply, parsing to figure out if we have it all")
   202  
   203  	intent := p.replyQueue[0]
   204  
   205  	logEntry := &cilium.LogEntry_GenericL7{
   206  		GenericL7: &cilium.L7LogEntry{
   207  			Proto: "textmemcached",
   208  			Fields: map[string]string{
   209  				"command": string(intent.command),
   210  			},
   211  		},
   212  	}
   213  	if p.watching {
   214  		// in watch mode we pass all replied lines
   215  		return proxylib.PASS, linefeed + 2
   216  	}
   217  
   218  	switch {
   219  	case p.isErrorReply(tokens[0]),
   220  		p.isCommandStorage(intent.command),
   221  		p.isCommandDelete(intent.command),
   222  		p.isCommandIncrDecr(intent.command),
   223  		bytes.Equal(intent.command, []byte("touch")),
   224  		bytes.Equal(intent.command, []byte("slabs")),
   225  		bytes.Equal(intent.command, []byte("lru")),
   226  		bytes.Equal(intent.command, []byte("flush_all")),
   227  		bytes.Equal(intent.command, []byte("cache_memlimit")),
   228  		bytes.Equal(intent.command, []byte("version")),
   229  		bytes.Equal(intent.command, []byte("misbehave")):
   230  
   231  		// passing one line of reply
   232  		p.connection.Log(cilium.EntryType_Response, logEntry)
   233  		p.replyQueue = p.replyQueue[1:]
   234  		return proxylib.PASS, linefeed + 2
   235  	case p.isCommandRetrieval(intent.command),
   236  		bytes.Equal(intent.command, []byte("stats")):
   237  		t, nBytes := p.untilEnd(data)
   238  		if t == proxylib.PASS {
   239  			p.connection.Log(cilium.EntryType_Response, logEntry)
   240  			p.replyQueue = p.replyQueue[1:]
   241  		}
   242  		return t, nBytes
   243  	case bytes.Equal(intent.command, []byte("lru_crawler")):
   244  		// check if it's response line
   245  		if bytes.Equal(tokens[0], []byte("OK")) ||
   246  			bytes.Equal(tokens[0], []byte("BUSY")) ||
   247  			bytes.Equal(tokens[0], []byte("BADCLASS")) {
   248  			p.connection.Log(cilium.EntryType_Response, logEntry)
   249  			p.replyQueue = p.replyQueue[1:]
   250  			return proxylib.PASS, linefeed + 2
   251  		}
   252  
   253  		t, nBytes := p.untilEnd(data)
   254  		if t == proxylib.PASS {
   255  			p.connection.Log(cilium.EntryType_Response, logEntry)
   256  			p.replyQueue = p.replyQueue[1:]
   257  		}
   258  		return t, nBytes
   259  	}
   260  	log.Error("Could not parse text memcache frame")
   261  	return proxylib.ERROR, 0
   262  }
   263  
   264  const payloadEnd = "\r\nEND\r\n"
   265  
   266  func (p *Parser) untilEnd(data []byte) (proxylib.OpType, int) {
   267  	// TODO: optimise this to not ask per byte, but take VALUES lines into account
   268  	endIndex := bytes.Index(data, []byte(payloadEnd))
   269  	if endIndex > 0 {
   270  		return proxylib.PASS, endIndex + len(payloadEnd)
   271  	}
   272  	return proxylib.MORE, 1
   273  }
   274  
   275  func (p *Parser) isCommandRetrieval(cmd []byte) bool {
   276  	return bytes.HasPrefix(cmd, []byte("get")) ||
   277  		bytes.HasPrefix(cmd, []byte("gat"))
   278  }
   279  
   280  func (p *Parser) isCommandStorage(cmd []byte) bool {
   281  	return bytes.Equal(cmd, []byte("set")) ||
   282  		bytes.Equal(cmd, []byte("add")) ||
   283  		bytes.Equal(cmd, []byte("replace")) ||
   284  		bytes.Equal(cmd, []byte("append")) ||
   285  		bytes.Equal(cmd, []byte("prepend")) ||
   286  		bytes.Equal(cmd, []byte("cas"))
   287  }
   288  
   289  func (p *Parser) isCommandDelete(cmd []byte) bool {
   290  	return bytes.Equal(cmd, []byte("delete"))
   291  }
   292  
   293  func (p *Parser) isCommandIncrDecr(cmd []byte) bool {
   294  	return bytes.Equal(cmd, []byte("incr")) ||
   295  		bytes.Equal(cmd, []byte("decr"))
   296  }
   297  
   298  func (p *Parser) isErrorReply(firstToken []byte) bool {
   299  	return bytes.Equal(firstToken, []byte("ERROR")) ||
   300  		bytes.Equal(firstToken, []byte("CLIENT_ERROR")) ||
   301  		bytes.Equal(firstToken, []byte("SERVER_ERROR"))
   302  }
   303  
   304  // returns injected bytes
   305  func (p *Parser) injectFromQueue() int {
   306  	injected := 0
   307  	for _, rep := range p.replyQueue {
   308  		if rep.denied {
   309  			injected++
   310  			p.injectDeniedMessage()
   311  		} else {
   312  			break
   313  		}
   314  
   315  	}
   316  	if injected > 0 {
   317  		p.replyQueue = p.replyQueue[injected:]
   318  	}
   319  	return injected * len(DeniedMsg)
   320  }
   321  
   322  func (p *Parser) injectDeniedMessage() {
   323  	p.connection.Inject(true, DeniedMsg)
   324  }
   325  
   326  // DeniedMsg is sent if policy denies the request. Exported for tests
   327  var DeniedMsg = []byte("CLIENT_ERROR access denied\r\n")
   328  
   329  // ErrorMsg is standard memcached error line
   330  var ErrorMsg = []byte("ERROR\r\n")