Skip to content

Commit b43040f

Browse files
committed
Initial implementation with basic commands
1 parent 44f4410 commit b43040f

9 files changed

Lines changed: 223 additions & 49 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.vscode
2+
__debug*

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@ This is a redis proxy to make ACL more convenient. It prefixes keys with the ACL
55
This software is currently WIP. Only support RESP2.
66

77
Your app can connect to this instance listening by default at port `6479`.
8+
9+
## Envar Options
10+
11+
| Env | Default |
12+
|:--|:--|
13+
|`LISTEN`|`:6479`|
14+
|`UPSTREAM_REDIS`|`:6379`|

config.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
package main
22

33
type Config struct {
4-
Listen int `default:":6479"`
4+
Listen string `default:":6479"`
55
UpstreamRedis string `default:"127.0.0.1:6379"`
66
}
7-
8-
var modCommands = map[string]bool{
9-
"GET": true, "SET": true, "MGET": true, "MSET": true, "DEL": true,
10-
"EXISTS": true, "INCR": true, "DECR": true, "HGET": true, "HSET": true,
11-
"LPUSH": true, "RPUSH": true, "LPOP": true, "RPOP": true,
12-
}

go.mod

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ module github.com/domcloud/rdproxy
22

33
go 1.24.0
44

5-
require github.com/gomodule/redigo v1.9.2
5+
require (
6+
github.com/kelseyhightower/envconfig v1.4.0
7+
github.com/tidwall/redcon v1.6.2
8+
)
69

710
require (
811
github.com/tidwall/btree v1.1.0 // indirect
912
github.com/tidwall/match v1.1.1 // indirect
10-
github.com/tidwall/redcon v1.6.2 // indirect
1113
)

go.sum

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2-
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3-
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
4-
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
5-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6-
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7-
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
8-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
1+
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
2+
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
93
github.com/tidwall/btree v1.1.0 h1:5P+9WU8ui5uhmcg3SoPyTwoI0mVyZ1nps7YQzTZFkYM=
104
github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
115
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
126
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
137
github.com/tidwall/redcon v1.6.2 h1:5qfvrrybgtO85jnhSravmkZyC0D+7WstbfCs3MmPhow=
148
github.com/tidwall/redcon v1.6.2/go.mod h1:p5Wbsgeyi2VSTBWOcA5vRXrOb9arFTcU2+ZzFjqV75Y=
15-
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
16-
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

handler.go

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package main
22

