diff --git a/ext/ext.go b/ext/ext.go index 69c7a52be49b0064ad5ab78dd9b95031cca525d6..6a2eb8ffba3bb37603eb40e890d2600101067942 100644 --- a/ext/ext.go +++ b/ext/ext.go @@ -1,6 +1,14 @@ package ext -import "github.com/d5/tengo/objects" +import ( + "errors" + "fmt" + + "github.com/d5/tengo/objects" + + "git.autistici.org/ai3/tools/iprep/ext/dnsbl" + "git.autistici.org/ai3/tools/iprep/ext/geoip" +) // An ExternalSource provides per-IP information from third-party // sources. The lookup can return any Tengo object, we don't want to @@ -9,3 +17,32 @@ import "github.com/d5/tengo/objects" type ExternalSource interface { LookupIP(string) (objects.Object, error) } + +// New creates a new ExternalSource. +func New(sourceType string, params map[string]interface{}) (ExternalSource, error) { + switch sourceType { + + case "dnsbl": + domain, ok := params["domain"].(string) + if !ok { + return nil, errors.New("missing parameter 'domain'") + } + match, ok := params["match"].(string) + if !ok { + return nil, errors.New("missing parameter 'match'") + } + return dnsbl.New(domain, match) + + case "geoip": + var paths []string + if l, ok := params["paths"].([]interface{}); ok { + for _, p := range l { + paths = append(paths, p.(string)) + } + } + return geoip.New(paths) + + default: + return nil, fmt.Errorf("unknown source type '%s'", sourceType) + } +} diff --git a/ext/geoip/geoip.go b/ext/geoip/geoip.go index 9eac8a24fd8fe3fedd27812155a9c854c637f30c..470a75a3b370eeaa91e14fa4e49d1dcddd8ff90b 100644 --- a/ext/geoip/geoip.go +++ b/ext/geoip/geoip.go @@ -15,7 +15,7 @@ type GeoIP struct { readers []*maxminddb.Reader } -func NewGeoIP(paths []string) (*GeoIP, error) { +func New(paths []string) (*GeoIP, error) { if len(paths) == 0 { paths = defaultGeoIPPaths } diff --git a/script/script.go b/script/script.go index 55f0be391f97619c5b5e8341e19afa4b4788f7ba..c55752b4c11e970e6b98416e4c72e1836decc7c7 100644 --- a/script/script.go +++ b/script/script.go @@ -8,6 +8,8 @@ import ( "github.com/d5/tengo/script" "github.com/d5/tengo/stdlib" + + "git.autistici.org/ai3/tools/iprep/ext" ) // A Script is a compiled Tengo script that will be executed on every @@ -72,7 +74,7 @@ func NewScript(src []byte) (*Script, error) { // RunIP evaluates the script once with the provided context and // returns the resulting score. -func (script *Script) RunIP(ctx context.Context, ip string, counts map[string]int64, intervalSecs float64, ext map[string]interface{}) (float64, error) { +func (script *Script) RunIP(ctx context.Context, ip string, counts map[string]int64, intervalSecs float64, extSrcs map[string]ext.ExternalSource) (float64, error) { c := script.compiled.Clone() // Set the global variables that constitute the script @@ -88,7 +90,7 @@ func (script *Script) RunIP(ctx context.Context, ip string, counts map[string]in if err := c.Set("interval", intervalSecs); err != nil { return 0, err } - if err := c.Set("ext", ext); err != nil { + if err := c.Set("ext", extLookupFunc(extSrcs)); err != nil { return 0, err } if err := c.Set("counts", intMap(counts)); err != nil { diff --git a/script/script_test.go b/script/script_test.go index 2a0b71adb25085f0a9398c8fea6ffd169dd1c3b3..3fc38784ef3a391841403d14946fd45ce3b79ddd 100644 --- a/script/script_test.go +++ b/script/script_test.go @@ -3,9 +3,23 @@ package script import ( "context" "testing" + + "github.com/d5/tengo/objects" + + "git.autistici.org/ai3/tools/iprep/ext" ) -func runTestScript(t *testing.T, src string, expected float64) { +type dummyExtSource map[string]int64 + +func (s dummyExtSource) LookupIP(ip string) (objects.Object, error) { + value, ok := s[ip] + if !ok { + return objects.UndefinedValue, nil + } + return &objects.Int{Value: value}, nil +} + +func runTestScript(t *testing.T, src string, expected float64, extSrcs map[string]ext.ExternalSource) { s, err := NewScript([]byte(src)) if err != nil { t.Fatalf("NewScript: %v", err) @@ -15,7 +29,7 @@ func runTestScript(t *testing.T, src string, expected float64) { "test2": 2, } - score, err := s.RunIP(context.Background(), "1.2.3.4", m, 3600, nil) + score, err := s.RunIP(context.Background(), "1.2.3.4", m, 3600, extSrcs) if err != nil { t.Fatalf("runScript: %v", err) } @@ -28,23 +42,33 @@ func TestScript_WithLookup(t *testing.T) { runTestScript(t, ` score = counts["test"] / 2 + counts["test2"] //score = 7 -`, 7) +`, 7, nil) } func TestScript_FloatScore(t *testing.T) { runTestScript(t, ` score = 7.0 -`, 7) +`, 7, nil) } func TestScript_IntScore(t *testing.T) { runTestScript(t, ` score = 7 -`, 7) +`, 7, nil) } func TestScript_StringScore(t *testing.T) { runTestScript(t, ` score = "7" -`, 7) +`, 7, nil) +} + +func TestScript_ExternalSource(t *testing.T) { + runTestScript(t, ` +score = ext("test", ip) +`, 7, map[string]ext.ExternalSource{ + "test": dummyExtSource{ + "1.2.3.4": 7, + }, + }) } diff --git a/script/types.go b/script/types.go index 4e9d7a01b8cc02ba20098b6d29fa5741b2ac642e..cd69b6231eff6aa307db8f73ef3f44d6fc63ca93 100644 --- a/script/types.go +++ b/script/types.go @@ -1,8 +1,12 @@ package script import ( + "log" + "github.com/d5/tengo/compiler/token" "github.com/d5/tengo/objects" + + "git.autistici.org/ai3/tools/iprep/ext" ) // Tengo object type that wraps a read-only map[string]int64, without @@ -105,3 +109,70 @@ func (i *intMapIterator) Key() objects.Object { func (i *intMapIterator) Value() objects.Object { return &objects.Int{Value: i.arr[i.idx-1].value} } + +// A Tengo callable to look up data in external sources. +type extLookupFunc map[string]ext.ExternalSource + +func (f extLookupFunc) String() string { + return "<extLookupFunc>" +} + +func (f extLookupFunc) TypeName() string { + return "ext-lookup-func" +} + +func (f extLookupFunc) Copy() objects.Object { + return f +} + +func (f extLookupFunc) IsFalsy() bool { + return len(f) == 0 +} + +func (f extLookupFunc) Equals(o objects.Object) bool { + return false +} + +func (f extLookupFunc) BinaryOp(op token.Token, rhs objects.Object) (objects.Object, error) { + return nil, objects.ErrInvalidOperator +} + +func (f extLookupFunc) Call(args ...objects.Object) (ret objects.Object, err error) { + // Expected arguments: source name, IP. + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + name, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidArgumentType{ + Name: "source_name", + Expected: "string", + Found: args[0].TypeName(), + } + } + + ip, ok := objects.ToString(args[1]) + if !ok { + return nil, objects.ErrInvalidArgumentType{ + Name: "ip", + Expected: "string", + Found: args[1].TypeName(), + } + } + + // Invoke the external source lookup method. + if f == nil { + return nil, objects.ErrInvalidIndexValueType + } + src, ok := f[name] + if !ok { + return nil, objects.ErrInvalidIndexValueType + } + result, err := src.LookupIP(ip) + if err != nil { + log.Printf("external source lookup error: ip=%s: %v", ip, err) + return objects.UndefinedValue, nil + } + return result, nil +} diff --git a/server/server.go b/server/server.go index 9edf106a08619e824192f7698afd185a914184e2..3d82f86f5d4779cbb85ce68f3c0af3aecd7f7c13 100644 --- a/server/server.go +++ b/server/server.go @@ -10,12 +10,14 @@ import ( "google.golang.org/grpc/status" "git.autistici.org/ai3/tools/iprep/db" + "git.autistici.org/ai3/tools/iprep/ext" ippb "git.autistici.org/ai3/tools/iprep/proto" "git.autistici.org/ai3/tools/iprep/script" ) type Server struct { *script.Manager + ext map[string]ext.ExternalSource horizon time.Duration db db.DB stop chan struct{} @@ -30,7 +32,7 @@ var ( defaultScript = `score = 0` ) -func New(dbPath, scriptPath string) (*Server, error) { +func New(dbPath, scriptPath string, extSrcs map[string]ext.ExternalSource) (*Server, error) { scriptMgr, err := script.NewManager(scriptPath, defaultScript) if err != nil { return nil, err @@ -43,6 +45,7 @@ func New(dbPath, scriptPath string) (*Server, error) { s := &Server{ Manager: scriptMgr, + ext: extSrcs, horizon: defaultHorizon, db: database, stop: make(chan struct{}), @@ -88,7 +91,7 @@ func (s *Server) GetScore(ctx context.Context, req *ippb.GetScoreRequest) (*ippb return nil, status.Errorf(codes.Unavailable, "%v", err) } - score, err := s.Script().RunIP(ctx, req.Ip, counts, s.horizon.Seconds(), nil) + score, err := s.Script().RunIP(ctx, req.Ip, counts, s.horizon.Seconds(), s.ext) if err != nil { return nil, status.Errorf(codes.Internal, "%v", err) }