Skip to content

Commit 44f4410

Browse files
committed
Initial prototype
0 parents  commit 44f4410

7 files changed

Lines changed: 203 additions & 0 deletions

File tree

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Wildan M
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# RDPROXY
2+
3+
This is a redis proxy to make ACL more convenient. It prefixes keys with the ACL username before sending it to upstream and undoing it when it about to send downstream.
4+
5+
This software is currently WIP. Only support RESP2.
6+
7+
Your app can connect to this instance listening by default at port `6479`.

config.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package main
2+
3+
type Config struct {
4+
Listen int `default:":6479"`
5+
UpstreamRedis string `default:"127.0.0.1:6379"`
6+
}
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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/domcloud/rdproxy
2+
3+
go 1.24.0
4+
5+
require github.com/gomodule/redigo v1.9.2
6+
7+
require (
8+
github.com/tidwall/btree v1.1.0 // indirect
9+
github.com/tidwall/match v1.1.1 // indirect
10+
github.com/tidwall/redcon v1.6.2 // indirect
11+
)

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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=
9+
github.com/tidwall/btree v1.1.0 h1:5P+9WU8ui5uhmcg3SoPyTwoI0mVyZ1nps7YQzTZFkYM=
10+
github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
11+
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
12+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
13+
github.com/tidwall/redcon v1.6.2 h1:5qfvrrybgtO85jnhSravmkZyC0D+7WstbfCs3MmPhow=
14+
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: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"log"
6+
"net"
7+
"strings"
8+
9+
"github.com/tidwall/redcon"
10+
)
11+
12+
type Handler struct {
13+
config Config
14+
netMode string
15+
}
16+
17+
type HandlerContext struct {
18+
upstreamConn net.Conn
19+
username string
20+
}
21+
22+
func NewHandler(config Config) *Handler {
23+
netMode := "tcp"
24+
if strings.HasPrefix(config.UpstreamRedis, "/") || strings.HasPrefix(config.UpstreamRedis, "@") {
25+
netMode = "unix"
26+
}
27+
return &Handler{
28+
config: config,
29+
netMode: netMode,
30+
}
31+
}
32+
33+
func (m *Handler) ServeRESP(conn redcon.Conn, cmd redcon.Command) {
34+
context, ok := conn.Context().(HandlerContext)
35+
if !ok || context.upstreamConn == nil {
36+
conn.Close()
37+
return
38+
}
39+
40+
upConn := context.upstreamConn
41+
command := strings.ToUpper(string(cmd.Args[0]))
42+
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
49+
request := buildRESPCommand(cmd.Args)
50+
51+
// Send to Redis
52+
_, err := upConn.Write(request)
53+
if err != nil {
54+
log.Printf("Failed to send command: %v", err)
55+
conn.Close()
56+
return
57+
}
58+
59+
// Read response from Redis
60+
response, err := bufio.NewReader(upConn).ReadBytes('\n')
61+
if err != nil {
62+
log.Printf("Failed to read response: %v", err)
63+
conn.Close()
64+
return
65+
}
66+
67+
// Send response back to client
68+
conn.WriteRaw(response)
69+
}
70+
71+
func (m *Handler) AcceptConn(conn redcon.Conn) bool {
72+
redisConn, err := net.Dial(m.netMode, m.config.UpstreamRedis)
73+
if err != nil {
74+
log.Printf("Failed to connect to Redis: %v", err)
75+
return false
76+
}
77+
78+
conn.SetContext(HandlerContext{
79+
upstreamConn: redisConn,
80+
username: "default", // TODO: Fetch username dynamically
81+
})
82+
return true
83+
}
84+
85+
func (m *Handler) ClosedConn(conn redcon.Conn, err error) {
86+
context, ok := conn.Context().(HandlerContext)
87+
if ok && context.upstreamConn != nil {
88+
context.upstreamConn.Close()
89+
}
90+
}
91+
92+
func buildRESPCommand(args [][]byte) []byte {
93+
var sb strings.Builder
94+
sb.WriteString("*")
95+
sb.WriteString(strings.TrimSpace(string([]byte{byte(len(args) + '0')})))
96+
sb.WriteString("\r\n")
97+
for _, arg := range args {
98+
sb.WriteString("$")
99+
sb.WriteString(strings.TrimSpace(string([]byte{byte(len(arg) + '0')})))
100+
sb.WriteString("\r\n")
101+
sb.Write(arg)
102+
sb.WriteString("\r\n")
103+
}
104+
return []byte(sb.String())
105+
}

main.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"log"
5+
6+
"github.com/tidwall/redcon"
7+
)
8+
9+
var addr = ":6380"
10+
11+
func main() {
12+
log.Printf("started server at %s", addr)
13+
14+
handler := NewHandler(Config{})
15+
16+
err := redcon.ListenAndServe(addr,
17+
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+
},
27+
)
28+
if err != nil {
29+
log.Fatal(err)
30+
}
31+
}

0 commit comments

Comments
 (0)