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