From f3f4245e7e2d0d21819e07bce9960fab0a9e1155 Mon Sep 17 00:00:00 2001 From: Dmitry Moskowski Date: Wed, 24 Apr 2024 12:31:00 +0000 Subject: [PATCH 1/3] support redis+socket scheme in url --- redis/conn.go | 114 ++++++++++++++++++++++++++------------------- redis/conn_test.go | 4 +- redis/test_test.go | 7 +-- 3 files changed, 74 insertions(+), 51 deletions(-) diff --git a/redis/conn.go b/redis/conn.go index 753644b1..c600f491 100644 --- a/redis/conn.go +++ b/redis/conn.go @@ -324,71 +324,91 @@ func DialURL(rawurl string, options ...DialOption) (Conn, error) { } // DialURLContext connects to a Redis server at the given URL using the Redis -// URI scheme. URLs should follow the draft IANA specification for the -// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). +// URI scheme. It supports: +// redis - unencrypted tcp connection +// rediss - TLS encrypted tcp connection +// redis+socket - UNIX socket connection func DialURLContext(ctx context.Context, rawurl string, options ...DialOption) (Conn, error) { u, err := url.Parse(rawurl) if err != nil { return nil, err } - - if u.Scheme != "redis" && u.Scheme != "rediss" { - return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) - } - if u.Opaque != "" { return nil, fmt.Errorf("invalid redis URL, url is opaque: %s", rawurl) } - // As per the IANA draft spec, the host defaults to localhost and - // the port defaults to 6379. - host, port, err := net.SplitHostPort(u.Host) - if err != nil { - // assume port is missing - host = u.Host - port = "6379" - } - if host == "" { - host = "localhost" - } - address := net.JoinHostPort(host, port) - - if u.User != nil { - password, isSet := u.User.Password() - username := u.User.Username() - if isSet { - if username != "" { - // ACL - options = append(options, DialUsername(username), DialPassword(password)) - } else { - // requirepass - user-info username:password with blank username - options = append(options, DialPassword(password)) + var ( + network string + address string + db = 0 + ) + switch u.Scheme { + case "redis", "rediss": + // As per the IANA draft spec, the host defaults to localhost and + // the port defaults to 6379. + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + // assume port is missing + host = u.Host + port = "6379" + } + if host == "" { + host = "localhost" + } + network = "tcp" + address = net.JoinHostPort(host, port) + + if u.User != nil { + password, isSet := u.User.Password() + username := u.User.Username() + if isSet { + if username != "" { + // ACL + options = append(options, DialUsername(username), DialPassword(password)) + } else { + // requirepass - user-info username:password with blank username + options = append(options, DialPassword(password)) + } + } else if username != "" { + // requirepass - redis-cli compatibility which treats as single arg in user-info as a password + options = append(options, DialPassword(username)) } - } else if username != "" { - // requirepass - redis-cli compatibility which treats as single arg in user-info as a password - options = append(options, DialPassword(username)) } - } + match := pathDBRegexp.FindStringSubmatch(u.Path) + if len(match) == 2 { + if len(match[1]) > 0 { + db, err = strconv.Atoi(match[1]) + if err != nil { + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + } + } + if db != 0 { + options = append(options, DialDatabase(db)) + } + } else if u.Path != "" { + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + } - match := pathDBRegexp.FindStringSubmatch(u.Path) - if len(match) == 2 { - db := 0 - if len(match[1]) > 0 { - db, err = strconv.Atoi(match[1]) + options = append(options, DialUseTLS(u.Scheme == "rediss")) + case "redis+socket": + network = "unix" + address = u.Path + dbParameter := u.Query().Get("db") + if dbParameter != "" { + db, err = strconv.Atoi(dbParameter) if err != nil { return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) } + if db != 0 { + options = append(options, DialDatabase(db)) + } } - if db != 0 { - options = append(options, DialDatabase(db)) - } - } else if u.Path != "" { - return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + options = append(options, DialUseTLS(false)) + default: + return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) } - options = append(options, DialUseTLS(u.Scheme == "rediss")) - - return DialContext(ctx, "tcp", address, options...) + return DialContext(ctx, network, address, options...) } // NewConn returns a new Redigo connection for the given net connection. diff --git a/redis/conn_test.go b/redis/conn_test.go index b94f0f42..daf78794 100644 --- a/redis/conn_test.go +++ b/redis/conn_test.go @@ -23,10 +23,10 @@ import ( "io" "math" "net" - "sync" "os" "reflect" "strings" + "sync" "testing" "time" @@ -680,6 +680,8 @@ var dialURLTests = []struct { {"database 3", "redis://localhost/3", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n"}, {"database 99", "redis://localhost/99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, {"no database", "redis://localhost/", "+OK\r\n", ""}, + {"database 99", "redis+socket://./server.sock?db=99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, + {"no database", "redis+socket://./server.sock", "+OK\r\n", ""}, } func TestDialURL(t *testing.T) { diff --git a/redis/test_test.go b/redis/test_test.go index f7598683..94ec0ea1 100644 --- a/redis/test_test.go +++ b/redis/test_test.go @@ -21,7 +21,6 @@ import ( "flag" "fmt" "io" - "io/ioutil" "os" "os/exec" "regexp" @@ -40,10 +39,11 @@ var ( ErrNegativeInt = errNegativeInt serverPath = flag.String("redis-server", "redis-server", "Path to redis server binary") - serverAddress = flag.String("redis-address", "127.0.0.1", "The address of the server") + serverAddress = flag.String("redis-address", "127.0.0.1", "The TCP address of the server") serverBasePort = flag.Int("redis-port", 16379, "Beginning of port range for test servers") + serverSocket = flag.String("redis-socket", "./server.sock", "The UNIX socket of the server") serverLogName = flag.String("redis-log", "", "Write Redis server logs to `filename`") - serverLog = ioutil.Discard + serverLog = io.Discard defaultServerMu sync.Mutex defaultServer *Server @@ -190,6 +190,7 @@ func DefaultServerAddr() (string, error) { "default", "--port", strconv.Itoa(*serverBasePort), "--bind", *serverAddress, + "--unixsocket", *serverSocket, "--save", "", "--appendonly", "no") return addr, defaultServerErr From de86cf432d1f45e88db1d7098b1aa46cb282b5d4 Mon Sep 17 00:00:00 2001 From: Dmitry Moskowski Date: Mon, 27 May 2024 23:22:47 +0000 Subject: [PATCH 2/3] experimenting with unix socket support using redis:// scheme --- redis/conn.go | 110 ++++++++++++++++++++++----------------------- redis/conn_test.go | 8 +++- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/redis/conn.go b/redis/conn.go index c600f491..b33b4d55 100644 --- a/redis/conn.go +++ b/redis/conn.go @@ -324,10 +324,8 @@ func DialURL(rawurl string, options ...DialOption) (Conn, error) { } // DialURLContext connects to a Redis server at the given URL using the Redis -// URI scheme. It supports: -// redis - unencrypted tcp connection -// rediss - TLS encrypted tcp connection -// redis+socket - UNIX socket connection +// URI scheme. URLs should follow the draft IANA specification for the +// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). func DialURLContext(ctx context.Context, rawurl string, options ...DialOption) (Conn, error) { u, err := url.Parse(rawurl) if err != nil { @@ -344,66 +342,68 @@ func DialURLContext(ctx context.Context, rawurl string, options ...DialOption) ( ) switch u.Scheme { case "redis", "rediss": - // As per the IANA draft spec, the host defaults to localhost and - // the port defaults to 6379. - host, port, err := net.SplitHostPort(u.Host) - if err != nil { - // assume port is missing - host = u.Host - port = "6379" - } - if host == "" { - host = "localhost" - } - network = "tcp" - address = net.JoinHostPort(host, port) - - if u.User != nil { - password, isSet := u.User.Password() - username := u.User.Username() - if isSet { - if username != "" { - // ACL - options = append(options, DialUsername(username), DialPassword(password)) - } else { - // requirepass - user-info username:password with blank username - options = append(options, DialPassword(password)) - } - } else if username != "" { - // requirepass - redis-cli compatibility which treats as single arg in user-info as a password - options = append(options, DialPassword(username)) - } - } - match := pathDBRegexp.FindStringSubmatch(u.Path) - if len(match) == 2 { - if len(match[1]) > 0 { - db, err = strconv.Atoi(match[1]) + if (u.Host == "" || u.Host == ".") && len(u.Path) > 0 && u.Path[0] == '/' { + network = "unix" + + address = u.Path + dbParameter := u.Query().Get("db") + if dbParameter != "" { + db, err = strconv.Atoi(dbParameter) if err != nil { return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) } + if db != 0 { + options = append(options, DialDatabase(db)) + } } - if db != 0 { - options = append(options, DialDatabase(db)) - } - } else if u.Path != "" { - return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) - } + } else { + network = "tcp" - options = append(options, DialUseTLS(u.Scheme == "rediss")) - case "redis+socket": - network = "unix" - address = u.Path - dbParameter := u.Query().Get("db") - if dbParameter != "" { - db, err = strconv.Atoi(dbParameter) + // As per the IANA draft spec, the host defaults to localhost and + // the port defaults to 6379. + host, port, err := net.SplitHostPort(u.Host) if err != nil { - return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + // assume port is missing + host = u.Host + port = "6379" + } + if host == "" { + host = "localhost" } - if db != 0 { - options = append(options, DialDatabase(db)) + address = net.JoinHostPort(host, port) + if u.User != nil { + password, isSet := u.User.Password() + username := u.User.Username() + if isSet { + if username != "" { + // ACL + options = append(options, DialUsername(username), DialPassword(password)) + } else { + // requirepass - user-info username:password with blank username + options = append(options, DialPassword(password)) + } + } else if username != "" { + // requirepass - redis-cli compatibility which treats as single arg in user-info as a password + options = append(options, DialPassword(username)) + } + } + match := pathDBRegexp.FindStringSubmatch(u.Path) + if len(match) == 2 { + if len(match[1]) > 0 { + db, err = strconv.Atoi(match[1]) + if err != nil { + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + } + } + if db != 0 { + options = append(options, DialDatabase(db)) + } + } else if u.Path != "" { + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) } } - options = append(options, DialUseTLS(false)) + + options = append(options, DialUseTLS(u.Scheme == "rediss")) default: return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) } diff --git a/redis/conn_test.go b/redis/conn_test.go index daf78794..9053ffa9 100644 --- a/redis/conn_test.go +++ b/redis/conn_test.go @@ -24,6 +24,7 @@ import ( "math" "net" "os" + "path/filepath" "reflect" "strings" "sync" @@ -661,6 +662,8 @@ func TestDialURLHost(t *testing.T) { } } +var workingDirectory, _ = os.Getwd() + var dialURLTests = []struct { description string url string @@ -680,8 +683,9 @@ var dialURLTests = []struct { {"database 3", "redis://localhost/3", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n"}, {"database 99", "redis://localhost/99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, {"no database", "redis://localhost/", "+OK\r\n", ""}, - {"database 99", "redis+socket://./server.sock?db=99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, - {"no database", "redis+socket://./server.sock", "+OK\r\n", ""}, + {"absolute socket path", "redis://"+filepath.Join(workingDirectory, "server.sock"), "+OK\r\n", ""}, + {"relative socket path", "redis://./server.sock", "+OK\r\n", ""}, + {"socket path database 99", "redis://./server.sock?db=99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, } func TestDialURL(t *testing.T) { From fd95552d463a47673b0fe28217cb174bdd16161c Mon Sep 17 00:00:00 2001 From: Dmitry Moskowski Date: Mon, 27 May 2024 23:25:47 +0000 Subject: [PATCH 3/3] revert changes back to support redis+unix:// scheme --- redis/conn.go | 109 ++++++++++++++++++++++----------------------- redis/conn_test.go | 6 +-- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/redis/conn.go b/redis/conn.go index b33b4d55..634300be 100644 --- a/redis/conn.go +++ b/redis/conn.go @@ -324,8 +324,10 @@ func DialURL(rawurl string, options ...DialOption) (Conn, error) { } // DialURLContext connects to a Redis server at the given URL using the Redis -// URI scheme. URLs should follow the draft IANA specification for the -// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). +// URI scheme. It supports: +// redis - unencrypted tcp connection +// rediss - TLS encrypted tcp connection +// redis+unix - UNIX socket connection func DialURLContext(ctx context.Context, rawurl string, options ...DialOption) (Conn, error) { u, err := url.Parse(rawurl) if err != nil { @@ -342,68 +344,65 @@ func DialURLContext(ctx context.Context, rawurl string, options ...DialOption) ( ) switch u.Scheme { case "redis", "rediss": - if (u.Host == "" || u.Host == ".") && len(u.Path) > 0 && u.Path[0] == '/' { - network = "unix" + network = "tcp" - address = u.Path - dbParameter := u.Query().Get("db") - if dbParameter != "" { - db, err = strconv.Atoi(dbParameter) + // As per the IANA draft spec, the host defaults to localhost and + // the port defaults to 6379. + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + // assume port is missing + host = u.Host + port = "6379" + } + if host == "" { + host = "localhost" + } + address = net.JoinHostPort(host, port) + if u.User != nil { + password, isSet := u.User.Password() + username := u.User.Username() + if isSet { + if username != "" { + // ACL + options = append(options, DialUsername(username), DialPassword(password)) + } else { + // requirepass - user-info username:password with blank username + options = append(options, DialPassword(password)) + } + } else if username != "" { + // requirepass - redis-cli compatibility which treats as single arg in user-info as a password + options = append(options, DialPassword(username)) + } + } + match := pathDBRegexp.FindStringSubmatch(u.Path) + if len(match) == 2 { + if len(match[1]) > 0 { + db, err = strconv.Atoi(match[1]) if err != nil { return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) } - if db != 0 { - options = append(options, DialDatabase(db)) - } } - } else { - network = "tcp" - - // As per the IANA draft spec, the host defaults to localhost and - // the port defaults to 6379. - host, port, err := net.SplitHostPort(u.Host) - if err != nil { - // assume port is missing - host = u.Host - port = "6379" + if db != 0 { + options = append(options, DialDatabase(db)) } - if host == "" { - host = "localhost" - } - address = net.JoinHostPort(host, port) - if u.User != nil { - password, isSet := u.User.Password() - username := u.User.Username() - if isSet { - if username != "" { - // ACL - options = append(options, DialUsername(username), DialPassword(password)) - } else { - // requirepass - user-info username:password with blank username - options = append(options, DialPassword(password)) - } - } else if username != "" { - // requirepass - redis-cli compatibility which treats as single arg in user-info as a password - options = append(options, DialPassword(username)) - } - } - match := pathDBRegexp.FindStringSubmatch(u.Path) - if len(match) == 2 { - if len(match[1]) > 0 { - db, err = strconv.Atoi(match[1]) - if err != nil { - return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) - } - } - if db != 0 { - options = append(options, DialDatabase(db)) - } - } else if u.Path != "" { + } else if u.Path != "" { + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + } + options = append(options, DialUseTLS(u.Scheme == "rediss")) + case "redis+unix": + network = "unix" + address = u.Path + dbParameter := u.Query().Get("db") + if dbParameter != "" { + db, err = strconv.Atoi(dbParameter) + if err != nil { return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) } + if db != 0 { + options = append(options, DialDatabase(db)) + } } - - options = append(options, DialUseTLS(u.Scheme == "rediss")) + options = append(options, DialUseTLS(false)) default: return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) } diff --git a/redis/conn_test.go b/redis/conn_test.go index 9053ffa9..6392d863 100644 --- a/redis/conn_test.go +++ b/redis/conn_test.go @@ -683,9 +683,9 @@ var dialURLTests = []struct { {"database 3", "redis://localhost/3", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n"}, {"database 99", "redis://localhost/99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, {"no database", "redis://localhost/", "+OK\r\n", ""}, - {"absolute socket path", "redis://"+filepath.Join(workingDirectory, "server.sock"), "+OK\r\n", ""}, - {"relative socket path", "redis://./server.sock", "+OK\r\n", ""}, - {"socket path database 99", "redis://./server.sock?db=99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, + {"absolute socket path", "redis+unix://" + filepath.Join(workingDirectory, "server.sock"), "+OK\r\n", ""}, + {"relative socket path", "redis+unix://./server.sock", "+OK\r\n", ""}, + {"unix socket path database 99", "redis+unix://./server.sock?db=99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, } func TestDialURL(t *testing.T) {