33
import (
44
"bufio"
5+
"bytes"
56
"log"
67
"net"
8+
"strconv"
79
"strings"
810

911
"github.com/tidwall/redcon"
@@ -39,16 +41,10 @@ func (m *Handler) ServeRESP(conn redcon.Conn, cmd redcon.Command) {
3941

4042
upConn := context.upstreamConn
4143
command := strings.ToUpper(string(cmd.Args[0]))
44+
reviver := modSingleCommand(command, context.username, cmd.Args)
4245

43-
// Modify key (if applicable)
44-
if modCommands[command] && len(cmd.Args) > 1 {
45-
cmd.Args[1] = append([]byte(context.username+":"), cmd.Args[1]...)
46-
}
47-
48-
// Construct RESP command
46+
// Construct RESP command & send to redis
4947
request := buildRESPCommand(cmd.Args)
50-
51-
// Send to Redis
5248
_, err := upConn.Write(request)
5349
if err != nil {
5450
log.Printf("Failed to send command: %v", err)
@@ -57,12 +53,22 @@ func (m *Handler) ServeRESP(conn redcon.Conn, cmd redcon.Command) {
5753
}
5854

5955
// Read response from Redis
60-
response, err := bufio.NewReader(upConn).ReadBytes('\n')
61-
if err != nil {
56+
var b bytes.Buffer
57+
58+
reader := newRespReader(bufio.NewReader(upConn), &b, reviver)
59+
if err = reader.readReply(); err != nil {
6260
log.Printf("Failed to read response: %v", err)
6361
conn.Close()
6462
return
6563
}
64+
response := b.Bytes()
65+
66+
if command == "AUTH" && len(cmd.Args) == 3 {
67+
if strings.HasPrefix(string(response), "+OK") {
68+
context.username = string(cmd.Args[1])
69+
conn.SetContext(context)
70+
}
71+
}
6672

6773
// Send response back to client
6874
conn.WriteRaw(response)
@@ -90,16 +96,16 @@ func (m *Handler) ClosedConn(conn redcon.Conn, err error) {
9096
}
9197

9298
func buildRESPCommand(args [][]byte) []byte {
93-
var sb strings.Builder
94-
sb.WriteString("*")
95-
sb.WriteString(strings.TrimSpace(string([]byte{byte(len(args) + '0')})))
99+
var sb bytes.Buffer
100+
sb.WriteByte('*')
101+
sb.WriteString(strconv.Itoa(len(args)))
96102
sb.WriteString("\r\n")
97103
for _, arg := range args {
98-
sb.WriteString("$")
99-
sb.WriteString(strings.TrimSpace(string([]byte{byte(len(arg) + '0')})))
104+
sb.WriteByte('$')
105+
sb.WriteString(strconv.Itoa(len(arg)))
100106
sb.WriteString("\r\n")
101107
sb.Write(arg)
102108
sb.WriteString("\r\n")
103109
}
104-
return []byte(sb.String())
110+
return sb.Bytes()
105111
}

main.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,25 @@ package main
33
import (
44
"log"
55

6+
"github.com/kelseyhightower/envconfig"
67
"github.com/tidwall/redcon"
78
)
89

9-
var addr = ":6380"
10-
1110
func main() {
12-
log.Printf("started server at %s", addr)
11+
var s Config
12+
err := envconfig.Process("", &s)
13+
if err != nil {
14+
log.Fatal(err.Error())
15+
}
16+
17+
handler := NewHandler(s)
1318

14-
handler := NewHandler(Config{})
19+
log.Printf("started server at %s", s.Listen)
1520

16-
err := redcon.ListenAndServe(addr,
21+
err = redcon.ListenAndServe(s.Listen,
1722
handler.ServeRESP,
18-
func(conn redcon.Conn) bool {
19-
// use this function to accept or deny the connection.
20-
// log.Printf("accept: %s", conn.RemoteAddr())
21-
return true
22-
},
23-
func(conn redcon.Conn, err error) {
24-
// this is called when the connection has been closed
25-
// log.Printf("closed: %s, err: %v", conn.RemoteAddr(), err)
26-
},
23+
handler.AcceptConn,
24+
handler.ClosedConn,
2725
)
2826
if err != nil {
2927
log.Fatal(err)

mod.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main
2+
3+
import "strings"
4+
5+
var modCommands = map[string]bool{
6+
"GET": true, "SET": true, "MGET": true, "MSET": true, "DEL": true,
7+
"EXISTS": true, "INCR": true, "DECR": true, "HGET": true, "HSET": true,
8+
"LPUSH": true, "RPUSH": true, "LPOP": true, "RPOP": true,
9+
"TYPE": true, "TTL": true,
10+
}
11+
12+
func modSingleCommand(command, username string, args [][]byte) transformerFn {
13+
if modCommands[command] && len(args) > 1 {
14+
args[1] = append([]byte(username+":"), args[1]...)
15+
return nil
16+
}
17+
18+
if command == "OBJECT" && len(args) == 3 {
19+
args[2] = append([]byte(username+":"), args[2]...)
20+
return nil
21+
}
22+
23+
if command == "KEYS" && len(args) == 2 {
24+
args[1] = append([]byte(username+":"), args[1]...)
25+
return func(code byte, line []byte) []byte {
26+
if len(line) == 0 || (line[0] >= '0' && line[0] <= '9') {
27+
return line
28+
}
29+
return line[len(username)+1:]
30+
}
31+
}
32+
33+
if command == "SCAN" {
34+
// Find "MATCH" argument and modify its pattern
35+
for i := 1; i < len(args)-1; i++ {
36+
if strings.ToUpper(string(args[i])) == "MATCH" {
37+
args[i+1] = append([]byte(username+":"), args[i+1]...)
38+
break
39+
}
40+
}
41+
return func(code byte, line []byte) []byte {
42+
if len(line) == 0 || (line[0] >= '0' && line[0] <= '9') {
43+
return line
44+
}
45+
return line[len(username)+1:]
46+
}
47+
}
48+
return nil
49+
}

reader.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"io"
8+
"strconv"
9+
)
10+
11+
type transformerFn func(code byte, line []byte) []byte
12+
13+
func newRespReader(b *bufio.Reader, w *bytes.Buffer, fn transformerFn) RespReader {
14+
return RespReader{
15+
br: b,
16+
bw: w,
17+
fn: fn,
18+
}
19+
}
20+
21+
type RespReader struct {
22+
br *bufio.Reader
23+
bw *bytes.Buffer
24+
fn transformerFn
25+
}
26+
27+
type readerError string
28+
29+
func (pe readerError) Error() string {
30+
return fmt.Sprintf("parse erro: %s", string(pe))
31+
}
32+
33+
func (c *RespReader) readReply() error {
34+
line, err := c.readLine()
35+
if err != nil {
36+
return err
37+
}
38+
if len(line) < 2 {
39+
return readerError("short response line")
40+
}
41+
switch line[0] {
42+
case '+', '-', ':':
43+
c.bw.Write(line)
44+
return nil
45+
case '$':
46+
n, err := parseLen(line[1:])
47+
if n < 0 || err != nil {
48+
return err
49+
}
50+
p := make([]byte, n+2)
51+
_, err = io.ReadFull(c.br, p)
52+
if err != nil {
53+
return err
54+
}
55+
if c.fn != nil {
56+
p = c.fn('$', p)
57+
}
58+
c.bw.WriteString("$" + strconv.Itoa(len(p)-2) + "\r\n")
59+
c.bw.Write(p)
60+
return nil
61+
case '*':
62+
n, err := parseLen(line[1:])
63+
if n < 0 || err != nil {
64+
return err
65+
}
66+
c.bw.Write(line)
67+
for range n {
68+
err = c.readReply()
69+
if err != nil {
70+
return err
71+
}
72+
}
73+
return nil
74+
}
75+
return readerError("unexpected response line")
76+
}
77+
78+
func (c *RespReader) readLine() ([]byte, error) {
79+
// To avoid allocations, attempt to read the line using ReadSlice. This
80+
// call typically succeeds. The known case where the call fails is when
81+
// reading the output from the MONITOR command.
82+
p, err := c.br.ReadSlice('\n')
83+
if err == bufio.ErrBufferFull {
84+
// The line does not fit in the bufio.Reader's buffer. Fall back to
85+
// allocating a buffer for the line.
86+
buf := append([]byte{}, p...)
87+
for err == bufio.ErrBufferFull {
88+
p, err = c.br.ReadSlice('\n')
89+
buf = append(buf, p...)
90+
}
91+
p = buf
92+
}
93+
if err != nil {
94+
return nil, err
95+
}
96+
i := len(p) - 2
97+
if i < 0 || p[i] != '\r' {
98+
return nil, readerError("bad response line terminator")
99+
}
100+
return p, nil
101+
}
102+
103+
// parseLen parses bulk string and array lengths.
104+
func parseLen(p []byte) (int, error) {
105+
if len(p) == 0 {
106+
return -1, readerError("malformed length")
107+
}
108+
109+
if p[0] == '-' && len(p) == 4 && p[1] == '1' {
110+
// handle $-1 and $-1 null replies.
111+
return -1, nil
112+
}
113+
114+
var n int
115+
for _, b := range p[:len(p)-2] {
116+
n *= 10
117+
if b < '0' || b > '9' {
118+
return -1, readerError("illegal bytes in length")
119+
}
120+
n += int(b - '0')
121+
}
122+
123+
return n, nil
124+
}

0 commit comments

Comments
 (0)