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