diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10f5f7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +testdata/GeoIP2-City.mmdb +testdata/GeoIP2-Connection-Type.mmdb +testdata/GeoIP2-Country.mmdb +testdata/GeoIP2-ISP.mmdb diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c0dfe01 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Aleksey Lin (https://incsw.in) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..20bb671 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Go Report Card](https://goreportcard.com/badge/github.com/IncSW/geoip2?style=flat-square)](https://goreportcard.com/report/github.com/IncSW/geoip2) + +# GeoIP2 Reader for Go + +This library reads MaxMind GeoIP2 databases. + +## Installation + +`go get github.com/IncSW/geoip2` + +## Quick Start + +```go +import "github.com/IncSW/geoip2" + +reader, err := geoip2.NewReaderFromFile("path/to/GeoIP2-City.mmdb") +if err != nil { + panic(err) +} +record, err := reader.LookupCity(net.ParseIP("1.1.1.1")) +if err != nil { + panic(err) +} +data, err := json.Marshal(record) +if err != nil { + panic(err) +} +println(string(data)) +// { +// "Continent": { +// "GeoNameID": 6255151, +// "Code": "OC", +// "Names": { +// "de": "Ozeanien", +// "en": "Oceania", +// "es": "Oceanía", +// "fr": "Océanie", +// "ja": "オセアニア", +// "pt-BR": "Oceania", +// "ru": "Океания", +// "zh-CN": "大洋洲" +// } +// }, +// "City": { +// "GeoNameID": 0, +// "Names": null +// }, +// "Country": { +// "GeoNameID": 2077456, +// "ISOCode": "AU", +// "IsInEuropeanEnion": false, +// "Names": { +// "de": "Australien", +// "en": "Australia", +// "es": "Australia", +// "fr": "Australie", +// "ja": "オーストラリア", +// "pt-BR": "Austrália", +// "ru": "Австралия", +// "zh-CN": "澳大利亚" +// }, +// "Type": "" +// }, +// "Subdivisions": null, +// "Location": { +// "AccuracyRadius": 1000, +// "MetroCode": 0, +// "Latitude": -33.494, +// "Longitude": 143.2104, +// "TimeZone": "Australia/Sydney" +// }, +// "Postal": { +// "Code": "" +// }, +// "RegisteredCountry": { +// "GeoNameID": 2077456, +// "ISOCode": "AU", +// "IsInEuropeanEnion": false, +// "Names": { +// "de": "Australien", +// "en": "Australia", +// "es": "Australia", +// "fr": "Australie", +// "ja": "オーストラリア", +// "pt-BR": "Austrália", +// "ru": "Австралия", +// "zh-CN": "澳大利亚" +// }, +// "Type": "" +// }, +// "RepresentedCountry": { +// "GeoNameID": 0, +// "ISOCode": "", +// "IsInEuropeanEnion": false, +// "Names": null, +// "Type": "" +// }, +// "Traits": { +// "IsAnonymousProxy": false, +// "IsSatelliteProvider": false, +// "StaticIPScore": 0 +// } +// } +``` + +## Performance + +TODO + +## License + +[MIT License](LICENSE). diff --git a/city.go b/city.go new file mode 100644 index 0000000..cd4f385 --- /dev/null +++ b/city.go @@ -0,0 +1,130 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +func readCityResponse(buffer []byte, offset uint) (*CityResponse, error) { + dataType, citySize, offset, err := readControl(buffer, offset) + if err != nil { + return nil, err + } + if dataType != dataTypeMap { + return nil, errors.New("invalid city type: " + strconv.Itoa(int(dataType))) + } + var key []byte + response := &CityResponse{} + for i := uint(0); i < citySize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return nil, err + } + switch b2s(key) { + case "city": + offset, err = readCity(&response.City, buffer, offset) + if err != nil { + return nil, err + } + case "continent": + offset, err = readContinent(&response.Continent, buffer, offset) + if err != nil { + return nil, err + } + case "country": + offset, err = readCountry(&response.Country, buffer, offset) + if err != nil { + return nil, err + } + case "location": + offset, err = readLocation(&response.Location, buffer, offset) + if err != nil { + return nil, err + } + case "postal": + offset, err = readPostal(&response.Postal, buffer, offset) + if err != nil { + return nil, err + } + case "registered_country": + offset, err = readCountry(&response.RegisteredCountry, buffer, offset) + if err != nil { + return nil, err + } + case "represented_country": + offset, err = readCountry(&response.RepresentedCountry, buffer, offset) + if err != nil { + return nil, err + } + case "subdivisions": + response.Subdivisions, offset, err = readSubdivisions(buffer, offset) + if err != nil { + return nil, err + } + case "traits": + offset, err = readTraits(&response.Traits, buffer, offset) + if err != nil { + return nil, err + } + default: + return nil, errors.New("unknown city response key: " + string(key) + ", type: " + strconv.Itoa(int(dataType))) + } + } + return response, nil +} + +func readCity(city *City, buffer []byte, offset uint) (uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, err + } + switch dataType { + case dataTypeMap: + return readCityMap(city, buffer, size, offset) + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, err + } + if dataType != dataTypeMap { + return 0, errors.New("invalid city pointer type: " + strconv.Itoa(int(dataType))) + } + _, err = readCityMap(city, buffer, size, offset) + if err != nil { + return 0, err + } + return newOffset, nil + default: + return 0, errors.New("invalid city type: " + strconv.Itoa(int(dataType))) + } +} + +func readCityMap(city *City, buffer []byte, mapSize uint, offset uint) (uint, error) { + var key []byte + var err error + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return 0, err + } + switch b2s(key) { + case "geoname_id": + city.GeoNameID, offset, err = readUInt32(buffer, offset) + if err != nil { + return 0, err + } + case "names": + city.Names, offset, err = readStringMap(buffer, offset) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unknown city key: " + string(key)) + } + } + return offset, nil +} diff --git a/common.go b/common.go new file mode 100644 index 0000000..73eae64 --- /dev/null +++ b/common.go @@ -0,0 +1,362 @@ +package geoip2 + +import ( + "encoding/binary" + "errors" + "math" + "strconv" + "unsafe" +) + +func readControl(buffer []byte, offset uint) (byte, uint, uint, error) { + controlByte := buffer[offset] + offset++ + dataType := controlByte >> 5 + if dataType == dataTypeExtended { + dataType = buffer[offset] + 7 + offset++ + } + size := uint(controlByte & 0x1f) + if dataType == dataTypeExtended || size < 29 { + return dataType, size, offset, nil + } + bytesToRead := size - 28 + newOffset := offset + bytesToRead + if newOffset > uint(len(buffer)) { + return 0, 0, 0, errors.New("invalid offset") + } + size = uint(bytesToUInt64(buffer[offset:newOffset])) + switch bytesToRead { + case 1: + size += 29 + case 2: + size += 285 + default: + size += 65821 + } + return dataType, size, newOffset, nil +} + +func readPointer(buffer []byte, size uint, offset uint) (uint, uint, error) { + pointerSize := ((size >> 3) & 0x3) + 1 + newOffset := offset + pointerSize + if newOffset > uint(len(buffer)) { + return 0, 0, errors.New("invalid offset") + } + prefix := uint64(0) + if pointerSize != 4 { + prefix = uint64(size) & 0x7 + } + unpacked := uint(bytesToUInt64WithPrefix(prefix, buffer[offset:newOffset])) + pointerValueOffset := uint(0) + switch pointerSize { + case 2: + pointerValueOffset = 2048 + case 3: + pointerValueOffset = 526336 + case 4: + pointerValueOffset = 0 + } + return unpacked + pointerValueOffset, newOffset, nil +} + +func readFloat64(buffer []byte, offset uint) (float64, uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, 0, err + } + switch dataType { + case dataTypeFloat64: + newOffset := offset + size + return bytesToFloat64(buffer[offset:newOffset]), newOffset, nil + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, 0, err + } + if dataType != dataTypeFloat64 { + return 0, 0, errors.New("invalid float64 pointer type: " + strconv.Itoa(int(dataType))) + } + return bytesToFloat64(buffer[offset : offset+size]), newOffset, nil + default: + return 0, 0, errors.New("invalid float64 type: " + strconv.Itoa(int(dataType))) + } +} + +func readUInt16(buffer []byte, offset uint) (uint16, uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, 0, err + } + switch dataType { + case dataTypeUint16: + newOffset := offset + size + return uint16(bytesToUInt64(buffer[offset:newOffset])), newOffset, nil + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, 0, err + } + if dataType != dataTypeUint16 { + return 0, 0, errors.New("invalid uint16 pointer type: " + strconv.Itoa(int(dataType))) + } + return uint16(bytesToUInt64(buffer[offset : offset+size])), newOffset, nil + default: + return 0, 0, errors.New("invalid uint16 type: " + strconv.Itoa(int(dataType))) + } +} + +func readUInt32(buffer []byte, offset uint) (uint32, uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, 0, err + } + switch dataType { + case dataTypeUint32: + newOffset := offset + size + return uint32(bytesToUInt64(buffer[offset:newOffset])), newOffset, nil + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, 0, err + } + if dataType != dataTypeUint32 { + return 0, 0, errors.New("invalid uint32 pointer type: " + strconv.Itoa(int(dataType))) + } + return uint32(bytesToUInt64(buffer[offset : offset+size])), newOffset, nil + default: + return 0, 0, errors.New("invalid uint32 type: " + strconv.Itoa(int(dataType))) + } +} + +func readBool(buffer []byte, offset uint) (bool, uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return false, 0, err + } + if dataType != dataTypeBool { + return false, 0, errors.New("invalid bool type: " + strconv.Itoa(int(dataType))) + } + return size != 0, offset, nil +} + +func readString(buffer []byte, offset uint) (string, uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return "", 0, err + } + switch dataType { + case dataTypeString: + newOffset := offset + size + return b2s(buffer[offset:newOffset]), newOffset, nil + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return "", 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return "", 0, err + } + if dataType != dataTypeString { + return "", 0, errors.New("invalid string pointer type: " + strconv.Itoa(int(dataType))) + } + return b2s(buffer[offset : offset+size]), newOffset, nil + default: + return "", 0, errors.New("invalid string type: " + strconv.Itoa(int(dataType))) + } +} + +func readStringMap(buffer []byte, offset uint) (map[string]string, uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return nil, 0, err + } + switch dataType { + case dataTypeMap: + return readStringMapMap(buffer, size, offset) + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return nil, 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return nil, 0, err + } + if dataType != dataTypeMap { + return nil, 0, errors.New("invalid stringMap pointer type: " + strconv.Itoa(int(dataType))) + } + value, _, err := readStringMapMap(buffer, size, offset) + if err != nil { + return nil, 0, err + } + return value, newOffset, nil + default: + return nil, 0, errors.New("invalid stringMap type: " + strconv.Itoa(int(dataType))) + } +} + +func readStringMapMap(buffer []byte, mapSize uint, offset uint) (map[string]string, uint, error) { + var key []byte + var err error + var dataType byte + var size uint + result := map[string]string{} + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return nil, 0, err + } + dataType, size, offset, err = readControl(buffer, offset) + if err != nil { + return nil, 0, err + } + switch dataType { + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return nil, 0, err + } + dataType, size, valueOffset, err := readControl(buffer, pointer) + if err != nil { + return nil, 0, err + } + if dataType != dataTypeString { + return nil, 0, errors.New("map key must be a string, got: " + strconv.Itoa(int(dataType))) + } + offset = newOffset + result[b2s(key)] = b2s(buffer[valueOffset : valueOffset+size]) + case dataTypeString: + newOffset := offset + size + value := b2s(buffer[offset:newOffset]) + offset = newOffset + result[b2s(key)] = value + default: + return nil, 0, errors.New("invalid data type of key " + string(key) + ": " + strconv.Itoa(int(dataType))) + } + } + return result, offset, nil +} + +func readMapKey(buffer []byte, offset uint) ([]byte, uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return nil, 0, err + } + if dataType == dataTypePointer { + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return nil, 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return nil, 0, err + } + if dataType != dataTypeString { + return nil, 0, errors.New("map key must be a string, got: " + strconv.Itoa(int(dataType))) + } + return buffer[offset : offset+size], newOffset, nil + } + if dataType != dataTypeString { + return nil, 0, errors.New("map key must be a string, got: " + strconv.Itoa(int(dataType))) + } + newOffset := offset + size + if newOffset > uint(len(buffer)) { + return nil, 0, errors.New("invalid offset") + } + return buffer[offset:newOffset], newOffset, nil +} + +func readStringSlice(buffer []byte, sliceSize uint, offset uint) ([]string, uint, error) { + var err error + var dataType byte + var size uint + result := make([]string, sliceSize) + for i := uint(0); i < sliceSize; i++ { + dataType, size, offset, err = readControl(buffer, offset) + if err != nil { + return nil, 0, err + } + if dataType != dataTypeString { + return nil, 0, errors.New("invalid data type: " + strconv.Itoa(int(dataType))) + } + newOffset := offset + size + value := b2s(buffer[offset:newOffset]) + offset = newOffset + result[i] = value + } + return result, offset, nil +} + +func bytesToUInt64(buffer []byte) uint64 { + switch len(buffer) { + case 1: + return uint64(buffer[0]) + case 2: + return uint64(buffer[0])<<8 | uint64(buffer[1]) + case 3: + return (uint64(buffer[0])<<8|uint64(buffer[1]))<<8 | uint64(buffer[2]) + case 4: + return ((uint64(buffer[0])<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8 | uint64(buffer[3]) + case 5: + return (((uint64(buffer[0])<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8|uint64(buffer[3]))<<8 | uint64(buffer[4]) + case 6: + return ((((uint64(buffer[0])<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8|uint64(buffer[3]))<<8|uint64(buffer[4]))<<8 | uint64(buffer[5]) + case 7: + return (((((uint64(buffer[0])<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8|uint64(buffer[3]))<<8|uint64(buffer[4]))<<8|uint64(buffer[5]))<<8 | uint64(buffer[6]) + case 8: + return ((((((uint64(buffer[0])<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8|uint64(buffer[3]))<<8|uint64(buffer[4]))<<8|uint64(buffer[5]))<<8|uint64(buffer[6]))<<8 | uint64(buffer[7]) + } + return 0 +} + +func bytesToUInt64WithPrefix(prefix uint64, buffer []byte) uint64 { + switch len(buffer) { + case 0: + return prefix + case 1: + return prefix<<8 | uint64(buffer[0]) + case 2: + return (prefix<<8|uint64(buffer[0]))<<8 | uint64(buffer[1]) + case 3: + return ((prefix<<8|uint64(buffer[0]))<<8|uint64(buffer[1]))<<8 | uint64(buffer[2]) + case 4: + return (((prefix<<8|uint64(buffer[0]))<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8 | uint64(buffer[3]) + case 5: + return ((((prefix<<8|uint64(buffer[0]))<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8|uint64(buffer[3]))<<8 | uint64(buffer[4]) + case 6: + return (((((prefix<<8|uint64(buffer[0]))<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8|uint64(buffer[3]))<<8|uint64(buffer[4]))<<8 | uint64(buffer[5]) + case 7: + return ((((((prefix<<8|uint64(buffer[0]))<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8|uint64(buffer[3]))<<8|uint64(buffer[4]))<<8|uint64(buffer[5]))<<8 | uint64(buffer[6]) + case 8: + return (((((((prefix<<8|uint64(buffer[0]))<<8|uint64(buffer[1]))<<8|uint64(buffer[2]))<<8|uint64(buffer[3]))<<8|uint64(buffer[4]))<<8|uint64(buffer[5]))<<8|uint64(buffer[6]))<<8 | uint64(buffer[7]) + } + return 0 +} + +func bytesToFloat32(buffer []byte) float32 { + bits := binary.BigEndian.Uint32(buffer) + return math.Float32frombits(bits) +} + +func bytesToFloat64(buffer []byte) float64 { + bits := binary.BigEndian.Uint64(buffer) + return math.Float64frombits(bits) +} + +func b2s(value []byte) string { + return *(*string)(unsafe.Pointer(&value)) +} diff --git a/connection_type.go b/connection_type.go new file mode 100644 index 0000000..8a931f8 --- /dev/null +++ b/connection_type.go @@ -0,0 +1,61 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +func readConnectionType(buffer []byte, offset uint) (string, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return "", err + } + connectionType := &ConnectionType{} + switch dataType { + case dataTypeMap: + _, err = readConnectionTypeMap(connectionType, buffer, size, offset) + if err != nil { + return "", err + } + case dataTypePointer: + pointer, _, err := readPointer(buffer, size, offset) + if err != nil { + return "", err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return "", err + } + if dataType != dataTypeMap { + return "", errors.New("invalid connectionType pointer type: " + strconv.Itoa(int(dataType))) + } + _, err = readConnectionTypeMap(connectionType, buffer, size, offset) + if err != nil { + return "", err + } + default: + return "", errors.New("invalid connectionType type: " + strconv.Itoa(int(dataType))) + } + return connectionType.ConnectionType, nil +} + +func readConnectionTypeMap(connectionType *ConnectionType, buffer []byte, mapSize uint, offset uint) (uint, error) { + var key []byte + var err error + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return 0, err + } + switch b2s(key) { + case "connection_type": + connectionType.ConnectionType, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unknown connectionType key: " + string(key)) + } + } + return offset, nil +} diff --git a/continent.go b/continent.go new file mode 100644 index 0000000..024a02d --- /dev/null +++ b/continent.go @@ -0,0 +1,67 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +func readContinent(continent *Continent, buffer []byte, offset uint) (uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, err + } + switch dataType { + case dataTypeMap: + return readContinentMap(continent, buffer, size, offset) + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, err + } + if dataType != dataTypeMap { + return 0, errors.New("invalid continent pointer type: " + strconv.Itoa(int(dataType))) + } + _, err = readContinentMap(continent, buffer, size, offset) + if err != nil { + return 0, err + } + return newOffset, nil + default: + return 0, errors.New("invalid continent type: " + strconv.Itoa(int(dataType))) + } +} + +func readContinentMap(continent *Continent, buffer []byte, mapSize uint, offset uint) (uint, error) { + var key []byte + var err error + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return 0, err + } + switch b2s(key) { + case "geoname_id": + continent.GeoNameID, offset, err = readUInt32(buffer, offset) + if err != nil { + return 0, err + } + case "code": + continent.Code, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + case "names": + continent.Names, offset, err = readStringMap(buffer, offset) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unknown continent key: " + string(key)) + } + } + return offset, nil +} diff --git a/country.go b/country.go new file mode 100644 index 0000000..617340c --- /dev/null +++ b/country.go @@ -0,0 +1,77 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +func readCountry(country *Country, buffer []byte, offset uint) (uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, err + } + switch dataType { + case dataTypeMap: + return readCountryMap(country, buffer, size, offset) + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, err + } + if dataType != dataTypeMap { + return 0, errors.New("invalid country pointer type: " + strconv.Itoa(int(dataType))) + } + _, err = readCountryMap(country, buffer, size, offset) + if err != nil { + return 0, err + } + return newOffset, nil + default: + return 0, errors.New("invalid country type: " + strconv.Itoa(int(dataType))) + } +} + +func readCountryMap(country *Country, buffer []byte, mapSize uint, offset uint) (uint, error) { + var key []byte + var err error + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return 0, err + } + switch b2s(key) { + case "geoname_id": + country.GeoNameID, offset, err = readUInt32(buffer, offset) + if err != nil { + return 0, err + } + case "iso_code": + country.ISOCode, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + case "names": + country.Names, offset, err = readStringMap(buffer, offset) + if err != nil { + return 0, err + } + case "is_in_european_union": + country.IsInEuropeanEnion, offset, err = readBool(buffer, offset) + if err != nil { + return 0, err + } + case "type": + country.Type, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unknown country key: " + string(key)) + } + } + return offset, nil +} diff --git a/geoip2_test.go b/geoip2_test.go new file mode 100644 index 0000000..af1ffc8 --- /dev/null +++ b/geoip2_test.go @@ -0,0 +1,25 @@ +package geoip2 + +import ( + "encoding/json" + "net" + "testing" +) + +func TestDebug(t *testing.T) { + reader, err := NewReaderFromFile("testdata/GeoIP2-City.mmdb") + if err != nil { + t.Fatal(err) + } + + record, err := reader.LookupCity(net.ParseIP("1.1.1.1")) + if err != nil { + t.Fatal(err) + } + + data, err := json.Marshal(record) + if err != nil { + t.Fatal(err) + } + t.Log(string(data)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a2a0340 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/IncSW/geoip2 + +go 1.14 diff --git a/isp.go b/isp.go new file mode 100644 index 0000000..3106cdc --- /dev/null +++ b/isp.go @@ -0,0 +1,76 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +func readISP(buffer []byte, offset uint) (*ISP, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return nil, err + } + isp := &ISP{} + switch dataType { + case dataTypeMap: + _, err = readISPMap(isp, buffer, size, offset) + if err != nil { + return nil, err + } + case dataTypePointer: + pointer, _, err := readPointer(buffer, size, offset) + if err != nil { + return nil, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return nil, err + } + if dataType != dataTypeMap { + return nil, errors.New("invalid isp pointer type: " + strconv.Itoa(int(dataType))) + } + _, err = readISPMap(isp, buffer, size, offset) + if err != nil { + return nil, err + } + default: + return nil, errors.New("invalid isp type: " + strconv.Itoa(int(dataType))) + } + return isp, nil +} + +func readISPMap(isp *ISP, buffer []byte, mapSize uint, offset uint) (uint, error) { + var key []byte + var err error + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return 0, err + } + switch b2s(key) { + case "autonomous_system_number": + isp.AutonomousSystemNumber, offset, err = readUInt32(buffer, offset) + if err != nil { + return 0, err + } + case "autonomous_system_organization": + isp.AutonomousSystemOrganization, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + case "isp": + isp.ISP, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + case "organization": + isp.Organization, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unknown isp key: " + string(key)) + } + } + return offset, nil +} diff --git a/location.go b/location.go new file mode 100644 index 0000000..8fed055 --- /dev/null +++ b/location.go @@ -0,0 +1,77 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +func readLocation(location *Location, buffer []byte, offset uint) (uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, err + } + switch dataType { + case dataTypeMap: + return readLocationMap(location, buffer, size, offset) + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, err + } + if dataType != dataTypeMap { + return 0, errors.New("invalid location pointer type: " + strconv.Itoa(int(dataType))) + } + _, err = readLocationMap(location, buffer, size, offset) + if err != nil { + return 0, err + } + return newOffset, nil + default: + return 0, errors.New("invalid location type: " + strconv.Itoa(int(dataType))) + } +} + +func readLocationMap(location *Location, buffer []byte, mapSize uint, offset uint) (uint, error) { + var key []byte + var err error + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return 0, err + } + switch b2s(key) { + case "latitude": + location.Latitude, offset, err = readFloat64(buffer, offset) + if err != nil { + return 0, err + } + case "longitude": + location.Longitude, offset, err = readFloat64(buffer, offset) + if err != nil { + return 0, err + } + case "accuracy_radius": + location.AccuracyRadius, offset, err = readUInt16(buffer, offset) + if err != nil { + return 0, err + } + case "time_zone": + location.TimeZone, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + case "metro_code": + location.MetroCode, offset, err = readUInt16(buffer, offset) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unknown location key: " + string(key)) + } + } + return offset, nil +} diff --git a/metadata.go b/metadata.go new file mode 100644 index 0000000..3b22210 --- /dev/null +++ b/metadata.go @@ -0,0 +1,108 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +type Metadata struct { + NodeCount uint32 // node_count This is an unsigned 32-bit integer indicating the number of nodes in the search tree. + RecordSize uint16 // record_size This is an unsigned 16-bit integer. It indicates the number of bits in a record in the search tree. Note that each node consists of two records. + IPVersion uint16 // ip_version This is an unsigned 16-bit integer which is always 4 or 6. It indicates whether the database contains IPv4 or IPv6 address data. + DatabaseType string // database_type This is a string that indicates the structure of each data record associated with an IP address. The actual definition of these structures is left up to the database creator. Names starting with “GeoIP” are reserved for use by MaxMind (and “GeoIP” is a trademark anyway). + Languages []string // languages An array of strings, each of which is a locale code. A given record may contain data items that have been localized to some or all of these locales. Records should not contain localized data for locales not included in this array. This is an optional key, as this may not be relevant for all types of data. + BinaryFormatMajorVersion uint16 // binary_format_major_version This is an unsigned 16-bit integer indicating the major version number for the database’s binary format. + BinaryFormatMinorVersion uint16 // binary_format_minor_version This is an unsigned 16-bit integer indicating the minor version number for the database’s binary format. + BuildEpoch uint64 // build_epoch This is an unsigned 64-bit integer that contains the database build timestamp as a Unix epoch value. + Description map[string]string // description This key will always point to a map. The keys of that map will be language codes, and the values will be a description in that language as a UTF-8 string. The codes may include additional information such as script or country identifiers, like “zh-TW” or “mn-Cyrl-MN”. The additional identifiers will be separated by a dash character (“-“). +} + +var metadataStartMarker = []byte("\xAB\xCD\xEFMaxMind.com") + +func readMetadata(buffer []byte) (*Metadata, error) { + dataType, metadataSize, offset, err := readControl(buffer, 0) + if err != nil { + return nil, err + } + if dataType != dataTypeMap { + return nil, errors.New("invalid metadata type: " + strconv.Itoa(int(dataType))) + } + var key []byte + metadata := &Metadata{} + for i := uint(0); i < metadataSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return nil, err + } + size := uint(0) + dataType, size, offset, err = readControl(buffer, offset) + if err != nil { + return nil, err + } + newOffset := uint(0) + switch b2s(key) { + case "binary_format_major_version": + if dataType != dataTypeUint16 { + return nil, errors.New("invalid binary_format_major_version type: " + strconv.Itoa(int(dataType))) + } + newOffset = offset + size + metadata.BinaryFormatMajorVersion = uint16(bytesToUInt64(buffer[offset:newOffset])) + case "binary_format_minor_version": + if dataType != dataTypeUint16 { + return nil, errors.New("invalid binary_format_minor_version type: " + strconv.Itoa(int(dataType))) + } + newOffset = offset + size + metadata.BinaryFormatMinorVersion = uint16(bytesToUInt64(buffer[offset:newOffset])) + case "build_epoch": + if dataType != dataTypeUint64 { + return nil, errors.New("invalid build_epoch type: " + strconv.Itoa(int(dataType))) + } + newOffset = offset + size + metadata.BuildEpoch = bytesToUInt64(buffer[offset:newOffset]) + case "database_type": + if dataType != dataTypeString { + return nil, errors.New("invalid database_type type: " + strconv.Itoa(int(dataType))) + } + newOffset = offset + size + metadata.DatabaseType = b2s(buffer[offset:newOffset]) + case "description": + if dataType != dataTypeMap { + return nil, errors.New("invalid description type: " + strconv.Itoa(int(dataType))) + } + metadata.Description, newOffset, err = readStringMapMap(buffer, size, offset) + if err != nil { + return nil, err + } + case "ip_version": + if dataType != dataTypeUint16 { + return nil, errors.New("invalid ip_version type: " + strconv.Itoa(int(dataType))) + } + newOffset = offset + size + metadata.IPVersion = uint16(bytesToUInt64(buffer[offset:newOffset])) + case "languages": + if dataType != dataTypeSlice { + return nil, errors.New("invalid languages type: " + strconv.Itoa(int(dataType))) + } + metadata.Languages, newOffset, err = readStringSlice(buffer, size, offset) + if err != nil { + return nil, err + } + case "node_count": + if dataType != dataTypeUint32 { + return nil, errors.New("invalid node_count type: " + strconv.Itoa(int(dataType))) + } + newOffset = offset + size + metadata.NodeCount = uint32(bytesToUInt64(buffer[offset:newOffset])) + case "record_size": + if dataType != dataTypeUint16 { + return nil, errors.New("invalid record_size type: " + strconv.Itoa(int(dataType))) + } + newOffset = offset + size + metadata.RecordSize = uint16(bytesToUInt64(buffer[offset:newOffset])) + default: + return nil, errors.New("unknown key: " + string(key) + ", type: " + strconv.Itoa(int(dataType))) + } + offset = newOffset + } + return metadata, nil +} diff --git a/postal.go b/postal.go new file mode 100644 index 0000000..6d949d6 --- /dev/null +++ b/postal.go @@ -0,0 +1,57 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +func readPostal(postal *Postal, buffer []byte, offset uint) (uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, err + } + switch dataType { + case dataTypeMap: + return readPostalMap(postal, buffer, size, offset) + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, err + } + if dataType != dataTypeMap { + return 0, errors.New("invalid postal pointer type: " + strconv.Itoa(int(dataType))) + } + _, err = readPostalMap(postal, buffer, size, offset) + if err != nil { + return 0, err + } + return newOffset, nil + default: + return 0, errors.New("invalid postal type: " + strconv.Itoa(int(dataType))) + } +} + +func readPostalMap(postal *Postal, buffer []byte, mapSize uint, offset uint) (uint, error) { + var key []byte + var err error + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return 0, err + } + switch b2s(key) { + case "code": + postal.Code, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unknown postal key: " + string(key)) + } + } + return offset, nil +} diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..ebcabb5 --- /dev/null +++ b/reader.go @@ -0,0 +1,169 @@ +package geoip2 + +import ( + "bytes" + "errors" + "io/ioutil" + "net" + "strconv" +) + +type Reader struct { + Metadata *Metadata + buffer []byte + decoderBuffer []byte + nodeBuffer []byte + ipV4Start uint + ipV4StartBitDepth uint + nodeOffsetMult uint +} + +func (r *Reader) LookupCity(ip net.IP) (*CityResponse, error) { + if r.Metadata.DatabaseType != "GeoIP2-City" { + return nil, errors.New("wrong MaxMind DB City type: " + r.Metadata.DatabaseType) + } + pointer, err := r.lookupPointer(ip) + if err != nil { + return nil, err + } + if pointer == 0 { + return nil, nil + } + offset := pointer - uint(r.Metadata.NodeCount) - uint(dataSectionSeparatorSize) + if offset >= uint(len(r.buffer)) { + return nil, errors.New("the MaxMind DB search tree is corrupt: " + strconv.Itoa(int(pointer))) + } + return readCityResponse(r.decoderBuffer, offset) +} + +func (r *Reader) LookupISP(ip net.IP) (*ISP, error) { + if r.Metadata.DatabaseType != "GeoIP2-ISP" { + return nil, errors.New("wrong MaxMind DB ISP type: " + r.Metadata.DatabaseType) + } + pointer, err := r.lookupPointer(ip) + if err != nil { + return nil, err + } + if pointer == 0 { + return nil, nil + } + offset := pointer - uint(r.Metadata.NodeCount) - uint(dataSectionSeparatorSize) + if offset >= uint(len(r.buffer)) { + return nil, errors.New("the MaxMind DB search tree is corrupt: " + strconv.Itoa(int(pointer))) + } + return readISP(r.decoderBuffer, offset) +} + +func (r *Reader) LookupConnectionType(ip net.IP) (string, error) { + if r.Metadata.DatabaseType != "GeoIP2-Connection-Type" { + return "", errors.New("wrong MaxMind DB ConnectionType type: " + r.Metadata.DatabaseType) + } + pointer, err := r.lookupPointer(ip) + if err != nil { + return "", err + } + if pointer == 0 { + return "", nil + } + offset := pointer - uint(r.Metadata.NodeCount) - uint(dataSectionSeparatorSize) + if offset >= uint(len(r.buffer)) { + return "", errors.New("the MaxMind DB search tree is corrupt: " + strconv.Itoa(int(pointer))) + } + return readConnectionType(r.decoderBuffer, offset) +} + +func (r *Reader) lookupPointer(ip net.IP) (uint, error) { + if ip == nil { + return 0, errors.New("IP cannot be nil") + } + ipV4 := ip.To4() + if ipV4 != nil { + ip = ipV4 + } + if len(ip) == 16 && r.Metadata.IPVersion == 4 { + return 0, errors.New("cannot look up an IPv6 address in an IPv4-only database") + } + bitCount := uint(len(ip)) * 8 + node := uint(0) + if bitCount == 32 { + node = r.ipV4Start + } + nodeCount := uint(r.Metadata.NodeCount) + i := uint(0) + for ; i < bitCount && node < nodeCount; i++ { + bit := 1 & (ip[i>>3] >> (7 - (i % 8))) + offset := node * r.nodeOffsetMult + if bit == 0 { + node = r.readLeft(offset) + } else { + node = r.readRight(offset) + } + } + if node == nodeCount { + return 0, nil + } else if node > nodeCount { + return node, nil + } + return 0, errors.New("invalid node in search tree") +} + +func (r *Reader) readLeft(nodeNumber uint) uint { + switch r.Metadata.RecordSize { + case 28: + return ((uint(r.nodeBuffer[nodeNumber+3]) & 0xF0) << 20) | (uint(r.nodeBuffer[nodeNumber]) << 16) | (uint(r.nodeBuffer[nodeNumber+1]) << 8) | uint(r.nodeBuffer[nodeNumber+2]) + case 24: + return (uint(r.nodeBuffer[nodeNumber]) << 16) | (uint(r.nodeBuffer[nodeNumber+1]) << 8) | uint(r.nodeBuffer[nodeNumber+2]) + default: // case 32: + return (uint(r.nodeBuffer[nodeNumber]) << 24) | (uint(r.nodeBuffer[nodeNumber+1]) << 16) | (uint(r.nodeBuffer[nodeNumber+2]) << 8) | uint(r.nodeBuffer[nodeNumber+3]) + } +} + +func (r *Reader) readRight(nodeNumber uint) uint { + switch r.Metadata.RecordSize { + case 28: + return ((uint(r.nodeBuffer[nodeNumber+3]) & 0x0F) << 24) | (uint(r.nodeBuffer[nodeNumber+4]) << 16) | (uint(r.nodeBuffer[nodeNumber+5]) << 8) | uint(r.nodeBuffer[nodeNumber+6]) + case 24: + return (uint(r.nodeBuffer[nodeNumber+3]) << 16) | (uint(r.nodeBuffer[nodeNumber+4]) << 8) | uint(r.nodeBuffer[nodeNumber+5]) + default: // case 32: + return (uint(r.nodeBuffer[nodeNumber+4]) << 24) | (uint(r.nodeBuffer[nodeNumber+5]) << 16) | (uint(r.nodeBuffer[nodeNumber+6]) << 8) | uint(r.nodeBuffer[nodeNumber+7]) + } +} + +func NewReaderFromFile(filename string) (*Reader, error) { + buffer, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return NewReader(buffer) +} + +func NewReader(buffer []byte) (*Reader, error) { + metadataStart := bytes.LastIndex(buffer, metadataStartMarker) + metadata, err := readMetadata(buffer[metadataStart+len(metadataStartMarker):]) + if err != nil { + return nil, err + } + nodeOffsetMult := uint(metadata.RecordSize) / 4 + searchTreeSize := uint(metadata.NodeCount) * nodeOffsetMult + dataSectionStart := searchTreeSize + dataSectionSeparatorSize + if dataSectionStart > uint(metadataStart) { + return nil, errors.New("the MaxMind DB contains invalid metadata") + } + reader := &Reader{ + Metadata: metadata, + buffer: buffer, + decoderBuffer: buffer[searchTreeSize+dataSectionSeparatorSize : metadataStart], + nodeBuffer: buffer[:searchTreeSize], + nodeOffsetMult: nodeOffsetMult, + } + if metadata.IPVersion == 6 { + node := uint(0) + i := uint(0) + for ; i < 96 && node < uint(metadata.NodeCount); i++ { + node = reader.readLeft(node * nodeOffsetMult) + } + reader.ipV4Start = node + reader.ipV4StartBitDepth = i + } + return reader, nil +} diff --git a/subdivision.go b/subdivision.go new file mode 100644 index 0000000..d36b940 --- /dev/null +++ b/subdivision.go @@ -0,0 +1,115 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +type Subdivision struct { + GeoNameID uint32 + ISOCode string + Names map[string]string +} + +func readSubdivisions(buffer []byte, offset uint) ([]Subdivision, uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return nil, 0, err + } + switch dataType { + case dataTypeSlice: + return readSubdivisionsSlice(buffer, size, offset) + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return nil, 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return nil, 0, err + } + if dataType != dataTypeSlice { + return nil, 0, errors.New("invalid subdivisions pointer type: " + strconv.Itoa(int(dataType))) + } + subdivisions, _, err := readSubdivisionsSlice(buffer, size, offset) + if err != nil { + return nil, 0, err + } + return subdivisions, newOffset, nil + default: + return nil, 0, errors.New("invalid subdivisions type: " + strconv.Itoa(int(dataType))) + } +} + +func readSubdivisionsSlice(buffer []byte, subdivisionsSize uint, offset uint) ([]Subdivision, uint, error) { + var err error + subdivisions := make([]Subdivision, subdivisionsSize) + for i := uint(0); i < subdivisionsSize; i++ { + offset, err = readSubdivision(&subdivisions[i], buffer, offset) + if err != nil { + return nil, 0, err + } + } + return subdivisions, offset, nil +} + +func readSubdivision(subdivision *Subdivision, buffer []byte, offset uint) (uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, err + } + switch dataType { + case dataTypeMap: + return readSubdivisionMap(subdivision, buffer, size, offset) + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, err + } + if dataType != dataTypeMap { + return 0, errors.New("invalid subdivision pointer type: " + strconv.Itoa(int(dataType))) + } + _, err = readSubdivisionMap(subdivision, buffer, size, offset) + if err != nil { + return 0, err + } + return newOffset, nil + default: + return 0, errors.New("invalid subdivision type: " + strconv.Itoa(int(dataType))) + } +} + +func readSubdivisionMap(subdivision *Subdivision, buffer []byte, mapSize uint, offset uint) (uint, error) { + var key []byte + var err error + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return 0, err + } + switch b2s(key) { + case "geoname_id": + subdivision.GeoNameID, offset, err = readUInt32(buffer, offset) + if err != nil { + return 0, err + } + case "iso_code": + subdivision.ISOCode, offset, err = readString(buffer, offset) + if err != nil { + return 0, err + } + case "names": + subdivision.Names, offset, err = readStringMap(buffer, offset) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unknown subdivision key: " + string(key)) + } + } + return offset, nil +} diff --git a/traits.go b/traits.go new file mode 100644 index 0000000..3605bfc --- /dev/null +++ b/traits.go @@ -0,0 +1,67 @@ +package geoip2 + +import ( + "errors" + "strconv" +) + +func readTraits(traits *Traits, buffer []byte, offset uint) (uint, error) { + dataType, size, offset, err := readControl(buffer, offset) + if err != nil { + return 0, err + } + switch dataType { + case dataTypeMap: + return readTraitsMap(traits, buffer, size, offset) + case dataTypePointer: + pointer, newOffset, err := readPointer(buffer, size, offset) + if err != nil { + return 0, err + } + dataType, size, offset, err := readControl(buffer, pointer) + if err != nil { + return 0, err + } + if dataType != dataTypeMap { + return 0, errors.New("invalid traits pointer type: " + strconv.Itoa(int(dataType))) + } + _, err = readTraitsMap(traits, buffer, size, offset) + if err != nil { + return 0, err + } + return newOffset, nil + default: + return 0, errors.New("invalid traits type: " + strconv.Itoa(int(dataType))) + } +} + +func readTraitsMap(traits *Traits, buffer []byte, mapSize uint, offset uint) (uint, error) { + var key []byte + var err error + for i := uint(0); i < mapSize; i++ { + key, offset, err = readMapKey(buffer, offset) + if err != nil { + return 0, err + } + switch b2s(key) { + case "is_anonymous_proxy": + traits.IsAnonymousProxy, offset, err = readBool(buffer, offset) + if err != nil { + return 0, err + } + case "is_satellite_provider": + traits.IsSatelliteProvider, offset, err = readBool(buffer, offset) + if err != nil { + return 0, err + } + case "static_ip_score": + traits.StaticIPScore, offset, err = readFloat64(buffer, offset) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unknown traits key: " + string(key)) + } + } + return offset, nil +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..b1bb1dd --- /dev/null +++ b/types.go @@ -0,0 +1,82 @@ +package geoip2 + +const ( + dataTypeExtended = 0 + dataTypePointer = 1 + dataTypeString = 2 + dataTypeFloat64 = 3 + dataTypeBytes = 4 + dataTypeUint16 = 5 + dataTypeUint32 = 6 + dataTypeMap = 7 + dataTypeInt32 = 8 + dataTypeUint64 = 9 + dataTypeUint128 = 10 + dataTypeSlice = 11 + dataTypeDataCacheContainer = 12 + dataTypeEndMarker = 13 + dataTypeBool = 14 + dataTypeFloat32 = 15 + + dataSectionSeparatorSize = 16 +) + +type Continent struct { + GeoNameID uint32 + Code string + Names map[string]string +} + +type Country struct { + GeoNameID uint32 + ISOCode string + IsInEuropeanEnion bool + Names map[string]string + Type string +} + +type City struct { + GeoNameID uint32 + Names map[string]string +} + +type Location struct { + AccuracyRadius uint16 + MetroCode uint16 + Latitude float64 + Longitude float64 + TimeZone string +} + +type Postal struct { + Code string +} + +type Traits struct { + IsAnonymousProxy bool + IsSatelliteProvider bool + StaticIPScore float64 +} + +type CityResponse struct { + Continent Continent + City City + Country Country + Subdivisions []Subdivision + Location Location + Postal Postal + RegisteredCountry Country + RepresentedCountry Country + Traits Traits +} + +type ISP struct { + AutonomousSystemNumber uint32 + AutonomousSystemOrganization string + ISP string + Organization string +} + +type ConnectionType struct { + ConnectionType string +}