github.com/argoproj/argo-cd/v3@v3.2.1/util/grpc/grpc.go (about)

     1  package grpc
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"net"
     8  	"runtime/debug"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
    13  	"github.com/sirupsen/logrus"
    14  	"golang.org/x/net/proxy"
    15  	"google.golang.org/grpc"
    16  	"google.golang.org/grpc/codes"
    17  	"google.golang.org/grpc/connectivity"
    18  	"google.golang.org/grpc/credentials"
    19  	"google.golang.org/grpc/credentials/insecure"
    20  	"google.golang.org/grpc/keepalive"
    21  	"google.golang.org/grpc/status"
    22  
    23  	"github.com/argoproj/argo-cd/v3/common"
    24  )
    25  
    26  // LoggerRecoveryHandler return a handler for recovering from panics and returning error
    27  func LoggerRecoveryHandler(log *logrus.Entry) recovery.RecoveryHandlerFunc {
    28  	return func(p any) (err error) {
    29  		log.Errorf("Recovered from panic: %+v\n%s", p, debug.Stack())
    30  		return status.Errorf(codes.Internal, "%s", p)
    31  	}
    32  }
    33  
    34  // BlockingNewClient is a helper method to dial the given address, using optional TLS credentials,
    35  // and blocking until the returned connection is ready. If the given credentials are nil, the
    36  // connection will be insecure (plain-text).
    37  // Lifted from: https://github.com/fullstorydev/grpcurl/blob/master/grpcurl.go
    38  func BlockingNewClient(ctx context.Context, network, address string, creds credentials.TransportCredentials, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
    39  	proxyDialer := proxy.FromEnvironment()
    40  	rawConn, err := proxyDialer.Dial(network, address)
    41  	if err != nil {
    42  		return nil, fmt.Errorf("error dial proxy: %w", err)
    43  	}
    44  
    45  	if creds != nil {
    46  		rawConn, _, err = creds.ClientHandshake(ctx, address, rawConn)
    47  		if err != nil {
    48  			return nil, fmt.Errorf("error creating connection: %w", err)
    49  		}
    50  	}
    51  	customDialer := func(_ context.Context, _ string) (net.Conn, error) {
    52  		return rawConn, nil
    53  	}
    54  
    55  	opts = append(opts,
    56  		grpc.WithContextDialer(customDialer),
    57  		grpc.WithTransportCredentials(insecure.NewCredentials()),
    58  		grpc.WithKeepaliveParams(keepalive.ClientParameters{Time: common.GetGRPCKeepAliveTime()}),
    59  	)
    60  
    61  	conn, err := grpc.NewClient("passthrough:"+address, opts...)
    62  	if err != nil {
    63  		return nil, fmt.Errorf("grpc.NewClient failed: %w", err)
    64  	}
    65  
    66  	conn.Connect()
    67  	if err := waitForReady(ctx, conn); err != nil {
    68  		return nil, fmt.Errorf("gRPC connection not ready: %w", err)
    69  	}
    70  
    71  	return conn, nil
    72  }
    73  
    74  func waitForReady(ctx context.Context, conn *grpc.ClientConn) error {
    75  	for {
    76  		state := conn.GetState()
    77  		if state == connectivity.Ready {
    78  			return nil
    79  		}
    80  		if !conn.WaitForStateChange(ctx, state) {
    81  			return ctx.Err() // context timeout or cancellation
    82  		}
    83  	}
    84  }
    85  
    86  type TLSTestResult struct {
    87  	TLS         bool
    88  	InsecureErr error
    89  }
    90  
    91  func TestTLS(address string, dialTime time.Duration) (*TLSTestResult, error) {
    92  	if parts := strings.Split(address, ":"); len(parts) == 1 {
    93  		// If port is unspecified, assume the most likely port
    94  		address += ":443"
    95  	}
    96  	var testResult TLSTestResult
    97  	var tlsConfig tls.Config
    98  	tlsConfig.InsecureSkipVerify = true
    99  	creds := credentials.NewTLS(&tlsConfig)
   100  
   101  	// Set timeout when dialing to the server
   102  	// fix: https://github.com/argoproj/argo-cd/issues/9679
   103  	ctx, cancel := context.WithTimeout(context.Background(), dialTime)
   104  	defer cancel()
   105  
   106  	conn, err := BlockingNewClient(ctx, "tcp", address, creds)
   107  	if err == nil {
   108  		_ = conn.Close()
   109  		testResult.TLS = true
   110  		creds := credentials.NewTLS(&tls.Config{})
   111  		ctx, cancel := context.WithTimeout(context.Background(), dialTime)
   112  		defer cancel()
   113  
   114  		conn, err := BlockingNewClient(ctx, "tcp", address, creds)
   115  		if err == nil {
   116  			_ = conn.Close()
   117  		} else {
   118  			// if connection was successful with InsecureSkipVerify true, but unsuccessful with
   119  			// InsecureSkipVerify false, it means server is not configured securely
   120  			testResult.InsecureErr = err
   121  		}
   122  		return &testResult, nil
   123  	}
   124  	// If we get here, we were unable to connect via TLS (even with InsecureSkipVerify: true)
   125  	// It may be because server is running without TLS, or because of real issues (e.g. connection
   126  	// refused). Test if server accepts plain-text connections
   127  	ctx, cancel = context.WithTimeout(context.Background(), dialTime)
   128  	defer cancel()
   129  	conn, err = BlockingNewClient(ctx, "tcp", address, nil)
   130  	if err == nil {
   131  		_ = conn.Close()
   132  		testResult.TLS = false
   133  		return &testResult, nil
   134  	}
   135  	return nil, err
   136  }