tools/geoipdatabases/geoip2.go (128 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. package main import ( "encoding/json" "fmt" "net/netip" "os" "path/filepath" "strings" "github.com/maxmind/mmdbwriter" "github.com/maxmind/mmdbwriter/mmdbtype" "go4.org/netipx" ) // WriteGeoIP2TestDB writes GeoIP2 test mmdb files. func (w *writer) WriteGeoIP2TestDB() error { dbTypes := []string{ "GeoLite2-ASN", "GeoLite2-City", "GeoLite2-Country", } for _, dbType := range dbTypes { languages := []string{"en"} description := map[string]string{ "en": strings.ReplaceAll(dbType, "-", " ") + " Test Database (fake GeoIP2 data, for example purposes only)", } if dbType == "GeoIP2-City" { languages = append(languages, "zh") description["zh"] = "小型数据库" } dbWriter, err := mmdbwriter.New( mmdbwriter.Options{ DatabaseType: dbType, Description: description, DisableIPv4Aliasing: false, IPVersion: 6, Languages: languages, RecordSize: 28, IncludeReservedNetworks: true, }, ) if err != nil { return fmt.Errorf("creating mmdbwriter: %w", err) } jsonFileName := dbType + "-Test.json" if err := w.insertJSON(dbWriter, jsonFileName); err != nil { return fmt.Errorf("inserting json: %w", err) } dbFileName := dbType + ".mmdb" if err := w.write(dbWriter, dbFileName); err != nil { return fmt.Errorf("writing database: %w", err) } } return nil } // insertJSON reads and parses a json file into mmdbtypes values and inserts // them into the mmdbwriter tree. func (w *writer) insertJSON(dbWriter *mmdbwriter.Tree, fileName string) error { file, err := os.Open(filepath.Clean(filepath.Join(w.source, fileName))) if err != nil { return fmt.Errorf("opening json file: %w", err) } defer file.Close() var data []map[string]any if err := json.NewDecoder(file).Decode(&data); err != nil { return fmt.Errorf("decoding json file: %w", err) } for _, record := range data { for k, v := range record { prefix, err := netip.ParsePrefix(k) if err != nil { return fmt.Errorf("parsing ip: %w", err) } mmdbValue, err := toMMDBType(prefix.String(), v) if err != nil { return fmt.Errorf("converting value to mmdbtype: %w", err) } err = dbWriter.Insert( netipx.PrefixIPNet(prefix), mmdbValue, ) if err != nil { return fmt.Errorf("inserting ip: %w", err) } } } return nil } // toMMDBType key converts field values read from json into their corresponding mmdbtype.DataType. // It makes some assumptions for numeric types based on previous knowledge about field types. func toMMDBType(key string, value any) (mmdbtype.DataType, error) { switch v := value.(type) { case bool: return mmdbtype.Bool(v), nil case string: return mmdbtype.String(v), nil case map[string]any: m := mmdbtype.Map{} for innerKey, val := range v { innerVal, err := toMMDBType(innerKey, val) if err != nil { return nil, fmt.Errorf("parsing mmdbtype.Map for key %q: %w", key, err) } m[mmdbtype.String(innerKey)] = innerVal } return m, nil case []any: s := mmdbtype.Slice{} for _, val := range v { innerVal, err := toMMDBType(key, val) if err != nil { return nil, fmt.Errorf("parsing mmdbtype.Slice for key %q: %w", key, err) } s = append(s, innerVal) } return s, nil case float64: switch key { case "accuracy_radius", "confidence", "metro_code": return mmdbtype.Uint16(v), nil case "autonomous_system_number", "average_income", "geoname_id", "ipv4_24", "ipv4_32", "ipv6_32", "ipv6_48", "ipv6_64", "population_density": return mmdbtype.Uint32(v), nil case "ip_risk", "latitude", "longitude", "score", "static_ip_score": return mmdbtype.Float64(v), nil default: return nil, fmt.Errorf("unsupported numeric type for key %q: %T", key, value) } default: return nil, fmt.Errorf("unsupported type for key %q: %T", key, value) } }