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  }