From 21f1207afee6915c14e1109834e3fc0dfed9f420 Mon Sep 17 00:00:00 2001 From: Sven Nebel Date: Wed, 14 Jul 2021 08:25:30 +0100 Subject: [PATCH] Create geoip plugin (#4688) * Create geoip plugin Signed-off-by: Sven Nebel * Update plugin/geoip/README.md Co-authored-by: Miek Gieben Signed-off-by: Sven Nebel * Update plugin/geoip/README.md Co-authored-by: Miek Gieben Signed-off-by: Sven Nebel * Update plugin/geoip/README.md Co-authored-by: Miek Gieben Signed-off-by: Sven Nebel * Move DBFILE bullet below example Signed-off-by: Sven Nebel * Update plugin/geoip/README.md Co-authored-by: Miek Gieben Signed-off-by: Sven Nebel * Remove plugin name test case Signed-off-by: Sven Nebel * Remove languages option Signed-off-by: Sven Nebel * Update free database link Signed-off-by: Sven Nebel * Remove last language bits Signed-off-by: Sven Nebel * Use 127.0.0.1 as probing IP Signed-off-by: Sven Nebel * Update plugin/geoip/geoip.go Co-authored-by: Miek Gieben Signed-off-by: Sven Nebel * Update plugin/geoip/geoip.go Co-authored-by: Miek Gieben Signed-off-by: Sven Nebel * Use relative path for fixtures dir Signed-off-by: Sven Nebel * Set names with default string zero value Signed-off-by: Sven Nebel * Remove unused db types Signed-off-by: Sven Nebel * Remove non city databases in testdata Signed-off-by: Sven Nebel * Remove create databases main Signed-off-by: Sven Nebel * Fix metadata label format test case Signed-off-by: Sven Nebel * Fix import path block Signed-off-by: Sven Nebel * go fmt after changes Signed-off-by: Sven Nebel * Tidy up go.mod and go.sum Signed-off-by: Sven Nebel * Add plugin to CODEOWNERS Signed-off-by: Sven Nebel Co-authored-by: Miek Gieben --- CODEOWNERS | 1 + core/dnsserver/zdirectives.go | 1 + core/plugin/zplugin.go | 1 + go.mod | 1 + go.sum | 5 + plugin.cfg | 1 + plugin/geoip/README.md | 73 ++++++++++++ plugin/geoip/city.go | 58 +++++++++ plugin/geoip/geoip.go | 95 +++++++++++++++ plugin/geoip/geoip_test.go | 61 ++++++++++ plugin/geoip/setup.go | 53 +++++++++ plugin/geoip/setup_test.go | 109 +++++++++++++++++ plugin/geoip/testdata/GeoLite2-City.mmdb | Bin 0 -> 3281 bytes .../testdata/GeoLite2-UnknownDbType.mmdb | Bin 0 -> 3280 bytes plugin/geoip/testdata/README.md | 112 ++++++++++++++++++ plugin/metadata/metadata_test.go | 2 +- plugin/metadata/provider.go | 6 +- 17 files changed, 573 insertions(+), 6 deletions(-) create mode 100644 plugin/geoip/README.md create mode 100644 plugin/geoip/city.go create mode 100644 plugin/geoip/geoip.go create mode 100644 plugin/geoip/geoip_test.go create mode 100644 plugin/geoip/setup.go create mode 100644 plugin/geoip/setup_test.go create mode 100644 plugin/geoip/testdata/GeoLite2-City.mmdb create mode 100644 plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb create mode 100644 plugin/geoip/testdata/README.md diff --git a/CODEOWNERS b/CODEOWNERS index 894d9afb4..e2562db12 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -30,6 +30,7 @@ go.mod @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip /plugin/etcd/ @miekg @nitisht /plugin/file/ @miekg @yongtang @stp-ip /plugin/forward/ @johnbelamaric @miekg @rdrozhdzh +/plugin/geoip/ @miekg @snebel29 /plugin/grpc/ @inigohu @miekg @zouyee /plugin/health/ @fastest963 @miekg @zouyee /plugin/hosts/ @johnbelamaric @pmoroney diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go index 4726345e3..134ed20b2 100644 --- a/core/dnsserver/zdirectives.go +++ b/core/dnsserver/zdirectives.go @@ -11,6 +11,7 @@ package dnsserver // care what plugin above them are doing. var Directives = []string{ "metadata", + "geoip", "cancel", "tls", "reload", diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go index eee813910..b2692fbbb 100644 --- a/core/plugin/zplugin.go +++ b/core/plugin/zplugin.go @@ -25,6 +25,7 @@ import ( _ "github.com/coredns/coredns/plugin/etcd" _ "github.com/coredns/coredns/plugin/file" _ "github.com/coredns/coredns/plugin/forward" + _ "github.com/coredns/coredns/plugin/geoip" _ "github.com/coredns/coredns/plugin/grpc" _ "github.com/coredns/coredns/plugin/health" _ "github.com/coredns/coredns/plugin/hosts" diff --git a/go.mod b/go.mod index b6fb26044..cff9a3cdb 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/opentracing/opentracing-go v1.2.0 github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 github.com/openzipkin/zipkin-go v0.2.2 + github.com/oschwald/geoip2-golang v1.5.0 github.com/philhofer/fwd v1.1.1 // indirect github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_model v0.2.0 diff --git a/go.sum b/go.sum index 01c0d7188..303abdc0d 100644 --- a/go.sum +++ b/go.sum @@ -342,6 +342,10 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/oschwald/geoip2-golang v1.5.0 h1:igg2yQIrrcRccB1ytFXqBfOHCjXWIoMv85lVJ1ONZzw= +github.com/oschwald/geoip2-golang v1.5.0/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s= +github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk= +github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= @@ -558,6 +562,7 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/plugin.cfg b/plugin.cfg index 58f176793..a80ea97ef 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -20,6 +20,7 @@ # log:log metadata:metadata +geoip:geoip cancel:cancel tls:tls reload:reload diff --git a/plugin/geoip/README.md b/plugin/geoip/README.md new file mode 100644 index 000000000..b666518f7 --- /dev/null +++ b/plugin/geoip/README.md @@ -0,0 +1,73 @@ +# geoip + +## Name +*geoip* - Lookup maxmind geoip2 databases using the client IP, then add associated geoip data to the context request. + +## Description +The *geoip* plugin add geo location data associated with the client IP, it allows you to configure a [geoIP2 maxmind database](https://dev.maxmind.com/geoip/docs/databases) to add the geo location data associated with the IP address. + +The data is added leveraging the *metadata* plugin, values can then be retrieved using it as well, for example: + +```go +import ( + "strconv" + "github.com/coredns/coredns/plugin/metadata" +) +// ... +if getLongitude := metadata.ValueFunc(ctx, "geoip/longitude"); getLongitude != nil { + if longitude, err := strconv.ParseFloat(getLongitude(), 64); err == nil { + // Do something useful with longitude. + } +} else { + // The metadata label geoip/longitude for some reason, was not set. +} +// ... +``` + +## Databases +The supported databases use city schema such as `City` and `Enterprise`. Other databases types with different schemas are not supported yet. + +You can download a [free and public City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data). + +## Syntax +```txt +geoip [DBFILE] +``` +* **DBFILE** the mmdb database file path. + +## Examples +The following configuration configures the `City` database. +```txt +. { + geoip /opt/geoip2/db/GeoLite2-City.mmdb + metadata # Note that metadata plugin must be enabled as well. +} +``` + +## Metadatada Labels +A limited set of fields will be exported as labels, all values are stored using strings **regardless of their underlying value type**, and therefore you may have to convert it back to its original type, note that numeric values are always represented in base 10. + +| Label | Type | Example | Description +| :----------------------------------- | :-------- | :-------------- | :------------------ +| `geoip/city/name` | `string` | `Cambridge` | Then city name in English language. +| `geoip/country/code` | `string` | `GB` | Country [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) code. +| `geoip/country/name` | `string` | `United Kingdom` | The country name in English language. +| `geoip/country/is_in_european_union` | `bool` | `false` | Either `true` or `false`. +| `geoip/continent/code` | `string` | `EU` | See [Continent codes](#ContinentCodes). +| `geoip/continent/name` | `string` | `Europe` | The continent name in English language. +| `geoip/latitude` | `float64` | `52.2242` | Base 10, max available precision. +| `geoip/longitude` | `float64` | `0.1315` | Base 10, max available precision. +| `geoip/timezone` | `string` | `Europe/London` | The timezone. +| `geoip/postalcode` | `string` | `CB4` | The postal code. + +## Continent Codes + +| Value | Continent (EN) | +| :---- | :------------- | +| AF | Africa | +| AN | Antarctica | +| AS | Asia | +| EU | Europe | +| NA | North America | +| OC | Oceania | +| SA | South America | diff --git a/plugin/geoip/city.go b/plugin/geoip/city.go new file mode 100644 index 000000000..4cfd254a6 --- /dev/null +++ b/plugin/geoip/city.go @@ -0,0 +1,58 @@ +package geoip + +import ( + "context" + "strconv" + + "github.com/coredns/coredns/plugin/metadata" + + "github.com/oschwald/geoip2-golang" +) + +const defaultLang = "en" + +func (g GeoIP) setCityMetadata(ctx context.Context, data *geoip2.City) { + // Set labels for city, country and continent names. + cityName := data.City.Names[defaultLang] + metadata.SetValueFunc(ctx, pluginName+"/city/name", func() string { + return cityName + }) + countryName := data.Country.Names[defaultLang] + metadata.SetValueFunc(ctx, pluginName+"/country/name", func() string { + return countryName + }) + continentName := data.Continent.Names[defaultLang] + metadata.SetValueFunc(ctx, pluginName+"/continent/name", func() string { + return continentName + }) + + countryCode := data.Country.IsoCode + metadata.SetValueFunc(ctx, pluginName+"/country/code", func() string { + return countryCode + }) + isInEurope := strconv.FormatBool(data.Country.IsInEuropeanUnion) + metadata.SetValueFunc(ctx, pluginName+"/country/is_in_european_union", func() string { + return isInEurope + }) + continentCode := data.Continent.Code + metadata.SetValueFunc(ctx, pluginName+"/continent/code", func() string { + return continentCode + }) + + latitude := strconv.FormatFloat(float64(data.Location.Latitude), 'f', -1, 64) + metadata.SetValueFunc(ctx, pluginName+"/latitude", func() string { + return latitude + }) + longitude := strconv.FormatFloat(float64(data.Location.Longitude), 'f', -1, 64) + metadata.SetValueFunc(ctx, pluginName+"/longitude", func() string { + return longitude + }) + timeZone := data.Location.TimeZone + metadata.SetValueFunc(ctx, pluginName+"/timezone", func() string { + return timeZone + }) + postalCode := data.Postal.Code + metadata.SetValueFunc(ctx, pluginName+"/postalcode", func() string { + return postalCode + }) +} diff --git a/plugin/geoip/geoip.go b/plugin/geoip/geoip.go new file mode 100644 index 000000000..674157716 --- /dev/null +++ b/plugin/geoip/geoip.go @@ -0,0 +1,95 @@ +// Package geoip implements a max mind database plugin. +package geoip + +import ( + "context" + "fmt" + "net" + "path/filepath" + + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + "github.com/oschwald/geoip2-golang" +) + +var log = clog.NewWithPlugin(pluginName) + +// GeoIP is a plugin that add geo location data to the request context by looking up a maxmind +// geoIP2 database, and which data can be later consumed by other middlewares. +type GeoIP struct { + Next plugin.Handler + db db +} + +type db struct { + *geoip2.Reader + // provides defines the schemas that can be obtained by querying this database, by using + // bitwise operations. + provides int +} + +const ( + city = 1 << iota +) + +var probingIP = net.ParseIP("127.0.0.1") + +func newGeoIP(dbPath string) (*GeoIP, error) { + reader, err := geoip2.Open(dbPath) + if err != nil { + return nil, fmt.Errorf("failed to open database file: %v", err) + } + db := db{Reader: reader} + schemas := []struct { + provides int + name string + validate func() error + }{ + {name: "city", provides: city, validate: func() error { _, err := reader.City(probingIP); return err }}, + } + // Query the database to figure out the database type. + for _, schema := range schemas { + if err := schema.validate(); err != nil { + // If we get an InvalidMethodError then we know this database does not provide that schema. + if _, ok := err.(geoip2.InvalidMethodError); !ok { + return nil, fmt.Errorf("unexpected failure looking up database %q schema %q: %v", filepath.Base(dbPath), schema.name, err) + } + } else { + db.provides = db.provides | schema.provides + } + } + + if db.provides&city == 0 { + return nil, fmt.Errorf("database does not provide city schema") + } + + return &GeoIP{db: db}, nil +} + +// ServeDNS implements the plugin.Handler interface. +func (g GeoIP) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return plugin.NextOrFailure(pluginName, g.Next, ctx, w, r) +} + +// Metadata implements the metadata.Provider Interface in the metadata plugin, and is used to store +// the data associated with the source IP of every request. +func (g GeoIP) Metadata(ctx context.Context, state request.Request) context.Context { + srcIP := net.ParseIP(state.IP()) + + switch { + case g.db.provides&city == city: + data, err := g.db.City(srcIP) + if err != nil { + log.Debugf("Setting up metadata failed due to database lookup error: %v", err) + return ctx + } + g.setCityMetadata(ctx, data) + } + return ctx +} + +// Name implements the Handler interface. +func (g GeoIP) Name() string { return pluginName } diff --git a/plugin/geoip/geoip_test.go b/plugin/geoip/geoip_test.go new file mode 100644 index 000000000..99213138b --- /dev/null +++ b/plugin/geoip/geoip_test.go @@ -0,0 +1,61 @@ +package geoip + +import ( + "context" + "fmt" + "testing" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" +) + +func TestMetadata(t *testing.T) { + + tests := []struct { + dbPath string + label string + expectedValue string + }{ + {cityDBPath, "geoip/city/name", "Cambridge"}, + + {cityDBPath, "geoip/country/code", "GB"}, + {cityDBPath, "geoip/country/name", "United Kingdom"}, + // is_in_european_union is set to true only to work around bool zero value, and test is really being set. + {cityDBPath, "geoip/country/is_in_european_union", "true"}, + + {cityDBPath, "geoip/continent/code", "EU"}, + {cityDBPath, "geoip/continent/name", "Europe"}, + + {cityDBPath, "geoip/latitude", "52.2242"}, + {cityDBPath, "geoip/longitude", "0.1315"}, + {cityDBPath, "geoip/timezone", "Europe/London"}, + {cityDBPath, "geoip/postalcode", "CB4"}, + } + + for i, _test := range tests { + geoIP, err := newGeoIP(_test.dbPath) + if err != nil { + t.Fatalf("Test %d: unable to create geoIP plugin: %v", i, err) + } + state := request.Request{ + W: &test.ResponseWriter{RemoteIP: "81.2.69.142"}, // This IP should be be part of the CDIR address range used to create the database fixtures. + } + ctx := metadata.ContextWithMetadata(context.Background()) + rCtx := geoIP.Metadata(ctx, state) + if fmt.Sprintf("%p", ctx) != fmt.Sprintf("%p", rCtx) { + t.Errorf("Test %d: returned context is expected to be the same one passed in the Metadata function", i) + } + + fn := metadata.ValueFunc(ctx, _test.label) + if fn == nil { + t.Errorf("Test %d: label %q not set in metadata plugin context", i, _test.label) + continue + } + value := fn() + if value != _test.expectedValue { + t.Errorf("Test %d: expected value for label %q should be %q, got %q instead", + i, _test.label, _test.expectedValue, value) + } + } +} diff --git a/plugin/geoip/setup.go b/plugin/geoip/setup.go new file mode 100644 index 000000000..6883bbe2d --- /dev/null +++ b/plugin/geoip/setup.go @@ -0,0 +1,53 @@ +package geoip + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +const pluginName = "geoip" + +func init() { plugin.Register(pluginName, setup) } + +func setup(c *caddy.Controller) error { + geoip, err := geoipParse(c) + if err != nil { + return plugin.Error(pluginName, err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + geoip.Next = next + return geoip + }) + + return nil +} + +func geoipParse(c *caddy.Controller) (*GeoIP, error) { + var dbPath string + + for c.Next() { + if !c.NextArg() { + return nil, c.ArgErr() + } + if dbPath != "" { + return nil, c.Errf("configuring multiple databases is not supported") + } + dbPath = c.Val() + // There shouldn't be any more arguments. + if len(c.RemainingArgs()) != 0 { + return nil, c.ArgErr() + } + // The plugin should not have any config block. + if c.NextBlock() { + return nil, c.Err("unexpected config block") + } + } + + geoIP, err := newGeoIP(dbPath) + if err != nil { + return geoIP, c.Err(err.Error()) + } + return geoIP, nil +} diff --git a/plugin/geoip/setup_test.go b/plugin/geoip/setup_test.go new file mode 100644 index 000000000..94d40adbc --- /dev/null +++ b/plugin/geoip/setup_test.go @@ -0,0 +1,109 @@ +package geoip + +import ( + "fmt" + "net" + "path/filepath" + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +var ( + fixturesDir = "./testdata" + cityDBPath = filepath.Join(fixturesDir, "GeoLite2-City.mmdb") + unknownDBPath = filepath.Join(fixturesDir, "GeoLite2-UnknownDbType.mmdb") +) + +func TestProbingIP(t *testing.T) { + if probingIP == nil { + t.Fatalf("Invalid probing IP: %q", probingIP) + } +} + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", fmt.Sprintf("%s %s", pluginName, cityDBPath)) + plugins := dnsserver.GetConfig(c).Plugin + if len(plugins) != 0 { + t.Fatalf("Expected zero plugins after setup, %d found", len(plugins)) + } + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + plugins = dnsserver.GetConfig(c).Plugin + if len(plugins) != 1 { + t.Fatalf("Expected one plugin after setup, %d found", len(plugins)) + } +} + +func TestGeoIPParse(t *testing.T) { + c := caddy.NewTestController("dns", fmt.Sprintf("%s %s", pluginName, cityDBPath)) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + tests := []struct { + shouldErr bool + config string + expectedErr string + expectedDBType int + }{ + // Valid + {false, fmt.Sprintf("%s %s\n", pluginName, cityDBPath), "", city}, + + // Invalid + {true, pluginName, "Wrong argument count", 0}, + {true, fmt.Sprintf("%s %s {\n\tlanguages en fr es zh-CN\n}\n", pluginName, cityDBPath), "unexpected config block", 0}, + {true, fmt.Sprintf("%s %s\n%s %s\n", pluginName, cityDBPath, pluginName, cityDBPath), "configuring multiple databases is not supported", 0}, + {true, fmt.Sprintf("%s 1 2 3", pluginName), "Wrong argument count", 0}, + {true, fmt.Sprintf("%s { }", pluginName), "Error during parsing", 0}, + {true, fmt.Sprintf("%s /dbpath { city }", pluginName), "unexpected config block", 0}, + {true, fmt.Sprintf("%s /invalidPath\n", pluginName), "failed to open database file: open /invalidPath: no such file or directory", 0}, + {true, fmt.Sprintf("%s %s\n", pluginName, unknownDBPath), "reader does not support the \"UnknownDbType\" database type", 0}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.config) + geoIP, err := geoipParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: expected error but found none for input %s", i, test.config) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.config, err) + } + + if !strings.Contains(err.Error(), test.expectedErr) { + t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.config) + } + continue + } + + if geoIP.db.Reader == nil { + t.Errorf("Test %d: after parsing database reader should be initialized", i) + } + + if geoIP.db.provides&test.expectedDBType == 0 { + t.Errorf("Test %d: expected db type %d not found, database file provides %d", i, test.expectedDBType, geoIP.db.provides) + } + } + + // Set nil probingIP to test unexpected validate error() + defer func(ip net.IP) { probingIP = ip }(probingIP) + probingIP = nil + + c = caddy.NewTestController("dns", fmt.Sprintf("%s %s\n", pluginName, cityDBPath)) + _, err := geoipParse(c) + if err != nil { + expectedErr := "unexpected failure looking up database" + if !strings.Contains(err.Error(), expectedErr) { + t.Errorf("expected error to contain: %s", expectedErr) + } + } else { + t.Errorf("with a nil probingIP test is expected to fail") + } +} diff --git a/plugin/geoip/testdata/GeoLite2-City.mmdb b/plugin/geoip/testdata/GeoLite2-City.mmdb new file mode 100644 index 0000000000000000000000000000000000000000..cd79ed914f078563e5f22bad01f4d30044b6c2d0 GIT binary patch literal 3281 zcmZA22YA$E7{Kv2ZKZ%Bf+8R*C}7zlpa`-$=q|Ie)FZuwYoM1L$z7pkxN&b0w-y(Q z3*37v4xG3r;6hPUa3boL{L3BM=eg&1@AyW(T%S)w6VZ|)S&F41*@|+J9HM=o1>{0Y z#epKNh+4zGuphL6{h=)!0PUbXsELAQ4}uPGFm!}Y&>0SaF3=UaL3ii@I`o7?;V|e0 zy`c{r4tdZQ`aypf00UtV42C0M2pkDRVHgaDqu^*50mr~d7zM||Xcz-yVH}Kyd?>nH5>=iVFt{ESuh)phZEpLh`<~$ zMC3A&8pV8(C`l76m8=z+OB4$U9wYeJ=}6{=V3v|O03slA5Vy%;WmOGARf3N_hnBrg|PnI0!sh+L^^H;P=9NnTCo zHE=Cl2iL<5&;+YOf`x7hXOWxX7Pu8whXh^0=XOQ5$Qq(M)Fyruxl{3>$Xz7whI>@< zZIQK#=S9|$ych0+`{4n25FUbu;SqQg9)ri>2{rX|B2N-M1y92>@N7u)=J!0q3+bf1 z$Vsdhd5Hlp!z=J=rgHN?})sciQW@=pK%|glWJSR&g3JJ zkEwkEpTcLVwuP3@MK+P#9J1Gg$rmDDQu``hldl=~jmXwa@>`PMiELxQ_nF!cBHPnZ zF#S*INPcGA4w0RV`vrc5-{5!n1O9|v@K;Fqw7Z$?Z}=y;_kypQG9WA68ANIyd)SAl z1>{0YXa%idU)T@Y!2Zw{4uE#hK1Ak-b`a}zfP90f>=?mw-Bs1(Ye`Gr>yRYDa^gh?gN=!8UBQA(dK2M`6TN^i)WG!Nf#2LEuhBz*ZhyL z_nav@>tP`*f(B?5ZE^DU>e zz=+p)wi&bXEnn|h8g-qd?O0B7OJUSCt^A^DJvTcm;tkL{Zq~a`C`x&5!qWTm#VC%t zDJSVQd=WI|4ZnVx?MG}UVuf9X6G=I?>u9+pw(my5>5B8id8(=%J87AEx$VSEH?H?D zpJLgLtE;r>mejdXBdG>{InjtlQ=SoRhNe@43JUUvP4TRl z?I$hIGSx=+Jd-|8cO_R}u3xBk+@yEcS629`8q=<~eYHD3D`!i1HmhIJmlqUO6^A{= zMNX{FaLlljw``{>6^yS8OI7Kk_G@j=TC-t$rLm~ecFaLhH@{aCS})|5i8+FYjd*ZY+JCa(lnArjp197WJAKLD$FmP zG_XMZ5C&GLV=p%?KkC_u;D|pjvlHo6T9m4t$5KYj@+GItQLmq~ct%@V?paaSGb6s; IXf<{H7i23y`Tzg` literal 0 HcmV?d00001 diff --git a/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb b/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb new file mode 100644 index 0000000000000000000000000000000000000000..23efbf3962ed1bb3ecf6c5a6e207157359a3e756 GIT binary patch literal 3280 zcmZA22Y3@@7{KxO!YC*%1Pcgwh+v@%m8Bp8ZPQ7MA_WvRTyjTxklcm4q)+YXDEU$ zik(He5^V$9LO0kBwuc>HN7xCvgPJHctU7#3xL2u{-yFy>+2mN6;&|v`V4g+Bi z7zBf1Pbh&QFceB*7z~FIFcS8HyNqFYE{7VFK(A6X5`u1e0M3ltDRE zKolyW3aX(7YC*m8;5|%*dT4-Ya3CC{P(P39Lk|1eIH>pj0CvvmmJ&{{T-U_#= zL? zN5S;p=Og)naX*S|X53HkGyDR-!f)_9`~iQ4gireyll=|<1ovL>RZ|8;@|{7X_OXRd zM4h1sxx6Lb%exuW%8y`HcO6hklQ4Sir&=nMUzKkNoN41nEX zAnXByU@&n1X(dEMU?`NrFc=OaU?l7Xdjt2MHi~F8jDfK*4)%rpU_4BK{b3>;0Fz)c zOo1{ehYE;7C2;>~)kHN=3+*p_Dp5T&z%)1z4g&5!?O>uA&LWuQ4HpS0dYux2^P!)?mzW&3f2sN!nSDh3(*3SDbdn}q(jn$jA#p~asRdd zBWyiqj?QLS1dE{smWZ~ru);Eu>V*IM_jyIa&bGAGCN0-7(pJniJ48yB{}c@Zzpc!2 zYRZjt+_TN3Rc876fSQEsWNpWCvKuNAu4$D;8}*_Nk(f73FK*NOS4MN5o3Zp#z8F;r zH|J!%mQRAFk}+4zu>F|r#H_H(aAG;fb{(y#+Vud|(`>8AD4 zy6KkfxVlQ4Zgt8{7+E#&(`iN`k@Jj1OUyG&JLj)@wmPLM*_>%LO|I>G$9WrDZ?8$Y zPLkB5N7w!JduU=Uf&)F$6W@X$&Q^&~MuNVGNZyH%6ZunL#+mf;B8=VD? zyU3}C&rngFY557y&IBjCzShp<7wKH1HlEBGNz0dxwT^oG9L3|>k#(Mxa6L2T+e@rf Geg6e*pFxoT literal 0 HcmV?d00001 diff --git a/plugin/geoip/testdata/README.md b/plugin/geoip/testdata/README.md new file mode 100644 index 000000000..2f6f884c9 --- /dev/null +++ b/plugin/geoip/testdata/README.md @@ -0,0 +1,112 @@ +# testdata +This directory contains mmdb database files used during the testing of this plugin. + +# Create mmdb database files +If you need to change them to add a new value, or field the best is to recreate them, the code snipped used to create them initially is provided next. + +```golang +package main + +import ( + "log" + "net" + "os" + + "github.com/maxmind/mmdbwriter" + "github.com/maxmind/mmdbwriter/inserter" + "github.com/maxmind/mmdbwriter/mmdbtype" +) + +const cdir = "81.2.69.142/32" + +// Create new mmdb database fixtures in this directory. +func main() { + createCityDB("GeoLite2-City.mmdb", "DBIP-City-Lite") + // Create unkwnon database type. + createCityDB("GeoLite2-UnknownDbType.mmdb", "UnknownDbType") +} + +func createCityDB(dbName, dbType string) { + // Load a database writer. + writer, err := mmdbwriter.New(mmdbwriter.Options{DatabaseType: dbType}) + if err != nil { + log.Fatal(err) + } + + // Define and insert the new data. + _, ip, err := net.ParseCIDR(cdir) + if err != nil { + log.Fatal(err) + } + + // TODO(snebel29): Find an alternative location in Europe Union. + record := mmdbtype.Map{ + "city": mmdbtype.Map{ + "geoname_id": mmdbtype.Uint64(2653941), + "names": mmdbtype.Map{ + "en": mmdbtype.String("Cambridge"), + "es": mmdbtype.String("Cambridge"), + }, + }, + "continent": mmdbtype.Map{ + "code": mmdbtype.String("EU"), + "geoname_id": mmdbtype.Uint64(6255148), + "names": mmdbtype.Map{ + "en": mmdbtype.String("Europe"), + "es": mmdbtype.String("Europa"), + }, + }, + "country": mmdbtype.Map{ + "iso_code": mmdbtype.String("GB"), + "geoname_id": mmdbtype.Uint64(2635167), + "names": mmdbtype.Map{ + "en": mmdbtype.String("United Kingdom"), + "es": mmdbtype.String("Reino Unido"), + }, + "is_in_european_union": mmdbtype.Bool(true), + }, + "location": mmdbtype.Map{ + "accuracy_radius": mmdbtype.Uint16(200), + "latitude": mmdbtype.Float64(52.2242), + "longitude": mmdbtype.Float64(0.1315), + "metro_code": mmdbtype.Uint64(0), + "time_zone": mmdbtype.String("Europe/London"), + }, + "postal": mmdbtype.Map{ + "code": mmdbtype.String("CB4"), + }, + "registered_country": mmdbtype.Map{ + "iso_code": mmdbtype.String("GB"), + "geoname_id": mmdbtype.Uint64(2635167), + "names": mmdbtype.Map{"en": mmdbtype.String("United Kingdom")}, + "is_in_european_union": mmdbtype.Bool(false), + }, + "subdivisions": mmdbtype.Slice{ + mmdbtype.Map{ + "iso_code": mmdbtype.String("ENG"), + "geoname_id": mmdbtype.Uint64(6269131), + "names": mmdbtype.Map{"en": mmdbtype.String("England")}, + }, + mmdbtype.Map{ + "iso_code": mmdbtype.String("CAM"), + "geoname_id": mmdbtype.Uint64(2653940), + "names": mmdbtype.Map{"en": mmdbtype.String("Cambridgeshire")}, + }, + }, + } + + if err := writer.InsertFunc(ip, inserter.TopLevelMergeWith(record)); err != nil { + log.Fatal(err) + } + + // Write the DB to the filesystem. + fh, err := os.Create(dbName) + if err != nil { + log.Fatal(err) + } + _, err = writer.WriteTo(fh) + if err != nil { + log.Fatal(err) + } +} +``` diff --git a/plugin/metadata/metadata_test.go b/plugin/metadata/metadata_test.go index be20f8770..3dc507de0 100644 --- a/plugin/metadata/metadata_test.go +++ b/plugin/metadata/metadata_test.go @@ -72,12 +72,12 @@ func TestLabelFormat(t *testing.T) { {"plugin/LABEL", true}, {"p/LABEL", true}, {"plugin/L", true}, + {"PLUGIN/LABEL/SUB-LABEL", true}, // fails {"LABEL", false}, {"plugin.LABEL", false}, {"/NO-PLUGIN-NOT-ACCEPTED", false}, {"ONLY-PLUGIN-NOT-ACCEPTED/", false}, - {"PLUGIN/LABEL/SUB-LABEL", false}, {"/", false}, {"//", false}, } diff --git a/plugin/metadata/provider.go b/plugin/metadata/provider.go index 309d304b7..06417cc6f 100644 --- a/plugin/metadata/provider.go +++ b/plugin/metadata/provider.go @@ -56,17 +56,13 @@ type Provider interface { // Func is the type of function in the metadata, when called they return the value of the label. type Func func() string -// IsLabel checks that the provided name is a valid label name, i.e. two words separated by a slash. +// IsLabel checks that the provided name is a valid label name, i.e. two or more words separated by a slash. func IsLabel(label string) bool { p := strings.Index(label, "/") if p <= 0 || p >= len(label)-1 { // cannot accept namespace empty nor label empty return false } - if strings.LastIndex(label, "/") != p { - // several slash in the Label - return false - } return true }