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  }