github.com/prestonp/nomad@v0.10.4/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 "github.com/hashicorp/go-connlimit" 20 log "github.com/hashicorp/go-hclog" 21 "github.com/hashicorp/nomad/helper/tlsutil" 22 "github.com/hashicorp/nomad/nomad/structs" 23 "github.com/rs/cors" 24 "github.com/ugorji/go/codec" 25 ) 26 27 const ( 28 // ErrInvalidMethod is used if the HTTP method is not supported 29 ErrInvalidMethod = "Invalid method" 30 31 // ErrEntOnly is the error returned if accessing an enterprise only 32 // endpoint 33 ErrEntOnly = "Nomad Enterprise only endpoint" 34 ) 35 36 var ( 37 // Set to false by stub_asset if the ui build tag isn't enabled 38 uiEnabled = true 39 40 // Overridden if the ui build tag isn't enabled 41 stubHTML = "" 42 43 // allowCORS sets permissive CORS headers for a handler 44 allowCORS = cors.New(cors.Options{ 45 AllowedOrigins: []string{"*"}, 46 AllowedMethods: []string{"HEAD", "GET"}, 47 AllowedHeaders: []string{"*"}, 48 AllowCredentials: true, 49 }) 50 ) 51 52 // HTTPServer is used to wrap an Agent and expose it over an HTTP interface 53 type HTTPServer struct { 54 agent *Agent 55 mux *http.ServeMux 56 listener net.Listener 57 listenerCh chan struct{} 58 logger log.Logger 59 Addr string 60 61 wsUpgrader *websocket.Upgrader 62 } 63 64 // NewHTTPServer starts new HTTP server over the agent 65 func NewHTTPServer(agent *Agent, config *Config) (*HTTPServer, error) { 66 // Start the listener 67 lnAddr, err := net.ResolveTCPAddr("tcp", config.normalizedAddrs.HTTP) 68 if err != nil { 69 return nil, err 70 } 71 ln, err := config.Listener("tcp", lnAddr.IP.String(), lnAddr.Port) 72 if err != nil { 73 return nil, fmt.Errorf("failed to start HTTP listener: %v", err) 74 } 75 76 // If TLS is enabled, wrap the listener with a TLS listener 77 if config.TLSConfig.EnableHTTP { 78 tlsConf, err := tlsutil.NewTLSConfiguration(config.TLSConfig, config.TLSConfig.VerifyHTTPSClient, true) 79 if err != nil { 80 return nil, err 81 } 82 83 tlsConfig, err := tlsConf.IncomingTLSConfig() 84 if err != nil { 85 return nil, err 86 } 87 ln = tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, tlsConfig) 88 } 89 90 // Create the mux 91 mux := http.NewServeMux() 92 93 wsUpgrader := &websocket.Upgrader{ 94 ReadBufferSize: 2048, 95 WriteBufferSize: 2048, 96 } 97 98 // Create the server 99 srv := &HTTPServer{ 100 agent: agent, 101 mux: mux, 102 listener: ln, 103 listenerCh: make(chan struct{}), 104 logger: agent.httpLogger, 105 Addr: ln.Addr().String(), 106 wsUpgrader: wsUpgrader, 107 } 108 srv.registerHandlers(config.EnableDebug) 109 110 // Handle requests with gzip compression 111 gzip, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(0)) 112 if err != nil { 113 return nil, err 114 } 115 116 // Get connection handshake timeout limit 117 handshakeTimeout, err := time.ParseDuration(config.Limits.HTTPSHandshakeTimeout) 118 if err != nil { 119 return nil, fmt.Errorf("error parsing https_handshake_timeout: %v", err) 120 } else if handshakeTimeout < 0 { 121 return nil, fmt.Errorf("https_handshake_timeout must be >= 0") 122 } 123 124 // Get max connection limit 125 maxConns := 0 126 if mc := config.Limits.HTTPMaxConnsPerClient; mc != nil { 127 maxConns = *mc 128 } 129 if maxConns < 0 { 130 return nil, fmt.Errorf("http_max_conns_per_client must be >= 0") 131 } 132 133 // Create HTTP server with timeouts 134 httpServer := http.Server{ 135 Addr: srv.Addr, 136 Handler: gzip(mux), 137 ConnState: makeConnState(config.TLSConfig.EnableHTTP, handshakeTimeout, maxConns), 138 } 139 140 go func() { 141 defer close(srv.listenerCh) 142 httpServer.Serve(ln) 143 }() 144 145 return srv, nil 146 } 147 148 // makeConnState returns a ConnState func for use in an http.Server. If 149 // isTLS=true and handshakeTimeout>0 then the handshakeTimeout will be applied 150 // as a connection deadline to new connections and removed when the connection 151 // is active (meaning it has successfully completed the TLS handshake). 152 // 153 // If limit > 0, a per-address connection limit will be enabled regardless of 154 // TLS. If connLimit == 0 there is no connection limit. 155 func makeConnState(isTLS bool, handshakeTimeout time.Duration, connLimit int) func(conn net.Conn, state http.ConnState) { 156 if !isTLS || handshakeTimeout == 0 { 157 if connLimit > 0 { 158 // Still return the connection limiter 159 return connlimit.NewLimiter(connlimit.Config{ 160 MaxConnsPerClientIP: connLimit, 161 }).HTTPConnStateFunc() 162 } 163 164 return nil 165 } 166 167 if connLimit > 0 { 168 // Return conn state callback with connection limiting and a 169 // handshake timeout. 170 171 connLimiter := connlimit.NewLimiter(connlimit.Config{ 172 MaxConnsPerClientIP: connLimit, 173 }).HTTPConnStateFunc() 174 175 return func(conn net.Conn, state http.ConnState) { 176 switch state { 177 case http.StateNew: 178 // Set deadline to prevent slow send before TLS handshake or first 179 // byte of request. 180 conn.SetDeadline(time.Now().Add(handshakeTimeout)) 181 case http.StateActive: 182 // Clear read deadline. We should maybe set read timeouts more 183 // generally but that's a bigger task as some HTTP endpoints may 184 // stream large requests and responses (e.g. snapshot) so we can't 185 // set sensible blanket timeouts here. 186 conn.SetDeadline(time.Time{}) 187 } 188 189 // Call connection limiter 190 connLimiter(conn, state) 191 } 192 } 193 194 // Return conn state callback with just a handshake timeout 195 // (connection limiting disabled). 196 return func(conn net.Conn, state http.ConnState) { 197 switch state { 198 case http.StateNew: 199 // Set deadline to prevent slow send before TLS handshake or first 200 // byte of request. 201 conn.SetDeadline(time.Now().Add(handshakeTimeout)) 202 case http.StateActive: 203 // Clear read deadline. We should maybe set read timeouts more 204 // generally but that's a bigger task as some HTTP endpoints may 205 // stream large requests and responses (e.g. snapshot) so we can't 206 // set sensible blanket timeouts here. 207 conn.SetDeadline(time.Time{}) 208 } 209 } 210 } 211 212 // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted 213 // connections. It's used by NewHttpServer so 214 // dead TCP connections eventually go away. 215 type tcpKeepAliveListener struct { 216 *net.TCPListener 217 } 218 219 func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 220 tc, err := ln.AcceptTCP() 221 if err != nil { 222 return 223 } 224 tc.SetKeepAlive(true) 225 tc.SetKeepAlivePeriod(30 * time.Second) 226 return tc, nil 227 } 228 229 // Shutdown is used to shutdown the HTTP server 230 func (s *HTTPServer) Shutdown() { 231 if s != nil { 232 s.logger.Debug("shutting down http server") 233 s.listener.Close() 234 <-s.listenerCh // block until http.Serve has returned. 235 } 236 } 237 238 // registerHandlers is used to attach our handlers to the mux 239 func (s *HTTPServer) registerHandlers(enableDebug bool) { 240 s.mux.HandleFunc("/v1/jobs", s.wrap(s.JobsRequest)) 241 s.mux.HandleFunc("/v1/jobs/parse", s.wrap(s.JobsParseRequest)) 242 s.mux.HandleFunc("/v1/job/", s.wrap(s.JobSpecificRequest)) 243 244 s.mux.HandleFunc("/v1/nodes", s.wrap(s.NodesRequest)) 245 s.mux.HandleFunc("/v1/node/", s.wrap(s.NodeSpecificRequest)) 246 247 s.mux.HandleFunc("/v1/allocations", s.wrap(s.AllocsRequest)) 248 s.mux.HandleFunc("/v1/allocation/", s.wrap(s.AllocSpecificRequest)) 249 250 s.mux.HandleFunc("/v1/evaluations", s.wrap(s.EvalsRequest)) 251 s.mux.HandleFunc("/v1/evaluation/", s.wrap(s.EvalSpecificRequest)) 252 253 s.mux.HandleFunc("/v1/deployments", s.wrap(s.DeploymentsRequest)) 254 s.mux.HandleFunc("/v1/deployment/", s.wrap(s.DeploymentSpecificRequest)) 255 256 s.mux.HandleFunc("/v1/acl/policies", s.wrap(s.ACLPoliciesRequest)) 257 s.mux.HandleFunc("/v1/acl/policy/", s.wrap(s.ACLPolicySpecificRequest)) 258 259 s.mux.HandleFunc("/v1/acl/bootstrap", s.wrap(s.ACLTokenBootstrap)) 260 s.mux.HandleFunc("/v1/acl/tokens", s.wrap(s.ACLTokensRequest)) 261 s.mux.HandleFunc("/v1/acl/token", s.wrap(s.ACLTokenSpecificRequest)) 262 s.mux.HandleFunc("/v1/acl/token/", s.wrap(s.ACLTokenSpecificRequest)) 263 264 s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest))) 265 s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest)) 266 s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest))) 267 s.mux.Handle("/v1/client/allocation/", wrapCORS(s.wrap(s.ClientAllocRequest))) 268 269 s.mux.HandleFunc("/v1/agent/self", s.wrap(s.AgentSelfRequest)) 270 s.mux.HandleFunc("/v1/agent/join", s.wrap(s.AgentJoinRequest)) 271 s.mux.HandleFunc("/v1/agent/members", s.wrap(s.AgentMembersRequest)) 272 s.mux.HandleFunc("/v1/agent/force-leave", s.wrap(s.AgentForceLeaveRequest)) 273 s.mux.HandleFunc("/v1/agent/servers", s.wrap(s.AgentServersRequest)) 274 s.mux.HandleFunc("/v1/agent/keyring/", s.wrap(s.KeyringOperationRequest)) 275 s.mux.HandleFunc("/v1/agent/health", s.wrap(s.HealthRequest)) 276 s.mux.HandleFunc("/v1/agent/monitor", s.wrap(s.AgentMonitor)) 277 278 s.mux.HandleFunc("/v1/agent/pprof/", s.wrapNonJSON(s.AgentPprofRequest)) 279 280 s.mux.HandleFunc("/v1/metrics", s.wrap(s.MetricsRequest)) 281 282 s.mux.HandleFunc("/v1/validate/job", s.wrap(s.ValidateJobRequest)) 283 284 s.mux.HandleFunc("/v1/regions", s.wrap(s.RegionListRequest)) 285 286 s.mux.HandleFunc("/v1/status/leader", s.wrap(s.StatusLeaderRequest)) 287 s.mux.HandleFunc("/v1/status/peers", s.wrap(s.StatusPeersRequest)) 288 289 s.mux.HandleFunc("/v1/search", s.wrap(s.SearchRequest)) 290 291 s.mux.HandleFunc("/v1/operator/raft/", s.wrap(s.OperatorRequest)) 292 s.mux.HandleFunc("/v1/operator/autopilot/configuration", s.wrap(s.OperatorAutopilotConfiguration)) 293 s.mux.HandleFunc("/v1/operator/autopilot/health", s.wrap(s.OperatorServerHealth)) 294 295 s.mux.HandleFunc("/v1/system/gc", s.wrap(s.GarbageCollectRequest)) 296 s.mux.HandleFunc("/v1/system/reconcile/summaries", s.wrap(s.ReconcileJobSummaries)) 297 298 s.mux.HandleFunc("/v1/operator/scheduler/configuration", s.wrap(s.OperatorSchedulerConfiguration)) 299 300 if uiEnabled { 301 s.mux.Handle("/ui/", http.StripPrefix("/ui/", handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()})))) 302 } else { 303 // Write the stubHTML 304 s.mux.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) { 305 w.Write([]byte(stubHTML)) 306 }) 307 } 308 s.mux.Handle("/", handleRootFallthrough()) 309 310 if enableDebug { 311 if !s.agent.config.DevMode { 312 s.logger.Warn("enable_debug is set to true. This is insecure and should not be enabled in production") 313 } 314 s.mux.HandleFunc("/debug/pprof/", pprof.Index) 315 s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 316 s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 317 s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 318 s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 319 } 320 321 // Register enterprise endpoints. 322 s.registerEnterpriseHandlers() 323 } 324 325 // HTTPCodedError is used to provide the HTTP error code 326 type HTTPCodedError interface { 327 error 328 Code() int 329 } 330 331 type UIAssetWrapper struct { 332 FileSystem *assetfs.AssetFS 333 } 334 335 func (fs *UIAssetWrapper) Open(name string) (http.File, error) { 336 if file, err := fs.FileSystem.Open(name); err == nil { 337 return file, nil 338 } else { 339 // serve index.html instead of 404ing 340 if err == os.ErrNotExist { 341 return fs.FileSystem.Open("index.html") 342 } 343 return nil, err 344 } 345 } 346 347 func CodedError(c int, s string) HTTPCodedError { 348 return &codedError{s, c} 349 } 350 351 type codedError struct { 352 s string 353 code int 354 } 355 356 func (e *codedError) Error() string { 357 return e.s 358 } 359 360 func (e *codedError) Code() int { 361 return e.code 362 } 363 364 func handleUI(h http.Handler) http.Handler { 365 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 366 header := w.Header() 367 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'") 368 h.ServeHTTP(w, req) 369 return 370 }) 371 } 372 373 func handleRootFallthrough() http.Handler { 374 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 375 if req.URL.Path == "/" { 376 http.Redirect(w, req, "/ui/", 307) 377 } else { 378 w.WriteHeader(http.StatusNotFound) 379 } 380 }) 381 } 382 383 // wrap is used to wrap functions to make them more convenient 384 func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) func(resp http.ResponseWriter, req *http.Request) { 385 f := func(resp http.ResponseWriter, req *http.Request) { 386 setHeaders(resp, s.agent.config.HTTPAPIResponseHeaders) 387 // Invoke the handler 388 reqURL := req.URL.String() 389 start := time.Now() 390 defer func() { 391 s.logger.Debug("request complete", "method", req.Method, "path", reqURL, "duration", time.Now().Sub(start)) 392 }() 393 obj, err := handler(resp, req) 394 395 // Check for an error 396 HAS_ERR: 397 if err != nil { 398 code := 500 399 errMsg := err.Error() 400 if http, ok := err.(HTTPCodedError); ok { 401 code = http.Code() 402 } else if ecode, emsg, ok := structs.CodeFromRPCCodedErr(err); ok { 403 code = ecode 404 errMsg = emsg 405 } else { 406 // RPC errors get wrapped, so manually unwrap by only looking at their suffix 407 if strings.HasSuffix(errMsg, structs.ErrPermissionDenied.Error()) { 408 errMsg = structs.ErrPermissionDenied.Error() 409 code = 403 410 } else if strings.HasSuffix(errMsg, structs.ErrTokenNotFound.Error()) { 411 errMsg = structs.ErrTokenNotFound.Error() 412 code = 403 413 } 414 } 415 416 resp.WriteHeader(code) 417 resp.Write([]byte(errMsg)) 418 s.logger.Error("request failed", "method", req.Method, "path", reqURL, "error", err, "code", code) 419 return 420 } 421 422 prettyPrint := false 423 if v, ok := req.URL.Query()["pretty"]; ok { 424 if len(v) > 0 && (len(v[0]) == 0 || v[0] != "0") { 425 prettyPrint = true 426 } 427 } 428 429 // Write out the JSON object 430 if obj != nil { 431 var buf bytes.Buffer 432 if prettyPrint { 433 enc := codec.NewEncoder(&buf, structs.JsonHandlePretty) 434 err = enc.Encode(obj) 435 if err == nil { 436 buf.Write([]byte("\n")) 437 } 438 } else { 439 enc := codec.NewEncoder(&buf, structs.JsonHandle) 440 err = enc.Encode(obj) 441 } 442 if err != nil { 443 goto HAS_ERR 444 } 445 resp.Header().Set("Content-Type", "application/json") 446 resp.Write(buf.Bytes()) 447 } 448 } 449 return f 450 } 451 452 // wrapNonJSON is used to wrap functions returning non JSON 453 // serializeable data to make them more convenient. It is primarily 454 // responsible for setting nomad headers and logging. 455 // Handler functions are responsible for setting Content-Type Header 456 func (s *HTTPServer) wrapNonJSON(handler func(resp http.ResponseWriter, req *http.Request) ([]byte, error)) func(resp http.ResponseWriter, req *http.Request) { 457 f := func(resp http.ResponseWriter, req *http.Request) { 458 setHeaders(resp, s.agent.config.HTTPAPIResponseHeaders) 459 // Invoke the handler 460 reqURL := req.URL.String() 461 start := time.Now() 462 defer func() { 463 s.logger.Debug("request complete", "method", req.Method, "path", reqURL, "duration", time.Now().Sub(start)) 464 }() 465 obj, err := handler(resp, req) 466 467 // Check for an error 468 if err != nil { 469 code := 500 470 errMsg := err.Error() 471 if http, ok := err.(HTTPCodedError); ok { 472 code = http.Code() 473 } else if ecode, emsg, ok := structs.CodeFromRPCCodedErr(err); ok { 474 code = ecode 475 errMsg = emsg 476 } else { 477 // RPC errors get wrapped, so manually unwrap by only looking at their suffix 478 if strings.HasSuffix(errMsg, structs.ErrPermissionDenied.Error()) { 479 errMsg = structs.ErrPermissionDenied.Error() 480 code = 403 481 } else if strings.HasSuffix(errMsg, structs.ErrTokenNotFound.Error()) { 482 errMsg = structs.ErrTokenNotFound.Error() 483 code = 403 484 } 485 } 486 487 resp.WriteHeader(code) 488 resp.Write([]byte(errMsg)) 489 s.logger.Error("request failed", "method", req.Method, "path", reqURL, "error", err, "code", code) 490 return 491 } 492 493 // write response 494 if obj != nil { 495 resp.Write(obj) 496 } 497 } 498 return f 499 } 500 501 // decodeBody is used to decode a JSON request body 502 func decodeBody(req *http.Request, out interface{}) error { 503 dec := json.NewDecoder(req.Body) 504 return dec.Decode(&out) 505 } 506 507 // setIndex is used to set the index response header 508 func setIndex(resp http.ResponseWriter, index uint64) { 509 resp.Header().Set("X-Nomad-Index", strconv.FormatUint(index, 10)) 510 } 511 512 // setKnownLeader is used to set the known leader header 513 func setKnownLeader(resp http.ResponseWriter, known bool) { 514 s := "true" 515 if !known { 516 s = "false" 517 } 518 resp.Header().Set("X-Nomad-KnownLeader", s) 519 } 520 521 // setLastContact is used to set the last contact header 522 func setLastContact(resp http.ResponseWriter, last time.Duration) { 523 lastMsec := uint64(last / time.Millisecond) 524 resp.Header().Set("X-Nomad-LastContact", strconv.FormatUint(lastMsec, 10)) 525 } 526 527 // setMeta is used to set the query response meta data 528 func setMeta(resp http.ResponseWriter, m *structs.QueryMeta) { 529 setIndex(resp, m.Index) 530 setLastContact(resp, m.LastContact) 531 setKnownLeader(resp, m.KnownLeader) 532 } 533 534 // setHeaders is used to set canonical response header fields 535 func setHeaders(resp http.ResponseWriter, headers map[string]string) { 536 for field, value := range headers { 537 resp.Header().Set(http.CanonicalHeaderKey(field), value) 538 } 539 } 540 541 // parseWait is used to parse the ?wait and ?index query params 542 // Returns true on error 543 func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool { 544 query := req.URL.Query() 545 if wait := query.Get("wait"); wait != "" { 546 dur, err := time.ParseDuration(wait) 547 if err != nil { 548 resp.WriteHeader(400) 549 resp.Write([]byte("Invalid wait time")) 550 return true 551 } 552 b.MaxQueryTime = dur 553 } 554 if idx := query.Get("index"); idx != "" { 555 index, err := strconv.ParseUint(idx, 10, 64) 556 if err != nil { 557 resp.WriteHeader(400) 558 resp.Write([]byte("Invalid index")) 559 return true 560 } 561 b.MinQueryIndex = index 562 } 563 return false 564 } 565 566 // parseConsistency is used to parse the ?stale query params. 567 func parseConsistency(req *http.Request, b *structs.QueryOptions) { 568 query := req.URL.Query() 569 if _, ok := query["stale"]; ok { 570 b.AllowStale = true 571 } 572 } 573 574 // parsePrefix is used to parse the ?prefix query param 575 func parsePrefix(req *http.Request, b *structs.QueryOptions) { 576 query := req.URL.Query() 577 if prefix := query.Get("prefix"); prefix != "" { 578 b.Prefix = prefix 579 } 580 } 581 582 // parseRegion is used to parse the ?region query param 583 func (s *HTTPServer) parseRegion(req *http.Request, r *string) { 584 if other := req.URL.Query().Get("region"); other != "" { 585 *r = other 586 } else if *r == "" { 587 *r = s.agent.config.Region 588 } 589 } 590 591 // parseNamespace is used to parse the ?namespace parameter 592 func parseNamespace(req *http.Request, n *string) { 593 if other := req.URL.Query().Get("namespace"); other != "" { 594 *n = other 595 } else if *n == "" { 596 *n = structs.DefaultNamespace 597 } 598 } 599 600 // parseToken is used to parse the X-Nomad-Token param 601 func (s *HTTPServer) parseToken(req *http.Request, token *string) { 602 if other := req.Header.Get("X-Nomad-Token"); other != "" { 603 *token = other 604 return 605 } 606 } 607 608 // parse is a convenience method for endpoints that need to parse multiple flags 609 func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, r *string, b *structs.QueryOptions) bool { 610 s.parseRegion(req, r) 611 s.parseToken(req, &b.AuthToken) 612 parseConsistency(req, b) 613 parsePrefix(req, b) 614 parseNamespace(req, &b.Namespace) 615 return parseWait(resp, req, b) 616 } 617 618 // parseWriteRequest is a convenience method for endpoints that need to parse a 619 // write request. 620 func (s *HTTPServer) parseWriteRequest(req *http.Request, w *structs.WriteRequest) { 621 parseNamespace(req, &w.Namespace) 622 s.parseToken(req, &w.AuthToken) 623 s.parseRegion(req, &w.Region) 624 } 625 626 // wrapCORS wraps a HandlerFunc in allowCORS and returns a http.Handler 627 func wrapCORS(f func(http.ResponseWriter, *http.Request)) http.Handler { 628 return allowCORS.Handler(http.HandlerFunc(f)) 629 }