github.com/remilapeyre/nomad@v0.8.5/command/agent/http.go (about) 1 package agent 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "encoding/json" 7 "fmt" 8 "log" 9 "net" 10 "net/http" 11 "net/http/pprof" 12 "os" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/NYTimes/gziphandler" 18 assetfs "github.com/elazarl/go-bindata-assetfs" 19 "github.com/hashicorp/nomad/helper/tlsutil" 20 "github.com/hashicorp/nomad/nomad/structs" 21 "github.com/rs/cors" 22 "github.com/ugorji/go/codec" 23 ) 24 25 const ( 26 // ErrInvalidMethod is used if the HTTP method is not supported 27 ErrInvalidMethod = "Invalid method" 28 29 // ErrEntOnly is the error returned if accessing an enterprise only 30 // endpoint 31 ErrEntOnly = "Nomad Enterprise only endpoint" 32 ) 33 34 var ( 35 // Set to false by stub_asset if the ui build tag isn't enabled 36 uiEnabled = true 37 38 // Overridden if the ui build tag isn't enabled 39 stubHTML = "" 40 41 // allowCORS sets permissive CORS headers for a handler 42 allowCORS = cors.New(cors.Options{ 43 AllowedOrigins: []string{"*"}, 44 AllowedMethods: []string{"HEAD", "GET"}, 45 AllowedHeaders: []string{"*"}, 46 }) 47 ) 48 49 // HTTPServer is used to wrap an Agent and expose it over an HTTP interface 50 type HTTPServer struct { 51 agent *Agent 52 mux *http.ServeMux 53 listener net.Listener 54 listenerCh chan struct{} 55 logger *log.Logger 56 Addr string 57 } 58 59 // NewHTTPServer starts new HTTP server over the agent 60 func NewHTTPServer(agent *Agent, config *Config) (*HTTPServer, error) { 61 // Start the listener 62 lnAddr, err := net.ResolveTCPAddr("tcp", config.normalizedAddrs.HTTP) 63 if err != nil { 64 return nil, err 65 } 66 ln, err := config.Listener("tcp", lnAddr.IP.String(), lnAddr.Port) 67 if err != nil { 68 return nil, fmt.Errorf("failed to start HTTP listener: %v", err) 69 } 70 71 // If TLS is enabled, wrap the listener with a TLS listener 72 if config.TLSConfig.EnableHTTP { 73 tlsConf, err := tlsutil.NewTLSConfiguration(config.TLSConfig, config.TLSConfig.VerifyHTTPSClient, true) 74 if err != nil { 75 return nil, err 76 } 77 78 tlsConfig, err := tlsConf.IncomingTLSConfig() 79 if err != nil { 80 return nil, err 81 } 82 ln = tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, tlsConfig) 83 } 84 85 // Create the mux 86 mux := http.NewServeMux() 87 88 // Create the server 89 srv := &HTTPServer{ 90 agent: agent, 91 mux: mux, 92 listener: ln, 93 listenerCh: make(chan struct{}), 94 logger: agent.logger, 95 Addr: ln.Addr().String(), 96 } 97 srv.registerHandlers(config.EnableDebug) 98 99 // Handle requests with gzip compression 100 gzip, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(0)) 101 if err != nil { 102 return nil, err 103 } 104 105 go func() { 106 defer close(srv.listenerCh) 107 http.Serve(ln, gzip(mux)) 108 }() 109 110 return srv, nil 111 } 112 113 // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted 114 // connections. It's used by NewHttpServer so 115 // dead TCP connections eventually go away. 116 type tcpKeepAliveListener struct { 117 *net.TCPListener 118 } 119 120 func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 121 tc, err := ln.AcceptTCP() 122 if err != nil { 123 return 124 } 125 tc.SetKeepAlive(true) 126 tc.SetKeepAlivePeriod(30 * time.Second) 127 return tc, nil 128 } 129 130 // Shutdown is used to shutdown the HTTP server 131 func (s *HTTPServer) Shutdown() { 132 if s != nil { 133 s.logger.Printf("[DEBUG] http: Shutting down http server") 134 s.listener.Close() 135 <-s.listenerCh // block until http.Serve has returned. 136 } 137 } 138 139 // registerHandlers is used to attach our handlers to the mux 140 func (s *HTTPServer) registerHandlers(enableDebug bool) { 141 s.mux.HandleFunc("/v1/jobs", s.wrap(s.JobsRequest)) 142 s.mux.HandleFunc("/v1/jobs/parse", s.wrap(s.JobsParseRequest)) 143 s.mux.HandleFunc("/v1/job/", s.wrap(s.JobSpecificRequest)) 144 145 s.mux.HandleFunc("/v1/nodes", s.wrap(s.NodesRequest)) 146 s.mux.HandleFunc("/v1/node/", s.wrap(s.NodeSpecificRequest)) 147 148 s.mux.HandleFunc("/v1/allocations", s.wrap(s.AllocsRequest)) 149 s.mux.HandleFunc("/v1/allocation/", s.wrap(s.AllocSpecificRequest)) 150 151 s.mux.HandleFunc("/v1/evaluations", s.wrap(s.EvalsRequest)) 152 s.mux.HandleFunc("/v1/evaluation/", s.wrap(s.EvalSpecificRequest)) 153 154 s.mux.HandleFunc("/v1/deployments", s.wrap(s.DeploymentsRequest)) 155 s.mux.HandleFunc("/v1/deployment/", s.wrap(s.DeploymentSpecificRequest)) 156 157 s.mux.HandleFunc("/v1/acl/policies", s.wrap(s.ACLPoliciesRequest)) 158 s.mux.HandleFunc("/v1/acl/policy/", s.wrap(s.ACLPolicySpecificRequest)) 159 160 s.mux.HandleFunc("/v1/acl/bootstrap", s.wrap(s.ACLTokenBootstrap)) 161 s.mux.HandleFunc("/v1/acl/tokens", s.wrap(s.ACLTokensRequest)) 162 s.mux.HandleFunc("/v1/acl/token", s.wrap(s.ACLTokenSpecificRequest)) 163 s.mux.HandleFunc("/v1/acl/token/", s.wrap(s.ACLTokenSpecificRequest)) 164 165 s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest))) 166 s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest)) 167 s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest))) 168 s.mux.Handle("/v1/client/allocation/", wrapCORS(s.wrap(s.ClientAllocRequest))) 169 170 s.mux.HandleFunc("/v1/agent/self", s.wrap(s.AgentSelfRequest)) 171 s.mux.HandleFunc("/v1/agent/join", s.wrap(s.AgentJoinRequest)) 172 s.mux.HandleFunc("/v1/agent/members", s.wrap(s.AgentMembersRequest)) 173 s.mux.HandleFunc("/v1/agent/force-leave", s.wrap(s.AgentForceLeaveRequest)) 174 s.mux.HandleFunc("/v1/agent/servers", s.wrap(s.AgentServersRequest)) 175 s.mux.HandleFunc("/v1/agent/keyring/", s.wrap(s.KeyringOperationRequest)) 176 s.mux.HandleFunc("/v1/agent/health", s.wrap(s.HealthRequest)) 177 178 s.mux.HandleFunc("/v1/metrics", s.wrap(s.MetricsRequest)) 179 180 s.mux.HandleFunc("/v1/validate/job", s.wrap(s.ValidateJobRequest)) 181 182 s.mux.HandleFunc("/v1/regions", s.wrap(s.RegionListRequest)) 183 184 s.mux.HandleFunc("/v1/status/leader", s.wrap(s.StatusLeaderRequest)) 185 s.mux.HandleFunc("/v1/status/peers", s.wrap(s.StatusPeersRequest)) 186 187 s.mux.HandleFunc("/v1/search", s.wrap(s.SearchRequest)) 188 189 s.mux.HandleFunc("/v1/operator/raft/", s.wrap(s.OperatorRequest)) 190 s.mux.HandleFunc("/v1/operator/autopilot/configuration", s.wrap(s.OperatorAutopilotConfiguration)) 191 s.mux.HandleFunc("/v1/operator/autopilot/health", s.wrap(s.OperatorServerHealth)) 192 193 s.mux.HandleFunc("/v1/system/gc", s.wrap(s.GarbageCollectRequest)) 194 s.mux.HandleFunc("/v1/system/reconcile/summaries", s.wrap(s.ReconcileJobSummaries)) 195 196 if uiEnabled { 197 s.mux.Handle("/ui/", http.StripPrefix("/ui/", handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()})))) 198 } else { 199 // Write the stubHTML 200 s.mux.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) { 201 w.Write([]byte(stubHTML)) 202 }) 203 } 204 s.mux.Handle("/", handleRootRedirect()) 205 206 if enableDebug { 207 s.mux.HandleFunc("/debug/pprof/", pprof.Index) 208 s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 209 s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 210 s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 211 s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 212 } 213 214 // Register enterprise endpoints. 215 s.registerEnterpriseHandlers() 216 } 217 218 // HTTPCodedError is used to provide the HTTP error code 219 type HTTPCodedError interface { 220 error 221 Code() int 222 } 223 224 type UIAssetWrapper struct { 225 FileSystem *assetfs.AssetFS 226 } 227 228 func (fs *UIAssetWrapper) Open(name string) (http.File, error) { 229 if file, err := fs.FileSystem.Open(name); err == nil { 230 return file, nil 231 } else { 232 // serve index.html instead of 404ing 233 if err == os.ErrNotExist { 234 return fs.FileSystem.Open("index.html") 235 } 236 return nil, err 237 } 238 } 239 240 func CodedError(c int, s string) HTTPCodedError { 241 return &codedError{s, c} 242 } 243 244 type codedError struct { 245 s string 246 code int 247 } 248 249 func (e *codedError) Error() string { 250 return e.s 251 } 252 253 func (e *codedError) Code() int { 254 return e.code 255 } 256 257 func handleUI(h http.Handler) http.Handler { 258 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 259 header := w.Header() 260 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'") 261 h.ServeHTTP(w, req) 262 return 263 }) 264 } 265 266 func handleRootRedirect() http.Handler { 267 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 268 http.Redirect(w, req, "/ui/", 307) 269 return 270 }) 271 } 272 273 // wrap is used to wrap functions to make them more convenient 274 func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) func(resp http.ResponseWriter, req *http.Request) { 275 f := func(resp http.ResponseWriter, req *http.Request) { 276 setHeaders(resp, s.agent.config.HTTPAPIResponseHeaders) 277 // Invoke the handler 278 reqURL := req.URL.String() 279 start := time.Now() 280 defer func() { 281 s.logger.Printf("[DEBUG] http: Request %v %v (%v)", req.Method, reqURL, time.Now().Sub(start)) 282 }() 283 obj, err := handler(resp, req) 284 285 // Check for an error 286 HAS_ERR: 287 if err != nil { 288 s.logger.Printf("[ERR] http: Request %v, error: %v", reqURL, err) 289 code := 500 290 errMsg := err.Error() 291 if http, ok := err.(HTTPCodedError); ok { 292 code = http.Code() 293 } else { 294 // RPC errors get wrapped, so manually unwrap by only looking at their suffix 295 if strings.HasSuffix(errMsg, structs.ErrPermissionDenied.Error()) { 296 errMsg = structs.ErrPermissionDenied.Error() 297 code = 403 298 } else if strings.HasSuffix(errMsg, structs.ErrTokenNotFound.Error()) { 299 errMsg = structs.ErrTokenNotFound.Error() 300 code = 403 301 } 302 } 303 304 resp.WriteHeader(code) 305 resp.Write([]byte(errMsg)) 306 return 307 } 308 309 prettyPrint := false 310 if v, ok := req.URL.Query()["pretty"]; ok { 311 if len(v) > 0 && (len(v[0]) == 0 || v[0] != "0") { 312 prettyPrint = true 313 } 314 } 315 316 // Write out the JSON object 317 if obj != nil { 318 var buf bytes.Buffer 319 if prettyPrint { 320 enc := codec.NewEncoder(&buf, structs.JsonHandlePretty) 321 err = enc.Encode(obj) 322 if err == nil { 323 buf.Write([]byte("\n")) 324 } 325 } else { 326 enc := codec.NewEncoder(&buf, structs.JsonHandle) 327 err = enc.Encode(obj) 328 } 329 if err != nil { 330 goto HAS_ERR 331 } 332 resp.Header().Set("Content-Type", "application/json") 333 resp.Write(buf.Bytes()) 334 } 335 } 336 return f 337 } 338 339 // decodeBody is used to decode a JSON request body 340 func decodeBody(req *http.Request, out interface{}) error { 341 dec := json.NewDecoder(req.Body) 342 return dec.Decode(&out) 343 } 344 345 // setIndex is used to set the index response header 346 func setIndex(resp http.ResponseWriter, index uint64) { 347 resp.Header().Set("X-Nomad-Index", strconv.FormatUint(index, 10)) 348 } 349 350 // setKnownLeader is used to set the known leader header 351 func setKnownLeader(resp http.ResponseWriter, known bool) { 352 s := "true" 353 if !known { 354 s = "false" 355 } 356 resp.Header().Set("X-Nomad-KnownLeader", s) 357 } 358 359 // setLastContact is used to set the last contact header 360 func setLastContact(resp http.ResponseWriter, last time.Duration) { 361 lastMsec := uint64(last / time.Millisecond) 362 resp.Header().Set("X-Nomad-LastContact", strconv.FormatUint(lastMsec, 10)) 363 } 364 365 // setMeta is used to set the query response meta data 366 func setMeta(resp http.ResponseWriter, m *structs.QueryMeta) { 367 setIndex(resp, m.Index) 368 setLastContact(resp, m.LastContact) 369 setKnownLeader(resp, m.KnownLeader) 370 } 371 372 // setHeaders is used to set canonical response header fields 373 func setHeaders(resp http.ResponseWriter, headers map[string]string) { 374 for field, value := range headers { 375 resp.Header().Set(http.CanonicalHeaderKey(field), value) 376 } 377 } 378 379 // parseWait is used to parse the ?wait and ?index query params 380 // Returns true on error 381 func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool { 382 query := req.URL.Query() 383 if wait := query.Get("wait"); wait != "" { 384 dur, err := time.ParseDuration(wait) 385 if err != nil { 386 resp.WriteHeader(400) 387 resp.Write([]byte("Invalid wait time")) 388 return true 389 } 390 b.MaxQueryTime = dur 391 } 392 if idx := query.Get("index"); idx != "" { 393 index, err := strconv.ParseUint(idx, 10, 64) 394 if err != nil { 395 resp.WriteHeader(400) 396 resp.Write([]byte("Invalid index")) 397 return true 398 } 399 b.MinQueryIndex = index 400 } 401 return false 402 } 403 404 // parseConsistency is used to parse the ?stale query params. 405 func parseConsistency(req *http.Request, b *structs.QueryOptions) { 406 query := req.URL.Query() 407 if _, ok := query["stale"]; ok { 408 b.AllowStale = true 409 } 410 } 411 412 // parsePrefix is used to parse the ?prefix query param 413 func parsePrefix(req *http.Request, b *structs.QueryOptions) { 414 query := req.URL.Query() 415 if prefix := query.Get("prefix"); prefix != "" { 416 b.Prefix = prefix 417 } 418 } 419 420 // parseRegion is used to parse the ?region query param 421 func (s *HTTPServer) parseRegion(req *http.Request, r *string) { 422 if other := req.URL.Query().Get("region"); other != "" { 423 *r = other 424 } else if *r == "" { 425 *r = s.agent.config.Region 426 } 427 } 428 429 // parseNamespace is used to parse the ?namespace parameter 430 func parseNamespace(req *http.Request, n *string) { 431 if other := req.URL.Query().Get("namespace"); other != "" { 432 *n = other 433 } else if *n == "" { 434 *n = structs.DefaultNamespace 435 } 436 } 437 438 // parseToken is used to parse the X-Nomad-Token param 439 func (s *HTTPServer) parseToken(req *http.Request, token *string) { 440 if other := req.Header.Get("X-Nomad-Token"); other != "" { 441 *token = other 442 return 443 } 444 } 445 446 // parse is a convenience method for endpoints that need to parse multiple flags 447 func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, r *string, b *structs.QueryOptions) bool { 448 s.parseRegion(req, r) 449 s.parseToken(req, &b.AuthToken) 450 parseConsistency(req, b) 451 parsePrefix(req, b) 452 parseNamespace(req, &b.Namespace) 453 return parseWait(resp, req, b) 454 } 455 456 // parseWriteRequest is a convenience method for endpoints that need to parse a 457 // write request. 458 func (s *HTTPServer) parseWriteRequest(req *http.Request, w *structs.WriteRequest) { 459 parseNamespace(req, &w.Namespace) 460 s.parseToken(req, &w.AuthToken) 461 s.parseRegion(req, &w.Region) 462 } 463 464 // wrapCORS wraps a HandlerFunc in allowCORS and returns a http.Handler 465 func wrapCORS(f func(http.ResponseWriter, *http.Request)) http.Handler { 466 return allowCORS.Handler(http.HandlerFunc(f)) 467 }