feat(home): implement home control plane integration with Redis and TLS support
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
)
|
||||
|
||||
func parseHomeFlagConfig(rawAddr string, password string) (config.HomeConfig, error) {
|
||||
rawAddr = strings.TrimSpace(rawAddr)
|
||||
if rawAddr == "" {
|
||||
return config.HomeConfig{}, fmt.Errorf("address is empty")
|
||||
}
|
||||
|
||||
if strings.Contains(rawAddr, "://") {
|
||||
return parseHomeURLConfig(rawAddr, password)
|
||||
}
|
||||
|
||||
host, portStr, errSplit := net.SplitHostPort(rawAddr)
|
||||
if errSplit != nil {
|
||||
return config.HomeConfig{}, fmt.Errorf("expected host:port, redis://host:port, or rediss://host:port: %w", errSplit)
|
||||
}
|
||||
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
return config.HomeConfig{}, fmt.Errorf("host is empty")
|
||||
}
|
||||
|
||||
port, errPort := parseHomePort(portStr)
|
||||
if errPort != nil {
|
||||
return config.HomeConfig{}, errPort
|
||||
}
|
||||
|
||||
return config.HomeConfig{
|
||||
Enabled: true,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Password: password,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseHomeURLConfig(rawAddr string, password string) (config.HomeConfig, error) {
|
||||
parsed, errParse := url.Parse(rawAddr)
|
||||
if errParse != nil {
|
||||
return config.HomeConfig{}, fmt.Errorf("parse URL: %w", errParse)
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
|
||||
if scheme != "redis" && scheme != "rediss" {
|
||||
return config.HomeConfig{}, fmt.Errorf("unsupported URL scheme %q", parsed.Scheme)
|
||||
}
|
||||
|
||||
host := strings.TrimSpace(parsed.Hostname())
|
||||
if host == "" {
|
||||
return config.HomeConfig{}, fmt.Errorf("host is empty")
|
||||
}
|
||||
|
||||
port, errPort := parseHomePort(parsed.Port())
|
||||
if errPort != nil {
|
||||
return config.HomeConfig{}, errPort
|
||||
}
|
||||
|
||||
if password == "" && parsed.User != nil {
|
||||
if urlPassword, ok := parsed.User.Password(); ok {
|
||||
password = urlPassword
|
||||
}
|
||||
}
|
||||
|
||||
homeCfg := config.HomeConfig{
|
||||
Enabled: true,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if scheme == "rediss" {
|
||||
homeCfg.TLS.Enable = true
|
||||
query := parsed.Query()
|
||||
homeCfg.TLS.ServerName = strings.TrimSpace(firstHomeQueryValue(query, "server-name", "server_name"))
|
||||
homeCfg.TLS.InsecureSkipVerify = parseHomeBoolQuery(query, "insecure-skip-verify", "insecure_skip_verify", "skip_verify")
|
||||
homeCfg.TLS.CACert = strings.TrimSpace(firstHomeQueryValue(query, "ca-cert", "ca_cert"))
|
||||
}
|
||||
|
||||
return homeCfg, nil
|
||||
}
|
||||
|
||||
func parseHomePort(rawPort string) (int, error) {
|
||||
rawPort = strings.TrimSpace(rawPort)
|
||||
if rawPort == "" {
|
||||
return 0, fmt.Errorf("port is empty")
|
||||
}
|
||||
|
||||
port, errPort := strconv.Atoi(rawPort)
|
||||
if errPort != nil || port <= 0 || port > 65535 {
|
||||
return 0, fmt.Errorf("invalid port %q", rawPort)
|
||||
}
|
||||
|
||||
return port, nil
|
||||
}
|
||||
|
||||
func firstHomeQueryValue(values url.Values, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := values.Get(key); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseHomeBoolQuery(values url.Values, keys ...string) bool {
|
||||
for _, key := range keys {
|
||||
value := strings.TrimSpace(values.Get(key))
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
parsed, errParse := strconv.ParseBool(value)
|
||||
return errParse == nil && parsed
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseHomeFlagConfigHostPort(t *testing.T) {
|
||||
cfg, err := parseHomeFlagConfig("home.example.com:8327", "secret")
|
||||
if err != nil {
|
||||
t.Fatalf("parseHomeFlagConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if !cfg.Enabled {
|
||||
t.Fatal("Enabled = false, want true")
|
||||
}
|
||||
if cfg.Host != "home.example.com" {
|
||||
t.Fatalf("Host = %q, want home.example.com", cfg.Host)
|
||||
}
|
||||
if cfg.Port != 8327 {
|
||||
t.Fatalf("Port = %d, want 8327", cfg.Port)
|
||||
}
|
||||
if cfg.Password != "secret" {
|
||||
t.Fatalf("Password = %q, want secret", cfg.Password)
|
||||
}
|
||||
if cfg.TLS.Enable {
|
||||
t.Fatal("TLS.Enable = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHomeFlagConfigRediss(t *testing.T) {
|
||||
cfg, err := parseHomeFlagConfig("rediss://:url-secret@home.example.com:444?server-name=home.example.com&skip_verify=true&ca-cert=C%3A%2Fcerts%2Fca.pem", "")
|
||||
if err != nil {
|
||||
t.Fatalf("parseHomeFlagConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Host != "home.example.com" {
|
||||
t.Fatalf("Host = %q, want home.example.com", cfg.Host)
|
||||
}
|
||||
if cfg.Port != 444 {
|
||||
t.Fatalf("Port = %d, want 444", cfg.Port)
|
||||
}
|
||||
if cfg.Password != "url-secret" {
|
||||
t.Fatalf("Password = %q, want url-secret", cfg.Password)
|
||||
}
|
||||
if !cfg.TLS.Enable {
|
||||
t.Fatal("TLS.Enable = false, want true")
|
||||
}
|
||||
if cfg.TLS.ServerName != "home.example.com" {
|
||||
t.Fatalf("TLS.ServerName = %q, want home.example.com", cfg.TLS.ServerName)
|
||||
}
|
||||
if !cfg.TLS.InsecureSkipVerify {
|
||||
t.Fatal("TLS.InsecureSkipVerify = false, want true")
|
||||
}
|
||||
if cfg.TLS.CACert != "C:/certs/ca.pem" {
|
||||
t.Fatalf("TLS.CACert = %q, want C:/certs/ca.pem", cfg.TLS.CACert)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHomeFlagConfigPasswordFlagOverridesURLPassword(t *testing.T) {
|
||||
cfg, err := parseHomeFlagConfig("rediss://:url-secret@home.example.com:444", "flag-secret")
|
||||
if err != nil {
|
||||
t.Fatalf("parseHomeFlagConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Password != "flag-secret" {
|
||||
t.Fatalf("Password = %q, want flag-secret", cfg.Password)
|
||||
}
|
||||
}
|
||||
+4
-23
@@ -10,11 +10,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -93,7 +91,7 @@ func main() {
|
||||
flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file")
|
||||
flag.StringVar(&vertexImportPrefix, "vertex-import-prefix", "", "Prefix for Vertex model namespacing (use with -vertex-import)")
|
||||
flag.StringVar(&password, "password", "", "")
|
||||
flag.StringVar(&homeAddr, "home", "", "Home control plane address in host:port format (loads config from home and skips local config file)")
|
||||
flag.StringVar(&homeAddr, "home", "", "Home control plane address in host:port, redis://host:port, or rediss://host:port format (loads config from home and skips local config file)")
|
||||
flag.StringVar(&homePassword, "home-password", "", "Home control plane password (Redis AUTH)")
|
||||
flag.BoolVar(&tuiMode, "tui", false, "Start with terminal management UI")
|
||||
flag.BoolVar(&standalone, "standalone", false, "In TUI mode, start an embedded local server")
|
||||
@@ -247,28 +245,11 @@ func main() {
|
||||
if strings.TrimSpace(homeAddr) != "" {
|
||||
configLoadedFromHome = true
|
||||
trimmedHomePassword := strings.TrimSpace(homePassword)
|
||||
host, portStr, errSplit := net.SplitHostPort(strings.TrimSpace(homeAddr))
|
||||
if errSplit != nil {
|
||||
log.Errorf("invalid -home address %q (expected host:port): %v", homeAddr, errSplit)
|
||||
homeCfg, errHomeCfg := parseHomeFlagConfig(homeAddr, trimmedHomePassword)
|
||||
if errHomeCfg != nil {
|
||||
log.Errorf("invalid -home address %q: %v", homeAddr, errHomeCfg)
|
||||
return
|
||||
}
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
log.Errorf("invalid -home address %q: host is empty", homeAddr)
|
||||
return
|
||||
}
|
||||
port, errPort := strconv.Atoi(strings.TrimSpace(portStr))
|
||||
if errPort != nil || port <= 0 {
|
||||
log.Errorf("invalid -home address %q: invalid port %q", homeAddr, portStr)
|
||||
return
|
||||
}
|
||||
|
||||
homeCfg := config.HomeConfig{
|
||||
Enabled: true,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Password: trimmedHomePassword,
|
||||
}
|
||||
homeClient := home.New(homeCfg)
|
||||
defer homeClient.Close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user