mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-07-21 19:48:59 +02:00
375 lines
8.3 KiB
Go
375 lines
8.3 KiB
Go
package redis
|
|
|
|
// My intepretation of the RESP protocol.
|
|
// Referenced from https://redis.io/docs/reference/protocol-spec/
|
|
// 8-7-2022
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/textproto"
|
|
"net"
|
|
"bufio"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type RespT byte
|
|
|
|
const (
|
|
SimpleString RespT = '+'
|
|
Error RespT = '-'
|
|
Integer RespT = ':'
|
|
BulkString RespT = '$'
|
|
Array RespT = '*'
|
|
)
|
|
|
|
type Message interface {
|
|
String() string
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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 (msg RedisMessage) AsInteger() (int64, error) {
|
|
if conv, ok := msg.Choice.(MsgInteger); ok {
|
|
return int64(conv), nil
|
|
} else {
|
|
return 0, NewParseError(msg.RedisType, Integer)
|
|
}
|
|
}
|
|
|
|
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 (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,
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
|
|
// 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
|
|
}
|
|
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
|
|
} |