1package agent
2
3import (
4	"fmt"
5	"net/http"
6	"net/http/httputil"
7	"net/url"
8	"path"
9	"sort"
10	"strings"
11
12	"github.com/hashicorp/consul/acl"
13	"github.com/hashicorp/consul/agent/config"
14	"github.com/hashicorp/consul/agent/structs"
15	"github.com/hashicorp/consul/api"
16	"github.com/hashicorp/consul/logging"
17	"github.com/hashicorp/go-hclog"
18)
19
20// ServiceSummary is used to summarize a service
21type ServiceSummary struct {
22	Kind                structs.ServiceKind `json:",omitempty"`
23	Name                string
24	Datacenter          string
25	Tags                []string
26	Nodes               []string
27	ExternalSources     []string
28	externalSourceSet   map[string]struct{} // internal to track uniqueness
29	checks              map[string]*structs.HealthCheck
30	InstanceCount       int
31	ChecksPassing       int
32	ChecksWarning       int
33	ChecksCritical      int
34	GatewayConfig       GatewayConfig
35	TransparentProxy    bool
36	transparentProxySet bool
37
38	structs.EnterpriseMeta
39}
40
41func (s *ServiceSummary) LessThan(other *ServiceSummary) bool {
42	if s.EnterpriseMeta.LessThan(&other.EnterpriseMeta) {
43		return true
44	}
45	return s.Name < other.Name
46}
47
48type GatewayConfig struct {
49	AssociatedServiceCount int      `json:",omitempty"`
50	Addresses              []string `json:",omitempty"`
51
52	// internal to track uniqueness
53	addressesSet map[string]struct{}
54}
55
56type ServiceListingSummary struct {
57	ServiceSummary
58
59	ConnectedWithProxy   bool
60	ConnectedWithGateway bool
61}
62
63type ServiceTopologySummary struct {
64	ServiceSummary
65
66	Source    string
67	Intention structs.IntentionDecisionSummary
68}
69
70type ServiceTopology struct {
71	Protocol         string
72	TransparentProxy bool
73	Upstreams        []*ServiceTopologySummary
74	Downstreams      []*ServiceTopologySummary
75	FilteredByACLs   bool
76}
77
78// UINodes is used to list the nodes in a given datacenter. We return a
79// NodeDump which provides overview information for all the nodes
80func (s *HTTPHandlers) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
81	// Parse arguments
82	args := structs.DCSpecificRequest{}
83	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
84		return nil, nil
85	}
86
87	if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
88		return nil, err
89	}
90
91	s.parseFilter(req, &args.Filter)
92
93	// Make the RPC request
94	var out structs.IndexedNodeDump
95	defer setMeta(resp, &out.QueryMeta)
96RPC:
97	if err := s.agent.RPC("Internal.NodeDump", &args, &out); err != nil {
98		// Retry the request allowing stale data if no leader
99		if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
100			args.AllowStale = true
101			goto RPC
102		}
103		return nil, err
104	}
105
106	// Use empty list instead of nil
107	for _, info := range out.Dump {
108		if info.Services == nil {
109			info.Services = make([]*structs.NodeService, 0)
110		}
111		if info.Checks == nil {
112			info.Checks = make([]*structs.HealthCheck, 0)
113		}
114	}
115	if out.Dump == nil {
116		out.Dump = make(structs.NodeDump, 0)
117	}
118	return out.Dump, nil
119}
120
121// UINodeInfo is used to get info on a single node in a given datacenter. We return a
122// NodeInfo which provides overview information for the node
123func (s *HTTPHandlers) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
124	// Parse arguments
125	args := structs.NodeSpecificRequest{}
126	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
127		return nil, nil
128	}
129
130	if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
131		return nil, err
132	}
133
134	// Verify we have some DC, or use the default
135	args.Node = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/node/")
136	if args.Node == "" {
137		resp.WriteHeader(http.StatusBadRequest)
138		fmt.Fprint(resp, "Missing node name")
139		return nil, nil
140	}
141
142	// Make the RPC request
143	var out structs.IndexedNodeDump
144	defer setMeta(resp, &out.QueryMeta)
145RPC:
146	if err := s.agent.RPC("Internal.NodeInfo", &args, &out); err != nil {
147		// Retry the request allowing stale data if no leader
148		if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
149			args.AllowStale = true
150			goto RPC
151		}
152		return nil, err
153	}
154
155	// Return only the first entry
156	if len(out.Dump) > 0 {
157		info := out.Dump[0]
158		if info.Services == nil {
159			info.Services = make([]*structs.NodeService, 0)
160		}
161		if info.Checks == nil {
162			info.Checks = make([]*structs.HealthCheck, 0)
163		}
164		return info, nil
165	}
166
167	resp.WriteHeader(http.StatusNotFound)
168	return nil, nil
169}
170
171// UIServices is used to list the services in a given datacenter. We return a
172// ServiceSummary which provides overview information for the service
173func (s *HTTPHandlers) UIServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
174	// Parse arguments
175	args := structs.ServiceDumpRequest{}
176	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
177		return nil, nil
178	}
179
180	if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
181		return nil, err
182	}
183
184	s.parseFilter(req, &args.Filter)
185
186	// Make the RPC request
187	var out structs.IndexedNodesWithGateways
188	defer setMeta(resp, &out.QueryMeta)
189RPC:
190	if err := s.agent.RPC("Internal.ServiceDump", &args, &out); err != nil {
191		// Retry the request allowing stale data if no leader
192		if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
193			args.AllowStale = true
194			goto RPC
195		}
196		return nil, err
197	}
198
199	// Store the names of the gateways associated with each service
200	var (
201		serviceGateways   = make(map[structs.ServiceName][]structs.ServiceName)
202		numLinkedServices = make(map[structs.ServiceName]int)
203	)
204	for _, gs := range out.Gateways {
205		serviceGateways[gs.Service] = append(serviceGateways[gs.Service], gs.Gateway)
206		numLinkedServices[gs.Gateway] += 1
207	}
208
209	summaries, hasProxy := summarizeServices(out.Nodes.ToServiceDump(), nil, "")
210	sorted := prepSummaryOutput(summaries, false)
211
212	// Ensure at least a zero length slice
213	result := make([]*ServiceListingSummary, 0)
214	for _, svc := range sorted {
215		sum := ServiceListingSummary{ServiceSummary: *svc}
216
217		sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
218		if hasProxy[sn] {
219			sum.ConnectedWithProxy = true
220		}
221
222		// Verify that at least one of the gateways linked by config entry has an instance registered in the catalog
223		for _, gw := range serviceGateways[sn] {
224			if s := summaries[gw]; s != nil && sum.InstanceCount > 0 {
225				sum.ConnectedWithGateway = true
226			}
227		}
228		sum.GatewayConfig.AssociatedServiceCount = numLinkedServices[sn]
229
230		result = append(result, &sum)
231	}
232	return result, nil
233}
234
235// UIGatewayServices is used to query all the nodes for services associated with a gateway along with their gateway config
236func (s *HTTPHandlers) UIGatewayServicesNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
237	// Parse arguments
238	args := structs.ServiceSpecificRequest{}
239	if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil {
240		return nil, err
241	}
242	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
243		return nil, nil
244	}
245
246	// Pull out the service name
247	args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/gateway-services-nodes/")
248	if args.ServiceName == "" {
249		resp.WriteHeader(http.StatusBadRequest)
250		fmt.Fprint(resp, "Missing gateway name")
251		return nil, nil
252	}
253
254	// Make the RPC request
255	var out structs.IndexedServiceDump
256	defer setMeta(resp, &out.QueryMeta)
257RPC:
258	if err := s.agent.RPC("Internal.GatewayServiceDump", &args, &out); err != nil {
259		// Retry the request allowing stale data if no leader
260		if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
261			args.AllowStale = true
262			goto RPC
263		}
264		return nil, err
265	}
266
267	summaries, _ := summarizeServices(out.Dump, s.agent.config, args.Datacenter)
268
269	prepped := prepSummaryOutput(summaries, false)
270	if prepped == nil {
271		prepped = make([]*ServiceSummary, 0)
272	}
273	return prepped, nil
274}
275
276// UIServiceTopology returns the list of upstreams and downstreams for a Connect enabled service.
277//   - Downstreams are services that list the given service as an upstream
278//   - Upstreams are the upstreams defined in the given service's proxy registrations
279func (s *HTTPHandlers) UIServiceTopology(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
280	// Parse arguments
281	args := structs.ServiceSpecificRequest{}
282	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
283		return nil, nil
284	}
285	if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
286		return nil, err
287	}
288
289	args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/service-topology/")
290	if args.ServiceName == "" {
291		resp.WriteHeader(http.StatusBadRequest)
292		fmt.Fprint(resp, "Missing service name")
293		return nil, nil
294	}
295
296	kind, ok := req.URL.Query()["kind"]
297	if !ok {
298		resp.WriteHeader(http.StatusBadRequest)
299		fmt.Fprint(resp, "Missing service kind")
300		return nil, nil
301	}
302	args.ServiceKind = structs.ServiceKind(kind[0])
303
304	switch args.ServiceKind {
305	case structs.ServiceKindTypical, structs.ServiceKindIngressGateway:
306		// allowed
307	default:
308		resp.WriteHeader(http.StatusBadRequest)
309		fmt.Fprintf(resp, "Unsupported service kind %q", args.ServiceKind)
310		return nil, nil
311	}
312
313	// Make the RPC request
314	var out structs.IndexedServiceTopology
315	defer setMeta(resp, &out.QueryMeta)
316RPC:
317	if err := s.agent.RPC("Internal.ServiceTopology", &args, &out); err != nil {
318		// Retry the request allowing stale data if no leader
319		if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
320			args.AllowStale = true
321			goto RPC
322		}
323		return nil, err
324	}
325
326	upstreams, _ := summarizeServices(out.ServiceTopology.Upstreams.ToServiceDump(), nil, "")
327	downstreams, _ := summarizeServices(out.ServiceTopology.Downstreams.ToServiceDump(), nil, "")
328
329	var (
330		upstreamResp   = make([]*ServiceTopologySummary, 0)
331		downstreamResp = make([]*ServiceTopologySummary, 0)
332	)
333
334	// Sort and attach intention data for upstreams and downstreams
335	sortedUpstreams := prepSummaryOutput(upstreams, true)
336	for _, svc := range sortedUpstreams {
337		sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
338		sum := ServiceTopologySummary{
339			ServiceSummary: *svc,
340			Intention:      out.ServiceTopology.UpstreamDecisions[sn.String()],
341			Source:         out.ServiceTopology.UpstreamSources[sn.String()],
342		}
343		upstreamResp = append(upstreamResp, &sum)
344	}
345
346	sortedDownstreams := prepSummaryOutput(downstreams, true)
347	for _, svc := range sortedDownstreams {
348		sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
349		sum := ServiceTopologySummary{
350			ServiceSummary: *svc,
351			Intention:      out.ServiceTopology.DownstreamDecisions[sn.String()],
352			Source:         out.ServiceTopology.DownstreamSources[sn.String()],
353		}
354		downstreamResp = append(downstreamResp, &sum)
355	}
356
357	topo := ServiceTopology{
358		TransparentProxy: out.ServiceTopology.TransparentProxy,
359		Protocol:         out.ServiceTopology.MetricsProtocol,
360		Upstreams:        upstreamResp,
361		Downstreams:      downstreamResp,
362		FilteredByACLs:   out.FilteredByACLs,
363	}
364	return topo, nil
365}
366
367func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc string) (map[structs.ServiceName]*ServiceSummary, map[structs.ServiceName]bool) {
368	var (
369		summary  = make(map[structs.ServiceName]*ServiceSummary)
370		hasProxy = make(map[structs.ServiceName]bool)
371	)
372
373	getService := func(service structs.ServiceName) *ServiceSummary {
374		serv, ok := summary[service]
375		if !ok {
376			serv = &ServiceSummary{
377				Name:           service.Name,
378				EnterpriseMeta: service.EnterpriseMeta,
379				// the other code will increment this unconditionally so we
380				// shouldn't initialize it to 1
381				InstanceCount: 0,
382			}
383			summary[service] = serv
384		}
385		return serv
386	}
387
388	for _, csn := range dump {
389		if cfg != nil && csn.GatewayService != nil {
390			gwsvc := csn.GatewayService
391			sum := getService(gwsvc.Service)
392			modifySummaryForGatewayService(cfg, dc, sum, gwsvc)
393		}
394
395		// Will happen in cases where we only have the GatewayServices mapping
396		if csn.Service == nil {
397			continue
398		}
399		sn := structs.NewServiceName(csn.Service.Service, &csn.Service.EnterpriseMeta)
400		sum := getService(sn)
401
402		svc := csn.Service
403		sum.Nodes = append(sum.Nodes, csn.Node.Node)
404		sum.Kind = svc.Kind
405		sum.Datacenter = csn.Node.Datacenter
406		sum.InstanceCount += 1
407		if svc.Kind == structs.ServiceKindConnectProxy {
408			sn := structs.NewServiceName(svc.Proxy.DestinationServiceName, &svc.EnterpriseMeta)
409			hasProxy[sn] = true
410
411			destination := getService(sn)
412			for _, check := range csn.Checks {
413				cid := structs.NewCheckID(check.CheckID, &check.EnterpriseMeta)
414				uid := structs.UniqueID(csn.Node.Node, cid.String())
415				if destination.checks == nil {
416					destination.checks = make(map[string]*structs.HealthCheck)
417				}
418				destination.checks[uid] = check
419			}
420
421			// Only consider the target service to be transparent when all its proxy instances are in that mode.
422			// This is done because the flag is used to display warnings about proxies needing to enable
423			// transparent proxy mode. If ANY instance isn't in the right mode then the warming applies.
424			if svc.Proxy.Mode == structs.ProxyModeTransparent && !destination.transparentProxySet {
425				destination.TransparentProxy = true
426			}
427			if svc.Proxy.Mode != structs.ProxyModeTransparent {
428				destination.TransparentProxy = false
429			}
430			destination.transparentProxySet = true
431		}
432		for _, tag := range svc.Tags {
433			found := false
434			for _, existing := range sum.Tags {
435				if existing == tag {
436					found = true
437					break
438				}
439			}
440			if !found {
441				sum.Tags = append(sum.Tags, tag)
442			}
443		}
444
445		// If there is an external source, add it to the list of external
446		// sources. We only want to add unique sources so there is extra
447		// accounting here with an unexported field to maintain the set
448		// of sources.
449		if len(svc.Meta) > 0 && svc.Meta[structs.MetaExternalSource] != "" {
450			source := svc.Meta[structs.MetaExternalSource]
451			if sum.externalSourceSet == nil {
452				sum.externalSourceSet = make(map[string]struct{})
453			}
454			if _, ok := sum.externalSourceSet[source]; !ok {
455				sum.externalSourceSet[source] = struct{}{}
456				sum.ExternalSources = append(sum.ExternalSources, source)
457			}
458		}
459
460		for _, check := range csn.Checks {
461			cid := structs.NewCheckID(check.CheckID, &check.EnterpriseMeta)
462			uid := structs.UniqueID(csn.Node.Node, cid.String())
463			if sum.checks == nil {
464				sum.checks = make(map[string]*structs.HealthCheck)
465			}
466			sum.checks[uid] = check
467		}
468	}
469
470	return summary, hasProxy
471}
472
473func prepSummaryOutput(summaries map[structs.ServiceName]*ServiceSummary, excludeSidecars bool) []*ServiceSummary {
474	var resp []*ServiceSummary
475	// Ensure at least a zero length slice
476	resp = make([]*ServiceSummary, 0)
477
478	// Collect and sort resp for display
479	for _, sum := range summaries {
480		sort.Strings(sum.Nodes)
481		sort.Strings(sum.Tags)
482
483		for _, chk := range sum.checks {
484			switch chk.Status {
485			case api.HealthPassing:
486				sum.ChecksPassing++
487			case api.HealthWarning:
488				sum.ChecksWarning++
489			case api.HealthCritical:
490				sum.ChecksCritical++
491			}
492		}
493		if excludeSidecars && sum.Kind != structs.ServiceKindTypical && sum.Kind != structs.ServiceKindIngressGateway {
494			continue
495		}
496		resp = append(resp, sum)
497	}
498	sort.Slice(resp, func(i, j int) bool {
499		return resp[i].LessThan(resp[j])
500	})
501	return resp
502}
503
504func modifySummaryForGatewayService(
505	cfg *config.RuntimeConfig,
506	datacenter string,
507	sum *ServiceSummary,
508	gwsvc *structs.GatewayService,
509) {
510	var dnsAddresses []string
511	for _, domain := range []string{cfg.DNSDomain, cfg.DNSAltDomain} {
512		// If the domain is empty, do not use it to construct a valid DNS
513		// address
514		if domain == "" {
515			continue
516		}
517		dnsAddresses = append(dnsAddresses, serviceIngressDNSName(
518			gwsvc.Service.Name,
519			datacenter,
520			domain,
521			&gwsvc.Service.EnterpriseMeta,
522		))
523	}
524
525	for _, addr := range gwsvc.Addresses(dnsAddresses) {
526		// check for duplicates, a service will have a ServiceInfo struct for
527		// every instance that is registered.
528		if _, ok := sum.GatewayConfig.addressesSet[addr]; !ok {
529			if sum.GatewayConfig.addressesSet == nil {
530				sum.GatewayConfig.addressesSet = make(map[string]struct{})
531			}
532			sum.GatewayConfig.addressesSet[addr] = struct{}{}
533			sum.GatewayConfig.Addresses = append(
534				sum.GatewayConfig.Addresses, addr,
535			)
536		}
537	}
538}
539
540// GET /v1/internal/ui/gateway-intentions/:gateway
541func (s *HTTPHandlers) UIGatewayIntentions(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
542	var args structs.IntentionQueryRequest
543	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
544		return nil, nil
545	}
546
547	var entMeta structs.EnterpriseMeta
548	if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
549		return nil, err
550	}
551
552	// Pull out the service name
553	name := strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/gateway-intentions/")
554	if name == "" {
555		resp.WriteHeader(http.StatusBadRequest)
556		fmt.Fprint(resp, "Missing gateway name")
557		return nil, nil
558	}
559	args.Match = &structs.IntentionQueryMatch{
560		Type: structs.IntentionMatchDestination,
561		Entries: []structs.IntentionMatchEntry{
562			{
563				Namespace: entMeta.NamespaceOrEmpty(),
564				Name:      name,
565			},
566		},
567	}
568
569	var reply structs.IndexedIntentions
570
571	defer setMeta(resp, &reply.QueryMeta)
572	if err := s.agent.RPC("Internal.GatewayIntentions", args, &reply); err != nil {
573		return nil, err
574	}
575
576	return reply.Intentions, nil
577}
578
579// UIMetricsProxy handles the /v1/internal/ui/metrics-proxy/ endpoint which, if
580// configured, provides a simple read-only HTTP proxy to a single metrics
581// backend to expose it to the UI.
582func (s *HTTPHandlers) UIMetricsProxy(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
583	// Check the UI was enabled at agent startup (note this is not reloadable
584	// currently).
585	if !s.IsUIEnabled() {
586		return nil, NotFoundError{Reason: "UI is not enabled"}
587	}
588
589	// Load reloadable proxy config
590	cfg, ok := s.metricsProxyCfg.Load().(config.UIMetricsProxy)
591	if !ok || cfg.BaseURL == "" {
592		// Proxy not configured
593		return nil, NotFoundError{Reason: "Metrics proxy is not enabled"}
594	}
595
596	// Fetch the ACL token, if provided, but ONLY from headers since other
597	// metrics proxies might use a ?token query string parameter for something.
598	var token string
599	s.parseTokenFromHeaders(req, &token)
600
601	// Clear the token from the headers so we don't end up proxying it.
602	s.clearTokenFromHeaders(req)
603
604	var entMeta structs.EnterpriseMeta
605	authz, err := s.agent.delegate.ResolveTokenAndDefaultMeta(token, &entMeta, nil)
606	if err != nil {
607		return nil, err
608	}
609
610	if authz != nil {
611		// This endpoint requires wildcard read on all services and all nodes.
612		//
613		// In enterprise it requires this _in all namespaces_ too.
614		wildMeta := structs.WildcardEnterpriseMeta()
615		var authzContext acl.AuthorizerContext
616		wildMeta.FillAuthzContext(&authzContext)
617
618		if authz.NodeReadAll(&authzContext) != acl.Allow || authz.ServiceReadAll(&authzContext) != acl.Allow {
619			return nil, acl.ErrPermissionDenied
620		}
621	}
622
623	log := s.agent.logger.Named(logging.UIMetricsProxy)
624
625	// Construct the new URL from the path and the base path. Note we do this here
626	// not in the Director function below because we can handle any errors cleanly
627	// here.
628
629	// Replace prefix in the path
630	subPath := strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/metrics-proxy")
631
632	// Append that to the BaseURL (which might contain a path prefix component)
633	newURL := cfg.BaseURL + subPath
634
635	// Parse it into a new URL
636	u, err := url.Parse(newURL)
637	if err != nil {
638		log.Error("couldn't parse target URL", "base_url", cfg.BaseURL, "path", subPath)
639		return nil, BadRequestError{Reason: "Invalid path."}
640	}
641
642	// Clean the new URL path to prevent path traversal attacks and remove any
643	// double slashes etc.
644	u.Path = path.Clean(u.Path)
645
646	if len(cfg.PathAllowlist) > 0 {
647		// This could be done better with a map, but for the prometheus default
648		// integration this list has two items in it, so the straight iteration
649		// isn't awful.
650		denied := true
651		for _, allowedPath := range cfg.PathAllowlist {
652			if u.Path == allowedPath {
653				denied = false
654				break
655			}
656		}
657		if denied {
658			log.Error("target URL path is not allowed",
659				"base_url", cfg.BaseURL,
660				"path", subPath,
661				"target_url", u.String(),
662				"path_allowlist", cfg.PathAllowlist,
663			)
664			resp.WriteHeader(http.StatusForbidden)
665			return nil, nil
666		}
667	}
668
669	// Pass through query params
670	u.RawQuery = req.URL.RawQuery
671
672	// Validate that the full BaseURL is still a prefix - if there was a path
673	// prefix on the BaseURL but an attacker tried to circumvent it with path
674	// traversal then the Clean above would have resolve the /../ components back
675	// to the actual path which means part of the prefix will now be missing.
676	//
677	// Note that in practice this is not currently possible since any /../ in the
678	// path would have already been resolved by the API server mux and so not even
679	// hit this handler. Any /../ that are far enough into the path to hit this
680	// handler, can't backtrack far enough to eat into the BaseURL either. But we
681	// leave this in anyway in case something changes in the future.
682	if !strings.HasPrefix(u.String(), cfg.BaseURL) {
683		log.Error("target URL escaped from base path",
684			"base_url", cfg.BaseURL,
685			"path", subPath,
686			"target_url", u.String(),
687		)
688		return nil, BadRequestError{Reason: "Invalid path."}
689	}
690
691	// Add any configured headers
692	for _, h := range cfg.AddHeaders {
693		req.Header.Set(h.Name, h.Value)
694	}
695
696	log.Debug("proxying request", "to", u.String())
697
698	proxy := httputil.ReverseProxy{
699		Director: func(r *http.Request) {
700			r.URL = u
701		},
702		ErrorLog: log.StandardLogger(&hclog.StandardLoggerOptions{
703			InferLevels: true,
704		}),
705	}
706
707	proxy.ServeHTTP(resp, req)
708	return nil, nil
709}
710