mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
refactor(backend): rename OpenFlare directory to lowercase openflare
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package data embeds the MaxMind GeoLite2 Country database for the control plane.
|
||||
//
|
||||
// Server keeps a Country-only embed so MaxMind provider can seed without network.
|
||||
// Agent does NOT use this package — Agent MMDB files are image COPY / download only.
|
||||
package data
|
||||
|
||||
import "embed"
|
||||
|
||||
// FS holds the embedded GeoLite2-Country.mmdb database.
|
||||
//
|
||||
//go:embed GeoLite2-Country.mmdb
|
||||
var FS embed.FS
|
||||
|
||||
// DefaultMMDBName is the filename of the embedded MaxMind Country database.
|
||||
const DefaultMMDBName = "GeoLite2-Country.mmdb"
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
|
||||
func TestEmbeddedCountryDatabaseIsValid(t *testing.T) {
|
||||
raw, err := fs.ReadFile(FS, DefaultMMDBName)
|
||||
if err != nil {
|
||||
t.Fatalf("read embedded Country database: %v", err)
|
||||
}
|
||||
reader, err := maxminddb.FromBytes(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("open embedded Country database: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
if !strings.Contains(reader.Metadata.DatabaseType, "Country") {
|
||||
t.Fatalf("unexpected database type %q", reader.Metadata.DatabaseType)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package geoip provides OpenFlare-compatible GeoIP lookup helpers.
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
pkggeoip "Wavelet/openflare/share/geoip"
|
||||
)
|
||||
|
||||
// LookupView is the legacy OpenFlare GeoIP lookup response shape.
|
||||
type LookupView struct {
|
||||
Provider string `json:"provider"`
|
||||
IP string `json:"ip"`
|
||||
ISOCode string `json:"iso_code"`
|
||||
Name string `json:"name"`
|
||||
Latitude *float64 `json:"latitude,omitempty"`
|
||||
Longitude *float64 `json:"longitude,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
errProviderInvalid = "归属方式仅支持 disabled、mmdb、ip-api、geojs、ipinfo"
|
||||
errIPEmpty = "IP 不能为空"
|
||||
errIPInvalid = "IP 格式无效"
|
||||
errLookupEmpty = "未获取到 IP 归属结果"
|
||||
)
|
||||
|
||||
// IsValidProvider reports whether provider is a supported GeoIP backend.
|
||||
func IsValidProvider(provider string) bool {
|
||||
return pkggeoip.IsValidProvider(provider)
|
||||
}
|
||||
|
||||
// GeoInfoFromIP resolves geographic information using the configured default provider.
|
||||
func GeoInfoFromIP(ip net.IP) (*pkggeoip.GeoInfo, error) {
|
||||
return pkggeoip.GetGeoInfo(ip)
|
||||
}
|
||||
|
||||
// Lookup resolves geographic information for rawIP using the given provider.
|
||||
func Lookup(provider, rawIP string) (*LookupView, error) {
|
||||
trimmedProvider := strings.TrimSpace(provider)
|
||||
if !pkggeoip.IsValidProvider(trimmedProvider) {
|
||||
return nil, errors.New(errProviderInvalid)
|
||||
}
|
||||
|
||||
trimmedIP := strings.TrimSpace(rawIP)
|
||||
if trimmedIP == "" {
|
||||
return nil, errors.New(errIPEmpty)
|
||||
}
|
||||
parsedIP := net.ParseIP(trimmedIP)
|
||||
if parsedIP == nil {
|
||||
return nil, errors.New(errIPInvalid)
|
||||
}
|
||||
|
||||
if trimmedProvider == pkggeoip.ProviderDisabled {
|
||||
return &LookupView{
|
||||
Provider: trimmedProvider,
|
||||
IP: parsedIP.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
info, err := pkggeoip.LookupGeoInfoWithProvider(trimmedProvider, parsedIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info == nil {
|
||||
return nil, errors.New(errLookupEmpty)
|
||||
}
|
||||
|
||||
return &LookupView{
|
||||
Provider: trimmedProvider,
|
||||
IP: parsedIP.String(),
|
||||
ISOCode: info.ISOCode,
|
||||
Name: info.Name,
|
||||
Latitude: info.Latitude,
|
||||
Longitude: info.Longitude,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
pkggeoip "Wavelet/openflare/share/geoip"
|
||||
)
|
||||
|
||||
type fakeLookupProvider struct{}
|
||||
|
||||
func (f *fakeLookupProvider) Name() string { return "fake-lookup" }
|
||||
|
||||
func (f *fakeLookupProvider) GetGeoInfo(_ net.IP) (*pkggeoip.GeoInfo, error) {
|
||||
lat := 37.7749
|
||||
lon := -122.4194
|
||||
return &pkggeoip.GeoInfo{
|
||||
ISOCode: "US",
|
||||
Name: "United States",
|
||||
Latitude: &lat,
|
||||
Longitude: &lon,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeLookupProvider) UpdateDatabase() error { return nil }
|
||||
|
||||
func (f *fakeLookupProvider) Close() error { return nil }
|
||||
|
||||
func TestLookupWithProvider(t *testing.T) {
|
||||
previousFactory := pkggeoip.ProviderFactoryForTest()
|
||||
pkggeoip.SetProviderFactoryForTest(func(provider string) (pkggeoip.Service, error) {
|
||||
return &fakeLookupProvider{}, nil
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
pkggeoip.SetProviderFactoryForTest(previousFactory)
|
||||
})
|
||||
|
||||
view, err := Lookup("ipinfo", "8.8.8.8")
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
if view.Provider != "ipinfo" || view.IP != "8.8.8.8" {
|
||||
t.Fatalf("unexpected lookup view: %+v", view)
|
||||
}
|
||||
if view.ISOCode != "US" || view.Name != "United States" {
|
||||
t.Fatalf("unexpected geo fields: %+v", view)
|
||||
}
|
||||
if view.Latitude == nil || view.Longitude == nil {
|
||||
t.Fatalf("expected coordinates, got %+v", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupRejectsInvalidInput(t *testing.T) {
|
||||
if _, err := Lookup("invalid", "8.8.8.8"); err == nil {
|
||||
t.Fatal("expected invalid provider to fail")
|
||||
}
|
||||
if _, err := Lookup("ipinfo", "not-an-ip"); err == nil {
|
||||
t.Fatal("expected invalid IP to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupDisabledProvider(t *testing.T) {
|
||||
view, err := Lookup("disabled", "8.8.8.8")
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
if view.Provider != "disabled" || view.IP != "8.8.8.8" {
|
||||
t.Fatalf("unexpected disabled view: %+v", view)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"Wavelet/openflare/plugins/server/kernel/model"
|
||||
pkggeoip "Wavelet/openflare/share/geoip"
|
||||
"Wavelet/pkg/logger"
|
||||
)
|
||||
|
||||
// ApplyNodeGeoFromIP resolves geographic metadata for node when geo is not manually locked.
|
||||
func ApplyNodeGeoFromIP(ctx context.Context, node *model.OpenFlareNode, rawIP string) {
|
||||
if node == nil || node.GeoManualOverride {
|
||||
return
|
||||
}
|
||||
node.GeoName = ""
|
||||
node.GeoLatitude = nil
|
||||
node.GeoLongitude = nil
|
||||
|
||||
ip := net.ParseIP(strings.TrimSpace(rawIP))
|
||||
if ip == nil {
|
||||
return
|
||||
}
|
||||
|
||||
info, err := GeoInfoFromIP(ip)
|
||||
if err != nil {
|
||||
logger.WarnF(ctx, "[GeoIP] resolve node geo failed: ip=%s error=%v", ip.String(), err)
|
||||
return
|
||||
}
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(info.Name) != "" {
|
||||
node.GeoName = strings.TrimSpace(info.Name)
|
||||
}
|
||||
if info.Latitude != nil && info.Longitude != nil {
|
||||
node.GeoLatitude = cloneCoordinate(info.Latitude)
|
||||
node.GeoLongitude = cloneCoordinate(info.Longitude)
|
||||
return
|
||||
}
|
||||
applyCountryCentroid(node, info.ISOCode, info.Name)
|
||||
}
|
||||
|
||||
func applyCountryCentroid(node *model.OpenFlareNode, isoCode, name string) {
|
||||
if lat, lon, ok := pkggeoip.CountryCentroidByISO(isoCode); ok {
|
||||
node.GeoLatitude = cloneFloat(lat)
|
||||
node.GeoLongitude = cloneFloat(lon)
|
||||
return
|
||||
}
|
||||
if lat, lon, ok := pkggeoip.CountryCentroidByName(name); ok {
|
||||
node.GeoLatitude = cloneFloat(lat)
|
||||
node.GeoLongitude = cloneFloat(lon)
|
||||
}
|
||||
}
|
||||
|
||||
func cloneFloat(value float64) *float64 {
|
||||
cloned := value
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneCoordinate(value *float64) *float64 {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *value
|
||||
return &cloned
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
geodata "Wavelet/openflare/plugins/server/kernel/geoip/data"
|
||||
"Wavelet/openflare/plugins/server/kernel/model"
|
||||
"Wavelet/openflare/plugins/server/kernel/repository"
|
||||
pkggeoip "Wavelet/openflare/share/geoip"
|
||||
"Wavelet/pkg/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
serverMMDBRelativePath = "data/GeoLite2-Country.mmdb"
|
||||
serverMMDBDirPerm = 0o750
|
||||
serverMMDBFilePerm = 0o644
|
||||
)
|
||||
|
||||
var (
|
||||
runtimeOnce sync.Once
|
||||
errRuntimeInit error
|
||||
currentProviderMu sync.RWMutex
|
||||
currentProvider string
|
||||
)
|
||||
|
||||
// EnsureRuntimeProvider loads GeoIP provider config from SystemConfig.
|
||||
func EnsureRuntimeProvider(ctx context.Context) error {
|
||||
runtimeOnce.Do(func() {
|
||||
errRuntimeInit = applyProviderFromSystemConfig(ctx)
|
||||
})
|
||||
return errRuntimeInit
|
||||
}
|
||||
|
||||
// RefreshRuntimeProvider reapplies GeoIPProvider after config updates.
|
||||
func RefreshRuntimeProvider(ctx context.Context) error {
|
||||
return applyProviderFromSystemConfig(ctx)
|
||||
}
|
||||
|
||||
func applyProviderFromSystemConfig(ctx context.Context) error {
|
||||
config, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyGeoIPProvider)
|
||||
if err != nil {
|
||||
// 降级到默认值
|
||||
return ApplyProvider(ctx, "ipinfo")
|
||||
}
|
||||
provider := strings.TrimSpace(config.Value)
|
||||
if provider == "" {
|
||||
provider = "ipinfo"
|
||||
}
|
||||
return ApplyProvider(ctx, provider)
|
||||
}
|
||||
|
||||
// ApplyProvider switches the process-wide GeoIP backend.
|
||||
func ApplyProvider(ctx context.Context, provider string) error {
|
||||
normalized := strings.TrimSpace(strings.ToLower(provider))
|
||||
if normalized == "" {
|
||||
normalized = pkggeoip.ProviderDisabled
|
||||
}
|
||||
|
||||
currentProviderMu.Lock()
|
||||
if currentProvider == normalized {
|
||||
currentProviderMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
currentProvider = normalized
|
||||
currentProviderMu.Unlock()
|
||||
|
||||
if normalized == pkggeoip.ProviderMaxMind {
|
||||
path, err := ensureServerMMDB()
|
||||
if err != nil {
|
||||
logger.WarnF(ctx, "[GeoIP] seed MaxMind database failed: %v", err)
|
||||
}
|
||||
if path != "" {
|
||||
pkggeoip.GeoIPFilePath = path
|
||||
}
|
||||
}
|
||||
|
||||
pkggeoip.InitGeoIP(normalized)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureServerMMDB() (string, error) {
|
||||
path, err := filepath.Abs(serverMMDBRelativePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, statErr := os.Stat(path)
|
||||
if statErr == nil {
|
||||
return path, nil
|
||||
}
|
||||
if !os.IsNotExist(statErr) {
|
||||
return "", statErr
|
||||
}
|
||||
|
||||
// Control plane: seed Country from embedded asset (no City; Agent uses disk/image).
|
||||
data, err := fs.ReadFile(geodata.FS, geodata.DefaultMMDBName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), serverMMDBDirPerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(path, data, serverMMDBFilePerm); err != nil { //nolint:gosec // world-readable mmdb
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// ResetRuntimeForTest clears lazy-init state for unit tests.
|
||||
func ResetRuntimeForTest() {
|
||||
runtimeOnce = sync.Once{}
|
||||
errRuntimeInit = nil
|
||||
currentProviderMu.Lock()
|
||||
currentProvider = ""
|
||||
currentProviderMu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"Wavelet/openflare/plugins/server/kernel/model"
|
||||
pkggeoip "Wavelet/openflare/share/geoip"
|
||||
db "Wavelet/plugins/infra/database"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestEnsureRuntimeProviderInitializesConfiguredProvider(t *testing.T) {
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := sqliteDB.AutoMigrate(&model.SystemConfig{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
db.SetDB(sqliteDB)
|
||||
t.Cleanup(func() {
|
||||
db.SetDB(nil)
|
||||
ResetRuntimeForTest()
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
ResetRuntimeForTest()
|
||||
// 通过 SystemConfig 设置 GeoIPProvider 配置
|
||||
if err := db.DB(ctx).Create(&model.SystemConfig{
|
||||
Key: model.ConfigKeyGeoIPProvider,
|
||||
Value: pkggeoip.ProviderIPInfo,
|
||||
Type: "business",
|
||||
Visibility: 0,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create system config: %v", err)
|
||||
}
|
||||
|
||||
if err := EnsureRuntimeProvider(ctx); err != nil {
|
||||
t.Fatalf("EnsureRuntimeProvider error = %v", err)
|
||||
}
|
||||
if pkggeoip.CurrentProvider == nil || pkggeoip.CurrentProvider.Name() == "EmptyProvider" {
|
||||
t.Fatalf("expected ipinfo provider, got %#v", pkggeoip.CurrentProvider)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user