github.com/core-coin/go-core/v2@v2.1.9/node/rpcstack_test.go (about)

     1  // Copyright 2020 by the Authors
     2  // This file is part of the go-core library.
     3  //
     4  // The go-core library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-core library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-core library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package node
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"net/http"
    23  	"net/url"
    24  	"strconv"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/golang-jwt/jwt/v4"
    30  	"github.com/gorilla/websocket"
    31  	"github.com/stretchr/testify/assert"
    32  
    33  	"github.com/core-coin/go-core/v2/internal/testlog"
    34  	"github.com/core-coin/go-core/v2/log"
    35  	"github.com/core-coin/go-core/v2/rpc"
    36  )
    37  
    38  // TestCorsHandler makes sure CORS are properly handled on the http server.
    39  func TestCorsHandler(t *testing.T) {
    40  	srv := createAndStartServer(t, &httpConfig{CorsAllowedOrigins: []string{"test", "test.com"}}, false, &wsConfig{})
    41  	defer srv.stop()
    42  	url := "http://" + srv.listenAddr()
    43  
    44  	resp := rpcRequest(t, url, "origin", "test.com")
    45  	assert.Equal(t, "test.com", resp.Header.Get("Access-Control-Allow-Origin"))
    46  
    47  	resp2 := rpcRequest(t, url, "origin", "bad")
    48  	assert.Equal(t, "", resp2.Header.Get("Access-Control-Allow-Origin"))
    49  }
    50  
    51  // TestVhosts makes sure vhosts are properly handled on the http server.
    52  func TestVhosts(t *testing.T) {
    53  	srv := createAndStartServer(t, &httpConfig{Vhosts: []string{"test"}}, false, &wsConfig{})
    54  	defer srv.stop()
    55  	url := "http://" + srv.listenAddr()
    56  
    57  	resp := rpcRequest(t, url, "host", "test")
    58  	assert.Equal(t, resp.StatusCode, http.StatusOK)
    59  
    60  	resp2 := rpcRequest(t, url, "host", "bad")
    61  	assert.Equal(t, resp2.StatusCode, http.StatusForbidden)
    62  }
    63  
    64  type originTest struct {
    65  	spec    string
    66  	expOk   []string
    67  	expFail []string
    68  }
    69  
    70  // splitAndTrim splits input separated by a comma
    71  // and trims excessive white space from the substrings.
    72  // Copied over from flags.go
    73  func splitAndTrim(input string) (ret []string) {
    74  	l := strings.Split(input, ",")
    75  	for _, r := range l {
    76  		r = strings.TrimSpace(r)
    77  		if len(r) > 0 {
    78  			ret = append(ret, r)
    79  		}
    80  	}
    81  	return ret
    82  }
    83  
    84  // TestWebsocketOrigins makes sure the websocket origins are properly handled on the websocket server.
    85  func TestWebsocketOrigins(t *testing.T) {
    86  	tests := []originTest{
    87  		{
    88  			spec: "*", // allow all
    89  			expOk: []string{"", "http://test", "https://test", "http://test:8540", "https://test:8540",
    90  				"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
    91  		},
    92  		{
    93  			spec:    "test",
    94  			expOk:   []string{"http://test", "https://test", "http://test:8540", "https://test:8540"},
    95  			expFail: []string{"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
    96  		},
    97  		// scheme tests
    98  		{
    99  			spec:  "https://test",
   100  			expOk: []string{"https://test", "https://test:9999"},
   101  			expFail: []string{
   102  				"test",                                // no scheme, required by spec
   103  				"http://test",                         // wrong scheme
   104  				"http://test.foo", "https://a.test.x", // subdomain variatoins
   105  				"http://testx:8540", "https://xtest:8540"},
   106  		},
   107  		// ip tests
   108  		{
   109  			spec:  "https://12.34.56.78",
   110  			expOk: []string{"https://12.34.56.78", "https://12.34.56.78:8540"},
   111  			expFail: []string{
   112  				"http://12.34.56.78",     // wrong scheme
   113  				"http://12.34.56.78:443", // wrong scheme
   114  				"http://1.12.34.56.78",   // wrong 'domain name'
   115  				"http://12.34.56.78.a",   // wrong 'domain name'
   116  				"https://87.65.43.21", "http://87.65.43.21:8540", "https://87.65.43.21:8540"},
   117  		},
   118  		// port tests
   119  		{
   120  			spec:  "test:8540",
   121  			expOk: []string{"http://test:8540", "https://test:8540"},
   122  			expFail: []string{
   123  				"http://test", "https://test", // spec says port required
   124  				"http://test:8541", "https://test:8541", // wrong port
   125  				"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
   126  		},
   127  		// scheme and port
   128  		{
   129  			spec:  "https://test:8540",
   130  			expOk: []string{"https://test:8540"},
   131  			expFail: []string{
   132  				"https://test",                          // missing port
   133  				"http://test",                           // missing port, + wrong scheme
   134  				"http://test:8540",                      // wrong scheme
   135  				"http://test:8541", "https://test:8541", // wrong port
   136  				"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
   137  		},
   138  		// several allowed origins
   139  		{
   140  			spec: "localhost,http://127.0.0.1",
   141  			expOk: []string{"localhost", "http://localhost", "https://localhost:8443",
   142  				"http://127.0.0.1", "http://127.0.0.1:8080"},
   143  			expFail: []string{
   144  				"https://127.0.0.1", // wrong scheme
   145  				"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
   146  		},
   147  	}
   148  	for _, tc := range tests {
   149  		srv := createAndStartServer(t, &httpConfig{}, true, &wsConfig{Origins: splitAndTrim(tc.spec)})
   150  		url := fmt.Sprintf("ws://%v", srv.listenAddr())
   151  		for _, origin := range tc.expOk {
   152  			if err := wsRequest(t, url, "Origin", origin); err != nil {
   153  				t.Errorf("spec '%v', origin '%v': expected ok, got %v", tc.spec, origin, err)
   154  			}
   155  		}
   156  		for _, origin := range tc.expFail {
   157  			if err := wsRequest(t, url, "Origin", origin); err == nil {
   158  				t.Errorf("spec '%v', origin '%v': expected not to allow,  got ok", tc.spec, origin)
   159  			}
   160  		}
   161  		srv.stop()
   162  	}
   163  }
   164  
   165  // TestIsWebsocket tests if an incoming websocket upgrade request is handled properly.
   166  func TestIsWebsocket(t *testing.T) {
   167  	r, _ := http.NewRequest("GET", "/", nil)
   168  
   169  	assert.False(t, isWebsocket(r))
   170  	r.Header.Set("upgrade", "websocket")
   171  	assert.False(t, isWebsocket(r))
   172  	r.Header.Set("connection", "upgrade")
   173  	assert.True(t, isWebsocket(r))
   174  	r.Header.Set("connection", "upgrade,keep-alive")
   175  	assert.True(t, isWebsocket(r))
   176  	r.Header.Set("connection", " UPGRADE,keep-alive")
   177  	assert.True(t, isWebsocket(r))
   178  }
   179  
   180  func Test_checkPath(t *testing.T) {
   181  	tests := []struct {
   182  		req      *http.Request
   183  		prefix   string
   184  		expected bool
   185  	}{
   186  		{
   187  			req:      &http.Request{URL: &url.URL{Path: "/test"}},
   188  			prefix:   "/test",
   189  			expected: true,
   190  		},
   191  		{
   192  			req:      &http.Request{URL: &url.URL{Path: "/testing"}},
   193  			prefix:   "/test",
   194  			expected: true,
   195  		},
   196  		{
   197  			req:      &http.Request{URL: &url.URL{Path: "/"}},
   198  			prefix:   "/test",
   199  			expected: false,
   200  		},
   201  		{
   202  			req:      &http.Request{URL: &url.URL{Path: "/fail"}},
   203  			prefix:   "/test",
   204  			expected: false,
   205  		},
   206  		{
   207  			req:      &http.Request{URL: &url.URL{Path: "/"}},
   208  			prefix:   "",
   209  			expected: true,
   210  		},
   211  		{
   212  			req:      &http.Request{URL: &url.URL{Path: "/fail"}},
   213  			prefix:   "",
   214  			expected: false,
   215  		},
   216  		{
   217  			req:      &http.Request{URL: &url.URL{Path: "/"}},
   218  			prefix:   "/",
   219  			expected: true,
   220  		},
   221  		{
   222  			req:      &http.Request{URL: &url.URL{Path: "/testing"}},
   223  			prefix:   "/",
   224  			expected: true,
   225  		},
   226  	}
   227  
   228  	for i, tt := range tests {
   229  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   230  			assert.Equal(t, tt.expected, checkPath(tt.req, tt.prefix))
   231  		})
   232  	}
   233  }
   234  
   235  func createAndStartServer(t *testing.T, conf *httpConfig, ws bool, wsConf *wsConfig) *httpServer {
   236  	t.Helper()
   237  
   238  	srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), rpc.DefaultHTTPTimeouts)
   239  	assert.NoError(t, srv.enableRPC(nil, *conf))
   240  	if ws {
   241  		assert.NoError(t, srv.enableWS(nil, *wsConf))
   242  	}
   243  	assert.NoError(t, srv.setListenAddr("localhost", 0))
   244  	assert.NoError(t, srv.start())
   245  	return srv
   246  }
   247  
   248  // wsRequest attempts to open a WebSocket connection to the given URL.
   249  func wsRequest(t *testing.T, url string, extraHeaders ...string) error {
   250  	t.Helper()
   251  	//t.Logf("checking WebSocket on %s (origin %q)", url, browserOrigin)
   252  
   253  	headers := make(http.Header)
   254  	// Apply extra headers.
   255  	if len(extraHeaders)%2 != 0 {
   256  		panic("odd extraHeaders length")
   257  	}
   258  	for i := 0; i < len(extraHeaders); i += 2 {
   259  		key, value := extraHeaders[i], extraHeaders[i+1]
   260  		headers.Set(key, value)
   261  	}
   262  	conn, _, err := websocket.DefaultDialer.Dial(url, headers)
   263  	if conn != nil {
   264  		conn.Close()
   265  	}
   266  	return err
   267  }
   268  
   269  // rpcRequest performs a JSON-RPC request to the given URL.
   270  func rpcRequest(t *testing.T, url string, extraHeaders ...string) *http.Response {
   271  	t.Helper()
   272  
   273  	// Create the request.
   274  	body := bytes.NewReader([]byte(`{"jsonrpc":"2.0","id":1,"method":"rpc_modules","params":[]}`))
   275  	req, err := http.NewRequest("POST", url, body)
   276  	if err != nil {
   277  		t.Fatal("could not create http request:", err)
   278  	}
   279  	req.Header.Set("content-type", "application/json")
   280  
   281  	// Apply extra headers.
   282  	if len(extraHeaders)%2 != 0 {
   283  		panic("odd extraHeaders length")
   284  	}
   285  	for i := 0; i < len(extraHeaders); i += 2 {
   286  		key, value := extraHeaders[i], extraHeaders[i+1]
   287  		if strings.ToLower(key) == "host" {
   288  			req.Host = value
   289  		} else {
   290  			req.Header.Set(key, value)
   291  		}
   292  	}
   293  
   294  	// Perform the request.
   295  	t.Logf("checking RPC/HTTP on %s %v", url, extraHeaders)
   296  	resp, err := http.DefaultClient.Do(req)
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  	if err != nil {
   301  		t.Fatal(err)
   302  	}
   303  	return resp
   304  }
   305  
   306  type testClaim map[string]interface{}
   307  
   308  func (testClaim) Valid() error {
   309  	return nil
   310  }
   311  
   312  func TestJWT(t *testing.T) {
   313  	var secret = []byte("secret")
   314  	issueToken := func(secret []byte, method jwt.SigningMethod, input map[string]interface{}) string {
   315  		if method == nil {
   316  			method = jwt.SigningMethodHS256
   317  		}
   318  		ss, _ := jwt.NewWithClaims(method, testClaim(input)).SignedString(secret)
   319  		return ss
   320  	}
   321  	expOk := []string{
   322  		fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
   323  		fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + 4})),
   324  		fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - 4})),
   325  		fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
   326  			"iat": time.Now().Unix(),
   327  			"exp": time.Now().Unix() + 2,
   328  		})),
   329  		fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
   330  			"iat": time.Now().Unix(),
   331  			"bar": "baz",
   332  		})),
   333  	}
   334  	expFail := []string{
   335  		// future
   336  		fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + int64(jwtExpiryTimeout.Seconds()) + 1})),
   337  		// stale
   338  		fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - int64(jwtExpiryTimeout.Seconds()) - 1})),
   339  		// wrong algo
   340  		fmt.Sprintf("Bearer %v", issueToken(secret, jwt.SigningMethodHS512, testClaim{"iat": time.Now().Unix() + 4})),
   341  		// expired
   342  		fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix(), "exp": time.Now().Unix()})),
   343  		// missing mandatory iat
   344  		fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{})),
   345  		// wrong secret
   346  		fmt.Sprintf("Bearer %v", issueToken([]byte("wrong"), nil, testClaim{"iat": time.Now().Unix()})),
   347  		fmt.Sprintf("Bearer %v", issueToken([]byte{}, nil, testClaim{"iat": time.Now().Unix()})),
   348  		fmt.Sprintf("Bearer %v", issueToken(nil, nil, testClaim{"iat": time.Now().Unix()})),
   349  		// Various malformed syntax
   350  		fmt.Sprintf("%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
   351  		fmt.Sprintf("Bearer  %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
   352  		fmt.Sprintf("bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
   353  		fmt.Sprintf("Bearer: %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
   354  		fmt.Sprintf("Bearer:%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
   355  		fmt.Sprintf("Bearer\t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
   356  		fmt.Sprintf("Bearer \t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
   357  	}
   358  	srv := createAndStartServer(t, &httpConfig{jwtSecret: []byte("secret")},
   359  		true, &wsConfig{Origins: []string{"*"}, jwtSecret: []byte("secret")})
   360  	wsUrl := fmt.Sprintf("ws://%v", srv.listenAddr())
   361  	htUrl := fmt.Sprintf("http://%v", srv.listenAddr())
   362  
   363  	for i, token := range expOk {
   364  		if err := wsRequest(t, wsUrl, "Authorization", token); err != nil {
   365  			t.Errorf("test %d-ws, token '%v': expected ok, got %v", i, token, err)
   366  		}
   367  		if resp := rpcRequest(t, htUrl, "Authorization", token); resp.StatusCode != 200 {
   368  			t.Errorf("test %d-http, token '%v': expected ok, got %v", i, token, resp.StatusCode)
   369  		}
   370  	}
   371  	for i, token := range expFail {
   372  		if err := wsRequest(t, wsUrl, "Authorization", token); err == nil {
   373  			t.Errorf("tc %d-ws, token '%v': expected not to allow,  got ok", i, token)
   374  		}
   375  		if resp := rpcRequest(t, htUrl, "Authorization", token); resp.StatusCode != 403 {
   376  			t.Errorf("tc %d-http, token '%v': expected not to allow,  got %v", i, token, resp.StatusCode)
   377  		}
   378  	}
   379  	srv.stop()
   380  }