管理端全部功能跑通

This commit is contained in:
robin
2026-02-27 10:35:22 +08:00
parent 4d275c921d
commit 150799f41d
263 changed files with 22664 additions and 4053 deletions

View File

@@ -17,6 +17,10 @@ func (this *AppAction) Init() {
func (this *AppAction) RunGet(params struct {
AppId int64
}) {
app := pickApp(params.AppId)
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
this.RedirectURL("/httpdns/apps/domains?appId=" + strconv.FormatInt(app.GetInt64("id"), 10))
}

View File

@@ -5,8 +5,9 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type AppSettingsAction struct {
@@ -22,33 +23,45 @@ func (this *AppSettingsAction) RunGet(params struct {
Section string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
// 顶部 tabbar
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "settings")
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "settings")
// 当前选中的 section
section := params.Section
if len(section) == 0 {
section = "basic"
}
this.Data["activeSection"] = section
appIdStr := strconv.FormatInt(params.AppId, 10)
this.Data["leftMenuItems"] = []map[string]interface{}{
appIDStr := strconv.FormatInt(app.GetInt64("id"), 10)
this.Data["leftMenuItems"] = []maps.Map{
{
"name": "基础配置",
"url": "/httpdns/apps/app/settings?appId=" + appIdStr + "&section=basic",
"url": "/httpdns/apps/app/settings?appId=" + appIDStr + "&section=basic",
"isActive": section == "basic",
},
{
"name": "认证与密钥",
"url": "/httpdns/apps/app/settings?appId=" + appIdStr + "&section=auth",
"url": "/httpdns/apps/app/settings?appId=" + appIDStr + "&section=auth",
"isActive": section == "auth",
},
}
settings := loadAppSettings(app)
this.Data["clusters"] = policies.LoadAvailableDeployClusters()
settings := maps.Map{
"appId": app.GetString("appId"),
"appStatus": app.GetBool("isOn"),
"primaryClusterId": app.GetInt64("primaryClusterId"),
"backupClusterId": app.GetInt64("backupClusterId"),
"signEnabled": app.GetBool("signEnabled"),
"signSecretPlain": app.GetString("signSecretPlain"),
"signSecretMasked": app.GetString("signSecretMasked"),
"signSecretUpdatedAt": app.GetString("signSecretUpdated"),
}
this.Data["app"] = app
this.Data["settings"] = settings
this.Show()
@@ -57,36 +70,37 @@ func (this *AppSettingsAction) RunGet(params struct {
func (this *AppSettingsAction) RunPost(params struct {
AppId int64
AppStatus bool
PrimaryClusterId int64
BackupClusterId int64
AppStatus bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "please select app")
params.Must.Field("primaryClusterId", params.PrimaryClusterId).Gt(0, "please select primary cluster")
if params.BackupClusterId > 0 && params.BackupClusterId == params.PrimaryClusterId {
this.FailField("backupClusterId", "backup cluster must be different from primary cluster")
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
appResp, err := this.RPC().HTTPDNSAppRPC().FindHTTPDNSApp(this.AdminContext(), &pb.FindHTTPDNSAppRequest{
AppDbId: params.AppId,
})
if err != nil {
this.ErrorPage(err)
return
}
if appResp.GetApp() == nil {
this.Fail("找不到对应的应用")
return
}
app := pickApp(params.AppId)
settings := loadAppSettings(app)
settings["appStatus"] = params.AppStatus
settings["primaryClusterId"] = params.PrimaryClusterId
settings["backupClusterId"] = params.BackupClusterId
_, err = this.RPC().HTTPDNSAppRPC().UpdateHTTPDNSApp(this.AdminContext(), &pb.UpdateHTTPDNSAppRequest{
AppDbId: params.AppId,
Name: appResp.GetApp().GetName(),
PrimaryClusterId: appResp.GetApp().GetPrimaryClusterId(),
BackupClusterId: appResp.GetApp().GetBackupClusterId(),
IsOn: params.AppStatus,
UserId: appResp.GetApp().GetUserId(),
})
if err != nil {
this.ErrorPage(err)
return
}
// SNI strategy is fixed to level2 empty.
settings["sniPolicy"] = "level2"
settings["level2Mode"] = "empty"
settings["publicSniDomain"] = ""
settings["echFallbackPolicy"] = "level2"
settings["ecsMode"] = "off"
settings["ecsIPv4Prefix"] = 24
settings["ecsIPv6Prefix"] = 56
settings["pinningMode"] = "off"
settings["sanMode"] = "off"
saveAppSettings(app.GetInt64("id"), settings)
this.Success()
}

View File

@@ -2,6 +2,7 @@ package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
@@ -13,11 +14,16 @@ func (this *AppSettingsResetSignSecretAction) RunPost(params struct {
AppId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
app := pickApp(params.AppId)
resetSignSecret(app)
_, err := this.RPC().HTTPDNSAppRPC().ResetHTTPDNSAppSignSecret(this.AdminContext(), &pb.ResetHTTPDNSAppSignSecretRequest{
AppDbId: params.AppId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -2,6 +2,7 @@ package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
@@ -14,14 +15,17 @@ func (this *AppSettingsToggleSignEnabledAction) RunPost(params struct {
IsOn int
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
app := pickApp(params.AppId)
settings := loadAppSettings(app)
settings["signEnabled"] = params.IsOn == 1
saveAppSettings(app.GetInt64("id"), settings)
_, err := this.RPC().HTTPDNSAppRPC().UpdateHTTPDNSAppSignEnabled(this.AdminContext(), &pb.UpdateHTTPDNSAppSignEnabledRequest{
AppDbId: params.AppId,
SignEnabled: params.IsOn == 1,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -1,223 +0,0 @@
package apps
import (
"fmt"
"sync"
"time"
"github.com/iwind/TeaGo/maps"
)
var appSettingsStore = struct {
sync.RWMutex
data map[int64]maps.Map
}{
data: map[int64]maps.Map{},
}
func defaultAppSettings(app maps.Map) maps.Map {
signSecretPlain := randomPlainSecret("ss")
return maps.Map{
"appId": app.GetString("appId"),
"primaryClusterId": app.GetInt64("clusterId"),
"backupClusterId": int64(0),
"signSecretPlain": signSecretPlain,
"signSecretMasked": maskSecret(signSecretPlain),
"signSecretUpdatedAt": "2026-02-20 12:30:00",
"appStatus": app.GetBool("isOn"),
"defaultTTL": 30,
"fallbackTimeoutMs": 300,
"sniPolicy": "level2",
"level2Mode": "empty",
"publicSniDomain": "",
"echFallbackPolicy": "level2",
"ecsMode": "off",
"ecsIPv4Prefix": 24,
"ecsIPv6Prefix": 56,
"pinningMode": "off",
"sanMode": "off",
"signEnabled": true,
}
}
func cloneSettings(settings maps.Map) maps.Map {
return maps.Map{
"appId": settings.GetString("appId"),
"primaryClusterId": settings.GetInt64("primaryClusterId"),
"backupClusterId": settings.GetInt64("backupClusterId"),
"signSecretPlain": settings.GetString("signSecretPlain"),
"signSecretMasked": settings.GetString("signSecretMasked"),
"signSecretUpdatedAt": settings.GetString("signSecretUpdatedAt"),
"appStatus": settings.GetBool("appStatus"),
"defaultTTL": settings.GetInt("defaultTTL"),
"fallbackTimeoutMs": settings.GetInt("fallbackTimeoutMs"),
"sniPolicy": settings.GetString("sniPolicy"),
"level2Mode": settings.GetString("level2Mode"),
"publicSniDomain": settings.GetString("publicSniDomain"),
"echFallbackPolicy": settings.GetString("echFallbackPolicy"),
"ecsMode": settings.GetString("ecsMode"),
"ecsIPv4Prefix": settings.GetInt("ecsIPv4Prefix"),
"ecsIPv6Prefix": settings.GetInt("ecsIPv6Prefix"),
"pinningMode": settings.GetString("pinningMode"),
"sanMode": settings.GetString("sanMode"),
"signEnabled": settings.GetBool("signEnabled"),
}
}
func loadAppSettings(app maps.Map) maps.Map {
appId := app.GetInt64("id")
appSettingsStore.RLock()
settings, ok := appSettingsStore.data[appId]
appSettingsStore.RUnlock()
if ok {
if ensureSettingsFields(settings) {
saveAppSettings(appId, settings)
}
return cloneSettings(settings)
}
settings = defaultAppSettings(app)
saveAppSettings(appId, settings)
return cloneSettings(settings)
}
func saveAppSettings(appId int64, settings maps.Map) {
appSettingsStore.Lock()
appSettingsStore.data[appId] = cloneSettings(settings)
appSettingsStore.Unlock()
}
func deleteAppSettings(appId int64) {
appSettingsStore.Lock()
delete(appSettingsStore.data, appId)
appSettingsStore.Unlock()
}
func resetSignSecret(app maps.Map) maps.Map {
settings := loadAppSettings(app)
signSecretPlain := randomPlainSecret("ss")
settings["signSecretPlain"] = signSecretPlain
settings["signSecretMasked"] = maskSecret(signSecretPlain)
settings["signSecretUpdatedAt"] = nowDateTime()
saveAppSettings(app.GetInt64("id"), settings)
return settings
}
func nowDateTime() string {
return time.Now().Format("2006-01-02 15:04:05")
}
func randomPlainSecret(prefix string) string {
suffix := time.Now().UnixNano() & 0xffff
return fmt.Sprintf("%s_%016x", prefix, suffix)
}
func maskSecret(secret string) string {
if len(secret) < 4 {
return "******"
}
prefix := ""
for i := 0; i < len(secret); i++ {
if secret[i] == '_' {
prefix = secret[:i+1]
break
}
}
if len(prefix) == 0 {
prefix = secret[:2]
}
if len(secret) <= 8 {
return prefix + "xxxx"
}
return prefix + "xxxxxxxx" + secret[len(secret)-4:]
}
func ensureSettingsFields(settings maps.Map) bool {
changed := false
if settings.GetInt64("primaryClusterId") <= 0 {
settings["primaryClusterId"] = int64(1)
changed = true
}
if settings.GetInt64("backupClusterId") < 0 {
settings["backupClusterId"] = int64(0)
changed = true
}
if settings.GetInt64("backupClusterId") > 0 && settings.GetInt64("backupClusterId") == settings.GetInt64("primaryClusterId") {
settings["backupClusterId"] = int64(0)
changed = true
}
signSecretPlain := settings.GetString("signSecretPlain")
if len(signSecretPlain) == 0 {
signSecretPlain = randomPlainSecret("ss")
settings["signSecretPlain"] = signSecretPlain
changed = true
}
if len(settings.GetString("signSecretMasked")) == 0 {
settings["signSecretMasked"] = maskSecret(signSecretPlain)
changed = true
}
if len(settings.GetString("signSecretUpdatedAt")) == 0 {
settings["signSecretUpdatedAt"] = nowDateTime()
changed = true
}
if len(settings.GetString("sniPolicy")) == 0 {
settings["sniPolicy"] = "level2"
changed = true
} else if settings.GetString("sniPolicy") != "level2" {
settings["sniPolicy"] = "level2"
changed = true
}
if settings.GetString("level2Mode") != "empty" {
settings["level2Mode"] = "empty"
changed = true
}
if len(settings.GetString("publicSniDomain")) > 0 {
settings["publicSniDomain"] = ""
changed = true
}
if len(settings.GetString("echFallbackPolicy")) == 0 {
settings["echFallbackPolicy"] = "level2"
changed = true
} else if settings.GetString("echFallbackPolicy") != "level2" {
settings["echFallbackPolicy"] = "level2"
changed = true
}
if settings.GetString("ecsMode") != "off" {
settings["ecsMode"] = "off"
changed = true
}
if settings.GetInt("ecsIPv4Prefix") <= 0 {
settings["ecsIPv4Prefix"] = 24
changed = true
}
if settings.GetInt("ecsIPv6Prefix") <= 0 {
settings["ecsIPv6Prefix"] = 56
changed = true
}
if settings.GetString("pinningMode") != "off" {
settings["pinningMode"] = "off"
changed = true
}
if settings.GetString("sanMode") != "off" {
settings["sanMode"] = "off"
changed = true
}
return changed
}
// LoadAppSettingsByAppID exposes app settings for other httpdns sub-modules
// such as sandbox mock responses.
func LoadAppSettingsByAppID(appID string) maps.Map {
for _, app := range mockApps() {
if app.GetString("appId") == appID {
return loadAppSettings(app)
}
}
return nil
}

View File

@@ -0,0 +1,92 @@
package apps
import (
"strconv"
"time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type CreateAction struct {
actionutils.ParentAction
}
func (this *CreateAction) Init() {
this.Nav("", "", "create")
}
func (this *CreateAction) RunGet(params struct{}) {
clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
clusters := make([]maps.Map, 0, len(clusterResp.GetClusters()))
for _, cluster := range clusterResp.GetClusters() {
clusters = append(clusters, maps.Map{
"id": cluster.GetId(),
"name": cluster.GetName(),
})
}
this.Data["clusters"] = clusters
defaultPrimaryClusterId := int64(0)
for _, cluster := range clusterResp.GetClusters() {
if cluster.GetIsDefault() {
defaultPrimaryClusterId = cluster.GetId()
break
}
}
if defaultPrimaryClusterId <= 0 && len(clusters) > 0 {
defaultPrimaryClusterId = clusters[0].GetInt64("id")
}
this.Data["defaultPrimaryClusterId"] = defaultPrimaryClusterId
defaultBackupClusterId := int64(0)
for _, cluster := range clusters {
clusterId := cluster.GetInt64("id")
if clusterId > 0 && clusterId != defaultPrimaryClusterId {
defaultBackupClusterId = clusterId
break
}
}
this.Data["defaultBackupClusterId"] = defaultBackupClusterId
this.Show()
}
func (this *CreateAction) RunPost(params struct {
Name string
PrimaryClusterId int64
BackupClusterId int64
UserId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("name", params.Name).Require("请输入应用名称")
params.Must.Field("primaryClusterId", params.PrimaryClusterId).Gt(0, "请输入主服务集群")
if params.BackupClusterId > 0 && params.BackupClusterId == params.PrimaryClusterId {
this.FailField("backupClusterId", "备用服务集群必须和主服务集群不一致")
}
createResp, err := this.RPC().HTTPDNSAppRPC().CreateHTTPDNSApp(this.AdminContext(), &pb.CreateHTTPDNSAppRequest{
Name: params.Name,
AppId: "app" + strconv.FormatInt(time.Now().UnixNano()%1_000_000_000_000, 36),
PrimaryClusterId: params.PrimaryClusterId,
BackupClusterId: params.BackupClusterId,
IsOn: true,
SignEnabled: true,
UserId: params.UserId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["appId"] = createResp.GetAppDbId()
this.Success()
}

View File

@@ -1,63 +0,0 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type CreatePopupAction struct {
actionutils.ParentAction
}
func (this *CreatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *CreatePopupAction) RunGet(params struct{}) {
clusters := policies.LoadAvailableDeployClusters()
this.Data["clusters"] = clusters
defaultPrimaryClusterId := policies.LoadDefaultClusterID()
if defaultPrimaryClusterId <= 0 && len(clusters) > 0 {
defaultPrimaryClusterId = clusters[0].GetInt64("id")
}
this.Data["defaultPrimaryClusterId"] = defaultPrimaryClusterId
defaultBackupClusterId := int64(0)
for _, cluster := range clusters {
clusterId := cluster.GetInt64("id")
if clusterId > 0 && clusterId != defaultPrimaryClusterId {
defaultBackupClusterId = clusterId
break
}
}
this.Data["defaultBackupClusterId"] = defaultBackupClusterId
// Mock users for dropdown
this.Data["users"] = []maps.Map{
{"id": int64(1), "name": "User A", "username": "zhangsan"},
{"id": int64(2), "name": "User B", "username": "lisi"},
{"id": int64(3), "name": "User C", "username": "wangwu"},
}
this.Show()
}
func (this *CreatePopupAction) RunPost(params struct {
Name string
PrimaryClusterId int64
BackupClusterId int64
UserId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("name", params.Name).Require("please input app name")
params.Must.Field("primaryClusterId", params.PrimaryClusterId).Gt(0, "please select primary cluster")
if params.BackupClusterId > 0 && params.BackupClusterId == params.PrimaryClusterId {
this.FailField("backupClusterId", "backup cluster must be different from primary cluster")
}
this.Success()
}

View File

@@ -20,25 +20,32 @@ func (this *CustomRecordsAction) RunGet(params struct {
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
// 自定义解析属于域名管理子页,顶部沿用应用 tabbar高亮域名列表
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "domains")
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "domains")
domains := mockDomains(app.GetInt64("id"))
domain := pickDomainFromDomains(domains, params.DomainId)
domainName := domain.GetString("name")
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
domain := findDomainMap(domains, params.DomainId)
records := make([]maps.Map, 0)
for _, record := range loadCustomRecords(app.GetInt64("id")) {
if len(domainName) > 0 && record.GetString("domain") != domainName {
continue
if domain.GetInt64("id") > 0 {
records, err = listCustomRuleMaps(this.Parent(), domain.GetInt64("id"))
if err != nil {
this.ErrorPage(err)
return
}
for _, record := range records {
record["domain"] = domain.GetString("name")
record["lineText"] = buildLineText(record)
record["recordValueText"] = buildRecordValueText(record)
}
records = append(records, record)
}
for _, record := range records {
record["lineText"] = buildLineText(record)
record["recordValueText"] = buildRecordValueText(record)
}
this.Data["app"] = app
@@ -47,17 +54,3 @@ func (this *CustomRecordsAction) RunGet(params struct {
this.Show()
}
func pickDomainFromDomains(domains []maps.Map, domainID int64) maps.Map {
if len(domains) == 0 {
return maps.Map{}
}
if domainID <= 0 {
return domains[0]
}
for _, domain := range domains {
if domain.GetInt64("id") == domainID {
return domain
}
}
return domains[0]
}

View File

@@ -6,6 +6,7 @@ import (
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
@@ -23,11 +24,18 @@ func (this *CustomRecordsCreatePopupAction) RunGet(params struct {
DomainId int64
RecordId int64
}) {
app := pickApp(params.AppId)
this.Data["app"] = app
domains := mockDomains(app.GetInt64("id"))
domain := pickDomainFromDomains(domains, params.DomainId)
this.Data["domain"] = domain
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
domain := findDomainMap(domains, params.DomainId)
record := maps.Map{
"id": int64(0),
@@ -45,77 +53,35 @@ func (this *CustomRecordsCreatePopupAction) RunGet(params struct {
"recordItemsJson": `[{"type":"A","value":"","weight":100}]`,
}
if params.RecordId > 0 {
existing := findCustomRecord(app.GetInt64("id"), params.RecordId)
if len(existing) > 0 {
record["id"] = existing.GetInt64("id")
if len(record.GetString("domain")) == 0 {
record["domain"] = existing.GetString("domain")
if params.RecordId > 0 && domain.GetInt64("id") > 0 {
rules, err := listCustomRuleMaps(this.Parent(), domain.GetInt64("id"))
if err != nil {
this.ErrorPage(err)
return
}
for _, rule := range rules {
if rule.GetInt64("id") != params.RecordId {
continue
}
record["lineScope"] = strings.TrimSpace(existing.GetString("lineScope"))
record["lineCarrier"] = strings.TrimSpace(existing.GetString("lineCarrier"))
record["lineRegion"] = strings.TrimSpace(existing.GetString("lineRegion"))
record["lineProvince"] = strings.TrimSpace(existing.GetString("lineProvince"))
record["lineContinent"] = strings.TrimSpace(existing.GetString("lineContinent"))
record["lineCountry"] = strings.TrimSpace(existing.GetString("lineCountry"))
record["ruleName"] = existing.GetString("ruleName")
record["weightEnabled"] = existing.GetBool("weightEnabled")
record["ttl"] = existing.GetInt("ttl")
record["isOn"] = existing.GetBool("isOn")
recordItems := make([]maps.Map, 0)
recordType := strings.ToUpper(strings.TrimSpace(existing.GetString("recordType")))
values, _ := existing["recordValues"].([]maps.Map)
for _, item := range values {
itemType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
if len(itemType) == 0 {
itemType = recordType
}
if itemType != "A" && itemType != "AAAA" {
itemType = "A"
}
recordItems = append(recordItems, maps.Map{
"type": itemType,
"value": strings.TrimSpace(item.GetString("value")),
"weight": item.GetInt("weight"),
})
}
if len(recordItems) == 0 {
recordItems = append(recordItems, maps.Map{
"type": "A",
"value": "",
"weight": 100,
})
}
record["recordItemsJson"] = marshalJSON(recordItems, "[]")
record["id"] = rule.GetInt64("id")
record["domain"] = domain.GetString("name")
record["lineScope"] = rule.GetString("lineScope")
record["lineCarrier"] = defaultLineField(rule.GetString("lineCarrier"))
record["lineRegion"] = defaultLineField(rule.GetString("lineRegion"))
record["lineProvince"] = defaultLineField(rule.GetString("lineProvince"))
record["lineContinent"] = defaultLineField(rule.GetString("lineContinent"))
record["lineCountry"] = defaultLineField(rule.GetString("lineCountry"))
record["ruleName"] = rule.GetString("ruleName")
record["weightEnabled"] = rule.GetBool("weightEnabled")
record["ttl"] = rule.GetInt("ttl")
record["isOn"] = rule.GetBool("isOn")
record["recordItemsJson"] = marshalJSON(rule["recordValues"], "[]")
break
}
}
if record.GetString("lineScope") != "china" && record.GetString("lineScope") != "overseas" {
if len(strings.TrimSpace(record.GetString("lineContinent"))) > 0 || len(strings.TrimSpace(record.GetString("lineCountry"))) > 0 {
record["lineScope"] = "overseas"
} else {
record["lineScope"] = "china"
}
}
if len(record.GetString("lineCarrier")) == 0 {
record["lineCarrier"] = "默认"
}
if len(record.GetString("lineRegion")) == 0 {
record["lineRegion"] = "默认"
}
if len(record.GetString("lineProvince")) == 0 {
record["lineProvince"] = "默认"
}
if len(record.GetString("lineContinent")) == 0 {
record["lineContinent"] = "默认"
}
if len(record.GetString("lineCountry")) == 0 {
record["lineCountry"] = "默认"
}
this.Data["app"] = app
this.Data["domain"] = domain
this.Data["record"] = record
this.Data["isEditing"] = params.RecordId > 0
this.Show()
@@ -138,40 +104,26 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
RuleName string
RecordItemsJSON string
WeightEnabled bool
TTL int
Ttl int
IsOn bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "please select app")
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
params.Must.Field("domainId", params.DomainId).Gt(0, "请选择所属域名")
params.Domain = strings.TrimSpace(params.Domain)
params.LineScope = strings.ToLower(strings.TrimSpace(params.LineScope))
params.RuleName = strings.TrimSpace(params.RuleName)
params.RecordItemsJSON = strings.TrimSpace(params.RecordItemsJSON)
domain := maps.Map{}
if params.DomainId > 0 {
domain = pickDomainFromDomains(mockDomains(params.AppId), params.DomainId)
}
if len(domain) > 0 {
params.Domain = strings.TrimSpace(domain.GetString("name"))
}
if len(params.Domain) == 0 {
this.Fail("please select domain")
return
}
if params.LineScope != "china" && params.LineScope != "overseas" {
params.LineScope = "china"
}
params.RuleName = strings.TrimSpace(params.RuleName)
if len(params.RuleName) == 0 {
this.Fail("please input rule name")
this.Fail("请输入规则名称")
return
}
if params.TTL <= 0 || params.TTL > 86400 {
this.Fail("ttl should be in 1-86400")
if params.Ttl <= 0 || params.Ttl > 86400 {
this.Fail("TTL值必须在 1-86400 范围内")
return
}
@@ -181,11 +133,11 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
return
}
if len(recordValues) == 0 {
this.Fail("please input record values")
this.Fail("请输入解析记录值")
return
}
if len(recordValues) > 10 {
this.Fail("record values should be <= 10")
this.Fail("单个规则最多只能添加 10 条解析记录")
return
}
@@ -209,7 +161,6 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
if len(lineCountry) == 0 {
lineCountry = "默认"
}
if params.LineScope == "overseas" {
lineCarrier = ""
lineRegion = ""
@@ -219,40 +170,57 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
lineCountry = ""
}
recordType := recordValues[0].GetString("type")
if len(recordType) == 0 {
recordType = "A"
records := make([]*pb.HTTPDNSRuleRecord, 0, len(recordValues))
for i, item := range recordValues {
records = append(records, &pb.HTTPDNSRuleRecord{
Id: 0,
RuleId: 0,
RecordType: item.GetString("type"),
RecordValue: item.GetString("value"),
Weight: int32(item.GetInt("weight")),
Sort: int32(i + 1),
})
}
saveCustomRecord(params.AppId, maps.Map{
"id": params.RecordId,
"domain": params.Domain,
"lineScope": params.LineScope,
"lineCarrier": lineCarrier,
"lineRegion": lineRegion,
"lineProvince": lineProvince,
"lineContinent": lineContinent,
"lineCountry": lineCountry,
"ruleName": params.RuleName,
"sdnsParams": []maps.Map{},
"recordType": recordType,
"recordValues": recordValues,
"weightEnabled": params.WeightEnabled,
"ttl": params.TTL,
"isOn": params.IsOn,
})
rule := &pb.HTTPDNSCustomRule{
Id: params.RecordId,
AppId: params.AppId,
DomainId: params.DomainId,
RuleName: params.RuleName,
LineScope: params.LineScope,
LineCarrier: lineCarrier,
LineRegion: lineRegion,
LineProvince: lineProvince,
LineContinent: lineContinent,
LineCountry: lineCountry,
Ttl: int32(params.Ttl),
IsOn: params.IsOn,
Priority: 100,
Records: records,
}
if params.RecordId > 0 {
err = updateCustomRule(this.Parent(), rule)
} else {
_, err = createCustomRule(this.Parent(), rule)
}
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}
func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
raw = strings.TrimSpace(raw)
if len(raw) == 0 {
return []maps.Map{}, nil
}
list := []maps.Map{}
if err := json.Unmarshal([]byte(raw), &list); err != nil {
return nil, fmt.Errorf("record items json is invalid")
return nil, fmt.Errorf("解析记录格式不正确")
}
result := make([]maps.Map, 0, len(list))
@@ -263,10 +231,10 @@ func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
continue
}
if recordType != "A" && recordType != "AAAA" {
return nil, fmt.Errorf("record type should be A or AAAA")
return nil, fmt.Errorf("记录类型只能是 A AAAA")
}
if len(recordValue) == 0 {
return nil, fmt.Errorf("record value should not be empty")
return nil, fmt.Errorf("记录值不能为空")
}
weight := item.GetInt("weight")
@@ -274,7 +242,7 @@ func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
weight = 100
}
if weight < 1 || weight > 100 {
return nil, fmt.Errorf("weight should be in 1-100")
return nil, fmt.Errorf("权重值必须在 1-100 之间")
}
result = append(result, maps.Map{
@@ -283,7 +251,6 @@ func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
"weight": weight,
})
}
return result, nil
}

View File

@@ -10,8 +10,12 @@ func (this *CustomRecordsDeleteAction) RunPost(params struct {
AppId int64
RecordId int64
}) {
if params.AppId > 0 && params.RecordId > 0 {
deleteCustomRecord(params.AppId, params.RecordId)
if params.RecordId > 0 {
err := deleteCustomRule(this.Parent(), params.RecordId)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

@@ -11,8 +11,12 @@ func (this *CustomRecordsToggleAction) RunPost(params struct {
RecordId int64
IsOn bool
}) {
if params.AppId > 0 && params.RecordId > 0 {
toggleCustomRecord(params.AppId, params.RecordId, params.IsOn)
if params.RecordId > 0 {
err := toggleCustomRule(this.Parent(), params.RecordId, params.IsOn)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

@@ -1,244 +0,0 @@
package apps
import (
"strconv"
"strings"
"sync"
"time"
"github.com/iwind/TeaGo/maps"
)
var customRecordStore = struct {
sync.RWMutex
nextID int64
data map[int64][]maps.Map
}{
nextID: 1000,
data: map[int64][]maps.Map{
1: {
{
"id": int64(1001),
"domain": "api.business.com",
"lineScope": "china",
"lineCarrier": "电信",
"lineRegion": "华东",
"lineProvince": "上海",
"ruleName": "上海电信灰度-v2",
"sdnsParams": []maps.Map{},
"recordType": "A",
"recordValues": []maps.Map{{"type": "A", "value": "1.1.1.10", "weight": 100}},
"weightEnabled": false,
"ttl": 30,
"isOn": true,
"updatedAt": "2026-02-23 10:20:00",
},
},
},
}
func loadCustomRecords(appID int64) []maps.Map {
customRecordStore.RLock()
defer customRecordStore.RUnlock()
records := customRecordStore.data[appID]
result := make([]maps.Map, 0, len(records))
for _, record := range records {
result = append(result, cloneCustomRecord(record))
}
return result
}
func countCustomRecordsByDomain(appID int64, domain string) int {
domain = strings.ToLower(strings.TrimSpace(domain))
if len(domain) == 0 {
return 0
}
customRecordStore.RLock()
defer customRecordStore.RUnlock()
count := 0
for _, record := range customRecordStore.data[appID] {
if strings.ToLower(strings.TrimSpace(record.GetString("domain"))) == domain {
count++
}
}
return count
}
func findCustomRecord(appID int64, recordID int64) maps.Map {
for _, record := range loadCustomRecords(appID) {
if record.GetInt64("id") == recordID {
return record
}
}
return maps.Map{}
}
func saveCustomRecord(appID int64, record maps.Map) maps.Map {
customRecordStore.Lock()
defer customRecordStore.Unlock()
if appID <= 0 {
return maps.Map{}
}
record = cloneCustomRecord(record)
recordID := record.GetInt64("id")
if recordID <= 0 {
customRecordStore.nextID++
recordID = customRecordStore.nextID
record["id"] = recordID
}
record["updatedAt"] = nowCustomRecordTime()
records := customRecordStore.data[appID]
found := false
for i, oldRecord := range records {
if oldRecord.GetInt64("id") == recordID {
records[i] = cloneCustomRecord(record)
found = true
break
}
}
if !found {
records = append(records, cloneCustomRecord(record))
}
customRecordStore.data[appID] = records
return cloneCustomRecord(record)
}
func deleteCustomRecord(appID int64, recordID int64) {
customRecordStore.Lock()
defer customRecordStore.Unlock()
records := customRecordStore.data[appID]
if len(records) == 0 {
return
}
filtered := make([]maps.Map, 0, len(records))
for _, record := range records {
if record.GetInt64("id") == recordID {
continue
}
filtered = append(filtered, record)
}
customRecordStore.data[appID] = filtered
}
func deleteCustomRecordsByApp(appID int64) {
customRecordStore.Lock()
defer customRecordStore.Unlock()
delete(customRecordStore.data, appID)
}
func toggleCustomRecord(appID int64, recordID int64, isOn bool) {
customRecordStore.Lock()
defer customRecordStore.Unlock()
records := customRecordStore.data[appID]
for i, record := range records {
if record.GetInt64("id") == recordID {
record["isOn"] = isOn
record["updatedAt"] = nowCustomRecordTime()
records[i] = record
break
}
}
customRecordStore.data[appID] = records
}
func cloneCustomRecord(src maps.Map) maps.Map {
dst := maps.Map{}
for k, v := range src {
switch k {
case "sdnsParams", "recordValues":
if list, ok := v.([]maps.Map); ok {
cloned := make([]maps.Map, 0, len(list))
for _, item := range list {
m := maps.Map{}
for k2, v2 := range item {
m[k2] = v2
}
cloned = append(cloned, m)
}
dst[k] = cloned
} else {
dst[k] = []maps.Map{}
}
default:
dst[k] = v
}
}
return dst
}
func nowCustomRecordTime() string {
return time.Now().Format("2006-01-02 15:04:05")
}
func buildLineText(record maps.Map) string {
parts := []string{}
if strings.TrimSpace(record.GetString("lineScope")) == "overseas" {
parts = append(parts,
strings.TrimSpace(record.GetString("lineContinent")),
strings.TrimSpace(record.GetString("lineCountry")),
)
} else {
parts = append(parts,
strings.TrimSpace(record.GetString("lineCarrier")),
strings.TrimSpace(record.GetString("lineRegion")),
strings.TrimSpace(record.GetString("lineProvince")),
)
}
finalParts := make([]string, 0, len(parts))
for _, part := range parts {
if len(part) == 0 || part == "默认" {
continue
}
finalParts = append(finalParts, part)
}
if len(finalParts) == 0 {
return "默认"
}
return strings.Join(finalParts, " / ")
}
func buildRecordValueText(record maps.Map) string {
values, ok := record["recordValues"].([]maps.Map)
if !ok || len(values) == 0 {
return "-"
}
weightEnabled := record.GetBool("weightEnabled")
defaultType := strings.ToUpper(strings.TrimSpace(record.GetString("recordType")))
parts := make([]string, 0, len(values))
for _, item := range values {
value := strings.TrimSpace(item.GetString("value"))
if len(value) == 0 {
continue
}
recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
if len(recordType) == 0 {
recordType = defaultType
}
if recordType != "A" && recordType != "AAAA" {
recordType = "A"
}
part := recordType + " " + value
if weightEnabled {
part += "(" + strconv.Itoa(item.GetInt("weight")) + ")"
} else {
// no extra suffix
}
parts = append(parts, part)
}
if len(parts) == 0 {
return "-"
}
return strings.Join(parts, ", ")
}

View File

@@ -17,10 +17,19 @@ func (this *DeleteAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "delete")
this.Data["app"] = app
this.Data["domainCount"] = len(mockDomains(app.GetInt64("id")))
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
this.Data["domainCount"] = len(domains)
this.Show()
}
@@ -28,9 +37,10 @@ func (this *DeleteAction) RunPost(params struct {
AppId int64
}) {
if params.AppId > 0 {
if deleteApp(params.AppId) {
deleteAppSettings(params.AppId)
deleteCustomRecordsByApp(params.AppId)
err := deleteAppByID(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
}

View File

@@ -17,15 +17,19 @@ func (this *DomainsAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
// 构建顶部 tabbar
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "domains")
domains := mockDomains(app.GetInt64("id"))
for _, domain := range domains {
domainName := domain.GetString("name")
domain["customRecordCount"] = countCustomRecordsByDomain(app.GetInt64("id"), domainName)
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
this.Data["app"] = app

View File

@@ -16,7 +16,12 @@ func (this *DomainsCreatePopupAction) Init() {
func (this *DomainsCreatePopupAction) RunGet(params struct {
AppId int64
}) {
this.Data["app"] = pickApp(params.AppId)
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["app"] = app
this.Show()
}
@@ -27,6 +32,13 @@ func (this *DomainsCreatePopupAction) RunPost(params struct {
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("domain", params.Domain).Require("please input domain")
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
params.Must.Field("domain", params.Domain).Require("请输入域名")
err := createDomain(this.Parent(), params.AppId, params.Domain)
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -9,6 +9,12 @@ type DomainsDeleteAction struct {
func (this *DomainsDeleteAction) RunPost(params struct {
DomainId int64
}) {
_ = params.DomainId
if params.DomainId > 0 {
err := deleteDomain(this.Parent(), params.DomainId)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

@@ -0,0 +1,92 @@
package apps
import (
"strconv"
"strings"
"github.com/iwind/TeaGo/maps"
)
func maskSecret(secret string) string {
secret = strings.TrimSpace(secret)
if len(secret) < 4 {
return "******"
}
prefix := ""
for i := 0; i < len(secret); i++ {
if secret[i] == '_' {
prefix = secret[:i+1]
break
}
}
if len(prefix) == 0 {
prefix = secret[:2]
}
if len(secret) <= 8 {
return prefix + "xxxx"
}
return prefix + "xxxxxxxx" + secret[len(secret)-4:]
}
func buildLineText(record maps.Map) string {
parts := []string{}
if strings.TrimSpace(record.GetString("lineScope")) == "overseas" {
parts = append(parts,
strings.TrimSpace(record.GetString("lineContinent")),
strings.TrimSpace(record.GetString("lineCountry")),
)
} else {
parts = append(parts,
strings.TrimSpace(record.GetString("lineCarrier")),
strings.TrimSpace(record.GetString("lineRegion")),
strings.TrimSpace(record.GetString("lineProvince")),
)
}
finalParts := make([]string, 0, len(parts))
for _, part := range parts {
if len(part) == 0 || part == "默认" {
continue
}
finalParts = append(finalParts, part)
}
if len(finalParts) == 0 {
return "默认"
}
return strings.Join(finalParts, " / ")
}
func buildRecordValueText(record maps.Map) string {
values, ok := record["recordValues"].([]maps.Map)
if !ok || len(values) == 0 {
return "-"
}
weightEnabled := record.GetBool("weightEnabled")
defaultType := strings.ToUpper(strings.TrimSpace(record.GetString("recordType")))
parts := make([]string, 0, len(values))
for _, item := range values {
value := strings.TrimSpace(item.GetString("value"))
if len(value) == 0 {
continue
}
recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
if len(recordType) == 0 {
recordType = defaultType
}
if recordType != "A" && recordType != "AAAA" {
recordType = "A"
}
part := recordType + " " + value
if weightEnabled {
part += "(" + strconv.Itoa(item.GetInt("weight")) + ")"
}
parts = append(parts, part)
}
if len(parts) == 0 {
return "-"
}
return strings.Join(parts, ", ")
}

View File

@@ -18,6 +18,11 @@ func (this *IndexAction) RunGet(params struct {
}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["keyword"] = params.Keyword
this.Data["apps"] = filterApps(params.Keyword, "", "", "")
apps, err := listAppMaps(this.Parent(), params.Keyword)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["apps"] = apps
this.Show()
}

View File

@@ -15,13 +15,17 @@ func init() {
Prefix("/httpdns/apps").
Get("", new(IndexAction)).
Get("/app", new(AppAction)).
Get("/sdk", new(SDKAction)).
Get("/sdk", new(SdkAction)).
GetPost("/sdk/upload", new(SdkUploadAction)).
Post("/sdk/upload/delete", new(SdkUploadDeleteAction)).
Get("/sdk/download", new(SdkDownloadAction)).
Get("/sdk/doc", new(SdkDocAction)).
GetPost("/app/settings", new(AppSettingsAction)).
Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)).
Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)).
Get("/domains", new(DomainsAction)).
Get("/customRecords", new(CustomRecordsAction)).
GetPost("/createPopup", new(CreatePopupAction)).
GetPost("/create", new(CreateAction)).
GetPost("/delete", new(DeleteAction)).
GetPost("/domains/createPopup", new(DomainsCreatePopupAction)).
Post("/domains/delete", new(DomainsDeleteAction)).

View File

@@ -1,190 +0,0 @@
package apps
import (
"strings"
"sync"
"github.com/iwind/TeaGo/maps"
)
var appStore = struct {
sync.RWMutex
data []maps.Map
}{
data: defaultMockApps(),
}
func defaultMockApps() []maps.Map {
return []maps.Map{
{
"id": int64(1),
"name": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
"appId": "ab12xc34s2",
"clusterId": int64(1),
"domainCount": 3,
"isOn": true,
"authStatus": "enabled",
"ecsMode": "auto",
"pinningMode": "report",
"sanMode": "strict",
"riskLevel": "medium",
"riskSummary": "Pinning \u5904\u4e8e\u89c2\u5bdf\u6a21\u5f0f",
"secretVersion": "v2026.02.20",
},
{
"id": int64(2),
"name": "\u89c6\u9891\u7f51\u5173\u4e1a\u52a1",
"appId": "vd8992ksm1",
"clusterId": int64(2),
"domainCount": 1,
"isOn": true,
"authStatus": "enabled",
"ecsMode": "custom",
"pinningMode": "enforce",
"sanMode": "strict",
"riskLevel": "low",
"riskSummary": "\u5df2\u542f\u7528\u5f3a\u6821\u9a8c",
"secretVersion": "v2026.02.18",
},
{
"id": int64(3),
"name": "\u6d77\u5916\u7070\u5ea6\u6d4b\u8bd5",
"appId": "ov7711hkq9",
"clusterId": int64(1),
"domainCount": 2,
"isOn": false,
"authStatus": "disabled",
"ecsMode": "off",
"pinningMode": "off",
"sanMode": "report",
"riskLevel": "high",
"riskSummary": "\u5e94\u7528\u5173\u95ed\u4e14\u8bc1\u4e66\u7b56\u7565\u504f\u5f31",
"secretVersion": "v2026.01.30",
},
}
}
func cloneMap(src maps.Map) maps.Map {
dst := maps.Map{}
for k, v := range src {
dst[k] = v
}
return dst
}
func cloneApps(apps []maps.Map) []maps.Map {
result := make([]maps.Map, 0, len(apps))
for _, app := range apps {
result = append(result, cloneMap(app))
}
return result
}
func mockApps() []maps.Map {
appStore.RLock()
defer appStore.RUnlock()
return cloneApps(appStore.data)
}
func deleteApp(appID int64) bool {
if appID <= 0 {
return false
}
appStore.Lock()
defer appStore.Unlock()
found := false
filtered := make([]maps.Map, 0, len(appStore.data))
for _, app := range appStore.data {
if app.GetInt64("id") == appID {
found = true
continue
}
filtered = append(filtered, app)
}
if found {
appStore.data = filtered
}
return found
}
func filterApps(keyword string, riskLevel string, ecsMode string, pinningMode string) []maps.Map {
all := mockApps()
if len(keyword) == 0 && len(riskLevel) == 0 && len(ecsMode) == 0 && len(pinningMode) == 0 {
return all
}
keyword = strings.ToLower(strings.TrimSpace(keyword))
result := make([]maps.Map, 0)
for _, app := range all {
if len(keyword) > 0 {
name := strings.ToLower(app.GetString("name"))
appID := strings.ToLower(app.GetString("appId"))
if !strings.Contains(name, keyword) && !strings.Contains(appID, keyword) {
continue
}
}
if len(riskLevel) > 0 && app.GetString("riskLevel") != riskLevel {
continue
}
if len(ecsMode) > 0 && app.GetString("ecsMode") != ecsMode {
continue
}
if len(pinningMode) > 0 && app.GetString("pinningMode") != pinningMode {
continue
}
result = append(result, app)
}
return result
}
func pickApp(appID int64) maps.Map {
apps := mockApps()
if len(apps) == 0 {
return maps.Map{
"id": int64(0),
"name": "",
"appId": "",
"clusterId": int64(0),
}
}
if appID <= 0 {
return apps[0]
}
for _, app := range apps {
if app.GetInt64("id") == appID {
return app
}
}
return apps[0]
}
func mockDomains(appID int64) []maps.Map {
_ = appID
return []maps.Map{
{
"id": int64(101),
"name": "api.business.com",
},
{
"id": int64(102),
"name": "payment.business.com",
},
}
}
func pickDomain(domainID int64) maps.Map {
domains := mockDomains(0)
if domainID <= 0 {
return domains[0]
}
for _, domain := range domains {
if domain.GetInt64("id") == domainID {
return domain
}
}
return domains[0]
}

View File

@@ -1,85 +0,0 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type PoliciesAction struct {
actionutils.ParentAction
}
func (this *PoliciesAction) Init() {
this.Nav("httpdns", "app", "")
}
func (this *PoliciesAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["policies"] = loadGlobalPolicies()
this.Show()
}
func (this *PoliciesAction) RunPost(params struct {
DefaultTTL int
DefaultSniPolicy string
DefaultFallbackMs int
ECSMode string
ECSIPv4Prefix int
ECSIPv6Prefix int
PinningMode string
SANMode string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("defaultTTL", params.DefaultTTL).Gt(0, "默认 TTL 需要大于 0")
params.Must.Field("defaultFallbackMs", params.DefaultFallbackMs).Gt(0, "默认超时需要大于 0")
if params.DefaultTTL > 86400 {
this.Fail("默认 TTL 不能超过 86400 秒")
return
}
if params.DefaultFallbackMs > 10000 {
this.Fail("默认超时不能超过 10000 毫秒")
return
}
if params.DefaultSniPolicy != "level1" && params.DefaultSniPolicy != "level2" && params.DefaultSniPolicy != "level3" {
this.Fail("默认 SNI 等级不正确")
return
}
if params.ECSMode != "off" && params.ECSMode != "auto" && params.ECSMode != "custom" {
this.Fail("ECS 模式不正确")
return
}
if params.ECSIPv4Prefix < 0 || params.ECSIPv4Prefix > 32 {
this.Fail("IPv4 掩码范围是 0-32")
return
}
if params.ECSIPv6Prefix < 0 || params.ECSIPv6Prefix > 128 {
this.Fail("IPv6 掩码范围是 0-128")
return
}
if params.PinningMode != "off" && params.PinningMode != "report" && params.PinningMode != "enforce" {
this.Fail("Pinning 策略不正确")
return
}
if params.SANMode != "off" && params.SANMode != "report" && params.SANMode != "strict" {
this.Fail("SAN 策略不正确")
return
}
saveGlobalPolicies(maps.Map{
"defaultTTL": params.DefaultTTL,
"defaultSniPolicy": params.DefaultSniPolicy,
"defaultFallbackMs": params.DefaultFallbackMs,
"ecsMode": params.ECSMode,
"ecsIPv4Prefix": params.ECSIPv4Prefix,
"ecsIPv6Prefix": params.ECSIPv6Prefix,
"pinningMode": params.PinningMode,
"sanMode": params.SANMode,
})
this.Success()
}

View File

@@ -1,54 +0,0 @@
package apps
import (
"sync"
"github.com/iwind/TeaGo/maps"
)
var globalPoliciesStore = struct {
sync.RWMutex
data maps.Map
}{
data: maps.Map{
"defaultTTL": 30,
"defaultSniPolicy": "level2",
"defaultFallbackMs": 300,
"ecsMode": "auto",
"ecsIPv4Prefix": 24,
"ecsIPv6Prefix": 56,
"pinningMode": "report",
"sanMode": "strict",
},
}
func loadGlobalPolicies() maps.Map {
globalPoliciesStore.RLock()
defer globalPoliciesStore.RUnlock()
return maps.Map{
"defaultTTL": globalPoliciesStore.data.GetInt("defaultTTL"),
"defaultSniPolicy": globalPoliciesStore.data.GetString("defaultSniPolicy"),
"defaultFallbackMs": globalPoliciesStore.data.GetInt("defaultFallbackMs"),
"ecsMode": globalPoliciesStore.data.GetString("ecsMode"),
"ecsIPv4Prefix": globalPoliciesStore.data.GetInt("ecsIPv4Prefix"),
"ecsIPv6Prefix": globalPoliciesStore.data.GetInt("ecsIPv6Prefix"),
"pinningMode": globalPoliciesStore.data.GetString("pinningMode"),
"sanMode": globalPoliciesStore.data.GetString("sanMode"),
}
}
func saveGlobalPolicies(policies maps.Map) {
globalPoliciesStore.Lock()
globalPoliciesStore.data = maps.Map{
"defaultTTL": policies.GetInt("defaultTTL"),
"defaultSniPolicy": policies.GetString("defaultSniPolicy"),
"defaultFallbackMs": policies.GetInt("defaultFallbackMs"),
"ecsMode": policies.GetString("ecsMode"),
"ecsIPv4Prefix": policies.GetInt("ecsIPv4Prefix"),
"ecsIPv6Prefix": policies.GetInt("ecsIPv6Prefix"),
"pinningMode": policies.GetString("pinningMode"),
"sanMode": policies.GetString("sanMode"),
}
globalPoliciesStore.Unlock()
}

View File

@@ -0,0 +1,295 @@
package apps
import (
"strconv"
"strings"
"time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
"github.com/iwind/TeaGo/maps"
)
func listAppMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map, error) {
resp, err := parent.RPC().HTTPDNSAppRPC().ListHTTPDNSApps(parent.AdminContext(), &pb.ListHTTPDNSAppsRequest{
Offset: 0,
Size: 10_000,
Keyword: strings.TrimSpace(keyword),
})
if err != nil {
return nil, err
}
result := make([]maps.Map, 0, len(resp.GetApps()))
for _, app := range resp.GetApps() {
domainResp, err := parent.RPC().HTTPDNSDomainRPC().ListHTTPDNSDomainsWithAppId(parent.AdminContext(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
AppDbId: app.GetId(),
})
if err != nil {
return nil, err
}
result = append(result, appPBToMap(app, int64(len(domainResp.GetDomains()))))
}
return result, nil
}
func findAppMap(parent *actionutils.ParentAction, appDbId int64) (maps.Map, error) {
if appDbId > 0 {
resp, err := parent.RPC().HTTPDNSAppRPC().FindHTTPDNSApp(parent.AdminContext(), &pb.FindHTTPDNSAppRequest{
AppDbId: appDbId,
})
if err != nil {
return nil, err
}
if resp.GetApp() != nil {
domainResp, err := parent.RPC().HTTPDNSDomainRPC().ListHTTPDNSDomainsWithAppId(parent.AdminContext(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
AppDbId: appDbId,
})
if err != nil {
return nil, err
}
return appPBToMap(resp.GetApp(), int64(len(domainResp.GetDomains()))), nil
}
}
apps, err := listAppMaps(parent, "")
if err != nil {
return nil, err
}
if len(apps) == 0 {
return maps.Map{
"id": int64(0),
"name": "",
"appId": "",
}, nil
}
return apps[0], nil
}
func createApp(parent *actionutils.ParentAction, name string, primaryClusterId int64, backupClusterId int64) (int64, error) {
newAppId := "app" + strconv.FormatInt(time.Now().UnixNano()%1_000_000_000_000, 36)
resp, err := parent.RPC().HTTPDNSAppRPC().CreateHTTPDNSApp(parent.AdminContext(), &pb.CreateHTTPDNSAppRequest{
Name: strings.TrimSpace(name),
AppId: newAppId,
PrimaryClusterId: primaryClusterId,
BackupClusterId: backupClusterId,
IsOn: true,
SignEnabled: true,
})
if err != nil {
return 0, err
}
return resp.GetAppDbId(), nil
}
func deleteAppByID(parent *actionutils.ParentAction, appDbId int64) error {
_, err := parent.RPC().HTTPDNSAppRPC().DeleteHTTPDNSApp(parent.AdminContext(), &pb.DeleteHTTPDNSAppRequest{
AppDbId: appDbId,
})
return err
}
func updateAppSettings(parent *actionutils.ParentAction, appDbId int64, name string, primaryClusterId int64, backupClusterId int64, isOn bool, userId int64) error {
_, err := parent.RPC().HTTPDNSAppRPC().UpdateHTTPDNSApp(parent.AdminContext(), &pb.UpdateHTTPDNSAppRequest{
AppDbId: appDbId,
Name: strings.TrimSpace(name),
PrimaryClusterId: primaryClusterId,
BackupClusterId: backupClusterId,
IsOn: isOn,
UserId: userId,
})
return err
}
func updateAppSignEnabled(parent *actionutils.ParentAction, appDbId int64, signEnabled bool) error {
_, err := parent.RPC().HTTPDNSAppRPC().UpdateHTTPDNSAppSignEnabled(parent.AdminContext(), &pb.UpdateHTTPDNSAppSignEnabledRequest{
AppDbId: appDbId,
SignEnabled: signEnabled,
})
return err
}
func resetAppSignSecret(parent *actionutils.ParentAction, appDbId int64) (*pb.ResetHTTPDNSAppSignSecretResponse, error) {
return parent.RPC().HTTPDNSAppRPC().ResetHTTPDNSAppSignSecret(parent.AdminContext(), &pb.ResetHTTPDNSAppSignSecretRequest{
AppDbId: appDbId,
})
}
func listDomainMaps(parent *actionutils.ParentAction, appDbId int64, keyword string) ([]maps.Map, error) {
resp, err := parent.RPC().HTTPDNSDomainRPC().ListHTTPDNSDomainsWithAppId(parent.AdminContext(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
AppDbId: appDbId,
Keyword: strings.TrimSpace(keyword),
})
if err != nil {
return nil, err
}
result := make([]maps.Map, 0, len(resp.GetDomains()))
for _, domain := range resp.GetDomains() {
result = append(result, maps.Map{
"id": domain.GetId(),
"name": domain.GetDomain(),
"isOn": domain.GetIsOn(),
"customRecordCount": domain.GetRuleCount(),
})
}
return result, nil
}
func createDomain(parent *actionutils.ParentAction, appDbId int64, domain string) error {
_, err := parent.RPC().HTTPDNSDomainRPC().CreateHTTPDNSDomain(parent.AdminContext(), &pb.CreateHTTPDNSDomainRequest{
AppDbId: appDbId,
Domain: strings.TrimSpace(domain),
IsOn: true,
})
return err
}
func deleteDomain(parent *actionutils.ParentAction, domainId int64) error {
_, err := parent.RPC().HTTPDNSDomainRPC().DeleteHTTPDNSDomain(parent.AdminContext(), &pb.DeleteHTTPDNSDomainRequest{
DomainId: domainId,
})
return err
}
func findDomainMap(domains []maps.Map, domainID int64) maps.Map {
if len(domains) == 0 {
return maps.Map{}
}
if domainID <= 0 {
return domains[0]
}
for _, domain := range domains {
if domain.GetInt64("id") == domainID {
return domain
}
}
return domains[0]
}
func listCustomRuleMaps(parent *actionutils.ParentAction, domainId int64) ([]maps.Map, error) {
resp, err := parent.RPC().HTTPDNSRuleRPC().ListHTTPDNSCustomRulesWithDomainId(parent.AdminContext(), &pb.ListHTTPDNSCustomRulesWithDomainIdRequest{
DomainId: domainId,
})
if err != nil {
return nil, err
}
result := make([]maps.Map, 0, len(resp.GetRules()))
for _, rule := range resp.GetRules() {
recordValues := make([]maps.Map, 0, len(rule.GetRecords()))
recordType := "A"
weightEnabled := false
for _, record := range rule.GetRecords() {
if len(recordType) == 0 {
recordType = strings.ToUpper(strings.TrimSpace(record.GetRecordType()))
}
if record.GetWeight() > 0 && record.GetWeight() != 100 {
weightEnabled = true
}
recordValues = append(recordValues, maps.Map{
"type": strings.ToUpper(strings.TrimSpace(record.GetRecordType())),
"value": record.GetRecordValue(),
"weight": record.GetWeight(),
})
}
if len(recordValues) == 0 {
recordValues = append(recordValues, maps.Map{
"type": "A",
"value": "",
"weight": 100,
})
}
item := maps.Map{
"id": rule.GetId(),
"lineScope": rule.GetLineScope(),
"lineCarrier": defaultLineField(rule.GetLineCarrier()),
"lineRegion": defaultLineField(rule.GetLineRegion()),
"lineProvince": defaultLineField(rule.GetLineProvince()),
"lineContinent": defaultLineField(rule.GetLineContinent()),
"lineCountry": defaultLineField(rule.GetLineCountry()),
"ruleName": rule.GetRuleName(),
"recordType": recordType,
"recordValues": recordValues,
"weightEnabled": weightEnabled,
"ttl": rule.GetTtl(),
"isOn": rule.GetIsOn(),
"updatedAt": formatDateTime(rule.GetUpdatedAt()),
}
item["lineText"] = buildLineText(item)
item["recordValueText"] = buildRecordValueText(item)
result = append(result, item)
}
return result, nil
}
func createCustomRule(parent *actionutils.ParentAction, rule *pb.HTTPDNSCustomRule) (int64, error) {
resp, err := parent.RPC().HTTPDNSRuleRPC().CreateHTTPDNSCustomRule(parent.AdminContext(), &pb.CreateHTTPDNSCustomRuleRequest{
Rule: rule,
})
if err != nil {
return 0, err
}
return resp.GetRuleId(), nil
}
func updateCustomRule(parent *actionutils.ParentAction, rule *pb.HTTPDNSCustomRule) error {
_, err := parent.RPC().HTTPDNSRuleRPC().UpdateHTTPDNSCustomRule(parent.AdminContext(), &pb.UpdateHTTPDNSCustomRuleRequest{
Rule: rule,
})
return err
}
func deleteCustomRule(parent *actionutils.ParentAction, ruleId int64) error {
_, err := parent.RPC().HTTPDNSRuleRPC().DeleteHTTPDNSCustomRule(parent.AdminContext(), &pb.DeleteHTTPDNSCustomRuleRequest{
RuleId: ruleId,
})
return err
}
func toggleCustomRule(parent *actionutils.ParentAction, ruleId int64, isOn bool) error {
_, err := parent.RPC().HTTPDNSRuleRPC().UpdateHTTPDNSCustomRuleStatus(parent.AdminContext(), &pb.UpdateHTTPDNSCustomRuleStatusRequest{
RuleId: ruleId,
IsOn: isOn,
})
return err
}
func appPBToMap(app *pb.HTTPDNSApp, domainCount int64) maps.Map {
signSecret := app.GetSignSecret()
return maps.Map{
"id": app.GetId(),
"name": app.GetName(),
"appId": app.GetAppId(),
"clusterId": app.GetPrimaryClusterId(),
"primaryClusterId": app.GetPrimaryClusterId(),
"backupClusterId": app.GetBackupClusterId(),
"userId": app.GetUserId(),
"isOn": app.GetIsOn(),
"domainCount": domainCount,
"sniPolicyText": "隐匿 SNI",
"signEnabled": app.GetSignEnabled(),
"signSecretPlain": signSecret,
"signSecretMasked": maskSecret(signSecret),
"signSecretUpdated": formatDateTime(app.GetSignUpdatedAt()),
}
}
func defaultLineField(value string) string {
value = strings.TrimSpace(value)
if len(value) == 0 {
return "默认"
}
return value
}
func formatDateTime(ts int64) string {
if ts <= 0 {
return ""
}
return timeutil.FormatTime("Y-m-d H:i:s", ts)
}

View File

@@ -5,23 +5,26 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
)
type SDKAction struct {
type SdkAction struct {
actionutils.ParentAction
}
func (this *SDKAction) Init() {
func (this *SdkAction) Init() {
this.Nav("httpdns", "app", "sdk")
}
func (this *SDKAction) RunGet(params struct {
func (this *SdkAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
// 构建顶部 tabbar
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "sdk")
this.Data["app"] = app
this.Show()
}

View File

@@ -0,0 +1,54 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"os"
"path/filepath"
"strings"
)
type SdkDocAction struct {
actionutils.ParentAction
}
func (this *SdkDocAction) Init() {
this.Nav("", "", "")
}
func (this *SdkDocAction) RunGet(params struct {
Platform string
}) {
platform, _, readmeRelativePath, _, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Fail(err.Error())
return
}
var data []byte
uploadedDocPath := findUploadedSDKDocPath(platform)
if len(uploadedDocPath) > 0 {
data, err = os.ReadFile(uploadedDocPath)
}
sdkRoot, sdkRootErr := findSDKRoot()
if len(data) == 0 && sdkRootErr == nil {
readmePath := filepath.Join(sdkRoot, readmeRelativePath)
data, err = os.ReadFile(readmePath)
}
if len(data) == 0 {
localDocPath := findLocalSDKDocPath(platform)
if len(localDocPath) > 0 {
data, err = os.ReadFile(localDocPath)
}
}
if len(data) == 0 || err != nil {
this.Fail("当前服务器未找到 SDK 集成文档请先在“SDK 集成”页面上传对应平台文档")
return
}
this.AddHeader("Content-Type", "text/markdown; charset=utf-8")
this.AddHeader("Content-Disposition", "attachment; filename=\"httpdns-sdk-"+strings.ToLower(platform)+"-README.md\";")
_, _ = this.ResponseWriter.Write(data)
}

View File

@@ -0,0 +1,45 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"io"
"os"
)
type SdkDownloadAction struct {
actionutils.ParentAction
}
func (this *SdkDownloadAction) Init() {
this.Nav("", "", "")
}
func (this *SdkDownloadAction) RunGet(params struct {
Platform string
}) {
_, _, _, filename, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Fail(err.Error())
return
}
archivePath := findSDKArchivePath(filename)
if len(archivePath) == 0 {
this.Fail("当前服务器未找到 SDK 包请先在“SDK 集成”页面上传对应平台包: " + filename)
return
}
fp, err := os.Open(archivePath)
if err != nil {
this.Fail("打开 SDK 包失败: " + err.Error())
return
}
defer func() {
_ = fp.Close()
}()
this.AddHeader("Content-Type", "application/zip")
this.AddHeader("Content-Disposition", "attachment; filename=\""+filename+"\";")
this.AddHeader("X-Accel-Buffering", "no")
_, _ = io.Copy(this.ResponseWriter, fp)
}

View File

@@ -0,0 +1,149 @@
package apps
import (
"errors"
"github.com/iwind/TeaGo/Tea"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
func sdkUploadDir() string {
return filepath.Clean(Tea.Root + "/data/httpdns/sdk")
}
func findFirstExistingDir(paths []string) string {
for _, path := range paths {
stat, err := os.Stat(path)
if err == nil && stat.IsDir() {
return path
}
}
return ""
}
func findFirstExistingFile(paths []string) string {
for _, path := range paths {
stat, err := os.Stat(path)
if err == nil && !stat.IsDir() {
return path
}
}
return ""
}
func findNewestExistingFile(paths []string) string {
type fileInfo struct {
path string
modTime time.Time
}
result := fileInfo{}
for _, path := range paths {
stat, err := os.Stat(path)
if err != nil || stat.IsDir() {
continue
}
if len(result.path) == 0 || stat.ModTime().After(result.modTime) || (stat.ModTime().Equal(result.modTime) && path > result.path) {
result.path = path
result.modTime = stat.ModTime()
}
}
return result.path
}
func findSDKRoot() (string, error) {
candidates := []string{
filepath.Clean(Tea.Root + "/EdgeHttpDNS/sdk"),
filepath.Clean(Tea.Root + "/edge-httpdns/sdk"),
filepath.Clean(Tea.Root + "/edge-httpdns/edge-httpdns/sdk"),
filepath.Clean(Tea.Root + "/../EdgeHttpDNS/sdk"),
filepath.Clean(Tea.Root + "/../../EdgeHttpDNS/sdk"),
filepath.Clean(Tea.Root + "/../edge-httpdns/sdk"),
filepath.Clean(Tea.Root + "/../../edge-httpdns/sdk"),
}
dir := findFirstExistingDir(candidates)
if len(dir) > 0 {
return dir, nil
}
return "", errors.New("SDK files are not found on current server")
}
func resolveSDKPlatform(platform string) (key string, relativeDir string, readmeRelativePath string, downloadFilename string, err error) {
switch strings.ToLower(strings.TrimSpace(platform)) {
case "android":
return "android", "android", "android/README.md", "httpdns-sdk-android.zip", nil
case "ios":
return "ios", "ios", "ios/README.md", "httpdns-sdk-ios.zip", nil
case "flutter":
return "flutter", "flutter/aliyun_httpdns", "flutter/aliyun_httpdns/README.md", "httpdns-sdk-flutter.zip", nil
default:
return "", "", "", "", errors.New("invalid platform, expected one of: android, ios, flutter")
}
}
func findSDKArchivePath(downloadFilename string) string {
searchDirs := []string{sdkUploadDir()}
// 1) Exact filename first.
exactFiles := []string{}
for _, dir := range searchDirs {
exactFiles = append(exactFiles, filepath.Join(dir, downloadFilename))
}
path := findFirstExistingFile(exactFiles)
if len(path) > 0 {
return path
}
// 2) Version-suffixed archives, e.g. httpdns-sdk-android-v1.4.8.zip
base := strings.TrimSuffix(downloadFilename, ".zip")
patternName := base + "-*.zip"
matches := []string{}
for _, dir := range searchDirs {
found, _ := filepath.Glob(filepath.Join(dir, patternName))
for _, file := range found {
stat, err := os.Stat(file)
if err == nil && !stat.IsDir() {
matches = append(matches, file)
}
}
}
if len(matches) > 0 {
return findNewestExistingFile(matches)
}
return ""
}
func findUploadedSDKDocPath(platform string) string {
platform = strings.ToLower(strings.TrimSpace(platform))
if len(platform) == 0 {
return ""
}
searchDir := sdkUploadDir()
exact := filepath.Join(searchDir, "httpdns-sdk-"+platform+".md")
if file := findFirstExistingFile([]string{exact}); len(file) > 0 {
return file
}
pattern := filepath.Join(searchDir, "httpdns-sdk-"+platform+"-*.md")
matches, _ := filepath.Glob(pattern)
if len(matches) == 0 {
return ""
}
sort.Strings(matches)
return findNewestExistingFile(matches)
}
func findLocalSDKDocPath(platform string) string {
filename := strings.ToLower(strings.TrimSpace(platform)) + ".md"
candidates := []string{
filepath.Clean(Tea.Root + "/edge-admin/web/views/@default/httpdns/apps/docs/" + filename),
filepath.Clean(Tea.Root + "/EdgeAdmin/web/views/@default/httpdns/apps/docs/" + filename),
}
return findFirstExistingFile(candidates)
}

View File

@@ -0,0 +1,264 @@
package apps
import (
"errors"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/actions"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
type SdkUploadAction struct {
actionutils.ParentAction
}
func (this *SdkUploadAction) Init() {
this.Nav("httpdns", "app", "sdk")
}
func (this *SdkUploadAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "sdk")
this.Data["app"] = app
this.Data["defaultVersion"] = "1.0.0"
this.Data["uploadedFiles"] = listUploadedSDKFiles()
this.Show()
}
func (this *SdkUploadAction) RunPost(params struct {
AppId int64
Platform string
Version string
SDKFile *actions.File
DocFile *actions.File
Must *actions.Must
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
platform, _, _, downloadFilename, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Fail(err.Error())
return
}
version, err := normalizeSDKVersion(params.Version)
if err != nil {
this.Fail(err.Error())
return
}
if params.SDKFile == nil && params.DocFile == nil {
this.Fail("请至少上传一个文件")
return
}
uploadDir := sdkUploadDir()
err = os.MkdirAll(uploadDir, 0755)
if err != nil {
this.Fail("创建上传目录失败: " + err.Error())
return
}
if params.SDKFile != nil {
filename := strings.ToLower(strings.TrimSpace(params.SDKFile.Filename))
if !strings.HasSuffix(filename, ".zip") {
this.Fail("SDK 包仅支持 .zip 文件")
return
}
sdkData, readErr := params.SDKFile.Read()
if readErr != nil {
this.Fail("读取 SDK 包失败: " + readErr.Error())
return
}
baseName := strings.TrimSuffix(downloadFilename, ".zip")
err = saveSDKUploadFile(uploadDir, downloadFilename, sdkData)
if err == nil {
err = saveSDKUploadFile(uploadDir, baseName+"-v"+version+".zip", sdkData)
}
if err != nil {
this.Fail("保存 SDK 包失败: " + err.Error())
return
}
}
if params.DocFile != nil {
docName := strings.ToLower(strings.TrimSpace(params.DocFile.Filename))
if !strings.HasSuffix(docName, ".md") {
this.Fail("集成文档仅支持 .md 文件")
return
}
docData, readErr := params.DocFile.Read()
if readErr != nil {
this.Fail("读取集成文档失败: " + readErr.Error())
return
}
filename := "httpdns-sdk-" + platform + ".md"
err = saveSDKUploadFile(uploadDir, filename, docData)
if err == nil {
err = saveSDKUploadFile(uploadDir, "httpdns-sdk-"+platform+"-v"+version+".md", docData)
}
if err != nil {
this.Fail("保存集成文档失败: " + err.Error())
return
}
}
this.Success()
}
func normalizeSDKVersion(version string) (string, error) {
version = strings.TrimSpace(version)
if len(version) == 0 {
version = "1.0.0"
}
for _, c := range version {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-' {
continue
}
return "", errors.New("版本号格式不正确")
}
return version, nil
}
func saveSDKUploadFile(baseDir string, filename string, data []byte) error {
targetPath := filepath.Join(baseDir, filename)
tmpPath := targetPath + ".tmp"
err := os.WriteFile(tmpPath, data, 0644)
if err != nil {
return err
}
return os.Rename(tmpPath, targetPath)
}
func listUploadedSDKFiles() []map[string]interface{} {
dir := sdkUploadDir()
entries, err := os.ReadDir(dir)
if err != nil {
return []map[string]interface{}{}
}
type item struct {
Name string
Platform string
FileType string
Version string
SizeBytes int64
UpdatedAt int64
}
items := make([]item, 0)
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
platform, version, fileType, ok := parseSDKUploadFilename(name)
if !ok {
continue
}
info, statErr := entry.Info()
if statErr != nil {
continue
}
items = append(items, item{
Name: name,
Platform: platform,
FileType: fileType,
Version: version,
SizeBytes: info.Size(),
UpdatedAt: info.ModTime().Unix(),
})
}
sort.Slice(items, func(i, j int) bool {
if items[i].UpdatedAt == items[j].UpdatedAt {
return items[i].Name > items[j].Name
}
return items[i].UpdatedAt > items[j].UpdatedAt
})
result := make([]map[string]interface{}, 0, len(items))
for _, item := range items {
result = append(result, map[string]interface{}{
"name": item.Name,
"platform": item.Platform,
"fileType": item.FileType,
"version": item.Version,
"sizeText": formatSDKFileSize(item.SizeBytes),
"updatedAt": time.Unix(item.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
})
}
return result
}
func parseSDKUploadFilename(filename string) (platform string, version string, fileType string, ok bool) {
if !strings.HasPrefix(filename, "httpdns-sdk-") {
return "", "", "", false
}
ext := ""
switch {
case strings.HasSuffix(filename, ".zip"):
ext = ".zip"
fileType = "SDK包"
case strings.HasSuffix(filename, ".md"):
ext = ".md"
fileType = "集成文档"
default:
return "", "", "", false
}
main := strings.TrimSuffix(strings.TrimPrefix(filename, "httpdns-sdk-"), ext)
version = "latest"
if idx := strings.Index(main, "-v"); idx > 0 && idx+2 < len(main) {
version = main[idx+2:]
main = main[:idx]
}
main = strings.ToLower(strings.TrimSpace(main))
switch main {
case "android", "ios", "flutter":
platform = main
return platform, version, fileType, true
default:
return "", "", "", false
}
}
func formatSDKFileSize(size int64) string {
if size < 1024 {
return strconv.FormatInt(size, 10) + " B"
}
sizeKB := float64(size) / 1024
if sizeKB < 1024 {
return strconv.FormatFloat(sizeKB, 'f', 1, 64) + " KB"
}
sizeMB := sizeKB / 1024
if sizeMB < 1024 {
return strconv.FormatFloat(sizeMB, 'f', 1, 64) + " MB"
}
sizeGB := sizeMB / 1024
return strconv.FormatFloat(sizeGB, 'f', 1, 64) + " GB"
}

View File

@@ -0,0 +1,57 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"os"
"path/filepath"
"strings"
)
type SdkUploadDeleteAction struct {
actionutils.ParentAction
}
func (this *SdkUploadDeleteAction) Init() {
this.Nav("httpdns", "app", "sdk")
}
func (this *SdkUploadDeleteAction) RunPost(params struct {
AppId int64
Filename string
}) {
if params.AppId <= 0 {
this.Fail("请选择应用")
return
}
filename := strings.TrimSpace(params.Filename)
if len(filename) == 0 {
this.Fail("文件名不能为空")
return
}
if strings.Contains(filename, "/") || strings.Contains(filename, "\\") || strings.Contains(filename, "..") {
this.Fail("文件名不合法")
return
}
if !strings.HasPrefix(filename, "httpdns-sdk-") {
this.Fail("不允许删除该文件")
return
}
if !(strings.HasSuffix(filename, ".zip") || strings.HasSuffix(filename, ".md")) {
this.Fail("不允许删除该文件")
return
}
fullPath := filepath.Join(sdkUploadDir(), filename)
_, err := os.Stat(fullPath)
if err != nil {
this.Success()
return
}
if err = os.Remove(fullPath); err != nil {
this.Fail("删除失败: " + err.Error())
return
}
this.Success()
}

View File

@@ -2,8 +2,6 @@ package clusters
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
)
type CertsAction struct {
@@ -15,7 +13,5 @@ func (this *CertsAction) Init() {
}
func (this *CertsAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["certs"] = policies.LoadPublicSNICertificates()
this.Show()
this.RedirectURL("/httpdns/clusters")
}

View File

@@ -1,8 +1,11 @@
package clusters
import (
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/maps"
)
type ClusterAction struct {
@@ -20,19 +23,75 @@ func (this *ClusterAction) RunGet(params struct {
Keyword string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
cluster := pickCluster(params.ClusterId)
// 构建顶部 tabbar
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "node")
nodes, err := listNodeMaps(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
nodes = filterClusterNodes(nodes, params.InstalledState, params.ActiveState, params.Keyword)
this.Data["clusterId"] = params.ClusterId
this.Data["cluster"] = cluster
this.Data["installState"] = params.InstalledState
this.Data["activeState"] = params.ActiveState
this.Data["keyword"] = params.Keyword
nodes := mockNodes(params.ClusterId, params.InstalledState, params.ActiveState, params.Keyword)
this.Data["nodes"] = nodes
this.Data["hasNodes"] = len(nodes) > 0
this.Data["page"] = ""
this.Show()
}
func filterClusterNodes(nodes []maps.Map, installedState int, activeState int, keyword string) []maps.Map {
keyword = strings.ToLower(strings.TrimSpace(keyword))
result := make([]maps.Map, 0, len(nodes))
for _, node := range nodes {
isInstalled := node.GetBool("isInstalled")
if installedState == 1 && !isInstalled {
continue
}
if installedState == 2 && isInstalled {
continue
}
status := node.GetMap("status")
isOnline := node.GetBool("isOn") && node.GetBool("isUp") && status.GetBool("isActive")
if activeState == 1 && !isOnline {
continue
}
if activeState == 2 && isOnline {
continue
}
if len(keyword) > 0 {
hit := strings.Contains(strings.ToLower(node.GetString("name")), keyword)
if !hit {
ipAddresses, ok := node["ipAddresses"].([]maps.Map)
if ok {
for _, ipAddr := range ipAddresses {
if strings.Contains(strings.ToLower(ipAddr.GetString("ip")), keyword) {
hit = true
break
}
}
}
}
if !hit {
continue
}
}
result = append(result, node)
}
return result
}

View File

@@ -1,8 +1,10 @@
package node
import (
"time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
@@ -18,51 +20,37 @@ func (this *IndexAction) RunGet(params struct {
ClusterId int64
NodeId int64
}) {
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
this.Data["nodeDatetime"] = "2026-02-22 12:00:00"
this.Data["nodeTimeDiff"] = 0
this.Data["shouldUpgrade"] = false
this.Data["newVersion"] = ""
this.Data["node"] = maps.Map{
"id": params.NodeId,
"name": "Mock HTTPDNS Node",
"ipAddresses": []maps.Map{{"ip": "100.200.100.200", "name": "Public IP", "canAccess": true, "isOn": true, "isUp": true}},
"cluster": maps.Map{"id": params.ClusterId, "name": "Mock Cluster", "installDir": "/opt/edge-httpdns"},
"installDir": "/opt/edge-httpdns",
"isInstalled": true,
"uniqueId": "m-1234567890",
"secret": "mock-secret-key",
"isOn": true,
"isUp": true,
"apiNodeAddrs": []string{"192.168.1.100:8001"},
"login": nil,
"status": maps.Map{
"isActive": true,
"updatedAt": 1670000000,
"hostname": "node-01.local",
"cpuUsage": 0.15,
"cpuUsageText": "15.00%",
"memUsage": 0.45,
"memUsageText": "45.00%",
"connectionCount": 100,
"buildVersion": "1.0.0",
"cpuPhysicalCount": 4,
"cpuLogicalCount": 8,
"load1m": "0.50",
"load5m": "0.60",
"load15m": "0.70",
"cacheTotalDiskSize": "10G",
"cacheTotalMemorySize": "2G",
"exePath": "/opt/edge-httpdns/bin/edge-httpdns",
"apiSuccessPercent": 100.0,
"apiAvgCostSeconds": 0.05,
},
node, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
if err != nil {
this.ErrorPage(err)
return
}
cluster, err := findHTTPDNSClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
this.Data["node"] = node
this.Data["currentCluster"] = cluster
status := node.GetMap("status")
updatedAt := status.GetInt64("updatedAt")
nodeDatetime := ""
nodeTimeDiff := int64(0)
if updatedAt > 0 {
nodeDatetime = timeutil.FormatTime("Y-m-d H:i:s", updatedAt)
nodeTimeDiff = time.Now().Unix() - updatedAt
if nodeTimeDiff < 0 {
nodeTimeDiff = -nodeTimeDiff
}
}
this.Data["nodeDatetime"] = nodeDatetime
this.Data["nodeTimeDiff"] = nodeTimeDiff
this.Data["shouldUpgrade"] = false
this.Data["newVersion"] = ""
this.Show()
}

View File

@@ -1,8 +1,12 @@
package node
package node
import (
"encoding/json"
"strings"
"time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type InstallAction struct {
@@ -14,28 +18,87 @@ func (this *InstallAction) Init() {
this.SecondMenu("nodes")
}
func (this *InstallAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
func (this *InstallAction) RunGet(params struct {
ClusterId int64
NodeId int64
}) {
node, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
if err != nil {
this.ErrorPage(err)
return
}
cluster, err := findHTTPDNSClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
this.Data["currentCluster"] = cluster
this.Data["node"] = node
this.Data["installStatus"] = node.GetMap("installStatus")
this.Data["apiEndpoints"] = "\"http://127.0.0.1:7788\""
this.Data["sshAddr"] = "192.168.1.100:22"
this.Data["node"] = maps.Map{
"id": params.NodeId,
"name": "Mock Node",
"isInstalled": false,
"uniqueId": "m-1234567890",
"secret": "mock-secret-key",
"installDir": "/opt/edge-httpdns",
"cluster": maps.Map{"installDir": "/opt/edge-httpdns"},
apiNodesResp, err := this.RPC().APINodeRPC().FindAllEnabledAPINodes(this.AdminContext(), &pb.FindAllEnabledAPINodesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["installStatus"] = nil
apiEndpoints := make([]string, 0, 8)
for _, apiNode := range apiNodesResp.GetApiNodes() {
if !apiNode.GetIsOn() {
continue
}
apiEndpoints = append(apiEndpoints, apiNode.GetAccessAddrs()...)
}
if len(apiEndpoints) == 0 {
apiEndpoints = []string{"http://127.0.0.1:7788"}
}
this.Data["apiEndpoints"] = "\"" + strings.Join(apiEndpoints, "\", \"") + "\""
this.Data["sshAddr"] = ""
this.Show()
}
func (this *InstallAction) RunPost(params struct{ NodeId int64 }) {
func (this *InstallAction) RunPost(params struct {
NodeId int64
}) {
nodeResp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
node := nodeResp.GetNode()
if node == nil {
this.Fail("节点不存在")
return
}
existingStatus := map[string]interface{}{}
if len(node.GetInstallStatusJSON()) > 0 {
_ = json.Unmarshal(node.GetInstallStatusJSON(), &existingStatus)
}
existingStatus["isRunning"] = true
existingStatus["isFinished"] = false
existingStatus["isOk"] = false
existingStatus["error"] = ""
existingStatus["errorCode"] = ""
existingStatus["updatedAt"] = time.Now().Unix()
installStatusJSON, _ := json.Marshal(existingStatus)
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
NodeId: params.NodeId,
IsUp: node.GetIsUp(),
IsInstalled: false,
IsActive: node.GetIsActive(),
StatusJSON: node.GetStatusJSON(),
InstallStatusJSON: installStatusJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -1,7 +1,11 @@
package node
package node
import (
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
"github.com/iwind/TeaGo/maps"
)
@@ -14,18 +18,65 @@ func (this *LogsAction) Init() {
this.SecondMenu("nodes")
}
func (this *LogsAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
func (this *LogsAction) RunGet(params struct {
ClusterId int64
NodeId int64
DayFrom string
DayTo string
Level string
Keyword string
}) {
node, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
if err != nil {
this.ErrorPage(err)
return
}
cluster, err := findHTTPDNSClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
this.Data["currentCluster"] = cluster
this.Data["node"] = node
this.Data["dayFrom"] = params.DayFrom
this.Data["dayTo"] = params.DayTo
this.Data["level"] = params.Level
this.Data["keyword"] = params.Keyword
this.Data["dayFrom"] = ""
this.Data["dayTo"] = ""
this.Data["keyword"] = ""
this.Data["level"] = ""
this.Data["logs"] = []maps.Map{}
day := strings.TrimSpace(params.DayFrom)
if len(day) == 0 {
day = strings.TrimSpace(params.DayTo)
}
resp, err := this.RPC().HTTPDNSRuntimeLogRPC().ListHTTPDNSRuntimeLogs(this.AdminContext(), &pb.ListHTTPDNSRuntimeLogsRequest{
Day: day,
ClusterId: params.ClusterId,
NodeId: params.NodeId,
Level: strings.TrimSpace(params.Level),
Keyword: strings.TrimSpace(params.Keyword),
Offset: 0,
Size: 100,
})
if err != nil {
this.ErrorPage(err)
return
}
logs := make([]maps.Map, 0, len(resp.GetLogs()))
for _, item := range resp.GetLogs() {
logs = append(logs, maps.Map{
"level": item.GetLevel(),
"tag": item.GetType(),
"description": item.GetDescription(),
"createdAt": item.GetCreatedAt(),
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", item.GetCreatedAt()),
"count": item.GetCount(),
})
}
this.Data["logs"] = logs
this.Data["page"] = ""
this.Data["node"] = maps.Map{"id": params.NodeId, "name": "Mock Node"}
this.Show()
}

View File

@@ -0,0 +1,183 @@
package node
import (
"encoding/json"
"fmt"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
func findHTTPDNSClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map, error) {
resp, err := parent.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(parent.AdminContext(), &pb.FindHTTPDNSClusterRequest{
ClusterId: clusterID,
})
if err != nil {
return nil, err
}
if resp.GetCluster() == nil {
return maps.Map{
"id": clusterID,
"name": "",
"installDir": "/opt/edge-httpdns",
}, nil
}
cluster := resp.GetCluster()
installDir := strings.TrimSpace(cluster.GetInstallDir())
if len(installDir) == 0 {
installDir = "/opt/edge-httpdns"
}
return maps.Map{
"id": cluster.GetId(),
"name": cluster.GetName(),
"installDir": installDir,
}, nil
}
func findHTTPDNSNodeMap(parent *actionutils.ParentAction, nodeID int64) (maps.Map, error) {
resp, err := parent.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(parent.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: nodeID,
})
if err != nil {
return nil, err
}
if resp.GetNode() == nil {
return maps.Map{}, nil
}
node := resp.GetNode()
statusMap := decodeNodeStatus(node.GetStatusJSON())
installStatusMap := decodeInstallStatus(node.GetInstallStatusJSON())
var ipAddresses = []maps.Map{}
if installStatusMap.Has("ipAddresses") {
for _, addr := range installStatusMap.GetSlice("ipAddresses") {
if addrMap, ok := addr.(map[string]interface{}); ok {
ipAddresses = append(ipAddresses, maps.Map(addrMap))
}
}
} else {
ip := node.GetName()
if savedIP := strings.TrimSpace(installStatusMap.GetString("ipAddr")); len(savedIP) > 0 {
ip = savedIP
} else if hostIP := strings.TrimSpace(statusMap.GetString("hostIP")); len(hostIP) > 0 {
ip = hostIP
}
ipAddresses = append(ipAddresses, maps.Map{
"id": node.GetId(),
"name": "Public IP",
"ip": ip,
"canAccess": true,
"isOn": node.GetIsOn(),
"isUp": node.GetIsUp(),
})
}
installDir := strings.TrimSpace(node.GetInstallDir())
if len(installDir) == 0 {
installDir = "/opt/edge-httpdns"
}
clusterMap, err := findHTTPDNSClusterMap(parent, node.GetClusterId())
if err != nil {
return nil, err
}
// 构造 node.login 用于 index.html 展示 SSH 信息
var loginMap maps.Map = nil
sshInfo := installStatusMap.GetMap("ssh")
if sshInfo != nil {
grantId := sshInfo.GetInt64("grantId")
var grantMap maps.Map = nil
if grantId > 0 {
grantResp, grantErr := parent.RPC().NodeGrantRPC().FindEnabledNodeGrant(parent.AdminContext(), &pb.FindEnabledNodeGrantRequest{
NodeGrantId: grantId,
})
if grantErr == nil && grantResp.GetNodeGrant() != nil {
g := grantResp.GetNodeGrant()
grantMap = maps.Map{
"id": g.Id,
"name": g.Name,
"methodName": g.Method,
"username": g.Username,
}
}
}
loginMap = maps.Map{
"params": maps.Map{
"host": sshInfo.GetString("host"),
"port": sshInfo.GetInt("port"),
},
"grant": grantMap,
}
}
return maps.Map{
"id": node.GetId(),
"clusterId": node.GetClusterId(),
"name": node.GetName(),
"isOn": node.GetIsOn(),
"isUp": node.GetIsUp(),
"isInstalled": node.GetIsInstalled(),
"isActive": node.GetIsActive(),
"uniqueId": node.GetUniqueId(),
"secret": node.GetSecret(),
"installDir": installDir,
"status": statusMap,
"installStatus": installStatusMap,
"cluster": clusterMap,
"login": loginMap,
"apiNodeAddrs": []string{},
"ipAddresses": ipAddresses,
}, nil
}
func decodeNodeStatus(raw []byte) maps.Map {
status := &nodeconfigs.NodeStatus{}
if len(raw) > 0 {
_ = json.Unmarshal(raw, status)
}
cpuText := fmt.Sprintf("%.2f%%", status.CPUUsage*100)
memText := fmt.Sprintf("%.2f%%", status.MemoryUsage*100)
return maps.Map{
"isActive": status.IsActive,
"updatedAt": status.UpdatedAt,
"hostname": status.Hostname,
"hostIP": status.HostIP,
"cpuUsage": status.CPUUsage,
"cpuUsageText": cpuText,
"memUsage": status.MemoryUsage,
"memUsageText": memText,
"load1m": status.Load1m,
"load5m": status.Load5m,
"load15m": status.Load15m,
"buildVersion": status.BuildVersion,
"cpuPhysicalCount": status.CPUPhysicalCount,
"cpuLogicalCount": status.CPULogicalCount,
"exePath": status.ExePath,
"apiSuccessPercent": status.APISuccessPercent,
"apiAvgCostSeconds": status.APIAvgCostSeconds,
}
}
func decodeInstallStatus(raw []byte) maps.Map {
result := maps.Map{
"isRunning": false,
"isFinished": true,
"isOk": true,
"error": "",
"errorCode": "",
}
if len(raw) == 0 {
return result
}
_ = json.Unmarshal(raw, &result)
return result
}

View File

@@ -1,13 +1,41 @@
package node
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type StartAction struct {
actionutils.ParentAction
}
func (this *StartAction) RunPost(params struct{ NodeId int64 }) {
func (this *StartAction) RunPost(params struct {
NodeId int64
}) {
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
node := resp.GetNode()
if node == nil {
this.Fail("节点不存在")
return
}
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
NodeId: params.NodeId,
IsUp: true,
IsInstalled: node.GetIsInstalled(),
IsActive: true,
StatusJSON: node.GetStatusJSON(),
InstallStatusJSON: node.GetInstallStatusJSON(),
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -2,7 +2,7 @@ package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type StatusAction struct {
@@ -14,14 +14,28 @@ func (this *StatusAction) Init() {
this.SecondMenu("nodes")
}
func (this *StatusAction) RunPost(params struct{ NodeId int64 }) {
this.Data["installStatus"] = maps.Map{
"isRunning": false,
"isFinished": true,
"isOk": true,
"error": "",
"errorCode": "",
func (this *StatusAction) RunPost(params struct {
NodeId int64
}) {
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["isInstalled"] = true
if resp.GetNode() == nil {
this.Fail("节点不存在")
return
}
nodeMap, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["installStatus"] = nodeMap.GetMap("installStatus")
this.Data["isInstalled"] = nodeMap.GetBool("isInstalled")
this.Success()
}

View File

@@ -1,13 +1,41 @@
package node
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type StopAction struct {
actionutils.ParentAction
}
func (this *StopAction) RunPost(params struct{ NodeId int64 }) {
func (this *StopAction) RunPost(params struct {
NodeId int64
}) {
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
node := resp.GetNode()
if node == nil {
this.Fail("节点不存在")
return
}
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
NodeId: params.NodeId,
IsUp: node.GetIsUp(),
IsInstalled: node.GetIsInstalled(),
IsActive: false,
StatusJSON: node.GetStatusJSON(),
InstallStatusJSON: node.GetInstallStatusJSON(),
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -1,7 +1,14 @@
package node
package node
import (
"encoding/json"
"net"
"regexp"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
@@ -14,28 +21,210 @@ func (this *UpdateAction) Init() {
this.SecondMenu("nodes")
}
func (this *UpdateAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
func (this *UpdateAction) RunGet(params struct {
ClusterId int64
NodeId int64
}) {
node, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
if err != nil {
this.ErrorPage(err)
return
}
cluster, err := findHTTPDNSClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
this.Data["clusters"] = []maps.Map{{"id": params.ClusterId, "name": "Mock Cluster"}}
this.Data["loginId"] = 0
this.Data["sshHost"] = "192.168.1.100"
this.Data["sshPort"] = 22
this.Data["grant"] = nil
this.Data["currentCluster"] = cluster
this.Data["clusters"] = []maps.Map{cluster}
this.Data["apiNodeAddrs"] = []string{}
this.Data["node"] = node
this.Data["node"] = maps.Map{
"id": params.NodeId,
"name": "Mock Node",
"isOn": true,
"ipAddresses": []maps.Map{},
sshHost := ""
sshPort := 22
ipAddresses := []maps.Map{}
var grantId int64
this.Data["grant"] = nil
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err == nil && resp.GetNode() != nil {
if len(resp.GetNode().GetInstallStatusJSON()) > 0 {
installStatus := maps.Map{}
_ = json.Unmarshal(resp.GetNode().GetInstallStatusJSON(), &installStatus)
sshInfo := installStatus.GetMap("ssh")
if sshInfo != nil {
if h := strings.TrimSpace(sshInfo.GetString("host")); len(h) > 0 {
sshHost = h
}
if p := sshInfo.GetInt("port"); p > 0 {
sshPort = p
}
grantId = sshInfo.GetInt64("grantId")
}
if installStatus.Has("ipAddresses") {
for _, addr := range installStatus.GetSlice("ipAddresses") {
if addrMap, ok := addr.(map[string]interface{}); ok {
ipAddresses = append(ipAddresses, maps.Map(addrMap))
}
}
} else if ip := strings.TrimSpace(installStatus.GetString("ipAddr")); len(ip) > 0 {
ipAddresses = append(ipAddresses, maps.Map{
"ip": ip,
"name": "",
"canAccess": true,
"isOn": true,
"isUp": true,
})
}
}
}
this.Data["sshHost"] = sshHost
this.Data["sshPort"] = sshPort
this.Data["ipAddresses"] = ipAddresses
if grantId > 0 {
grantResp, grantErr := this.RPC().NodeGrantRPC().FindEnabledNodeGrant(this.AdminContext(), &pb.FindEnabledNodeGrantRequest{
NodeGrantId: grantId,
})
if grantErr == nil && grantResp.GetNodeGrant() != nil {
g := grantResp.GetNodeGrant()
this.Data["grant"] = maps.Map{
"id": g.Id,
"name": g.Name,
"methodName": g.Method,
}
}
}
this.Show()
}
func (this *UpdateAction) RunPost(params struct{ NodeId int64 }) {
func (this *UpdateAction) RunPost(params struct {
NodeId int64
Name string
ClusterId int64
IsOn bool
SshHost string
SshPort int
GrantId int64
IpAddressesJSON []byte
Must *actions.Must
}) {
params.Name = strings.TrimSpace(params.Name)
params.Must.Field("name", params.Name).Require("请输入节点名称")
params.SshHost = strings.TrimSpace(params.SshHost)
hasSSHUpdate := len(params.SshHost) > 0 || params.SshPort > 0 || params.GrantId > 0
if hasSSHUpdate {
if len(params.SshHost) == 0 {
this.Fail("请输入 SSH 主机地址")
}
if params.SshPort <= 0 || params.SshPort > 65535 {
this.Fail("SSH 端口范围必须在 1-65535 之间")
}
if params.GrantId <= 0 {
this.Fail("请选择 SSH 登录认证")
}
if regexp.MustCompile(`^\d+\.\d+\.\d+\.\d+$`).MatchString(params.SshHost) && net.ParseIP(params.SshHost) == nil {
this.Fail("SSH 主机地址格式错误")
}
}
ipAddresses := []maps.Map{}
if len(params.IpAddressesJSON) > 0 {
err := json.Unmarshal(params.IpAddressesJSON, &ipAddresses)
if err != nil {
this.ErrorPage(err)
return
}
}
for _, addr := range ipAddresses {
ip := addr.GetString("ip")
if net.ParseIP(ip) == nil {
this.Fail("IP地址格式错误: " + ip)
}
}
needUpdateInstallStatus := hasSSHUpdate || len(ipAddresses) > 0
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
if resp.GetNode() == nil {
this.Fail("节点不存在")
return
}
node := resp.GetNode()
installDir := strings.TrimSpace(node.GetInstallDir())
if len(installDir) == 0 {
installDir = "/opt/edge-httpdns"
}
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNode(this.AdminContext(), &pb.UpdateHTTPDNSNodeRequest{
NodeId: params.NodeId,
Name: params.Name,
InstallDir: installDir,
IsOn: params.IsOn,
})
if err != nil {
this.ErrorPage(err)
return
}
if needUpdateInstallStatus {
installStatus := maps.Map{
"isRunning": false,
"isFinished": true,
"isOk": node.GetIsInstalled(),
"error": "",
"errorCode": "",
}
if len(node.GetInstallStatusJSON()) > 0 {
_ = json.Unmarshal(node.GetInstallStatusJSON(), &installStatus)
}
if hasSSHUpdate {
installStatus["ssh"] = maps.Map{
"host": params.SshHost,
"port": params.SshPort,
"grantId": params.GrantId,
}
}
if len(ipAddresses) > 0 {
installStatus["ipAddresses"] = ipAddresses
} else {
delete(installStatus, "ipAddresses")
delete(installStatus, "ipAddr") // Cleanup legacy
}
installStatusJSON, _ := json.Marshal(installStatus)
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
NodeId: params.NodeId,
IsUp: node.GetIsUp(),
IsInstalled: node.GetIsInstalled(),
IsActive: node.GetIsActive(),
StatusJSON: node.GetStatusJSON(),
InstallStatusJSON: installStatusJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

@@ -1,13 +1,58 @@
package node
package node
import (
"encoding/json"
"time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type UpdateInstallStatusAction struct {
actionutils.ParentAction
}
func (this *UpdateInstallStatusAction) RunPost(params struct{ NodeId int64 }) {
func (this *UpdateInstallStatusAction) RunPost(params struct {
NodeId int64
IsInstalled bool
}) {
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
node := resp.GetNode()
if node == nil {
this.Fail("节点不存在")
return
}
installStatus := map[string]interface{}{}
if len(node.GetInstallStatusJSON()) > 0 {
_ = json.Unmarshal(node.GetInstallStatusJSON(), &installStatus)
}
installStatus["isRunning"] = false
installStatus["isFinished"] = true
installStatus["isOk"] = params.IsInstalled
installStatus["error"] = ""
installStatus["errorCode"] = ""
installStatus["updatedAt"] = time.Now().Unix()
installStatusJSON, _ := json.Marshal(installStatus)
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
NodeId: params.NodeId,
IsUp: node.GetIsUp(),
IsInstalled: params.IsInstalled,
IsActive: node.GetIsActive(),
StatusJSON: node.GetStatusJSON(),
InstallStatusJSON: installStatusJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions"
@@ -27,80 +27,79 @@ func (this *ClusterSettingsAction) RunGet(params struct {
Section string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
cluster := pickCluster(params.ClusterId)
settings := loadClusterSettings(cluster)
cluster["name"] = settings.GetString("name")
// 构建顶部 tabbar
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "setting")
// 当前选中的 section
section := params.Section
section := strings.TrimSpace(params.Section)
if len(section) == 0 {
section = "basic"
}
this.Data["activeSection"] = section
// 左侧菜单
settings := maps.Map{
"name": cluster.GetString("name"),
"gatewayDomain": cluster.GetString("gatewayDomain"),
"cacheTtl": cluster.GetInt("defaultTTL"),
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
"installDir": cluster.GetString("installDir"),
"isOn": cluster.GetBool("isOn"),
"isDefaultCluster": cluster.GetBool("isDefault"),
}
if settings.GetInt("cacheTtl") <= 0 {
settings["cacheTtl"] = 30
}
if settings.GetInt("fallbackTimeout") <= 0 {
settings["fallbackTimeout"] = 300
}
if len(settings.GetString("installDir")) == 0 {
settings["installDir"] = "/opt/edge-httpdns"
}
listenAddresses := []*serverconfigs.NetworkAddressConfig{
{
Protocol: serverconfigs.ProtocolHTTPS,
Host: "",
PortRange: "443",
},
}
sslPolicy := &sslconfigs.SSLPolicy{
IsOn: true,
MinVersion: "TLS 1.1",
}
if rawTLS := strings.TrimSpace(cluster.GetString("tlsPolicyJSON")); len(rawTLS) > 0 {
tlsConfig := maps.Map{}
if err := json.Unmarshal([]byte(rawTLS), &tlsConfig); err == nil {
if listenRaw := tlsConfig.Get("listen"); listenRaw != nil {
if data, err := json.Marshal(listenRaw); err == nil {
_ = json.Unmarshal(data, &listenAddresses)
}
}
if sslRaw := tlsConfig.Get("sslPolicy"); sslRaw != nil {
if data, err := json.Marshal(sslRaw); err == nil {
_ = json.Unmarshal(data, sslPolicy)
}
}
}
}
this.Data["activeSection"] = section
cid := strconv.FormatInt(params.ClusterId, 10)
this.Data["leftMenuItems"] = []map[string]interface{}{
{"name": "基础设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=basic", "isActive": section == "basic"},
{"name": "端口设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=tls", "isActive": section == "tls"},
{"name": "TLS", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=tls", "isActive": section == "tls"},
}
settings["isDefaultCluster"] = (policies.LoadDefaultClusterID() == cluster.GetInt64("id"))
this.Data["cluster"] = cluster
// 构造前端需要的 tlsConfig 格式
var listenAddresses []*serverconfigs.NetworkAddressConfig
listenAddrsRaw := settings.GetString("listenAddrsJSON")
if len(listenAddrsRaw) > 0 {
_ = json.Unmarshal([]byte(listenAddrsRaw), &listenAddresses)
} else {
// 默认 443 端口
listenAddresses = []*serverconfigs.NetworkAddressConfig{
{
Protocol: serverconfigs.ProtocolHTTPS,
Host: "",
PortRange: "443",
},
}
}
// 构造前端需要的 SSLPolicy
var sslPolicy *sslconfigs.SSLPolicy
originCertPem := settings.GetString("originCertPem")
originKeyPem := settings.GetString("originKeyPem")
if len(originCertPem) > 0 && len(originKeyPem) > 0 {
sslPolicy = &sslconfigs.SSLPolicy{
IsOn: true,
MinVersion: settings.GetString("tlsMinVersion"),
CipherSuitesIsOn: settings.GetBool("tlsCipherSuitesOn"),
OCSPIsOn: settings.GetBool("tlsOcspOn"),
ClientAuthType: int(settings.GetInt32("tlsClientAuthType")),
Certs: []*sslconfigs.SSLCertConfig{
{
IsOn: true,
CertData: []byte(originCertPem),
KeyData: []byte(originKeyPem),
},
},
}
} else {
sslPolicy = &sslconfigs.SSLPolicy{
IsOn: true,
MinVersion: "TLS 1.1",
}
}
this.Data["settings"] = settings
this.Data["tlsConfig"] = maps.Map{
"isOn": true,
"listen": listenAddresses,
"sslPolicy": sslPolicy,
}
this.Data["cluster"] = cluster
this.Data["settings"] = settings
this.Show()
}
@@ -120,88 +119,80 @@ func (this *ClusterSettingsAction) RunPost(params struct {
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("clusterId", params.ClusterId).Gt(0, "please select cluster")
params.Must.Field("name", params.Name).Require("please input cluster name")
params.Must.Field("gatewayDomain", params.GatewayDomain).Require("please input service domain")
params.Must.Field("cacheTtl", params.CacheTtl).Gt(0, "cache ttl should be greater than 0")
params.Must.Field("fallbackTimeout", params.FallbackTimeout).Gt(0, "fallback timeout should be greater than 0")
params.Must.Field("installDir", params.InstallDir).Require("please input install dir")
params.Name = strings.TrimSpace(params.Name)
params.GatewayDomain = strings.TrimSpace(params.GatewayDomain)
params.InstallDir = strings.TrimSpace(params.InstallDir)
params.Must.Field("clusterId", params.ClusterId).Gt(0, "请选择集群")
params.Must.Field("name", params.Name).Require("请输入集群名称")
params.Must.Field("gatewayDomain", params.GatewayDomain).Require("请输入服务域名")
if params.CacheTtl <= 0 {
params.CacheTtl = 30
}
if params.FallbackTimeout <= 0 {
params.FallbackTimeout = 300
}
if len(params.InstallDir) == 0 {
params.InstallDir = "/opt/edge-httpdns"
}
if params.IsDefaultCluster && !params.IsOn {
this.Fail("默认集群必须保持启用状态")
return
}
cluster := pickCluster(params.ClusterId)
settings := loadClusterSettings(cluster)
settings["name"] = strings.TrimSpace(params.Name)
settings["gatewayDomain"] = strings.TrimSpace(params.GatewayDomain)
settings["cacheTtl"] = int(params.CacheTtl)
settings["fallbackTimeout"] = int(params.FallbackTimeout)
settings["installDir"] = strings.TrimSpace(params.InstallDir)
settings["isOn"] = params.IsOn
// 处理地址
var addresses = []*serverconfigs.NetworkAddressConfig{}
if len(params.Addresses) > 0 {
err := json.Unmarshal(params.Addresses, &addresses)
if err != nil {
this.Fail("端口地址解析失败:" + err.Error())
}
addressesJSON, _ := json.Marshal(addresses)
settings["listenAddrsJSON"] = string(addressesJSON)
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
// 处理 SSL 配置
var originCertPem = ""
var originKeyPem = ""
var tlsMinVersion = "TLS 1.1"
var tlsCipherSuitesOn = false
var tlsOcspOn = false
var tlsClientAuthType = sslconfigs.SSLClientAuthType(0)
tlsConfig := maps.Map{}
if rawTLS := strings.TrimSpace(cluster.GetString("tlsPolicyJSON")); len(rawTLS) > 0 {
_ = json.Unmarshal([]byte(rawTLS), &tlsConfig)
}
if len(params.Addresses) > 0 {
var addresses []*serverconfigs.NetworkAddressConfig
if err := json.Unmarshal(params.Addresses, &addresses); err != nil {
this.Fail("监听端口配置格式不正确")
return
}
tlsConfig["listen"] = addresses
}
if len(params.SslPolicyJSON) > 0 {
sslPolicy := &sslconfigs.SSLPolicy{}
err := json.Unmarshal(params.SslPolicyJSON, sslPolicy)
if err == nil {
tlsMinVersion = sslPolicy.MinVersion
tlsCipherSuitesOn = sslPolicy.CipherSuitesIsOn
tlsOcspOn = sslPolicy.OCSPIsOn
tlsClientAuthType = sslconfigs.SSLClientAuthType(sslPolicy.ClientAuthType)
if err := json.Unmarshal(params.SslPolicyJSON, sslPolicy); err != nil {
this.Fail("TLS 配置格式不正确")
return
}
tlsConfig["sslPolicy"] = sslPolicy
}
if len(sslPolicy.Certs) > 0 {
cert := sslPolicy.Certs[0]
originCertPem = string(cert.CertData)
originKeyPem = string(cert.KeyData)
}
var tlsPolicyJSON []byte
if len(tlsConfig) > 0 {
tlsPolicyJSON, err = json.Marshal(tlsConfig)
if err != nil {
this.ErrorPage(err)
return
}
}
if len(originCertPem) == 0 || len(originKeyPem) == 0 {
this.Fail("请上传或选择证书")
}
settings["originHttps"] = true
settings["originCertPem"] = originCertPem
settings["originKeyPem"] = originKeyPem
if len(tlsMinVersion) == 0 {
tlsMinVersion = "TLS 1.1"
}
settings["tlsMinVersion"] = tlsMinVersion
settings["tlsCipherSuitesOn"] = tlsCipherSuitesOn
settings["tlsOcspOn"] = tlsOcspOn
settings["tlsClientAuthType"] = int(tlsClientAuthType)
settings["lastModifiedAt"] = nowDateTime()
settings["certUpdatedAt"] = nowDateTime()
saveClusterSettings(params.ClusterId, settings)
currentDefaultClusterId := policies.LoadDefaultClusterID()
if params.IsDefaultCluster {
policies.SaveDefaultClusterID(params.ClusterId)
} else if currentDefaultClusterId == params.ClusterId {
policies.SaveDefaultClusterID(0)
_, err = this.RPC().HTTPDNSClusterRPC().UpdateHTTPDNSCluster(this.AdminContext(), &pb.UpdateHTTPDNSClusterRequest{
ClusterId: params.ClusterId,
Name: params.Name,
ServiceDomain: params.GatewayDomain,
DefaultTTL: params.CacheTtl,
FallbackTimeoutMs: params.FallbackTimeout,
InstallDir: params.InstallDir,
TlsPolicyJSON: tlsPolicyJSON,
IsOn: params.IsOn,
IsDefault: params.IsDefaultCluster,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()

View File

@@ -1,110 +0,0 @@
package clusters
import (
"strings"
"sync"
"time"
"github.com/iwind/TeaGo/maps"
)
var clusterSettingsStore = struct {
sync.RWMutex
data map[int64]maps.Map
}{
data: map[int64]maps.Map{},
}
func defaultClusterSettings(cluster maps.Map) maps.Map {
installDir := strings.TrimSpace(cluster.GetString("installDir"))
if len(installDir) == 0 {
installDir = "/opt/edge-httpdns"
}
return maps.Map{
"name": cluster.GetString("name"),
"gatewayDomain": strings.TrimSpace(cluster.GetString("gatewayDomain")),
"cacheTtl": cluster.GetInt("cacheTtl"),
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
"installDir": installDir,
"isOn": cluster.GetBool("isOn"),
"originHttps": true,
"originCertPem": "",
"originKeyPem": "",
"tlsMinVersion": "TLS 1.1",
"tlsCipherSuitesOn": false,
"tlsOcspOn": false,
"tlsClientAuthType": 0,
"certUpdatedAt": "",
"lastModifiedAt": "",
}
}
func cloneClusterSettings(settings maps.Map) maps.Map {
return maps.Map{
"name": settings.GetString("name"),
"gatewayDomain": settings.GetString("gatewayDomain"),
"cacheTtl": settings.GetInt("cacheTtl"),
"fallbackTimeout": settings.GetInt("fallbackTimeout"),
"installDir": settings.GetString("installDir"),
"isOn": settings.GetBool("isOn"),
"originHttps": true,
"originCertPem": settings.GetString("originCertPem"),
"originKeyPem": settings.GetString("originKeyPem"),
"tlsMinVersion": settings.GetString("tlsMinVersion"),
"tlsCipherSuitesOn": settings.GetBool("tlsCipherSuitesOn"),
"tlsOcspOn": settings.GetBool("tlsOcspOn"),
"tlsClientAuthType": settings.GetInt("tlsClientAuthType"),
"certUpdatedAt": settings.GetString("certUpdatedAt"),
"lastModifiedAt": settings.GetString("lastModifiedAt"),
}
}
func loadClusterSettings(cluster maps.Map) maps.Map {
clusterId := cluster.GetInt64("id")
clusterSettingsStore.RLock()
settings, ok := clusterSettingsStore.data[clusterId]
clusterSettingsStore.RUnlock()
if ok {
if len(settings.GetString("tlsMinVersion")) == 0 {
settings["tlsMinVersion"] = "TLS 1.1"
}
return cloneClusterSettings(settings)
}
settings = defaultClusterSettings(cluster)
saveClusterSettings(clusterId, settings)
return cloneClusterSettings(settings)
}
func saveClusterSettings(clusterId int64, settings maps.Map) {
clusterSettingsStore.Lock()
clusterSettingsStore.data[clusterId] = cloneClusterSettings(settings)
clusterSettingsStore.Unlock()
}
func applyClusterSettingsOverrides(cluster maps.Map) {
clusterId := cluster.GetInt64("id")
if clusterId <= 0 {
return
}
clusterSettingsStore.RLock()
settings, ok := clusterSettingsStore.data[clusterId]
clusterSettingsStore.RUnlock()
if !ok {
return
}
cluster["name"] = settings.GetString("name")
cluster["gatewayDomain"] = settings.GetString("gatewayDomain")
cluster["cacheTtl"] = settings.GetInt("cacheTtl")
cluster["fallbackTimeout"] = settings.GetInt("fallbackTimeout")
cluster["installDir"] = settings.GetString("installDir")
cluster["isOn"] = settings.GetBool("isOn")
}
func nowDateTime() string {
return time.Now().Format("2006-01-02 15:04:05")
}

View File

@@ -1,8 +1,12 @@
package clusters
import (
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
type CreateAction struct {
@@ -17,3 +21,48 @@ func (this *CreateAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Show()
}
func (this *CreateAction) RunPost(params struct {
Name string
GatewayDomain string
CacheTtl int32
FallbackTimeout int32
InstallDir string
IsOn bool
IsDefault bool
Must *actions.Must
}) {
params.Name = strings.TrimSpace(params.Name)
params.GatewayDomain = strings.TrimSpace(params.GatewayDomain)
params.InstallDir = strings.TrimSpace(params.InstallDir)
if len(params.InstallDir) == 0 {
params.InstallDir = "/opt/edge-httpdns"
}
if params.CacheTtl <= 0 {
params.CacheTtl = 30
}
if params.FallbackTimeout <= 0 {
params.FallbackTimeout = 300
}
params.Must.Field("name", params.Name).Require("请输入集群名称")
params.Must.Field("gatewayDomain", params.GatewayDomain).Require("请输入服务域名")
resp, err := this.RPC().HTTPDNSClusterRPC().CreateHTTPDNSCluster(this.AdminContext(), &pb.CreateHTTPDNSClusterRequest{
Name: params.Name,
ServiceDomain: params.GatewayDomain,
DefaultTTL: params.CacheTtl,
FallbackTimeoutMs: params.FallbackTimeout,
InstallDir: params.InstallDir,
IsOn: params.IsOn,
IsDefault: params.IsDefault,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["clusterId"] = resp.GetClusterId()
this.Success()
}

View File

@@ -1,8 +1,11 @@
package clusters
import (
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/actions"
)
type CreateNodeAction struct {
@@ -13,13 +16,18 @@ func (this *CreateNodeAction) Init() {
this.Nav("httpdns", "cluster", "createNode")
}
func (this *CreateNodeAction) RunGet(params struct{ ClusterId int64 }) {
func (this *CreateNodeAction) RunGet(params struct {
ClusterId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
cluster := pickCluster(params.ClusterId)
// 构建顶部 tabbar
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "node")
this.Data["clusterId"] = params.ClusterId
this.Data["cluster"] = cluster
this.Show()
@@ -28,6 +36,28 @@ func (this *CreateNodeAction) RunGet(params struct{ ClusterId int64 }) {
func (this *CreateNodeAction) RunPost(params struct {
ClusterId int64
Name string
InstallDir string
Must *actions.Must
}) {
params.Name = strings.TrimSpace(params.Name)
params.InstallDir = strings.TrimSpace(params.InstallDir)
params.Must.Field("clusterId", params.ClusterId).Gt(0, "请选择集群")
params.Must.Field("name", params.Name).Require("请输入节点名称")
if len(params.InstallDir) == 0 {
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err == nil {
params.InstallDir = strings.TrimSpace(cluster.GetString("installDir"))
}
if len(params.InstallDir) == 0 {
params.InstallDir = "/opt/edge-httpdns"
}
}
if err := createNode(this.Parent(), params.ClusterId, params.Name, params.InstallDir); err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -3,6 +3,7 @@ package clusters
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type DeleteAction struct {
@@ -17,11 +18,14 @@ func (this *DeleteAction) RunGet(params struct {
ClusterId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
cluster := pickCluster(params.ClusterId)
// 构建顶部 tabbar
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "delete")
this.Data["cluster"] = cluster
this.Show()
}
@@ -29,6 +33,12 @@ func (this *DeleteAction) RunGet(params struct {
func (this *DeleteAction) RunPost(params struct {
ClusterId int64
}) {
_ = params.ClusterId
_, err := this.RPC().HTTPDNSClusterRPC().DeleteHTTPDNSCluster(this.AdminContext(), &pb.DeleteHTTPDNSClusterRequest{
ClusterId: params.ClusterId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -1,6 +1,18 @@
package clusters
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
)
type DeleteNodeAction struct { actionutils.ParentAction }
func (this *DeleteNodeAction) RunPost(params struct{ ClusterId int64; NodeId int64 }) { this.Success() }
package clusters
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
type DeleteNodeAction struct {
actionutils.ParentAction
}
func (this *DeleteNodeAction) RunPost(params struct {
ClusterId int64
NodeId int64
}) {
if err := deleteNode(this.Parent(), params.NodeId); err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -13,8 +13,27 @@ func (this *IndexAction) Init() {
this.Nav("httpdns", "cluster", "")
}
func (this *IndexAction) RunGet(params struct{}) {
func (this *IndexAction) RunGet(params struct {
Keyword string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["clusters"] = mockClusters()
this.Data["keyword"] = params.Keyword
clusters, err := listClusterMaps(this.Parent(), params.Keyword)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["clusters"] = clusters
hasErrorLogs := false
for _, cluster := range clusters {
if cluster.GetInt("countAllNodes") > cluster.GetInt("countActiveNodes") {
hasErrorLogs = true
break
}
}
this.Data["hasErrorLogs"] = hasErrorLogs
this.Data["page"] = ""
this.Show()
}

View File

@@ -15,7 +15,7 @@ func init() {
Data("teaSubMenu", "cluster").
Prefix("/httpdns/clusters").
Get("", new(IndexAction)).
Get("/create", new(CreateAction)).
GetPost("/create", new(CreateAction)).
Get("/cluster", new(ClusterAction)).
GetPost("/cluster/settings", new(ClusterSettingsAction)).
GetPost("/delete", new(DeleteAction)).

View File

@@ -1,132 +0,0 @@
package clusters
import "github.com/iwind/TeaGo/maps"
func mockClusters() []maps.Map {
clusters := []maps.Map{
{
"id": int64(1),
"name": "gateway-cn-hz",
"region": "cn-hangzhou",
"gatewayDomain": "gw-hz.httpdns.example.com",
"installDir": "/opt/edge-httpdns",
"countAllNodes": 3,
"countActiveNodes": 3,
"countApps": 5,
"cacheTtl": 30,
"fallbackTimeout": 300,
"isOn": true,
},
{
"id": int64(2),
"name": "gateway-cn-bj",
"region": "cn-beijing",
"gatewayDomain": "gw-bj.httpdns.example.com",
"installDir": "/opt/edge-httpdns",
"countAllNodes": 3,
"countActiveNodes": 2,
"countApps": 2,
"cacheTtl": 30,
"fallbackTimeout": 300,
"isOn": true,
},
}
for _, cluster := range clusters {
applyClusterSettingsOverrides(cluster)
}
return clusters
}
func pickCluster(clusterId int64) maps.Map {
clusters := mockClusters()
if clusterId <= 0 {
return clusters[0]
}
for _, c := range clusters {
if c.GetInt64("id") == clusterId {
return c
}
}
return clusters[0]
}
func mockNodes(clusterId int64, installState int, activeState int, keyword string) []maps.Map {
_ = clusterId
return []maps.Map{
{
"id": int64(101),
"name": "45.250.184.56",
"isInstalled": true,
"isOn": true,
"isUp": true,
"installStatus": maps.Map{
"isRunning": false,
"isFinished": true,
"isOk": true,
"error": "",
},
"status": maps.Map{
"isActive": true,
"updatedAt": 1700000000,
"hostname": "node-01",
"cpuUsage": 0.0253,
"cpuUsageText": "2.53%",
"memUsage": 0.5972,
"memUsageText": "59.72%",
"load1m": 0.02,
},
"ipAddresses": []maps.Map{
{
"id": 1,
"name": "",
"ip": "45.250.184.56",
"canAccess": true,
},
},
},
{
"id": int64(102),
"name": "45.250.184.53",
"isInstalled": true,
"isOn": true,
"isUp": true,
"installStatus": maps.Map{
"isRunning": false,
"isFinished": true,
"isOk": true,
"error": "",
},
"status": maps.Map{
"isActive": true,
"updatedAt": 1700000000,
"hostname": "node-02",
"cpuUsage": 0.0039,
"cpuUsageText": "0.39%",
"memUsage": 0.0355,
"memUsageText": "3.55%",
"load1m": 0.0,
},
"ipAddresses": []maps.Map{
{
"id": 2,
"name": "",
"ip": "45.250.184.53",
"canAccess": true,
},
},
},
}
}
func mockCerts() []maps.Map {
return []maps.Map{
{
"id": int64(11),
"domain": "resolve.edge.example.com",
"issuer": "Mock CA",
"expiresAt": "2026-12-31 23:59:59",
},
}
}

View File

@@ -0,0 +1,248 @@
package clusters
import (
"encoding/json"
"fmt"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map, error) {
resp, err := parent.RPC().HTTPDNSClusterRPC().ListHTTPDNSClusters(parent.AdminContext(), &pb.ListHTTPDNSClustersRequest{
Offset: 0,
Size: 10_000,
Keyword: strings.TrimSpace(keyword),
})
if err != nil {
return nil, err
}
result := make([]maps.Map, 0, len(resp.GetClusters()))
for _, cluster := range resp.GetClusters() {
nodesResp, err := parent.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(parent.AdminContext(), &pb.ListHTTPDNSNodesRequest{
ClusterId: cluster.GetId(),
})
if err != nil {
return nil, err
}
countAllNodes := len(nodesResp.GetNodes())
countActiveNodes := 0
for _, node := range nodesResp.GetNodes() {
if node.GetIsOn() && node.GetIsUp() && node.GetIsActive() {
countActiveNodes++
}
}
result = append(result, maps.Map{
"id": cluster.GetId(),
"name": cluster.GetName(),
"gatewayDomain": cluster.GetServiceDomain(),
"defaultTTL": cluster.GetDefaultTTL(),
"fallbackTimeout": cluster.GetFallbackTimeoutMs(),
"installDir": cluster.GetInstallDir(),
"isOn": cluster.GetIsOn(),
"isDefault": cluster.GetIsDefault(),
"countAllNodes": countAllNodes,
"countActiveNodes": countActiveNodes,
})
}
return result, nil
}
func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map, error) {
if clusterID > 0 {
resp, err := parent.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(parent.AdminContext(), &pb.FindHTTPDNSClusterRequest{
ClusterId: clusterID,
})
if err != nil {
return nil, err
}
if resp.GetCluster() != nil {
cluster := resp.GetCluster()
return maps.Map{
"id": cluster.GetId(),
"name": cluster.GetName(),
"gatewayDomain": cluster.GetServiceDomain(),
"defaultTTL": cluster.GetDefaultTTL(),
"fallbackTimeout": cluster.GetFallbackTimeoutMs(),
"installDir": cluster.GetInstallDir(),
"isOn": cluster.GetIsOn(),
"isDefault": cluster.GetIsDefault(),
"tlsPolicyJSON": cluster.GetTlsPolicyJSON(),
}, nil
}
}
clusters, err := listClusterMaps(parent, "")
if err != nil {
return nil, err
}
if len(clusters) == 0 {
return maps.Map{
"id": int64(0),
"name": "",
"gatewayDomain": "",
"defaultTTL": 30,
"fallbackTimeout": 300,
"installDir": "/opt/edge-httpdns",
}, nil
}
return clusters[0], nil
}
func listNodeMaps(parent *actionutils.ParentAction, clusterID int64) ([]maps.Map, error) {
resp, err := parent.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(parent.AdminContext(), &pb.ListHTTPDNSNodesRequest{
ClusterId: clusterID,
})
if err != nil {
return nil, err
}
result := make([]maps.Map, 0, len(resp.GetNodes()))
for _, node := range resp.GetNodes() {
statusMap := decodeNodeStatus(node.GetStatusJSON())
installStatusMap := decodeInstallStatus(node.GetInstallStatusJSON())
ip := node.GetName()
if parsed := strings.TrimSpace(statusMap.GetString("hostIP")); len(parsed) > 0 {
ip = parsed
}
nodeMap := maps.Map{
"id": node.GetId(),
"clusterId": node.GetClusterId(),
"name": node.GetName(),
"isOn": node.GetIsOn(),
"isUp": node.GetIsUp(),
"isInstalled": node.GetIsInstalled(),
"isActive": node.GetIsActive(),
"installDir": node.GetInstallDir(),
"uniqueId": node.GetUniqueId(),
"secret": node.GetSecret(),
"status": statusMap,
"installStatus": installStatusMap,
"region": nil,
"login": nil,
"apiNodeAddrs": []string{},
"cluster": maps.Map{
"id": node.GetClusterId(),
"installDir": node.GetInstallDir(),
},
"ipAddresses": []maps.Map{
{
"id": node.GetId(),
"name": "Public IP",
"ip": ip,
"canAccess": true,
"isOn": node.GetIsOn(),
"isUp": node.GetIsUp(),
},
},
}
result = append(result, nodeMap)
}
return result, nil
}
func findNodeMap(parent *actionutils.ParentAction, nodeID int64) (maps.Map, error) {
resp, err := parent.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(parent.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: nodeID,
})
if err != nil {
return nil, err
}
if resp.GetNode() == nil {
return maps.Map{}, nil
}
nodes, err := listNodeMaps(parent, resp.GetNode().GetClusterId())
if err != nil {
return nil, err
}
for _, node := range nodes {
if node.GetInt64("id") == nodeID {
return node, nil
}
}
return maps.Map{
"id": resp.GetNode().GetId(),
"name": resp.GetNode().GetName(),
}, nil
}
func createNode(parent *actionutils.ParentAction, clusterID int64, name string, installDir string) error {
_, err := parent.RPC().HTTPDNSNodeRPC().CreateHTTPDNSNode(parent.AdminContext(), &pb.CreateHTTPDNSNodeRequest{
ClusterId: clusterID,
Name: strings.TrimSpace(name),
InstallDir: strings.TrimSpace(installDir),
IsOn: true,
})
return err
}
func updateNode(parent *actionutils.ParentAction, nodeID int64, name string, installDir string, isOn bool) error {
_, err := parent.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNode(parent.AdminContext(), &pb.UpdateHTTPDNSNodeRequest{
NodeId: nodeID,
Name: strings.TrimSpace(name),
InstallDir: strings.TrimSpace(installDir),
IsOn: isOn,
})
return err
}
func deleteNode(parent *actionutils.ParentAction, nodeID int64) error {
_, err := parent.RPC().HTTPDNSNodeRPC().DeleteHTTPDNSNode(parent.AdminContext(), &pb.DeleteHTTPDNSNodeRequest{
NodeId: nodeID,
})
return err
}
func decodeNodeStatus(raw []byte) maps.Map {
status := &nodeconfigs.NodeStatus{}
if len(raw) > 0 {
_ = json.Unmarshal(raw, status)
}
cpuText := fmt.Sprintf("%.2f%%", status.CPUUsage*100)
memText := fmt.Sprintf("%.2f%%", status.MemoryUsage*100)
return maps.Map{
"isActive": status.IsActive,
"updatedAt": status.UpdatedAt,
"hostname": status.Hostname,
"hostIP": status.HostIP,
"cpuUsage": status.CPUUsage,
"cpuUsageText": cpuText,
"memUsage": status.MemoryUsage,
"memUsageText": memText,
"load1m": status.Load1m,
"load5m": status.Load5m,
"load15m": status.Load15m,
"buildVersion": status.BuildVersion,
"cpuPhysicalCount": status.CPUPhysicalCount,
"cpuLogicalCount": status.CPULogicalCount,
"exePath": status.ExePath,
}
}
func decodeInstallStatus(raw []byte) maps.Map {
if len(raw) == 0 {
return maps.Map{
"isRunning": false,
"isFinished": true,
"isOk": true,
"error": "",
"errorCode": "",
}
}
result := maps.Map{}
if err := json.Unmarshal(raw, &result); err != nil {
return maps.Map{
"isRunning": false,
"isFinished": true,
"isOk": true,
"error": "",
"errorCode": "",
}
}
return result
}

View File

@@ -1,7 +1,14 @@
package clusters
import (
"encoding/json"
"net"
"regexp"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
@@ -12,18 +19,52 @@ type UpdateNodeSSHAction struct {
func (this *UpdateNodeSSHAction) RunGet(params struct {
NodeId int64
}) {
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
clusterId := int64(0)
nodeName := ""
if resp.GetNode() != nil {
clusterId = resp.GetNode().GetClusterId()
nodeName = resp.GetNode().GetName()
}
this.Data["nodeId"] = params.NodeId
this.Data["clusterId"] = 0
this.Data["clusterId"] = clusterId
this.Data["node"] = maps.Map{
"id": params.NodeId,
"name": "Mock Node",
"name": nodeName,
}
this.Data["loginId"] = 0
this.Data["params"] = maps.Map{
"host": "1.2.3.4",
loginParams := maps.Map{
"host": "",
"port": 22,
"grantId": 0,
}
this.Data["loginId"] = 0
if resp.GetNode() != nil && len(resp.GetNode().GetInstallStatusJSON()) > 0 {
installStatus := maps.Map{}
_ = json.Unmarshal(resp.GetNode().GetInstallStatusJSON(), &installStatus)
sshInfo := installStatus.GetMap("ssh")
if sshInfo != nil {
if host := strings.TrimSpace(sshInfo.GetString("host")); len(host) > 0 {
loginParams["host"] = host
}
if port := sshInfo.GetInt("port"); port > 0 {
loginParams["port"] = port
}
if grantID := sshInfo.GetInt64("grantId"); grantID > 0 {
loginParams["grantId"] = grantID
}
}
}
this.Data["params"] = loginParams
this.Data["grant"] = nil
this.Show()
}
@@ -34,6 +75,66 @@ func (this *UpdateNodeSSHAction) RunPost(params struct {
SshHost string
SshPort int
GrantId int64
Must *actions.Must
}) {
params.SshHost = strings.TrimSpace(params.SshHost)
params.Must.
Field("sshHost", params.SshHost).
Require("请输入 SSH 主机地址").
Field("sshPort", params.SshPort).
Gt(0, "SSH 端口必须大于 0").
Lt(65535, "SSH 端口必须小于 65535")
if params.GrantId <= 0 {
this.Fail("请选择节点登录认证信息")
}
if regexp.MustCompile(`^\d+\.\d+\.\d+\.\d+$`).MatchString(params.SshHost) && net.ParseIP(params.SshHost) == nil {
this.Fail("SSH 主机地址 IP 格式错误")
}
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
node := resp.GetNode()
if node == nil {
this.Fail("节点不存在")
return
}
installStatus := maps.Map{
"isRunning": false,
"isFinished": true,
"isOk": node.GetIsInstalled(),
"error": "",
"errorCode": "",
}
if len(node.GetInstallStatusJSON()) > 0 {
_ = json.Unmarshal(node.GetInstallStatusJSON(), &installStatus)
}
installStatus["ssh"] = maps.Map{
"host": params.SshHost,
"port": params.SshPort,
"grantId": params.GrantId,
}
installStatusJSON, _ := json.Marshal(installStatus)
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
NodeId: params.NodeId,
IsUp: node.GetIsUp(),
IsInstalled: node.GetIsInstalled(),
IsActive: node.GetIsActive(),
StatusJSON: node.GetStatusJSON(),
InstallStatusJSON: installStatusJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -1,9 +1,6 @@
package ech
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
)
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
type AuditAction struct {
actionutils.ParentAction
@@ -14,15 +11,5 @@ func (this *AuditAction) Init() {
}
func (this *AuditAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["auditLogs"] = []map[string]interface{}{
{
"id": 1,
"scope": "tenant:1001 -> domain:api.business.com -> region:cn-hz",
"operator": "admin",
"result": "success",
"createdAt": "2026-02-21 10:00:00",
},
}
this.Show()
this.RedirectURL("/httpdns/apps")
}

View File

@@ -1,9 +1,6 @@
package ech
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
)
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
type IndexAction struct {
actionutils.ParentAction
@@ -14,34 +11,5 @@ func (this *IndexAction) Init() {
}
func (this *IndexAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["health"] = map[string]interface{}{
"keySyncRate": 0.9995,
"decryptFailRate": 0.0001,
"isHealthy": true,
}
this.Data["echLogs"] = []map[string]interface{}{
{
"id": 1024,
"version": "ech-v20260221-01",
"recordType": "HTTPS(Type65)",
"publicKey": "BFCf8h5Qmock_public_key_for_ui_preview_lx9v2k7p0n",
"publishTime": "2026-02-21 00:00:00",
"syncStatus": "success",
"nodesPending": 0,
"isCurrent": true,
},
{
"id": 1023,
"version": "ech-v20260220-01",
"recordType": "TXT",
"publicKey": "BE9x3a2Qmock_prev_key_for_ui_preview_vd1n7x5k2c",
"publishTime": "2026-02-20 00:00:00",
"syncStatus": "success",
"nodesPending": 0,
"isCurrent": false,
},
}
this.Show()
this.RedirectURL("/httpdns/apps")
}

View File

@@ -16,8 +16,7 @@ func (this *RollbackMfaPopupAction) Init() {
func (this *RollbackMfaPopupAction) RunGet(params struct {
LogId int64
}) {
this.Data["logId"] = params.LogId
this.Show()
this.RedirectURL("/httpdns/apps")
}
func (this *RollbackMfaPopupAction) RunPost(params struct {
@@ -29,8 +28,5 @@ func (this *RollbackMfaPopupAction) RunPost(params struct {
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("reason", params.Reason).Require("please input rollback reason")
params.Must.Field("otpCode1", params.OtpCode1).Require("please input first otp code")
params.Must.Field("otpCode2", params.OtpCode2).Require("please input second otp code")
this.Success()
this.Fail("feature removed")
}

View File

@@ -1,11 +1,6 @@
package policies
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
type IndexAction struct {
actionutils.ParentAction
@@ -16,52 +11,9 @@ func (this *IndexAction) Init() {
}
func (this *IndexAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["policies"] = loadGlobalPolicies()
this.Data["availableClusters"] = loadAvailableDeployClusters()
this.Show()
this.RedirectURL("/httpdns/clusters")
}
func (this *IndexAction) RunPost(params struct {
DefaultClusterId int64
DefaultTTL int
DefaultFallbackMs int
Must *actions.Must
CSRF *actionutils.CSRF
}) {
if params.DefaultClusterId <= 0 || !isValidClusterID(params.DefaultClusterId) {
this.Fail("please select a valid default cluster")
return
}
params.Must.Field("defaultTTL", params.DefaultTTL).Gt(0, "default ttl should be > 0")
params.Must.Field("defaultFallbackMs", params.DefaultFallbackMs).Gt(0, "default fallback should be > 0")
if params.DefaultTTL > 86400 {
this.Fail("default TTL should be <= 86400")
return
}
if params.DefaultFallbackMs > 10000 {
this.Fail("default fallback should be <= 10000 ms")
return
}
saveGlobalPolicies(maps.Map{
"defaultClusterId": params.DefaultClusterId,
"enableUserDomainVerify": true,
"defaultTTL": params.DefaultTTL,
"defaultFallbackMs": params.DefaultFallbackMs,
})
func (this *IndexAction) RunPost(params struct{}) {
this.Success()
}
func isValidClusterID(clusterID int64) bool {
for _, cluster := range loadAvailableDeployClusters() {
if cluster.GetInt64("id") == clusterID {
return true
}
}
return false
}

View File

@@ -1,180 +0,0 @@
package policies
import (
"strings"
"sync"
"github.com/iwind/TeaGo/maps"
)
var globalPoliciesStore = struct {
sync.RWMutex
data maps.Map
publicSniPools []maps.Map
publicSniCertificates []maps.Map
}{
data: maps.Map{
"defaultClusterId": int64(1),
"enableUserDomainVerify": true,
"defaultTTL": 30,
"defaultFallbackMs": 300,
},
publicSniPools: []maps.Map{
{
"domain": "public-sni-a.waf.example.com",
"certId": "cert_public_sni_a",
},
{
"domain": "public-sni-b.waf.example.com",
"certId": "cert_public_sni_b",
},
},
publicSniCertificates: []maps.Map{
{
"id": "cert_public_sni_a",
"name": "public-sni-a.waf.example.com",
"issuer": "Mock CA",
"expiresAt": "2026-12-31 23:59:59",
},
{
"id": "cert_public_sni_b",
"name": "public-sni-b.waf.example.com",
"issuer": "Mock CA",
"expiresAt": "2027-03-31 23:59:59",
},
},
}
func loadGlobalPolicies() maps.Map {
globalPoliciesStore.RLock()
defer globalPoliciesStore.RUnlock()
return maps.Map{
"defaultClusterId": globalPoliciesStore.data.GetInt64("defaultClusterId"),
"enableUserDomainVerify": true,
"defaultTTL": globalPoliciesStore.data.GetInt("defaultTTL"),
"defaultFallbackMs": globalPoliciesStore.data.GetInt("defaultFallbackMs"),
}
}
func saveGlobalPolicies(policies maps.Map) {
globalPoliciesStore.Lock()
globalPoliciesStore.data = maps.Map{
"defaultClusterId": policies.GetInt64("defaultClusterId"),
"enableUserDomainVerify": true,
"defaultTTL": policies.GetInt("defaultTTL"),
"defaultFallbackMs": policies.GetInt("defaultFallbackMs"),
}
globalPoliciesStore.Unlock()
}
func loadPublicSNICertificates() []maps.Map {
globalPoliciesStore.RLock()
defer globalPoliciesStore.RUnlock()
return cloneMapSlice(globalPoliciesStore.publicSniCertificates)
}
// LoadPublicSNICertificates returns global public SNI certificates for other httpdns modules.
func LoadPublicSNICertificates() []maps.Map {
return loadPublicSNICertificates()
}
func loadAvailableDeployClusters() []maps.Map {
return []maps.Map{
{
"id": int64(1),
"name": "gateway-cn-hz",
"region": "cn-hangzhou",
"gatewayDomain": "gw-hz.httpdns.example.com",
},
{
"id": int64(2),
"name": "gateway-cn-bj",
"region": "cn-beijing",
"gatewayDomain": "gw-bj.httpdns.example.com",
},
}
}
// LoadAvailableDeployClusters returns selectable clusters for HTTPDNS user defaults.
func LoadAvailableDeployClusters() []maps.Map {
return loadAvailableDeployClusters()
}
// LoadDefaultClusterID returns current default deploy cluster id for HTTPDNS users.
func LoadDefaultClusterID() int64 {
globalPoliciesStore.RLock()
defer globalPoliciesStore.RUnlock()
return globalPoliciesStore.data.GetInt64("defaultClusterId")
}
// SaveDefaultClusterID updates default deploy cluster id.
// Pass 0 to clear and let caller fallback to first available cluster.
func SaveDefaultClusterID(clusterID int64) {
globalPoliciesStore.Lock()
globalPoliciesStore.data["defaultClusterId"] = clusterID
globalPoliciesStore.Unlock()
}
// LoadClusterGatewayByID returns gateway domain for the selected cluster.
func LoadClusterGatewayByID(clusterID int64) string {
clusters := loadAvailableDeployClusters()
for _, cluster := range clusters {
if cluster.GetInt64("id") == clusterID {
gatewayDomain := strings.TrimSpace(cluster.GetString("gatewayDomain"))
if len(gatewayDomain) > 0 {
return gatewayDomain
}
}
}
if len(clusters) > 0 {
fallback := strings.TrimSpace(clusters[0].GetString("gatewayDomain"))
if len(fallback) > 0 {
return fallback
}
}
return "gw.httpdns.example.com"
}
// LoadPublicSNIPools returns global public SNI domain pool for other httpdns modules.
func LoadPublicSNIPools() []maps.Map {
globalPoliciesStore.RLock()
defer globalPoliciesStore.RUnlock()
return cloneMapSlice(globalPoliciesStore.publicSniPools)
}
// HasPublicSNIDomain checks if a public SNI domain exists in global pool.
func HasPublicSNIDomain(domain string) bool {
domain = strings.ToLower(strings.TrimSpace(domain))
if len(domain) == 0 {
return false
}
globalPoliciesStore.RLock()
defer globalPoliciesStore.RUnlock()
for _, pool := range globalPoliciesStore.publicSniPools {
if strings.ToLower(strings.TrimSpace(pool.GetString("domain"))) == domain {
return true
}
}
return false
}
func cloneMapSlice(src []maps.Map) []maps.Map {
result := make([]maps.Map, 0, len(src))
for _, item := range src {
cloned := maps.Map{}
for k, v := range item {
cloned[k] = v
}
result = append(result, cloned)
}
return result
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
@@ -40,120 +42,88 @@ func (this *IndexAction) RunGet(params struct {
this.Data["status"] = params.Status
this.Data["keyword"] = params.Keyword
clusters := []map[string]interface{}{
{"id": int64(1), "name": "gateway-cn-hz"},
{"id": int64(2), "name": "gateway-cn-bj"},
clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
nodes := []map[string]interface{}{
{"id": int64(101), "clusterId": int64(1), "name": "hz-node-01"},
{"id": int64(102), "clusterId": int64(1), "name": "hz-node-02"},
{"id": int64(201), "clusterId": int64(2), "name": "bj-node-01"},
clusters := make([]map[string]interface{}, 0, len(clusterResp.GetClusters()))
nodes := make([]map[string]interface{}, 0)
for _, cluster := range clusterResp.GetClusters() {
clusters = append(clusters, map[string]interface{}{
"id": cluster.GetId(),
"name": cluster.GetName(),
})
nodeResp, err := this.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(this.AdminContext(), &pb.ListHTTPDNSNodesRequest{
ClusterId: cluster.GetId(),
})
if err != nil {
this.ErrorPage(err)
return
}
for _, node := range nodeResp.GetNodes() {
nodes = append(nodes, map[string]interface{}{
"id": node.GetId(),
"clusterId": node.GetClusterId(),
"name": node.GetName(),
})
}
}
this.Data["clusters"] = clusters
this.Data["nodes"] = nodes
allLogs := []map[string]interface{}{
{
"time": "2026-02-23 10:21:51",
"clusterId": int64(1),
"clusterName": "gateway-cn-hz",
"nodeId": int64(101),
"nodeName": "hz-node-01",
"appName": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
"appId": "ab12xc34s2",
"domain": "api.business.com",
"query": "A",
"clientIp": "203.0.113.25",
"os": "Android",
"sdkVersion": "2.4.1",
"ips": "198.51.100.10,198.51.100.11",
"status": "success",
"errorCode": "none",
"costMs": 37,
"pinningMode": "report",
"pinningResult": "warn",
"sanMode": "strict",
"sanResult": "pass",
},
{
"time": "2026-02-23 10:18:02",
"clusterId": int64(2),
"clusterName": "gateway-cn-bj",
"nodeId": int64(201),
"nodeName": "bj-node-01",
"appName": "\u652f\u4ed8\u7f51\u5173\u4e1a\u52a1",
"appId": "vd8992ksm1",
"domain": "payment.business.com",
"query": "A",
"clientIp": "198.51.100.67",
"os": "iOS",
"sdkVersion": "2.3.9",
"ips": "198.51.100.22",
"status": "failed",
"errorCode": "40301",
"costMs": 52,
"pinningMode": "enforce",
"pinningResult": "fail",
"sanMode": "strict",
"sanResult": "fail",
},
{
"time": "2026-02-23 10:12:44",
"clusterId": int64(1),
"clusterName": "gateway-cn-hz",
"nodeId": int64(102),
"nodeName": "hz-node-02",
"appName": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
"appId": "ab12xc34s2",
"domain": "www.aliyun.com",
"query": "A",
"clientIp": "106.11.63.180",
"os": "Unknown",
"sdkVersion": "none",
"ips": "3.33.120.190,15.197.120.33",
"status": "success",
"errorCode": "none",
"costMs": 41,
"pinningMode": "off",
"pinningResult": "na",
"sanMode": "report",
"sanResult": "warn",
},
logResp, err := this.RPC().HTTPDNSAccessLogRPC().ListHTTPDNSAccessLogs(this.AdminContext(), &pb.ListHTTPDNSAccessLogsRequest{
Day: "",
ClusterId: params.ClusterId,
NodeId: params.NodeId,
AppId: strings.TrimSpace(params.AppId),
Domain: strings.TrimSpace(params.Domain),
Status: strings.TrimSpace(params.Status),
Keyword: strings.TrimSpace(params.Keyword),
Offset: 0,
Size: 100,
})
if err != nil {
this.ErrorPage(err)
return
}
status := strings.TrimSpace(strings.ToLower(params.Status))
appID := strings.TrimSpace(strings.ToLower(params.AppId))
domain := strings.TrimSpace(strings.ToLower(params.Domain))
keyword := strings.TrimSpace(strings.ToLower(params.Keyword))
logs := make([]map[string]interface{}, 0, len(logResp.GetLogs()))
for _, item := range logResp.GetLogs() {
createdTime := ""
if item.GetCreatedAt() > 0 {
createdTime = timeutil.FormatTime("Y-m-d H:i:s", item.GetCreatedAt())
}
status := item.GetStatus()
if len(status) == 0 {
status = "failed"
}
errorCode := item.GetErrorCode()
if len(errorCode) == 0 {
errorCode = "none"
}
filtered := make([]map[string]interface{}, 0)
for _, log := range allLogs {
if params.ClusterId > 0 && log["clusterId"].(int64) != params.ClusterId {
continue
}
if params.NodeId > 0 && log["nodeId"].(int64) != params.NodeId {
continue
}
if len(status) > 0 && log["status"].(string) != status {
continue
}
if len(appID) > 0 && !strings.Contains(strings.ToLower(log["appId"].(string)), appID) {
continue
}
if len(domain) > 0 && !strings.Contains(strings.ToLower(log["domain"].(string)), domain) {
continue
}
if len(keyword) > 0 {
if !strings.Contains(strings.ToLower(log["appName"].(string)), keyword) &&
!strings.Contains(strings.ToLower(log["domain"].(string)), keyword) &&
!strings.Contains(strings.ToLower(log["clientIp"].(string)), keyword) &&
!strings.Contains(strings.ToLower(log["ips"].(string)), keyword) {
continue
}
}
filtered = append(filtered, log)
logs = append(logs, map[string]interface{}{
"time": createdTime,
"clusterId": item.GetClusterId(),
"clusterName": item.GetClusterName(),
"nodeId": item.GetNodeId(),
"nodeName": item.GetNodeName(),
"appName": item.GetAppName(),
"appId": item.GetAppId(),
"domain": item.GetDomain(),
"query": item.GetQtype(),
"clientIp": item.GetClientIP(),
"os": item.GetOs(),
"sdkVersion": item.GetSdkVersion(),
"ips": item.GetResultIPs(),
"status": status,
"errorCode": errorCode,
"costMs": item.GetCostMs(),
})
}
this.Data["resolveLogs"] = filtered
this.Data["resolveLogs"] = logs
this.Show()
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
@@ -40,105 +42,75 @@ func (this *IndexAction) RunGet(params struct {
this.Data["level"] = params.Level
this.Data["keyword"] = params.Keyword
clusters := []map[string]interface{}{
{"id": int64(1), "name": "gateway-cn-hz"},
{"id": int64(2), "name": "gateway-cn-bj"},
clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
nodes := []map[string]interface{}{
{"id": int64(101), "clusterId": int64(1), "name": "hz-node-01"},
{"id": int64(102), "clusterId": int64(1), "name": "hz-node-02"},
{"id": int64(201), "clusterId": int64(2), "name": "bj-node-01"},
clusters := make([]map[string]interface{}, 0, len(clusterResp.GetClusters()))
nodes := make([]map[string]interface{}, 0)
for _, cluster := range clusterResp.GetClusters() {
clusters = append(clusters, map[string]interface{}{
"id": cluster.GetId(),
"name": cluster.GetName(),
})
nodeResp, err := this.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(this.AdminContext(), &pb.ListHTTPDNSNodesRequest{
ClusterId: cluster.GetId(),
})
if err != nil {
this.ErrorPage(err)
return
}
for _, node := range nodeResp.GetNodes() {
nodes = append(nodes, map[string]interface{}{
"id": node.GetId(),
"clusterId": node.GetClusterId(),
"name": node.GetName(),
})
}
}
this.Data["clusters"] = clusters
this.Data["nodes"] = nodes
allLogs := []map[string]interface{}{
{
"createdTime": "2026-02-23 10:30:12",
"clusterId": int64(1),
"clusterName": "gateway-cn-hz",
"nodeId": int64(101),
"nodeName": "hz-node-01",
"level": "info",
"tag": "config",
"module": "resolver",
"description": "\u914d\u7f6e\u70ed\u52a0\u8f7d\u5b8c\u6210\uff0c\u5df2\u542f\u7528\u6700\u65b0\u7b56\u7565\u5feb\u7167\u3002",
"count": int64(1),
"requestId": "rid-20260223-001",
},
{
"createdTime": "2026-02-23 10:27:38",
"clusterId": int64(2),
"clusterName": "gateway-cn-bj",
"nodeId": int64(201),
"nodeName": "bj-node-01",
"level": "warning",
"tag": "sni",
"module": "policy-engine",
"description": "\u68c0\u6d4b\u5230 level3 \u6761\u4ef6\u4e0d\u6ee1\u8db3\uff0c\u81ea\u52a8\u964d\u7ea7\u5230 level2\u3002",
"count": int64(3),
"requestId": "rid-20260223-002",
},
{
"createdTime": "2026-02-23 10:22:49",
"clusterId": int64(1),
"clusterName": "gateway-cn-hz",
"nodeId": int64(102),
"nodeName": "hz-node-02",
"level": "success",
"tag": "cache-refresh",
"module": "cache",
"description": "\u57df\u540d\u7f13\u5b58\u5237\u65b0\u4efb\u52a1\u5b8c\u6210\uff0c\u6210\u529f 2/2\u3002",
"count": int64(1),
"requestId": "rid-20260223-003",
},
{
"createdTime": "2026-02-23 10:18:11",
"clusterId": int64(1),
"clusterName": "gateway-cn-hz",
"nodeId": int64(101),
"nodeName": "hz-node-01",
"level": "error",
"tag": "upstream",
"module": "resolver",
"description": "\u4e0a\u6e38\u6743\u5a01 DNS \u8bf7\u6c42\u8d85\u65f6\uff0c\u89e6\u53d1\u91cd\u8bd5\u540e\u6062\u590d\u3002",
"count": int64(2),
"requestId": "rid-20260223-004",
},
day := strings.TrimSpace(params.DayFrom)
if len(day) == 0 {
day = strings.TrimSpace(params.DayTo)
}
logResp, err := this.RPC().HTTPDNSRuntimeLogRPC().ListHTTPDNSRuntimeLogs(this.AdminContext(), &pb.ListHTTPDNSRuntimeLogsRequest{
Day: day,
ClusterId: params.ClusterId,
NodeId: params.NodeId,
Level: strings.TrimSpace(params.Level),
Keyword: strings.TrimSpace(params.Keyword),
Offset: 0,
Size: 100,
})
if err != nil {
this.ErrorPage(err)
return
}
keyword := strings.TrimSpace(strings.ToLower(params.Keyword))
filtered := make([]map[string]interface{}, 0)
for _, log := range allLogs {
if params.ClusterId > 0 && log["clusterId"].(int64) != params.ClusterId {
continue
runtimeLogs := make([]map[string]interface{}, 0, len(logResp.GetLogs()))
for _, item := range logResp.GetLogs() {
createdTime := ""
if item.GetCreatedAt() > 0 {
createdTime = timeutil.FormatTime("Y-m-d H:i:s", item.GetCreatedAt())
}
if params.NodeId > 0 && log["nodeId"].(int64) != params.NodeId {
continue
}
if len(params.Level) > 0 && log["level"].(string) != params.Level {
continue
}
if len(params.DayFrom) > 0 {
if log["createdTime"].(string)[:10] < params.DayFrom {
continue
}
}
if len(params.DayTo) > 0 {
if log["createdTime"].(string)[:10] > params.DayTo {
continue
}
}
if len(keyword) > 0 {
if !strings.Contains(strings.ToLower(log["tag"].(string)), keyword) &&
!strings.Contains(strings.ToLower(log["description"].(string)), keyword) &&
!strings.Contains(strings.ToLower(log["nodeName"].(string)), keyword) {
continue
}
}
filtered = append(filtered, log)
runtimeLogs = append(runtimeLogs, map[string]interface{}{
"createdTime": createdTime,
"clusterId": item.GetClusterId(),
"clusterName": item.GetClusterName(),
"nodeId": item.GetNodeId(),
"nodeName": item.GetNodeName(),
"level": item.GetLevel(),
"tag": item.GetType(),
"module": item.GetModule(),
"description": item.GetDescription(),
"count": item.GetCount(),
"requestId": item.GetRequestId(),
})
}
this.Data["runtimeLogs"] = filtered
this.Data["runtimeLogs"] = runtimeLogs
this.Show()
}

View File

@@ -3,7 +3,8 @@ package sandbox
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
@@ -17,29 +18,48 @@ func (this *IndexAction) Init() {
func (this *IndexAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["clusters"] = policies.LoadAvailableDeployClusters()
this.Data["apps"] = []map[string]interface{}{
{
"id": int64(1),
"name": "主站移动业务",
"appId": "ab12xc34s2",
"clusterId": int64(1),
"domains": []string{"api.business.com", "www.aliyun.com"},
},
{
"id": int64(2),
"name": "支付网关业务",
"appId": "vd8992ksm1",
"clusterId": int64(2),
"domains": []string{"payment.business.com"},
},
{
"id": int64(3),
"name": "海外灰度测试",
"appId": "ov7711hkq9",
"clusterId": int64(1),
"domains": []string{"global.example.com", "edge.example.com"},
},
clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
clusters := make([]maps.Map, 0, len(clusterResp.GetClusters()))
for _, cluster := range clusterResp.GetClusters() {
clusters = append(clusters, maps.Map{
"id": cluster.GetId(),
"name": cluster.GetName(),
})
}
this.Data["clusters"] = clusters
appResp, err := this.RPC().HTTPDNSAppRPC().FindAllHTTPDNSApps(this.AdminContext(), &pb.FindAllHTTPDNSAppsRequest{})
if err != nil {
this.ErrorPage(err)
return
}
apps := make([]maps.Map, 0, len(appResp.GetApps()))
for _, app := range appResp.GetApps() {
domainResp, err := this.RPC().HTTPDNSDomainRPC().ListHTTPDNSDomainsWithAppId(this.AdminContext(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
AppDbId: app.GetId(),
})
if err != nil {
this.ErrorPage(err)
return
}
domains := make([]string, 0, len(domainResp.GetDomains()))
for _, domain := range domainResp.GetDomains() {
domains = append(domains, domain.GetDomain())
}
apps = append(apps, maps.Map{
"id": app.GetId(),
"name": app.GetName(),
"appId": app.GetAppId(),
"clusterId": app.GetPrimaryClusterId(),
"primaryClusterId": app.GetPrimaryClusterId(),
"backupClusterId": app.GetBackupClusterId(),
"domains": domains,
})
}
this.Data["apps"] = apps
this.Show()
}

View File

@@ -1,13 +1,19 @@
package sandbox
import (
"net"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/url"
"strconv"
"strings"
"time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/index/loginutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
)
type TestAction struct {
@@ -21,96 +27,125 @@ func (this *TestAction) RunPost(params struct {
ClientIp string
Qtype string
}) {
if len(params.ClientIp) == 0 {
params.ClientIp = "203.0.113.100"
clientIP := strings.TrimSpace(params.ClientIp)
if len(clientIP) == 0 {
clientIP = strings.TrimSpace(loginutils.RemoteIP(&this.ActionObject))
}
qtype := strings.ToUpper(strings.TrimSpace(params.Qtype))
if len(qtype) == 0 {
qtype = "A"
}
clientSubnet := this.maskSubnet(params.ClientIp, 24, 56)
if len(clientSubnet) == 0 {
clientSubnet = "203.0.113.0/24"
resp, err := this.RPC().HTTPDNSSandboxRPC().TestHTTPDNSResolve(this.AdminContext(), &pb.TestHTTPDNSResolveRequest{
ClusterId: params.ClusterId,
AppId: strings.TrimSpace(params.AppId),
Domain: strings.TrimSpace(params.Domain),
Qtype: qtype,
ClientIP: clientIP,
Sid: "",
SdkVersion: "",
Os: "",
})
if err != nil {
this.ErrorPage(err)
return
}
sniPolicy := "empty"
publicSNI := ""
ecsMode := "off"
ecsIPv4Prefix := 24
ecsIPv6Prefix := 56
pinningMode := "off"
sanMode := "off"
clusterDomain := ""
if params.ClusterId > 0 {
clusterResp, findErr := this.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(this.AdminContext(), &pb.FindHTTPDNSClusterRequest{
ClusterId: params.ClusterId,
})
if findErr == nil && clusterResp.GetCluster() != nil {
clusterDomain = strings.TrimSpace(clusterResp.GetCluster().GetServiceDomain())
}
}
if len(clusterDomain) == 0 {
clusterDomain = "httpdns.example.com"
}
query := url.Values{}
query.Set("appId", params.AppId)
query.Set("dn", params.Domain)
query.Set("cip", params.ClientIp)
query.Set("qtype", params.Qtype)
clusterServiceDomain := policies.LoadClusterGatewayByID(params.ClusterId)
if len(clusterServiceDomain) == 0 {
clusterServiceDomain = "gw.httpdns.example.com"
query.Set("qtype", qtype)
if len(clientIP) > 0 {
query.Set("cip", clientIP)
}
signEnabled, signSecret := this.findAppSignConfig(params.AppId)
if signEnabled && len(signSecret) > 0 {
exp := strconv.FormatInt(time.Now().Unix()+300, 10)
nonce := "sandbox-" + rands.HexString(16)
sign := buildSandboxResolveSign(signSecret, params.AppId, params.Domain, qtype, exp, nonce)
query.Set("exp", exp)
query.Set("nonce", nonce)
query.Set("sign", sign)
}
requestURL := "https://" + clusterDomain + "/resolve?" + query.Encode()
resultCode := 1
if strings.EqualFold(resp.GetCode(), "SUCCESS") {
resultCode = 0
}
rows := make([]maps.Map, 0, len(resp.GetRecords()))
ips := make([]string, 0, len(resp.GetRecords()))
lineName := strings.TrimSpace(resp.GetClientCarrier())
if len(lineName) == 0 {
lineName = "-"
}
for _, record := range resp.GetRecords() {
ips = append(ips, record.GetIp())
if lineName == "-" && len(record.GetLine()) > 0 {
lineName = record.GetLine()
}
rows = append(rows, maps.Map{
"domain": resp.GetDomain(),
"type": record.GetType(),
"ip": record.GetIp(),
"ttl": record.GetTtl(),
"region": record.GetRegion(),
"line": record.GetLine(),
})
}
requestURL := "https://" + clusterServiceDomain + "/resolve?" + query.Encode()
this.Data["result"] = maps.Map{
"code": 0,
"message": "ok (mock)",
"requestId": "mock-rid-20260221-001",
"code": resultCode,
"message": resp.GetMessage(),
"requestId": resp.GetRequestId(),
"data": maps.Map{
"request_url": requestURL,
"client_ip": params.ClientIp,
"client_region": "中国, 上海, 上海",
"line_name": "默认线路",
"ips": []string{"203.0.113.10", "203.0.113.11"},
"records": []maps.Map{
{
"domain": params.Domain,
"type": params.Qtype,
"ip": "203.0.113.10",
"ttl": 30,
"region": "中国-上海-上海",
"line": "默认线路",
},
{
"domain": params.Domain,
"type": params.Qtype,
"ip": "203.0.113.11",
"ttl": 30,
"region": "中国-上海-上海",
"line": "默认线路",
},
},
"ttl": 30,
"sni_policy": sniPolicy,
"public_sni": publicSNI,
"client_subnet": clientSubnet,
"ecs_mode": ecsMode,
"ecs_ipv4_prefix": ecsIPv4Prefix,
"ecs_ipv6_prefix": ecsIPv6Prefix,
"pinning_mode": pinningMode,
"san_mode": sanMode,
"fingerprint_algo": "sha256",
"cert_fingerprints": []string{"8f41ab8d32fca7f9a2b0fdd09a0e2ccf31e5579cd3a0b65f1a9f2f2ec8f38d4a"},
"verify_headers": maps.Map{
"X-Edge-Resolve-AppId": params.AppId,
"X-Edge-Resolve-Expire": "1768953600",
"X-Edge-Resolve-Nonce": "mock-nonce-123456",
"X-Edge-Resolve-Target": params.Domain,
"X-Edge-Resolve-Sign": "mock-signature-base64url",
},
"client_ip": resp.GetClientIP(),
"client_region": resp.GetClientRegion(),
"line_name": lineName,
"ips": ips,
"records": rows,
"ttl": resp.GetTtl(),
},
}
this.Success()
}
func (this *TestAction) maskSubnet(ip string, ipv4Prefix int, ipv6Prefix int) string {
parsed := net.ParseIP(ip)
if parsed == nil {
return ""
func (this *TestAction) findAppSignConfig(appId string) (bool, string) {
appId = strings.TrimSpace(appId)
if len(appId) == 0 {
return false, ""
}
ipv4 := parsed.To4()
if ipv4 != nil {
mask := net.CIDRMask(ipv4Prefix, 32)
return ipv4.Mask(mask).String() + "/" + strconv.Itoa(ipv4Prefix)
resp, err := this.RPC().HTTPDNSAppRPC().FindAllHTTPDNSApps(this.AdminContext(), &pb.FindAllHTTPDNSAppsRequest{})
if err != nil {
return false, ""
}
mask := net.CIDRMask(ipv6Prefix, 128)
return parsed.Mask(mask).String() + "/" + strconv.Itoa(ipv6Prefix)
for _, app := range resp.GetApps() {
if strings.EqualFold(strings.TrimSpace(app.GetAppId()), appId) {
return app.GetSignEnabled(), strings.TrimSpace(app.GetSignSecret())
}
}
return false, ""
}
func buildSandboxResolveSign(signSecret string, appID string, domain string, qtype string, exp string, nonce string) string {
raw := strings.TrimSpace(appID) + "|" + strings.ToLower(strings.TrimSpace(domain)) + "|" + strings.ToUpper(strings.TrimSpace(qtype)) + "|" + strings.TrimSpace(exp) + "|" + strings.TrimSpace(nonce)
mac := hmac.New(sha256.New, []byte(strings.TrimSpace(signSecret)))
_, _ = mac.Write([]byte(raw))
return hex.EncodeToString(mac.Sum(nil))
}

View File

@@ -141,9 +141,7 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/ech"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/guide"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/sandbox"