feat: geo ip.

This commit is contained in:
tx7do
2023-10-19 15:05:49 +08:00
parent 92be8b878a
commit 01bdf5cc06
21 changed files with 343 additions and 38 deletions

View File

@@ -0,0 +1,6 @@
package assets
import _ "embed"
//go:embed bank_card.db
var BankCardDatabase []byte

View File

@@ -3,17 +3,15 @@ package bank_card
import (
"context"
"database/sql"
_ "embed"
"errors"
"fmt"
"log"
"github.com/mattn/go-sqlite3"
_ "github.com/mattn/go-sqlite3"
)
//go:embed bank_card.db
var embeddedDatabase []byte
"github.com/tx7do/kratos-utils/bank_card/assets"
)
type Database struct {
db *sql.DB
@@ -33,7 +31,7 @@ func NewDatabase(openFile bool) *Database {
// openFromFile 从文件打开数据库
func (d *Database) openFromFile() {
db, err := sql.Open("sqlite3", "bank_card.db")
db, err := sql.Open("sqlite3", "assets/bank_card.db")
if err != nil {
panic("failed to connect database")
}
@@ -59,7 +57,7 @@ func (d *Database) openFromEmbed() error {
defer conn.Close()
if err = conn.Raw(func(raw interface{}) error {
return raw.(*sqlite3.SQLiteConn).Deserialize(embeddedDatabase, "")
return raw.(*sqlite3.SQLiteConn).Deserialize(assets.BankCardDatabase, "")
}); err != nil {
log.Fatal(err)
return err

View File

@@ -18,7 +18,7 @@ func TestImportBankName(t *testing.T) {
//db.openFromFile()
file, err := os.Open("name.csv")
file, err := os.Open("assets/name.csv")
if err != nil {
fmt.Println("Read file err, err =", err)
return
@@ -65,7 +65,7 @@ func TestImportBankCard(t *testing.T) {
//db.openFromFile()
file, err := os.Open("bin.csv")
file, err := os.Open("assets/bin.csv")
if err != nil {
fmt.Println("Read file err, err =", err)
return

View File

@@ -1,12 +1,6 @@
# GEOIP2GeoLite2
# IP地理位置查询
GeoLite2数据库是免费的IP地理位置数据库它可以通过IP地址识别其所在的城市和国家。与MaxMind的GeoIP2数据库相比准确性较差。
## 支持库
GeoLite2-*.mmdb文件是一个二进制数据库文件包含了MaxMind GeoLite2所需的全部数据。
GeoLite2的数据库分为国家(Country),城市(City)和ASN三个数据库。每周两次更新。
## 数据库下载地址
1. <https://github.com/P3TERX/GeoLite.mmdb/releases>
2. <https://dev.maxmind.com/geoip/geolite2-free-geolocation-data>
- [GeoLite2](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data)
- [纯真](https://update.cz88.net/geo-public)

12
geoip/geolite/README.md Normal file
View File

@@ -0,0 +1,12 @@
# GEOIP2GeoLite2
GeoLite2数据库是免费的IP地理位置数据库它可以通过IP地址识别其所在的城市和国家。与MaxMind的GeoIP2数据库相比准确性较差。
GeoLite2-*.mmdb文件是一个二进制数据库文件包含了MaxMind GeoLite2所需的全部数据。
GeoLite2的数据库分为国家(Country),城市(City)和ASN三个数据库。每周两次更新。
## 数据库下载地址
1. <https://github.com/P3TERX/GeoLite.mmdb/releases>
2. <https://dev.maxmind.com/geoip/geolite2-free-geolocation-data>

View File

Before

Width:  |  Height:  |  Size: 68 MiB

After

Width:  |  Height:  |  Size: 68 MiB

View File

@@ -0,0 +1,6 @@
package assets
import _ "embed"
//go:embed GeoLite2-City.mmdb
var GeoLite2CityData []byte

View File

@@ -1,48 +1,52 @@
package geoip
package geolite
import (
_ "embed"
"errors"
"net"
"github.com/go-kratos/kratos/v2/log"
"github.com/oschwald/geoip2-golang"
"github.com/tx7do/kratos-utils/geoip/geolite/assets"
)
//go:embed GeoLite2-City.mmdb
var geoipMmdbByte []byte
type Result struct {
Country string // 国家
Province string // 省
City string // 城市
}
// GeoIp 地理位置解析结构体
type GeoIp struct {
const defaultOutputLanguage = "zh-CN"
// Client 地理位置解析结构体
type Client struct {
db *geoip2.Reader
outputLanguage string
}
// NewGeoIp .
func NewGeoIp() (geoip *GeoIp, err error) {
db, err := geoip2.FromBytes(geoipMmdbByte)
// NewClient .
func NewClient() (*Client, error) {
db, err := geoip2.FromBytes(assets.GeoLite2CityData)
if err != nil {
return nil, err
}
return &GeoIp{db: db, outputLanguage: "zh-CN"}, nil
return &Client{db: db, outputLanguage: defaultOutputLanguage}, nil
}
func (g *GeoIp) Close() (err error) {
// Close 关闭客户端
func (g *Client) Close() error {
if g.db == nil {
return nil
}
return g.db.Close()
}
// SetLanguage 设置输出的语言默认为zh-CN
func (g *GeoIp) SetLanguage(code string) {
func (g *Client) SetLanguage(code string) {
g.outputLanguage = code
}
func (g *GeoIp) query(rawIP string) (city *geoip2.City, err error) {
// query 查询城市级别数据
func (g *Client) query(rawIP string) (city *geoip2.City, err error) {
ip := net.ParseIP(rawIP)
if ip == nil {
return nil, errors.New("invalid ip")
@@ -51,8 +55,8 @@ func (g *GeoIp) query(rawIP string) (city *geoip2.City, err error) {
return g.db.City(ip)
}
// GetAreaFromIP 通过IP获取地区
func (g *GeoIp) GetAreaFromIP(rawIP string) (ret Result, err error) {
// Query 通过IP获取地区
func (g *Client) Query(rawIP string) (ret Result, err error) {
record, err := g.query(rawIP)
if err != nil {
log.Fatal(err)

View File

@@ -1,15 +1,15 @@
package geoip
package geolite
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestGeoIp(t *testing.T) {
g, err := NewGeoIp()
func TestGeoLite(t *testing.T) {
g, err := NewClient()
assert.Nil(t, err)
ret, err := g.GetAreaFromIP("47.108.149.89")
ret, err := g.Query("47.108.149.89")
assert.Nil(t, err)
assert.Equal(t, ret.Country, "中国")
assert.Equal(t, ret.Province, "四川省")

View File

@@ -6,6 +6,7 @@ require (
github.com/go-kratos/kratos/v2 v2.7.0
github.com/oschwald/geoip2-golang v1.9.0
github.com/stretchr/testify v1.8.4
golang.org/x/text v0.11.0
)
require (

View File

@@ -17,6 +17,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

8
geoip/qqwry/README.md Normal file
View File

@@ -0,0 +1,8 @@
# QQWry
## 数据库下载地址
- [纯真社区版IP库](https://update.cz88.net/geo-public)
- [qqwry.dat - out0fmemory - Github](https://github.com/out0fmemory/qqwry.dat/blob/master/qqwry_lastest.dat)
- [qqwry - xiaoqidun - Github](https://github.com/xiaoqidun/qqwry)
- [qqwry - freshcn - Github](https://github.com/freshcn/qqwry)

View File

@@ -0,0 +1,6 @@
package assets
import _ "embed"
//go:embed qqwry.dat
var QQWryDat []byte

Binary file not shown.

20
geoip/qqwry/consts.go Normal file
View File

@@ -0,0 +1,20 @@
package qqwry
const (
ipRecordLength = 7 // IndexLen 索引长度
redirectMode1 = 0x01 // RedirectMode1 国家的类型, 指向另一个指向
redirectMode2 = 0x02 // RedirectMode2 国家的类型, 指向一个指向
)
//var unCountry = []byte{"未知国家"}
//var unArea = []byte{"未知地区"}
// Result 归属地信息
type Result struct {
IP string `json:"ip"`
Country string `json:"country"`
Area string `json:"area"`
ISP string `json:"isp"`
}

204
geoip/qqwry/qqwry.go Normal file
View File

@@ -0,0 +1,204 @@
package qqwry
import (
"encoding/binary"
"errors"
"net"
"strings"
"sync"
"github.com/tx7do/kratos-utils/geoip/qqwry/assets"
)
type Client struct {
data []byte
dataLen uint32
ipCache sync.Map
IPNum int64
startPos uint32
endPos uint32
}
func NewClient() *Client {
cli := &Client{
ipCache: sync.Map{},
}
cli.init()
return cli
}
func (c *Client) init() {
c.startPos, c.endPos = c.readHeader()
c.IPNum = int64((c.endPos-c.startPos)/ipRecordLength + 1)
}
// parseIp 解析IP
func (c *Client) parseIp(queryIp string) (uint32, error) {
ip := net.ParseIP(queryIp).To4()
if ip == nil {
return 0, errors.New("ip is not ipv4")
}
ip32 := binary.BigEndian.Uint32(ip)
return ip32, nil
}
// readHeader 读取文件头
func (c *Client) readHeader() (uint32, uint32) {
startPos := binary.LittleEndian.Uint32(assets.QQWryDat[:4])
endPos := binary.LittleEndian.Uint32(assets.QQWryDat[4:8])
return startPos, endPos
}
// readMode 获取偏移值类型
func (c *Client) readMode(offset uint32) byte {
return assets.QQWryDat[offset]
}
// readIpRecord 读取IP记录 前4字节起始IP后3字节偏移量
func (c *Client) readIpRecord(offset uint32) (ip32 uint32, ipOffset uint32) {
buf := assets.QQWryDat[offset : offset+ipRecordLength]
ip32 = binary.LittleEndian.Uint32(buf[:4])
ipOffset = byte3ToUInt32(buf[4:])
return ip32, ipOffset
}
// locateIP 定位IP
func (c *Client) locateIP(ip32 uint32) int32 {
var _ip32 uint32
var _ipOffset uint32
var offset uint32
var mid uint32
i := c.startPos
j := c.endPos
for {
mid = getMiddleOffset(i, j)
_ip32, _ipOffset = c.readIpRecord(mid)
if j-i == ipRecordLength {
offset = _ipOffset
_ip32, _ipOffset = c.readIpRecord(mid + ipRecordLength)
if ip32 < _ip32 {
break
} else {
offset = 0
break
}
}
if _ip32 > ip32 {
j = mid
} else if _ip32 < ip32 {
i = mid
} else if _ip32 == ip32 {
offset = _ipOffset
break
}
}
return int32(offset)
}
// readArea 读取区域
func (c *Client) readArea(offset uint32) []byte {
mode := c.readMode(offset)
if mode == redirectMode1 || mode == redirectMode2 {
areaOffset := c.readUInt24(int32(offset) + 1)
if areaOffset == 0 {
return []byte{}
}
return c.readString(areaOffset)
}
return c.readString(offset)
}
// readString 获取字符串
func (c *Client) readString(offset uint32) []byte {
data := make([]byte, 0, 30)
for i := offset; i < uint32(len(assets.QQWryDat)); i++ {
if assets.QQWryDat[i] == 0 {
data = assets.QQWryDat[offset:i]
break
}
}
return data
}
func (c *Client) readUInt24(offset int32) uint32 {
i := uint32(assets.QQWryDat[offset+0]) & 0xFF
i |= (uint32(assets.QQWryDat[offset+1]) << 8) & 0xFF00
i |= (uint32(assets.QQWryDat[offset+2]) << 16) & 0xFF0000
return i
}
func (c *Client) Query(queryIp string) (country, city, isp string, err error) {
ip32, err := c.parseIp(queryIp)
if err != nil {
return
}
offset := c.locateIP(ip32)
if offset <= 0 {
err = errors.New("ip not found")
return
}
//读取第一个字节判断是否是标志字节
offset += 4
mode := c.readMode(uint32(offset))
var _city []byte
var ispPos uint32
switch mode {
case redirectMode1:
posC := c.readUInt24(offset + 1)
mode = c.readMode(posC)
posCA := posC
if mode == redirectMode2 {
posCA = c.readUInt24(int32(posC) + 1)
posC += 4
}
_city = c.readString(posCA)
if mode != redirectMode2 {
posC += uint32(len(city) + 1)
}
ispPos = posC
case redirectMode2:
posCA := c.readUInt24(offset + 1)
_city = c.readString(posCA)
ispPos = uint32(offset) + 4
default:
posCA := offset + 0
_city = c.readString(uint32(posCA))
ispPos = uint32(offset) + uint32(len(city)) + 1
}
if len(_city) != 0 {
city = strings.TrimSpace(gb18030Decode(_city))
}
ispMode := assets.QQWryDat[ispPos]
if ispMode == redirectMode1 || ispMode == redirectMode2 {
ispPos = c.readUInt24(int32(ispPos + 1))
}
if ispPos > 0 {
var _isp []byte
_isp = c.readString(ispPos)
isp = strings.TrimSpace(gb18030Decode(_isp))
if isp != "" {
if strings.Contains(isp, "CZ88.NET") {
isp = ""
}
}
}
return
}

16
geoip/qqwry/qqwry_test.go Normal file
View File

@@ -0,0 +1,16 @@
package qqwry
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestClient(t *testing.T) {
g := NewClient()
assert.NotNil(t, g)
country, city, isp, err := g.Query("47.108.149.89")
assert.Nil(t, err)
fmt.Println("国家:", country, "城市:", city, "服务商:", isp)
}

28
geoip/qqwry/utils.go Normal file
View File

@@ -0,0 +1,28 @@
package qqwry
import (
"bytes"
"io"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
func gb18030Decode(src []byte) string {
in := bytes.NewReader(src)
out := transform.NewReader(in, simplifiedchinese.GB18030.NewDecoder())
d, _ := io.ReadAll(out)
return string(d)
}
// getMiddleOffset 取得begin和end之间的偏移量用于二分搜索
func getMiddleOffset(start uint32, end uint32) uint32 {
return start + (((end-start)/ipRecordLength)>>1)*ipRecordLength
}
func byte3ToUInt32(data []byte) uint32 {
i := uint32(data[0]) & 0xff
i |= (uint32(data[1]) << 8) & 0xff00
i |= (uint32(data[2]) << 16) & 0xff0000
return i
}