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 }