github.com/anycable/anycable-go@v1.5.1/rpc/config.go (about) 1 package rpc 2 3 import ( 4 "crypto/tls" 5 "crypto/x509" 6 "errors" 7 "fmt" 8 "log/slog" 9 "net/url" 10 "os" 11 "strings" 12 13 pb "github.com/anycable/anycable-go/protos" 14 ) 15 16 const ( 17 defaultRPCHost = "localhost:50051" 18 // Slightly less than default Ruby gRPC server concurrency 19 defaultRPCConcurrency = 28 20 ) 21 22 // ClientHelper provides additional methods to operate gRPC client 23 type ClientHelper interface { 24 Ready() error 25 SupportsActiveConns() bool 26 ActiveConns() int 27 Close() 28 } 29 30 // Dialer is factory function to build a new client with its helper 31 type Dialer = func(c *Config, l *slog.Logger) (pb.RPCClient, ClientHelper, error) 32 33 // Config contains RPC controller configuration 34 type Config struct { 35 // RPC instance host 36 Host string 37 // The max number of simultaneous requests. 38 // Should be slightly less than the RPC server concurrency to avoid 39 // ResourceExhausted errors 40 Concurrency int 41 // Enable client-side TLS on RPC connections? 42 EnableTLS bool 43 // Whether to verify the RPC server's certificate chain and host name 44 TLSVerify bool 45 // CA root TLS certificate path 46 TLSRootCA string 47 // Max receive msg size (bytes) 48 MaxRecvSize int 49 // Max send msg size (bytes) 50 MaxSendSize int 51 // Underlying implementation (grpc, http, or none) 52 Implementation string 53 // Alternative dialer implementation 54 DialFun Dialer 55 // Secret for HTTP RPC authentication 56 Secret string 57 // Timeout for HTTP RPC requests (in ms) 58 RequestTimeout int 59 // SecretBase is a secret used to generate authentication token 60 SecretBase string 61 } 62 63 // NewConfig builds a new config 64 func NewConfig() Config { 65 return Config{ 66 Concurrency: defaultRPCConcurrency, 67 EnableTLS: false, 68 TLSVerify: true, 69 Host: defaultRPCHost, 70 Implementation: "", 71 RequestTimeout: 3000, 72 } 73 } 74 75 // Return chosen implementation either from the user provided value 76 // or from the host scheme 77 func (c *Config) Impl() string { 78 if c.Implementation != "" { 79 return c.Implementation 80 } 81 82 uri, err := url.Parse(ensureGrpcScheme(c.Host)) 83 84 if err != nil { 85 return fmt.Sprintf("<invalid RPC host: %s>", c.Host) 86 } 87 88 if uri.Scheme == "http" || uri.Scheme == "https" { 89 return "http" 90 } 91 92 return "grpc" 93 } 94 95 // Whether secure connection to RPC server is enabled either explicitly or implicitly 96 func (c *Config) TLSEnabled() bool { 97 return c.EnableTLS || c.TLSRootCA != "" 98 } 99 100 // TLSConfig builds TLS configuration for RPC client 101 func (c *Config) TLSConfig() (*tls.Config, error) { 102 if !c.TLSEnabled() { 103 return nil, nil 104 } 105 106 var certPool *x509.CertPool = nil // use system CA certificates 107 if c.TLSRootCA != "" { 108 var rootCertificate []byte 109 var error error 110 if info, err := os.Stat(c.TLSRootCA); !os.IsNotExist(err) && !info.IsDir() { 111 rootCertificate, error = os.ReadFile(c.TLSRootCA) 112 if error != nil { 113 return nil, fmt.Errorf("failed to read RPC root CA certificate: %s", error) 114 } 115 } else { 116 rootCertificate = []byte(c.TLSRootCA) 117 } 118 119 certPool = x509.NewCertPool() 120 ok := certPool.AppendCertsFromPEM(rootCertificate) 121 if !ok { 122 return nil, errors.New("failed to parse RPC root CA certificate") 123 } 124 } 125 126 // #nosec G402: InsecureSkipVerify explicitly allowed to be set to true for development/testing 127 tlsConfig := &tls.Config{ 128 InsecureSkipVerify: !c.TLSVerify, 129 MinVersion: tls.VersionTLS12, 130 RootCAs: certPool, 131 } 132 133 return tlsConfig, nil 134 } 135 136 func ensureGrpcScheme(url string) string { 137 if strings.Contains(url, "://") { 138 return url 139 } 140 141 return "grpc://" + url 142 }