redis included

This commit is contained in:
Max Lerebourg
2022-10-20 02:42:24 +02:00
parent f94e48aa03
commit ffcf4356fc
10 changed files with 767 additions and 558 deletions
+165 -342
View File
@@ -1,375 +1,198 @@
package redis
// My intepretation of the RESP protocol.
// Referenced from https://redis.io/docs/reference/protocol-spec/
// 8-7-2022
package simpleredis
import (
"errors"
"fmt"
"io"
"log"
"net/textproto"
"net"
"bufio"
"strconv"
"strings"
"sync"
"time"
"github.com/tehnerd/goUtils/netutils"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
type RespT byte
const (
SimpleString RespT = '+'
Error RespT = '-'
Integer RespT = ':'
BulkString RespT = '$'
Array RespT = '*'
)
type Message interface {
String() string
type RedisCmd struct {
Command string
Name string
Data []byte
Error error
}
type MsgSimpleStr string
type MsgError string
type MsgBulkStr string
type MsgInteger int64
type MsgArray []Message
func (m MsgInteger) String() string {
return fmt.Sprintf("%d", m)
type SimpleRedis struct {
redisChanRead chan RedisCmd
redisChanWrite chan RedisCmd
redisHost string
redisCmd RedisCmd
}
func (m MsgBulkStr) String() string {
return string(m)
}
func (m MsgError) String() string {
return string(m)
}
func (m MsgSimpleStr) String() string {
return string(m)
}
// func (m MsgArray) String() string {
// return fmt.Sprintf("[%s]", strings.Join(Map(m, Message.String), ","))
// }
func (m RedisMessage) String() string {
return m.Choice.String()
}
type RedisMessage struct {
RedisType RespT
Raw string
Choice Message
}
func NewParseError(got RespT, expected ...RespT) error {
return fmt.Errorf("got %v but expected %v", got, expected)
}
func (msg RedisMessage) Symbol() rune {
return rune(msg.RedisType)
}
func (msg RedisMessage) AsString() (string, error) {
if conv, ok := msg.Choice.(MsgSimpleStr); ok {
return string(conv), nil
} else if conv, ok := msg.Choice.(MsgBulkStr); ok {
return string(conv), nil
} else {
return "", NewParseError(msg.RedisType, SimpleString, BulkString)
func GenRedisArray(params ...[]byte) []byte {
CRLF := "\r\n"
MSG := ""
for cntr := 0; cntr < len(params); cntr++ {
MSG = strings.Join([]string{MSG, string(params[cntr])}, " ")
}
MSG = strings.Trim(MSG, " ")
MSG = strings.Join([]string{MSG, CRLF}, "")
return []byte(MSG)
}
func (msg RedisMessage) AsInteger() (int64, error) {
if conv, ok := msg.Choice.(MsgInteger); ok {
return int64(conv), nil
} else {
return 0, NewParseError(msg.RedisType, Integer)
}
func RedisSet(name string, data []byte) []byte {
return GenRedisArray([]byte("SET"), []byte(name), data)
}
func (msg RedisMessage) AsError() (error, error) {
if conv, ok := msg.Choice.(MsgError); ok {
return errors.New(string(conv)), nil
} else {
return nil, NewParseError(msg.RedisType, Error)
}
func RedisGet(name string) []byte {
return GenRedisArray([]byte("GET"), []byte(name))
}
// func (msg RedisMessage) AsArray() ([]Message, error) {
// if conv, ok := (msg.Choice).(MsgArray); ok {
// return conv, nil
// } else {
// return nil, NewParseError(msg.RedisType, Array)
// }
// }
type RedisReader struct {
Rd *textproto.Reader
Out chan RedisMessage
}
// Reset the parsing state of the reader
func (rr *RedisReader) Reset() {
}
func NewRespReader(tp *textproto.Reader, out chan RedisMessage) RedisReader {
return RedisReader{
Rd: tp,
Out: out,
func ParseRedisResponse(response []byte, dataBuf []byte, Len *int) ([]byte, []byte, error) {
dataBuf = append(dataBuf, response...)
lenCRLF := 2
if *Len != 0 {
if len(dataBuf) < *Len {
return nil, dataBuf, nil
} else {
return dataBuf[:*Len], dataBuf[*Len:], nil
}
}
}
// Fetch grabs the next incoming RESP type from the IO input
func Fetch(rr *RedisReader) *RedisMessage {
byteCode, err := rr.TryReadByte()
if err != nil {
return nil
}
switch RespT(byteCode) {
case BulkString:
return RespBulkStr(rr)
case SimpleString:
return RespSimpleStr(rr)
case Integer:
return RespInt(rr)
case Error:
return RespError(rr)
// case Array:
// return RespArray(rr)
default:
panic(fmt.Sprint("Unknown bytecode: ", byteCode))
}
}
// Scan is the main API to send redis messages out to a channel.
func (rr *RedisReader) Scan() {
rr.Out <- *Fetch(rr)
}
// RespSimpleStr attempts to parse a string from the resulting line.
//
// TODO: I don't think we need the high level facilities from net/textproto anymore
// since the parsing is quite trivial.
func RespSimpleStr(rr *RedisReader) *RedisMessage {
s, err := rr.Rd.ReadLine()
if err != nil {
log.Printf("Fatal RespSimpleStr: %+v", err)
return nil
}
return &RedisMessage{
RedisType: RespT(SimpleString),
Raw: s,
Choice: MsgSimpleStr(s),
}
}
func RespError(rr *RedisReader) *RedisMessage {
s, err := rr.Rd.ReadLine()
if err != nil {
log.Printf("Fatal RespError: %+v", err)
return nil
}
return &RedisMessage{
RedisType: RespT(Error),
Raw: s,
Choice: MsgError(s),
}
}
// RespSimpleStr attempts to parse a string from the resulting line.
//
// It first expects a length value parsed as an integer,
// then directly copies the subsequent payload into a string
// buffer
func RespBulkStr(rr *RedisReader) *RedisMessage {
len, err := loopReadInt(rr)
if err != nil {
return nil
}
// Read the string data into buffer
bulkStr := make([]byte, len)
io.ReadFull(rr.Rd.R, bulkStr)
// Discard two bytes since we don't need the '\r\n'
_, err = rr.Rd.R.Discard(2)
if err != nil {
log.Printf("Fatal: %+v", err)
}
return &RedisMessage{
RedisType: BulkString,
Raw: string(bulkStr),
Choice: MsgBulkStr(string(bulkStr)),
}
}
// RespInt reads an integer from the IO stream
func RespInt(rr *RedisReader) *RedisMessage {
val, err := loopReadInt(rr)
if err != nil {
return nil
}
return &RedisMessage{
RedisType: Integer,
Raw: fmt.Sprintf("%d", val),
Choice: MsgInteger(val),
}
}
// RespArray reads an array of RESP objects from the IO stream
// func RespArray(rr *RedisReader) *RedisMessage {
// len, err := loopReadInt(rr)
// if err != nil {
// return nil
// }
// fetched := make([]RedisMessage, 0, len)
// for i := 0; i < len; i++ {
// fetched[i] = *Fetch(rr)
// }
// return &RedisMessage{
// RedisType: Array,
// Raw: fmt.Sprint(Map(fetched, func(rm RedisMessage) string { return rm.Raw })),
// Choice: MsgArray(Map(fetched, func(rm RedisMessage) Message { return rm.Choice })),
// }
// }
// loopReadInt reads from IO and returns an integer, discarding \r\n.
func loopReadInt(rr *RedisReader) (int, error) {
val := 0
for b, _ := rr.TryReadByte(); b != '\r'; b, _ = rr.TryReadByte() {
val = (val * 10) + int(b-byte('0'))
}
_, err := rr.Rd.R.Discard(1)
if err != nil {
return 0, fmt.Errorf("Fatal loopReadInt: %+v", err)
}
return val, nil
}
// TryReadByte attempts to read a single byte from IO and panics on any error
func (rr *RedisReader) TryReadByte() (byte, error) {
b, err := rr.Rd.R.ReadByte()
if err != nil {
return byte('0'), fmt.Errorf("Fatal TryReadByte: %+v", err)
}
return b, nil
}
// Map. Your standard good old fashioned generic map function :)
//
// A la []T -> []K
func Map[T any, K any](slice []T, fn func(T) K) []K {
out := make([]K, 0, len(slice))
for i := range out {
out[i] = fn(slice[i])
}
return out
}
func loopScan(reader *textproto.Reader, msg_ch chan RedisMessage) {
rr := NewRespReader(reader, msg_ch)
for {
rr.Scan()
switch string(dataBuf[0]) {
case "+", "-", ":":
//simple strings, error,int. usually ther are in format (+|-|:)DATA\r\n"
if len(dataBuf) < 3 {
return nil, dataBuf, nil
}
cntr := 1
for ; cntr < len(dataBuf); cntr++ {
if dataBuf[cntr] == '\r' {
break
}
}
if cntr == len(dataBuf) {
return nil, dataBuf, nil
}
response = dataBuf[1:cntr]
return response, dataBuf[cntr+2:], nil
case "$":
//bulk string. format $LEN\r\nDATA\r\n. up to 512MB
cntr := 1
for ; cntr < len(dataBuf); cntr++ {
if string(dataBuf[cntr]) == "\r" {
break
}
}
if cntr == len(dataBuf) || cntr+lenCRLF > len(dataBuf) {
return nil, dataBuf, nil
}
dataLen, err := strconv.Atoi(string(dataBuf[1:cntr]))
if err != nil {
return nil, dataBuf[cntr:], nil
}
if dataLen == -1 {
return nil, dataBuf[cntr:], fmt.Errorf("NOT FOUND")
}
if cntr+lenCRLF > len(dataBuf)-lenCRLF {
*Len = dataLen
return nil, dataBuf[cntr+lenCRLF:], nil
}
if len(dataBuf[cntr+lenCRLF:len(dataBuf)-lenCRLF]) < dataLen {
*Len = dataLen
return nil, dataBuf[cntr+lenCRLF:], nil
} else {
return dataBuf[cntr+lenCRLF : cntr+dataLen+lenCRLF], dataBuf[cntr+dataLen+lenCRLF:], nil
}
case "*":
panic("array")
default:
if len(dataBuf) > 1 {
dataBuf = dataBuf[1:]
} else {
return nil, dataBuf, nil
}
}
}
return nil, dataBuf, nil
}
// repl starts an read input loop on the standard input.
//
// Commands are sent out after a new line is parsed.
//
// TODO: Improve ergonomics of the repl.
func repl(input *textproto.Reader, wr *textproto.Writer) {
for {
print("> ")
input_str, err := input.ReadLine()
if len(input_str) > 0 && err == nil {
// The redis server expects arrays of bulk strings for sending commands.
commands := strings.Split(input_str, " ")
// Specify the array length and create the bulk strings
pipe := []string{fmt.Sprintf("*%d", len(commands))}
pipe = append(pipe, createBulkStrings(commands)...)
for _, out := range pipe {
wr.PrintfLine(out)
func RedisContext(hostnamePort string, redisCmdWrite, redisCmdRead chan RedisCmd) {
tcpRemoteAddress, err := net.ResolveTCPAddr("tcp", hostnamePort)
if err != nil {
panic("cant resolve remote redis address")
}
var ladr *net.TCPAddr
msgBuf := make([]byte, 65000)
initMsg := []byte("*1\r\n$4\r\nPING\r\n")
writeChan := make(chan []byte)
readChan := make(chan []byte)
flushChan := make(chan int)
go netutils.AutoRecoonectedTCP(ladr, tcpRemoteAddress, msgBuf, initMsg, writeChan, readChan, flushChan)
<-readChan
dataBuf := make([]byte, 0)
dataLen := 0
for true {
select {
case cmd := <-redisCmdWrite:
switch cmd.Command {
case "SET":
data := RedisSet(cmd.Name, cmd.Data)
writeChan <- data
case "GET":
data := RedisGet(cmd.Name)
writeChan <- data
}
case response := <-readChan:
data, dataBuf, err := ParseRedisResponse(response, dataBuf, &dataLen)
if dataLen != 0 {
for data == nil {
response = <-readChan
data, dataBuf, err = ParseRedisResponse(response, dataBuf, &dataLen)
}
}
if err != nil {
select {
case redisCmdRead <- RedisCmd{
Error: err,
}:
case <-time.After(time.Second * 5):
}
}
if data != nil && string(data) != "PONG" {
select {
case redisCmdRead <- RedisCmd{
Data: data,
}:
case <-time.After(time.Second * 5):
}
dataLen = 0
}
case <-flushChan:
dataBuf = dataBuf[:]
dataLen = 0
select {
case redisCmdRead <- RedisCmd{}:
case <-time.After(time.Second * 5):
}
}
}
}
// createBulkStrings converts normal strings into bulk strings to send to redis.
func createBulkStrings(commands []string) []string {
cmds := make([]string, len(commands)*2)
for i, v := range commands {
cmds[i*2] = fmt.Sprintf("$%d", len(commands[i]))
cmds[(i*2)+1] = v
}
return cmds
func (sr *SimpleRedis) Init(redisHost string) {
sr.redisHost = redisHost
sr.redisChanWrite = make(chan RedisCmd)
sr.redisChanRead = make(chan RedisCmd)
go RedisContext(sr.redisHost, sr.redisChanWrite, sr.redisChanRead)
}
// cmd/client starts a simple Read-Send-Print-Loop (RSPL?)
// with a redis server.
//
// TODO: Implement command flags
func Init(host string, password string) (*textproto.Writer, *textproto.Reader) {
log.Printf("yo")
// Create the connection to the redis server
// TODO: cli, rm hardcode
conn, err := net.Dial("tcp", host)
if err != nil {
log.Printf("[net] unable to connect: %v", err)
return nil, nil
func (sr *SimpleRedis) Do(cmd, name string, data []byte) ([]byte, error) {
sr.redisCmd.Command = cmd
sr.redisCmd.Name = name
sr.redisCmd.Data = data
sr.redisChanWrite <- sr.redisCmd
resp := <- sr.redisChanRead
if resp.Error != nil {
return nil, resp.Error
}
defer conn.Close()
// Create the readers and writers we will pass to our subroutines
msg_channel := make(chan RedisMessage)
write_sock := textproto.NewWriter(bufio.NewWriter(conn))
read_sock := textproto.NewReader(bufio.NewReader(conn))
go runAsWaitGroup(
// Subscribe to redis connection
func() {
loopScan(read_sock, msg_channel)
},
// Write to output
func() {
for {
msg := <-msg_channel
log.Printf("%v", msg)
}
},
).Wait()
log.Printf("yolo")
return write_sock, read_sock
}
// runAsWaitGroup runs closures within a sync.WaitGroup
//
// Calling .Wait() on the resultant WaitGroup is a blocking operation until all closures finish running.
func runAsWaitGroup(closures ...func()) *sync.WaitGroup {
wg := sync.WaitGroup{}
for _, fn := range closures {
wg.Add(1)
go func(fn func()) {
fn()
wg.Done()
}(fn)
}
return &wg
return resp.Data, nil
}