mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 06:38:20 +00:00
371 lines
12 KiB
Text
371 lines
12 KiB
Text
# Handle any Redis-specific "parsing"
|
|
|
|
module Redis;
|
|
|
|
import RESP;
|
|
|
|
public type KnownCommand = enum {
|
|
APPEND,
|
|
AUTH,
|
|
BITCOUNT,
|
|
BITFIELD,
|
|
BITFIELD_RO,
|
|
BITOP,
|
|
BITPOS,
|
|
BLMPOP,
|
|
BLPOP,
|
|
BRPOP,
|
|
CLIENT,
|
|
COPY,
|
|
DECR,
|
|
DECRBY,
|
|
DEL,
|
|
DUMP,
|
|
EXISTS,
|
|
EXPIRE,
|
|
EXPIREAT,
|
|
EXPIRETIME,
|
|
GET,
|
|
GETBIT,
|
|
GETDEL,
|
|
GETEX,
|
|
GETRANGE,
|
|
GETSET,
|
|
HDEL,
|
|
HGET,
|
|
HSET,
|
|
INCR,
|
|
INCRBY,
|
|
KEYS,
|
|
MGET,
|
|
MOVE,
|
|
MSET,
|
|
PERSIST,
|
|
RENAME,
|
|
SET,
|
|
STRLEN,
|
|
TTL,
|
|
TYPE,
|
|
};
|
|
|
|
type Command = struct {
|
|
raw: vector<bytes>;
|
|
command: bytes;
|
|
key: optional<bytes>;
|
|
value:optional<bytes>;
|
|
known: optional<KnownCommand>;
|
|
};
|
|
|
|
# This just assumes all elements in the array is a bulk string and puts them in a vector
|
|
public function make_command(command: RESP::ClientData): Command {
|
|
if (command?.multibulk)
|
|
return bulk_command(command);
|
|
else
|
|
return inline_command(command);
|
|
}
|
|
|
|
public function bulk_command(command: RESP::ClientData): Command {
|
|
local v: vector<bytes>;
|
|
for (ele in command.multibulk.elements) {
|
|
v.push_back(ele.content);
|
|
}
|
|
return parse_command(v);
|
|
}
|
|
|
|
public function inline_command(command: RESP::ClientData): Command {
|
|
# Only call this if it's inline :)
|
|
assert command?.inline;
|
|
|
|
local tokenized: vector<bytes>;
|
|
local it = command.inline.at(0);
|
|
# Redis whitespace characters are null, tab, LF, CR, and space
|
|
local whitespace = set<uint8>(0, 9, 10, 13, 32);
|
|
# Note: this logic is a bit different from Redis. Hopefully it doesn't matter
|
|
while (True) {
|
|
while (it != end(command.inline) && ((*it) in whitespace))
|
|
it++;
|
|
|
|
# Get a token
|
|
local start = it;
|
|
if (it != end(command.inline)) {
|
|
local double_quotes = False;
|
|
local single_quotes = False;
|
|
local done = False;
|
|
while (!done) {
|
|
if (double_quotes) {
|
|
if (*it == '\' && it + 1 != end(command.inline) && *(it + 1) == '"') {
|
|
# Skip one, then later we skip another
|
|
it++;
|
|
} else if (*it == '"') {
|
|
double_quotes = False;
|
|
}
|
|
|
|
it++;
|
|
} else if (single_quotes) {
|
|
if (*it == '\' && it + 1 != end(command.inline) && *(it + 1) == ''') {
|
|
# Skip one, then later we skip another
|
|
it++;
|
|
} else if (*it == ''') {
|
|
single_quotes = False;
|
|
}
|
|
|
|
it++;
|
|
} else {
|
|
if (it != end(command.inline)) {
|
|
switch (*it) {
|
|
case '"': double_quotes = True;
|
|
case ''': single_quotes = True;
|
|
default: {
|
|
if ((*it) in whitespace)
|
|
done = True;
|
|
}
|
|
}
|
|
|
|
if (!done)
|
|
it++;
|
|
}
|
|
}
|
|
|
|
if (it == end(command.inline)) {
|
|
done = True;
|
|
# If we're still in quotes, that's weird, but not really too bad.
|
|
#if (double_quotes || single_quotes)
|
|
# zeek::weird("unbalanced_quotes", "unbalanced quotes in inline buffer: '" + command.inline.sub(start, it).decode() + "'");
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
tokenized.push_back(command.inline.sub(start, it));
|
|
}
|
|
return parse_command(tokenized);
|
|
}
|
|
|
|
# Parses the vector of bytes to get a Command object
|
|
function parse_command(raw: vector<bytes>): Command {
|
|
assert |raw| >= 1;
|
|
local cmd = command_from(raw[0]);
|
|
local parsed: Command = [$raw = raw, $command = raw[0], $key = Null, $value = Null, $known = cmd];
|
|
if (!cmd)
|
|
return parsed;
|
|
|
|
if (|raw| >= 2) {
|
|
switch (*cmd) {
|
|
case KnownCommand::KEYS:
|
|
parsed.key = raw[1];
|
|
case KnownCommand::APPEND,
|
|
KnownCommand::BITCOUNT,
|
|
KnownCommand::BITFIELD,
|
|
KnownCommand::BITFIELD_RO,
|
|
KnownCommand::BITPOS,
|
|
KnownCommand::BLPOP,
|
|
KnownCommand::BRPOP,
|
|
KnownCommand::COPY,
|
|
KnownCommand::DECR,
|
|
KnownCommand::DECRBY,
|
|
KnownCommand::DEL,
|
|
KnownCommand::DUMP,
|
|
KnownCommand::EXISTS,
|
|
KnownCommand::EXPIRE,
|
|
KnownCommand::EXPIREAT,
|
|
KnownCommand::EXPIRETIME,
|
|
KnownCommand::GET,
|
|
KnownCommand::GETBIT,
|
|
KnownCommand::GETDEL,
|
|
KnownCommand::GETEX,
|
|
KnownCommand::GETRANGE,
|
|
KnownCommand::GETSET,
|
|
KnownCommand::HDEL,
|
|
KnownCommand::HGET,
|
|
KnownCommand::HSET,
|
|
KnownCommand::INCR,
|
|
KnownCommand::INCRBY,
|
|
KnownCommand::MGET,
|
|
KnownCommand::MOVE,
|
|
KnownCommand::MSET,
|
|
KnownCommand::PERSIST,
|
|
KnownCommand::RENAME,
|
|
KnownCommand::SET,
|
|
KnownCommand::STRLEN,
|
|
KnownCommand::TTL,
|
|
KnownCommand::TYPE:
|
|
parsed.key = raw[1];
|
|
default: ();
|
|
}
|
|
}
|
|
|
|
if (|raw| >= 3) {
|
|
switch (*cmd) {
|
|
case KnownCommand::SET,
|
|
KnownCommand::APPEND,
|
|
KnownCommand::DECRBY,
|
|
KnownCommand::EXPIRE,
|
|
KnownCommand::EXPIREAT,
|
|
KnownCommand::GETBIT,
|
|
KnownCommand::GETSET,
|
|
KnownCommand::HDEL,
|
|
KnownCommand::HGET,
|
|
KnownCommand::INCRBY,
|
|
KnownCommand::MOVE,
|
|
KnownCommand::MSET,
|
|
KnownCommand::RENAME:
|
|
parsed.value = raw[2];
|
|
# Op first, destination second, then a list of keys. Just log dest
|
|
case KnownCommand::BITOP: parsed.key = raw[2];
|
|
default: ();
|
|
}
|
|
}
|
|
|
|
if (|raw| >= 4) {
|
|
switch (*cmd) {
|
|
# timeout, numkeys, then key
|
|
case KnownCommand::BLMPOP: parsed.key = raw[3];
|
|
default: ();
|
|
}
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function command_from(cmd_bytes: bytes): optional<KnownCommand> {
|
|
local cmd: optional<KnownCommand> = Null;
|
|
switch (cmd_bytes.lower()) {
|
|
case b"set": cmd = KnownCommand::SET;
|
|
case b"append": cmd = KnownCommand::APPEND;
|
|
case b"auth": cmd = KnownCommand::AUTH;
|
|
case b"bitcount": cmd = KnownCommand::BITCOUNT;
|
|
case b"bitfield": cmd = KnownCommand::BITFIELD;
|
|
case b"bitfield_ro": cmd = KnownCommand::BITFIELD_RO;
|
|
case b"bitop": cmd = KnownCommand::BITOP;
|
|
case b"bitpos": cmd = KnownCommand::BITPOS;
|
|
case b"blmpop": cmd = KnownCommand::BLMPOP;
|
|
case b"blpop": cmd = KnownCommand::BLPOP;
|
|
case b"brpop": cmd = KnownCommand::BRPOP;
|
|
case b"client": cmd = KnownCommand::CLIENT;
|
|
case b"copy": cmd = KnownCommand::COPY;
|
|
case b"decr": cmd = KnownCommand::DECR;
|
|
case b"decrby": cmd = KnownCommand::DECRBY;
|
|
case b"del": cmd = KnownCommand::DEL;
|
|
case b"dump": cmd = KnownCommand::DUMP;
|
|
case b"exists": cmd = KnownCommand::EXISTS;
|
|
case b"expire": cmd = KnownCommand::EXPIRE;
|
|
case b"expireat": cmd = KnownCommand::EXPIREAT;
|
|
case b"expiretime": cmd = KnownCommand::EXPIRETIME;
|
|
case b"expiretime": cmd = KnownCommand::EXPIRETIME;
|
|
case b"get": cmd = KnownCommand::GET;
|
|
case b"getbit": cmd = KnownCommand::GETBIT;
|
|
case b"getdel": cmd = KnownCommand::GETDEL;
|
|
case b"getex": cmd = KnownCommand::GETEX;
|
|
case b"getrange": cmd = KnownCommand::GETRANGE;
|
|
case b"getset": cmd = KnownCommand::GETSET;
|
|
case b"hdel": cmd = KnownCommand::HDEL;
|
|
case b"hget": cmd = KnownCommand::HGET;
|
|
case b"hset": cmd = KnownCommand::HSET;
|
|
case b"incr": cmd = KnownCommand::INCR;
|
|
case b"incrby": cmd = KnownCommand::INCRBY;
|
|
case b"keys": cmd = KnownCommand::KEYS;
|
|
case b"mget": cmd = KnownCommand::MGET;
|
|
case b"move": cmd = KnownCommand::MOVE;
|
|
case b"mset": cmd = KnownCommand::MSET;
|
|
case b"persist": cmd = KnownCommand::PERSIST;
|
|
case b"rename": cmd = KnownCommand::RENAME;
|
|
case b"strlen": cmd = KnownCommand::STRLEN;
|
|
case b"ttl": cmd = KnownCommand::TTL;
|
|
case b"type": cmd = KnownCommand::TYPE;
|
|
default: cmd = Null;
|
|
}
|
|
|
|
return cmd;
|
|
}
|
|
|
|
type Set = struct {
|
|
key: bytes;
|
|
value: bytes;
|
|
nx: bool &default=False;
|
|
xx: bool &default=False;
|
|
get: bool &default=False;
|
|
ex: optional<uint64> &default=Null;
|
|
px: optional<uint64> &default=Null;
|
|
exat: optional<uint64> &default=Null;
|
|
pxat: optional<uint64> &default=Null;
|
|
keep_ttl: bool &default=False;
|
|
};
|
|
|
|
public function make_set(command: Command): Set {
|
|
assert |command.raw| >= 3 : "Must have at least 3 elements in SET";
|
|
assert command.key : "SET must validate a key";
|
|
assert command.value : "SET must validate a value";
|
|
local parsed: Set = [$key = *command.key, $value = *command.value];
|
|
local i = 3;
|
|
while (i < |command.raw|) {
|
|
switch (command.raw[i].lower()) {
|
|
case b"nx": parsed.nx = True;
|
|
case b"xx": parsed.xx = True;
|
|
case b"get": parsed.get = True;
|
|
case b"ex": {
|
|
++i;
|
|
if (i >= |command.raw|)
|
|
break;
|
|
parsed.ex = command.raw[i].to_uint();
|
|
}
|
|
case b"px": {
|
|
++i;
|
|
if (i >= |command.raw|)
|
|
break;
|
|
parsed.px = command.raw[i].to_uint();
|
|
}
|
|
case b"exat": {
|
|
++i;
|
|
if (i >= |command.raw|)
|
|
break;
|
|
parsed.exat = command.raw[i].to_uint();
|
|
}
|
|
case b"pxat": {
|
|
++i;
|
|
if (i >= |command.raw|)
|
|
break;
|
|
parsed.pxat = command.raw[i].to_uint();
|
|
}
|
|
case b"keepttl": parsed.keep_ttl = True;
|
|
default: ();
|
|
}
|
|
|
|
++i;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
public function is_set(data: RESP::ClientData): bool {
|
|
return data.command.known && *(data.command.known) == KnownCommand::SET && data.command.key && data.command.value;
|
|
}
|
|
|
|
type Get = struct {
|
|
key: bytes;
|
|
};
|
|
|
|
public function make_get(command: Command): Get {
|
|
assert command.key : "GET must validate a key";
|
|
return [$key = *command.key];
|
|
}
|
|
|
|
public function is_get(data: RESP::ClientData): bool {
|
|
return data.command.known && *(data.command.known) == KnownCommand::GET && |data.command.raw| >= 2;
|
|
}
|
|
|
|
type Auth = struct {
|
|
username: optional<bytes>;
|
|
password: bytes;
|
|
};
|
|
|
|
public function make_auth(command: Command): Auth {
|
|
assert |command.raw| >= 2 : "AUTH must have arguments";
|
|
if (|command.raw| == 2) {
|
|
return [$username = Null, $password = command.raw[1]];
|
|
}
|
|
|
|
return [$username = command.raw[1], $password = command.raw[2]];
|
|
}
|
|
|
|
public function is_auth(data: RESP::ClientData): bool {
|
|
return data.command.known && *(data.command.known) == KnownCommand::AUTH && |data.command.raw| >= 2;
|
|
}
|