diff --git a/README.md b/README.md index 2fb9fdb..47cdaa1 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ at port `8080`. * `PORT`(required) - Which port to use for Rosetta. * `GETH` (optional) - Point to a remote `geth` node instead of initializing one * `SKIP_GETH_ADMIN` (optional, default: `FALSE`) - Instruct Rosetta to not use the `geth` `admin` RPC calls. This is typically disabled by hosted blockchain node services. +* `GETH_HEADERS` (optional) - Pass a key:value comma-separated list to be passed to the `geth` clients. e.g. `X-Auth-Token:12345-ABCDE,X-Other-Header:SomeOtherValue` #### Mainnet:Online ```text diff --git a/cmd/run.go b/cmd/run.go index 1c69439..1c4a28b 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -91,7 +91,7 @@ func runRunCmd(cmd *cobra.Command, args []string) error { } var err error - client, err = ethereum.NewClient(cfg.GethURL, cfg.Params, cfg.SkipGethAdmin) + client, err = ethereum.NewClient(cfg.GethURL, cfg.Params, cfg.SkipGethAdmin, cfg.GethHeaders) if err != nil { return fmt.Errorf("%w: cannot initialize ethereum client", err) } diff --git a/configuration/configuration.go b/configuration/configuration.go index a00078f..15e8aa2 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -19,6 +19,7 @@ import ( "fmt" "os" "strconv" + "strings" "github.com/coinbase/rosetta-ethereum/ethereum" @@ -86,6 +87,11 @@ const ( // by hosted node services. When not set, defaults to false. SkipGethAdminEnv = "SKIP_GETH_ADMIN" + // GethHeadersEnv is an optional environment variable + // of a comma-separated list of key:value pairs to apply + // to geth clients as headers. When not set, defaults to [] + GethHeadersEnv = "GETH_HEADERS" + // MiddlewareVersion is the version of rosetta-ethereum. MiddlewareVersion = "0.0.4" ) @@ -100,6 +106,7 @@ type Configuration struct { Port int GethArguments string SkipGethAdmin bool + GethHeaders []*ethereum.HTTPHeader // Block Reward Data Params *params.ChainConfig @@ -179,6 +186,20 @@ func LoadConfiguration() (*Configuration, error) { config.SkipGethAdmin = val } + envGethHeaders := os.Getenv(GethHeadersEnv) + if len(envGethHeaders) > 0 { + headers := strings.Split(envGethHeaders, ",") + headerKVs := make([]*ethereum.HTTPHeader, len(headers)) + for i, pair := range headers { + kv := strings.Split(pair, ":") + headerKVs[i] = ðereum.HTTPHeader{ + Key: kv[0], + Value: kv[1], + } + } + config.GethHeaders = headerKVs + } + portValue := os.Getenv(PortEnv) if len(portValue) == 0 { return nil, errors.New("PORT must be populated") diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index a894c57..bb051ab 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -33,6 +33,7 @@ func TestLoadConfiguration(t *testing.T) { Port string Geth string SkipGethAdmin string + GethHeaders string cfg *Configuration err error @@ -54,6 +55,7 @@ func TestLoadConfiguration(t *testing.T) { Network: Mainnet, Port: "1000", SkipGethAdmin: "FALSE", + GethHeaders: "", cfg: &Configuration{ Mode: Online, Network: &types.NetworkIdentifier{ @@ -66,6 +68,7 @@ func TestLoadConfiguration(t *testing.T) { GethURL: DefaultGethURL, GethArguments: ethereum.MainnetGethArguments, SkipGethAdmin: false, + GethHeaders: nil, }, }, "all set (mainnet) + geth": { @@ -74,6 +77,7 @@ func TestLoadConfiguration(t *testing.T) { Port: "1000", Geth: "http://blah", SkipGethAdmin: "TRUE", + GethHeaders: "X-Auth-Token:12345-ABCDE,X-Api-Version:2", cfg: &Configuration{ Mode: Online, Network: &types.NetworkIdentifier{ @@ -87,6 +91,10 @@ func TestLoadConfiguration(t *testing.T) { RemoteGeth: true, GethArguments: ethereum.MainnetGethArguments, SkipGethAdmin: true, + GethHeaders: []*ethereum.HTTPHeader{ + {Key: "X-Auth-Token", Value: "12345-ABCDE"}, + {Key: "X-Api-Version", Value: "2"}, + }, }, }, "all set (ropsten)": { @@ -104,6 +112,7 @@ func TestLoadConfiguration(t *testing.T) { Port: 1000, GethURL: DefaultGethURL, GethArguments: ethereum.RopstenGethArguments, + GethHeaders: nil, }, }, "all set (rinkeby)": { @@ -121,6 +130,7 @@ func TestLoadConfiguration(t *testing.T) { Port: 1000, GethURL: DefaultGethURL, GethArguments: ethereum.RinkebyGethArguments, + GethHeaders: nil, }, }, "all set (goerli)": { @@ -138,6 +148,7 @@ func TestLoadConfiguration(t *testing.T) { Port: 1000, GethURL: DefaultGethURL, GethArguments: ethereum.GoerliGethArguments, + GethHeaders: nil, }, }, "all set (testnet)": { @@ -145,6 +156,7 @@ func TestLoadConfiguration(t *testing.T) { Network: Testnet, Port: "1000", SkipGethAdmin: "TRUE", + GethHeaders: "X-Auth-Token:12345-ABCDE,X-Api-Version:2", cfg: &Configuration{ Mode: Online, Network: &types.NetworkIdentifier{ @@ -157,6 +169,10 @@ func TestLoadConfiguration(t *testing.T) { GethURL: DefaultGethURL, GethArguments: ethereum.RopstenGethArguments, SkipGethAdmin: true, + GethHeaders: []*ethereum.HTTPHeader{ + {Key: "X-Auth-Token", Value: "12345-ABCDE"}, + {Key: "X-Api-Version", Value: "2"}, + }, }, }, "invalid mode": { @@ -186,6 +202,7 @@ func TestLoadConfiguration(t *testing.T) { os.Setenv(PortEnv, test.Port) os.Setenv(GethEnv, test.Geth) os.Setenv(SkipGethAdminEnv, test.SkipGethAdmin) + os.Setenv(GethHeadersEnv, test.GethHeaders) cfg, err := LoadConfiguration() if test.err != nil { diff --git a/ethereum/client.go b/ethereum/client.go index 7349373..3538471 100644 --- a/ethereum/client.go +++ b/ethereum/client.go @@ -65,7 +65,7 @@ type Client struct { } // NewClient creates a Client that from the provided url and params. -func NewClient(url string, params *params.ChainConfig, skipAdminCalls bool) (*Client, error) { +func NewClient(url string, params *params.ChainConfig, skipAdminCalls bool, headers []*HTTPHeader) (*Client, error) { c, err := rpc.DialHTTPWithClient(url, &http.Client{ Timeout: gethHTTPTimeout, }) @@ -73,12 +73,16 @@ func NewClient(url string, params *params.ChainConfig, skipAdminCalls bool) (*Cl return nil, fmt.Errorf("%w: unable to dial node", err) } + for _, header := range headers { + c.SetHeader(header.Key, header.Value) + } + tc, err := loadTraceConfig() if err != nil { return nil, fmt.Errorf("%w: unable to load trace config", err) } - g, err := newGraphQLClient(url) + g, err := newGraphQLClient(url, headers) if err != nil { return nil, fmt.Errorf("%w: unable to create GraphQL client", err) } diff --git a/ethereum/graphql_client.go b/ethereum/graphql_client.go index 05b5cdd..8387f11 100644 --- a/ethereum/graphql_client.go +++ b/ethereum/graphql_client.go @@ -35,8 +35,9 @@ const ( // GraphQLClient is a client used to make graphQL // queries to geth's graphql endpoint. type GraphQLClient struct { - client *http.Client - url string + client *http.Client + url string + headers []*HTTPHeader } // Query makes a query to the graphQL endpoint. @@ -56,6 +57,10 @@ func (g *GraphQLClient) Query(ctx context.Context, input string) (string, error) g.url, bytes.NewBuffer(jsonValue), ) + for _, header := range g.headers { + request.Header.Set(header.Key, header.Value) + } + if err != nil { return "", err } @@ -74,7 +79,7 @@ func (g *GraphQLClient) Query(ctx context.Context, input string) (string, error) return string(data), nil } -func newGraphQLClient(baseURL string) (*GraphQLClient, error) { +func newGraphQLClient(baseURL string, headers []*HTTPHeader) (*GraphQLClient, error) { // Compute GraphQL Endpoint u, err := url.Parse(baseURL) if err != nil { @@ -97,7 +102,8 @@ func newGraphQLClient(baseURL string) (*GraphQLClient, error) { client.Transport = customTransport return &GraphQLClient{ - client: client, - url: u.String(), + client: client, + url: u.String(), + headers: headers, }, nil } diff --git a/ethereum/types.go b/ethereum/types.go index f77447e..0207690 100644 --- a/ethereum/types.go +++ b/ethereum/types.go @@ -216,6 +216,12 @@ type GraphQL interface { Query(ctx context.Context, input string) (string, error) } +// HTTPHeader is key, value pair to be set on the HTTP and GraphQL client. +type HTTPHeader struct { + Key string + Value string +} + // CallType returns a boolean indicating // if the provided trace type is a call type. func CallType(t string) bool {