feat: geo ip.
This commit is contained in:
6
bank_card/assets/assets.go
Normal file
6
bank_card/assets/assets.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package assets
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed bank_card.db
|
||||
var BankCardDatabase []byte
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
# GEOIP2(GeoLite2)
|
||||
# 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
12
geoip/geolite/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# GEOIP2(GeoLite2)
|
||||
|
||||
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>
|
||||
|
Before Width: | Height: | Size: 68 MiB After Width: | Height: | Size: 68 MiB |
6
geoip/geolite/assets/assets.go
Normal file
6
geoip/geolite/assets/assets.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package assets
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed GeoLite2-City.mmdb
|
||||
var GeoLite2CityData []byte
|
||||
@@ -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)
|
||||
@@ -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, "四川省")
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
8
geoip/qqwry/README.md
Normal 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)
|
||||
6
geoip/qqwry/assets/assets.go
Normal file
6
geoip/qqwry/assets/assets.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package assets
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed qqwry.dat
|
||||
var QQWryDat []byte
|
||||
BIN
geoip/qqwry/assets/qqwry.dat
Normal file
BIN
geoip/qqwry/assets/qqwry.dat
Normal file
Binary file not shown.
20
geoip/qqwry/consts.go
Normal file
20
geoip/qqwry/consts.go
Normal 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
204
geoip/qqwry/qqwry.go
Normal 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
16
geoip/qqwry/qqwry_test.go
Normal 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
28
geoip/qqwry/utils.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user