diff --git a/cmd/thalos/server.go b/cmd/thalos/server.go index 2aed773..0fa0c4a 100644 --- a/cmd/thalos/server.go +++ b/cmd/thalos/server.go @@ -238,6 +238,8 @@ func GetConfig(flags *pflag.FlagSet) (*config.Config, error) { } } + cfg.Ship.Blacklist.SetWhitelist(cfg.Ship.BlacklistIsWhitelist) + return cfg, nil } diff --git a/config.example.yml b/config.example.yml index 84990b1..9d6deef 100644 --- a/config.example.yml +++ b/config.example.yml @@ -54,6 +54,8 @@ ship: # blacklist all action from a contract # evilcontract: ["*"] + # blacklist_is_whitelist: true + # Telegram notifications #telegram: # id: "123456789:GPdmGPBWvpgHPxlergJLavus-PoAURTjMWP" diff --git a/internal/config/builder.go b/internal/config/builder.go index 026dd33..2967f63 100644 --- a/internal/config/builder.go +++ b/internal/config/builder.go @@ -53,6 +53,7 @@ func NewBuilder() *Builder { "ship.max_messages_in_flight": "max-msg-in-flight", "ship.chain": "chain", "ship.blacklist": "blacklist", + "ship.blacklist_is_whitelist": "blacklist-is-whitelist", }, } } @@ -116,30 +117,9 @@ func (b *Builder) Build() (*Config, error) { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToSliceHookFunc(","), func(f reflect.Type, t reflect.Type, in interface{}) (interface{}, error) { - if t == reflect.TypeOf(types.Blacklist{}) && f.Kind() == reflect.Slice { - if v, ok := in.([]string); ok { - list := types.Blacklist{} - for _, i := range v { - var action string - parts := strings.SplitN(i, ":", 2) - - if len(parts) < 2 { - action = "*" - } else { - action = parts[1] - } - - list.Add(parts[0], action) - } - - if len(list) < 1 { - list = nil - } - return list, nil - } - return nil, fmt.Errorf("Must be a string slice") + if t == reflect.TypeOf(types.Blacklist{}) { + return decodeIntoBlacklist(in) } - return in, nil }, ) @@ -151,3 +131,61 @@ func (b *Builder) Build() (*Config, error) { return &conf, nil } + +// Decode a generic structure into types.Blacklist +func decodeIntoBlacklist(in any) (*types.Blacklist, error) { + switch v := in.(type) { + // Standard map structure. + case map[string]any: + return blacklistParseMap(v) + + // slice of "contract:action" pairs. Usually from CLI + case []string: + return blacklistParseSlice(v) + + // Sometimes we have a slice of interfaces. + // Need to convert it to a slice of strings. + case []any: + sv := make([]string, len(v)) + for i, j := range v { + sv[i] = j.(string) + } + return blacklistParseSlice(sv) + } + + return nil, fmt.Errorf("Must be a string slice") +} + +// Blacklist map parser +func blacklistParseMap(in map[string]any) (*types.Blacklist, error) { + list := &types.Blacklist{} + for k, v := range in { + switch v := v.(type) { + case []any: + for _, v := range v { + list.Add(k, v.(string)) + } + case any: + list.Add(k, v.(string)) + } + } + return list, nil +} + +// Blacklist slice parser +func blacklistParseSlice(in []string) (*types.Blacklist, error) { + list := &types.Blacklist{} + for _, i := range in { + var action string + parts := strings.SplitN(i, ":", 2) + + if len(parts) < 2 { + action = "*" + } else { + action = parts[1] + } + + list.Add(parts[0], action) + } + return list, nil +} diff --git a/internal/config/builder_test.go b/internal/config/builder_test.go index d226a30..df24552 100644 --- a/internal/config/builder_test.go +++ b/internal/config/builder_test.go @@ -28,10 +28,11 @@ func TestBuilder(t *testing.T) { EndBlockNum: 23872222, IrreversibleOnly: true, MaxMessagesInFlight: 1337, - Blacklist: types.Blacklist{ + Blacklist: *types.NewBlacklist(map[string][]string{ "eosio": {"noop"}, "contract": {"skip1", "skip2"}, - }, + }), + BlacklistIsWhitelist: true, }, Telegram: TelegramConfig{ Id: "110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw", @@ -64,10 +65,11 @@ ship: start_block_num: 23671836 end_block_num: 23872222 blacklist: - eosio: ["noop"] + eosio: noop contract: - skip1 - skip2 + blacklist_is_whitelist: true telegram: id: "110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw" channel: -123456789 @@ -186,6 +188,7 @@ func TestBuilder_Flags(t *testing.T) { require.NoError(t, flags.Set("max-msg-in-flight", "98")) require.NoError(t, flags.Set("chain", "wax")) require.NoError(t, flags.Set("blacklist", "contract:action1,contract:action2,contract2:action1")) + require.NoError(t, flags.Set("blacklist-is-whitelist", "true")) cfg, err := NewBuilder(). SetSource(bytes.NewReader([]byte(``))). @@ -207,10 +210,11 @@ func TestBuilder_Flags(t *testing.T) { MaxMessagesInFlight: 98, IrreversibleOnly: true, Chain: "wax", - Blacklist: types.Blacklist{ + Blacklist: *types.NewBlacklist(map[string][]string{ "contract": {"action1", "action2"}, "contract2": {"action1"}, - }, + }), + BlacklistIsWhitelist: true, }, Telegram: TelegramConfig{ Id: "72983126312982618", @@ -229,20 +233,28 @@ func TestBuilder_Flags(t *testing.T) { require.Equal(t, &expected, cfg) } -func TestBuilder_BlacklistFlag(t *testing.T) { - flags := GetFlags() - - require.NoError(t, flags.Set("blacklist", "contract,contract:action2")) - - conf, err := NewBuilder(). - SetSource(bytes.NewReader([]byte(``))). - SetFlags(flags). - Build() - - expected := types.Blacklist{ - "contract": {"*", "action2"}, +func TestBuilder_BlacklistSlice(t *testing.T) { + expected := Config{ + Ship: ShipConfig{ + Blacklist: *types.NewBlacklist(map[string][]string{ + "contract": {"action"}, + "contract2": {"action2"}, + "contract3": {"*"}, + }), + }, } + builder := NewBuilder() + builder.SetSource(bytes.NewBuffer([]byte(` +ship: + blacklist: + - "contract:action" + - "contract2:action2" + - contract3 +`))) + + cfg, err := builder.Build() + require.NoError(t, err) - require.Equal(t, expected, conf.Ship.Blacklist) + require.Equal(t, &expected, cfg) } diff --git a/internal/config/cli.go b/internal/config/cli.go index 38439a5..85d8f39 100644 --- a/internal/config/cli.go +++ b/internal/config/cli.go @@ -44,6 +44,7 @@ func GetFlags() *pflag.FlagSet { flags.String("chain", "", "ChainID used in channel namespace, can be any string (default from api)") flags.StringSlice("blacklist", []string{}, "Define a list of 'contract:action' pairs that will be blacklisted (thalos will not process those actions)") + flags.Bool("blacklist-is-whitelist", false, "Thalos will treat the blacklist as a whitelist") return &flags } diff --git a/internal/config/config.go b/internal/config/config.go index 5e84072..7bb8b4b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,13 +19,14 @@ type TelegramConfig struct { } type ShipConfig struct { - Url string `yaml:"url" mapstructure:"url"` - IrreversibleOnly bool `yaml:"irreversible_only" mapstructure:"irreversible_only"` - MaxMessagesInFlight uint32 `yaml:"max_messages_in_flight" mapstructure:"max_messages_in_flight"` - StartBlockNum uint32 `yaml:"start_block_num" mapstructure:"start_block_num"` - EndBlockNum uint32 `yaml:"end_block_num" mapstructure:"end_block_num"` - Chain string `yaml:"chain" mapstructure:"chain"` - Blacklist types.Blacklist `yaml:"blacklist" mapstructure:"blacklist"` + Url string `yaml:"url" mapstructure:"url"` + IrreversibleOnly bool `yaml:"irreversible_only" mapstructure:"irreversible_only"` + MaxMessagesInFlight uint32 `yaml:"max_messages_in_flight" mapstructure:"max_messages_in_flight"` + StartBlockNum uint32 `yaml:"start_block_num" mapstructure:"start_block_num"` + EndBlockNum uint32 `yaml:"end_block_num" mapstructure:"end_block_num"` + Chain string `yaml:"chain" mapstructure:"chain"` + Blacklist types.Blacklist `yaml:"blacklist" mapstructure:"blacklist"` + BlacklistIsWhitelist bool `yaml:"blacklist_is_whitelist" mapstructure:"blacklist_is_whitelist"` } type Config struct { diff --git a/internal/server/ship_processor.go b/internal/server/ship_processor.go index 38ef73f..7106f4f 100644 --- a/internal/server/ship_processor.go +++ b/internal/server/ship_processor.go @@ -153,7 +153,7 @@ func (processor *ShipProcessor) proccessActionTrace(logger *log.Entry, trace *sh } // Check blacklist if we should skip this action - if processor.blacklist.Lookup(trace.Act.Account.String(), trace.Act.Name.String()) { + if !processor.blacklist.IsAllowed(trace.Act.Account.String(), trace.Act.Name.String()) { logger.WithFields(log.Fields{ "contract": trace.Act.Account, "action": trace.Act.Name, diff --git a/internal/types/blacklist.go b/internal/types/blacklist.go index cbe28b5..24419c8 100644 --- a/internal/types/blacklist.go +++ b/internal/types/blacklist.go @@ -1,21 +1,47 @@ package types -type Blacklist map[string][]string - -func (bl Blacklist) Add(contract string, action string) { - if len(bl[contract]) < 1 { - bl[contract] = []string{} - } - bl[contract] = append(bl[contract], action) +type Blacklist struct { + table map[string][]string + isWhitelist bool } -func (bl Blacklist) Lookup(contract string, action string) bool { - if v, ok := bl[contract]; ok { +func NewBlacklist(entries map[string][]string) *Blacklist { + return &Blacklist{ + table: entries, + } +} + +func (bl *Blacklist) SetWhitelist(value bool) *Blacklist { + bl.isWhitelist = value + return bl +} + +func (bl Blacklist) Empty() bool { + return len(bl.table) < 1 +} + +func (bl *Blacklist) Add(contract string, action string) { + if bl.table == nil { + bl.table = map[string][]string{} + } + + if len(bl.table[contract]) < 1 { + bl.table[contract] = []string{} + } + bl.table[contract] = append(bl.table[contract], action) +} + +func (bl Blacklist) IsAllowed(contract string, action string) bool { + if v, ok := bl.table[contract]; ok { for _, act := range v { if act == action || act == "*" { - return true + return bl.isWhitelist == true } } } - return false + return bl.isWhitelist == false +} + +func (bl Blacklist) IsDenied(contract string, action string) bool { + return bl.IsAllowed(contract, action) } diff --git a/internal/types/blacklist_test.go b/internal/types/blacklist_test.go index a897802..631d329 100644 --- a/internal/types/blacklist_test.go +++ b/internal/types/blacklist_test.go @@ -6,38 +6,73 @@ import ( "github.com/stretchr/testify/require" ) +func TestBlacklist_Empty(t *testing.T) { + bl := Blacklist{ + table: map[string][]string{}, + } + + require.True(t, bl.Empty()) + + bl.Add("contract", "action1") + + require.False(t, bl.Empty()) +} + func TestBlacklist_Add(t *testing.T) { - bl := Blacklist{} + bl := Blacklist{ + table: map[string][]string{}, + } bl.Add("contract", "action1") bl.Add("contract", "action2") bl.Add("contract2", "action1") expected := Blacklist{ - "contract": {"action1", "action2"}, - "contract2": {"action1"}, + table: map[string][]string{ + "contract": {"action1", "action2"}, + "contract2": {"action1"}, + }, } require.Equal(t, expected, bl) } -func TestBlacklist_Lookup(t *testing.T) { +func TestBlacklist_IsAllowed(t *testing.T) { bl := Blacklist{ - "mycontract": {"myaction", "noop"}, + table: map[string][]string{ + "mycontract": {"myaction", "noop"}, + }, } - require.True(t, bl.Lookup("mycontract", "myaction")) - require.True(t, bl.Lookup("mycontract", "noop")) - require.False(t, bl.Lookup("mycontract", "xxx")) - require.False(t, bl.Lookup("xxx", "yyy")) + require.False(t, bl.IsAllowed("mycontract", "myaction")) + require.False(t, bl.IsAllowed("mycontract", "noop")) + require.True(t, bl.IsAllowed("mycontract", "xxx")) + require.True(t, bl.IsAllowed("xxx", "yyy")) } -func TestBlacklist_LookupWildcard(t *testing.T) { +func TestBlacklist_IsAllowedWildcard(t *testing.T) { bl := Blacklist{ - "mycontract": {"*"}, + table: map[string][]string{ + "mycontract": {"*"}, + }, } - require.True(t, bl.Lookup("mycontract", "myaction")) - require.True(t, bl.Lookup("mycontract", "noop")) - require.True(t, bl.Lookup("mycontract", "xxx")) - require.False(t, bl.Lookup("xxx", "yyy")) + require.False(t, bl.IsAllowed("mycontract", "myaction")) + require.False(t, bl.IsAllowed("mycontract", "noop")) + require.False(t, bl.IsAllowed("mycontract", "xxx")) + require.True(t, bl.IsAllowed("xxx", "yyy")) +} + +func TestBlacklist_Whitelist(t *testing.T) { + bl := Blacklist{ + table: map[string][]string{ + "mycontract": {"myaction", "noop"}, + }, + } + + bl.SetWhitelist(true) + + require.True(t, bl.IsAllowed("mycontract", "myaction")) + require.True(t, bl.IsAllowed("mycontract", "noop")) + require.False(t, bl.IsAllowed("mycontract", "xxx")) + require.False(t, bl.IsAllowed("xxx", "yyy")) }