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