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
27 statExpiration = 1500 * time.Millisecond
28
29
30
31 statCleanupInterval = 5 * time.Minute
32 )
33
34 type (
35
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
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
83
84
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,
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
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
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
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
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
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
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
194 server.router.GET("/dist/*filepath", mkStaticHandler(staticDir))
195
196
197 server.router.GET("/api/version", handler.handleAPIVersion)
198
199
200
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
213 if grafanaExternalAddr == "" {
214 server.handleAllOperationsForPath("/grafana/*grafanapath", handler.handleGrafana)
215 }
216
217
218 server.handleAllOperationsForPath("/jaeger/*jaegerpath", handler.handleJaeger)
219
220 return httpServer
221 }
222
223
224
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
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
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
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