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")