diff --git a/Gopkg.lock b/Gopkg.lock index f29660b22..8bb1744bd 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -82,6 +82,12 @@ revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" version = "v1.1" +[[projects]] + name = "github.com/gorilla/handlers" + packages = ["."] + revision = "90663712d74cb411cbef281bc1e08c19d1a76145" + version = "v1.3.0" + [[projects]] name = "github.com/gorilla/mux" packages = ["."] @@ -206,22 +212,14 @@ [[projects]] name = "github.com/shirou/gopsutil" packages = [ - "cpu", "host", "internal/common", "mem", - "net", "process" ] revision = "bfe3c2e8f406bf352bc8df81f98c752224867349" version = "v2.17.11" -[[projects]] - branch = "master" - name = "github.com/shirou/w32" - packages = ["."] - revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b" - [[projects]] branch = "master" name = "github.com/spf13/afero" @@ -304,6 +302,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "61a00007fe7398d958fc8668543d42293f1310f9f9b74c63ff96f5a892eab1e7" + inputs-digest = "8f633d73d966ca439d2fdf3704a41d8ea59be8ed9a2cab0ab73de4b72c5772ba" solver-name = "gps-cdcl" solver-version = 1 diff --git a/ctl/server.go b/ctl/server.go index 4816f6170..1fd40bd40 100644 --- a/ctl/server.go +++ b/ctl/server.go @@ -33,6 +33,9 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) { // TLS SetTLSConfig(flags, &srv.Config.TLS.CertificatePath, &srv.Config.TLS.CertificateKeyPath, &srv.Config.TLS.SkipVerify) + // Handler + flags.StringSliceVarP(&srv.Config.Handler.AllowedOrigins, "handler.allowed-origins", "", []string{"http://localhost/"}, "Comma separated list of allowed origin hosts (for CORS/WebUI).") + // Cluster flags.BoolVarP(&srv.Config.Cluster.Disabled, "cluster.disabled", "", srv.Config.Cluster.Disabled, "Disabled multi-node cluster communication (used for testing)") flags.BoolVarP(&srv.Config.Cluster.Coordinator, "cluster.coordinator", "", srv.Config.Cluster.Coordinator, "Host that will act as cluster coordinator during startup and resizing.") diff --git a/handler.go b/handler.go index a11c5b43f..369ad855f 100644 --- a/handler.go +++ b/handler.go @@ -31,14 +31,16 @@ import ( "time" "github.com/gogo/protobuf/proto" + "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/pilosa/pilosa/internal" + "github.com/pkg/errors" ) // Handler represents an HTTP handler. type Handler struct { - Router *mux.Router + Handler http.Handler FileSystem FileSystem @@ -48,6 +50,8 @@ type Handler struct { validators map[string]*queryValidationSpec API *API + + AllowedOrigins []string } // externalPrefixFlag denotes endpoints that are intended to be exposed to clients. @@ -67,15 +71,36 @@ type errorResponse struct { Error string `json:"error"` } +// HandlerOption is a functional option type for pilosa.Handler +type HandlerOption func(s *Handler) error + +func OptHandlerAllowedOrigins(origins []string) HandlerOption { + return func(h *Handler) error { + h.Handler = handlers.CORS( + handlers.AllowedOrigins(origins), + handlers.AllowedHeaders([]string{"Content-Type"}), + )(h.Handler) + return nil + } +} + // NewHandler returns a new instance of Handler with a default logger. -func NewHandler() *Handler { +func NewHandler(opts ...HandlerOption) (*Handler, error) { handler := &Handler{ FileSystem: NopFileSystem, Logger: NopLogger, } - handler.Router = NewRouter(handler) + handler.Handler = NewRouter(handler) handler.populateValidators() - return handler + + for _, opt := range opts { + err := opt(handler) + if err != nil { + return nil, errors.Wrap(err, "applying option") + } + } + + return handler, nil } func (h *Handler) populateValidators() { @@ -183,7 +208,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { }() t := time.Now() - h.Router.ServeHTTP(w, r) + h.Handler.ServeHTTP(w, r) dif := time.Since(t) // Calculate per request StatsD metrics when the handler is fully configured. diff --git a/handler_test.go b/handler_test.go index b39b207c5..36a002b5a 100644 --- a/handler_test.go +++ b/handler_test.go @@ -37,7 +37,7 @@ import ( ) func TestHandlerPanics(t *testing.T) { - h := test.NewHandler() + h := test.MustNewHandler() bufLogger := test.NewBufferLogger() h.Handler.Logger = bufLogger @@ -65,7 +65,7 @@ func TestHandler_NotFound(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder @@ -100,7 +100,7 @@ func TestHandler_Schema(t *testing.T) { t.Fatal(err) } - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(1) w := httptest.NewRecorder() @@ -139,7 +139,7 @@ func TestHandler_Status(t *testing.T) { t.Fatal(err) } - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(1) h.API.Cluster.SetState(pilosa.ClusterStateNormal) @@ -158,7 +158,7 @@ func TestHandler_Status(t *testing.T) { func TestHandler_Info(t *testing.T) { s := test.NewServer() defer s.Close() - h := test.NewHandler() + h := test.MustNewHandler() w := httptest.NewRecorder() h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/info", nil)) @@ -173,7 +173,7 @@ func TestHandler_Info(t *testing.T) { func TestHandler_ClusterResizeAbort(t *testing.T) { t.Run("No resize job", func(t *testing.T) { - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Cluster.SetState(pilosa.ClusterStateResizing) @@ -202,7 +202,7 @@ func TestHandler_MaxSlices(t *testing.T) { hldr.MustCreateFragmentIfNotExists("i1", "f1", pilosa.ViewStandard, 0).MustSetBits(40, (0*SliceWidth)+2) hldr.MustCreateFragmentIfNotExists("i1", "f1", pilosa.ViewStandard, 0).MustSetBits(40, (0*SliceWidth)+8) - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(1) w := httptest.NewRecorder() @@ -243,7 +243,7 @@ func TestHandler_MaxSlices_Inverse(t *testing.T) { t.Fatal(err) } - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(1) w := httptest.NewRecorder() @@ -260,7 +260,7 @@ func TestHandler_Query_Args_URL(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -288,7 +288,7 @@ func TestHandler_Query_Args_Protobuf(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -328,7 +328,7 @@ func TestHandler_Query_Args_Err(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder @@ -341,7 +341,7 @@ func TestHandler_Query_Args_Err(t *testing.T) { } func TestHandler_Query_Params_Err(t *testing.T) { w := httptest.NewRecorder() - test.NewHandler().ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/idx0/query?slices=0,1&db=sample", strings.NewReader("Bitmap(id=100)"))) + test.MustNewHandler().ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/idx0/query?slices=0,1&db=sample", strings.NewReader("Bitmap(id=100)"))) if w.Code != http.StatusBadRequest { t.Fatalf("unexpected status code: %d", w.Code) } else if body := w.Body.String(); body != `{"error":"db is not a valid argument"}`+"\n" { @@ -355,7 +355,7 @@ func TestHandler_Query_Uint64_JSON(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -376,7 +376,7 @@ func TestHandler_Query_Uint64_Protobuf(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -406,7 +406,7 @@ func TestHandler_Query_Bitmap_JSON(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -439,7 +439,7 @@ func TestHandler_Query_Row_ColumnAttrs_JSON(t *testing.T) { t.Fatal(err) } - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(1) h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -462,7 +462,7 @@ func TestHandler_Query_Row_Protobuf(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -510,7 +510,7 @@ func TestHandler_Query_Row_ColumnAttrs_Protobuf(t *testing.T) { t.Fatal(err) } - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(1) h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -571,7 +571,7 @@ func TestHandler_Query_Pairs_JSON(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -595,7 +595,7 @@ func TestHandler_Query_Pairs_Protobuf(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -628,7 +628,7 @@ func TestHandler_Query_Err_JSON(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -649,7 +649,7 @@ func TestHandler_Query_Err_Protobuf(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder h.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -677,7 +677,7 @@ func TestHandler_Query_MethodNotAllowed(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder w := httptest.NewRecorder() @@ -692,7 +692,7 @@ func TestHandler_Query_ErrParse(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder w := httptest.NewRecorder() @@ -749,7 +749,7 @@ func TestHandler_DeleteFrame(t *testing.T) { t.Fatal(err) } - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(1) w := httptest.NewRecorder() @@ -1155,7 +1155,7 @@ func TestHandler_Version(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder @@ -1178,7 +1178,7 @@ func TestHandler_Fragment_Nodes(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(3) h.API.Cluster.ReplicaN = 2 @@ -1214,7 +1214,7 @@ func TestHandler_Expvars(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Cluster = test.NewCluster(1) h.API.Holder = hldr.Holder w := httptest.NewRecorder() @@ -1237,7 +1237,7 @@ func TestHandler_RecalculateCaches(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(1) @@ -1253,7 +1253,7 @@ func TestHandler_WebUI(t *testing.T) { hldr := test.MustOpenHolder() defer hldr.Close() - h := test.NewHandler() + h := test.MustNewHandler() h.API.Holder = hldr.Holder h.API.Cluster = test.NewCluster(1) h.FileSystem = &statik.FileSystem{} @@ -1276,3 +1276,41 @@ func TestHandler_WebUI(t *testing.T) { t.Fatalf("WebUI is not being served correctly.") } } + +func TestHandler_CORS(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() + + s := test.NewServer() + s.Handler.API.Holder = hldr.Holder + defer s.Close() + + // No CORS config present, so should fail + handler := test.MustNewHandler() + + req := test.MustNewHTTPRequest("OPTIONS", "/index/foo/query", nil) + req.Header.Add("Origin", "http://test/") + req.Header.Add("Access-Control-Request-Method", "POST") + + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + result := w.Result() + + // This handler does not support CORS, return Method Not Allowed (405) + if result.StatusCode != 405 { + t.Fatalf("CORS preflight status should be 405, but is %v", result.StatusCode) + } + + // CORS config should allow preflight response + handler = test.MustNewHandler(pilosa.OptHandlerAllowedOrigins([]string{"http://test/"})) + w = httptest.NewRecorder() + handler.ServeHTTP(w, req) + result = w.Result() + + if result.StatusCode != 200 { + t.Fatalf("CORS preflight status should be 200, but is %v", result.StatusCode) + } + if w.HeaderMap["Access-Control-Allow-Origin"][0] != "http://test/" { + t.Fatal("CORS header not present") + } +} diff --git a/server.go b/server.go index 2731ad71a..aa34ec539 100644 --- a/server.go +++ b/server.go @@ -203,11 +203,15 @@ func OptServerURI(uri *URI) ServerOption { // NewServer returns a new instance of Server. func NewServer(opts ...ServerOption) (*Server, error) { + handler, err := NewHandler() + if err != nil { + return nil, errors.Wrap(err, "initializing handler") + } s := &Server{ closing: make(chan struct{}), Cluster: NewCluster(), Holder: NewHolder(), - handler: NewHandler(), + handler: handler, Broadcaster: NopBroadcaster, BroadcastReceiver: NopBroadcastReceiver, diagnostics: NewDiagnosticsCollector(DefaultDiagnosticServer), diff --git a/server/config.go b/server/config.go index 2e2feeeb2..b8e7cf78b 100644 --- a/server/config.go +++ b/server/config.go @@ -57,6 +57,11 @@ type Config struct { // Verbose toggles verbose logging which can be useful for debugging. Verbose bool `toml:"verbose"` + // HTTP Handler options + Handler struct { + AllowedOrigins []string `toml:"allowed-origins"` + } + // TLS TLS TLSConfig diff --git a/server/server.go b/server/server.go index 27c6c06e5..345807ec2 100644 --- a/server/server.go +++ b/server/server.go @@ -165,7 +165,10 @@ func (m *Command) SetupServer() error { } m.logger.Printf("%s %s, build time %s\n", productName, pilosa.Version, pilosa.BuildTime) - handler := pilosa.NewHandler() + handler, err := pilosa.NewHandler(pilosa.OptHandlerAllowedOrigins(m.Config.Handler.AllowedOrigins)) + if err != nil { + return errors.Wrap(err, "wrapping handler") + } handler.Logger = m.logger handler.FileSystem = &statik.FileSystem{} handler.API = pilosa.NewAPI() diff --git a/test/handler.go b/test/handler.go index 50b081812..401fa64ab 100644 --- a/test/handler.go +++ b/test/handler.go @@ -36,9 +36,13 @@ type Handler struct { } // NewHandler returns a new instance of Handler. -func NewHandler() *Handler { +func NewHandler(opts ...pilosa.HandlerOption) (*Handler, error) { + handler, err := pilosa.NewHandler(opts...) + if err != nil { + return nil, err + } h := &Handler{ - Handler: pilosa.NewHandler(), + Handler: handler, } h.API = pilosa.NewAPI() h.Handler.API = h.API @@ -47,6 +51,15 @@ func NewHandler() *Handler { // Handler test messages can no-op. h.API.Broadcaster = pilosa.NopBroadcaster + return h, nil +} + +// MustNewHandler returns a new instance of Handler. +func MustNewHandler(opts ...pilosa.HandlerOption) *Handler { + h, err := NewHandler(opts...) + if err != nil { + panic(err) + } return h } @@ -70,8 +83,12 @@ type Server struct { // NewServer returns a test server running on a random port. func NewServer() *Server { + handler, err := NewHandler() + if err != nil { + panic(err) + } s := &Server{ - Handler: NewHandler(), + Handler: handler, } s.Server = httptest.NewServer(s.Handler.Handler)