diff --git a/NOTICES.txt b/NOTICES.txt new file mode 100644 index 0000000000..f28b102663 --- /dev/null +++ b/NOTICES.txt @@ -0,0 +1,19 @@ +M3DB includes derived work from etcd (https://github.com/etcd-io/etcd) under the Apache License 2.0: + + Copyright 2015 The etcd Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The derived work can be found in the files: +- src/x/net/http/cors/cors.go +- src/x/net/http/cors/cors_test.go diff --git a/src/query/api/v1/httpd/handler.go b/src/query/api/v1/httpd/handler.go index d8803a2e91..4aa05fbd20 100644 --- a/src/query/api/v1/httpd/handler.go +++ b/src/query/api/v1/httpd/handler.go @@ -47,8 +47,8 @@ import ( "github.com/m3db/m3/src/query/storage/m3" "github.com/m3db/m3/src/query/util/logging" xhttp "github.com/m3db/m3/src/x/net/http" + "github.com/m3db/m3/src/x/net/http/cors" - "github.com/coreos/etcd/pkg/cors" "github.com/gorilla/mux" "github.com/uber-go/tally" ) @@ -98,9 +98,9 @@ func NewHandler( r := mux.NewRouter() // apply middleware. Just CORS for now, but we could add more here as needed. - withMiddleware := &cors.CORSHandler{ + withMiddleware := &cors.Handler{ Handler: r, - Info: &cors.CORSInfo{ + Info: &cors.Info{ "*": true, }, } diff --git a/src/x/net/http/cors/cors.go b/src/x/net/http/cors/cors.go new file mode 100644 index 0000000000..61bd8de35a --- /dev/null +++ b/src/x/net/http/cors/cors.go @@ -0,0 +1,103 @@ +// Copyright (c) 2018 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Derived from https://github.com/etcd-io/etcd/tree/v3.2.10/pkg/cors under +// http://www.apache.org/licenses/LICENSE-2.0#redistribution . +// See https://github.com/m3db/m3/blob/master/NOTICES.txt for the original copyright. + +// Package cors handles cross-origin HTTP requests (CORS). +package cors + +import ( + "fmt" + "net/http" + "net/url" + "sort" + "strings" +) + +// Info represents a set of allowed origins. +type Info map[string]bool + +// Set implements the flag.Value interface to allow users to define a list of CORS origins +func (ci *Info) Set(s string) error { + m := make(map[string]bool) + for _, v := range strings.Split(s, ",") { + v = strings.TrimSpace(v) + if v == "" { + continue + } + if v != "*" { + if _, err := url.Parse(v); err != nil { + return fmt.Errorf("Invalid CORS origin: %s", err) + } + } + m[v] = true + + } + *ci = Info(m) + return nil +} + +func (ci *Info) String() string { + o := make([]string, 0) + for k := range *ci { + o = append(o, k) + } + sort.StringSlice(o).Sort() + return strings.Join(o, ",") +} + +// OriginAllowed determines whether the server will allow a given CORS origin. +func (ci Info) OriginAllowed(origin string) bool { + return ci["*"] || ci[origin] +} + +// Handler wraps an http.Handler instance to provide configurable CORS support. CORS headers will be added to all +// responses. +type Handler struct { + Handler http.Handler + Info *Info +} + +// addHeader adds the correct cors headers given an origin +func (h *Handler) addHeader(w http.ResponseWriter, origin string) { + w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + w.Header().Add("Access-Control-Allow-Origin", origin) + w.Header().Add("Access-Control-Allow-Headers", "accept, content-type, authorization") +} + +// ServeHTTP adds the correct CORS headers based on the origin and returns immediately +// with a 200 OK if the method is OPTIONS. +func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // Write CORS header. + if h.Info.OriginAllowed("*") { + h.addHeader(w, "*") + } else if origin := req.Header.Get("Origin"); h.Info.OriginAllowed(origin) { + h.addHeader(w, origin) + } + + if req.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + h.Handler.ServeHTTP(w, req) +} diff --git a/src/x/net/http/cors/cors_test.go b/src/x/net/http/cors/cors_test.go new file mode 100644 index 0000000000..ff510f4905 --- /dev/null +++ b/src/x/net/http/cors/cors_test.go @@ -0,0 +1,135 @@ +// Copyright (c) 2018 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// Derived from https://github.com/etcd-io/etcd/tree/v3.2.10/pkg/cors under +// http://www.apache.org/licenses/LICENSE-2.0#redistribution . +// See https://github.com/m3db/m3/blob/master/NOTICES.txt for the original copyright. + +package cors + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestCORSInfo(t *testing.T) { + tests := []struct { + s string + winfo Info + ws string + }{ + {"", Info{}, ""}, + {"http://127.0.0.1", Info{"http://127.0.0.1": true}, "http://127.0.0.1"}, + {"*", Info{"*": true}, "*"}, + // with space around + {" http://127.0.0.1 ", Info{"http://127.0.0.1": true}, "http://127.0.0.1"}, + // multiple addrs + { + "http://127.0.0.1,http://127.0.0.2", + Info{"http://127.0.0.1": true, "http://127.0.0.2": true}, + "http://127.0.0.1,http://127.0.0.2", + }, + } + for i, tt := range tests { + info := Info{} + if err := info.Set(tt.s); err != nil { + t.Errorf("#%d: set error = %v, want nil", i, err) + } + if !reflect.DeepEqual(info, tt.winfo) { + t.Errorf("#%d: info = %v, want %v", i, info, tt.winfo) + } + if g := info.String(); g != tt.ws { + t.Errorf("#%d: info string = %s, want %s", i, g, tt.ws) + } + } +} + +func TestCORSInfoOriginAllowed(t *testing.T) { + tests := []struct { + set string + origin string + wallowed bool + }{ + {"http://127.0.0.1,http://127.0.0.2", "http://127.0.0.1", true}, + {"http://127.0.0.1,http://127.0.0.2", "http://127.0.0.2", true}, + {"http://127.0.0.1,http://127.0.0.2", "*", false}, + {"http://127.0.0.1,http://127.0.0.2", "http://127.0.0.3", false}, + {"*", "*", true}, + {"*", "http://127.0.0.1", true}, + } + for i, tt := range tests { + info := Info{} + if err := info.Set(tt.set); err != nil { + t.Errorf("#%d: set error = %v, want nil", i, err) + } + if g := info.OriginAllowed(tt.origin); g != tt.wallowed { + t.Errorf("#%d: allowed = %v, want %v", i, g, tt.wallowed) + } + } +} + +func TestCORSHandler(t *testing.T) { + info := &Info{} + if err := info.Set("http://127.0.0.1,http://127.0.0.2"); err != nil { + t.Fatalf("unexpected set error: %v", err) + } + h := &Handler{ + Handler: http.NotFoundHandler(), + Info: info, + } + + header := func(origin string) http.Header { + return http.Header{ + "Access-Control-Allow-Methods": []string{"POST, GET, OPTIONS, PUT, DELETE"}, + "Access-Control-Allow-Origin": []string{origin}, + "Access-Control-Allow-Headers": []string{"accept, content-type, authorization"}, + } + } + tests := []struct { + method string + origin string + wcode int + wheader http.Header + }{ + {"GET", "http://127.0.0.1", http.StatusNotFound, header("http://127.0.0.1")}, + {"GET", "http://127.0.0.2", http.StatusNotFound, header("http://127.0.0.2")}, + {"GET", "http://127.0.0.3", http.StatusNotFound, http.Header{}}, + {"OPTIONS", "http://127.0.0.1", http.StatusOK, header("http://127.0.0.1")}, + } + for i, tt := range tests { + rr := httptest.NewRecorder() + req := &http.Request{ + Method: tt.method, + Header: http.Header{"Origin": []string{tt.origin}}, + } + h.ServeHTTP(rr, req) + if rr.Code != tt.wcode { + t.Errorf("#%d: code = %v, want %v", i, rr.Code, tt.wcode) + } + // it is set by http package, and there is no need to test it + rr.HeaderMap.Del("Content-Type") + rr.HeaderMap.Del("X-Content-Type-Options") + if !reflect.DeepEqual(rr.HeaderMap, tt.wheader) { + t.Errorf("#%d: header = %+v, want %+v", i, rr.HeaderMap, tt.wheader) + } + } +}