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