mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 00:26:37 +08:00
fix(openflare): Agent 上报 IP 后由 Server 自动解析节点地理位置
- 启动时按 of_options.GeoIPProvider 初始化 pkg/geoip(bootstrap + runtime) - mmdb 模式从内置 GeoLite2 种子到 data/;保存归属方式后热刷新 Provider - Agent/Relay 心跳在服务端根据 IP 写入 geo 字段,尊重 geo_manual_override - ipinfo 归属名称改为 City, Region, Country 可读格式
This commit is contained in:
@@ -18,6 +18,8 @@ sidebar: false
|
|||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
|
- 修复 Agent 心跳上报公网 IP 后节点地理位置未自动更新:进程启动时按 `GeoIPProvider` 初始化 `pkg/geoip`,`mmdb` 模式从内置 GeoLite2 种子到 `data/`,并在 Relay 心跳同步地理位置。
|
||||||
|
|
||||||
- 修复 Agent 启动时 Pages 部署包下载失败:Pages 部署包统一下载走 upload 文件存储框架,部署记录持久化 `upload_id`,legacy `artifact_path` 仅用于一次性回填 upload。
|
- 修复 Agent 启动时 Pages 部署包下载失败:Pages 部署包统一下载走 upload 文件存储框架,部署记录持久化 `upload_id`,legacy `artifact_path` 仅用于一次性回填 upload。
|
||||||
|
|
||||||
- 修复登录 Cap 人机验证:前端 `cap-solver` 与 Cap 路由测试对齐 `b3a55d4` 之后的统一 API 信封 `{ error_msg, data }`,避免 `challenge` 解构失败。
|
- 修复登录 Cap 人机验证:前端 `cap-solver` 与 Cap 路由测试对齐 `b3a55d4` 之后的统一 API 信封 `{ error_msg, data }`,避免 `challenge` 解构失败。
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (e payloadError) Error() string { return string(e) }
|
|||||||
|
|
||||||
func errPayload(message string) error { return payloadError(message) }
|
func errPayload(message string) error { return payloadError(message) }
|
||||||
|
|
||||||
func applyNodeRuntime(node *model.OpenFlareNode, payload NodePayload, preserveName bool) {
|
func applyNodeRuntime(ctx context.Context, node *model.OpenFlareNode, payload NodePayload, preserveName bool) {
|
||||||
if !preserveName || strings.TrimSpace(node.Name) == "" {
|
if !preserveName || strings.TrimSpace(node.Name) == "" {
|
||||||
if strings.TrimSpace(payload.Name) != "" {
|
if strings.TrimSpace(payload.Name) != "" {
|
||||||
node.Name = strings.TrimSpace(payload.Name)
|
node.Name = strings.TrimSpace(payload.Name)
|
||||||
@@ -101,31 +101,7 @@ func applyNodeRuntime(node *model.OpenFlareNode, payload NodePayload, preserveNa
|
|||||||
node.LastSeenAt = &now
|
node.LastSeenAt = &now
|
||||||
node.LastError = truncateForDatabase(payload.LastError, maxDatabaseTextLength)
|
node.LastError = truncateForDatabase(payload.LastError, maxDatabaseTextLength)
|
||||||
if !node.GeoManualOverride {
|
if !node.GeoManualOverride {
|
||||||
applyGeoInfoFromIP(node, node.IP)
|
ofgeoip.ApplyNodeGeoFromIP(ctx, node, node.IP)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyGeoInfoFromIP(node *model.OpenFlareNode, rawIP string) {
|
|
||||||
if node == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
node.GeoName = ""
|
|
||||||
node.GeoLatitude = nil
|
|
||||||
node.GeoLongitude = nil
|
|
||||||
ip := net.ParseIP(strings.TrimSpace(rawIP))
|
|
||||||
if ip == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
info, err := ofgeoip.GeoInfoFromIP(ip)
|
|
||||||
if err != nil || 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
ofgeoip "github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip"
|
pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip"
|
||||||
)
|
)
|
||||||
@@ -48,7 +50,7 @@ func TestApplyGeoInfoFromIP(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
node := &model.OpenFlareNode{IP: "203.0.113.10"}
|
node := &model.OpenFlareNode{IP: "203.0.113.10"}
|
||||||
applyGeoInfoFromIP(node, node.IP)
|
ofgeoip.ApplyNodeGeoFromIP(context.Background(), node, node.IP)
|
||||||
|
|
||||||
if node.GeoName != "Shanghai" {
|
if node.GeoName != "Shanghai" {
|
||||||
t.Fatalf("expected geo_name Shanghai, got %q", node.GeoName)
|
t.Fatalf("expected geo_name Shanghai, got %q", node.GeoName)
|
||||||
@@ -70,7 +72,7 @@ func TestApplyGeoInfoFromIPSkipsInvalidIP(t *testing.T) {
|
|||||||
GeoLatitude: geoipFloat(1),
|
GeoLatitude: geoipFloat(1),
|
||||||
GeoLongitude: geoipFloat(2),
|
GeoLongitude: geoipFloat(2),
|
||||||
}
|
}
|
||||||
applyGeoInfoFromIP(node, "not-an-ip")
|
ofgeoip.ApplyNodeGeoFromIP(context.Background(), node, "not-an-ip")
|
||||||
|
|
||||||
if node.GeoName != "" || node.GeoLatitude != nil || node.GeoLongitude != nil {
|
if node.GeoName != "" || node.GeoLatitude != nil || node.GeoLongitude != nil {
|
||||||
t.Fatalf("expected geo fields to be cleared on invalid IP, got %+v", node)
|
t.Fatalf("expected geo fields to be cleared on invalid IP, got %+v", node)
|
||||||
@@ -90,7 +92,7 @@ func TestApplyNodeRuntimeRespectsGeoManualOverride(t *testing.T) {
|
|||||||
GeoLatitude: geoipFloat(10),
|
GeoLatitude: geoipFloat(10),
|
||||||
GeoLongitude: geoipFloat(20),
|
GeoLongitude: geoipFloat(20),
|
||||||
}
|
}
|
||||||
applyNodeRuntime(node, NodePayload{
|
applyNodeRuntime(context.Background(), node, NodePayload{
|
||||||
IP: "203.0.113.10",
|
IP: "203.0.113.10",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
}, true)
|
}, true)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
ofgeoip "github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/node"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/node"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
|
|
||||||
// RegisterWithAccessToken registers an agent on a reserved node token.
|
// RegisterWithAccessToken registers an agent on a reserved node token.
|
||||||
func RegisterWithAccessToken(ctx context.Context, authNode *model.OpenFlareNode, payload NodePayload) (*RegistrationResponse, error) {
|
func RegisterWithAccessToken(ctx context.Context, authNode *model.OpenFlareNode, payload NodePayload) (*RegistrationResponse, error) {
|
||||||
|
_ = ofgeoip.EnsureRuntimeProvider(ctx)
|
||||||
payload = normalizeNodePayload(payload)
|
payload = normalizeNodePayload(payload)
|
||||||
if authNode == nil {
|
if authNode == nil {
|
||||||
return nil, errors.New(errNodeNotFound)
|
return nil, errors.New(errNodeNotFound)
|
||||||
@@ -24,7 +26,7 @@ func RegisterWithAccessToken(ctx context.Context, authNode *model.OpenFlareNode,
|
|||||||
if err := validateNodePayload(payload); err != nil {
|
if err := validateNodePayload(payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
applyNodeRuntime(authNode, payload, true)
|
applyNodeRuntime(ctx, authNode, payload, true)
|
||||||
if err := model.SaveOpenFlareNode(ctx, authNode); err != nil {
|
if err := model.SaveOpenFlareNode(ctx, authNode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -38,6 +40,7 @@ func RegisterWithAccessToken(ctx context.Context, authNode *model.OpenFlareNode,
|
|||||||
|
|
||||||
// RegisterWithDiscovery registers a new node using the global discovery token.
|
// RegisterWithDiscovery registers a new node using the global discovery token.
|
||||||
func RegisterWithDiscovery(ctx context.Context, payload NodePayload) (*RegistrationResponse, error) {
|
func RegisterWithDiscovery(ctx context.Context, payload NodePayload) (*RegistrationResponse, error) {
|
||||||
|
_ = ofgeoip.EnsureRuntimeProvider(ctx)
|
||||||
payload = normalizeNodePayload(payload)
|
payload = normalizeNodePayload(payload)
|
||||||
if err := validateNodePayload(payload); err != nil {
|
if err := validateNodePayload(payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -66,7 +69,7 @@ func RegisterWithDiscovery(ctx context.Context, payload NodePayload) (*Registrat
|
|||||||
CapabilitiesJSON: "[]",
|
CapabilitiesJSON: "[]",
|
||||||
UpdateChannel: releaseChannelStable,
|
UpdateChannel: releaseChannelStable,
|
||||||
}
|
}
|
||||||
applyNodeRuntime(record, payload, false)
|
applyNodeRuntime(ctx, record, payload, false)
|
||||||
|
|
||||||
if err = model.CreateOpenFlareNode(ctx, record); err != nil {
|
if err = model.CreateOpenFlareNode(ctx, record); err != nil {
|
||||||
if isUniqueConstraintError(err) {
|
if isUniqueConstraintError(err) {
|
||||||
@@ -84,6 +87,7 @@ func RegisterWithDiscovery(ctx context.Context, payload NodePayload) (*Registrat
|
|||||||
|
|
||||||
// HeartbeatNode updates runtime state and returns agent settings.
|
// HeartbeatNode updates runtime state and returns agent settings.
|
||||||
func HeartbeatNode(ctx context.Context, authNode *model.OpenFlareNode, payload NodePayload) (*HeartbeatResponse, error) {
|
func HeartbeatNode(ctx context.Context, authNode *model.OpenFlareNode, payload NodePayload) (*HeartbeatResponse, error) {
|
||||||
|
_ = ofgeoip.EnsureRuntimeProvider(ctx)
|
||||||
if authNode == nil {
|
if authNode == nil {
|
||||||
return nil, errors.New(errNodeNotFound)
|
return nil, errors.New(errNodeNotFound)
|
||||||
}
|
}
|
||||||
@@ -99,7 +103,7 @@ func HeartbeatNode(ctx context.Context, authNode *model.OpenFlareNode, payload N
|
|||||||
updateChannel := strings.TrimSpace(authNode.UpdateChannel)
|
updateChannel := strings.TrimSpace(authNode.UpdateChannel)
|
||||||
updateTag := strings.TrimSpace(authNode.UpdateTag)
|
updateTag := strings.TrimSpace(authNode.UpdateTag)
|
||||||
|
|
||||||
applyNodeRuntime(authNode, payload, true)
|
applyNodeRuntime(ctx, authNode, payload, true)
|
||||||
authNode.UpdateRequested = false
|
authNode.UpdateRequested = false
|
||||||
authNode.UpdateChannel = releaseChannelStable
|
authNode.UpdateChannel = releaseChannelStable
|
||||||
authNode.UpdateTag = ""
|
authNode.UpdateTag = ""
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package geoip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneCoordinate(value *float64) *float64 {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cloned := *value
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package geoip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/geoipdata"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
serverMMDBRelativePath = "data/GeoLite2-Country.mmdb"
|
||||||
|
serverMMDBDirPerm = 0o750
|
||||||
|
serverMMDBFilePerm = 0o644
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
runtimeOnce sync.Once
|
||||||
|
runtimeInitErr error
|
||||||
|
currentProviderMu sync.RWMutex
|
||||||
|
currentProvider string
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnsureRuntimeProvider loads OpenFlare options once and configures pkg/geoip.
|
||||||
|
func EnsureRuntimeProvider(ctx context.Context) error {
|
||||||
|
runtimeOnce.Do(func() {
|
||||||
|
if err := model.InitOptionMap(ctx); err != nil {
|
||||||
|
runtimeInitErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runtimeInitErr = applyProviderFromModel()
|
||||||
|
})
|
||||||
|
return runtimeInitErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshRuntimeProvider reapplies GeoIPProvider after option updates.
|
||||||
|
func RefreshRuntimeProvider(ctx context.Context) error {
|
||||||
|
if err := model.InitOptionMap(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return applyProviderFromModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyProviderFromModel() error {
|
||||||
|
model.OptionMapRWMutex.RLock()
|
||||||
|
provider := strings.TrimSpace(model.GeoIPProvider)
|
||||||
|
model.OptionMapRWMutex.RUnlock()
|
||||||
|
return ApplyProvider(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyProvider switches the process-wide GeoIP backend.
|
||||||
|
func ApplyProvider(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(context.Background(), "[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
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := fs.ReadFile(geoipdata.FS, geoipdata.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{}
|
||||||
|
runtimeInitErr = nil
|
||||||
|
currentProviderMu.Lock()
|
||||||
|
currentProvider = ""
|
||||||
|
currentProviderMu.Unlock()
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package geoip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip"
|
||||||
|
"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.OpenFlareOption{}); err != nil {
|
||||||
|
t.Fatalf("migrate: %v", err)
|
||||||
|
}
|
||||||
|
db.SetDB(sqliteDB)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
db.SetDB(nil)
|
||||||
|
model.ResetOptionMapForTest()
|
||||||
|
ResetRuntimeForTest()
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
model.ResetOptionMapForTest()
|
||||||
|
ResetRuntimeForTest()
|
||||||
|
if err := model.UpdateOpenFlareOption(ctx, "GeoIPProvider", pkggeoip.ProviderIPInfo); err != nil {
|
||||||
|
t.Fatalf("update option: %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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,7 +136,15 @@ func updateOptions(ctx context.Context, options []model.OpenFlareOption) error {
|
|||||||
if err := validateOptions(options); err != nil {
|
if err := validateOptions(options); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return model.UpdateOpenFlareOptions(ctx, options)
|
if err := model.UpdateOpenFlareOptions(ctx, options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, item := range options {
|
||||||
|
if item.Key == "GeoIPProvider" {
|
||||||
|
return geoip.RefreshRuntimeProvider(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNotice(ctx context.Context) (string, error) {
|
func getNotice(ctx context.Context) (string, error) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
||||||
|
ofgeoip "github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
)
|
)
|
||||||
@@ -52,6 +53,21 @@ func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload Heartbeat
|
|||||||
changes["ip"] = payload.IP
|
changes["ip"] = payload.IP
|
||||||
node.IP = payload.IP
|
node.IP = payload.IP
|
||||||
}
|
}
|
||||||
|
if !node.GeoManualOverride {
|
||||||
|
beforeGeo := node.GeoName
|
||||||
|
beforeLat := node.GeoLatitude
|
||||||
|
beforeLon := node.GeoLongitude
|
||||||
|
ofgeoip.ApplyNodeGeoFromIP(ctx, node, node.IP)
|
||||||
|
if node.GeoName != beforeGeo {
|
||||||
|
changes["geo_name"] = node.GeoName
|
||||||
|
}
|
||||||
|
if !coordinatesEqual(beforeLat, node.GeoLatitude) {
|
||||||
|
changes["geo_latitude"] = node.GeoLatitude
|
||||||
|
}
|
||||||
|
if !coordinatesEqual(beforeLon, node.GeoLongitude) {
|
||||||
|
changes["geo_longitude"] = node.GeoLongitude
|
||||||
|
}
|
||||||
|
}
|
||||||
if !previous.UpdateRequested {
|
if !previous.UpdateRequested {
|
||||||
delete(changes, "update_requested")
|
delete(changes, "update_requested")
|
||||||
}
|
}
|
||||||
@@ -86,3 +102,10 @@ func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload Heartbeat
|
|||||||
RelaySettings: BuildSettings(node, updateNow, updateChannel, updateTag),
|
RelaySettings: BuildSettings(node, updateNow, updateChannel, updateTag),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func coordinatesEqual(before *float64, after *float64) bool {
|
||||||
|
if before == nil || after == nil {
|
||||||
|
return before == after
|
||||||
|
}
|
||||||
|
return *before == *after
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
admin_push "github.com/Rain-kl/Wavelet/internal/apps/admin/push"
|
admin_push "github.com/Rain-kl/Wavelet/internal/apps/admin/push"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/admin/push/custom_events"
|
"github.com/Rain-kl/Wavelet/internal/apps/admin/push/custom_events"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/chwriter"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/chwriter"
|
||||||
|
ofgeoip "github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/risk_control"
|
"github.com/Rain-kl/Wavelet/internal/apps/risk_control"
|
||||||
"github.com/Rain-kl/Wavelet/internal/lifecycle"
|
"github.com/Rain-kl/Wavelet/internal/lifecycle"
|
||||||
taskhandlers "github.com/Rain-kl/Wavelet/internal/task/handlers"
|
taskhandlers "github.com/Rain-kl/Wavelet/internal/task/handlers"
|
||||||
@@ -80,6 +81,9 @@ func RegisterAll() {
|
|||||||
// Call from cmd entry points after wiring registration and database migration, not from router.
|
// Call from cmd entry points after wiring registration and database migration, not from router.
|
||||||
func Init(ctx context.Context, opts Options) {
|
func Init(ctx context.Context, opts Options) {
|
||||||
initRuntimeOnce.Do(func() {
|
initRuntimeOnce.Do(func() {
|
||||||
|
if err := ofgeoip.EnsureRuntimeProvider(ctx); err != nil {
|
||||||
|
logger.ErrorF(ctx, "[Bootstrap] init GeoIP provider failed: %v", err)
|
||||||
|
}
|
||||||
if err := admin_push.SyncEvents(ctx); err != nil {
|
if err := admin_push.SyncEvents(ctx); err != nil {
|
||||||
logger.ErrorF(ctx, "[Bootstrap] sync push events failed: %v", err)
|
logger.ErrorF(ctx, "[Bootstrap] sync push events failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -98,4 +102,4 @@ func Stop(ctx context.Context) {
|
|||||||
// ResetInitRuntimeOnceForTest clears initRuntimeOnce so Init can run again in unit tests.
|
// ResetInitRuntimeOnceForTest clears initRuntimeOnce so Init can run again in unit tests.
|
||||||
func ResetInitRuntimeOnceForTest() {
|
func ResetInitRuntimeOnceForTest() {
|
||||||
initRuntimeOnce = sync.Once{}
|
initRuntimeOnce = sync.Once{}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -81,9 +81,10 @@ func (s *IPInfoService) GetGeoInfo(ip net.IP) (*GeoInfo, error) {
|
|||||||
// 实际上,IPinfo 的 'country' 字段就是 ISO 2-letter code。
|
// 实际上,IPinfo 的 'country' 字段就是 ISO 2-letter code。
|
||||||
// 如果需要完整的国家名称,可能需要一个本地的 ISO 代码到名称的映射。
|
// 如果需要完整的国家名称,可能需要一个本地的 ISO 代码到名称的映射。
|
||||||
// 为了与 GetRegionUnicodeEmoji 函数兼容,我们直接使用 country 作为 ISOCode。
|
// 为了与 GetRegionUnicodeEmoji 函数兼容,我们直接使用 country 作为 ISOCode。
|
||||||
|
name := formatIPInfoLocation(apiResp)
|
||||||
return &GeoInfo{
|
return &GeoInfo{
|
||||||
ISOCode: apiResp.Country,
|
ISOCode: apiResp.Country,
|
||||||
Name: apiResp.Country,
|
Name: name,
|
||||||
Latitude: latitude,
|
Latitude: latitude,
|
||||||
Longitude: longitude,
|
Longitude: longitude,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -115,3 +116,20 @@ func parseIPInfoCoordinates(value string) (*float64, *float64) {
|
|||||||
|
|
||||||
return float64Pointer(latitudeValue), float64Pointer(longitudeValue)
|
return float64Pointer(latitudeValue), float64Pointer(longitudeValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatIPInfoLocation(resp ipInfoResponse) string {
|
||||||
|
parts := make([]string, 0, 3)
|
||||||
|
if city := strings.TrimSpace(resp.City); city != "" {
|
||||||
|
parts = append(parts, city)
|
||||||
|
}
|
||||||
|
if region := strings.TrimSpace(resp.Region); region != "" {
|
||||||
|
parts = append(parts, region)
|
||||||
|
}
|
||||||
|
if country := strings.TrimSpace(resp.Country); country != "" {
|
||||||
|
parts = append(parts, country)
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user