From e0fe22da4d9ca442c133e8e9ea153f075ecb4910 Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Tue, 28 Jul 2015 22:59:36 -0700 Subject: [PATCH] more serious CORS tests. this commit introduces more serious CORS tests that check status response codes, and run real HTTP requests. License: MIT Signed-off-by: Juan Batiz-Benet --- commands/http/handler_test.go | 359 +++++++++++++++++++++++++++++----- core/mock/mock.go | 34 +++- 2 files changed, 342 insertions(+), 51 deletions(-) diff --git a/commands/http/handler_test.go b/commands/http/handler_test.go index 17a2ba36dcd..4539d16415e 100644 --- a/commands/http/handler_test.go +++ b/commands/http/handler_test.go @@ -3,79 +3,338 @@ package http import ( "net/http" "net/http/httptest" + "net/url" "testing" cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" - "github.com/ipfs/go-ipfs/commands" + cmds "github.com/ipfs/go-ipfs/commands" + ipfscmd "github.com/ipfs/go-ipfs/core/commands" + coremock "github.com/ipfs/go-ipfs/core/mock" ) func assertHeaders(t *testing.T, resHeaders http.Header, reqHeaders map[string]string) { for name, value := range reqHeaders { if resHeaders.Get(name) != value { - t.Errorf("Invalid header `%s', wanted `%s', got `%s'", name, value, resHeaders.Get(name)) + t.Errorf("Invalid header '%s', wanted '%s', got '%s'", name, value, resHeaders.Get(name)) } } } -func originCfg(origin string) *ServerConfig { +func assertStatus(t *testing.T, actual, expected int) { + if actual != expected { + t.Errorf("Expected status: %d got: %d", expected, actual) + } +} + +func originCfg(origins []string) *ServerConfig { return &ServerConfig{ CORSOpts: &cors.Options{ - AllowedOrigins: []string{origin}, + AllowedOrigins: origins, + }, + } +} + +type testCase struct { + Method string + Path string + Code int + Origin string + Referer string + AllowOrigins []string + ReqHeaders map[string]string + ResHeaders map[string]string +} + +func getTestServer(t *testing.T, origins []string) *httptest.Server { + cmdsCtx, err := coremock.MockCmdsCtx() + if err != nil { + t.Error("failure to initialize mock cmds ctx", err) + return nil + } + + cmdRoot := &cmds.Command{ + Subcommands: map[string]*cmds.Command{ + "version": ipfscmd.VersionCmd, }, } + + handler := NewHandler(cmdsCtx, cmdRoot, originCfg(origins)) + return httptest.NewServer(handler) } -func TestDisallowedOrigin(t *testing.T) { - res := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "http://example.com/foo", nil) - req.Header.Add("Origin", "http://barbaz.com") - - handler := NewHandler(commands.Context{}, nil, originCfg("")) - handler.ServeHTTP(res, req) - - assertHeaders(t, res.Header(), map[string]string{ - "Access-Control-Allow-Origin": "", - "Access-Control-Allow-Methods": "", - "Access-Control-Allow-Credentials": "", - "Access-Control-Max-Age": "", - "Access-Control-Expose-Headers": "", - }) +func (tc *testCase) test(t *testing.T) { + // defaults + method := tc.Method + if method == "" { + method = "GET" + } + + path := tc.Path + if path == "" { + path = "/api/v0/version" + } + + expectCode := tc.Code + if expectCode == 0 { + expectCode = 200 + } + + // request + req, err := http.NewRequest(method, path, nil) + if err != nil { + t.Error(err) + return + } + + for k, v := range tc.ReqHeaders { + req.Header.Add(k, v) + } + if tc.Origin != "" { + req.Header.Add("Origin", tc.Origin) + } + if tc.Referer != "" { + req.Header.Add("Referer", tc.Referer) + } + + // server + server := getTestServer(t, tc.AllowOrigins) + if server == nil { + return + } + defer server.Close() + + req.URL, err = url.Parse(server.URL + path) + if err != nil { + t.Error(err) + return + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Error(err) + return + } + + // checks + t.Log("GET", server.URL+path, req.Header, res.Header) + assertHeaders(t, res.Header, tc.ResHeaders) + assertStatus(t, res.StatusCode, expectCode) +} + +func TestDisallowedOrigins(t *testing.T) { + gtc := func(origin string, allowedOrigins []string) testCase { + return testCase{ + Origin: origin, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: "", + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusForbidden, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", nil), + gtc("http://barbaz.com", []string{"http://localhost"}), + gtc("http://127.0.0.1", []string{"http://localhost"}), + gtc("http://localhost", []string{"http://127.0.0.1"}), + gtc("http://127.0.0.1:1234", nil), + gtc("http://localhost:1234", nil), + } + + for _, tc := range tcs { + tc.test(t) + } +} + +func TestAllowedOrigins(t *testing.T) { + gtc := func(origin string, allowedOrigins []string) testCase { + return testCase{ + Origin: origin, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: origin, + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusOK, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", []string{"http://barbaz.com", "http://localhost"}), + gtc("http://localhost", []string{"http://barbaz.com", "http://localhost"}), + gtc("http://localhost", nil), + gtc("http://127.0.0.1", nil), + } + + for _, tc := range tcs { + tc.test(t) + } } func TestWildcardOrigin(t *testing.T) { - res := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "http://example.com/foo", nil) - req.Header.Add("Origin", "http://foobar.com") - - handler := NewHandler(commands.Context{}, nil, originCfg("*")) - handler.ServeHTTP(res, req) - - assertHeaders(t, res.Header(), map[string]string{ - "Access-Control-Allow-Origin": "http://foobar.com", - "Access-Control-Allow-Methods": "", - "Access-Control-Allow-Headers": "", - "Access-Control-Allow-Credentials": "", - "Access-Control-Max-Age": "", - "Access-Control-Expose-Headers": "", - }) + gtc := func(origin string, allowedOrigins []string) testCase { + return testCase{ + Origin: origin, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: origin, + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusOK, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", []string{"*"}), + gtc("http://barbaz.com", []string{"http://localhost", "*"}), + gtc("http://127.0.0.1", []string{"http://localhost", "*"}), + gtc("http://localhost", []string{"http://127.0.0.1", "*"}), + gtc("http://127.0.0.1", []string{"*"}), + gtc("http://localhost", []string{"*"}), + gtc("http://127.0.0.1:1234", []string{"*"}), + gtc("http://localhost:1234", []string{"*"}), + } + + for _, tc := range tcs { + tc.test(t) + } +} + +func TestDisallowedReferer(t *testing.T) { + gtc := func(referer string, allowedOrigins []string) testCase { + return testCase{ + Origin: "http://localhost", + Referer: referer, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: "http://localhost", + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusForbidden, + } + } + + tcs := []testCase{ + gtc("http://foobar.com", nil), + gtc("http://localhost:1234", nil), + gtc("http://127.0.0.1:1234", nil), + } + + for _, tc := range tcs { + tc.test(t) + } +} + +func TestAllowedReferer(t *testing.T) { + gtc := func(referer string, allowedOrigins []string) testCase { + return testCase{ + Origin: "http://localhost", + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: "http://localhost", + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusOK, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", []string{"http://barbaz.com", "http://localhost"}), + gtc("http://localhost", []string{"http://barbaz.com", "http://localhost"}), + gtc("http://localhost", nil), + gtc("http://127.0.0.1", nil), + } + + for _, tc := range tcs { + tc.test(t) + } +} + +func TestWildcardReferer(t *testing.T) { + gtc := func(origin string, allowedOrigins []string) testCase { + return testCase{ + Origin: origin, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: origin, + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusOK, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", []string{"*"}), + gtc("http://barbaz.com", []string{"http://localhost", "*"}), + gtc("http://127.0.0.1", []string{"http://localhost", "*"}), + gtc("http://localhost", []string{"http://127.0.0.1", "*"}), + gtc("http://127.0.0.1", []string{"*"}), + gtc("http://localhost", []string{"*"}), + gtc("http://127.0.0.1:1234", []string{"*"}), + gtc("http://localhost:1234", []string{"*"}), + } + + for _, tc := range tcs { + tc.test(t) + } } func TestAllowedMethod(t *testing.T) { - res := httptest.NewRecorder() - req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) - req.Header.Add("Origin", "http://www.foobar.com") - req.Header.Add("Access-Control-Request-Method", "PUT") - - handler := NewHandler(commands.Context{}, nil, originCfg("http://www.foobar.com")) - handler.ServeHTTP(res, req) - - assertHeaders(t, res.Header(), map[string]string{ - "Access-Control-Allow-Origin": "http://www.foobar.com", - "Access-Control-Allow-Methods": "PUT", - "Access-Control-Allow-Headers": "", - "Access-Control-Allow-Credentials": "", - "Access-Control-Max-Age": "", - "Access-Control-Expose-Headers": "", - }) + gtc := func(method string, ok bool) testCase { + code := http.StatusOK + hdrs := map[string]string{ + ACAOrigin: "http://localhost", + ACAMethods: method, + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + } + + if !ok { + hdrs[ACAOrigin] = "" + hdrs[ACAMethods] = "" + } + + return testCase{ + Method: "OPTIONS", + Origin: "http://localhost", + AllowOrigins: []string{"*"}, + ReqHeaders: map[string]string{ + "Access-Control-Request-Method": method, + }, + ResHeaders: hdrs, + Code: code, + } + } + + tcs := []testCase{ + gtc("PUT", true), + gtc("GET", true), + gtc("FOOBAR", false), + } + + for _, tc := range tcs { + tc.test(t) + } } diff --git a/core/mock/mock.go b/core/mock/mock.go index 14f90f56c8e..0145af6bb82 100644 --- a/core/mock/mock.go +++ b/core/mock/mock.go @@ -6,6 +6,7 @@ import ( context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context" "github.com/ipfs/go-ipfs/blocks/blockstore" blockservice "github.com/ipfs/go-ipfs/blockservice" + commands "github.com/ipfs/go-ipfs/commands" core "github.com/ipfs/go-ipfs/core" "github.com/ipfs/go-ipfs/exchange/offline" mdag "github.com/ipfs/go-ipfs/merkledag" @@ -27,7 +28,7 @@ import ( // NewMockNode constructs an IpfsNode for use in tests. func NewMockNode() (*core.IpfsNode, error) { - ctx := context.TODO() + ctx := context.Background() // Generate Identity ident, err := testutil.RandIdentity() @@ -82,3 +83,34 @@ func NewMockNode() (*core.IpfsNode, error) { return nd, nil } + +func MockCmdsCtx() (commands.Context, error) { + // Generate Identity + ident, err := testutil.RandIdentity() + if err != nil { + return commands.Context{}, err + } + p := ident.ID() + + conf := config.Config{ + Identity: config.Identity{ + PeerID: p.String(), + }, + } + + node, err := core.NewIPFSNode(context.Background(), core.Offline(&repo.Mock{ + D: ds2.CloserWrap(syncds.MutexWrap(datastore.NewMapDatastore())), + C: conf, + })) + + return commands.Context{ + Online: true, + ConfigRoot: "/tmp/.mockipfsconfig", + LoadConfig: func(path string) (*config.Config, error) { + return &conf, nil + }, + ConstructNode: func() (*core.IpfsNode, error) { + return node, nil + }, + }, nil +}