Feature/rpc filter (#4173)
* fix unexpected chain id (https://github.com/harmony-one/harmony/issues/4129) * update epoch numbers for ChainId fix * updated ChainID Epoch for test net * align comment with other comments * fix the build using goimports * added flags to enable/disable rpc for staking and eth * add feature to be able to feed custom configuration for each single node in testnet * add method filter * add rpc method filters file and one new flag for legacy APIs * fixed tests and improved method filter and added more tests * fix format and removed extra comments * fix method_filter comments * remove extra debug print Co-authored-by: “GheisMohammadi” <“Gheis.Mohammadi@gmail.com”>pull/4179/head
parent
5c5b8156e7
commit
ea96987816
@ -0,0 +1,175 @@ |
||||
package rpc |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"regexp" |
||||
"strings" |
||||
|
||||
"github.com/pelletier/go-toml" |
||||
) |
||||
|
||||
type RpcMethodFilter struct { |
||||
Allow []string |
||||
Deny []string |
||||
} |
||||
|
||||
// ExposeAll - init Allow and Deny array in a way to expose all APIs
|
||||
func (rmf *RpcMethodFilter) ExposeAll() error { |
||||
rmf.Allow = rmf.Allow[:0] |
||||
rmf.Allow = rmf.Deny[:0] |
||||
rmf.Allow = append(rmf.Allow, "*") |
||||
return nil |
||||
} |
||||
|
||||
// LoadRpcMethodFilters - load RPC method filters from toml file
|
||||
/* ex: filters.toml |
||||
Allow = [ ... ] |
||||
Deny = [ ... ] |
||||
*/ |
||||
func (rmf *RpcMethodFilter) LoadRpcMethodFiltersFromFile(file string) error { |
||||
// check if file exist
|
||||
if _, err := os.Stat(file); err == nil { |
||||
b, errRead := ioutil.ReadFile(file) |
||||
if errRead != nil { |
||||
return fmt.Errorf("rpc filter file read error - %s", errRead.Error()) |
||||
} |
||||
return rmf.LoadRpcMethodFilters(b) |
||||
} else if errors.Is(err, os.ErrNotExist) { |
||||
// file path does not exist
|
||||
return fmt.Errorf("rpc filter file doesn't exist") |
||||
} else { |
||||
// some other errors happened
|
||||
return fmt.Errorf("rpc filter file stat error - %s", err.Error()) |
||||
} |
||||
} |
||||
|
||||
// LoadRpcMethodFilters - load RPC method filters from binary array (given from toml file)
|
||||
func (rmf *RpcMethodFilter) LoadRpcMethodFilters(b []byte) error { |
||||
fTree, err := toml.LoadBytes(b) |
||||
if err != nil { |
||||
return fmt.Errorf("rpc filter file parse error - %s", err.Error()) |
||||
} |
||||
if err := fTree.Unmarshal(rmf); err != nil { |
||||
return fmt.Errorf("rpc filter parse error - %s", err.Error()) |
||||
} |
||||
if len(rmf.Allow) == 0 { |
||||
rmf.Allow = append(rmf.Allow, "*") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Expose - checks whether specific method have to expose or not
|
||||
func (rmf *RpcMethodFilter) Expose(name string) bool { |
||||
allow := checkFilters(rmf.Allow, name) |
||||
deny := checkFilters(rmf.Deny, name) |
||||
return allow && !deny |
||||
} |
||||
|
||||
// checkFilters - checks whether any of filters match with value
|
||||
func checkFilters(filters []string, value string) bool { |
||||
if len(filters) == 0 { |
||||
return false |
||||
} |
||||
for _, filter := range filters { |
||||
if Match(filter, value) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// Match - finds whether the text matches/satisfies the pattern string.
|
||||
// pattern can include match type (ex: regex:^[a-z]bc )
|
||||
func Match(pattern string, value string) bool { |
||||
parts := strings.SplitN(pattern, ":", 2) |
||||
|
||||
// check if pattern defines match type
|
||||
if len(parts) > 1 { |
||||
matchType := strings.Trim(strings.ToLower(parts[0]), " ") |
||||
matchPattern := strings.Trim(parts[1], " ") |
||||
switch matchType { |
||||
case "exact": |
||||
return matchPattern == value |
||||
case "simple": |
||||
return MatchSimple(matchPattern, value) |
||||
case "wildcard": |
||||
return MatchWildCard(matchPattern, value) |
||||
case "regex": |
||||
isAllowed, _ := regexp.MatchString(matchPattern, value) |
||||
return isAllowed |
||||
default: |
||||
isAllowed, _ := regexp.MatchString(matchPattern, value) |
||||
return isAllowed |
||||
} |
||||
} |
||||
// auto detect simple checking or wildcard
|
||||
if regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(pattern) { |
||||
return strings.EqualFold(pattern, value) |
||||
} else if regexp.MustCompile(`^[a-zA-Z0-9_*?]+$`).MatchString(pattern) { |
||||
return MatchWildCard(pattern, value) |
||||
} |
||||
// by default we use regex matching
|
||||
allowed, _ := regexp.MatchString(pattern, value) |
||||
return allowed |
||||
} |
||||
|
||||
// MatchSimple - finds whether the text matches/satisfies the pattern string.
|
||||
// supports only '*' wildcard in the pattern.
|
||||
func MatchSimple(pattern, name string) bool { |
||||
if pattern == "" { |
||||
return name == pattern |
||||
} |
||||
|
||||
if pattern == "*" { |
||||
return true |
||||
} |
||||
// Does only wildcard '*' match.
|
||||
return deepMatchRune([]rune(name), []rune(pattern), true) |
||||
} |
||||
|
||||
// MatchWildCard - finds whether the text matches/satisfies the wildcard pattern string.
|
||||
// supports '*' and '?' wildcards in the pattern string.
|
||||
func MatchWildCard(pattern, name string) (matched bool) { |
||||
if pattern == "" { |
||||
return name == pattern |
||||
} |
||||
|
||||
if pattern == "*" { |
||||
return true |
||||
} |
||||
// Does extended wildcard '*' and '?' match.
|
||||
return deepMatchRune([]rune(name), []rune(pattern), false) |
||||
} |
||||
|
||||
func deepMatchRune(str, pattern []rune, simple bool) bool { |
||||
for len(pattern) > 0 { |
||||
switch pattern[0] { |
||||
default: |
||||
if len(str) == 0 || str[0] != pattern[0] { |
||||
return false |
||||
} |
||||
case '?': |
||||
if len(str) == 0 && !simple { |
||||
return false |
||||
} |
||||
case '*': |
||||
return deepMatchRune(str, pattern[1:], simple) || |
||||
(len(str) > 0 && deepMatchRune(str[1:], pattern, simple)) |
||||
} |
||||
|
||||
str = str[1:] |
||||
pattern = pattern[1:] |
||||
} |
||||
|
||||
return len(str) == 0 && len(pattern) == 0 |
||||
} |
||||
|
||||
// MatchRegex - finds whether the text matches/satisfies the regex pattern string.
|
||||
func MatchRegex(pattern string, value string) bool { |
||||
result, _ := regexp.MatchString(pattern, value) |
||||
return result |
||||
} |
@ -0,0 +1,205 @@ |
||||
package rpc |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
func TestRpcMethodFilter(t *testing.T) { |
||||
method_filters_toml := ` |
||||
Allow = [
|
||||
"hmy_method1", |
||||
"wildcard:hmyv2_method?", |
||||
"eth*", |
||||
"hmy_getNetworkInfo", |
||||
"regex:^hmy_send[a-zA-Z]+" |
||||
] |
||||
|
||||
Deny = [
|
||||
"*staking*", |
||||
"eth_get*", |
||||
"hmy_getNetworkInfo", |
||||
"exact:hmy_sendTx" |
||||
] |
||||
` |
||||
b := []byte(method_filters_toml) |
||||
|
||||
var rmf RpcMethodFilter |
||||
rmf.LoadRpcMethodFilters(b) |
||||
|
||||
tests := []struct { |
||||
name string |
||||
exposure bool |
||||
}{ |
||||
0: {"hmy_method1", true}, // auto detected exact match which exists (case-insensitive)
|
||||
1: {"hmy_MeThoD1", true}, // check case-insensitive
|
||||
2: {"hmy_method2", false}, // not exist in allows
|
||||
3: {"hmyv2_method5", true}, // wildcard
|
||||
4: {"hmyv2_method", false}, // false case for wild card
|
||||
5: {"eth_chainID", true}, // auto detected wild card in allow filters
|
||||
6: {"eth_getValidator", false}, // auto detected wild card in deny filters
|
||||
7: {"hmy_getStakingInfo", false}, // deny wild card
|
||||
8: {"abc", false}, // not exist pattern
|
||||
9: {"hmy_getNetworkInfo", false}, // case-insensitive normal word match
|
||||
10: {"hmy_sendTx", false}, // exact match (case-sensitive)
|
||||
11: {"hmy_sendtx", true}, // exact match (case-sensitive)
|
||||
} |
||||
|
||||
for i, test := range tests { |
||||
mustExpose := rmf.Expose(test.name) |
||||
|
||||
if mustExpose != test.exposure { |
||||
t.Errorf("Test %d got unexpected value, want %t, got %t", i, test.exposure, mustExpose) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestRpcMethodAllowAllFilter(t *testing.T) { |
||||
method_filters_toml := ` |
||||
Allow = [
|
||||
"*" |
||||
] |
||||
|
||||
Deny = [
|
||||
"mtd1", |
||||
"*staking*", |
||||
"eth_get*", |
||||
"^hmy_[a-z]+" |
||||
] |
||||
` |
||||
b := []byte(method_filters_toml) |
||||
|
||||
var rmf RpcMethodFilter |
||||
rmf.LoadRpcMethodFilters(b) |
||||
|
||||
tests := []struct { |
||||
name string |
||||
exposure bool |
||||
}{ |
||||
0: {"mtd1", false}, |
||||
1: {"hmy_method1", false}, |
||||
2: {"hmyv2_method5", true}, |
||||
3: {"hmyv2_method", true}, |
||||
4: {"eth_chainID", true}, |
||||
5: {"eth_getValidator", false}, |
||||
6: {"hmy_getStakingInfo", false}, |
||||
7: {"abc", true}, |
||||
8: {"hmy_getStakingNetworkInfo", false}, |
||||
} |
||||
|
||||
for i, test := range tests { |
||||
mustExpose := rmf.Expose(test.name) |
||||
|
||||
if mustExpose != test.exposure { |
||||
t.Errorf("Test %d got unexpected value, want %t, got %t", i, test.exposure, mustExpose) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestRpcMethodDenyAllFilter(t *testing.T) { |
||||
method_filters_toml := ` |
||||
Allow = [
|
||||
"mtd1", |
||||
"*staking*", |
||||
"eth_get*", |
||||
"regex:^hmy_[a-z]+" |
||||
] |
||||
|
||||
Deny = [
|
||||
"*" |
||||
] |
||||
` |
||||
b := []byte(method_filters_toml) |
||||
|
||||
var rmf RpcMethodFilter |
||||
rmf.LoadRpcMethodFilters(b) |
||||
|
||||
tests := []struct { |
||||
name string |
||||
exposure bool |
||||
}{ |
||||
0: {"mtd1", false}, |
||||
1: {"hmy_method1", false}, |
||||
2: {"hmyv2_method5", false}, |
||||
3: {"hmyv2_method", false}, |
||||
4: {"eth_chainID", false}, |
||||
5: {"eth_getValidator", false}, |
||||
6: {"hmy_getStakingInfo", false}, |
||||
7: {"abc", false}, |
||||
8: {"hmy_getStakingNetworkInfo", false}, |
||||
} |
||||
|
||||
for i, test := range tests { |
||||
mustExpose := rmf.Expose(test.name) |
||||
|
||||
if mustExpose != test.exposure { |
||||
t.Errorf("Test %d got unexpected value, want %t, got %t", i, test.exposure, mustExpose) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestEmptyRpcMethodFilter(t *testing.T) { |
||||
|
||||
b := []byte("") |
||||
var rmf RpcMethodFilter |
||||
rmf.LoadRpcMethodFilters(b) |
||||
|
||||
tests := []struct { |
||||
name string |
||||
exposure bool |
||||
}{ |
||||
0: {"hmy_method1", true}, |
||||
1: {"hmy_method2", true}, |
||||
2: {"hmyv2_method5", true}, |
||||
3: {"hmyv2_method", true}, |
||||
4: {"eth_chainID", true}, |
||||
5: {"eth_getValidator", true}, |
||||
6: {"hmy_getStakingInfo", true}, |
||||
7: {"abc", true}, |
||||
8: {"hmy_getNetworkInfo", true}, |
||||
} |
||||
|
||||
for i, test := range tests { |
||||
mustExpose := rmf.Expose(test.name) |
||||
|
||||
if mustExpose != test.exposure { |
||||
t.Errorf("Test %d got unexpected value, want %t, got %t", i, test.exposure, mustExpose) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestFilter(t *testing.T) { |
||||
tests := []struct { |
||||
input string |
||||
pattern string |
||||
expectedAllowance bool |
||||
}{ |
||||
0: {"abc", "abc", true}, |
||||
1: {"AbC", "abc", true}, // case-insensitive
|
||||
2: {"AbC", "exact:AbC", true}, // case-insensitive
|
||||
3: {"AbC", "exact:abc", false}, // case-insensitive
|
||||
4: {"abcd", "*", true}, // check * to pass everything
|
||||
5: {"abc", "simple:abc", true}, // check simple matching
|
||||
6: {"abcd", "simple:abc", false}, |
||||
7: {"abcd", "regex:^a([a-z]+)d$", true}, // check regex
|
||||
8: {"abcde", "regex:^a([a-z]+)d$", false}, |
||||
9: {"abcd", "^a([a-z]+)d$", true}, // auto detected regex
|
||||
10: {"abc", "wildcard:abc*", true}, // check wild card
|
||||
11: {"abc", "abc*", true}, // auto detected wild card
|
||||
12: {"abcdef", "abc*", true}, |
||||
13: {"dabcd", "?abc*", true}, // check * and ? for wild card
|
||||
14: {"abc", "*abc?", false}, // ? can't be empty
|
||||
15: {"abcdef", "*a?c*", true}, |
||||
16: {"defabc", "*ab?*", true}, |
||||
17: {"defabcghi", "*abc*", true}, |
||||
18: {"ab", "*abc*", false}, |
||||
19: {"defghabc", "*abc*", true}, |
||||
} |
||||
|
||||
for i, test := range tests { |
||||
isAllowed := Match(test.pattern, test.input) |
||||
|
||||
if isAllowed != test.expectedAllowance { |
||||
t.Errorf("Test %d got unexpected value, want %t, got %t", i, test.expectedAllowance, isAllowed) |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue