github.com/hernad/nomad@v1.6.112/command/agent/http.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package agent 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/tls" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "net" 14 "net/http" 15 "net/http/pprof" 16 "os" 17 "strconv" 18 "strings" 19 "sync" 20 "time" 21 22 "github.com/armon/go-metrics" 23 assetfs "github.com/elazarl/go-bindata-assetfs" 24 "github.com/gorilla/handlers" 25 "github.com/gorilla/websocket" 26 "github.com/hashicorp/go-connlimit" 27 log "github.com/hashicorp/go-hclog" 28 "github.com/hashicorp/go-msgpack/codec" 29 multierror "github.com/hashicorp/go-multierror" 30 "github.com/rs/cors" 31 "golang.org/x/time/rate" 32 33 "github.com/hernad/nomad/acl" 34 "github.com/hernad/nomad/client" 35 "github.com/hernad/nomad/command/agent/event" 36 "github.com/hernad/nomad/helper/noxssrw" 37 "github.com/hernad/nomad/helper/tlsutil" 38 "github.com/hernad/nomad/nomad" 39 "github.com/hernad/nomad/nomad/structs" 40 ) 41 42 const ( 43 // ErrInvalidMethod is used if the HTTP method is not supported 44 ErrInvalidMethod = "Invalid method" 45 46 // ErrEntOnly is the error returned if accessing an enterprise only 47 // endpoint 48 ErrEntOnly = "Nomad Enterprise only endpoint" 49 50 // ErrServerOnly is the error text returned if accessing a server only 51 // endpoint 52 ErrServerOnly = "Server only endpoint" 53 54 // ContextKeyReqID is a unique ID for a given request 55 ContextKeyReqID = "requestID" 56 57 // MissingRequestID is a placeholder if we cannot retrieve a request 58 // UUID from context 59 MissingRequestID = "<missing request id>" 60 ) 61 62 var ( 63 // Set to false by stub_asset if the ui build tag isn't enabled 64 uiEnabled = true 65 66 // Displayed when ui is disabled, but overridden if the ui build 67 // tag isn't enabled 68 stubHTML = "<html><p>Nomad UI is disabled</p></html>" 69 70 // allowCORSWithMethods sets permissive CORS headers for a handler, used by 71 // wrapCORS and wrapCORSWithMethods 72 allowCORSWithMethods = func(methods ...string) *cors.Cors { 73 return cors.New(cors.Options{ 74 AllowedOrigins: []string{"*"}, 75 AllowedMethods: methods, 76 AllowedHeaders: []string{"*"}, 77 AllowCredentials: true, 78 }) 79 } 80 ) 81 82 type handlerFn func(resp http.ResponseWriter, req *http.Request) (interface{}, error) 83 type handlerByteFn func(resp http.ResponseWriter, req *http.Request) ([]byte, error) 84 85 type RPCer interface { 86 RPC(string, any, any) error 87 Server() *nomad.Server 88 Client() *client.Client 89 Stats() map[string]map[string]string 90 GetConfig() *Config 91 GetMetricsSink() *metrics.InmemSink 92 } 93 94 // HTTPServer is used to wrap an Agent and expose it over an HTTP interface 95 type HTTPServer struct { 96 agent RPCer 97 98 // eventAuditor is the enterprise audit log feature which is needed by the 99 // HTTP server. 100 eventAuditor event.Auditor 101 102 mux *http.ServeMux 103 listener net.Listener 104 listenerCh chan struct{} 105 logger log.Logger 106 Addr string 107 108 wsUpgrader *websocket.Upgrader 109 } 110 111 // NewHTTPServers starts an HTTP server for every address.http configured in 112 // the agent. 113 func NewHTTPServers(agent *Agent, config *Config) ([]*HTTPServer, error) { 114 var srvs []*HTTPServer 115 var serverInitializationErrors error 116 117 // Get connection handshake timeout limit 118 handshakeTimeout, err := time.ParseDuration(config.Limits.HTTPSHandshakeTimeout) 119 if err != nil { 120 return srvs, fmt.Errorf("error parsing https_handshake_timeout: %v", err) 121 } else if handshakeTimeout < 0 { 122 return srvs, fmt.Errorf("https_handshake_timeout must be >= 0") 123 } 124 125 // Get max connection limit 126 maxConns := 0 127 if mc := config.Limits.HTTPMaxConnsPerClient; mc != nil { 128 maxConns = *mc 129 } 130 if maxConns < 0 { 131 return srvs, fmt.Errorf("http_max_conns_per_client must be >= 0") 132 } 133 134 tlsConf, err := tlsutil.NewTLSConfiguration(config.TLSConfig, config.TLSConfig.VerifyHTTPSClient, true) 135 if err != nil && config.TLSConfig.EnableHTTP { 136 return srvs, fmt.Errorf("failed to initialize HTTP server TLS configuration: %s", err) 137 } 138 139 wsUpgrader := &websocket.Upgrader{ 140 ReadBufferSize: 2048, 141 WriteBufferSize: 2048, 142 } 143 144 // Start the listener 145 for _, addr := range config.normalizedAddrs.HTTP { 146 lnAddr, err := net.ResolveTCPAddr("tcp", addr) 147 if err != nil { 148 serverInitializationErrors = multierror.Append(serverInitializationErrors, err) 149 continue 150 } 151 ln, err := config.Listener("tcp", lnAddr.IP.String(), lnAddr.Port) 152 if err != nil { 153 serverInitializationErrors = multierror.Append(serverInitializationErrors, fmt.Errorf("failed to start HTTP listener: %v", err)) 154 continue 155 } 156 157 // If TLS is enabled, wrap the listener with a TLS listener 158 if config.TLSConfig.EnableHTTP { 159 tlsConfig, err := tlsConf.IncomingTLSConfig() 160 if err != nil { 161 serverInitializationErrors = multierror.Append(serverInitializationErrors, err) 162 continue 163 } 164 ln = tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, tlsConfig) 165 } 166 167 // Create the server 168 srv := &HTTPServer{ 169 agent: agent, 170 eventAuditor: agent.auditor, 171 mux: http.NewServeMux(), 172 listener: ln, 173 listenerCh: make(chan struct{}), 174 logger: agent.httpLogger, 175 Addr: ln.Addr().String(), 176 wsUpgrader: wsUpgrader, 177 } 178 srv.registerHandlers(config.EnableDebug) 179 180 // Create HTTP server with timeouts 181 httpServer := http.Server{ 182 Addr: srv.Addr, 183 Handler: handlers.CompressHandler(srv.mux), 184 ConnState: makeConnState(config.TLSConfig.EnableHTTP, handshakeTimeout, maxConns, srv.logger), 185 ErrorLog: newHTTPServerLogger(srv.logger), 186 } 187 188 go func() { 189 defer close(srv.listenerCh) 190 httpServer.Serve(ln) 191 }() 192 193 srvs = append(srvs, srv) 194 } 195 196 // Return early on errors 197 if serverInitializationErrors != nil { 198 for _, srv := range srvs { 199 srv.Shutdown() 200 } 201 202 return srvs, serverInitializationErrors 203 } 204 205 // This HTTP server is only created when running in client mode, otherwise 206 // the builtinDialer and builtinListener will be nil. 207 if agent.builtinDialer != nil && agent.builtinListener != nil { 208 srv := &HTTPServer{ 209 agent: agent, 210 eventAuditor: agent.auditor, 211 mux: http.NewServeMux(), 212 listener: agent.builtinListener, 213 listenerCh: make(chan struct{}), 214 logger: agent.httpLogger, 215 Addr: "builtin", 216 wsUpgrader: wsUpgrader, 217 } 218 219 srv.registerHandlers(config.EnableDebug) 220 221 // builtinServer adds a wrapper to always authenticate requests 222 httpServer := http.Server{ 223 Addr: srv.Addr, 224 Handler: newAuthMiddleware(srv, srv.mux), 225 ErrorLog: newHTTPServerLogger(srv.logger), 226 } 227 228 agent.taskAPIServer.SetServer(&httpServer) 229 230 go func() { 231 defer close(srv.listenerCh) 232 httpServer.Serve(agent.builtinListener) 233 }() 234 235 // Don't append builtin servers to srvs as they don't need to be reloaded 236 // when TLS changes. This does mean they need to be shutdown independently. 237 } 238 239 return srvs, nil 240 } 241 242 // makeConnState returns a ConnState func for use in an http.Server. If 243 // isTLS=true and handshakeTimeout>0 then the handshakeTimeout will be applied 244 // as a connection deadline to new connections and removed when the connection 245 // is active (meaning it has successfully completed the TLS handshake). 246 // 247 // If limit > 0, a per-address connection limit will be enabled regardless of 248 // TLS. If connLimit == 0 there is no connection limit. 249 func makeConnState(isTLS bool, handshakeTimeout time.Duration, connLimit int, logger log.Logger) func(conn net.Conn, state http.ConnState) { 250 connLimiter := connLimiter(connLimit, logger) 251 if !isTLS || handshakeTimeout == 0 { 252 if connLimit > 0 { 253 // Still return the connection limiter 254 return connLimiter 255 } 256 return nil 257 } 258 259 if connLimit > 0 { 260 // Return conn state callback with connection limiting and a 261 // handshake timeout. 262 263 return func(conn net.Conn, state http.ConnState) { 264 switch state { 265 case http.StateNew: 266 // Set deadline to prevent slow send before TLS handshake or first 267 // byte of request. 268 conn.SetDeadline(time.Now().Add(handshakeTimeout)) 269 case http.StateActive: 270 // Clear read deadline. We should maybe set read timeouts more 271 // generally but that's a bigger task as some HTTP endpoints may 272 // stream large requests and responses (e.g. snapshot) so we can't 273 // set sensible blanket timeouts here. 274 conn.SetDeadline(time.Time{}) 275 } 276 277 // Call connection limiter 278 connLimiter(conn, state) 279 } 280 } 281 282 // Return conn state callback with just a handshake timeout 283 // (connection limiting disabled). 284 return func(conn net.Conn, state http.ConnState) { 285 switch state { 286 case http.StateNew: 287 // Set deadline to prevent slow send before TLS handshake or first 288 // byte of request. 289 conn.SetDeadline(time.Now().Add(handshakeTimeout)) 290 case http.StateActive: 291 // Clear read deadline. We should maybe set read timeouts more 292 // generally but that's a bigger task as some HTTP endpoints may 293 // stream large requests and responses (e.g. snapshot) so we can't 294 // set sensible blanket timeouts here. 295 conn.SetDeadline(time.Time{}) 296 } 297 } 298 } 299 300 // connLimiter returns a connection-limiter function with a rate-limited 429-response error handler. 301 // The rate-limit prevents the TLS handshake necessary to write the HTTP response 302 // from consuming too many server resources. 303 func connLimiter(connLimit int, logger log.Logger) func(conn net.Conn, state http.ConnState) { 304 // Global rate-limit of 10 responses per second with a 100-response burst. 305 limiter := rate.NewLimiter(10, 100) 306 307 tooManyConnsMsg := "Your IP is issuing too many concurrent connections, please rate limit your calls\n" 308 tooManyRequestsResponse := []byte(fmt.Sprintf("HTTP/1.1 429 Too Many Requests\r\n"+ 309 "Content-Type: text/plain\r\n"+ 310 "Content-Length: %d\r\n"+ 311 "Connection: close\r\n\r\n%s", len(tooManyConnsMsg), tooManyConnsMsg)) 312 return connlimit.NewLimiter(connlimit.Config{ 313 MaxConnsPerClientIP: connLimit, 314 }).HTTPConnStateFuncWithErrorHandler(func(err error, conn net.Conn) { 315 if err == connlimit.ErrPerClientIPLimitReached { 316 metrics.IncrCounter([]string{"nomad", "agent", "http", "exceeded"}, 1) 317 if n := limiter.Reserve(); n.Delay() == 0 { 318 logger.Warn("Too many concurrent connections", "address", conn.RemoteAddr().String(), "limit", connLimit) 319 conn.SetDeadline(time.Now().Add(10 * time.Millisecond)) 320 conn.Write(tooManyRequestsResponse) 321 } else { 322 n.Cancel() 323 } 324 } 325 conn.Close() 326 }) 327 } 328 329 // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted 330 // connections. It's used by NewHttpServer so 331 // dead TCP connections eventually go away. 332 type tcpKeepAliveListener struct { 333 *net.TCPListener 334 } 335 336 func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 337 tc, err := ln.AcceptTCP() 338 if err != nil { 339 return 340 } 341 tc.SetKeepAlive(true) 342 tc.SetKeepAlivePeriod(30 * time.Second) 343 return tc, nil 344 } 345 346 // Shutdown is used to shutdown the HTTP server 347 func (s *HTTPServer) Shutdown() { 348 if s != nil { 349 s.logger.Debug("shutting down http server") 350 s.listener.Close() 351 <-s.listenerCh // block until http.Serve has returned. 352 } 353 } 354 355 // ResolveToken extracts the ACL token secret ID from the request and 356 // translates it into an ACL object. Returns nil if ACLs are disabled. 357 func (s *HTTPServer) ResolveToken(req *http.Request) (*acl.ACL, error) { 358 var secret string 359 s.parseToken(req, &secret) 360 361 var aclObj *acl.ACL 362 var err error 363 364 if srv := s.agent.Server(); srv != nil { 365 aclObj, err = srv.ResolveToken(secret) 366 } else { 367 // Not a Server, so use the Client for token resolution. Note 368 // this gets forwarded to a server with AllowStale = true if 369 // the local ACL cache TTL has expired (30s by default) 370 aclObj, err = s.agent.Client().ResolveToken(secret) 371 } 372 373 if err != nil { 374 return nil, fmt.Errorf("failed to resolve ACL token: %v", err) 375 } 376 377 return aclObj, nil 378 } 379 380 // registerHandlers is used to attach our handlers to the mux 381 func (s *HTTPServer) registerHandlers(enableDebug bool) { 382 s.mux.HandleFunc("/v1/jobs", s.wrap(s.JobsRequest)) 383 s.mux.HandleFunc("/v1/jobs/parse", s.wrap(s.JobsParseRequest)) 384 s.mux.HandleFunc("/v1/job/", s.wrap(s.JobSpecificRequest)) 385 386 s.mux.HandleFunc("/v1/nodes", s.wrap(s.NodesRequest)) 387 s.mux.HandleFunc("/v1/node/", s.wrap(s.NodeSpecificRequest)) 388 389 s.mux.HandleFunc("/v1/node/pools", s.wrap(s.NodePoolsRequest)) 390 s.mux.HandleFunc("/v1/node/pool/", s.wrap(s.NodePoolSpecificRequest)) 391 392 s.mux.HandleFunc("/v1/allocations", s.wrap(s.AllocsRequest)) 393 s.mux.HandleFunc("/v1/allocation/", s.wrap(s.AllocSpecificRequest)) 394 395 s.mux.HandleFunc("/v1/evaluations", s.wrap(s.EvalsRequest)) 396 s.mux.HandleFunc("/v1/evaluations/count", s.wrap(s.EvalsCountRequest)) 397 s.mux.HandleFunc("/v1/evaluation/", s.wrap(s.EvalSpecificRequest)) 398 399 s.mux.HandleFunc("/v1/deployments", s.wrap(s.DeploymentsRequest)) 400 s.mux.HandleFunc("/v1/deployment/", s.wrap(s.DeploymentSpecificRequest)) 401 402 s.mux.HandleFunc("/v1/volumes", s.wrap(s.CSIVolumesRequest)) 403 s.mux.HandleFunc("/v1/volumes/external", s.wrap(s.CSIExternalVolumesRequest)) 404 s.mux.HandleFunc("/v1/volumes/snapshot", s.wrap(s.CSISnapshotsRequest)) 405 s.mux.HandleFunc("/v1/volume/csi/", s.wrap(s.CSIVolumeSpecificRequest)) 406 s.mux.HandleFunc("/v1/plugins", s.wrap(s.CSIPluginsRequest)) 407 s.mux.HandleFunc("/v1/plugin/csi/", s.wrap(s.CSIPluginSpecificRequest)) 408 409 s.mux.HandleFunc("/v1/acl/policies", s.wrap(s.ACLPoliciesRequest)) 410 s.mux.HandleFunc("/v1/acl/policy/", s.wrap(s.ACLPolicySpecificRequest)) 411 412 s.mux.HandleFunc("/v1/acl/token/onetime", s.wrap(s.UpsertOneTimeToken)) 413 s.mux.HandleFunc("/v1/acl/token/onetime/exchange", s.wrap(s.ExchangeOneTimeToken)) 414 s.mux.HandleFunc("/v1/acl/bootstrap", s.wrap(s.ACLTokenBootstrap)) 415 s.mux.HandleFunc("/v1/acl/tokens", s.wrap(s.ACLTokensRequest)) 416 s.mux.HandleFunc("/v1/acl/token", s.wrap(s.ACLTokenSpecificRequest)) 417 s.mux.HandleFunc("/v1/acl/token/", s.wrap(s.ACLTokenSpecificRequest)) 418 419 // Register our ACL role handlers. 420 s.mux.HandleFunc("/v1/acl/roles", s.wrap(s.ACLRoleListRequest)) 421 s.mux.HandleFunc("/v1/acl/role", s.wrap(s.ACLRoleRequest)) 422 s.mux.HandleFunc("/v1/acl/role/", s.wrap(s.ACLRoleSpecificRequest)) 423 424 // Register our ACL auth-method handlers. 425 s.mux.HandleFunc("/v1/acl/auth-methods", s.wrap(s.ACLAuthMethodListRequest)) 426 s.mux.HandleFunc("/v1/acl/auth-method", s.wrap(s.ACLAuthMethodRequest)) 427 s.mux.HandleFunc("/v1/acl/auth-method/", s.wrap(s.ACLAuthMethodSpecificRequest)) 428 429 // Register our ACL binding rule handlers. 430 s.mux.HandleFunc("/v1/acl/binding-rules", s.wrap(s.ACLBindingRuleListRequest)) 431 s.mux.HandleFunc("/v1/acl/binding-rule", s.wrap(s.ACLBindingRuleRequest)) 432 s.mux.HandleFunc("/v1/acl/binding-rule/", s.wrap(s.ACLBindingRuleSpecificRequest)) 433 434 // Register out ACL OIDC SSO and auth handlers. 435 s.mux.HandleFunc("/v1/acl/oidc/auth-url", s.wrap(s.ACLOIDCAuthURLRequest)) 436 s.mux.HandleFunc("/v1/acl/oidc/complete-auth", s.wrap(s.ACLOIDCCompleteAuthRequest)) 437 s.mux.HandleFunc("/v1/acl/login", s.wrap(s.ACLLoginRequest)) 438 439 s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest))) 440 s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest)) 441 s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest))) 442 s.mux.Handle("/v1/client/allocation/", wrapCORS(s.wrap(s.ClientAllocRequest))) 443 s.mux.Handle("/v1/client/metadata", wrapCORS(s.wrap(s.NodeMetaRequest))) 444 445 s.mux.HandleFunc("/v1/agent/self", s.wrap(s.AgentSelfRequest)) 446 s.mux.HandleFunc("/v1/agent/join", s.wrap(s.AgentJoinRequest)) 447 s.mux.HandleFunc("/v1/agent/members", s.wrap(s.AgentMembersRequest)) 448 s.mux.HandleFunc("/v1/agent/force-leave", s.wrap(s.AgentForceLeaveRequest)) 449 s.mux.HandleFunc("/v1/agent/servers", s.wrap(s.AgentServersRequest)) 450 s.mux.HandleFunc("/v1/agent/schedulers", s.wrap(s.AgentSchedulerWorkerInfoRequest)) 451 s.mux.HandleFunc("/v1/agent/schedulers/config", s.wrap(s.AgentSchedulerWorkerConfigRequest)) 452 s.mux.HandleFunc("/v1/agent/keyring/", s.wrap(s.KeyringOperationRequest)) 453 s.mux.HandleFunc("/v1/agent/health", s.wrap(s.HealthRequest)) 454 s.mux.HandleFunc("/v1/agent/host", s.wrap(s.AgentHostRequest)) 455 456 // Register our service registration handlers. 457 s.mux.HandleFunc("/v1/services", s.wrap(s.ServiceRegistrationListRequest)) 458 s.mux.HandleFunc("/v1/service/", s.wrap(s.ServiceRegistrationRequest)) 459 460 // Monitor is *not* an untrusted endpoint despite the log contents 461 // potentially containing unsanitized user input. Monitor, like 462 // "/v1/client/fs/logs", explicitly sets a "text/plain" or 463 // "application/json" Content-Type depending on the ?plain= query 464 // parameter. 465 s.mux.HandleFunc("/v1/agent/monitor", s.wrap(s.AgentMonitor)) 466 467 s.mux.HandleFunc("/v1/agent/pprof/", s.wrapNonJSON(s.AgentPprofRequest)) 468 469 s.mux.HandleFunc("/v1/metrics", s.wrap(s.MetricsRequest)) 470 471 s.mux.HandleFunc("/v1/validate/job", s.wrap(s.ValidateJobRequest)) 472 473 s.mux.HandleFunc("/v1/regions", s.wrap(s.RegionListRequest)) 474 475 s.mux.HandleFunc("/v1/scaling/policies", s.wrap(s.ScalingPoliciesRequest)) 476 s.mux.HandleFunc("/v1/scaling/policy/", s.wrap(s.ScalingPolicySpecificRequest)) 477 478 s.mux.HandleFunc("/v1/status/leader", s.wrap(s.StatusLeaderRequest)) 479 s.mux.HandleFunc("/v1/status/peers", s.wrap(s.StatusPeersRequest)) 480 481 s.mux.HandleFunc("/v1/search/fuzzy", s.wrap(s.FuzzySearchRequest)) 482 s.mux.HandleFunc("/v1/search", s.wrap(s.SearchRequest)) 483 s.mux.HandleFunc("/v1/operator/license", s.wrap(s.LicenseRequest)) 484 s.mux.HandleFunc("/v1/operator/raft/", s.wrap(s.OperatorRequest)) 485 s.mux.HandleFunc("/v1/operator/keyring/", s.wrap(s.KeyringRequest)) 486 s.mux.HandleFunc("/v1/operator/autopilot/configuration", s.wrap(s.OperatorAutopilotConfiguration)) 487 s.mux.HandleFunc("/v1/operator/autopilot/health", s.wrap(s.OperatorServerHealth)) 488 s.mux.HandleFunc("/v1/operator/snapshot", s.wrap(s.SnapshotRequest)) 489 490 s.mux.HandleFunc("/v1/system/gc", s.wrap(s.GarbageCollectRequest)) 491 s.mux.HandleFunc("/v1/system/reconcile/summaries", s.wrap(s.ReconcileJobSummaries)) 492 493 s.mux.HandleFunc("/v1/operator/scheduler/configuration", s.wrap(s.OperatorSchedulerConfiguration)) 494 495 s.mux.HandleFunc("/v1/event/stream", s.wrap(s.EventStream)) 496 497 s.mux.HandleFunc("/v1/namespaces", s.wrap(s.NamespacesRequest)) 498 s.mux.HandleFunc("/v1/namespace", s.wrap(s.NamespaceCreateRequest)) 499 s.mux.HandleFunc("/v1/namespace/", s.wrap(s.NamespaceSpecificRequest)) 500 501 s.mux.Handle("/v1/vars", wrapCORS(s.wrap(s.VariablesListRequest))) 502 s.mux.Handle("/v1/var/", wrapCORSWithAllowedMethods(s.wrap(s.VariableSpecificRequest), "HEAD", "GET", "PUT", "DELETE")) 503 504 agentConfig := s.agent.GetConfig() 505 uiConfigEnabled := agentConfig.UI != nil && agentConfig.UI.Enabled 506 507 if uiEnabled && uiConfigEnabled { 508 s.mux.Handle("/ui/", http.StripPrefix("/ui/", s.handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()})))) 509 s.logger.Debug("UI is enabled") 510 } else { 511 // Write the stubHTML 512 s.mux.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) { 513 w.Write([]byte(stubHTML)) 514 }) 515 if uiEnabled && !uiConfigEnabled { 516 s.logger.Warn("UI is disabled") 517 } else { 518 s.logger.Debug("UI is disabled in this build") 519 } 520 } 521 s.mux.Handle("/", s.handleRootFallthrough()) 522 523 if enableDebug { 524 if !agentConfig.DevMode { 525 s.logger.Warn("enable_debug is set to true. This is insecure and should not be enabled in production") 526 } 527 s.mux.HandleFunc("/debug/pprof/", pprof.Index) 528 s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 529 s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 530 s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 531 s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 532 } 533 534 // Register enterprise endpoints. 535 s.registerEnterpriseHandlers() 536 } 537 538 // builtinAPI is a wrapper around serving the HTTP API to arbitrary listeners 539 // such as the Task API. It is necessary because the HTTP servers are created 540 // *after* the client has been initialized, so this wrapper blocks Serve 541 // requests from task api hooks until the HTTP server is setup and ready to 542 // accept from new listeners. 543 // 544 // bufconndialer provides similar functionality to consul-template except it 545 // satisfies the Dialer API as opposed to the Serve(Listener) API. 546 type builtinAPI struct { 547 // srvReadyCh is closed when srv is ready 548 srvReadyCh chan struct{} 549 550 // srv is a builtin http server. Must lock around setting as it could happen 551 // concurrently with shutting down. 552 srv *http.Server 553 srvLock sync.Mutex 554 } 555 556 func newBuiltinAPI() *builtinAPI { 557 return &builtinAPI{ 558 srvReadyCh: make(chan struct{}), 559 } 560 } 561 562 // SetServer sets the API HTTP server for Serve to add listeners to. 563 // 564 // It must be called exactly once and will noop on subsequent calls. 565 func (b *builtinAPI) SetServer(srv *http.Server) { 566 select { 567 case <-b.srvReadyCh: 568 return 569 default: 570 } 571 572 b.srvLock.Lock() 573 defer b.srvLock.Unlock() 574 575 b.srv = srv 576 close(b.srvReadyCh) 577 } 578 579 // Serve the HTTP API on the listener unless the context is canceled before the 580 // HTTP API is ready to serve listeners. A non-nil error will always be 581 // returned, but http.ErrServerClosed and net.ErrClosed can likely be ignored 582 // as they indicate the server or listener is being shutdown. 583 func (b *builtinAPI) Serve(ctx context.Context, l net.Listener) error { 584 select { 585 case <-ctx.Done(): 586 // Caller canceled context before server was ready. 587 return ctx.Err() 588 case <-b.srvReadyCh: 589 // Server ready for listeners! Continue on... 590 } 591 592 return b.srv.Serve(l) 593 } 594 595 func (b *builtinAPI) Shutdown() { 596 b.srvLock.Lock() 597 defer b.srvLock.Unlock() 598 599 if b.srv != nil { 600 b.srv.Close() 601 } 602 603 select { 604 case <-b.srvReadyCh: 605 default: 606 close(b.srvReadyCh) 607 } 608 } 609 610 // HTTPCodedError is used to provide the HTTP error code 611 type HTTPCodedError interface { 612 error 613 Code() int 614 } 615 616 type UIAssetWrapper struct { 617 FileSystem *assetfs.AssetFS 618 } 619 620 func (fs *UIAssetWrapper) Open(name string) (http.File, error) { 621 if file, err := fs.FileSystem.Open(name); err == nil { 622 return file, nil 623 } else { 624 // serve index.html instead of 404ing 625 if err == os.ErrNotExist { 626 return fs.FileSystem.Open("index.html") 627 } 628 return nil, err 629 } 630 } 631 632 func CodedError(c int, s string) HTTPCodedError { 633 return &codedError{s, c} 634 } 635 636 type codedError struct { 637 s string 638 code int 639 } 640 641 func (e *codedError) Error() string { 642 return e.s 643 } 644 645 func (e *codedError) Code() int { 646 return e.code 647 } 648 649 func (s *HTTPServer) handleUI(h http.Handler) http.Handler { 650 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 651 header := w.Header() 652 header.Add("Content-Security-Policy", "default-src 'none'; connect-src *; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; form-action 'none'; frame-ancestors 'none'") 653 h.ServeHTTP(w, req) 654 }) 655 } 656 657 func (s *HTTPServer) handleRootFallthrough() http.Handler { 658 return s.auditHTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 659 if req.URL.Path == "/" { 660 url := "/ui/" 661 if req.URL.RawQuery != "" { 662 url = url + "?" + req.URL.RawQuery 663 } 664 http.Redirect(w, req, url, 307) 665 } else { 666 w.WriteHeader(http.StatusNotFound) 667 } 668 })) 669 } 670 671 func errCodeFromHandler(err error) (int, string) { 672 if err == nil { 673 return 0, "" 674 } 675 676 code := 500 677 errMsg := err.Error() 678 if http, ok := err.(HTTPCodedError); ok { 679 code = http.Code() 680 } else if ecode, emsg, ok := structs.CodeFromRPCCodedErr(err); ok { 681 code = ecode 682 errMsg = emsg 683 } else { 684 // RPC errors get wrapped, so manually unwrap by only looking at their suffix 685 if strings.HasSuffix(errMsg, structs.ErrPermissionDenied.Error()) { 686 errMsg = structs.ErrPermissionDenied.Error() 687 code = 403 688 } else if strings.HasSuffix(errMsg, structs.ErrTokenNotFound.Error()) { 689 errMsg = structs.ErrTokenNotFound.Error() 690 code = 403 691 } else if strings.HasSuffix(errMsg, structs.ErrJobRegistrationDisabled.Error()) { 692 errMsg = structs.ErrJobRegistrationDisabled.Error() 693 code = 403 694 } 695 } 696 697 return code, errMsg 698 } 699 700 // wrap is used to wrap functions to make them more convenient 701 func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) func(resp http.ResponseWriter, req *http.Request) { 702 f := func(resp http.ResponseWriter, req *http.Request) { 703 setHeaders(resp, s.agent.GetConfig().HTTPAPIResponseHeaders) 704 // Invoke the handler 705 reqURL := req.URL.String() 706 start := time.Now() 707 defer func() { 708 s.logger.Debug("request complete", "method", req.Method, "path", reqURL, "duration", time.Since(start)) 709 }() 710 obj, err := s.auditHandler(handler)(resp, req) 711 712 // Check for an error 713 HAS_ERR: 714 if err != nil { 715 code := 500 716 errMsg := err.Error() 717 if http, ok := err.(HTTPCodedError); ok { 718 code = http.Code() 719 } else if ecode, emsg, ok := structs.CodeFromRPCCodedErr(err); ok { 720 code = ecode 721 errMsg = emsg 722 } else { 723 // RPC errors get wrapped, so manually unwrap by only looking at their suffix 724 if strings.HasSuffix(errMsg, structs.ErrPermissionDenied.Error()) { 725 errMsg = structs.ErrPermissionDenied.Error() 726 code = 403 727 } else if strings.HasSuffix(errMsg, structs.ErrTokenNotFound.Error()) { 728 errMsg = structs.ErrTokenNotFound.Error() 729 code = 403 730 } else if strings.HasSuffix(errMsg, structs.ErrJobRegistrationDisabled.Error()) { 731 errMsg = structs.ErrJobRegistrationDisabled.Error() 732 code = 403 733 } else if strings.HasSuffix(errMsg, structs.ErrIncompatibleFiltering.Error()) { 734 errMsg = structs.ErrIncompatibleFiltering.Error() 735 code = 400 736 } 737 } 738 739 resp.WriteHeader(code) 740 resp.Write([]byte(errMsg)) 741 if isAPIClientError(code) { 742 s.logger.Debug("request failed", "method", req.Method, "path", reqURL, "error", err, "code", code) 743 } else { 744 s.logger.Error("request failed", "method", req.Method, "path", reqURL, "error", err, "code", code) 745 } 746 return 747 } 748 749 prettyPrint := false 750 if v, ok := req.URL.Query()["pretty"]; ok { 751 if len(v) > 0 && (len(v[0]) == 0 || v[0] != "0") { 752 prettyPrint = true 753 } 754 } 755 756 // Write out the JSON object 757 if obj != nil { 758 var buf bytes.Buffer 759 if prettyPrint { 760 enc := codec.NewEncoder(&buf, structs.JsonHandlePretty) 761 err = enc.Encode(obj) 762 if err == nil { 763 buf.Write([]byte("\n")) 764 } 765 } else { 766 enc := codec.NewEncoder(&buf, structs.JsonHandleWithExtensions) 767 err = enc.Encode(obj) 768 } 769 if err != nil { 770 goto HAS_ERR 771 } 772 resp.Header().Set("Content-Type", "application/json") 773 resp.Write(buf.Bytes()) 774 } 775 } 776 return f 777 } 778 779 // wrapNonJSON is used to wrap functions returning non JSON 780 // serializeable data to make them more convenient. It is primarily 781 // responsible for setting nomad headers and logging. 782 // Handler functions are responsible for setting Content-Type Header 783 func (s *HTTPServer) wrapNonJSON(handler func(resp http.ResponseWriter, req *http.Request) ([]byte, error)) func(resp http.ResponseWriter, req *http.Request) { 784 f := func(resp http.ResponseWriter, req *http.Request) { 785 setHeaders(resp, s.agent.GetConfig().HTTPAPIResponseHeaders) 786 // Invoke the handler 787 reqURL := req.URL.String() 788 start := time.Now() 789 defer func() { 790 s.logger.Debug("request complete", "method", req.Method, "path", reqURL, "duration", time.Since(start)) 791 }() 792 obj, err := s.auditNonJSONHandler(handler)(resp, req) 793 794 // Check for an error 795 if err != nil { 796 code, errMsg := errCodeFromHandler(err) 797 resp.WriteHeader(code) 798 resp.Write([]byte(errMsg)) 799 if isAPIClientError(code) { 800 s.logger.Debug("request failed", "method", req.Method, "path", reqURL, "error", err, "code", code) 801 } else { 802 s.logger.Error("request failed", "method", req.Method, "path", reqURL, "error", err, "code", code) 803 } 804 return 805 } 806 807 // write response 808 if obj != nil { 809 resp.Write(obj) 810 } 811 } 812 return f 813 } 814 815 // isAPIClientError returns true if the passed http code represents a client error 816 func isAPIClientError(code int) bool { 817 return 400 <= code && code <= 499 818 } 819 820 // decodeBody is used to decode a JSON request body 821 func decodeBody(req *http.Request, out interface{}) error { 822 823 if req.Body == http.NoBody { 824 return errors.New("Request body is empty") 825 } 826 827 dec := json.NewDecoder(req.Body) 828 return dec.Decode(&out) 829 } 830 831 // setIndex is used to set the index response header 832 func setIndex(resp http.ResponseWriter, index uint64) { 833 resp.Header().Set("X-Nomad-Index", strconv.FormatUint(index, 10)) 834 } 835 836 // setKnownLeader is used to set the known leader header 837 func setKnownLeader(resp http.ResponseWriter, known bool) { 838 s := "true" 839 if !known { 840 s = "false" 841 } 842 resp.Header().Set("X-Nomad-KnownLeader", s) 843 } 844 845 // setLastContact is used to set the last contact header 846 func setLastContact(resp http.ResponseWriter, last time.Duration) { 847 lastMsec := uint64(last / time.Millisecond) 848 resp.Header().Set("X-Nomad-LastContact", strconv.FormatUint(lastMsec, 10)) 849 } 850 851 // setNextToken is used to set the next token header for pagination 852 func setNextToken(resp http.ResponseWriter, nextToken string) { 853 if nextToken != "" { 854 resp.Header().Set("X-Nomad-NextToken", nextToken) 855 } 856 } 857 858 // setMeta is used to set the query response meta data 859 func setMeta(resp http.ResponseWriter, m *structs.QueryMeta) { 860 setIndex(resp, m.Index) 861 setLastContact(resp, m.LastContact) 862 setKnownLeader(resp, m.KnownLeader) 863 setNextToken(resp, m.NextToken) 864 } 865 866 // setHeaders is used to set canonical response header fields 867 func setHeaders(resp http.ResponseWriter, headers map[string]string) { 868 for field, value := range headers { 869 resp.Header().Set(field, value) 870 } 871 } 872 873 // parseWait is used to parse the ?wait and ?index query params 874 // Returns true on error 875 func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool { 876 query := req.URL.Query() 877 if wait := query.Get("wait"); wait != "" { 878 dur, err := time.ParseDuration(wait) 879 if err != nil { 880 resp.WriteHeader(400) 881 resp.Write([]byte("Invalid wait time")) 882 return true 883 } 884 b.MaxQueryTime = dur 885 } 886 if idx := query.Get("index"); idx != "" { 887 index, err := strconv.ParseUint(idx, 10, 64) 888 if err != nil { 889 resp.WriteHeader(400) 890 resp.Write([]byte("Invalid index")) 891 return true 892 } 893 b.MinQueryIndex = index 894 } 895 return false 896 } 897 898 // parseConsistency is used to parse the ?stale query params. 899 func parseConsistency(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) { 900 query := req.URL.Query() 901 if staleVal, ok := query["stale"]; ok { 902 if len(staleVal) == 0 || staleVal[0] == "" { 903 b.AllowStale = true 904 return 905 } 906 staleQuery, err := strconv.ParseBool(staleVal[0]) 907 if err != nil { 908 resp.WriteHeader(400) 909 _, _ = resp.Write([]byte(fmt.Sprintf("Expect `true` or `false` for `stale` query string parameter, got %s", staleVal[0]))) 910 return 911 } 912 b.AllowStale = staleQuery 913 } 914 } 915 916 // parsePrefix is used to parse the ?prefix query param 917 func parsePrefix(req *http.Request, b *structs.QueryOptions) { 918 query := req.URL.Query() 919 if prefix := query.Get("prefix"); prefix != "" { 920 b.Prefix = prefix 921 } 922 } 923 924 // parseRegion is used to parse the ?region query param 925 func (s *HTTPServer) parseRegion(req *http.Request, r *string) { 926 if other := req.URL.Query().Get("region"); other != "" { 927 *r = other 928 } else if *r == "" { 929 *r = s.agent.GetConfig().Region 930 } 931 } 932 933 // parseNamespace is used to parse the ?namespace parameter 934 func parseNamespace(req *http.Request, n *string) { 935 if other := req.URL.Query().Get("namespace"); other != "" { 936 *n = other 937 } else if *n == "" { 938 *n = structs.DefaultNamespace 939 } 940 } 941 942 // parseIdempotencyToken is used to parse the ?idempotency_token parameter 943 func parseIdempotencyToken(req *http.Request, n *string) { 944 if idempotencyToken := req.URL.Query().Get("idempotency_token"); idempotencyToken != "" { 945 *n = idempotencyToken 946 } 947 } 948 949 // parseBool parses a query parameter to a boolean or returns (nil, nil) if the 950 // parameter is not present. 951 func parseBool(req *http.Request, field string) (*bool, error) { 952 if str := req.URL.Query().Get(field); str != "" { 953 param, err := strconv.ParseBool(str) 954 if err != nil { 955 return nil, fmt.Errorf("Failed to parse value of %q (%v) as a bool: %v", field, str, err) 956 } 957 return ¶m, nil 958 } 959 960 return nil, nil 961 } 962 963 // parseInt parses a query parameter to a int or returns (nil, nil) if the 964 // parameter is not present. 965 func parseInt(req *http.Request, field string) (*int, error) { 966 if str := req.URL.Query().Get(field); str != "" { 967 param, err := strconv.Atoi(str) 968 if err != nil { 969 return nil, fmt.Errorf("Failed to parse value of %q (%v) as a int: %v", field, str, err) 970 } 971 return ¶m, nil 972 } 973 return nil, nil 974 } 975 976 // parseToken is used to parse the X-Nomad-Token param 977 func (s *HTTPServer) parseToken(req *http.Request, token *string) { 978 if other := req.Header.Get("X-Nomad-Token"); other != "" { 979 *token = strings.TrimSpace(other) 980 return 981 } 982 983 if other := req.Header.Get("Authorization"); other != "" { 984 // HTTP Authorization headers are in the format: <Scheme>[SPACE]<Value> 985 // Ref. https://tools.ietf.org/html/rfc7236#section-3 986 parts := strings.Split(other, " ") 987 988 // Authorization Header is invalid if containing 1 or 0 parts, e.g.: 989 // "" || "<Scheme><Value>" || "<Scheme>" || "<Value>" 990 if len(parts) > 1 { 991 scheme := parts[0] 992 // Everything after "<Scheme>" is "<Value>", trimmed 993 value := strings.TrimSpace(strings.Join(parts[1:], " ")) 994 995 // <Scheme> must be "Bearer" 996 if strings.ToLower(scheme) == "bearer" { 997 // Since Bearer tokens shouldn't contain spaces (rfc6750#section-2.1) 998 // "value" is tokenized, only the first item is used 999 *token = strings.TrimSpace(strings.Split(value, " ")[0]) 1000 } 1001 } 1002 } 1003 } 1004 1005 // parse is a convenience method for endpoints that need to parse multiple flags 1006 // It sets r to the region and b to the QueryOptions in req 1007 func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, r *string, b *structs.QueryOptions) bool { 1008 s.parseRegion(req, r) 1009 s.parseToken(req, &b.AuthToken) 1010 parseConsistency(resp, req, b) 1011 parsePrefix(req, b) 1012 parseNamespace(req, &b.Namespace) 1013 parsePagination(req, b) 1014 parseFilter(req, b) 1015 parseReverse(req, b) 1016 return parseWait(resp, req, b) 1017 } 1018 1019 // parsePagination parses the pagination fields for QueryOptions 1020 func parsePagination(req *http.Request, b *structs.QueryOptions) { 1021 query := req.URL.Query() 1022 rawPerPage := query.Get("per_page") 1023 if rawPerPage != "" { 1024 perPage, err := strconv.ParseInt(rawPerPage, 10, 32) 1025 if err == nil { 1026 b.PerPage = int32(perPage) 1027 } 1028 } 1029 1030 b.NextToken = query.Get("next_token") 1031 } 1032 1033 // parseFilter parses the filter query parameter for QueryOptions 1034 func parseFilter(req *http.Request, b *structs.QueryOptions) { 1035 query := req.URL.Query() 1036 if filter := query.Get("filter"); filter != "" { 1037 b.Filter = filter 1038 } 1039 } 1040 1041 // parseReverse parses the reverse query parameter for QueryOptions 1042 func parseReverse(req *http.Request, b *structs.QueryOptions) { 1043 query := req.URL.Query() 1044 b.Reverse = query.Get("reverse") == "true" 1045 } 1046 1047 // parseNode parses the node_id query parameter for node specific requests. 1048 func parseNode(req *http.Request, nodeID *string) { 1049 if n := req.URL.Query().Get("node_id"); n != "" { 1050 *nodeID = n 1051 } 1052 } 1053 1054 // parseNodeListStubFields parses query parameters related to node list stubs 1055 // fields. 1056 func parseNodeListStubFields(req *http.Request) (*structs.NodeStubFields, error) { 1057 fields := &structs.NodeStubFields{} 1058 1059 // Parse resources field selection. 1060 resources, err := parseBool(req, "resources") 1061 if err != nil { 1062 return nil, err 1063 } 1064 if resources != nil { 1065 fields.Resources = *resources 1066 } 1067 1068 // Parse OS field selection. 1069 os, err := parseBool(req, "os") 1070 if err != nil { 1071 return nil, err 1072 } 1073 if os != nil { 1074 fields.OS = *os 1075 } 1076 1077 return fields, nil 1078 } 1079 1080 // parseWriteRequest is a convenience method for endpoints that need to parse a 1081 // write request. 1082 func (s *HTTPServer) parseWriteRequest(req *http.Request, w *structs.WriteRequest) { 1083 parseNamespace(req, &w.Namespace) 1084 s.parseToken(req, &w.AuthToken) 1085 s.parseRegion(req, &w.Region) 1086 parseIdempotencyToken(req, &w.IdempotencyToken) 1087 } 1088 1089 // wrapUntrustedContent wraps handlers in a http.ResponseWriter that prevents 1090 // setting Content-Types that a browser may render (eg text/html). Any API that 1091 // returns service-generated content (eg /v1/client/fs/cat) must be wrapped. 1092 func (s *HTTPServer) wrapUntrustedContent(handler handlerFn) handlerFn { 1093 return func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 1094 resp, closeWriter := noxssrw.NewResponseWriter(resp) 1095 defer func() { 1096 if _, err := closeWriter(); err != nil { 1097 // Can't write an error response at this point so just 1098 // log. s.wrap does not even log when resp.Write fails, 1099 // so log at low level. 1100 s.logger.Debug("error writing HTTP response", "error", err, 1101 "method", req.Method, "path", req.URL.String()) 1102 } 1103 }() 1104 1105 // Call the wrapped handler 1106 return handler(resp, req) 1107 } 1108 } 1109 1110 // wrapCORS wraps a HandlerFunc in allowCORS with read ("HEAD", "GET") methods 1111 // and returns a http.Handler 1112 func wrapCORS(f func(http.ResponseWriter, *http.Request)) http.Handler { 1113 return wrapCORSWithAllowedMethods(f, "HEAD", "GET") 1114 } 1115 1116 // wrapCORSWithAllowedMethods wraps a HandlerFunc in an allowCORS with the given 1117 // method list and returns a http.Handler 1118 func wrapCORSWithAllowedMethods(f func(http.ResponseWriter, *http.Request), methods ...string) http.Handler { 1119 return allowCORSWithMethods(methods...).Handler(http.HandlerFunc(f)) 1120 } 1121 1122 // authMiddleware implements the http.Handler interface to enforce 1123 // authentication for *all* requests. Even with ACLs enabled there are 1124 // endpoints which are accessible without authenticating. This middleware is 1125 // used for the Task API to enfoce authentication for all API access. 1126 type authMiddleware struct { 1127 srv *HTTPServer 1128 wrapped http.Handler 1129 } 1130 1131 func newAuthMiddleware(srv *HTTPServer, h http.Handler) http.Handler { 1132 return &authMiddleware{ 1133 srv: srv, 1134 wrapped: h, 1135 } 1136 } 1137 1138 func (a *authMiddleware) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 1139 args := structs.GenericRequest{} 1140 reply := structs.ACLWhoAmIResponse{} 1141 if a.srv.parse(resp, req, &args.Region, &args.QueryOptions) { 1142 // Error parsing request, 400 1143 resp.WriteHeader(http.StatusBadRequest) 1144 resp.Write([]byte(http.StatusText(http.StatusBadRequest))) 1145 return 1146 } 1147 1148 if args.AuthToken == "" { 1149 // 401 instead of 403 since no token was present. 1150 resp.WriteHeader(http.StatusUnauthorized) 1151 resp.Write([]byte(http.StatusText(http.StatusUnauthorized))) 1152 return 1153 } 1154 1155 if err := a.srv.agent.RPC("ACL.WhoAmI", &args, &reply); err != nil { 1156 // When ACLs are enabled, WhoAmI returns ErrPermissionDenied on bad 1157 // credentials, so convert it to a Forbidden response code. 1158 if strings.HasSuffix(err.Error(), structs.ErrPermissionDenied.Error()) { 1159 a.srv.logger.Debug("Failed to authenticated Task API request", "method", req.Method, "url", req.URL) 1160 resp.WriteHeader(http.StatusForbidden) 1161 resp.Write([]byte(http.StatusText(http.StatusForbidden))) 1162 return 1163 } 1164 1165 a.srv.logger.Error("error authenticating built API request", "error", err, "url", req.URL, "method", req.Method) 1166 resp.WriteHeader(500) 1167 resp.Write([]byte("Server error authenticating request\n")) 1168 return 1169 } 1170 1171 // Require an acl token or workload identity 1172 if reply.Identity == nil || (reply.Identity.ACLToken == nil && reply.Identity.Claims == nil) { 1173 a.srv.logger.Debug("Failed to authenticated Task API request", "method", req.Method, "url", req.URL) 1174 resp.WriteHeader(http.StatusForbidden) 1175 resp.Write([]byte(http.StatusText(http.StatusForbidden))) 1176 return 1177 } 1178 1179 a.srv.logger.Trace("Authenticated request", "id", reply.Identity, "method", req.Method, "url", req.URL) 1180 a.wrapped.ServeHTTP(resp, req) 1181 }