feat: #11 添加Geoip,Custom CSS、JS 合并为CustomHead

This commit is contained in:
Akizon77
2025-05-03 20:41:04 +08:00
parent 204c434764
commit d0b94abeb5
8 changed files with 206 additions and 7 deletions
+2
View File
@@ -8,8 +8,10 @@
*.so
*.dylib
# local development data
komari.db
/data
/utils/geoip/data
.vscode/
# Test binary, built with `go test -c`
+21
View File
@@ -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
+6 -5
View File
@@ -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
}
+1
View File
@@ -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
+2
View File
@@ -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=
+1 -2
View File
@@ -47,8 +47,7 @@ func initIndex() {
}
func UpdateIndex(cfg models.Config) {
replaceMap := map[string]string{
"<!-- customize css -->": cfg.CustomCSS,
"<!-- customize js -->": cfg.CustomJS,
"<!-- customize head -->": cfg.CustomHead,
}
for k, v := range replaceMap {
IndexFile = strings.Replace(RawIndexFile, k, v, 1)
+107
View File
@@ -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)
}
+66
View File
@@ -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)
}