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 {
|
||||
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 (
|
||||
@ -36,5 +44,9 @@ service ppanel {
|
||||
@doc "Get Version"
|
||||
@handler GetVersion
|
||||
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/goccy/go-json v0.10.4
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
github.com/oschwald/geoip2-golang v1.13.0
|
||||
github.com/spaolacci/murmur3 v1.1.0
|
||||
google.golang.org/grpc v1.64.1
|
||||
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/reflect2 v1.0.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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
|
||||
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/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
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
|
||||
func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
l := common.NewGetClientLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetClient()
|
||||
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
|
||||
Queue *asynq.Client
|
||||
ExchangeRate float64
|
||||
GeoIP *IPLocation
|
||||
|
||||
//NodeCache *cache.NodeCacheClient
|
||||
AuthModel auth.Model
|
||||
@ -68,9 +69,17 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
db, err := orm.ConnectMysql(orm.Mysql{
|
||||
Config: c.MySQL,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
// IP location initialize
|
||||
geoIP, err := NewIPLocation("./cache/GeoLite2-City.mmdb")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
rds := redis.NewClient(&redis.Options{
|
||||
Addr: c.Redis.Host,
|
||||
Password: c.Redis.Pass,
|
||||
@ -89,6 +98,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
Config: c,
|
||||
Queue: NewAsynqClient(c),
|
||||
ExchangeRate: 1.0,
|
||||
GeoIP: geoIP,
|
||||
//NodeCache: cache.NewNodeCacheClient(rds),
|
||||
AuthLimiter: authLimiter,
|
||||
AdsModel: ads.NewModel(db, rds),
|
||||
|
||||
@ -1571,6 +1571,16 @@ type QueryDocumentListResponse struct {
|
||||
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 {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
@ -2598,6 +2608,7 @@ type UserSubscribe struct {
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
Short string `json:"short"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
@ -2617,6 +2628,7 @@ type UserSubscribeDetail struct {
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
@ -2635,6 +2647,7 @@ type UserSubscribeInfo struct {
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
IsTryOut bool `json:"is_try_out"`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user