From d0b94abeb5fcbab29d8a4907bba23987175ca792 Mon Sep 17 00:00:00 2001 From: Akizon77 Date: Sat, 3 May 2025 20:41:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20#11=20=E6=B7=BB=E5=8A=A0Geoip=EF=BC=8CC?= =?UTF-8?q?ustom=20CSS=E3=80=81JS=20=E5=90=88=E5=B9=B6=E4=B8=BACustomHead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + api/client/uploadBasicInfo.go | 21 +++++++ database/models/models.go | 11 ++-- go.mod | 1 + go.sum | 2 + public/public.go | 3 +- utils/geoip/geoip.go | 107 ++++++++++++++++++++++++++++++++++ utils/geoip/geoip_test.go | 66 +++++++++++++++++++++ 8 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 utils/geoip/geoip.go create mode 100644 utils/geoip/geoip_test.go diff --git a/.gitignore b/.gitignore index 8f8039a..b31be40 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,10 @@ *.so *.dylib +# local development data komari.db /data +/utils/geoip/data .vscode/ # Test binary, built with `go test -c` diff --git a/api/client/uploadBasicInfo.go b/api/client/uploadBasicInfo.go index c972ecf..6b9c451 100644 --- a/api/client/uploadBasicInfo.go +++ b/api/client/uploadBasicInfo.go @@ -1,8 +1,12 @@ package client import ( + "net" + "github.com/komari-monitor/komari/common" "github.com/komari-monitor/komari/database/clients" + "github.com/komari-monitor/komari/database/config" + "github.com/komari-monitor/komari/utils/geoip" "github.com/gin-gonic/gin" ) @@ -22,6 +26,23 @@ func UploadBasicInfo(c *gin.Context) { } cbi.UUID = uuid + + if cfg, err := config.Get(); err != nil && cfg.GeoIpEnabled { + if cbi.IPv4 != "" { + ip4 := net.ParseIP(cbi.IPv4) + ip4_record, _ := geoip.GetGeoIpInfo(ip4) + if ip4_record != nil { + cbi.Country = geoip.GetCountryUnicodeEmoji(ip4_record.Country.ISOCode) + } + } else if cbi.IPv6 != "" { + ip6 := net.ParseIP(cbi.IPv6) + ip6_record, _ := geoip.GetGeoIpInfo(ip6) + if ip6_record != nil { + cbi.Country = geoip.GetCountryUnicodeEmoji(ip6_record.Country.ISOCode) + } + } + } + if err := clients.UpdateOrInsertBasicInfo(cbi); err != nil { c.JSON(500, gin.H{"status": "error", "error": err}) return diff --git a/database/models/models.go b/database/models/models.go index b456b6a..8049bb3 100644 --- a/database/models/models.go +++ b/database/models/models.go @@ -60,14 +60,15 @@ type Config struct { Sitename string `json:"sitename,omitempty" gorm:"type:varchar(100);not null"` Description string `json:"desc,omitempty" gorm:"type:text"` AllowCros bool `json:"allow_cros" gorm:"default:false"` + // GeoIP 配置 + GeoIpEnabled bool `json:"geoip_enable" gorm:"default:true"` + GeoIpProvider string `json:"geoip_provider" gorm:"type:varchar(20);default:'mmdb'"` // mmdb, bilibili, ip-api. 暂时只实现了mmdb // OAuth 配置 OAuthClientID string `json:"oauth_id" gorm:"type:varchar(255);not null"` OAuthClientSecret string `json:"oauth_secret" gorm:"type:varchar(255);not null"` - OAuthRedirectURI string `json:"oauth_redirect_uri" gorm:"type:varchar(255);not null"` OAuthEnabled bool `json:"oauth_enable" gorm:"default:false"` // 自定义美化 - CustomCSS string `json:"custom_css" gorm:"type:longtext"` - CustomJS string `json:"custom_js" gorm:"type:longtext"` - CreatedAt time.Time - UpdatedAt time.Time + CustomHead string `json:"custom_head" gorm:"type:longtext"` + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/go.mod b/go.mod index 4ce4ca2..8324f62 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/oschwald/maxminddb-golang v1.13.1 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 gorm.io/driver/sqlite v1.5.7 diff --git a/go.sum b/go.sum index 48cbada..4760414 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= +github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/public/public.go b/public/public.go index 3d8e019..c1437f2 100644 --- a/public/public.go +++ b/public/public.go @@ -47,8 +47,7 @@ func initIndex() { } func UpdateIndex(cfg models.Config) { replaceMap := map[string]string{ - "": cfg.CustomCSS, - "": cfg.CustomJS, + "": cfg.CustomHead, } for k, v := range replaceMap { IndexFile = strings.Replace(RawIndexFile, k, v, 1) diff --git a/utils/geoip/geoip.go b/utils/geoip/geoip.go new file mode 100644 index 0000000..c1035b9 --- /dev/null +++ b/utils/geoip/geoip.go @@ -0,0 +1,107 @@ +package geoip + +import ( + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "strings" + "unicode" + + "github.com/oschwald/maxminddb-golang" +) + +var ( + GeoIpUrl = "https://gh-proxy.com/raw.githubusercontent.com/Loyalsoldier/geoip/release/GeoLite2-Country.mmdb" + GeoIpFilePath = "./data/GeoLite2-Country.mmdb" + geoIpDb *maxminddb.Reader = nil +) + +type GeoIpRecord struct { + Country struct { + ISOCode string `maxminddb:"iso_code"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"country"` +} + +// 更新Geoip数据库,使用 GeoIpUrl下载最新的数据库文件,并覆盖本地的 GeoIpFilePath 文件 +func UpdateGeoIpDatabase() error { + if geoIpDb != nil { + geoIpDb.Close() + geoIpDb = nil + } + log.Println("Downloading GeoIP database...") + resp, err := http.Get(GeoIpUrl) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download file: %s", resp.Status) + } + + out, err := os.Create(GeoIpFilePath) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + return nil +} + +func InitGeoIp() { + if os.MkdirAll("./data", os.ModePerm) != nil { + return + } + if _, err := os.Stat(GeoIpFilePath); os.IsNotExist(err) { + err := UpdateGeoIpDatabase() + if err != nil { + fmt.Println("Error updating GeoIP database:", err) + } else { + fmt.Println("GeoIP database updated successfully.") + } + } + var err error + geoIpDb, err = maxminddb.Open(GeoIpFilePath) + if err != nil { + log.Printf("Error opening GeoIP database: %v", err) + } +} + +func GetGeoIpInfo(ip net.IP) (*GeoIpRecord, error) { + if geoIpDb == nil { + InitGeoIp() + } + if ip == nil { + return nil, fmt.Errorf("IP address is nil") + } + var record GeoIpRecord + err := geoIpDb.Lookup(ip, &record) + if err != nil { + log.Printf("Error looking up IP %s: %v", ip.String(), err) + return nil, err + } + return &record, nil +} + +func GetCountryUnicodeEmoji(isoCode string) string { + if len(isoCode) != 2 { + return "" + } + isoCode = strings.ToUpper(isoCode) + + if !unicode.IsLetter(rune(isoCode[0])) || !unicode.IsLetter(rune(isoCode[1])) { + return "" + } + + rune1 := rune(0x1F1E6 + (rune(isoCode[0]) - 'A')) + rune2 := rune(0x1F1E6 + (rune(isoCode[1]) - 'A')) + return string(rune1) + string(rune2) +} diff --git a/utils/geoip/geoip_test.go b/utils/geoip/geoip_test.go new file mode 100644 index 0000000..14026d0 --- /dev/null +++ b/utils/geoip/geoip_test.go @@ -0,0 +1,66 @@ +package geoip_test + +import ( + "net" + "os" + "testing" + + "github.com/komari-monitor/komari/utils/geoip" +) + +// 测试GeoIP数据库的初始化和更新功能 +func TestInitGeoIp(t *testing.T) { + geoip.InitGeoIp() + + // 检查数据库 + fileInfo, err := os.Stat(geoip.GeoIpFilePath) + if err != nil { + t.Errorf("Failed to get file info: %v", err) + } + if fileInfo.Size() == 0 { + t.Errorf("GeoIP database file is empty: %s", geoip.GeoIpFilePath) + } + + // IPv4 + ipaddr := "8.8.8.8" + ip := net.ParseIP(ipaddr) + record, err := geoip.GetGeoIpInfo(ip) + if err != nil { + t.Errorf("Failed to get GeoIP info for IP %s: %v", ipaddr, err) + } + + if record != nil { + if record.Country.ISOCode == "" && record.Country.Names["zh-CN"] == "" { + t.Errorf("Country information is missing for IP %s", ipaddr) + } + } else { + t.Errorf("GeoIP record is nil for IP %s", ipaddr) + } + + t.Logf("IPv4:[%s]%s - %s", ipaddr, record.Country.ISOCode, record.Country.Names["zh-CN"]) + + // IPv6 + ipaddr = "2001:4860:4860::8888" + ip = net.ParseIP(ipaddr) + record, err = geoip.GetGeoIpInfo(ip) + if err != nil { + t.Errorf("Failed to get GeoIP info for IPv6 %s: %v", ipaddr, err) + } + if record != nil { + if record.Country.ISOCode == "" && record.Country.Names["zh-CN"] == "" { + t.Errorf("Country information is missing for IPv6 %s", ipaddr) + } + } else { + t.Errorf("GeoIP record is nil for IPv6 %s", ipaddr) + } + t.Logf("IPv6:[%s]%s - %s", ipaddr, record.Country.ISOCode, record.Country.Names["zh-CN"]) +} + +func TestUnicodeEmoji(t *testing.T) { + ISOCode := "CN" + emoji := geoip.GetCountryUnicodeEmoji(ISOCode) + if emoji != "🇨🇳" { + t.Errorf("Expected emoji for %s, got %s", ISOCode, emoji) + } + t.Logf("Emoji for %s: %s", ISOCode, emoji) +}