go-ethereum/node/rpcstack_test.go
wbt f1e1d9f874
node: support expressive origin rules in ws.origins (#21481)
* Only compare hostnames in ws.origins

Also using a helper function for ToLower consolidates all preparation steps in one function for more maintainable consistency.

Spaces => tabs

Remove a semicolon

Add space at start of comment

Remove parens around conditional

Handle case wehre parsed hostname is empty

When passing a single word like "localhost" the parsed hostname is an empty string. Handle this and the error-parsing case together as default, and the nonempty hostname case in the conditional.

Refactor with new originIsAllowed functions

Adds originIsAllowed() & ruleAllowsOrigin(); removes prepOriginForComparison

Remove blank line

Added tests for simple allowed-orign rule

which does not specify a protocol or port, just a hostname

Fix copy-paste: `:=` => `=`

Remove parens around conditional

Remove autoadded whitespace on blank lines

Compare scheme, hostname, and port with rule

if the rule specifies those portions.

Remove one autoadded trailing whitespace

Better handle case where only origin host is given

e.g. "localhost"

Remove parens around conditional

Refactor: attemptWebsocketConnectionFromOrigin DRY

Include return type on helper function

Provide srv obj in helper fn

Provide srv to helper fn

Remove stray underscore

Remove blank line

parent 93e666b4c1e7e49b8406dc83ed93f4a02ea49ac1
author wbt <wbt@users.noreply.github.com> 1598559718 -0400
committer Martin Holst Swende <martin@swende.se> 1605602257 +0100
gpgsig -----BEGIN PGP SIGNATURE-----

 iQFFBAABCAAvFiEEypmrtbNuJK1doP1AaDtDjAWl3fAFAl+zi9ARHG1hcnRpbkBz
 d2VuZGUuc2UACgkQaDtDjAWl3fDRiwgAoMtzU8dwRV7Q9xkCwWEx9Wz2f3n6jUr2
 VWBycDKGKwRkPPOER3oc9kzjGU/P1tFlK07PjfnAKZ9KWzxpDcJZwYM3xCBurG7A
 16y4YsQnzgPNONv3xIkdi3RZtDBIiPFFEmdZFFvZ/jKexfI6JIYPngCAoqdTIFb9
 On/aPvvVWQn1ExfmarsvvJ7kUDUG77tZipuacEH5FfFsfelBWOEYPe+I9ToUHskv
 +qO6rOkV1Ojk8eBc6o0R1PnApwCAlEhJs7aM/SEOg4B4ZJJneiFuEXBIG9+0yS2I
 NOicuDPLGucOB5nBsfIKI3USPeE+3jxdT8go2lN5Nrhm6MimoILDsQ==
 =sgUp
 -----END PGP SIGNATURE-----

Refactor: drop err var for more concise test lines

Add several tests for new WebSocket origin checks

Remove autoadded whitespace on blank lines

Restore TestWebsocketOrigins originally-named test

and rename the others to be helpers rather than full tests

Remove autoadded whitespace on blank line

Temporarily comment out new test sets

Uncomment test around origin rule with scheme

Remove tests without scheme on browser origin

per https://github.com/ethereum/go-ethereum/pull/21481/files#r479371498

Uncomment tests with port; remove some blank lines

Handle when browser does not specify scheme/port

Uncomment test for including scheme & port in rule

Add IP tests

* node: more tests + table-driven, ws origin changes

Co-authored-by: Martin Holst Swende <martin@swende.se>
2020-11-19 14:54:49 +01:00

217 lines
7.2 KiB
Go

// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"bytes"
"net/http"
"strings"
"testing"
"github.com/ethereum/go-ethereum/internal/testlog"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
)
// TestCorsHandler makes sure CORS are properly handled on the http server.
func TestCorsHandler(t *testing.T) {
srv := createAndStartServer(t, httpConfig{CorsAllowedOrigins: []string{"test", "test.com"}}, false, wsConfig{})
defer srv.stop()
resp := testRequest(t, "origin", "test.com", "", srv)
assert.Equal(t, "test.com", resp.Header.Get("Access-Control-Allow-Origin"))
resp2 := testRequest(t, "origin", "bad", "", srv)
assert.Equal(t, "", resp2.Header.Get("Access-Control-Allow-Origin"))
}
// TestVhosts makes sure vhosts are properly handled on the http server.
func TestVhosts(t *testing.T) {
srv := createAndStartServer(t, httpConfig{Vhosts: []string{"test"}}, false, wsConfig{})
defer srv.stop()
resp := testRequest(t, "", "", "test", srv)
assert.Equal(t, resp.StatusCode, http.StatusOK)
resp2 := testRequest(t, "", "", "bad", srv)
assert.Equal(t, resp2.StatusCode, http.StatusForbidden)
}
type originTest struct {
spec string
expOk []string
expFail []string
}
// splitAndTrim splits input separated by a comma
// and trims excessive white space from the substrings.
// Copied over from flags.go
func splitAndTrim(input string) (ret []string) {
l := strings.Split(input, ",")
for _, r := range l {
r = strings.TrimSpace(r)
if len(r) > 0 {
ret = append(ret, r)
}
}
return ret
}
// TestWebsocketOrigins makes sure the websocket origins are properly handled on the websocket server.
func TestWebsocketOrigins(t *testing.T) {
tests := []originTest{
{
spec: "*", // allow all
expOk: []string{"", "http://test", "https://test", "http://test:8540", "https://test:8540",
"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
},
{
spec: "test",
expOk: []string{"http://test", "https://test", "http://test:8540", "https://test:8540"},
expFail: []string{"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
},
// scheme tests
{
spec: "https://test",
expOk: []string{"https://test", "https://test:9999"},
expFail: []string{
"test", // no scheme, required by spec
"http://test", // wrong scheme
"http://test.foo", "https://a.test.x", // subdomain variatoins
"http://testx:8540", "https://xtest:8540"},
},
// ip tests
{
spec: "https://12.34.56.78",
expOk: []string{"https://12.34.56.78", "https://12.34.56.78:8540"},
expFail: []string{
"http://12.34.56.78", // wrong scheme
"http://12.34.56.78:443", // wrong scheme
"http://1.12.34.56.78", // wrong 'domain name'
"http://12.34.56.78.a", // wrong 'domain name'
"https://87.65.43.21", "http://87.65.43.21:8540", "https://87.65.43.21:8540"},
},
// port tests
{
spec: "test:8540",
expOk: []string{"http://test:8540", "https://test:8540"},
expFail: []string{
"http://test", "https://test", // spec says port required
"http://test:8541", "https://test:8541", // wrong port
"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
},
// scheme and port
{
spec: "https://test:8540",
expOk: []string{"https://test:8540"},
expFail: []string{
"https://test", // missing port
"http://test", // missing port, + wrong scheme
"http://test:8540", // wrong scheme
"http://test:8541", "https://test:8541", // wrong port
"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
},
// several allowed origins
{
spec: "localhost,http://127.0.0.1",
expOk: []string{"localhost", "http://localhost", "https://localhost:8443",
"http://127.0.0.1", "http://127.0.0.1:8080"},
expFail: []string{
"https://127.0.0.1", // wrong scheme
"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
},
}
for _, tc := range tests {
srv := createAndStartServer(t, httpConfig{}, true, wsConfig{Origins: splitAndTrim(tc.spec)})
for _, origin := range tc.expOk {
if err := attemptWebsocketConnectionFromOrigin(t, srv, origin); err != nil {
t.Errorf("spec '%v', origin '%v': expected ok, got %v", tc.spec, origin, err)
}
}
for _, origin := range tc.expFail {
if err := attemptWebsocketConnectionFromOrigin(t, srv, origin); err == nil {
t.Errorf("spec '%v', origin '%v': expected not to allow, got ok", tc.spec, origin)
}
}
srv.stop()
}
}
// TestIsWebsocket tests if an incoming websocket upgrade request is handled properly.
func TestIsWebsocket(t *testing.T) {
r, _ := http.NewRequest("GET", "/", nil)
assert.False(t, isWebsocket(r))
r.Header.Set("upgrade", "websocket")
assert.False(t, isWebsocket(r))
r.Header.Set("connection", "upgrade")
assert.True(t, isWebsocket(r))
r.Header.Set("connection", "upgrade,keep-alive")
assert.True(t, isWebsocket(r))
r.Header.Set("connection", " UPGRADE,keep-alive")
assert.True(t, isWebsocket(r))
}
func createAndStartServer(t *testing.T, conf httpConfig, ws bool, wsConf wsConfig) *httpServer {
t.Helper()
srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), rpc.DefaultHTTPTimeouts)
assert.NoError(t, srv.enableRPC(nil, conf))
if ws {
assert.NoError(t, srv.enableWS(nil, wsConf))
}
assert.NoError(t, srv.setListenAddr("localhost", 0))
assert.NoError(t, srv.start())
return srv
}
func attemptWebsocketConnectionFromOrigin(t *testing.T, srv *httpServer, browserOrigin string) error {
t.Helper()
dialer := websocket.DefaultDialer
_, _, err := dialer.Dial("ws://"+srv.listenAddr(), http.Header{
"Content-type": []string{"application/json"},
"Sec-WebSocket-Version": []string{"13"},
"Origin": []string{browserOrigin},
})
return err
}
func testRequest(t *testing.T, key, value, host string, srv *httpServer) *http.Response {
t.Helper()
body := bytes.NewReader([]byte(`{"jsonrpc":"2.0","id":1,method":"rpc_modules"}`))
req, _ := http.NewRequest("POST", "http://"+srv.listenAddr(), body)
req.Header.Set("content-type", "application/json")
if key != "" && value != "" {
req.Header.Set(key, value)
}
if host != "" {
req.Host = host
}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
return resp
}