Skip to content

Commit 019ff6e

Browse files
author
Edward Muller
committed
Add support for parsing redis:// and rediss:// URLs
This includes setting up a default dialer that handles the ssl handshake.
1 parent 80cf5d1 commit 019ff6e

File tree

2 files changed

+164
-0
lines changed

2 files changed

+164
-0
lines changed

options.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package redis
22

33
import (
4+
"crypto/tls"
5+
"errors"
46
"net"
7+
"net/url"
8+
"strconv"
9+
"strings"
510
"time"
611

712
"gopkg.in/redis.v5/internal/pool"
@@ -58,6 +63,9 @@ type Options struct {
5863

5964
// Enables read only queries on slave nodes.
6065
ReadOnly bool
66+
67+
// Config to use when connecting via TLS
68+
TLSConfig *tls.Config
6169
}
6270

6371
func (opt *Options) init() {
@@ -92,6 +100,70 @@ func (opt *Options) init() {
92100
}
93101
}
94102

103+
// ParseURL parses a redis URL into options that can be used to connect to redis
104+
func ParseURL(redisURL string) (*Options, error) {
105+
o := &Options{Network: "tcp"}
106+
u, err := url.Parse(redisURL)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
if u.Scheme != "redis" && u.Scheme != "rediss" {
112+
return nil, errors.New("invalid redis URL scheme: " + u.Scheme)
113+
}
114+
115+
if u.User != nil {
116+
if p, ok := u.User.Password(); ok {
117+
o.Password = p
118+
}
119+
}
120+
121+
if len(u.Query()) > 0 {
122+
return nil, errors.New("no options supported")
123+
}
124+
125+
h, p, err := net.SplitHostPort(u.Host)
126+
if err != nil {
127+
h = u.Host
128+
}
129+
if h == "" {
130+
h = "localhost"
131+
}
132+
if p == "" {
133+
p = "6379"
134+
}
135+
o.Addr = net.JoinHostPort(h, p)
136+
137+
f := strings.FieldsFunc(u.Path, func(r rune) bool {
138+
return r == '/'
139+
})
140+
switch len(f) {
141+
case 0:
142+
o.DB = 0
143+
case 1:
144+
if o.DB, err = strconv.Atoi(f[0]); err != nil {
145+
return nil, errors.New("Invalid redis database number: " + err.Error())
146+
}
147+
default:
148+
return nil, errors.New("invalid redis URL path: " + u.Path)
149+
}
150+
151+
if u.Scheme == "rediss" {
152+
o.Dialer = func() (net.Conn, error) {
153+
conn, err := net.DialTimeout(o.Network, o.Addr, o.DialTimeout)
154+
if err != nil {
155+
return nil, err
156+
}
157+
if o.TLSConfig == nil {
158+
o.TLSConfig = &tls.Config{InsecureSkipVerify: true}
159+
}
160+
t := tls.Client(conn, o.TLSConfig)
161+
return t, t.Handshake()
162+
}
163+
}
164+
return o, nil
165+
}
166+
95167
func newConnPool(opt *Options) *pool.ConnPool {
96168
return pool.NewConnPool(
97169
opt.Dialer,

options_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package redis
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
func TestParseURL(t *testing.T) {
9+
cases := []struct {
10+
u string
11+
addr string
12+
db int
13+
dialer bool
14+
err error
15+
}{
16+
{
17+
"redis://localhost:123/1",
18+
"localhost:123",
19+
1, false, nil,
20+
},
21+
{
22+
"redis://localhost:123",
23+
"localhost:123",
24+
0, false, nil,
25+
},
26+
{
27+
"redis://localhost/1",
28+
"localhost:6379",
29+
1, false, nil,
30+
},
31+
{
32+
"redis://12345",
33+
"12345:6379",
34+
0, false, nil,
35+
},
36+
{
37+
"rediss://localhost:123",
38+
"localhost:123",
39+
0, true, nil,
40+
},
41+
{
42+
"redis://localhost/?abc=123",
43+
"",
44+
0, false, errors.New("no options supported"),
45+
},
46+
{
47+
"http://google.com",
48+
"",
49+
0, false, errors.New("invalid redis URL scheme: http"),
50+
},
51+
{
52+
"redis://localhost/1/2/3/4",
53+
"",
54+
0, false, errors.New("invalid redis URL path: /1/2/3/4"),
55+
},
56+
{
57+
"12345",
58+
"",
59+
0, false, errors.New("invalid redis URL scheme: "),
60+
},
61+
{
62+
"redis://localhost/iamadatabase",
63+
"",
64+
0, false, errors.New("Invalid redis database number: strconv.ParseInt: parsing \"iamadatabase\": invalid syntax"),
65+
},
66+
}
67+
68+
for _, c := range cases {
69+
t.Run(c.u, func(t *testing.T) {
70+
o, err := ParseURL(c.u)
71+
if c.err == nil && err != nil {
72+
t.Fatalf("Expected err to be nil, but got: '%q'", err)
73+
return
74+
}
75+
if c.err != nil && err != nil {
76+
if c.err.Error() != err.Error() {
77+
t.Fatalf("Expected err to be '%q', but got '%q'", c.err, err)
78+
}
79+
return
80+
}
81+
if o.Addr != c.addr {
82+
t.Errorf("Expected Addr to be '%s', but got '%s'", c.addr, o.Addr)
83+
}
84+
if o.DB != c.db {
85+
t.Errorf("Expecdted DB to be '%d', but got '%d'", c.db, o.DB)
86+
}
87+
if c.dialer && o.Dialer == nil {
88+
t.Errorf("Expected a Dialer to be set, but isn't")
89+
}
90+
})
91+
}
92+
}

0 commit comments

Comments
 (0)