diff --git a/ext/dnsbl/dnsbl.go b/ext/dnsbl/dnsbl.go new file mode 100644 index 0000000000000000000000000000000000000000..eed1904e05e3cb7a324dfb997a8ff4a5e97ccfb5 --- /dev/null +++ b/ext/dnsbl/dnsbl.go @@ -0,0 +1,61 @@ +package dnsbl + +import ( + "fmt" + "net" + "regexp" + "strings" + + "github.com/d5/tengo/objects" +) + +// The DNSBL external source looks up IP addresses in a DNS-based +// blacklist, and expects the result to match a specific pattern. +type DNSBL struct { + domain string + matchRx *regexp.Regexp +} + +func New(domain, match string) (*DNSBL, error) { + if match == "" { + match = `^127\.0\.0\.[0-9]*$` + } + rx, err := regexp.Compile(match) + if err != nil { + return nil, err + } + return &DNSBL{ + domain: domain, + matchRx: rx, + }, nil +} + +func reverseIP(ip net.IP) string { + if ip.To4() != nil { + parts := strings.Split(ip.String(), ".") + rev := make([]string, len(parts)) + for i := 0; i < len(parts); i++ { + rev[len(parts)-i-1] = parts[i] + } + return strings.Join(rev, ".") + } + return "" +} + +// LookupIP implements the ExternalSource interface. We'd like to +// return a boolean but can't figure out how to build one with Tengo, +// so we return a 0/1 int result instead. +func (d *DNSBL) LookupIP(ip string) (objects.Object, error) { + rev := reverseIP(net.ParseIP(ip)) + if rev == "" { + return objects.UndefinedValue, nil + } + query := fmt.Sprintf("%s.%s", rev, d.domain) + ips, err := net.LookupIP(query) + + var retval int64 = 0 + if err == nil && len(ips) > 0 && d.matchRx.MatchString(ips[0].String()) { + retval = 1 + } + return &objects.Int{Value: retval}, nil +} diff --git a/ext/ext.go b/ext/ext.go new file mode 100644 index 0000000000000000000000000000000000000000..69c7a52be49b0064ad5ab78dd9b95031cca525d6 --- /dev/null +++ b/ext/ext.go @@ -0,0 +1,11 @@ +package ext + +import "github.com/d5/tengo/objects" + +// An ExternalSource provides per-IP information from third-party +// sources. The lookup can return any Tengo object, we don't want to +// force a specific return type yet (int or string can both be +// useful, we'll see). +type ExternalSource interface { + LookupIP(string) (objects.Object, error) +} diff --git a/ext/geoip/geoip.go b/ext/geoip/geoip.go new file mode 100644 index 0000000000000000000000000000000000000000..9eac8a24fd8fe3fedd27812155a9c854c637f30c --- /dev/null +++ b/ext/geoip/geoip.go @@ -0,0 +1,47 @@ +package geoip + +import ( + "net" + + "github.com/d5/tengo/objects" + "github.com/oschwald/maxminddb-golang" +) + +var defaultGeoIPPaths = []string{ + "/var/lib/GeoIP/GeoLite2-Country.mmdb", +} + +type GeoIP struct { + readers []*maxminddb.Reader +} + +func NewGeoIP(paths []string) (*GeoIP, error) { + if len(paths) == 0 { + paths = defaultGeoIPPaths + } + + g := new(GeoIP) + for _, path := range paths { + r, err := maxminddb.Open(path) + if err != nil { + return nil, err + } + g.readers = append(g.readers, r) + } + return g, nil +} + +func (g *GeoIP) LookupIP(ipStr string) (objects.Object, error) { + ip := net.ParseIP(ipStr) + var record struct { + Country struct { + ISOCode string `maxminddb:"iso_code"` + } `maxminddb:"country"` + } + for _, r := range g.readers { + if err := r.Lookup(ip, &record); err == nil { + return &objects.String{Value: record.Country.ISOCode}, nil + } + } + return objects.UndefinedValue, nil +}