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