diff --git a/options.go b/options.go index 376fa6bf..77ffbce3 100644 --- a/options.go +++ b/options.go @@ -1,7 +1,12 @@ package redis import ( + "crypto/tls" + "errors" "net" + "net/url" + "strconv" + "strings" "time" "gopkg.in/redis.v5/internal/pool" @@ -58,6 +63,9 @@ type Options struct { // Enables read only queries on slave nodes. ReadOnly bool + + // TLS Config to use. When set TLS will be negotiated. + TLSConfig *tls.Config } func (opt *Options) init() { @@ -66,7 +74,12 @@ func (opt *Options) init() { } if opt.Dialer == nil { opt.Dialer = func() (net.Conn, error) { - return net.DialTimeout(opt.Network, opt.Addr, opt.DialTimeout) + conn, err := net.DialTimeout(opt.Network, opt.Addr, opt.DialTimeout) + if opt.TLSConfig == nil || err != nil { + return conn, err + } + t := tls.Client(conn, opt.TLSConfig) + return t, t.Handshake() } } if opt.PoolSize == 0 { @@ -92,6 +105,60 @@ func (opt *Options) init() { } } +// ParseURL parses a redis URL into options that can be used to connect to redis +func ParseURL(redisURL string) (*Options, error) { + o := &Options{Network: "tcp"} + u, err := url.Parse(redisURL) + if err != nil { + return nil, err + } + + if u.Scheme != "redis" && u.Scheme != "rediss" { + return nil, errors.New("invalid redis URL scheme: " + u.Scheme) + } + + if u.User != nil { + if p, ok := u.User.Password(); ok { + o.Password = p + } + } + + if len(u.Query()) > 0 { + return nil, errors.New("no options supported") + } + + h, p, err := net.SplitHostPort(u.Host) + if err != nil { + h = u.Host + } + if h == "" { + h = "localhost" + } + if p == "" { + p = "6379" + } + o.Addr = net.JoinHostPort(h, p) + + f := strings.FieldsFunc(u.Path, func(r rune) bool { + return r == '/' + }) + switch len(f) { + case 0: + o.DB = 0 + case 1: + if o.DB, err = strconv.Atoi(f[0]); err != nil { + return nil, errors.New("invalid redis database number: " + err.Error()) + } + default: + return nil, errors.New("invalid redis URL path: " + u.Path) + } + + if u.Scheme == "rediss" { + o.TLSConfig = &tls.Config{ServerName: h} + } + return o, nil +} + func newConnPool(opt *Options) *pool.ConnPool { return pool.NewConnPool( opt.Dialer, diff --git a/options_test.go b/options_test.go new file mode 100644 index 00000000..effebd5a --- /dev/null +++ b/options_test.go @@ -0,0 +1,94 @@ +// +build go1.7 + +package redis + +import ( + "errors" + "testing" +) + +func TestParseURL(t *testing.T) { + cases := []struct { + u string + addr string + db int + tls bool + err error + }{ + { + "redis://localhost:123/1", + "localhost:123", + 1, false, nil, + }, + { + "redis://localhost:123", + "localhost:123", + 0, false, nil, + }, + { + "redis://localhost/1", + "localhost:6379", + 1, false, nil, + }, + { + "redis://12345", + "12345:6379", + 0, false, nil, + }, + { + "rediss://localhost:123", + "localhost:123", + 0, true, nil, + }, + { + "redis://localhost/?abc=123", + "", + 0, false, errors.New("no options supported"), + }, + { + "http://google.com", + "", + 0, false, errors.New("invalid redis URL scheme: http"), + }, + { + "redis://localhost/1/2/3/4", + "", + 0, false, errors.New("invalid redis URL path: /1/2/3/4"), + }, + { + "12345", + "", + 0, false, errors.New("invalid redis URL scheme: "), + }, + { + "redis://localhost/iamadatabase", + "", + 0, false, errors.New("invalid redis database number: strconv.ParseInt: parsing \"iamadatabase\": invalid syntax"), + }, + } + + for _, c := range cases { + t.Run(c.u, func(t *testing.T) { + o, err := ParseURL(c.u) + if c.err == nil && err != nil { + t.Fatalf("unexpected error: '%q'", err) + return + } + if c.err != nil && err != nil { + if c.err.Error() != err.Error() { + t.Fatalf("got %q, expected %q", err, c.err) + } + return + } + if o.Addr != c.addr { + t.Errorf("got %q, want %q", o.Addr, c.addr) + } + if o.DB != c.db { + t.Errorf("got %q, expected %q", o.DB, c.db) + } + if c.tls && o.TLSConfig == nil { + t.Errorf("got nil TLSConfig, expected a TLSConfig") + } + }) + } +}