...

Source file src/github.com/linkerd/linkerd2/web/srv/server.go

Documentation: github.com/linkerd/linkerd2/web/srv

     1  package srv
     2  
     3  import (
     4  	"fmt"
     5  	"html"
     6  	"html/template"
     7  	"net/http"
     8  	"path"
     9  	"path/filepath"
    10  	"regexp"
    11  	"time"
    12  
    13  	"github.com/julienschmidt/httprouter"
    14  	"github.com/linkerd/linkerd2/pkg/filesonly"
    15  	"github.com/linkerd/linkerd2/pkg/healthcheck"
    16  	"github.com/linkerd/linkerd2/pkg/k8s"
    17  	"github.com/linkerd/linkerd2/pkg/prometheus"
    18  	vizPb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
    19  	"github.com/patrickmn/go-cache"
    20  	log "github.com/sirupsen/logrus"
    21  )
    22  
    23  const (
    24  	timeout = 15 * time.Second
    25  
    26  	// statExpiration indicates when items in the stat cache expire.
    27  	statExpiration = 1500 * time.Millisecond
    28  
    29  	// statCleanupInterval indicates how often expired items in the stat cache
    30  	// are cleaned up.
    31  	statCleanupInterval = 5 * time.Minute
    32  )
    33  
    34  type (
    35  	// Server encapsulates the Linkerd control plane's web dashboard server.
    36  	Server struct {
    37  		templateDir string
    38  		reload      bool
    39  		templates   map[string]*template.Template
    40  		router      *httprouter.Router
    41  		reHost      *regexp.Regexp
    42  	}
    43  
    44  	templatePayload struct {
    45  		Contents interface{}
    46  	}
    47  	appParams struct {
    48  		UUID                string
    49  		ReleaseVersion      string
    50  		ControllerNamespace string
    51  		Error               bool
    52  		ErrorMessage        string
    53  		PathPrefix          string
    54  		Jaeger              string
    55  		Grafana             string
    56  		GrafanaExternalURL  string
    57  		GrafanaPrefix       string
    58  	}
    59  
    60  	healthChecker interface {
    61  		RunChecks(observer healthcheck.CheckObserver) (bool, bool)
    62  	}
    63  )
    64  
    65  // this is called by the HTTP server to actually respond to a request
    66  func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    67  	if !s.reHost.MatchString(req.Host) {
    68  		err := fmt.Sprintf(`It appears that you are trying to reach this service with a host of '%s'.
    69  This does not match /%s/ and has been denied for security reasons.
    70  Please see https://linkerd.io/dns-rebinding for an explanation of what is happening and how to fix it.`,
    71  			html.EscapeString(req.Host),
    72  			html.EscapeString(s.reHost.String()))
    73  		http.Error(w, err, http.StatusBadRequest)
    74  		return
    75  	}
    76  	w.Header().Set("X-Content-Type-Options", "nosniff")
    77  	w.Header().Set("X-Frame-Options", "SAMEORIGIN")
    78  	w.Header().Set("X-XSS-Protection", "1; mode=block")
    79  	s.router.ServeHTTP(w, req)
    80  }
    81  
    82  // NewServer returns an initialized `http.Server`, configured to listen on an
    83  // address, render templates, and serve static assets, for a given Linkerd
    84  // control plane.
    85  func NewServer(
    86  	addr string,
    87  	grafanaAddr string,
    88  	grafanaExternalAddr string,
    89  	grafanaPrefix string,
    90  	jaegerAddr string,
    91  	templateDir string,
    92  	staticDir string,
    93  	uuid string,
    94  	version string,
    95  	controllerNamespace string,
    96  	clusterDomain string,
    97  	reload bool,
    98  	reHost *regexp.Regexp,
    99  	apiClient vizPb.ApiClient,
   100  	k8sAPI *k8s.KubernetesAPI,
   101  	hc healthChecker,
   102  ) *http.Server {
   103  	server := &Server{
   104  		templateDir: templateDir,
   105  		reload:      reload,
   106  		reHost:      reHost,
   107  	}
   108  
   109  	server.router = &httprouter.Router{
   110  		RedirectTrailingSlash:  true,
   111  		RedirectFixedPath:      true,
   112  		HandleMethodNotAllowed: false, // disable 405s
   113  	}
   114  
   115  	wrappedServer := prometheus.WithTelemetry(server)
   116  	handler := &handler{
   117  		apiClient:           apiClient,
   118  		k8sAPI:              k8sAPI,
   119  		render:              server.RenderTemplate,
   120  		uuid:                uuid,
   121  		version:             version,
   122  		controllerNamespace: controllerNamespace,
   123  		clusterDomain:       clusterDomain,
   124  		jaegerProxy:         newReverseProxy(jaegerAddr, ""),
   125  		grafana:             grafanaAddr,
   126  		grafanaExternalURL:  grafanaExternalAddr,
   127  		grafanaPrefix:       grafanaPrefix,
   128  		jaeger:              jaegerAddr,
   129  		hc:                  hc,
   130  		statCache:           cache.New(statExpiration, statCleanupInterval),
   131  	}
   132  
   133  	// Only create the grafana reverse proxy if we aren't using external grafana
   134  	if grafanaExternalAddr == "" {
   135  		handler.grafanaProxy = newReverseProxy(grafanaAddr, "/grafana")
   136  	}
   137  
   138  	httpServer := &http.Server{
   139  		Addr:              addr,
   140  		ReadTimeout:       timeout,
   141  		ReadHeaderTimeout: timeout,
   142  		WriteTimeout:      timeout,
   143  		Handler:           wrappedServer,
   144  	}
   145  
   146  	// webapp routes
   147  	server.router.GET("/", handler.handleIndex)
   148  	server.router.GET("/controlplane", handler.handleIndex)
   149  	server.router.GET("/namespaces", handler.handleIndex)
   150  	server.router.GET("/gateways", handler.handleIndex)
   151  
   152  	// paths for a list of resources by namespace
   153  	server.router.GET("/namespaces/:namespace/daemonsets", handler.handleIndex)
   154  	server.router.GET("/namespaces/:namespace/statefulsets", handler.handleIndex)
   155  	server.router.GET("/namespaces/:namespace/jobs", handler.handleIndex)
   156  	server.router.GET("/namespaces/:namespace/deployments", handler.handleIndex)
   157  	server.router.GET("/namespaces/:namespace/services", handler.handleIndex)
   158  	server.router.GET("/namespaces/:namespace/replicationcontrollers", handler.handleIndex)
   159  	server.router.GET("/namespaces/:namespace/pods", handler.handleIndex)
   160  	server.router.GET("/namespaces/:namespace/cronjobs", handler.handleIndex)
   161  	server.router.GET("/namespaces/:namespace/replicasets", handler.handleIndex)
   162  
   163  	// legacy paths that are deprecated but should not 404
   164  	server.router.GET("/overview", handler.handleIndex)
   165  	server.router.GET("/daemonsets", handler.handleIndex)
   166  	server.router.GET("/statefulsets", handler.handleIndex)
   167  	server.router.GET("/jobs", handler.handleIndex)
   168  	server.router.GET("/deployments", handler.handleIndex)
   169  	server.router.GET("/services", handler.handleIndex)
   170  	server.router.GET("/replicationcontrollers", handler.handleIndex)
   171  	server.router.GET("/pods", handler.handleIndex)
   172  
   173  	// paths for individual resource view
   174  	server.router.GET("/namespaces/:namespace", handler.handleIndex)
   175  	server.router.GET("/namespaces/:namespace/pods/:pod", handler.handleIndex)
   176  	server.router.GET("/namespaces/:namespace/daemonsets/:daemonset", handler.handleIndex)
   177  	server.router.GET("/namespaces/:namespace/statefulsets/:statefulset", handler.handleIndex)
   178  	server.router.GET("/namespaces/:namespace/deployments/:deployment", handler.handleIndex)
   179  	server.router.GET("/namespaces/:namespace/services/:deployment", handler.handleIndex)
   180  	server.router.GET("/namespaces/:namespace/jobs/:job", handler.handleIndex)
   181  	server.router.GET("/namespaces/:namespace/replicationcontrollers/:replicationcontroller", handler.handleIndex)
   182  	server.router.GET("/namespaces/:namespace/cronjobs/:cronjob", handler.handleIndex)
   183  	server.router.GET("/namespaces/:namespace/replicasets/:replicaset", handler.handleIndex)
   184  
   185  	// tools and community paths
   186  	server.router.GET("/tap", handler.handleIndex)
   187  	server.router.GET("/top", handler.handleIndex)
   188  	server.router.GET("/community", handler.handleIndex)
   189  	server.router.GET("/routes", handler.handleIndex)
   190  	server.router.GET("/extensions", handler.handleIndex)
   191  	server.router.GET("/profiles/new", handler.handleProfileDownload)
   192  
   193  	// add catch-all parameter to match all files in dir
   194  	server.router.GET("/dist/*filepath", mkStaticHandler(staticDir))
   195  
   196  	// webapp api routes
   197  	server.router.GET("/api/version", handler.handleAPIVersion)
   198  	// Traffic Performance Summary.  This route used to be called /api/stat
   199  	// but was renamed to avoid triggering ad blockers.
   200  	// See: https://github.com/linkerd/linkerd2/issues/970
   201  	server.router.GET("/api/tps-reports", handler.handleAPIStat)
   202  	server.router.GET("/api/pods", handler.handleAPIPods)
   203  	server.router.GET("/api/services", handler.handleAPIServices)
   204  	server.router.GET("/api/tap", handler.handleAPITap)
   205  	server.router.GET("/api/routes", handler.handleAPITopRoutes)
   206  	server.router.GET("/api/edges", handler.handleAPIEdges)
   207  	server.router.GET("/api/check", handler.handleAPICheck)
   208  	server.router.GET("/api/resource-definition", handler.handleAPIResourceDefinition)
   209  	server.router.GET("/api/gateways", handler.handleAPIGateways)
   210  	server.router.GET("/api/extensions", handler.handleGetExtensions)
   211  
   212  	// grafana proxy, only used if external grafana is not in use
   213  	if grafanaExternalAddr == "" {
   214  		server.handleAllOperationsForPath("/grafana/*grafanapath", handler.handleGrafana)
   215  	}
   216  
   217  	// jaeger proxy
   218  	server.handleAllOperationsForPath("/jaeger/*jaegerpath", handler.handleJaeger)
   219  
   220  	return httpServer
   221  }
   222  
   223  // RenderTemplate writes a rendered template into a buffer, given an HTTP
   224  // request and template information.
   225  func (s *Server) RenderTemplate(w http.ResponseWriter, templateFile, templateName string, args interface{}) error {
   226  	log.Debugf("emitting template %s", templateFile)
   227  	template, err := s.loadTemplate(templateFile)
   228  
   229  	if err != nil {
   230  		log.Error(err.Error())
   231  		http.Error(w, "internal server error", http.StatusInternalServerError)
   232  		return nil
   233  	}
   234  
   235  	w.Header().Set("Content-Type", "text/html")
   236  	if templateName == "" {
   237  		return template.Execute(w, args)
   238  	}
   239  
   240  	return template.ExecuteTemplate(w, templateName, templatePayload{Contents: args})
   241  }
   242  
   243  func (s *Server) loadTemplate(templateFile string) (template *template.Template, err error) {
   244  	// load template from disk if necessary
   245  	template = s.templates[templateFile]
   246  
   247  	if template == nil || s.reload {
   248  		templatePath := safelyJoinPath(s.templateDir, templateFile)
   249  		includes, err := filepath.Glob(filepath.Join(s.templateDir, "includes", "*.tmpl.html"))
   250  		if err != nil {
   251  			return nil, err
   252  		}
   253  		// for cases where you're not calling a named template, the passed-in path needs to be first
   254  		templateFiles := append([]string{templatePath}, includes...)
   255  		log.Debugf("loading templates from %v", templateFiles)
   256  		template, err = template.ParseFiles(templateFiles...)
   257  		if err == nil && !s.reload {
   258  			s.templates[templateFile] = template
   259  		}
   260  	}
   261  	return template, err
   262  }
   263  
   264  func (s *Server) handleAllOperationsForPath(path string, handle httprouter.Handle) {
   265  	s.router.DELETE(path, handle)
   266  	s.router.GET(path, handle)
   267  	s.router.HEAD(path, handle)
   268  	s.router.OPTIONS(path, handle)
   269  	s.router.PATCH(path, handle)
   270  	s.router.POST(path, handle)
   271  	s.router.PUT(path, handle)
   272  }
   273  
   274  func safelyJoinPath(rootPath, userPath string) string {
   275  	return filepath.Join(rootPath, path.Clean("/"+userPath))
   276  }
   277  
   278  func mkStaticHandler(staticDir string) httprouter.Handle {
   279  	fileServer := http.FileServer(filesonly.FileSystem(staticDir))
   280  
   281  	return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) {
   282  		filepath := p.ByName("filepath")
   283  		if filepath == "/index_bundle.js" {
   284  			// don't cache the bundle because it references a hashed js file
   285  			w.Header().Set("Cache-Control", "no-store, must-revalidate")
   286  		}
   287  
   288  		req.URL.Path = filepath
   289  		fileServer.ServeHTTP(w, req)
   290  	}
   291  }
   292  

View as plain text