feat(ip-location): implement IP location querying and GeoIP database management
This commit is contained in:
parent
e3999ba75f
commit
c4166cef6b
@ -17,6 +17,14 @@ type (
|
|||||||
VersionResponse {
|
VersionResponse {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
QueryIPLocationRequest {
|
||||||
|
IP string `form:"ip" validate:"required"`
|
||||||
|
}
|
||||||
|
QueryIPLocationResponse {
|
||||||
|
Country string `json:"country"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
City string `json:"city"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -36,5 +44,9 @@ service ppanel {
|
|||||||
@doc "Get Version"
|
@doc "Get Version"
|
||||||
@handler GetVersion
|
@handler GetVersion
|
||||||
get /version returns (VersionResponse)
|
get /version returns (VersionResponse)
|
||||||
|
|
||||||
|
@doc "Query IP Location"
|
||||||
|
@handler QueryIPLocation
|
||||||
|
get /ip/location (QueryIPLocationRequest) returns (QueryIPLocationResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8080
|
||||||
|
TLS:
|
||||||
|
Enable: false
|
||||||
|
CertFile: ""
|
||||||
|
KeyFile: ""
|
||||||
|
Debug: false
|
||||||
|
JwtAuth:
|
||||||
|
AccessSecret: 4672dccd-2c50-4833-8e17-81e6c2119893
|
||||||
|
AccessExpire: 604800
|
||||||
|
Logger:
|
||||||
|
ServiceName: PPanel
|
||||||
|
Mode: console
|
||||||
|
Encoding: plain
|
||||||
|
TimeFormat: "2006-01-02 15:04:05.000"
|
||||||
|
Path: logs
|
||||||
|
Level: info
|
||||||
|
MaxContentLength: 0
|
||||||
|
Compress: false
|
||||||
|
Stat: true
|
||||||
|
KeepDays: 0
|
||||||
|
StackCooldownMillis: 100
|
||||||
|
MaxBackups: 0
|
||||||
|
MaxSize: 0
|
||||||
|
Rotation: daily
|
||||||
|
FileTimeFormat: 2006-01-02T15:04:05.000Z07:00
|
||||||
|
MySQL:
|
||||||
|
Addr: localhost:3306
|
||||||
|
Username: root
|
||||||
|
Password: password
|
||||||
|
Dbname: ppanel_dev
|
||||||
|
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||||
|
MaxIdleConns: 10
|
||||||
|
MaxOpenConns: 10
|
||||||
|
SlowThreshold: 1000
|
||||||
|
Redis:
|
||||||
|
Host: 127.0.0.1:6379
|
||||||
|
Pass: ""
|
||||||
|
DB: 0
|
||||||
2
go.mod
2
go.mod
@ -60,6 +60,7 @@ require (
|
|||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/goccy/go-json v0.10.4
|
github.com/goccy/go-json v0.10.4
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||||
|
github.com/oschwald/geoip2-golang v1.13.0
|
||||||
github.com/spaolacci/murmur3 v1.1.0
|
github.com/spaolacci/murmur3 v1.1.0
|
||||||
google.golang.org/grpc v1.64.1
|
google.golang.org/grpc v1.64.1
|
||||||
google.golang.org/protobuf v1.36.3
|
google.golang.org/protobuf v1.36.3
|
||||||
@ -117,6 +118,7 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/openzipkin/zipkin-go v0.4.2 // indirect
|
github.com/openzipkin/zipkin-go v0.4.2 // indirect
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -285,6 +285,10 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
|
|||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
|
github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
|
||||||
github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY=
|
github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY=
|
||||||
|
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
||||||
|
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
|||||||
26
internal/handler/admin/tool/queryIPLocationHandler.go
Normal file
26
internal/handler/admin/tool/queryIPLocationHandler.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package tool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/perfect-panel/server/internal/logic/admin/tool"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueryIPLocationHandler Query IP Location
|
||||||
|
func QueryIPLocationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var req types.QueryIPLocationRequest
|
||||||
|
_ = c.ShouldBind(&req)
|
||||||
|
validateErr := svcCtx.Validate(&req)
|
||||||
|
if validateErr != nil {
|
||||||
|
result.ParamErrorResult(c, validateErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := tool.NewQueryIPLocationLogic(c.Request.Context(), svcCtx)
|
||||||
|
resp, err := l.QueryIPLocation(&req)
|
||||||
|
result.HttpResult(c, resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,6 @@ import (
|
|||||||
// Get Client
|
// Get Client
|
||||||
func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|
||||||
l := common.NewGetClientLogic(c.Request.Context(), svcCtx)
|
l := common.NewGetClientLogic(c.Request.Context(), svcCtx)
|
||||||
resp, err := l.GetClient()
|
resp, err := l.GetClient()
|
||||||
result.HttpResult(c, resp, err)
|
result.HttpResult(c, resp, err)
|
||||||
|
|||||||
57
internal/logic/admin/tool/queryIPLocationLogic.go
Normal file
57
internal/logic/admin/tool/queryIPLocationLogic.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package tool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueryIPLocationLogic struct {
|
||||||
|
logger.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQueryIPLocationLogic Query IP Location
|
||||||
|
func NewQueryIPLocationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryIPLocationLogic {
|
||||||
|
return &QueryIPLocationLogic{
|
||||||
|
Logger: logger.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *QueryIPLocationLogic) QueryIPLocation(req *types.QueryIPLocationRequest) (resp *types.QueryIPLocationResponse, err error) {
|
||||||
|
if l.svcCtx.GeoIP == nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " GeoIP database not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(req.IP)
|
||||||
|
record, err := l.svcCtx.GeoIP.DB.City(ip)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorf("Failed to query IP location: %v", err)
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to query IP location")
|
||||||
|
}
|
||||||
|
|
||||||
|
var country, region, city string
|
||||||
|
if record.Country.Names != nil {
|
||||||
|
country = record.Country.Names["en"]
|
||||||
|
}
|
||||||
|
if len(record.Subdivisions) > 0 && record.Subdivisions[0].Names != nil {
|
||||||
|
region = record.Subdivisions[0].Names["en"]
|
||||||
|
}
|
||||||
|
if record.City.Names != nil {
|
||||||
|
city = record.City.Names["en"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.QueryIPLocationResponse{
|
||||||
|
Country: country,
|
||||||
|
Region: region,
|
||||||
|
City: city,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
74
internal/svc/mmdb.go
Normal file
74
internal/svc/mmdb.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package svc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/oschwald/geoip2-golang"
|
||||||
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const GeoIPDBURL = "https://raw.githubusercontent.com/adysec/IP_database/main/geolite/GeoLite2-City.mmdb"
|
||||||
|
|
||||||
|
type IPLocation struct {
|
||||||
|
Path string
|
||||||
|
DB *geoip2.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIPLocation(path string) (*IPLocation, error) {
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
logger.Infof("[GeoIP] Database not found, downloading from %s", GeoIPDBURL)
|
||||||
|
// 文件不存在,下载数据库
|
||||||
|
err := DownloadGeoIPDatabase(GeoIPDBURL, path)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("[GeoIP] Failed to download database: %v", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger.Infof("[GeoIP] Database downloaded successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := geoip2.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &IPLocation{
|
||||||
|
Path: path,
|
||||||
|
DB: db,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ipLoc *IPLocation) Close() error {
|
||||||
|
return ipLoc.DB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadGeoIPDatabase(url, path string) error {
|
||||||
|
|
||||||
|
// 创建路径, 确保目录存在
|
||||||
|
err := os.MkdirAll(filepath.Dir(path), 0755)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("[GeoIP] Failed to create directory: %v", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文件
|
||||||
|
out, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
// 请求远程文件
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@ type ServiceContext struct {
|
|||||||
Config config.Config
|
Config config.Config
|
||||||
Queue *asynq.Client
|
Queue *asynq.Client
|
||||||
ExchangeRate float64
|
ExchangeRate float64
|
||||||
|
GeoIP *IPLocation
|
||||||
|
|
||||||
//NodeCache *cache.NodeCacheClient
|
//NodeCache *cache.NodeCacheClient
|
||||||
AuthModel auth.Model
|
AuthModel auth.Model
|
||||||
@ -68,9 +69,17 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
db, err := orm.ConnectMysql(orm.Mysql{
|
db, err := orm.ConnectMysql(orm.Mysql{
|
||||||
Config: c.MySQL,
|
Config: c.MySQL,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IP location initialize
|
||||||
|
geoIP, err := NewIPLocation("./cache/GeoLite2-City.mmdb")
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
rds := redis.NewClient(&redis.Options{
|
rds := redis.NewClient(&redis.Options{
|
||||||
Addr: c.Redis.Host,
|
Addr: c.Redis.Host,
|
||||||
Password: c.Redis.Pass,
|
Password: c.Redis.Pass,
|
||||||
@ -89,6 +98,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
Config: c,
|
Config: c,
|
||||||
Queue: NewAsynqClient(c),
|
Queue: NewAsynqClient(c),
|
||||||
ExchangeRate: 1.0,
|
ExchangeRate: 1.0,
|
||||||
|
GeoIP: geoIP,
|
||||||
//NodeCache: cache.NewNodeCacheClient(rds),
|
//NodeCache: cache.NewNodeCacheClient(rds),
|
||||||
AuthLimiter: authLimiter,
|
AuthLimiter: authLimiter,
|
||||||
AdsModel: ads.NewModel(db, rds),
|
AdsModel: ads.NewModel(db, rds),
|
||||||
|
|||||||
@ -1571,6 +1571,16 @@ type QueryDocumentListResponse struct {
|
|||||||
List []Document `json:"list"`
|
List []Document `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QueryIPLocationRequest struct {
|
||||||
|
IP string `form:"ip" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryIPLocationResponse struct {
|
||||||
|
Country string `json:"country"`
|
||||||
|
Region string `json:"regio,omitempty"`
|
||||||
|
City string `json:"city"`
|
||||||
|
}
|
||||||
|
|
||||||
type QueryNodeTagResponse struct {
|
type QueryNodeTagResponse struct {
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
}
|
}
|
||||||
@ -2598,6 +2608,7 @@ type UserSubscribe struct {
|
|||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
Short string `json:"short"`
|
Short string `json:"short"`
|
||||||
|
Note string `json:"note"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@ -2617,6 +2628,7 @@ type UserSubscribeDetail struct {
|
|||||||
Upload int64 `json:"upload"`
|
Upload int64 `json:"upload"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
|
Note string `json:"note"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@ -2635,6 +2647,7 @@ type UserSubscribeInfo struct {
|
|||||||
Upload int64 `json:"upload"`
|
Upload int64 `json:"upload"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
|
Note string `json:"note"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
IsTryOut bool `json:"is_try_out"`
|
IsTryOut bool `json:"is_try_out"`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user