cmd/geth, node, rpc: implement jwt tokens (#24364)
* rpc, node: refactor request validation and add jwt validation * node, rpc: fix error message, ignore engine api in RegisterAPIs * node: make authenticated port configurable * eth/catalyst: enable unauthenticated version of engine api * node: rework obtainjwtsecret (backport later) * cmd/geth: added auth port flag * node: happy lint, happy life * node: refactor authenticated api Modifies the authentication mechanism to use default values * node: trim spaces and newline away from secret Co-authored-by: Marius van der Wijden <m.vanderwijden@live.de>
This commit is contained in:
parent
37f9d25ba0
commit
4860e50e05
@ -661,7 +661,7 @@ func signer(c *cli.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatalf("Could not register API: %w", err)
|
utils.Fatalf("Could not register API: %w", err)
|
||||||
}
|
}
|
||||||
handler := node.NewHTTPHandlerStack(srv, cors, vhosts)
|
handler := node.NewHTTPHandlerStack(srv, cors, vhosts, nil)
|
||||||
|
|
||||||
// set port
|
// set port
|
||||||
port := c.Int(rpcPortFlag.Name)
|
port := c.Int(rpcPortFlag.Name)
|
||||||
|
@ -164,6 +164,8 @@ var (
|
|||||||
utils.HTTPListenAddrFlag,
|
utils.HTTPListenAddrFlag,
|
||||||
utils.HTTPPortFlag,
|
utils.HTTPPortFlag,
|
||||||
utils.HTTPCORSDomainFlag,
|
utils.HTTPCORSDomainFlag,
|
||||||
|
utils.AuthPortFlag,
|
||||||
|
utils.JWTSecretFlag,
|
||||||
utils.HTTPVirtualHostsFlag,
|
utils.HTTPVirtualHostsFlag,
|
||||||
utils.GraphQLEnabledFlag,
|
utils.GraphQLEnabledFlag,
|
||||||
utils.GraphQLCORSDomainFlag,
|
utils.GraphQLCORSDomainFlag,
|
||||||
|
@ -135,6 +135,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{
|
|||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
utils.IPCDisabledFlag,
|
utils.IPCDisabledFlag,
|
||||||
utils.IPCPathFlag,
|
utils.IPCPathFlag,
|
||||||
|
utils.JWTSecretFlag,
|
||||||
utils.HTTPEnabledFlag,
|
utils.HTTPEnabledFlag,
|
||||||
utils.HTTPListenAddrFlag,
|
utils.HTTPListenAddrFlag,
|
||||||
utils.HTTPPortFlag,
|
utils.HTTPPortFlag,
|
||||||
|
@ -518,6 +518,16 @@ var (
|
|||||||
Usage: "Sets a cap on transaction fee (in ether) that can be sent via the RPC APIs (0 = no cap)",
|
Usage: "Sets a cap on transaction fee (in ether) that can be sent via the RPC APIs (0 = no cap)",
|
||||||
Value: ethconfig.Defaults.RPCTxFeeCap,
|
Value: ethconfig.Defaults.RPCTxFeeCap,
|
||||||
}
|
}
|
||||||
|
// Authenticated port settings
|
||||||
|
AuthPortFlag = cli.IntFlag{
|
||||||
|
Name: "authrpc.port",
|
||||||
|
Usage: "Listening port for authenticated APIs",
|
||||||
|
Value: node.DefaultAuthPort,
|
||||||
|
}
|
||||||
|
JWTSecretFlag = cli.StringFlag{
|
||||||
|
Name: "authrpc.jwtsecret",
|
||||||
|
Usage: "JWT secret (or path to a jwt secret) to use for authenticated RPC endpoints",
|
||||||
|
}
|
||||||
// Logging and debug settings
|
// Logging and debug settings
|
||||||
EthStatsURLFlag = cli.StringFlag{
|
EthStatsURLFlag = cli.StringFlag{
|
||||||
Name: "ethstats",
|
Name: "ethstats",
|
||||||
@ -951,6 +961,10 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
|
|||||||
cfg.HTTPPort = ctx.GlobalInt(HTTPPortFlag.Name)
|
cfg.HTTPPort = ctx.GlobalInt(HTTPPortFlag.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctx.GlobalIsSet(AuthPortFlag.Name) {
|
||||||
|
cfg.AuthPort = ctx.GlobalInt(AuthPortFlag.Name)
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.GlobalIsSet(HTTPCORSDomainFlag.Name) {
|
if ctx.GlobalIsSet(HTTPCORSDomainFlag.Name) {
|
||||||
cfg.HTTPCors = SplitAndTrim(ctx.GlobalString(HTTPCORSDomainFlag.Name))
|
cfg.HTTPCors = SplitAndTrim(ctx.GlobalString(HTTPCORSDomainFlag.Name))
|
||||||
}
|
}
|
||||||
@ -1218,6 +1232,10 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
|
|||||||
setDataDir(ctx, cfg)
|
setDataDir(ctx, cfg)
|
||||||
setSmartCard(ctx, cfg)
|
setSmartCard(ctx, cfg)
|
||||||
|
|
||||||
|
if ctx.GlobalIsSet(JWTSecretFlag.Name) {
|
||||||
|
cfg.JWTSecret = ctx.GlobalString(JWTSecretFlag.Name)
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.GlobalIsSet(ExternalSignerFlag.Name) {
|
if ctx.GlobalIsSet(ExternalSignerFlag.Name) {
|
||||||
cfg.ExternalSigner = ctx.GlobalString(ExternalSignerFlag.Name)
|
cfg.ExternalSigner = ctx.GlobalString(ExternalSignerFlag.Name)
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,14 @@ func Register(stack *node.Node, backend *eth.Ethereum) error {
|
|||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
Service: NewConsensusAPI(backend),
|
Service: NewConsensusAPI(backend),
|
||||||
Public: true,
|
Public: true,
|
||||||
|
Authenticated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Namespace: "engine",
|
||||||
|
Version: "1.0",
|
||||||
|
Service: NewConsensusAPI(backend),
|
||||||
|
Public: true,
|
||||||
|
Authenticated: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
|
1
go.mod
1
go.mod
@ -25,6 +25,7 @@ require (
|
|||||||
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
|
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
|
||||||
github.com/go-ole/go-ole v1.2.1 // indirect
|
github.com/go-ole/go-ole v1.2.1 // indirect
|
||||||
github.com/go-stack/stack v1.8.0
|
github.com/go-stack/stack v1.8.0
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.3.0 // indirect
|
||||||
github.com/golang/protobuf v1.4.3
|
github.com/golang/protobuf v1.4.3
|
||||||
github.com/golang/snappy v0.0.4
|
github.com/golang/snappy v0.0.4
|
||||||
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa
|
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa
|
||||||
|
4
go.sum
4
go.sum
@ -154,6 +154,10 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
|
|||||||
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
@ -74,7 +74,7 @@ func newHandler(stack *node.Node, backend ethapi.Backend, cors, vhosts []string)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h := handler{Schema: s}
|
h := handler{Schema: s}
|
||||||
handler := node.NewHTTPHandlerStack(h, cors, vhosts)
|
handler := node.NewHTTPHandlerStack(h, cors, vhosts, nil)
|
||||||
|
|
||||||
stack.RegisterHandler("GraphQL UI", "/graphql/ui", GraphiQL{})
|
stack.RegisterHandler("GraphQL UI", "/graphql/ui", GraphiQL{})
|
||||||
stack.RegisterHandler("GraphQL", "/graphql", handler)
|
stack.RegisterHandler("GraphQL", "/graphql", handler)
|
||||||
|
@ -38,6 +38,7 @@ func Register(stack *node.Node, backend *les.LightEthereum) error {
|
|||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
Service: NewConsensusAPI(backend),
|
Service: NewConsensusAPI(backend),
|
||||||
Public: true,
|
Public: true,
|
||||||
|
Authenticated: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
|
@ -274,11 +274,12 @@ func (api *privateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enable WebSocket on the server.
|
// Enable WebSocket on the server.
|
||||||
server := api.node.wsServerForPort(*port)
|
server := api.node.wsServerForPort(*port, false)
|
||||||
if err := server.setListenAddr(*host, *port); err != nil {
|
if err := server.setListenAddr(*host, *port); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if err := server.enableWS(api.node.rpcAPIs, config); err != nil {
|
openApis, _ := api.node.GetAPIs()
|
||||||
|
if err := server.enableWS(openApis, config); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if err := server.start(); err != nil {
|
if err := server.start(); err != nil {
|
||||||
|
@ -36,6 +36,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
datadirPrivateKey = "nodekey" // Path within the datadir to the node's private key
|
datadirPrivateKey = "nodekey" // Path within the datadir to the node's private key
|
||||||
|
datadirJWTKey = "jwtsecret" // Path within the datadir to the node's jwt secret
|
||||||
datadirDefaultKeyStore = "keystore" // Path within the datadir to the keystore
|
datadirDefaultKeyStore = "keystore" // Path within the datadir to the keystore
|
||||||
datadirStaticNodes = "static-nodes.json" // Path within the datadir to the static node list
|
datadirStaticNodes = "static-nodes.json" // Path within the datadir to the static node list
|
||||||
datadirTrustedNodes = "trusted-nodes.json" // Path within the datadir to the trusted node list
|
datadirTrustedNodes = "trusted-nodes.json" // Path within the datadir to the trusted node list
|
||||||
@ -112,6 +113,9 @@ type Config struct {
|
|||||||
// for ephemeral nodes).
|
// for ephemeral nodes).
|
||||||
HTTPPort int `toml:",omitempty"`
|
HTTPPort int `toml:",omitempty"`
|
||||||
|
|
||||||
|
// Authport is the port number on which the authenticated API is provided.
|
||||||
|
AuthPort int `toml:",omitempty"`
|
||||||
|
|
||||||
// HTTPCors is the Cross-Origin Resource Sharing header to send to requesting
|
// HTTPCors is the Cross-Origin Resource Sharing header to send to requesting
|
||||||
// clients. Please be aware that CORS is a browser enforced security, it's fully
|
// clients. Please be aware that CORS is a browser enforced security, it's fully
|
||||||
// useless for custom HTTP clients.
|
// useless for custom HTTP clients.
|
||||||
@ -190,6 +194,9 @@ type Config struct {
|
|||||||
|
|
||||||
// AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC.
|
// AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC.
|
||||||
AllowUnprotectedTxs bool `toml:",omitempty"`
|
AllowUnprotectedTxs bool `toml:",omitempty"`
|
||||||
|
|
||||||
|
// JWTSecret is the hex-encoded jwt secret.
|
||||||
|
JWTSecret string `toml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into
|
// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into
|
||||||
@ -248,7 +255,7 @@ func (c *Config) HTTPEndpoint() string {
|
|||||||
|
|
||||||
// DefaultHTTPEndpoint returns the HTTP endpoint used by default.
|
// DefaultHTTPEndpoint returns the HTTP endpoint used by default.
|
||||||
func DefaultHTTPEndpoint() string {
|
func DefaultHTTPEndpoint() string {
|
||||||
config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort}
|
config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort, AuthPort: DefaultAuthPort}
|
||||||
return config.HTTPEndpoint()
|
return config.HTTPEndpoint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,12 +34,23 @@ const (
|
|||||||
DefaultWSPort = 8546 // Default TCP port for the websocket RPC server
|
DefaultWSPort = 8546 // Default TCP port for the websocket RPC server
|
||||||
DefaultGraphQLHost = "localhost" // Default host interface for the GraphQL server
|
DefaultGraphQLHost = "localhost" // Default host interface for the GraphQL server
|
||||||
DefaultGraphQLPort = 8547 // Default TCP port for the GraphQL server
|
DefaultGraphQLPort = 8547 // Default TCP port for the GraphQL server
|
||||||
|
DefaultAuthHost = "localhost" // Default host interface for the authenticated apis
|
||||||
|
DefaultAuthPort = 8551 // Default port for the authenticated apis
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultAuthCors = []string{"localhost"} // Default cors domain for the authenticated apis
|
||||||
|
DefaultAuthVhosts = []string{"localhost"} // Default virtual hosts for the authenticated apis
|
||||||
|
DefaultAuthOrigins = []string{"localhost"} // Default origins for the authenticated apis
|
||||||
|
DefaultAuthPrefix = "" // Default prefix for the authenticated apis
|
||||||
|
DefaultAuthModules = []string{"eth", "engine"}
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultConfig contains reasonable default settings.
|
// DefaultConfig contains reasonable default settings.
|
||||||
var DefaultConfig = Config{
|
var DefaultConfig = Config{
|
||||||
DataDir: DefaultDataDir(),
|
DataDir: DefaultDataDir(),
|
||||||
HTTPPort: DefaultHTTPPort,
|
HTTPPort: DefaultHTTPPort,
|
||||||
|
AuthPort: DefaultAuthPort,
|
||||||
HTTPModules: []string{"net", "web3"},
|
HTTPModules: []string{"net", "web3"},
|
||||||
HTTPVirtualHosts: []string{"localhost"},
|
HTTPVirtualHosts: []string{"localhost"},
|
||||||
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
|
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
|
||||||
|
@ -60,10 +60,12 @@ func checkModuleAvailability(modules []string, apis []rpc.API) (bad, available [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, name := range modules {
|
for _, name := range modules {
|
||||||
if _, ok := availableSet[name]; !ok && name != rpc.MetadataApi {
|
if _, ok := availableSet[name]; !ok {
|
||||||
|
if name != rpc.MetadataApi && name != rpc.EngineApi {
|
||||||
bad = append(bad, name)
|
bad = append(bad, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return bad, available
|
return bad, available
|
||||||
}
|
}
|
||||||
|
|
||||||
|
78
node/jwt_handler.go
Normal file
78
node/jwt_handler.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// Copyright 2022 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 (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jwtHandler struct {
|
||||||
|
keyFunc func(token *jwt.Token) (interface{}, error)
|
||||||
|
next http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// newJWTHandler creates a http.Handler with jwt authentication support.
|
||||||
|
func newJWTHandler(secret []byte, next http.Handler) http.Handler {
|
||||||
|
return &jwtHandler{
|
||||||
|
keyFunc: func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return secret, nil
|
||||||
|
},
|
||||||
|
next: next,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements http.Handler
|
||||||
|
func (handler *jwtHandler) ServeHTTP(out http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
strToken string
|
||||||
|
claims jwt.RegisteredClaims
|
||||||
|
)
|
||||||
|
if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
|
||||||
|
strToken = strings.TrimPrefix(auth, "Bearer ")
|
||||||
|
}
|
||||||
|
if len(strToken) == 0 {
|
||||||
|
http.Error(out, "missing token", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// We explicitly set only HS256 allowed, and also disables the
|
||||||
|
// claim-check: the RegisteredClaims internally requires 'iat' to
|
||||||
|
// be no later than 'now', but we allow for a bit of drift.
|
||||||
|
token, err := jwt.ParseWithClaims(strToken, &claims, handler.keyFunc,
|
||||||
|
jwt.WithValidMethods([]string{"HS256"}),
|
||||||
|
jwt.WithoutClaimsValidation())
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
http.Error(out, err.Error(), http.StatusForbidden)
|
||||||
|
case !token.Valid:
|
||||||
|
http.Error(out, "invalid token", http.StatusForbidden)
|
||||||
|
case !claims.VerifyExpiresAt(time.Now(), false): // optional
|
||||||
|
http.Error(out, "token is expired", http.StatusForbidden)
|
||||||
|
case claims.IssuedAt == nil:
|
||||||
|
http.Error(out, "missing issued-at", http.StatusForbidden)
|
||||||
|
case time.Since(claims.IssuedAt.Time) > 5*time.Second:
|
||||||
|
http.Error(out, "stale token", http.StatusForbidden)
|
||||||
|
case time.Until(claims.IssuedAt.Time) > 5*time.Second:
|
||||||
|
http.Error(out, "future token", http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
handler.next.ServeHTTP(out, r)
|
||||||
|
}
|
||||||
|
}
|
177
node/node.go
177
node/node.go
@ -17,6 +17,7 @@
|
|||||||
package node
|
package node
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
crand "crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -27,6 +28,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
@ -55,6 +58,8 @@ type Node struct {
|
|||||||
rpcAPIs []rpc.API // List of APIs currently provided by the node
|
rpcAPIs []rpc.API // List of APIs currently provided by the node
|
||||||
http *httpServer //
|
http *httpServer //
|
||||||
ws *httpServer //
|
ws *httpServer //
|
||||||
|
httpAuth *httpServer //
|
||||||
|
wsAuth *httpServer //
|
||||||
ipc *ipcServer // Stores information about the ipc http server
|
ipc *ipcServer // Stores information about the ipc http server
|
||||||
inprocHandler *rpc.Server // In-process RPC request handler to process the API requests
|
inprocHandler *rpc.Server // In-process RPC request handler to process the API requests
|
||||||
|
|
||||||
@ -147,7 +152,9 @@ func New(conf *Config) (*Node, error) {
|
|||||||
|
|
||||||
// Configure RPC servers.
|
// Configure RPC servers.
|
||||||
node.http = newHTTPServer(node.log, conf.HTTPTimeouts)
|
node.http = newHTTPServer(node.log, conf.HTTPTimeouts)
|
||||||
|
node.httpAuth = newHTTPServer(node.log, conf.HTTPTimeouts)
|
||||||
node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
|
node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
|
||||||
|
node.wsAuth = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
|
||||||
node.ipc = newIPCServer(node.log, conf.IPCEndpoint())
|
node.ipc = newIPCServer(node.log, conf.IPCEndpoint())
|
||||||
|
|
||||||
return node, nil
|
return node, nil
|
||||||
@ -335,7 +342,50 @@ func (n *Node) closeDataDir() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// configureRPC is a helper method to configure all the various RPC endpoints during node
|
// obtainJWTSecret loads the jwt-secret, either from the provided config,
|
||||||
|
// or from the default location. If neither of those are present, it generates
|
||||||
|
// a new secret and stores to the default location.
|
||||||
|
func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) {
|
||||||
|
var fileName string
|
||||||
|
if len(cliParam) > 0 {
|
||||||
|
// If a plaintext secret was provided via cli flags, use that
|
||||||
|
jwtSecret := common.FromHex(cliParam)
|
||||||
|
if len(jwtSecret) == 32 && strings.HasPrefix(cliParam, "0x") {
|
||||||
|
log.Warn("Plaintext JWT secret provided, please consider passing via file")
|
||||||
|
return jwtSecret, nil
|
||||||
|
}
|
||||||
|
// path provided
|
||||||
|
fileName = cliParam
|
||||||
|
} else {
|
||||||
|
// no path provided, use default
|
||||||
|
fileName = n.ResolvePath(datadirJWTKey)
|
||||||
|
}
|
||||||
|
// try reading from file
|
||||||
|
log.Debug("Reading JWT secret", "path", fileName)
|
||||||
|
if data, err := os.ReadFile(fileName); err == nil {
|
||||||
|
jwtSecret := common.FromHex(strings.TrimSpace(string(data)))
|
||||||
|
if len(jwtSecret) == 32 {
|
||||||
|
return jwtSecret, nil
|
||||||
|
}
|
||||||
|
log.Error("Invalid JWT secret", "path", fileName, "length", len(jwtSecret))
|
||||||
|
return nil, errors.New("invalid JWT secret")
|
||||||
|
}
|
||||||
|
// Need to generate one
|
||||||
|
jwtSecret := make([]byte, 32)
|
||||||
|
crand.Read(jwtSecret)
|
||||||
|
// if we're in --dev mode, don't bother saving, just show it
|
||||||
|
if fileName == "" {
|
||||||
|
log.Info("Generated ephemeral JWT secret", "secret", hexutil.Encode(jwtSecret))
|
||||||
|
return jwtSecret, nil
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(fileName, []byte(hexutil.Encode(jwtSecret)), 0600); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Info("Generated JWT secret", "path", fileName)
|
||||||
|
return jwtSecret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// startRPC is a helper method to configure all the various RPC endpoints during node
|
||||||
// startup. It's not meant to be called at any time afterwards as it makes certain
|
// startup. It's not meant to be called at any time afterwards as it makes certain
|
||||||
// assumptions about the state of the node.
|
// assumptions about the state of the node.
|
||||||
func (n *Node) startRPC() error {
|
func (n *Node) startRPC() error {
|
||||||
@ -349,55 +399,123 @@ func (n *Node) startRPC() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var (
|
||||||
|
servers []*httpServer
|
||||||
|
open, all = n.GetAPIs()
|
||||||
|
)
|
||||||
|
|
||||||
// Configure HTTP.
|
initHttp := func(server *httpServer, apis []rpc.API, port int) error {
|
||||||
if n.config.HTTPHost != "" {
|
if err := server.setListenAddr(n.config.HTTPHost, port); err != nil {
|
||||||
config := httpConfig{
|
return err
|
||||||
|
}
|
||||||
|
if err := server.enableRPC(apis, httpConfig{
|
||||||
CorsAllowedOrigins: n.config.HTTPCors,
|
CorsAllowedOrigins: n.config.HTTPCors,
|
||||||
Vhosts: n.config.HTTPVirtualHosts,
|
Vhosts: n.config.HTTPVirtualHosts,
|
||||||
Modules: n.config.HTTPModules,
|
Modules: n.config.HTTPModules,
|
||||||
prefix: n.config.HTTPPathPrefix,
|
prefix: n.config.HTTPPathPrefix,
|
||||||
}
|
}); err != nil {
|
||||||
if err := n.http.setListenAddr(n.config.HTTPHost, n.config.HTTPPort); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := n.http.enableRPC(n.rpcAPIs, config); err != nil {
|
servers = append(servers, server)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
initWS := func(apis []rpc.API, port int) error {
|
||||||
|
server := n.wsServerForPort(port, false)
|
||||||
|
if err := server.setListenAddr(n.config.WSHost, port); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
if err := server.enableWS(n.rpcAPIs, wsConfig{
|
||||||
|
|
||||||
// Configure WebSocket.
|
|
||||||
if n.config.WSHost != "" {
|
|
||||||
server := n.wsServerForPort(n.config.WSPort)
|
|
||||||
config := wsConfig{
|
|
||||||
Modules: n.config.WSModules,
|
Modules: n.config.WSModules,
|
||||||
Origins: n.config.WSOrigins,
|
Origins: n.config.WSOrigins,
|
||||||
prefix: n.config.WSPathPrefix,
|
prefix: n.config.WSPathPrefix,
|
||||||
}
|
}); err != nil {
|
||||||
if err := server.setListenAddr(n.config.WSHost, n.config.WSPort); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := server.enableWS(n.rpcAPIs, config); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
servers = append(servers, server)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := n.http.start(); err != nil {
|
initAuth := func(apis []rpc.API, port int, secret []byte) error {
|
||||||
|
// Enable auth via HTTP
|
||||||
|
server := n.httpAuth
|
||||||
|
if err := server.setListenAddr(DefaultAuthHost, port); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return n.ws.start()
|
if err := server.enableRPC(apis, httpConfig{
|
||||||
|
CorsAllowedOrigins: DefaultAuthCors,
|
||||||
|
Vhosts: DefaultAuthVhosts,
|
||||||
|
Modules: DefaultAuthModules,
|
||||||
|
prefix: DefaultAuthPrefix,
|
||||||
|
jwtSecret: secret,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
servers = append(servers, server)
|
||||||
|
// Enable auth via WS
|
||||||
|
server = n.wsServerForPort(port, true)
|
||||||
|
if err := server.setListenAddr(DefaultAuthHost, port); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := server.enableWS(apis, wsConfig{
|
||||||
|
Modules: DefaultAuthModules,
|
||||||
|
Origins: DefaultAuthOrigins,
|
||||||
|
prefix: DefaultAuthPrefix,
|
||||||
|
jwtSecret: secret,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
servers = append(servers, server)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Set up HTTP.
|
||||||
|
if n.config.HTTPHost != "" {
|
||||||
|
// Configure legacy unauthenticated HTTP.
|
||||||
|
if err := initHttp(n.http, open, n.config.HTTPPort); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Configure WebSocket.
|
||||||
|
if n.config.WSHost != "" {
|
||||||
|
// legacy unauthenticated
|
||||||
|
if err := initWS(open, n.config.WSPort); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Configure authenticated API
|
||||||
|
if len(open) != len(all) {
|
||||||
|
jwtSecret, err := n.obtainJWTSecret(n.config.JWTSecret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := initAuth(all, n.config.AuthPort, jwtSecret); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Start the servers
|
||||||
|
for _, server := range servers {
|
||||||
|
if err := server.start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) wsServerForPort(port int) *httpServer {
|
func (n *Node) wsServerForPort(port int, authenticated bool) *httpServer {
|
||||||
if n.config.HTTPHost == "" || n.http.port == port {
|
httpServer, wsServer := n.http, n.ws
|
||||||
return n.http
|
if authenticated {
|
||||||
|
httpServer, wsServer = n.httpAuth, n.wsAuth
|
||||||
}
|
}
|
||||||
return n.ws
|
if n.config.HTTPHost == "" || httpServer.port == port {
|
||||||
|
return httpServer
|
||||||
|
}
|
||||||
|
return wsServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) stopRPC() {
|
func (n *Node) stopRPC() {
|
||||||
n.http.stop()
|
n.http.stop()
|
||||||
n.ws.stop()
|
n.ws.stop()
|
||||||
|
n.httpAuth.stop()
|
||||||
|
n.wsAuth.stop()
|
||||||
n.ipc.stop()
|
n.ipc.stop()
|
||||||
n.stopInProc()
|
n.stopInProc()
|
||||||
}
|
}
|
||||||
@ -458,6 +576,17 @@ func (n *Node) RegisterAPIs(apis []rpc.API) {
|
|||||||
n.rpcAPIs = append(n.rpcAPIs, apis...)
|
n.rpcAPIs = append(n.rpcAPIs, apis...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAPIs return two sets of APIs, both the ones that do not require
|
||||||
|
// authentication, and the complete set
|
||||||
|
func (n *Node) GetAPIs() (unauthenticated, all []rpc.API) {
|
||||||
|
for _, api := range n.rpcAPIs {
|
||||||
|
if !api.Authenticated {
|
||||||
|
unauthenticated = append(unauthenticated, api)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unauthenticated, n.rpcAPIs
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterHandler mounts a handler on the given path on the canonical HTTP server.
|
// RegisterHandler mounts a handler on the given path on the canonical HTTP server.
|
||||||
//
|
//
|
||||||
// The name of the handler is shown in a log message when the HTTP server starts
|
// The name of the handler is shown in a log message when the HTTP server starts
|
||||||
|
@ -577,13 +577,13 @@ func (test rpcPrefixTest) check(t *testing.T, node *Node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, path := range test.wantWS {
|
for _, path := range test.wantWS {
|
||||||
err := wsRequest(t, wsBase+path, "")
|
err := wsRequest(t, wsBase+path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error: %s: WebSocket connection failed: %v", path, err)
|
t.Errorf("Error: %s: WebSocket connection failed: %v", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, path := range test.wantNoWS {
|
for _, path := range test.wantNoWS {
|
||||||
err := wsRequest(t, wsBase+path, "")
|
err := wsRequest(t, wsBase+path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Error: %s: WebSocket connection succeeded for path in wantNoWS", path)
|
t.Errorf("Error: %s: WebSocket connection succeeded for path in wantNoWS", path)
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ type httpConfig struct {
|
|||||||
CorsAllowedOrigins []string
|
CorsAllowedOrigins []string
|
||||||
Vhosts []string
|
Vhosts []string
|
||||||
prefix string // path prefix on which to mount http handler
|
prefix string // path prefix on which to mount http handler
|
||||||
|
jwtSecret []byte // optional JWT secret
|
||||||
}
|
}
|
||||||
|
|
||||||
// wsConfig is the JSON-RPC/Websocket configuration
|
// wsConfig is the JSON-RPC/Websocket configuration
|
||||||
@ -47,6 +48,7 @@ type wsConfig struct {
|
|||||||
Origins []string
|
Origins []string
|
||||||
Modules []string
|
Modules []string
|
||||||
prefix string // path prefix on which to mount ws handler
|
prefix string // path prefix on which to mount ws handler
|
||||||
|
jwtSecret []byte // optional JWT secret
|
||||||
}
|
}
|
||||||
|
|
||||||
type rpcHandler struct {
|
type rpcHandler struct {
|
||||||
@ -157,7 +159,7 @@ func (h *httpServer) start() error {
|
|||||||
}
|
}
|
||||||
// Log http endpoint.
|
// Log http endpoint.
|
||||||
h.log.Info("HTTP server started",
|
h.log.Info("HTTP server started",
|
||||||
"endpoint", listener.Addr(),
|
"endpoint", listener.Addr(), "auth", (h.httpConfig.jwtSecret != nil),
|
||||||
"prefix", h.httpConfig.prefix,
|
"prefix", h.httpConfig.prefix,
|
||||||
"cors", strings.Join(h.httpConfig.CorsAllowedOrigins, ","),
|
"cors", strings.Join(h.httpConfig.CorsAllowedOrigins, ","),
|
||||||
"vhosts", strings.Join(h.httpConfig.Vhosts, ","),
|
"vhosts", strings.Join(h.httpConfig.Vhosts, ","),
|
||||||
@ -285,7 +287,7 @@ func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error {
|
|||||||
}
|
}
|
||||||
h.httpConfig = config
|
h.httpConfig = config
|
||||||
h.httpHandler.Store(&rpcHandler{
|
h.httpHandler.Store(&rpcHandler{
|
||||||
Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts),
|
Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret),
|
||||||
server: srv,
|
server: srv,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
@ -309,7 +311,6 @@ func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error {
|
|||||||
if h.wsAllowed() {
|
if h.wsAllowed() {
|
||||||
return fmt.Errorf("JSON-RPC over WebSocket is already enabled")
|
return fmt.Errorf("JSON-RPC over WebSocket is already enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create RPC server and handler.
|
// Create RPC server and handler.
|
||||||
srv := rpc.NewServer()
|
srv := rpc.NewServer()
|
||||||
if err := RegisterApis(apis, config.Modules, srv, false); err != nil {
|
if err := RegisterApis(apis, config.Modules, srv, false); err != nil {
|
||||||
@ -317,7 +318,7 @@ func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error {
|
|||||||
}
|
}
|
||||||
h.wsConfig = config
|
h.wsConfig = config
|
||||||
h.wsHandler.Store(&rpcHandler{
|
h.wsHandler.Store(&rpcHandler{
|
||||||
Handler: srv.WebsocketHandler(config.Origins),
|
Handler: NewWSHandlerStack(srv.WebsocketHandler(config.Origins), config.jwtSecret),
|
||||||
server: srv,
|
server: srv,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
@ -362,13 +363,24 @@ func isWebsocket(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewHTTPHandlerStack returns wrapped http-related handlers
|
// NewHTTPHandlerStack returns wrapped http-related handlers
|
||||||
func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string) http.Handler {
|
func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string, jwtSecret []byte) http.Handler {
|
||||||
// Wrap the CORS-handler within a host-handler
|
// Wrap the CORS-handler within a host-handler
|
||||||
handler := newCorsHandler(srv, cors)
|
handler := newCorsHandler(srv, cors)
|
||||||
handler = newVHostHandler(vhosts, handler)
|
handler = newVHostHandler(vhosts, handler)
|
||||||
|
if len(jwtSecret) != 0 {
|
||||||
|
handler = newJWTHandler(jwtSecret, handler)
|
||||||
|
}
|
||||||
return newGzipHandler(handler)
|
return newGzipHandler(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewWSHandlerStack returns a wrapped ws-related handler.
|
||||||
|
func NewWSHandlerStack(srv http.Handler, jwtSecret []byte) http.Handler {
|
||||||
|
if len(jwtSecret) != 0 {
|
||||||
|
return newJWTHandler(jwtSecret, srv)
|
||||||
|
}
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler {
|
func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler {
|
||||||
// disable CORS support if user has not specified a custom CORS configuration
|
// disable CORS support if user has not specified a custom CORS configuration
|
||||||
if len(allowedOrigins) == 0 {
|
if len(allowedOrigins) == 0 {
|
||||||
|
@ -24,10 +24,12 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/internal/testlog"
|
"github.com/ethereum/go-ethereum/internal/testlog"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -146,12 +148,12 @@ func TestWebsocketOrigins(t *testing.T) {
|
|||||||
srv := createAndStartServer(t, &httpConfig{}, true, &wsConfig{Origins: splitAndTrim(tc.spec)})
|
srv := createAndStartServer(t, &httpConfig{}, true, &wsConfig{Origins: splitAndTrim(tc.spec)})
|
||||||
url := fmt.Sprintf("ws://%v", srv.listenAddr())
|
url := fmt.Sprintf("ws://%v", srv.listenAddr())
|
||||||
for _, origin := range tc.expOk {
|
for _, origin := range tc.expOk {
|
||||||
if err := wsRequest(t, url, origin); err != nil {
|
if err := wsRequest(t, url, "Origin", origin); err != nil {
|
||||||
t.Errorf("spec '%v', origin '%v': expected ok, got %v", tc.spec, origin, err)
|
t.Errorf("spec '%v', origin '%v': expected ok, got %v", tc.spec, origin, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, origin := range tc.expFail {
|
for _, origin := range tc.expFail {
|
||||||
if err := wsRequest(t, url, origin); err == nil {
|
if err := wsRequest(t, url, "Origin", origin); err == nil {
|
||||||
t.Errorf("spec '%v', origin '%v': expected not to allow, got ok", tc.spec, origin)
|
t.Errorf("spec '%v', origin '%v': expected not to allow, got ok", tc.spec, origin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,13 +245,18 @@ func createAndStartServer(t *testing.T, conf *httpConfig, ws bool, wsConf *wsCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wsRequest attempts to open a WebSocket connection to the given URL.
|
// wsRequest attempts to open a WebSocket connection to the given URL.
|
||||||
func wsRequest(t *testing.T, url, browserOrigin string) error {
|
func wsRequest(t *testing.T, url string, extraHeaders ...string) error {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Logf("checking WebSocket on %s (origin %q)", url, browserOrigin)
|
//t.Logf("checking WebSocket on %s (origin %q)", url, browserOrigin)
|
||||||
|
|
||||||
headers := make(http.Header)
|
headers := make(http.Header)
|
||||||
if browserOrigin != "" {
|
// Apply extra headers.
|
||||||
headers.Set("Origin", browserOrigin)
|
if len(extraHeaders)%2 != 0 {
|
||||||
|
panic("odd extraHeaders length")
|
||||||
|
}
|
||||||
|
for i := 0; i < len(extraHeaders); i += 2 {
|
||||||
|
key, value := extraHeaders[i], extraHeaders[i+1]
|
||||||
|
headers.Set(key, value)
|
||||||
}
|
}
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(url, headers)
|
conn, _, err := websocket.DefaultDialer.Dial(url, headers)
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
@ -291,3 +298,79 @@ func rpcRequest(t *testing.T, url string, extraHeaders ...string) *http.Response
|
|||||||
}
|
}
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testClaim map[string]interface{}
|
||||||
|
|
||||||
|
func (testClaim) Valid() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJWT(t *testing.T) {
|
||||||
|
var secret = []byte("secret")
|
||||||
|
issueToken := func(secret []byte, method jwt.SigningMethod, input map[string]interface{}) string {
|
||||||
|
if method == nil {
|
||||||
|
method = jwt.SigningMethodHS256
|
||||||
|
}
|
||||||
|
ss, _ := jwt.NewWithClaims(method, testClaim(input)).SignedString(secret)
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
expOk := []string{
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + 4})),
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - 4})),
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
"exp": time.Now().Unix() + 2,
|
||||||
|
})),
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
"bar": "baz",
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
expFail := []string{
|
||||||
|
// future
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + 6})),
|
||||||
|
// stale
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - 6})),
|
||||||
|
// wrong algo
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, jwt.SigningMethodHS512, testClaim{"iat": time.Now().Unix() + 4})),
|
||||||
|
// expired
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix(), "exp": time.Now().Unix()})),
|
||||||
|
// missing mandatory iat
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{})),
|
||||||
|
// wrong secret
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken([]byte("wrong"), nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken([]byte{}, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(nil, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
// Various malformed syntax
|
||||||
|
fmt.Sprintf("%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
fmt.Sprintf("bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
fmt.Sprintf("Bearer: %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
fmt.Sprintf("Bearer:%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
fmt.Sprintf("Bearer\t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
fmt.Sprintf("Bearer \t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()})),
|
||||||
|
}
|
||||||
|
srv := createAndStartServer(t, &httpConfig{jwtSecret: []byte("secret")},
|
||||||
|
true, &wsConfig{Origins: []string{"*"}, jwtSecret: []byte("secret")})
|
||||||
|
wsUrl := fmt.Sprintf("ws://%v", srv.listenAddr())
|
||||||
|
htUrl := fmt.Sprintf("http://%v", srv.listenAddr())
|
||||||
|
|
||||||
|
for i, token := range expOk {
|
||||||
|
if err := wsRequest(t, wsUrl, "Authorization", token); err != nil {
|
||||||
|
t.Errorf("test %d-ws, token '%v': expected ok, got %v", i, token, err)
|
||||||
|
}
|
||||||
|
if resp := rpcRequest(t, htUrl, "Authorization", token); resp.StatusCode != 200 {
|
||||||
|
t.Errorf("test %d-http, token '%v': expected ok, got %v", i, token, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, token := range expFail {
|
||||||
|
if err := wsRequest(t, wsUrl, "Authorization", token); err == nil {
|
||||||
|
t.Errorf("tc %d-ws, token '%v': expected not to allow, got ok", i, token)
|
||||||
|
}
|
||||||
|
if resp := rpcRequest(t, htUrl, "Authorization", token); resp.StatusCode != 403 {
|
||||||
|
t.Errorf("tc %d-http, token '%v': expected not to allow, got %v", i, token, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
srv.stop()
|
||||||
|
}
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const MetadataApi = "rpc"
|
const MetadataApi = "rpc"
|
||||||
|
const EngineApi = "engine"
|
||||||
|
|
||||||
// CodecOption specifies which type of messages a codec supports.
|
// CodecOption specifies which type of messages a codec supports.
|
||||||
//
|
//
|
||||||
|
@ -34,6 +34,7 @@ type API struct {
|
|||||||
Version string // api version for DApp's
|
Version string // api version for DApp's
|
||||||
Service interface{} // receiver instance which holds the methods
|
Service interface{} // receiver instance which holds the methods
|
||||||
Public bool // indication if the methods must be considered safe for public use
|
Public bool // indication if the methods must be considered safe for public use
|
||||||
|
Authenticated bool // whether the api should only be available behind authentication.
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerCodec implements reading, parsing and writing RPC messages for the server side of
|
// ServerCodec implements reading, parsing and writing RPC messages for the server side of
|
||||||
|
@ -76,7 +76,7 @@ func TestWebsocketOriginCheck(t *testing.T) {
|
|||||||
// Connections without origin header should work.
|
// Connections without origin header should work.
|
||||||
client, err = DialWebsocket(context.Background(), wsURL, "")
|
client, err = DialWebsocket(context.Background(), wsURL, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("error for empty origin")
|
t.Fatalf("error for empty origin: %v", err)
|
||||||
}
|
}
|
||||||
client.Close()
|
client.Close()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user