Gitea was not able to supply any authentication parameters to it. So this brings support to do that, along with some light extraction of a couple of bits into some separate functions for easier testing. I looked at other libraries supporting similar RedisUri-style connection strings (e.g. Lettuce), but it looks like this type of configuration is beyond what would typically be done in a connection string. Since gitea doesn't have configuration options for manually specifying all this redis connection detail, I went ahead and just chose straightforward names for these new parameters.tags/v1.18.0-dev
@@ -32,7 +32,7 @@ require ( | |||
github.com/go-git/go-billy/v5 v5.3.1 | |||
github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 | |||
github.com/go-ldap/ldap/v3 v3.4.2 | |||
github.com/go-redis/redis/v8 v8.11.4 | |||
github.com/go-redis/redis/v8 v8.11.5 | |||
github.com/go-sql-driver/mysql v1.6.0 | |||
github.com/go-swagger/go-swagger v0.29.0 | |||
github.com/go-testfixtures/testfixtures/v3 v3.6.1 |
@@ -593,8 +593,8 @@ github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w | |||
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= | |||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= | |||
github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M= | |||
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= | |||
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= | |||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= | |||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= | |||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | |||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | |||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | |||
@@ -758,6 +758,7 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe | |||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= | |||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= | |||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= | |||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= | |||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= | |||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= | |||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= | |||
@@ -1233,15 +1234,18 @@ github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W | |||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | |||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= | |||
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= | |||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= | |||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= | |||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= | |||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= | |||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= | |||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | |||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | |||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= | |||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= | |||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= | |||
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= | |||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= | |||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= | |||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= | |||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= | |||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= | |||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= | |||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= |
@@ -6,10 +6,13 @@ package nosql | |||
import ( | |||
"crypto/tls" | |||
"net/url" | |||
"path" | |||
"strconv" | |||
"strings" | |||
"code.gitea.io/gitea/modules/log" | |||
"github.com/go-redis/redis/v8" | |||
) | |||
@@ -59,8 +62,59 @@ func (m *Manager) GetRedisClient(connection string) redis.UniversalClient { | |||
name: []string{connection, uri.String()}, | |||
} | |||
opts := getRedisOptions(uri) | |||
tlsConfig := getRedisTLSOptions(uri) | |||
clientName := uri.Query().Get("clientname") | |||
if len(clientName) > 0 { | |||
client.name = append(client.name, clientName) | |||
} | |||
switch uri.Scheme { | |||
case "redis+sentinels": | |||
fallthrough | |||
case "rediss+sentinel": | |||
opts.TLSConfig = tlsConfig | |||
fallthrough | |||
case "redis+sentinel": | |||
client.UniversalClient = redis.NewFailoverClient(opts.Failover()) | |||
case "redis+clusters": | |||
fallthrough | |||
case "rediss+cluster": | |||
opts.TLSConfig = tlsConfig | |||
fallthrough | |||
case "redis+cluster": | |||
client.UniversalClient = redis.NewClusterClient(opts.Cluster()) | |||
case "redis+socket": | |||
simpleOpts := opts.Simple() | |||
simpleOpts.Network = "unix" | |||
simpleOpts.Addr = path.Join(uri.Host, uri.Path) | |||
client.UniversalClient = redis.NewClient(simpleOpts) | |||
case "rediss": | |||
opts.TLSConfig = tlsConfig | |||
fallthrough | |||
case "redis": | |||
client.UniversalClient = redis.NewClient(opts.Simple()) | |||
default: | |||
return nil | |||
} | |||
for _, name := range client.name { | |||
m.RedisConnections[name] = client | |||
} | |||
client.count++ | |||
return client | |||
} | |||
// getRedisOptions pulls various configuration options based on the RedisUri format and converts them to go-redis's | |||
// UniversalOptions fields. This function explicitly excludes fields related to TLS configuration, which is | |||
// conditionally attached to this options struct before being converted to the specific type for the redis scheme being | |||
// used, and only in scenarios where TLS is applicable (e.g. rediss://, redis+clusters://). | |||
func getRedisOptions(uri *url.URL) *redis.UniversalOptions { | |||
opts := &redis.UniversalOptions{} | |||
tlsConfig := &tls.Config{} | |||
// Handle username/password | |||
if password, ok := uri.User.Password(); ok { | |||
@@ -131,75 +185,54 @@ func (m *Manager) GetRedisClient(connection string) redis.UniversalClient { | |||
fallthrough | |||
case "mastername": | |||
opts.MasterName = v[0] | |||
case "skipverify": | |||
fallthrough | |||
case "insecureskipverify": | |||
insecureSkipVerify, _ := strconv.ParseBool(v[0]) | |||
tlsConfig.InsecureSkipVerify = insecureSkipVerify | |||
case "clientname": | |||
client.name = append(client.name, v[0]) | |||
case "sentinelusername": | |||
opts.SentinelUsername = v[0] | |||
case "sentinelpassword": | |||
opts.SentinelPassword = v[0] | |||
} | |||
} | |||
switch uri.Scheme { | |||
case "redis+sentinels": | |||
fallthrough | |||
case "rediss+sentinel": | |||
opts.TLSConfig = tlsConfig | |||
fallthrough | |||
case "redis+sentinel": | |||
if uri.Host != "" { | |||
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...) | |||
} | |||
if uri.Path != "" { | |||
if db, err := strconv.Atoi(uri.Path[1:]); err == nil { | |||
opts.DB = db | |||
} | |||
} | |||
if uri.Host != "" { | |||
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...) | |||
} | |||
client.UniversalClient = redis.NewFailoverClient(opts.Failover()) | |||
case "redis+clusters": | |||
fallthrough | |||
case "rediss+cluster": | |||
opts.TLSConfig = tlsConfig | |||
fallthrough | |||
case "redis+cluster": | |||
if uri.Host != "" { | |||
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...) | |||
} | |||
if uri.Path != "" { | |||
if db, err := strconv.Atoi(uri.Path[1:]); err == nil { | |||
opts.DB = db | |||
} | |||
// A redis connection string uses the path section of the URI in two different ways. In a TCP-based connection, the | |||
// path will be a database index to automatically have the client SELECT. In a Unix socket connection, it will be the | |||
// file path. We only want to try to coerce this to the database index when we're not expecting a file path so that | |||
// the error log stays clean. | |||
if uri.Path != "" && uri.Scheme != "redis+socket" { | |||
if db, err := strconv.Atoi(uri.Path[1:]); err == nil { | |||
opts.DB = db | |||
} else { | |||
log.Error("Provided database identifier '%s' is not a valid integer. Gitea will ignore this option.", uri.Path) | |||
} | |||
client.UniversalClient = redis.NewClusterClient(opts.Cluster()) | |||
case "redis+socket": | |||
simpleOpts := opts.Simple() | |||
simpleOpts.Network = "unix" | |||
simpleOpts.Addr = path.Join(uri.Host, uri.Path) | |||
client.UniversalClient = redis.NewClient(simpleOpts) | |||
case "rediss": | |||
opts.TLSConfig = tlsConfig | |||
fallthrough | |||
case "redis": | |||
if uri.Host != "" { | |||
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...) | |||
} | |||
if uri.Path != "" { | |||
if db, err := strconv.Atoi(uri.Path[1:]); err == nil { | |||
opts.DB = db | |||
} | |||
} | |||
client.UniversalClient = redis.NewClient(opts.Simple()) | |||
default: | |||
return nil | |||
} | |||
for _, name := range client.name { | |||
m.RedisConnections[name] = client | |||
return opts | |||
} | |||
// getRedisTlsOptions parses RedisUri TLS configuration parameters and converts them to the go TLS configuration | |||
// equivalent fields. | |||
func getRedisTLSOptions(uri *url.URL) *tls.Config { | |||
tlsConfig := &tls.Config{} | |||
skipverify := uri.Query().Get("skipverify") | |||
if len(skipverify) > 0 { | |||
skipverify, err := strconv.ParseBool(skipverify) | |||
if err != nil { | |||
tlsConfig.InsecureSkipVerify = skipverify | |||
} | |||
} | |||
client.count++ | |||
insecureskipverify := uri.Query().Get("insecureskipverify") | |||
return client | |||
if len(insecureskipverify) > 0 { | |||
insecureskipverify, err := strconv.ParseBool(insecureskipverify) | |||
if err != nil { | |||
tlsConfig.InsecureSkipVerify = insecureskipverify | |||
} | |||
} | |||
return tlsConfig | |||
} |
@@ -0,0 +1,64 @@ | |||
// Copyright 2022 The Gitea Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package nosql | |||
import ( | |||
"net/url" | |||
"testing" | |||
) | |||
func TestRedisUsernameOpt(t *testing.T) { | |||
uri, _ := url.Parse("redis://redis:password@myredis/0") | |||
opts := getRedisOptions(uri) | |||
if opts.Username != "redis" { | |||
t.Fail() | |||
} | |||
} | |||
func TestRedisPasswordOpt(t *testing.T) { | |||
uri, _ := url.Parse("redis://redis:password@myredis/0") | |||
opts := getRedisOptions(uri) | |||
if opts.Password != "password" { | |||
t.Fail() | |||
} | |||
} | |||
func TestRedisSentinelUsernameOpt(t *testing.T) { | |||
uri, _ := url.Parse("redis+sentinel://redis:password@myredis/0?sentinelusername=suser&sentinelpassword=spass") | |||
opts := getRedisOptions(uri).Failover() | |||
if opts.SentinelUsername != "suser" { | |||
t.Fail() | |||
} | |||
} | |||
func TestRedisSentinelPasswordOpt(t *testing.T) { | |||
uri, _ := url.Parse("redis+sentinel://redis:password@myredis/0?sentinelusername=suser&sentinelpassword=spass") | |||
opts := getRedisOptions(uri).Failover() | |||
if opts.SentinelPassword != "spass" { | |||
t.Fail() | |||
} | |||
} | |||
func TestRedisDatabaseIndexTcp(t *testing.T) { | |||
uri, _ := url.Parse("redis://redis:password@myredis/12") | |||
opts := getRedisOptions(uri) | |||
if opts.DB != 12 { | |||
t.Fail() | |||
} | |||
} | |||
func TestRedisDatabaseIndexUnix(t *testing.T) { | |||
uri, _ := url.Parse("redis+socket:///var/run/redis.sock?database=12") | |||
opts := getRedisOptions(uri) | |||
if opts.DB != 12 { | |||
t.Fail() | |||
} | |||
} |