feat(home): implement home control plane integration with Redis and TLS support
This commit is contained in:
+13
-4
@@ -2,8 +2,17 @@ package config
|
||||
|
||||
// HomeConfig configures the optional "home" control plane integration over Redis protocol.
|
||||
type HomeConfig struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Host string `yaml:"host" json:"-"`
|
||||
Port int `yaml:"port" json:"-"`
|
||||
Password string `yaml:"password" json:"-"`
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Host string `yaml:"host" json:"-"`
|
||||
Port int `yaml:"port" json:"-"`
|
||||
Password string `yaml:"password" json:"-"`
|
||||
TLS HomeTLSConfig `yaml:"tls" json:"-"`
|
||||
}
|
||||
|
||||
// HomeTLSConfig configures client-side TLS for the home Redis connection.
|
||||
type HomeTLSConfig struct {
|
||||
Enable bool `yaml:"enable" json:"-"`
|
||||
ServerName string `yaml:"server-name" json:"-"`
|
||||
InsecureSkipVerify bool `yaml:"insecure-skip-verify" json:"-"`
|
||||
CACert string `yaml:"ca-cert" json:"-"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseConfigBytesHomeTLS(t *testing.T) {
|
||||
cfg, err := ParseConfigBytes([]byte(`
|
||||
home:
|
||||
enabled: true
|
||||
host: home.example.com
|
||||
port: 444
|
||||
password: secret
|
||||
tls:
|
||||
enable: true
|
||||
server-name: home.example.com
|
||||
ca-cert: C:/certs/ca.pem
|
||||
insecure-skip-verify: true
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseConfigBytes() error = %v", err)
|
||||
}
|
||||
|
||||
if !cfg.Home.Enabled {
|
||||
t.Fatal("Home.Enabled = false, want true")
|
||||
}
|
||||
if cfg.Home.Host != "home.example.com" {
|
||||
t.Fatalf("Home.Host = %q, want home.example.com", cfg.Home.Host)
|
||||
}
|
||||
if cfg.Home.Port != 444 {
|
||||
t.Fatalf("Home.Port = %d, want 444", cfg.Home.Port)
|
||||
}
|
||||
if cfg.Home.Password != "secret" {
|
||||
t.Fatalf("Home.Password = %q, want secret", cfg.Home.Password)
|
||||
}
|
||||
if !cfg.Home.TLS.Enable {
|
||||
t.Fatal("Home.TLS.Enable = false, want true")
|
||||
}
|
||||
if cfg.Home.TLS.ServerName != "home.example.com" {
|
||||
t.Fatalf("Home.TLS.ServerName = %q, want home.example.com", cfg.Home.TLS.ServerName)
|
||||
}
|
||||
if cfg.Home.TLS.CACert != "C:/certs/ca.pem" {
|
||||
t.Fatalf("Home.TLS.CACert = %q, want C:/certs/ca.pem", cfg.Home.TLS.CACert)
|
||||
}
|
||||
if !cfg.Home.TLS.InsecureSkipVerify {
|
||||
t.Fatal("Home.TLS.InsecureSkipVerify = false, want true")
|
||||
}
|
||||
}
|
||||
+74
-8
@@ -2,11 +2,14 @@ package home
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -151,20 +154,83 @@ func (c *Client) ensureClients() error {
|
||||
}
|
||||
|
||||
if c.cmd == nil {
|
||||
c.cmd = redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
Password: c.homeCfg.Password,
|
||||
})
|
||||
options, errOptions := c.redisOptionsLocked(addr)
|
||||
if errOptions != nil {
|
||||
return errOptions
|
||||
}
|
||||
c.cmd = redis.NewClient(options)
|
||||
}
|
||||
if c.sub == nil {
|
||||
c.sub = redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
Password: c.homeCfg.Password,
|
||||
})
|
||||
options, errOptions := c.redisOptionsLocked(addr)
|
||||
if errOptions != nil {
|
||||
return errOptions
|
||||
}
|
||||
c.sub = redis.NewClient(options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) redisOptionsLocked(addr string) (*redis.Options, error) {
|
||||
tlsConfig, errTLS := c.homeTLSConfigLocked()
|
||||
if errTLS != nil {
|
||||
return nil, errTLS
|
||||
}
|
||||
return &redis.Options{
|
||||
Addr: addr,
|
||||
Password: c.homeCfg.Password,
|
||||
TLSConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) homeTLSConfigLocked() (*tls.Config, error) {
|
||||
serverName := strings.TrimSpace(c.homeCfg.TLS.ServerName)
|
||||
if serverName == "" {
|
||||
serverName = strings.TrimSpace(c.seedHost)
|
||||
}
|
||||
if serverName == "" {
|
||||
serverName = strings.TrimSpace(c.homeCfg.Host)
|
||||
}
|
||||
return newHomeTLSConfig(c.homeCfg.TLS, serverName)
|
||||
}
|
||||
|
||||
func newHomeTLSConfig(cfg config.HomeTLSConfig, fallbackServerName string) (*tls.Config, error) {
|
||||
if !cfg.Enable {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
serverName := strings.TrimSpace(cfg.ServerName)
|
||||
if serverName == "" {
|
||||
serverName = strings.TrimSpace(fallbackServerName)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ServerName: serverName,
|
||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||
}
|
||||
|
||||
caCertPath := strings.TrimSpace(cfg.CACert)
|
||||
if caCertPath == "" {
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
caCertPEM, errRead := os.ReadFile(caCertPath)
|
||||
if errRead != nil {
|
||||
return nil, fmt.Errorf("home tls: read ca-cert: %w", errRead)
|
||||
}
|
||||
|
||||
certPool, errPool := x509.SystemCertPool()
|
||||
if errPool != nil || certPool == nil {
|
||||
certPool = x509.NewCertPool()
|
||||
}
|
||||
if !certPool.AppendCertsFromPEM(caCertPEM) {
|
||||
return nil, fmt.Errorf("home tls: ca-cert contains no PEM certificates")
|
||||
}
|
||||
tlsConfig.RootCAs = certPool
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func (c *Client) commandClient() (*redis.Client, error) {
|
||||
if errEnsure := c.ensureClients(); errEnsure != nil {
|
||||
return nil, errEnsure
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
)
|
||||
|
||||
func TestAuthDispatchRequestIncludesCount(t *testing.T) {
|
||||
@@ -30,3 +33,85 @@ func TestAuthDispatchRequestDefaultsCountToOne(t *testing.T) {
|
||||
t.Fatalf("count = %d, want 1", req.Count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisOptionsHomeTLSDisabled(t *testing.T) {
|
||||
client := New(config.HomeConfig{
|
||||
Enabled: true,
|
||||
Host: "127.0.0.1",
|
||||
Port: 6379,
|
||||
Password: "secret",
|
||||
})
|
||||
|
||||
client.mu.Lock()
|
||||
options, err := client.redisOptionsLocked("127.0.0.1:6379")
|
||||
client.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Fatalf("redisOptionsLocked() error = %v", err)
|
||||
}
|
||||
|
||||
if options.TLSConfig != nil {
|
||||
t.Fatalf("TLSConfig = %#v, want nil", options.TLSConfig)
|
||||
}
|
||||
if options.Password != "secret" {
|
||||
t.Fatalf("Password = %q, want secret", options.Password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisOptionsHomeTLSEnabledUsesSeedHostAsServerName(t *testing.T) {
|
||||
client := New(config.HomeConfig{
|
||||
Enabled: true,
|
||||
Host: "home.example.com",
|
||||
Port: 444,
|
||||
TLS: config.HomeTLSConfig{
|
||||
Enable: true,
|
||||
},
|
||||
})
|
||||
client.homeCfg.Host = "127.0.0.1"
|
||||
|
||||
client.mu.Lock()
|
||||
options, err := client.redisOptionsLocked("127.0.0.1:444")
|
||||
client.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Fatalf("redisOptionsLocked() error = %v", err)
|
||||
}
|
||||
|
||||
if options.TLSConfig == nil {
|
||||
t.Fatal("TLSConfig is nil")
|
||||
}
|
||||
if options.TLSConfig.ServerName != "home.example.com" {
|
||||
t.Fatalf("ServerName = %q, want home.example.com", options.TLSConfig.ServerName)
|
||||
}
|
||||
if options.TLSConfig.MinVersion != tls.VersionTLS12 {
|
||||
t.Fatalf("MinVersion = %d, want TLS 1.2", options.TLSConfig.MinVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisOptionsHomeTLSEnabledUsesExplicitServerName(t *testing.T) {
|
||||
client := New(config.HomeConfig{
|
||||
Enabled: true,
|
||||
Host: "127.0.0.1",
|
||||
Port: 444,
|
||||
TLS: config.HomeTLSConfig{
|
||||
Enable: true,
|
||||
ServerName: "home.example.com",
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
})
|
||||
|
||||
client.mu.Lock()
|
||||
options, err := client.redisOptionsLocked("127.0.0.1:444")
|
||||
client.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Fatalf("redisOptionsLocked() error = %v", err)
|
||||
}
|
||||
|
||||
if options.TLSConfig == nil {
|
||||
t.Fatal("TLSConfig is nil")
|
||||
}
|
||||
if options.TLSConfig.ServerName != "home.example.com" {
|
||||
t.Fatalf("ServerName = %q, want home.example.com", options.TLSConfig.ServerName)
|
||||
}
|
||||
if !options.TLSConfig.InsecureSkipVerify {
|
||||
t.Fatal("InsecureSkipVerify = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user