github.com/argoproj/argo-cd@v1.8.7/server/server.go (about)

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"math"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"os/exec"
    14  	"path"
    15  	"regexp"
    16  	"strings"
    17  	"time"
    18  
    19  	// nolint:staticcheck
    20  	golang_proto "github.com/golang/protobuf/proto"
    21  
    22  	"github.com/argoproj/pkg/jwt/zjwt"
    23  	"github.com/argoproj/pkg/sync"
    24  	"github.com/dgrijalva/jwt-go/v4"
    25  	"github.com/go-redis/redis/v8"
    26  	"github.com/gorilla/handlers"
    27  	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
    28  	grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
    29  	grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
    30  	grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
    31  	"github.com/grpc-ecosystem/grpc-gateway/runtime"
    32  	"github.com/improbable-eng/grpc-web/go/grpcweb"
    33  	log "github.com/sirupsen/logrus"
    34  	"github.com/soheilhy/cmux"
    35  	netCtx "golang.org/x/net/context"
    36  	"google.golang.org/grpc"
    37  	"google.golang.org/grpc/codes"
    38  	"google.golang.org/grpc/credentials"
    39  	"google.golang.org/grpc/metadata"
    40  	"google.golang.org/grpc/reflection"
    41  	"google.golang.org/grpc/status"
    42  	"gopkg.in/yaml.v2"
    43  	v1 "k8s.io/api/core/v1"
    44  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    45  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    46  	"k8s.io/apimachinery/pkg/util/wait"
    47  	"k8s.io/client-go/kubernetes"
    48  	"k8s.io/client-go/tools/cache"
    49  
    50  	"github.com/argoproj/argo-cd/common"
    51  	"github.com/argoproj/argo-cd/pkg/apiclient"
    52  	accountpkg "github.com/argoproj/argo-cd/pkg/apiclient/account"
    53  	applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
    54  	certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate"
    55  	clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster"
    56  	gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
    57  	projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
    58  	repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
    59  	repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
    60  	sessionpkg "github.com/argoproj/argo-cd/pkg/apiclient/session"
    61  	settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
    62  	versionpkg "github.com/argoproj/argo-cd/pkg/apiclient/version"
    63  	"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    64  	appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
    65  	appinformer "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
    66  	applisters "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
    67  	repoapiclient "github.com/argoproj/argo-cd/reposerver/apiclient"
    68  	repocache "github.com/argoproj/argo-cd/reposerver/cache"
    69  	"github.com/argoproj/argo-cd/server/account"
    70  	"github.com/argoproj/argo-cd/server/application"
    71  	"github.com/argoproj/argo-cd/server/badge"
    72  	servercache "github.com/argoproj/argo-cd/server/cache"
    73  	"github.com/argoproj/argo-cd/server/certificate"
    74  	"github.com/argoproj/argo-cd/server/cluster"
    75  	"github.com/argoproj/argo-cd/server/gpgkey"
    76  	"github.com/argoproj/argo-cd/server/logout"
    77  	"github.com/argoproj/argo-cd/server/metrics"
    78  	"github.com/argoproj/argo-cd/server/project"
    79  	"github.com/argoproj/argo-cd/server/rbacpolicy"
    80  	"github.com/argoproj/argo-cd/server/repocreds"
    81  	"github.com/argoproj/argo-cd/server/repository"
    82  	"github.com/argoproj/argo-cd/server/session"
    83  	"github.com/argoproj/argo-cd/server/settings"
    84  	"github.com/argoproj/argo-cd/server/version"
    85  	"github.com/argoproj/argo-cd/util/assets"
    86  	cacheutil "github.com/argoproj/argo-cd/util/cache"
    87  	"github.com/argoproj/argo-cd/util/db"
    88  	"github.com/argoproj/argo-cd/util/dex"
    89  	dexutil "github.com/argoproj/argo-cd/util/dex"
    90  	"github.com/argoproj/argo-cd/util/env"
    91  	"github.com/argoproj/argo-cd/util/errors"
    92  	grpc_util "github.com/argoproj/argo-cd/util/grpc"
    93  	"github.com/argoproj/argo-cd/util/healthz"
    94  	httputil "github.com/argoproj/argo-cd/util/http"
    95  	"github.com/argoproj/argo-cd/util/io"
    96  	kubeutil "github.com/argoproj/argo-cd/util/kube"
    97  	"github.com/argoproj/argo-cd/util/oidc"
    98  	"github.com/argoproj/argo-cd/util/rbac"
    99  	util_session "github.com/argoproj/argo-cd/util/session"
   100  	settings_util "github.com/argoproj/argo-cd/util/settings"
   101  	"github.com/argoproj/argo-cd/util/swagger"
   102  	tlsutil "github.com/argoproj/argo-cd/util/tls"
   103  	"github.com/argoproj/argo-cd/util/webhook"
   104  )
   105  
   106  const maxConcurrentLoginRequestsCountEnv = "ARGOCD_MAX_CONCURRENT_LOGIN_REQUESTS_COUNT"
   107  const replicasCountEnv = "ARGOCD_API_SERVER_REPLICAS"
   108  
   109  // ErrNoSession indicates no auth token was supplied as part of a request
   110  var ErrNoSession = status.Errorf(codes.Unauthenticated, "no session information")
   111  
   112  var noCacheHeaders = map[string]string{
   113  	"Expires":         time.Unix(0, 0).Format(time.RFC1123),
   114  	"Cache-Control":   "no-cache, private, max-age=0",
   115  	"Pragma":          "no-cache",
   116  	"X-Accel-Expires": "0",
   117  }
   118  
   119  var backoff = wait.Backoff{
   120  	Steps:    5,
   121  	Duration: 500 * time.Millisecond,
   122  	Factor:   1.0,
   123  	Jitter:   0.1,
   124  }
   125  
   126  var (
   127  	clientConstraint = fmt.Sprintf(">= %s", common.MinClientVersion)
   128  	baseHRefRegex    = regexp.MustCompile(`<base href="(.*)">`)
   129  	// limits number of concurrent login requests to prevent password brute forcing. If set to 0 then no limit is enforced.
   130  	maxConcurrentLoginRequestsCount = 50
   131  	replicasCount                   = 1
   132  	enableGRPCTimeHistogram         = true
   133  )
   134  
   135  func init() {
   136  	maxConcurrentLoginRequestsCount = env.ParseNumFromEnv(maxConcurrentLoginRequestsCountEnv, maxConcurrentLoginRequestsCount, 0, math.MaxInt32)
   137  	replicasCount = env.ParseNumFromEnv(replicasCountEnv, replicasCount, 0, math.MaxInt32)
   138  	if replicasCount > 0 {
   139  		maxConcurrentLoginRequestsCount = maxConcurrentLoginRequestsCount / replicasCount
   140  	}
   141  	enableGRPCTimeHistogram = os.Getenv(common.EnvEnableGRPCTimeHistogramEnv) == "true"
   142  }
   143  
   144  // ArgoCDServer is the API server for Argo CD
   145  type ArgoCDServer struct {
   146  	ArgoCDServerOpts
   147  
   148  	ssoClientApp   *oidc.ClientApp
   149  	settings       *settings_util.ArgoCDSettings
   150  	log            *log.Entry
   151  	sessionMgr     *util_session.SessionManager
   152  	settingsMgr    *settings_util.SettingsManager
   153  	enf            *rbac.Enforcer
   154  	projInformer   cache.SharedIndexInformer
   155  	policyEnforcer *rbacpolicy.RBACPolicyEnforcer
   156  	appInformer    cache.SharedIndexInformer
   157  	appLister      applisters.ApplicationNamespaceLister
   158  
   159  	// stopCh is the channel which when closed, will shutdown the Argo CD server
   160  	stopCh chan struct{}
   161  }
   162  
   163  type ArgoCDServerOpts struct {
   164  	DisableAuth         bool
   165  	EnableGZip          bool
   166  	Insecure            bool
   167  	ListenPort          int
   168  	MetricsPort         int
   169  	Namespace           string
   170  	DexServerAddr       string
   171  	StaticAssetsDir     string
   172  	BaseHRef            string
   173  	RootPath            string
   174  	KubeClientset       kubernetes.Interface
   175  	AppClientset        appclientset.Interface
   176  	RepoClientset       repoapiclient.Clientset
   177  	Cache               *servercache.Cache
   178  	RedisClient         *redis.Client
   179  	TLSConfigCustomizer tlsutil.ConfigCustomizer
   180  	XFrameOptions       string
   181  }
   182  
   183  // initializeDefaultProject creates the default project if it does not already exist
   184  func initializeDefaultProject(opts ArgoCDServerOpts) error {
   185  	defaultProj := &v1alpha1.AppProject{
   186  		ObjectMeta: metav1.ObjectMeta{Name: common.DefaultAppProjectName, Namespace: opts.Namespace},
   187  		Spec: v1alpha1.AppProjectSpec{
   188  			SourceRepos:              []string{"*"},
   189  			Destinations:             []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}},
   190  			ClusterResourceWhitelist: []metav1.GroupKind{{Group: "*", Kind: "*"}},
   191  		},
   192  	}
   193  
   194  	_, err := opts.AppClientset.ArgoprojV1alpha1().AppProjects(opts.Namespace).Get(context.Background(), defaultProj.Name, metav1.GetOptions{})
   195  	if apierrors.IsNotFound(err) {
   196  		_, err = opts.AppClientset.ArgoprojV1alpha1().AppProjects(opts.Namespace).Create(context.Background(), defaultProj, metav1.CreateOptions{})
   197  		if apierrors.IsAlreadyExists(err) {
   198  			return nil
   199  		}
   200  	}
   201  	return err
   202  }
   203  
   204  // NewServer returns a new instance of the Argo CD API server
   205  func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer {
   206  	settingsMgr := settings_util.NewSettingsManager(ctx, opts.KubeClientset, opts.Namespace)
   207  	settings, err := settingsMgr.InitializeSettings(opts.Insecure)
   208  	errors.CheckError(err)
   209  	err = initializeDefaultProject(opts)
   210  	errors.CheckError(err)
   211  
   212  	factory := appinformer.NewFilteredSharedInformerFactory(opts.AppClientset, 0, opts.Namespace, func(options *metav1.ListOptions) {})
   213  	projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer()
   214  	projLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(opts.Namespace)
   215  
   216  	appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
   217  	appLister := factory.Argoproj().V1alpha1().Applications().Lister().Applications(opts.Namespace)
   218  
   219  	sessionMgr := util_session.NewSessionManager(settingsMgr, projLister, opts.DexServerAddr, opts.Cache)
   220  	enf := rbac.NewEnforcer(opts.KubeClientset, opts.Namespace, common.ArgoCDRBACConfigMapName, nil)
   221  	enf.EnableEnforce(!opts.DisableAuth)
   222  	err = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
   223  	errors.CheckError(err)
   224  	enf.EnableLog(os.Getenv(common.EnvVarRBACDebug) == "1")
   225  
   226  	policyEnf := rbacpolicy.NewRBACPolicyEnforcer(enf, projLister)
   227  	enf.SetClaimsEnforcerFunc(policyEnf.EnforceClaims)
   228  
   229  	return &ArgoCDServer{
   230  		ArgoCDServerOpts: opts,
   231  		log:              log.NewEntry(log.StandardLogger()),
   232  		settings:         settings,
   233  		sessionMgr:       sessionMgr,
   234  		settingsMgr:      settingsMgr,
   235  		enf:              enf,
   236  		projInformer:     projInformer,
   237  		appInformer:      appInformer,
   238  		appLister:        appLister,
   239  		policyEnforcer:   policyEnf,
   240  	}
   241  }
   242  
   243  const (
   244  	// catches corrupted informer state; see https://github.com/argoproj/argo-cd/issues/4960 for more information
   245  	notObjectErrMsg = "object does not implement the Object interfaces"
   246  )
   247  
   248  func (a *ArgoCDServer) healthCheck(r *http.Request) error {
   249  	if val, ok := r.URL.Query()["full"]; ok && len(val) > 0 && val[0] == "true" {
   250  		argoDB := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset)
   251  		_, err := argoDB.ListClusters(r.Context())
   252  		if err != nil && strings.Contains(err.Error(), notObjectErrMsg) {
   253  			return err
   254  		}
   255  	}
   256  	return nil
   257  }
   258  
   259  // Run runs the API Server
   260  // We use k8s.io/code-generator/cmd/go-to-protobuf to generate the .proto files from the API types.
   261  // k8s.io/ go-to-protobuf uses protoc-gen-gogo, which comes from gogo/protobuf (a fork of
   262  // golang/protobuf).
   263  func (a *ArgoCDServer) Run(ctx context.Context, port int, metricsPort int) {
   264  	grpcS := a.newGRPCServer()
   265  	grpcWebS := grpcweb.WrapServer(grpcS)
   266  	var httpS *http.Server
   267  	var httpsS *http.Server
   268  	if a.useTLS() {
   269  		httpS = newRedirectServer(port, a.RootPath)
   270  		httpsS = a.newHTTPServer(ctx, port, grpcWebS)
   271  	} else {
   272  		httpS = a.newHTTPServer(ctx, port, grpcWebS)
   273  	}
   274  	if a.RootPath != "" {
   275  		httpS.Handler = withRootPath(httpS.Handler, a)
   276  
   277  		if httpsS != nil {
   278  			httpsS.Handler = withRootPath(httpsS.Handler, a)
   279  		}
   280  	}
   281  	httpS.Handler = &bug21955Workaround{handler: httpS.Handler}
   282  	if httpsS != nil {
   283  		httpsS.Handler = &bug21955Workaround{handler: httpsS.Handler}
   284  	}
   285  
   286  	metricsServ := metrics.NewMetricsServer(metricsPort)
   287  	if a.RedisClient != nil {
   288  		cacheutil.CollectMetrics(a.RedisClient, metricsServ)
   289  	}
   290  
   291  	// Start listener
   292  	var conn net.Listener
   293  	var realErr error
   294  	_ = wait.ExponentialBackoff(backoff, func() (bool, error) {
   295  		conn, realErr = net.Listen("tcp", fmt.Sprintf(":%d", port))
   296  		if realErr != nil {
   297  			a.log.Warnf("failed listen: %v", realErr)
   298  			return false, nil
   299  		}
   300  		return true, nil
   301  	})
   302  	errors.CheckError(realErr)
   303  
   304  	// Cmux is used to support servicing gRPC and HTTP1.1+JSON on the same port
   305  	tcpm := cmux.New(conn)
   306  	var tlsm cmux.CMux
   307  	var grpcL net.Listener
   308  	var httpL net.Listener
   309  	var httpsL net.Listener
   310  	if !a.useTLS() {
   311  		httpL = tcpm.Match(cmux.HTTP1Fast())
   312  		grpcL = tcpm.Match(cmux.HTTP2HeaderField("content-type", "application/grpc"))
   313  	} else {
   314  		// We first match on HTTP 1.1 methods.
   315  		httpL = tcpm.Match(cmux.HTTP1Fast())
   316  
   317  		// If not matched, we assume that its TLS.
   318  		tlsl := tcpm.Match(cmux.Any())
   319  		tlsConfig := tls.Config{
   320  			Certificates: []tls.Certificate{*a.settings.Certificate},
   321  		}
   322  		if a.TLSConfigCustomizer != nil {
   323  			a.TLSConfigCustomizer(&tlsConfig)
   324  		}
   325  		tlsl = tls.NewListener(tlsl, &tlsConfig)
   326  
   327  		// Now, we build another mux recursively to match HTTPS and gRPC.
   328  		tlsm = cmux.New(tlsl)
   329  		httpsL = tlsm.Match(cmux.HTTP1Fast())
   330  		grpcL = tlsm.Match(cmux.Any())
   331  	}
   332  
   333  	// Start the muxed listeners for our servers
   334  	log.Infof("argocd %s serving on port %d (url: %s, tls: %v, namespace: %s, sso: %v)",
   335  		common.GetVersion(), port, a.settings.URL, a.useTLS(), a.Namespace, a.settings.IsSSOConfigured())
   336  
   337  	go a.projInformer.Run(ctx.Done())
   338  	go a.appInformer.Run(ctx.Done())
   339  	go func() { a.checkServeErr("grpcS", grpcS.Serve(grpcL)) }()
   340  	go func() { a.checkServeErr("httpS", httpS.Serve(httpL)) }()
   341  	if a.useTLS() {
   342  		go func() { a.checkServeErr("httpsS", httpsS.Serve(httpsL)) }()
   343  		go func() { a.checkServeErr("tlsm", tlsm.Serve()) }()
   344  	}
   345  	go a.watchSettings()
   346  	go a.rbacPolicyLoader(ctx)
   347  	go func() { a.checkServeErr("tcpm", tcpm.Serve()) }()
   348  	go func() { a.checkServeErr("metrics", metricsServ.ListenAndServe()) }()
   349  	if !cache.WaitForCacheSync(ctx.Done(), a.projInformer.HasSynced, a.appInformer.HasSynced) {
   350  		log.Fatal("Timed out waiting for project cache to sync")
   351  	}
   352  
   353  	a.stopCh = make(chan struct{})
   354  	<-a.stopCh
   355  	errors.CheckError(conn.Close())
   356  }
   357  
   358  // checkServeErr checks the error from a .Serve() call to decide if it was a graceful shutdown
   359  func (a *ArgoCDServer) checkServeErr(name string, err error) {
   360  	if err != nil {
   361  		if a.stopCh == nil {
   362  			// a nil stopCh indicates a graceful shutdown
   363  			log.Infof("graceful shutdown %s: %v", name, err)
   364  		} else {
   365  			log.Fatalf("%s: %v", name, err)
   366  		}
   367  	} else {
   368  		log.Infof("graceful shutdown %s", name)
   369  	}
   370  }
   371  
   372  // Shutdown stops the Argo CD server
   373  func (a *ArgoCDServer) Shutdown() {
   374  	log.Info("Shut down requested")
   375  	stopCh := a.stopCh
   376  	a.stopCh = nil
   377  	if stopCh != nil {
   378  		close(stopCh)
   379  	}
   380  }
   381  
   382  // watchSettings watches the configmap and secret for any setting updates that would warrant a
   383  // restart of the API server.
   384  func (a *ArgoCDServer) watchSettings() {
   385  	updateCh := make(chan *settings_util.ArgoCDSettings, 1)
   386  	a.settingsMgr.Subscribe(updateCh)
   387  
   388  	prevURL := a.settings.URL
   389  	prevOIDCConfig := a.settings.OIDCConfigRAW
   390  	prevDexCfgBytes, err := dex.GenerateDexConfigYAML(a.settings)
   391  	errors.CheckError(err)
   392  	prevGitHubSecret := a.settings.WebhookGitHubSecret
   393  	prevGitLabSecret := a.settings.WebhookGitLabSecret
   394  	prevBitbucketUUID := a.settings.WebhookBitbucketUUID
   395  	prevBitbucketServerSecret := a.settings.WebhookBitbucketServerSecret
   396  	prevGogsSecret := a.settings.WebhookGogsSecret
   397  	var prevCert, prevCertKey string
   398  	if a.settings.Certificate != nil && !a.ArgoCDServerOpts.Insecure {
   399  		prevCert, prevCertKey = tlsutil.EncodeX509KeyPairString(*a.settings.Certificate)
   400  	}
   401  
   402  	for {
   403  		newSettings := <-updateCh
   404  		a.settings = newSettings
   405  		newDexCfgBytes, err := dex.GenerateDexConfigYAML(a.settings)
   406  		errors.CheckError(err)
   407  		if string(newDexCfgBytes) != string(prevDexCfgBytes) {
   408  			log.Infof("dex config modified. restarting")
   409  			break
   410  		}
   411  		if prevOIDCConfig != a.settings.OIDCConfigRAW {
   412  			log.Infof("odic config modified. restarting")
   413  			break
   414  		}
   415  		if prevURL != a.settings.URL {
   416  			log.Infof("url modified. restarting")
   417  			break
   418  		}
   419  		if prevGitHubSecret != a.settings.WebhookGitHubSecret {
   420  			log.Infof("github secret modified. restarting")
   421  			break
   422  		}
   423  		if prevGitLabSecret != a.settings.WebhookGitLabSecret {
   424  			log.Infof("gitlab secret modified. restarting")
   425  			break
   426  		}
   427  		if prevBitbucketUUID != a.settings.WebhookBitbucketUUID {
   428  			log.Infof("bitbucket uuid modified. restarting")
   429  			break
   430  		}
   431  		if prevBitbucketServerSecret != a.settings.WebhookBitbucketServerSecret {
   432  			log.Infof("bitbucket server secret modified. restarting")
   433  			break
   434  		}
   435  		if prevGogsSecret != a.settings.WebhookGogsSecret {
   436  			log.Infof("gogs secret modified. restarting")
   437  			break
   438  		}
   439  		if !a.ArgoCDServerOpts.Insecure {
   440  			var newCert, newCertKey string
   441  			if a.settings.Certificate != nil {
   442  				newCert, newCertKey = tlsutil.EncodeX509KeyPairString(*a.settings.Certificate)
   443  			}
   444  			if newCert != prevCert || newCertKey != prevCertKey {
   445  				log.Infof("tls certificate modified. restarting")
   446  				break
   447  			}
   448  		}
   449  	}
   450  	log.Info("shutting down settings watch")
   451  	a.Shutdown()
   452  	a.settingsMgr.Unsubscribe(updateCh)
   453  	close(updateCh)
   454  }
   455  
   456  func (a *ArgoCDServer) rbacPolicyLoader(ctx context.Context) {
   457  	err := a.enf.RunPolicyLoader(ctx, func(cm *v1.ConfigMap) error {
   458  		var scopes []string
   459  		if scopesStr, ok := cm.Data[rbac.ConfigMapScopesKey]; len(scopesStr) > 0 && ok {
   460  			scopes = make([]string, 0)
   461  			err := yaml.Unmarshal([]byte(scopesStr), &scopes)
   462  			if err != nil {
   463  				return err
   464  			}
   465  		}
   466  
   467  		a.policyEnforcer.SetScopes(scopes)
   468  		return nil
   469  	})
   470  	errors.CheckError(err)
   471  }
   472  
   473  func (a *ArgoCDServer) useTLS() bool {
   474  	if a.Insecure || a.settings.Certificate == nil {
   475  		return false
   476  	}
   477  	return true
   478  }
   479  
   480  func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
   481  	if enableGRPCTimeHistogram {
   482  		grpc_prometheus.EnableHandlingTimeHistogram()
   483  	}
   484  
   485  	sOpts := []grpc.ServerOption{
   486  		// Set the both send and receive the bytes limit to be 100MB
   487  		// The proper way to achieve high performance is to have pagination
   488  		// while we work toward that, we can have high limit first
   489  		grpc.MaxRecvMsgSize(apiclient.MaxGRPCMessageSize),
   490  		grpc.MaxSendMsgSize(apiclient.MaxGRPCMessageSize),
   491  		grpc.ConnectionTimeout(300 * time.Second),
   492  	}
   493  	sensitiveMethods := map[string]bool{
   494  		"/cluster.ClusterService/Create":                          true,
   495  		"/cluster.ClusterService/Update":                          true,
   496  		"/session.SessionService/Create":                          true,
   497  		"/account.AccountService/UpdatePassword":                  true,
   498  		"/gpgkey.GPGKeyService/CreateGnuPGPublicKey":              true,
   499  		"/repository.RepositoryService/Create":                    true,
   500  		"/repository.RepositoryService/Update":                    true,
   501  		"/repository.RepositoryService/CreateRepository":          true,
   502  		"/repository.RepositoryService/UpdateRepository":          true,
   503  		"/repository.RepositoryService/ValidateAccess":            true,
   504  		"/repocreds.RepoCredsService/CreateRepositoryCredentials": true,
   505  		"/repocreds.RepoCredsService/UpdateRepositoryCredentials": true,
   506  		"/application.ApplicationService/PatchResource":           true,
   507  	}
   508  	// NOTE: notice we do not configure the gRPC server here with TLS (e.g. grpc.Creds(creds))
   509  	// This is because TLS handshaking occurs in cmux handling
   510  	sOpts = append(sOpts, grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
   511  		grpc_logrus.StreamServerInterceptor(a.log),
   512  		grpc_prometheus.StreamServerInterceptor,
   513  		grpc_auth.StreamServerInterceptor(a.Authenticate),
   514  		grpc_util.UserAgentStreamServerInterceptor(common.ArgoCDUserAgentName, clientConstraint),
   515  		grpc_util.PayloadStreamServerInterceptor(a.log, true, func(ctx netCtx.Context, fullMethodName string, servingObject interface{}) bool {
   516  			return !sensitiveMethods[fullMethodName]
   517  		}),
   518  		grpc_util.ErrorCodeStreamServerInterceptor(),
   519  		grpc_util.PanicLoggerStreamServerInterceptor(a.log),
   520  	)))
   521  	sOpts = append(sOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
   522  		bug21955WorkaroundInterceptor,
   523  		grpc_logrus.UnaryServerInterceptor(a.log),
   524  		grpc_prometheus.UnaryServerInterceptor,
   525  		grpc_auth.UnaryServerInterceptor(a.Authenticate),
   526  		grpc_util.UserAgentUnaryServerInterceptor(common.ArgoCDUserAgentName, clientConstraint),
   527  		grpc_util.PayloadUnaryServerInterceptor(a.log, true, func(ctx netCtx.Context, fullMethodName string, servingObject interface{}) bool {
   528  			return !sensitiveMethods[fullMethodName]
   529  		}),
   530  		grpc_util.ErrorCodeUnaryServerInterceptor(),
   531  		grpc_util.PanicLoggerUnaryServerInterceptor(a.log),
   532  	)))
   533  	grpcS := grpc.NewServer(sOpts...)
   534  	db := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset)
   535  	kubectl := kubeutil.NewKubectl()
   536  	clusterService := cluster.NewServer(db, a.enf, a.Cache, kubectl)
   537  	repoService := repository.NewServer(a.RepoClientset, db, a.enf, a.Cache, a.settingsMgr)
   538  	repoCredsService := repocreds.NewServer(a.RepoClientset, db, a.enf, a.settingsMgr)
   539  	var loginRateLimiter func() (io.Closer, error)
   540  	if maxConcurrentLoginRequestsCount > 0 {
   541  		loginRateLimiter = session.NewLoginRateLimiter(maxConcurrentLoginRequestsCount)
   542  	}
   543  	sessionService := session.NewServer(a.sessionMgr, a, a.policyEnforcer, loginRateLimiter)
   544  	projectLock := sync.NewKeyLock()
   545  	applicationService := application.NewServer(
   546  		a.Namespace,
   547  		a.KubeClientset,
   548  		a.AppClientset,
   549  		a.appLister,
   550  		a.appInformer,
   551  		a.RepoClientset,
   552  		a.Cache,
   553  		kubectl,
   554  		db,
   555  		a.enf,
   556  		projectLock,
   557  		a.settingsMgr,
   558  		a.projInformer)
   559  	projectService := project.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.enf, projectLock, a.sessionMgr, a.policyEnforcer, a.projInformer, a.settingsMgr)
   560  	settingsService := settings.NewServer(a.settingsMgr, a, a.DisableAuth)
   561  	accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf)
   562  	certificateService := certificate.NewServer(a.RepoClientset, db, a.enf)
   563  	gpgkeyService := gpgkey.NewServer(a.RepoClientset, db, a.enf)
   564  	versionpkg.RegisterVersionServiceServer(grpcS, version.NewServer(a, func() (bool, error) {
   565  		if a.DisableAuth {
   566  			return true, nil
   567  		}
   568  		sett, err := a.settingsMgr.GetSettings()
   569  		if err != nil {
   570  			return false, err
   571  		}
   572  		return sett.AnonymousUserEnabled, err
   573  	}))
   574  	clusterpkg.RegisterClusterServiceServer(grpcS, clusterService)
   575  	applicationpkg.RegisterApplicationServiceServer(grpcS, applicationService)
   576  	repositorypkg.RegisterRepositoryServiceServer(grpcS, repoService)
   577  	repocredspkg.RegisterRepoCredsServiceServer(grpcS, repoCredsService)
   578  	sessionpkg.RegisterSessionServiceServer(grpcS, sessionService)
   579  	settingspkg.RegisterSettingsServiceServer(grpcS, settingsService)
   580  	projectpkg.RegisterProjectServiceServer(grpcS, projectService)
   581  	accountpkg.RegisterAccountServiceServer(grpcS, accountService)
   582  	certificatepkg.RegisterCertificateServiceServer(grpcS, certificateService)
   583  	gpgkeypkg.RegisterGPGKeyServiceServer(grpcS, gpgkeyService)
   584  	// Register reflection service on gRPC server.
   585  	reflection.Register(grpcS)
   586  	grpc_prometheus.Register(grpcS)
   587  	errors.CheckError(projectService.NormalizeProjs())
   588  	return grpcS
   589  }
   590  
   591  // TranslateGrpcCookieHeader conditionally sets a cookie on the response.
   592  func (a *ArgoCDServer) translateGrpcCookieHeader(ctx context.Context, w http.ResponseWriter, resp golang_proto.Message) error {
   593  	if sessionResp, ok := resp.(*sessionpkg.SessionResponse); ok {
   594  		cookiePath := fmt.Sprintf("path=/%s", strings.TrimRight(strings.TrimLeft(a.ArgoCDServerOpts.RootPath, "/"), "/"))
   595  		flags := []string{cookiePath, "SameSite=lax", "httpOnly"}
   596  		if !a.Insecure {
   597  			flags = append(flags, "Secure")
   598  		}
   599  		token := sessionResp.Token
   600  		if token != "" {
   601  			var err error
   602  			token, err = zjwt.ZJWT(token)
   603  			if err != nil {
   604  				return err
   605  			}
   606  		}
   607  		cookie, err := httputil.MakeCookieMetadata(common.AuthCookieName, token, flags...)
   608  		if err != nil {
   609  			return err
   610  		}
   611  		w.Header().Set("Set-Cookie", cookie)
   612  	}
   613  	return nil
   614  }
   615  
   616  func withRootPath(handler http.Handler, a *ArgoCDServer) http.Handler {
   617  	// get rid of slashes
   618  	root := strings.TrimRight(strings.TrimLeft(a.RootPath, "/"), "/")
   619  
   620  	mux := http.NewServeMux()
   621  	mux.Handle("/"+root+"/", http.StripPrefix("/"+root, handler))
   622  
   623  	healthz.ServeHealthCheck(mux, a.healthCheck)
   624  
   625  	return mux
   626  }
   627  
   628  func compressHandler(handler http.Handler) http.Handler {
   629  	compr := handlers.CompressHandler(handler)
   630  	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
   631  		if request.Header.Get("Accept") == "text/event-stream" {
   632  			handler.ServeHTTP(writer, request)
   633  		} else {
   634  			compr.ServeHTTP(writer, request)
   635  		}
   636  	})
   637  }
   638  
   639  // newHTTPServer returns the HTTP server to serve HTTP/HTTPS requests. This is implemented
   640  // using grpc-gateway as a proxy to the gRPC server.
   641  func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandler http.Handler) *http.Server {
   642  	endpoint := fmt.Sprintf("localhost:%d", port)
   643  	mux := http.NewServeMux()
   644  	httpS := http.Server{
   645  		Addr: endpoint,
   646  		Handler: &handlerSwitcher{
   647  			handler: mux,
   648  			urlToHandler: map[string]http.Handler{
   649  				"/api/badge":          badge.NewHandler(a.AppClientset, a.settingsMgr, a.Namespace),
   650  				common.LogoutEndpoint: logout.NewHandler(a.AppClientset, a.settingsMgr, a.sessionMgr, a.ArgoCDServerOpts.RootPath, a.Namespace),
   651  			},
   652  			contentTypeToHandler: map[string]http.Handler{
   653  				"application/grpc-web+proto": grpcWebHandler,
   654  			},
   655  		},
   656  	}
   657  	var dOpts []grpc.DialOption
   658  	dOpts = append(dOpts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(apiclient.MaxGRPCMessageSize)))
   659  	dOpts = append(dOpts, grpc.WithUserAgent(fmt.Sprintf("%s/%s", common.ArgoCDUserAgentName, common.GetVersion().Version)))
   660  	if a.useTLS() {
   661  		// The following sets up the dial Options for grpc-gateway to talk to gRPC server over TLS.
   662  		// grpc-gateway is just translating HTTP/HTTPS requests as gRPC requests over localhost,
   663  		// so we need to supply the same certificates to establish the connections that a normal,
   664  		// external gRPC client would need.
   665  		tlsConfig := a.settings.TLSConfig()
   666  		if a.TLSConfigCustomizer != nil {
   667  			a.TLSConfigCustomizer(tlsConfig)
   668  		}
   669  		tlsConfig.InsecureSkipVerify = true
   670  		dCreds := credentials.NewTLS(tlsConfig)
   671  		dOpts = append(dOpts, grpc.WithTransportCredentials(dCreds))
   672  	} else {
   673  		dOpts = append(dOpts, grpc.WithInsecure())
   674  	}
   675  
   676  	// HTTP 1.1+JSON Server
   677  	// grpc-ecosystem/grpc-gateway is used to proxy HTTP requests to the corresponding gRPC call
   678  	// NOTE: if a marshaller option is not supplied, grpc-gateway will default to the jsonpb from
   679  	// golang/protobuf. Which does not support types such as time.Time. gogo/protobuf does support
   680  	// time.Time, but does not support custom UnmarshalJSON() and MarshalJSON() methods. Therefore
   681  	// we use our own Marshaler
   682  	gwMuxOpts := runtime.WithMarshalerOption(runtime.MIMEWildcard, new(grpc_util.JSONMarshaler))
   683  	gwCookieOpts := runtime.WithForwardResponseOption(a.translateGrpcCookieHeader)
   684  	gwmux := runtime.NewServeMux(gwMuxOpts, gwCookieOpts)
   685  
   686  	var handler http.Handler = gwmux
   687  	if a.EnableGZip {
   688  		handler = compressHandler(handler)
   689  	}
   690  	mux.Handle("/api/", handler)
   691  
   692  	mustRegisterGWHandler(versionpkg.RegisterVersionServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   693  	mustRegisterGWHandler(clusterpkg.RegisterClusterServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   694  	mustRegisterGWHandler(applicationpkg.RegisterApplicationServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   695  	mustRegisterGWHandler(repositorypkg.RegisterRepositoryServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   696  	mustRegisterGWHandler(repocredspkg.RegisterRepoCredsServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   697  	mustRegisterGWHandler(sessionpkg.RegisterSessionServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   698  	mustRegisterGWHandler(settingspkg.RegisterSettingsServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   699  	mustRegisterGWHandler(projectpkg.RegisterProjectServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   700  	mustRegisterGWHandler(accountpkg.RegisterAccountServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   701  	mustRegisterGWHandler(certificatepkg.RegisterCertificateServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   702  	mustRegisterGWHandler(gpgkeypkg.RegisterGPGKeyServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
   703  
   704  	// Swagger UI
   705  	swagger.ServeSwaggerUI(mux, assets.SwaggerJSON, "/swagger-ui", a.RootPath)
   706  	healthz.ServeHealthCheck(mux, a.healthCheck)
   707  
   708  	// Dex reverse proxy and client app and OAuth2 login/callback
   709  	a.registerDexHandlers(mux)
   710  
   711  	// Webhook handler for git events
   712  	acdWebhookHandler := webhook.NewHandler(a.Namespace, a.AppClientset, a.settings, a.settingsMgr, repocache.NewCache(a.Cache.GetCache(), 24*time.Hour))
   713  	mux.HandleFunc("/api/webhook", acdWebhookHandler.Handler)
   714  
   715  	// Serve cli binaries directly from API server
   716  	registerDownloadHandlers(mux, "/download")
   717  
   718  	// Serve UI static assets
   719  	if a.StaticAssetsDir != "" {
   720  		mux.HandleFunc("/", a.newStaticAssetsHandler(a.StaticAssetsDir, a.BaseHRef))
   721  	}
   722  	return &httpS
   723  }
   724  
   725  // registerDexHandlers will register dex HTTP handlers, creating the the OAuth client app
   726  func (a *ArgoCDServer) registerDexHandlers(mux *http.ServeMux) {
   727  	if !a.settings.IsSSOConfigured() {
   728  		return
   729  	}
   730  	// Run dex OpenID Connect Identity Provider behind a reverse proxy (served at /api/dex)
   731  	var err error
   732  	mux.HandleFunc(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy(a.DexServerAddr, a.BaseHRef))
   733  	if a.useTLS() {
   734  		tlsConfig := a.settings.TLSConfig()
   735  		tlsConfig.InsecureSkipVerify = true
   736  	}
   737  	a.ssoClientApp, err = oidc.NewClientApp(a.settings, a.Cache, a.DexServerAddr, a.BaseHRef)
   738  	errors.CheckError(err)
   739  	mux.HandleFunc(common.LoginEndpoint, a.ssoClientApp.HandleLogin)
   740  	mux.HandleFunc(common.CallbackEndpoint, a.ssoClientApp.HandleCallback)
   741  }
   742  
   743  // newRedirectServer returns an HTTP server which does a 307 redirect to the HTTPS server
   744  func newRedirectServer(port int, rootPath string) *http.Server {
   745  	addr := fmt.Sprintf("localhost:%d/%s", port, strings.TrimRight(strings.TrimLeft(rootPath, "/"), "/"))
   746  	return &http.Server{
   747  		Addr: addr,
   748  		Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   749  			target := "https://" + req.Host
   750  			if rootPath != "" {
   751  				target += "/" + strings.TrimRight(strings.TrimLeft(rootPath, "/"), "/")
   752  			}
   753  			target += req.URL.Path
   754  			if len(req.URL.RawQuery) > 0 {
   755  				target += "?" + req.URL.RawQuery
   756  			}
   757  			http.Redirect(w, req, target, http.StatusTemporaryRedirect)
   758  		}),
   759  	}
   760  }
   761  
   762  // registerDownloadHandlers registers HTTP handlers to support downloads directly from the API server
   763  // (e.g. argocd CLI)
   764  func registerDownloadHandlers(mux *http.ServeMux, base string) {
   765  	linuxPath, err := exec.LookPath("argocd")
   766  	if err != nil {
   767  		log.Warnf("argocd not in PATH")
   768  	} else {
   769  		mux.HandleFunc(base+"/argocd-linux-amd64", func(w http.ResponseWriter, r *http.Request) {
   770  			http.ServeFile(w, r, linuxPath)
   771  		})
   772  	}
   773  	darwinPath, err := exec.LookPath("argocd-darwin-amd64")
   774  	if err != nil {
   775  		log.Warnf("argocd-darwin-amd64 not in PATH")
   776  	} else {
   777  		mux.HandleFunc(base+"/argocd-darwin-amd64", func(w http.ResponseWriter, r *http.Request) {
   778  			http.ServeFile(w, r, darwinPath)
   779  		})
   780  	}
   781  	windowsPath, err := exec.LookPath("argocd-windows-amd64.exe")
   782  	if err != nil {
   783  		log.Warnf("argocd-windows-amd64.exe not in PATH")
   784  	} else {
   785  		mux.HandleFunc(base+"/argocd-windows-amd64.exe", func(w http.ResponseWriter, r *http.Request) {
   786  			http.ServeFile(w, r, windowsPath)
   787  		})
   788  	}
   789  }
   790  
   791  func indexFilePath(srcPath string, baseHRef string) (string, error) {
   792  	if baseHRef == "/" {
   793  		return srcPath, nil
   794  	}
   795  	filePath := path.Join(os.TempDir(), fmt.Sprintf("index_%s.html", strings.Replace(strings.Trim(baseHRef, "/"), "/", "_", -1)))
   796  	f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
   797  	if err != nil {
   798  		if os.IsExist(err) {
   799  			return filePath, nil
   800  		}
   801  		return "", err
   802  	}
   803  	defer io.Close(f)
   804  
   805  	data, err := ioutil.ReadFile(srcPath)
   806  	if err != nil {
   807  		return "", err
   808  	}
   809  	if baseHRef != "/" {
   810  		data = []byte(baseHRefRegex.ReplaceAllString(string(data), fmt.Sprintf(`<base href="/%s/">`, strings.Trim(baseHRef, "/"))))
   811  	}
   812  	_, err = f.Write(data)
   813  	if err != nil {
   814  		return "", err
   815  	}
   816  
   817  	return filePath, nil
   818  }
   819  
   820  func fileExists(filename string) bool {
   821  	info, err := os.Stat(filename)
   822  	if os.IsNotExist(err) {
   823  		return false
   824  	}
   825  	return !info.IsDir()
   826  }
   827  
   828  // newStaticAssetsHandler returns an HTTP handler to serve UI static assets
   829  func (server *ArgoCDServer) newStaticAssetsHandler(dir string, baseHRef string) func(http.ResponseWriter, *http.Request) {
   830  	return func(w http.ResponseWriter, r *http.Request) {
   831  		acceptHTML := false
   832  		for _, acceptType := range strings.Split(r.Header.Get("Accept"), ",") {
   833  			if acceptType == "text/html" || acceptType == "html" {
   834  				acceptHTML = true
   835  				break
   836  			}
   837  		}
   838  		fileRequest := r.URL.Path != "/index.html" && fileExists(path.Join(dir, r.URL.Path))
   839  
   840  		// Set X-Frame-Options according to configuration
   841  		if server.XFrameOptions != "" {
   842  			w.Header().Set("X-Frame-Options", server.XFrameOptions)
   843  		}
   844  		w.Header().Set("X-XSS-Protection", "1")
   845  
   846  		// serve index.html for non file requests to support HTML5 History API
   847  		if acceptHTML && !fileRequest && (r.Method == "GET" || r.Method == "HEAD") {
   848  			for k, v := range noCacheHeaders {
   849  				w.Header().Set(k, v)
   850  			}
   851  			indexHtmlPath, err := indexFilePath(path.Join(dir, "index.html"), baseHRef)
   852  			if err != nil {
   853  				http.Error(w, fmt.Sprintf("Unable to access index.html: %v", err), http.StatusInternalServerError)
   854  				return
   855  			}
   856  			http.ServeFile(w, r, indexHtmlPath)
   857  		} else {
   858  			http.ServeFile(w, r, path.Join(dir, r.URL.Path))
   859  		}
   860  	}
   861  }
   862  
   863  type registerFunc func(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error
   864  
   865  // mustRegisterGWHandler is a convenience function to register a gateway handler
   866  func mustRegisterGWHandler(register registerFunc, ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) {
   867  	err := register(ctx, mux, endpoint, opts)
   868  	if err != nil {
   869  		panic(err)
   870  	}
   871  }
   872  
   873  // Authenticate checks for the presence of a valid token when accessing server-side resources.
   874  func (a *ArgoCDServer) Authenticate(ctx context.Context) (context.Context, error) {
   875  	if a.DisableAuth {
   876  		return ctx, nil
   877  	}
   878  	claims, claimsErr := a.getClaims(ctx)
   879  	if claims != nil {
   880  		// Add claims to the context to inspect for RBAC
   881  		// nolint:staticcheck
   882  		ctx = context.WithValue(ctx, "claims", claims)
   883  	}
   884  
   885  	if claimsErr != nil {
   886  		argoCDSettings, err := a.settingsMgr.GetSettings()
   887  		if err != nil {
   888  			return ctx, status.Errorf(codes.Internal, "unable to load settings: %v", err)
   889  		}
   890  		if !argoCDSettings.AnonymousUserEnabled {
   891  			return ctx, claimsErr
   892  		}
   893  	}
   894  
   895  	return ctx, nil
   896  }
   897  
   898  func (a *ArgoCDServer) getClaims(ctx context.Context) (jwt.Claims, error) {
   899  	md, ok := metadata.FromIncomingContext(ctx)
   900  	if !ok {
   901  		return nil, ErrNoSession
   902  	}
   903  	tokenString := getToken(md)
   904  	if tokenString == "" {
   905  		return nil, ErrNoSession
   906  	}
   907  	claims, err := a.sessionMgr.VerifyToken(tokenString)
   908  	if err != nil {
   909  		return claims, status.Errorf(codes.Unauthenticated, "invalid session: %v", err)
   910  	}
   911  	return claims, nil
   912  }
   913  
   914  // getToken extracts the token from gRPC metadata or cookie headers
   915  func getToken(md metadata.MD) string {
   916  	// check the "token" metadata
   917  	{
   918  		tokens, ok := md[apiclient.MetaDataTokenKey]
   919  		if ok && len(tokens) > 0 {
   920  			return tokens[0]
   921  		}
   922  	}
   923  
   924  	var tokens []string
   925  
   926  	// looks for the HTTP header `Authorization: Bearer ...`
   927  	for _, t := range md["authorization"] {
   928  		if strings.HasPrefix(t, "Bearer ") {
   929  			tokens = append(tokens, strings.TrimPrefix(t, "Bearer "))
   930  		}
   931  	}
   932  
   933  	// check the HTTP cookie
   934  	for _, t := range md["grpcgateway-cookie"] {
   935  		header := http.Header{}
   936  		header.Add("Cookie", t)
   937  		request := http.Request{Header: header}
   938  		token, err := request.Cookie(common.AuthCookieName)
   939  		if err == nil {
   940  			tokens = append(tokens, token.Value)
   941  		}
   942  	}
   943  
   944  	for _, t := range tokens {
   945  		value, err := zjwt.JWT(t)
   946  		if err == nil {
   947  			return value
   948  		}
   949  	}
   950  	return ""
   951  }
   952  
   953  type handlerSwitcher struct {
   954  	handler              http.Handler
   955  	urlToHandler         map[string]http.Handler
   956  	contentTypeToHandler map[string]http.Handler
   957  }
   958  
   959  func (s *handlerSwitcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   960  	if urlHandler, ok := s.urlToHandler[r.URL.Path]; ok {
   961  		urlHandler.ServeHTTP(w, r)
   962  	} else if contentHandler, ok := s.contentTypeToHandler[r.Header.Get("content-type")]; ok {
   963  		contentHandler.ServeHTTP(w, r)
   964  	} else {
   965  		s.handler.ServeHTTP(w, r)
   966  	}
   967  }
   968  
   969  // Workaround for https://github.com/golang/go/issues/21955 to support escaped URLs in URL path.
   970  type bug21955Workaround struct {
   971  	handler http.Handler
   972  }
   973  
   974  var pathPatters = []*regexp.Regexp{
   975  	regexp.MustCompile(`/api/v1/clusters/[^/]+`),
   976  	regexp.MustCompile(`/api/v1/repositories/[^/]+`),
   977  	regexp.MustCompile(`/api/v1/repocreds/[^/]+`),
   978  	regexp.MustCompile(`/api/v1/repositories/[^/]+/apps`),
   979  	regexp.MustCompile(`/api/v1/repositories/[^/]+/apps/[^/]+`),
   980  	regexp.MustCompile(`/settings/clusters/[^/]+`),
   981  }
   982  
   983  func (bf *bug21955Workaround) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   984  	for _, pattern := range pathPatters {
   985  		if pattern.MatchString(r.URL.RawPath) {
   986  			r.URL.Path = r.URL.RawPath
   987  			break
   988  		}
   989  	}
   990  	bf.handler.ServeHTTP(w, r)
   991  }
   992  
   993  func bug21955WorkaroundInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
   994  	if rq, ok := req.(*repositorypkg.RepoQuery); ok {
   995  		repo, err := url.QueryUnescape(rq.Repo)
   996  		if err != nil {
   997  			return nil, err
   998  		}
   999  		rq.Repo = repo
  1000  	} else if rk, ok := req.(*repositorypkg.RepoAppsQuery); ok {
  1001  		repo, err := url.QueryUnescape(rk.Repo)
  1002  		if err != nil {
  1003  			return nil, err
  1004  		}
  1005  		rk.Repo = repo
  1006  	} else if rdq, ok := req.(*repositorypkg.RepoAppDetailsQuery); ok {
  1007  		repo, err := url.QueryUnescape(rdq.Source.RepoURL)
  1008  		if err != nil {
  1009  			return nil, err
  1010  		}
  1011  		rdq.Source.RepoURL = repo
  1012  	} else if ru, ok := req.(*repositorypkg.RepoUpdateRequest); ok {
  1013  		repo, err := url.QueryUnescape(ru.Repo.Repo)
  1014  		if err != nil {
  1015  			return nil, err
  1016  		}
  1017  		ru.Repo.Repo = repo
  1018  	} else if rk, ok := req.(*repocredspkg.RepoCredsQuery); ok {
  1019  		pattern, err := url.QueryUnescape(rk.Url)
  1020  		if err != nil {
  1021  			return nil, err
  1022  		}
  1023  		rk.Url = pattern
  1024  	} else if rk, ok := req.(*repocredspkg.RepoCredsDeleteRequest); ok {
  1025  		pattern, err := url.QueryUnescape(rk.Url)
  1026  		if err != nil {
  1027  			return nil, err
  1028  		}
  1029  		rk.Url = pattern
  1030  	} else if cq, ok := req.(*clusterpkg.ClusterQuery); ok {
  1031  		server, err := url.QueryUnescape(cq.Server)
  1032  		if err != nil {
  1033  			return nil, err
  1034  		}
  1035  		cq.Server = server
  1036  	} else if cu, ok := req.(*clusterpkg.ClusterUpdateRequest); ok {
  1037  		server, err := url.QueryUnescape(cu.Cluster.Server)
  1038  		if err != nil {
  1039  			return nil, err
  1040  		}
  1041  		cu.Cluster.Server = server
  1042  	}
  1043  	return handler(ctx, req)
  1044  }