github.com/kiali/kiali@v1.84.0/routing/router.go (about)

     1  package routing
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	hpprof "net/http/pprof"
     9  	"os"
    10  	"path/filepath"
    11  	rpprof "runtime/pprof"
    12  	"strings"
    13  
    14  	"github.com/gorilla/mux"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  
    17  	"github.com/kiali/kiali/business"
    18  	"github.com/kiali/kiali/business/authentication"
    19  	"github.com/kiali/kiali/config"
    20  	"github.com/kiali/kiali/handlers"
    21  	"github.com/kiali/kiali/kubernetes"
    22  	"github.com/kiali/kiali/kubernetes/cache"
    23  	"github.com/kiali/kiali/log"
    24  	kialiprometheus "github.com/kiali/kiali/prometheus"
    25  	"github.com/kiali/kiali/prometheus/internalmetrics"
    26  	"github.com/kiali/kiali/tracing"
    27  )
    28  
    29  // NewRouter creates the router with all API routes and the static files handler
    30  func NewRouter(conf *config.Config, kialiCache cache.KialiCache, clientFactory kubernetes.ClientFactory, prom kialiprometheus.ClientInterface, traceClientLoader func() tracing.ClientInterface, cpm business.ControlPlaneMonitor) (*mux.Router, error) {
    31  	webRoot := conf.Server.WebRoot
    32  	webRootWithSlash := webRoot + "/"
    33  
    34  	rootRouter := mux.NewRouter().StrictSlash(false)
    35  	appRouter := rootRouter
    36  
    37  	staticFileServer := http.FileServer(http.Dir(conf.Server.StaticContentRootDirectory))
    38  
    39  	if webRoot != "/" {
    40  		// help the user out - if a request comes in for "/", redirect to our true webroot
    41  		rootRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    42  			http.Redirect(w, r, webRootWithSlash, http.StatusFound)
    43  		})
    44  
    45  		appRouter = rootRouter.PathPrefix(conf.Server.WebRoot).Subrouter()
    46  		staticFileServer = http.StripPrefix(webRootWithSlash, staticFileServer)
    47  
    48  		// Because of OIDC, when we receive a request for the webroot without
    49  		// the trailing slash, we can not redirect the user to the correct
    50  		// webroot as the hash params are lost (and they are not sent to the
    51  		// server).
    52  		//
    53  		// See https://github.com/kiali/kiali/issues/3103
    54  		rootRouter.HandleFunc(webRoot, func(w http.ResponseWriter, r *http.Request) {
    55  			r.URL.Path = webRootWithSlash
    56  			rootRouter.ServeHTTP(w, r)
    57  		})
    58  	} else {
    59  		webRootWithSlash = "/"
    60  	}
    61  
    62  	fileServerHandler := func(w http.ResponseWriter, r *http.Request) {
    63  		urlPath := r.RequestURI
    64  		if r.URL != nil {
    65  			urlPath = r.URL.Path
    66  		}
    67  
    68  		if urlPath == webRootWithSlash || urlPath == webRoot || urlPath == webRootWithSlash+"index.html" {
    69  			serveIndexFile(w)
    70  		} else if urlPath == webRootWithSlash+"env.js" {
    71  			serveEnvJsFile(w)
    72  		} else {
    73  			staticFileServer.ServeHTTP(w, r)
    74  		}
    75  	}
    76  
    77  	appRouter = appRouter.StrictSlash(true)
    78  
    79  	persistor := authentication.NewCookieSessionPersistor(conf)
    80  	strategy := conf.Auth.Strategy
    81  
    82  	var authController authentication.AuthController
    83  	if strategy == config.AuthStrategyToken {
    84  		authController = authentication.NewTokenAuthController(persistor, clientFactory, kialiCache, conf)
    85  	} else if strategy == config.AuthStrategyOpenId {
    86  		authController = authentication.NewOpenIdAuthController(persistor, kialiCache, clientFactory, conf)
    87  	} else if strategy == config.AuthStrategyOpenshift {
    88  		openshiftOAuthService, err := business.NewOpenshiftOAuthService(context.TODO(), conf, clientFactory.GetSAClients(), clientFactory)
    89  		if err != nil {
    90  			log.Errorf("Error creating OpenshiftOAuthService: %v", err)
    91  			return nil, err
    92  		}
    93  		openshiftAuth, err := authentication.NewOpenshiftAuthController(persistor, openshiftOAuthService, conf)
    94  		if err != nil {
    95  			log.Errorf("Error creating OpenshiftAuthController: %v", err)
    96  			return nil, err
    97  		}
    98  		authController = openshiftAuth
    99  	} else if strategy == config.AuthStrategyHeader {
   100  		authController = authentication.NewHeaderAuthController(persistor, clientFactory.GetSAHomeClusterClient())
   101  	}
   102  
   103  	// Build our API server routes and install them.
   104  	apiRoutes := NewRoutes(conf, kialiCache, clientFactory, prom, traceClientLoader, cpm, authController)
   105  	authenticationHandler := handlers.NewAuthenticationHandler(*conf, authController, clientFactory.GetSAHomeClusterClient())
   106  
   107  	allRoutes := apiRoutes.Routes
   108  
   109  	// Add the Profiler handlers if enabled
   110  	if conf.Server.Profiler.Enabled {
   111  		log.Infof("Profiler is enabled")
   112  		allRoutes = append(allRoutes,
   113  			Route{
   114  				Method:        "GET",
   115  				Name:          "PProf Index",
   116  				Pattern:       "/debug/pprof/", // the ending slash is important
   117  				HandlerFunc:   hpprof.Index,
   118  				Authenticated: true,
   119  			},
   120  			Route{
   121  				Method:        "GET",
   122  				Name:          "PProf Cmdline",
   123  				Pattern:       "/debug/pprof/cmdline",
   124  				HandlerFunc:   hpprof.Cmdline,
   125  				Authenticated: true,
   126  			},
   127  			Route{
   128  				Method:        "GET",
   129  				Name:          "PProf Profile",
   130  				Pattern:       "/debug/pprof/profile",
   131  				HandlerFunc:   hpprof.Profile,
   132  				Authenticated: true,
   133  			},
   134  			Route{
   135  				Method:        "GET",
   136  				Name:          "PProf Symbol",
   137  				Pattern:       "/debug/pprof/symbol",
   138  				HandlerFunc:   hpprof.Symbol,
   139  				Authenticated: true,
   140  			},
   141  			Route{
   142  				Method:        "GET",
   143  				Name:          "PProf Trace",
   144  				Pattern:       "/debug/pprof/trace",
   145  				HandlerFunc:   hpprof.Trace,
   146  				Authenticated: true,
   147  			},
   148  		)
   149  		for _, p := range rpprof.Profiles() {
   150  			allRoutes = append(allRoutes,
   151  				Route{
   152  					Method:        "GET",
   153  					Name:          "PProf " + p.Name(),
   154  					Pattern:       "/debug/pprof/" + p.Name(),
   155  					HandlerFunc:   hpprof.Handler(p.Name()).ServeHTTP,
   156  					Authenticated: true,
   157  				},
   158  			)
   159  		}
   160  	}
   161  
   162  	for _, route := range allRoutes {
   163  		handlerFunction := metricHandler(route.HandlerFunc, route)
   164  		if route.Authenticated {
   165  			handlerFunction = authenticationHandler.Handle(handlerFunction)
   166  		} else {
   167  			handlerFunction = authenticationHandler.HandleUnauthenticated(handlerFunction)
   168  		}
   169  		appRouter.
   170  			Methods(route.Method).
   171  			Path(route.Pattern).
   172  			Name(route.Name).
   173  			Handler(handlerFunction)
   174  	}
   175  
   176  	if authController != nil {
   177  		if ac, ok := authController.(*authentication.OpenIdAuthController); ok {
   178  			ac.PostRoutes(appRouter)
   179  		} else if ac, ok := authController.(*authentication.OpenshiftAuthController); ok {
   180  			ac.PostRoutes(appRouter)
   181  		}
   182  	}
   183  
   184  	// All client-side routes are prefixed with /console.
   185  	// They are forwarded to index.html and will be handled by react-router.
   186  	appRouter.PathPrefix("/console").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   187  		serveIndexFile(w)
   188  	})
   189  
   190  	if authController != nil {
   191  		if ac, ok := authController.(*authentication.OpenIdAuthController); ok {
   192  			authCallback := ac.GetAuthCallbackHandler(http.HandlerFunc(fileServerHandler))
   193  			rootRouter.Methods("GET").Path(webRootWithSlash).Handler(authCallback)
   194  			// Need a URL to catch for openshift too.
   195  		} else if ac, ok := authController.(*authentication.OpenshiftAuthController); ok {
   196  			authCallback := ac.GetAuthCallbackHandler(http.HandlerFunc(fileServerHandler))
   197  			rootRouter.Methods("GET").Path(webRootWithSlash).Handler(authCallback)
   198  		}
   199  	}
   200  
   201  	rootRouter.PathPrefix(webRootWithSlash).HandlerFunc(fileServerHandler)
   202  
   203  	return rootRouter, nil
   204  }
   205  
   206  // statusResponseWriter contains a ResponseWriter and a StatusCode to read in the metrics middleware
   207  type statusResponseWriter struct {
   208  	http.ResponseWriter
   209  	StatusCode int
   210  }
   211  
   212  // WriteHeader will be called by any function that needs to set an status code, in this function the StatusCode is also set
   213  func (srw *statusResponseWriter) WriteHeader(code int) {
   214  	srw.ResponseWriter.WriteHeader(code)
   215  	srw.StatusCode = code
   216  }
   217  
   218  // updateMetric evaluates the StatusCode, if there is an error, increase the API failure counter, otherwise save the duration
   219  func updateMetric(route string, srw *statusResponseWriter, timer *prometheus.Timer) {
   220  	// Always measure the duration even if the API call ended in an error
   221  	timer.ObserveDuration()
   222  	// Increase the error counter on 500 and 503 errors
   223  	if srw.StatusCode == http.StatusInternalServerError || srw.StatusCode == http.StatusServiceUnavailable {
   224  		internalmetrics.GetAPIFailureMetric(route).Inc()
   225  	}
   226  }
   227  
   228  func metricHandler(next http.Handler, route Route) http.Handler {
   229  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   230  		// By default, if there is no call to WriteHeader, an 200 will be
   231  		srw := &statusResponseWriter{
   232  			ResponseWriter: w,
   233  			StatusCode:     http.StatusOK,
   234  		}
   235  		promtimer := internalmetrics.GetAPIProcessingTimePrometheusTimer(route.Name)
   236  		defer updateMetric(route.Name, srw, promtimer)
   237  		next.ServeHTTP(srw, r)
   238  	})
   239  }
   240  
   241  // serveEnvJsFile generates the env.js file needed by the UI from Kiali configs. The
   242  // generated file is sent to the HTTP response.
   243  func serveEnvJsFile(w http.ResponseWriter) {
   244  	conf := config.Get()
   245  	var body string
   246  	if len(conf.Server.WebHistoryMode) > 0 {
   247  		body += fmt.Sprintf("window.HISTORY_MODE='%s';", conf.Server.WebHistoryMode)
   248  	}
   249  
   250  	body += "window.WEB_ROOT = document.getElementsByTagName('base')[0].getAttribute('href').replace(/^https?:\\/\\/[^#?\\/]+/g, '').replace(/\\/+$/g, '')"
   251  
   252  	w.Header().Set("content-type", "text/javascript")
   253  	_, err := io.WriteString(w, body)
   254  	if err != nil {
   255  		log.Errorf("HTTP I/O error [%v]", err.Error())
   256  	}
   257  }
   258  
   259  // serveIndexFile takes UI's index.html as a template to generate a modified index file that takes
   260  // into account the web_root path configured in the Kiali CR. The result is sent to the HTTP response.
   261  func serveIndexFile(w http.ResponseWriter) {
   262  	webRootPath := config.Get().Server.WebRoot
   263  	webRootPath = strings.TrimSuffix(webRootPath, "/")
   264  
   265  	path, _ := filepath.Abs("./console/index.html")
   266  	b, err := os.ReadFile(path)
   267  	if err != nil {
   268  		log.Errorf("File I/O error [%v]", err.Error())
   269  		handlers.RespondWithDetailedError(w, http.StatusInternalServerError, "Unable to read index.html template file", err.Error())
   270  		return
   271  	}
   272  
   273  	html := string(b)
   274  	newHTML := html
   275  
   276  	if len(webRootPath) != 0 {
   277  		searchStr := `<base href="/"`
   278  		newStr := `<base href="` + webRootPath + `/"`
   279  		newHTML = strings.Replace(html, searchStr, newStr, -1)
   280  	}
   281  
   282  	w.Header().Set("content-type", "text/html")
   283  	_, err = io.WriteString(w, newHTML)
   284  	if err != nil {
   285  		log.Errorf("HTTP I/O error [%v]", err.Error())
   286  	}
   287  }