管理端全部功能跑通

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

@@ -1,9 +1,9 @@
package teaconst
const (
Version = "1.4.7" //1.3.9
Version = "1.4.8" //1.3.9
APINodeVersion = "1.4.7" //1.3.9
APINodeVersion = "1.4.8" //1.3.9
ProductName = "Edge Admin"
ProcessName = "edge-admin"

View File

@@ -349,6 +349,38 @@ func (this *RPCClient) DNSTaskRPC() pb.DNSTaskServiceClient {
return pb.NewDNSTaskServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSClusterRPC() pb.HTTPDNSClusterServiceClient {
return pb.NewHTTPDNSClusterServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSNodeRPC() pb.HTTPDNSNodeServiceClient {
return pb.NewHTTPDNSNodeServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSAppRPC() pb.HTTPDNSAppServiceClient {
return pb.NewHTTPDNSAppServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSDomainRPC() pb.HTTPDNSDomainServiceClient {
return pb.NewHTTPDNSDomainServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSRuleRPC() pb.HTTPDNSRuleServiceClient {
return pb.NewHTTPDNSRuleServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSAccessLogRPC() pb.HTTPDNSAccessLogServiceClient {
return pb.NewHTTPDNSAccessLogServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSRuntimeLogRPC() pb.HTTPDNSRuntimeLogServiceClient {
return pb.NewHTTPDNSRuntimeLogServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSSandboxRPC() pb.HTTPDNSSandboxServiceClient {
return pb.NewHTTPDNSSandboxServiceClient(this.pickConn())
}
func (this *RPCClient) ACMEUserRPC() pb.ACMEUserServiceClient {
return pb.NewACMEUserServiceClient(this.pickConn())
}

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"

1
EdgeAdmin/test_db.go Normal file
View File

@@ -0,0 +1 @@
package main; import ("fmt"; "github.com/TeaOSLab/EdgeAPI/internal/db/models"; "github.com/iwind/TeaGo/dbs"; "github.com/iwind/TeaGo/Tea"); func main() { Tea.Env = "prod"; dbs.OnReady(func(){ apps, _ := models.SharedHTTPDNSAppDAO.FindAllEnabledApps(nil); for _, app := range apps { fmt.Printf("App: %s, Primary: %d\n", app.AppId, app.PrimaryClusterId) } }); }

1
EdgeAdmin/test_db2.go Normal file
View File

@@ -0,0 +1 @@
package main; import ("fmt"; "github.com/TeaOSLab/EdgeAPI/internal/db/models"; _ "github.com/go-sql-driver/mysql"; "github.com/iwind/TeaGo/dbs"; "github.com/iwind/TeaGo/Tea"); func main() { Tea.Env = "prod"; dbConfig := &dbs.Config{Driver: "mysql", Dsn: "edge:123456@tcp(127.0.0.1:3306)/edge?charset=utf8mb4&parseTime=true&loc=Local", Prefix: "edge"}; dbs.DefaultDB(dbConfig); apps, _ := models.SharedHTTPDNSAppDAO.FindAllEnabledApps(nil); for _, app := range apps { fmt.Printf("App: %s, Primary: %d\n", app.AppId, app.PrimaryClusterId) } }

View File

@@ -1,4 +1,4 @@
<first-menu>
<menu-item href="/httpdns/apps" code="index">应用列表</menu-item>
<a href="" class="item" @click.prevent="createApp()">[添加应用]</a>
</first-menu>
<a href="/httpdns/apps/create" class="item">[添加应用]</a>
</first-menu>

View File

@@ -1,8 +1,7 @@
{$layout "layout_popup"}
{$layout}
{$template "menu"}
<h3>添加应用</h3>
<form method="post" class="ui form" data-tea-action="$">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<table class="ui table definition selectable">
<tr>
@@ -35,13 +34,10 @@
<tr>
<td>所属用户</td>
<td>
<select class="ui dropdown" name="userId">
<option value="0">[平台自用 / 不指定]</option>
<option v-for="user in users" :value="user.id">{{user.name}} ({{user.username}})</option>
</select>
<p class="comment">可分配给指定租户用户;留空表示平台管理员自用。</p>
<user-selector></user-selector>
<p class="comment">可以选择当前应用所属的平台用户。</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>
</form>

Binary file not shown.

View File

@@ -10,7 +10,12 @@
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id, {
width: "42em",
height: "33em",
title: "新增自定义解析规则"
title: "新增自定义解析规则",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload();
});
}
});
};
@@ -21,7 +26,12 @@
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id + "&recordId=" + recordId, {
width: "42em",
height: "33em",
title: "编辑自定义解析规则"
title: "编辑自定义解析规则",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload();
});
}
});
};

View File

@@ -1,4 +1,4 @@
{$layout "layout_popup"}
{$layout "layout_popup"}
<style>
.httpdns-inline-actions {
@@ -37,7 +37,7 @@
<h3 v-if="isEditing">编辑自定义解析规则</h3>
<h3 v-else>新增自定义解析规则</h3>
<form method="post" class="ui form" data-tea-action="$">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="appId" :value="app.id" />
<input type="hidden" name="domainId" :value="domain.id" />
@@ -55,7 +55,7 @@
<td class="title">规则名称 *</td>
<td>
<input type="text" name="ruleName" maxlength="50" v-model="record.ruleName" ref="focus" />
<p class="comment">例如:上海电信灰度-v2</p>
<p class="comment">例如:上海电信灰度-v2</p>
</td>
</tr>
<tr>
@@ -105,7 +105,7 @@
<div style="margin-bottom: 0.5em;">
<div class="ui checkbox" style="margin-bottom: 0.5em;">
<input type="checkbox" name="weightEnabled" value="1" v-model="record.weightEnabled" />
<label>启权重调度设置</label>
<label>权重调度设置</label>
</div>
</div>
@@ -157,4 +157,4 @@
</table>
<submit-btn></submit-btn>
</form>
</form>

View File

@@ -1,4 +1,4 @@
Tea.context(function () {
Tea.context(function () {
var vm = this;
if (typeof vm.record == "undefined" || vm.record == null) {

View File

@@ -0,0 +1,36 @@
# Android SDK
## 初始化
```java
new InitConfig.Builder()
.setContext(context)
.setPrimaryServiceHost("httpdns-a.example.com")
.setBackupServiceHost("httpdns-b.example.com")
.setServicePort(443)
.setSecretKey("your-sign-secret")
.setEnableHttps(true)
.buildFor("app1f1ndpo9");
```
## 解析
```java
HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(
"api.business.com",
RequestIpType.auto,
null,
null
);
```
## 官方业务适配器
```java
HttpDnsHttpAdapter adapter = HttpDns.buildHttpClientAdapter(httpDnsService);
HttpDnsAdapterResponse resp = adapter.execute(
new HttpDnsAdapterRequest("GET", "https://api.business.com/v1/ping")
);
```
固定策略IP 直连 + 空 SNI + Host=真实域名,不回退到带 SNI。

View File

@@ -0,0 +1,35 @@
# Flutter SDK
## 初始化
```dart
await AliyunHttpdns.init(
appId: 'app1f1ndpo9',
primaryServiceHost: 'httpdns-a.example.com',
backupServiceHost: 'httpdns-b.example.com',
servicePort: 443,
secretKey: 'your-sign-secret',
);
await AliyunHttpdns.build();
```
## 解析
```dart
final result = await AliyunHttpdns.resolveHostSyncNonBlocking(
'api.business.com',
ipType: 'both',
);
```
## 官方业务适配器
```dart
final adapter = AliyunHttpdns.createHttpAdapter();
final resp = await adapter.request(
Uri.parse('https://api.business.com/v1/ping'),
method: 'GET',
);
```
固定策略IP 直连 + 空 SNI + Host=真实域名,不回退到带 SNI。

View File

@@ -0,0 +1,31 @@
# iOS SDK
## 初始化
```objc
HttpdnsEdgeService *service = [[HttpdnsEdgeService alloc]
initWithAppId:@"app1f1ndpo9"
primaryServiceHost:@"httpdns-a.example.com"
backupServiceHost:@"httpdns-b.example.com"
servicePort:443
signSecret:@"your-sign-secret"];
```
## 解析
```objc
[service resolveHost:@"api.business.com" queryType:@"A" completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) {
// result.ipv4s / result.ipv6s
}];
```
## 官方业务适配器
```objc
NSURL *url = [NSURL URLWithString:@"https://api.business.com/v1/ping"];
[service requestURL:url method:@"GET" headers:nil body:nil completion:^(NSData * _Nullable data, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
// handle
}];
```
固定策略IP 直连 + 空 SNI + Host=真实域名,不回退到带 SNI。

View File

@@ -36,7 +36,12 @@
teaweb.popup("/httpdns/apps/domains/createPopup?appId=" + this.app.id, {
height: "24em",
width: "46em",
title: "添加域名"
title: "添加域名",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload();
});
}
});
};

View File

@@ -2,8 +2,9 @@
<h3>添加域名</h3>
<form method="post" class="ui form" data-tea-action="$">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="appId" :value="app.id" />
<table class="ui table definition selectable">
<tr>
<td class="title">域名 *</td>

View File

@@ -54,7 +54,7 @@
<code>{{app.appId}}</code>
<copy-icon :text="app.appId"></copy-icon>
</td>
<td><a :href="'/httpdns/apps/domains?appId=' + app.id">{{app.domainCount}}</a></td>
<td class="center"><a :href="'/httpdns/apps/domains?appId=' + app.id">{{app.domainCount}}</a></td>
<td class="center">
<label-on :v-is-on="app.isOn"></label-on>
</td>

View File

@@ -4,10 +4,6 @@ Tea.context(function () {
}
this.createApp = function () {
teaweb.popup("/httpdns/apps/createPopup", {
height: "26em",
width: "48em",
title: "添加应用"
});
window.location = "/httpdns/apps/create";
};
});

View File

@@ -34,6 +34,12 @@
<div class="item"><strong>SDK 集成</strong></div>
</second-menu>
<div style="margin-top: 1em; text-align: right">
<a class="ui button tiny blue" :href="'/httpdns/apps/sdk/upload?appId=' + app.id">
<i class="icon upload"></i> 上传 SDK/文档
</a>
</div>
<div class="ui three stackable cards httpdns-sdk-cards">
<div class="card">
<div class="content">
@@ -43,8 +49,8 @@
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="javascript:;"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="javascript:;"><i class="icon book"></i> 集成文档</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=android"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=android"><i class="icon book"></i> 下载文档</a>
</div>
</div>
</div>
@@ -57,8 +63,8 @@
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="javascript:;"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="javascript:;"><i class="icon book"></i> 集成文档</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=ios"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=ios"><i class="icon book"></i> 下载文档</a>
</div>
</div>
</div>
@@ -71,8 +77,8 @@
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="javascript:;"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="javascript:;"><i class="icon book"></i> 集成文档</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=flutter"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=flutter"><i class="icon book"></i> 下载文档</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,93 @@
{$layout}
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<a class="item" :href="'/httpdns/apps/sdk?appId=' + app.id">SDK 集成</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>上传 SDK</strong></div>
</second-menu>
<form method="post"
enctype="multipart/form-data"
class="ui form"
data-tea-action="$"
data-tea-timeout="300"
data-tea-before="beforeUpload"
data-tea-done="doneUpload"
data-tea-success="successUpload">
<csrf-token></csrf-token>
<input type="hidden" name="appId" :value="app.id"/>
<table class="ui table selectable definition">
<tr>
<td class="title">平台 *</td>
<td>
<select name="platform" class="ui dropdown auto-width" v-model="platform">
<option value="android">Android</option>
<option value="ios">iOS</option>
<option value="flutter">Flutter</option>
</select>
</td>
</tr>
<tr>
<td class="title">版本号 *</td>
<td>
<input type="text" name="version" v-model="version" maxlength="32"/>
<p class="comment">默认 `1.0.0`。同平台上传会覆盖“最新版本”下载内容。</p>
</td>
</tr>
<tr>
<td class="title">SDK 包</td>
<td>
<input type="file" name="sdkFile" accept=".zip"/>
<p class="comment">支持 zip 包,例如 `httpdns-sdk-android.zip`。</p>
</td>
</tr>
<tr>
<td class="title">集成文档</td>
<td>
<input type="file" name="docFile" accept=".md,text/markdown"/>
<p class="comment">支持 Markdown 文件(`.md`)。</p>
</td>
</tr>
</table>
<div v-if="isUploading" class="ui message blue">
正在上传,请稍候...
</div>
<submit-btn v-show="!isUploading">上传并生效</submit-btn>
<button v-if="isUploading" class="ui button disabled" type="button">上传中...</button>
<a class="ui button" :href="'/httpdns/apps/sdk?appId=' + app.id">返回</a>
</form>
<h4 style="margin-top: 1.5em">已上传文件</h4>
<table class="ui table selectable celled" v-if="uploadedFiles.length > 0">
<thead>
<tr>
<th>平台</th>
<th>类型</th>
<th>版本</th>
<th>文件名</th>
<th>大小</th>
<th>更新时间</th>
<th class="one wide">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="file in uploadedFiles">
<td>{{file.platform}}</td>
<td>{{file.fileType}}</td>
<td>{{file.version}}</td>
<td>{{file.name}}</td>
<td>{{file.sizeText}}</td>
<td>{{file.updatedAt}}</td>
<td><a href="" @click.prevent="deleteUploadedFile(file.name)">删除</a></td>
</tr>
</tbody>
</table>
<div class="ui message" v-else>
暂无上传记录。
</div>

View File

@@ -0,0 +1,39 @@
Tea.context(function () {
this.platform = "android";
this.version = this.defaultVersion || "1.0.0";
this.isUploading = false;
if (!Array.isArray(this.uploadedFiles)) {
this.uploadedFiles = [];
}
this.beforeUpload = function () {
this.isUploading = true;
};
this.doneUpload = function () {
this.isUploading = false;
};
this.successUpload = function () {
teaweb.success("上传成功", function () {
window.location = "/httpdns/apps/sdk?appId=" + this.app.id;
}.bind(this));
};
this.deleteUploadedFile = function (filename) {
let that = this;
teaweb.confirm("确定要删除文件 " + filename + " 吗?", function () {
that.$post("/httpdns/apps/sdk/upload/delete")
.params({
appId: that.app.id,
filename: filename
})
.success(function () {
teaweb.success("删除成功", function () {
window.location.reload();
});
});
});
};
});

View File

@@ -1,10 +1,10 @@
<second-menu>
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + currentCluster.id">{{currentCluster.name}}</menu-item>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + clusterId">节点列表</menu-item>
<span class="item disabled">|</span>
<menu-item :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + nodeId" code="node">节点详情</menu-item>
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + clusterId">节点列表</menu-item>
<span class="item disabled">|</span>
<menu-item :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + nodeId" code="node">节点详情</menu-item>
<menu-item :href="'/httpdns/clusters/cluster/node/logs?clusterId=' + clusterId + '&nodeId=' + nodeId" code="log">运行日志</menu-item>
<menu-item :href="'/httpdns/clusters/cluster/node/update?clusterId=' + clusterId + '&nodeId=' + nodeId" code="update">修改设置</menu-item>
<menu-item :href="'/httpdns/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + nodeId" code="install">安装节点</menu-item>
</second-menu>
<menu-item :href="'/httpdns/clusters/cluster/node/update?clusterId=' + clusterId + '&nodeId=' + nodeId" code="update">修改设置</menu-item>
<menu-item :href="'/httpdns/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + nodeId" code="install">安装节点</menu-item>
</second-menu>

View File

@@ -2,17 +2,16 @@
{$template "node_menu"}
{$template "/code_editor"}
<!-- 已安装 -->
<div v-if="node.isInstalled">
<div class="ui message green">当前节点为已安装状态。</div>
<a href="" @click.prevent="updateNodeIsInstalled(false)">[重新安装]</a>
<a href="" @click.prevent="updateNodeIsInstalled(false)">[修改为未安装]</a>
<h4>配置文件</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">配置文件</td>
<td>
configs/api_httpdns.yaml &nbsp;
configs/api_httpdns.yaml
<download-link :v-element="'rpc-code'" :v-file="'api_httpdns.yaml'">[下载]</download-link>
</td>
</tr>
@@ -20,45 +19,27 @@
<td>配置内容</td>
<td>
<source-code-box id="rpc-code" type="text/yaml">rpc.endpoints: [ {{apiEndpoints}} ]
nodeId: "{{node.uniqueId}}"
secret: "{{node.secret}}"</source-code-box>
<p class="comment">每个节点的配置文件内容均不相同,不能混用。</p>
nodeId: "{{node.uniqueId}}"
secret: "{{node.secret}}"</source-code-box>
</td>
</tr>
<tr>
<td class="title">安装目录</td>
<td>
<div v-if="node.installDir.length == 0">使用集群设置<span
v-if="node.cluster != null && node.cluster.installDir.length > 0">{{node.cluster.installDir}}</span>
</div>
<div v-if="node.installDir.length == 0">使用集群设置<span v-if="node.cluster != null && node.cluster.installDir.length > 0">{{node.cluster.installDir}}</span></div>
<span v-else>{{node.installDir}}</span>
</td>
</tr>
</table>
</div>
<!-- 未安装 -->
<div v-if="!node.isInstalled">
<h4>方法1通过SSH自动安装</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">SSH地址</td>
<td>
<span v-if="sshAddr.length > 0">{{sshAddr}} &nbsp; <a href=""
@click.prevent="showSSHPopup(nodeId)">[修改]</a></span>
<span v-else><span class="red">尚未设置</span> &nbsp; <a href=""
@click.prevent="showSSHPopup(nodeId)">[设置]</a></span>
</td>
</tr>
</table>
<div v-if="installStatus != null && (installStatus.isRunning || installStatus.isFinished)"
class="ui segment installing-box">
<h4>通过控制台标记安装</h4>
<div v-if="installStatus != null && (installStatus.isRunning || installStatus.isFinished)" class="ui segment installing-box">
<div v-if="installStatus.isRunning" class="blue">安装中...</div>
<div v-if="installStatus.isFinished">
<span v-if="installStatus.isOk" class="green">安装成功</span>
<span v-if="!installStatus.isOk" class="red">安装过程中发生错误{{installStatus.error}}</span>
<span v-if="installStatus.isOk" class="green">安装成功</span>
<span v-else class="red">安装失败{{installStatus.error}}</span>
</div>
</div>
<div v-if="installStatus != null && installStatus.isFinished">
@@ -68,12 +49,12 @@
<button class="ui button small primary" type="button" @click.prevent="install()">开始安装</button>
</div>
<h4>方法2手动安装</h4>
<h4>配置文件</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">配置文件</td>
<td>
configs/api_httpdns.yaml &nbsp;
configs/api_httpdns.yaml
<download-link :v-element="'rpc-code'" :v-file="'api_httpdns.yaml'">[下载]</download-link>
</td>
</tr>
@@ -81,20 +62,18 @@
<td>配置内容</td>
<td>
<source-code-box id="rpc-code" type="text/yaml">rpc.endpoints: [ {{apiEndpoints}} ]
nodeId: "{{node.uniqueId}}"
secret: "{{node.secret}}"</source-code-box>
nodeId: "{{node.uniqueId}}"
secret: "{{node.secret}}"</source-code-box>
</td>
</tr>
<tr>
<td class="title">安装目录</td>
<td>
<div v-if="node.installDir.length == 0">使用集群设置<span
v-if="node.cluster != null && node.cluster.installDir.length > 0">{{node.cluster.installDir}}</span>
</div>
<div v-if="node.installDir.length == 0">使用集群设置<span v-if="node.cluster != null && node.cluster.installDir.length > 0">{{node.cluster.installDir}}</span></div>
<span v-else>{{node.installDir}}</span>
</td>
</tr>
</table>
<a href="" @click.prevent="updateNodeIsInstalled(true)">[修改为已安装状态]</a>
</div>
<a href="" @click.prevent="updateNodeIsInstalled(true)">[修改为已安装]</a>
</div>

View File

@@ -7,7 +7,6 @@ Tea.context(function () {
this.install = function () {
isInstalling = true
this.$post("$")
.params({
nodeId: this.nodeId
@@ -17,8 +16,8 @@ Tea.context(function () {
this.updateNodeIsInstalled = function (isInstalled) {
let msg = isInstalled
? "html:确认要将当前节点修改为 <strong>已安装</strong> 状态?"
: "html:确认要将当前节点修改为 <strong>未安装</strong> 状态?"
? "html:确认要将当前节点修改为 <strong>已安装</strong> 状态"
: "html:确认要将当前节点修改为 <strong>未安装</strong> 状态"
teaweb.confirm(msg, function () {
this.$post("/httpdns/clusters/cluster/node/updateInstallStatus")
.params({
@@ -44,60 +43,16 @@ Tea.context(function () {
return
}
let currentNodeId = this.node.id
let installStatus = this.installStatus || {}
let errMsg = installStatus.error || ""
let errorCode = installStatus.errorCode || ""
if (errorCode.length > 0) {
isInstalling = false
}
switch (errorCode) {
case "EMPTY_LOGIN":
case "EMPTY_SSH_HOST":
case "EMPTY_SSH_PORT":
case "EMPTY_GRANT":
teaweb.warn("需要补充 SSH 登录信息", function () {
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + currentNodeId, {
height: "30em",
callback: function () {
that.install()
}
})
})
return
case "SSH_LOGIN_FAILED":
teaweb.warn("SSH 登录失败,请检查设置", function () {
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + currentNodeId, {
height: "30em",
callback: function () {
that.install()
}
})
})
return
case "CREATE_ROOT_DIRECTORY_FAILED":
teaweb.warn("创建根目录失败:" + errMsg)
return
case "INSTALL_HELPER_FAILED":
teaweb.warn("安装助手失败:" + errMsg)
return
case "TEST_FAILED":
teaweb.warn("环境测试失败:" + errMsg)
return
case "RPC_TEST_FAILED":
teaweb.confirm("html:节点到 API 的 RPC 连通性测试失败:" + errMsg + "<br/>现在去修改 API 信息?", function () {
window.location = "/settings/api"
})
return
default:
break
teaweb.warn("安装失败:" + (installStatus.error || "未知错误"))
}
})
.done(function () {
this.$delay(function () {
this.reloadStatus(nodeId)
that.reloadStatus(nodeId)
}, 1000)
})
}

View File

@@ -7,22 +7,22 @@
<input type="hidden" name="nodeId" :value="nodeId"/>
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" value="" style="width:8em" id="day-from-picker"/>
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" style="width:8em" id="day-from-picker"/>
</div>
<div class="ui field">
<input type="text" name="dayTo" placeholder="结束日期" v-model="dayTo" value="" style="width:8em" id="day-to-picker"/>
<input type="text" name="dayTo" placeholder="结束日期" v-model="dayTo" style="width:8em" id="day-to-picker"/>
</div>
<div class="ui field">
<select class="ui dropdown" name="level" v-model="level">
<option value="">[级别]</option>
<option value="error">错误</option>
<option value="warning"></option>
<option value="warning"></option>
<option value="info">信息</option>
<option value="success">成功</option>
</select>
</div>
<div class="ui field">
<input type="text" name="keyword" style="width:10em" v-model="keyword" placeholder="关键词"/>
<input type="text" name="keyword" style="width:14em" v-model="keyword" placeholder="类型/模块/详情"/>
</div>
<div class="ui field">
<button type="submit" class="ui button">查询</button>
@@ -36,16 +36,13 @@
<p class="comment" v-if="logs.length == 0">暂时还没有日志。</p>
<table class="ui table selectable" v-if="logs.length > 0">
<thead>
<tr>
<tbody>
<tr v-for="log in logs">
<td>
<node-log-row :v-log="log" :v-keyword="keyword"></node-log-row>
</td>
</tr>
</thead>
<tr v-for="log in logs">
<td>
<node-log-row :v-log="log" :v-keyword="keyword"></node-log-row>
</td>
</tr>
</tbody>
</table>
<div class="page" v-html="page"></div>
<div class="page" v-html="page"></div>

View File

@@ -4,8 +4,8 @@
<h3>修改节点</h3>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="nodeId" :value="node.id" />
<input type="hidden" name="loginId" :value="loginId" />
<table class="ui table definition selectable">
<tr>
<td class="title">节点名称 *</td>
@@ -13,66 +13,50 @@
<input type="text" name="name" maxlength="50" ref="focus" v-model="node.name" />
</td>
</tr>
<tr>
<td>IP地址 *</td>
<td>
<node-ip-addresses-box role="ns" :v-ip-addresses="node.ipAddresses"></node-ip-addresses-box>
<p class="comment">用于访问节点和处理HTTPDNS解析请求等。</p>
</td>
</tr>
<tr>
<td>所属集群</td>
<td>
<select class="ui dropdown" name="clusterId" style="width:10em" v-model="clusterId">
<select class="ui dropdown" name="clusterId" style="width:16em" v-model="clusterId" disabled>
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
<p class="comment">当前版本暂不支持在此页面变更节点所属集群。</p>
</td>
</tr>
<tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td>
<td>IP地址</td>
<td>
<node-ip-addresses-box :v-ip-addresses="ipAddresses" :v-node-id="node.id"></node-ip-addresses-box>
</td>
</tr>
<tr>
<td>启用当前节点</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" v-model="node.isOn" />
<label></label>
</div>
</td>
</tr>
<tr>
<td>SSH 主机地址</td>
<td>
<input type="text" name="sshHost" maxlength="64" v-model="sshHost" />
<p class="comment">例如 192.168.1.100</p>
</td>
</tr>
<tr>
<td>SSH 主机端口</td>
<td>
<input type="text" name="sshPort" maxlength="5" v-model="sshPort" style="width: 6em" />
<p class="comment">例如 22</p>
</td>
</tr>
<tr>
<td>SSH 登录认证</td>
<td>
<grant-selector :v-grant="grant" :v-node-cluster-id="clusterId"></grant-selector>
</td>
</tr>
<tbody v-show="moreOptionsVisible">
<tr>
<td>SSH主机地址</td>
<td>
<input type="text" name="sshHost" maxlength="64" v-model="sshHost" />
<p class="comment">比如192.168.1.100</p>
</td>
</tr>
<tr>
<td>SSH主机端口</td>
<td>
<input type="text" name="sshPort" maxlength="5" v-model="sshPort" />
<p class="comment">比如22。</p>
</td>
</tr>
<tr>
<td>SSH登录认证</td>
<td>
<grant-selector :v-grant="grant" :v-node-cluster-id="clusterId"></grant-selector>
</td>
</tr>
<tr>
<td class="title">API节点地址</td>
<td>
<div style="margin-bottom: 0.5em">
<api-node-addresses-box :v-name="'apiNodeAddrsJSON'"
:v-addrs="apiNodeAddrs"></api-node-addresses-box>
</div>
<p class="comment">当前节点单独使用的API节点设置。<pro-warning-label></pro-warning-label></p>
</td>
</tr>
<tr>
<td>启用当前节点</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" v-model="node.isOn" />
<label></label>
</div>
<p class="comment">如果不启用此节点,此节点上的所有网站将不能访问。</p>
</td>
</tr>
</tbody>
</table>
<submit-btn></submit-btn>
</form>
</form>

View File

@@ -1,35 +1,25 @@
Tea.context(function () {
this.clusterId = 0;
if (this.node.cluster != null && this.node.cluster.id > 0) {
this.clusterId = this.node.cluster.id;
}
if (typeof this.clusterId !== "number" || this.clusterId <= 0) {
this.clusterId = 0
}
if (this.clusterId <= 0 && this.node.cluster != null && this.node.cluster.id > 0) {
this.clusterId = this.node.cluster.id
}
this.success = NotifySuccess("保存成功", "/httpdns/clusters/cluster/node?clusterId=" + this.clusterId + "&nodeId=" + this.node.id);
this.success = NotifySuccess("保存成功", "/httpdns/clusters/cluster/node?clusterId=" + this.clusterId + "&nodeId=" + this.node.id)
// 认证相关
this.grant = null
if (typeof this.sshHost !== "string") {
this.sshHost = ""
}
this.sshHost = ""
this.sshPort = ""
this.loginId = 0
if (this.node.login != null) {
this.loginId = this.node.login.id
let sshPort = parseInt(this.sshPort)
if (isNaN(sshPort) || sshPort <= 0) {
this.sshPort = 22
} else {
this.sshPort = sshPort
}
if (this.node.login.params != null) {
this.sshHost = this.node.login.params.host
if (this.node.login.params.port > 0) {
this.sshPort = this.node.login.params.port
}
}
if (this.node.login.grant != null && typeof this.node.login.grant.id != "undefined") {
this.grant = {
id: this.node.login.grant.id,
name: this.node.login.grant.name,
method: this.node.login.grant.method,
methodName: this.node.login.grant.methodName,
username: this.node.login.grant.username
}
}
}
})
if (typeof this.loginId !== "number") {
this.loginId = 0
}
})

View File

@@ -16,6 +16,7 @@
<div class="right-box with-menu">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="clusterId" :value="cluster.id" />
<table class="ui table definition selectable" v-show="activeSection == 'basic'">
@@ -102,4 +103,4 @@
<submit-btn></submit-btn>
</form>
</div>
</div>

View File

@@ -1,11 +1,16 @@
Tea.context(function () {
this.success = NotifyReloadSuccess("保存成功");
Tea.context(function () {
this.success = NotifyReloadSuccess("保存成功")
this.activeSection = this.activeSection || "basic";
this.tlsAdvancedVisible = false;
this.activeSection = this.activeSection || "basic"
this.tlsAdvancedVisible = false
if (!this.settings) {
this.settings = {};
this.settings = {}
}
});
this.syncDefaultCluster = function () {
if (!this.settings.isOn) {
this.settings.isDefaultCluster = false
}
}
})

View File

@@ -1,23 +1,72 @@
{$layout}
{$layout}
{$template "menu"}
<div class="ui margin"></div>
<div class="ui message info">目前这是一个纯前端占位页面Mock后续将对接真实的后端 API。</div>
<form class="ui form">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<table class="ui table definition selectable">
<tr>
<td class="title">集群名称 *</td>
<td>
<input type="text" placeholder="如 gateway-cn-hz" />
<input type="text" name="name" maxlength="50" ref="focus" placeholder="如 gateway-cn-hz" />
<p class="comment">用于在系统内部标识该 HTTPDNS 集群。</p>
</td>
</tr>
<tr>
<td>集群服务域名 *</td>
<td>服务域名 *</td>
<td>
<input type="text" placeholder="如 gw-hz.httpdns.example.com" />
<input type="text" name="gatewayDomain" maxlength="255" placeholder="如 gw-hz.httpdns.example.com" />
<p class="comment">当前集群对外提供 HTTPDNS 服务的接入域名。</p>
</td>
</tr>
<tr>
<td>默认解析 TTL</td>
<td>
<div class="ui input right labeled">
<input type="text" name="cacheTtl" maxlength="5" value="30" style="width: 6em" />
<span class="ui label"></span>
</div>
<p class="comment">SDK 向 HTTPDNS 请求域名解析时,返回的默认缓存有效期 (TTL)。SDK 超时后将重新发起请求。</p>
</td>
</tr>
<tr>
<td>降级超时容忍度</td>
<td>
<div class="ui input right labeled">
<input type="text" name="fallbackTimeout" maxlength="5" value="300" style="width: 6em" />
<span class="ui label">毫秒</span>
</div>
<p class="comment">HTTPDNS 节点在回源查询其它 DNS 时的最大等待时间。超出此时间将触发服务降级逻辑(返回上一有效缓存或错误)。</p>
</td>
</tr>
<tr>
<td>节点安装根目录</td>
<td>
<input type="text" name="installDir" maxlength="255" value="/opt/edge-httpdns" />
<p class="comment">边缘节点安装 HTTPDNS 服务的默认所在目录,此目录将被用于下发配置。通常保持默认即可。</p>
</td>
</tr>
<tr>
<td>启用当前集群</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" checked />
<label></label>
</div>
<p class="comment">如果取消启用,整个集群的 HTTPDNS 解析服务将不被系统分配。</p>
</td>
</tr>
<tr>
<td>默认集群</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isDefault" value="1" />
<label>设置为默认部署集群</label>
</div>
<p class="comment">全局设置。如果应用未单独指定集群,将默认分配和部署到该集群中。</p>
</td>
</tr>
</table>
<button type="button" class="ui button primary">保存</button>
</form>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,17 @@
Tea.context(function () {
this.success = function (resp) {
let clusterId = 0
if (resp != null && resp.data != null && typeof resp.data.clusterId != "undefined") {
clusterId = resp.data.clusterId
}
if (clusterId > 0) {
teaweb.success("保存成功", function () {
window.location = "/httpdns/clusters/cluster?clusterId=" + clusterId
})
return
}
teaweb.success("保存成功", function () {
window.location = "/httpdns/clusters"
})
}
})

View File

@@ -7,6 +7,7 @@
</second-menu>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="clusterId" :value="clusterId" />
<table class="ui table definition selectable">
<tr>
@@ -16,12 +17,12 @@
</td>
</tr>
<tr>
<td>IP地址 *</td>
<td>安装目录</td>
<td>
<node-ip-addresses-box role="ns"></node-ip-addresses-box>
<p class="comment">用于访问节点和处理HTTPDNS解析请求等</p>
<input type="text" name="installDir" maxlength="100" :value="cluster.installDir || '/opt/edge-httpdns'" />
<p class="comment">默认使用集群配置目录</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>
</form>

View File

@@ -1,3 +1,3 @@
Tea.context(function () {
this.success = NotifySuccess("保存成功", "/httpdns/clusters/cluster?clusterId=" + this.clusterId);
});
this.success = NotifySuccess("保存成功", "/httpdns/clusters/cluster?clusterId=" + this.clusterId)
})

View File

@@ -8,4 +8,4 @@
<div class="buttons-box">
<button class="ui button red" type="button" @click.prevent="deleteCluster(cluster.id)">删除当前集群</button>
</div>
</div>

View File

@@ -1,31 +1,32 @@
{$layout "layout_popup"}
<h3>淇敼鑺傜偣"{{node.name}}"鐨凷SH鐧诲綍淇℃伅</h3>
<h3>修改节点"{{node.name}}"的SSH登录信息</h3>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<input type="hidden" name="nodeId" :value="node.id"/>
<input type="hidden" name="loginId" :value="loginId"/>
<csrf-token></csrf-token>
<input type="hidden" name="nodeId" :value="node.id" />
<input type="hidden" name="loginId" :value="loginId" />
<table class="ui table definition">
<tr>
<td class="title">SSH涓绘満鍦板潃 *</td>
<td class="title">SSH主机地址 *</td>
<td>
<input type="text" name="sshHost" maxlength="64" v-model="params.host" ref="focus"/>
<p class="comment">姣斿192.168.1.100</p>
<input type="text" name="sshHost" maxlength="64" v-model="params.host" ref="focus" />
<p class="comment">比如 192.168.1.100</p>
</td>
</tr>
<tr>
<td>SSH涓绘満绔彛 *</td>
<td>SSH主机端口 *</td>
<td>
<input type="text" name="sshPort" maxlength="5" v-model="params.port" style="width:6em"/>
<p class="comment">姣斿22銆?/p>
<input type="text" name="sshPort" maxlength="5" v-model="params.port" style="width:6em" />
<p class="comment">比如 22。</p>
</td>
</tr>
<tr>
<td>SSH鐧诲綍璁よ瘉 *</td>
<td>SSH登录认证 *</td>
<td>
<grant-selector :v-grant="grant" :v-node-cluster-id="clusterId"></grant-selector>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>
</form>

View File

@@ -1,4 +1,4 @@
Tea.context(function () {
Tea.context(function () {
if (typeof this.health == "undefined") {
this.health = {
keySyncRate: 1.0,
@@ -14,7 +14,12 @@ Tea.context(function () {
teaweb.popup("/httpdns/ech/rollbackMfaPopup?logId=" + logId, {
height: "26em",
width: "48em",
title: "全域安全受控降级告警双人MFA授权"
})
title: "全域安全受控降级告警双人MFA授权",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload();
});
}
});
};
});

View File

@@ -2,7 +2,7 @@
<h3>受控 ECH 回滚降级</h3>
<form method="post" class="ui form" data-tea-action="$">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<input type="hidden" name="logId" :value="logId" />
<csrf-token></csrf-token>
<table class="ui table definition selectable">
@@ -24,4 +24,4 @@
</tr>
</table>
<submit-btn></submit-btn>
</form>
</form>

View File

@@ -1,4 +1,4 @@
{$layout}
{$layout}
{$template "menu"}
<style>
@@ -22,11 +22,11 @@
<div class="ui form" style="margin-bottom: 1.5em;">
<div class="ui fields inline">
<div class="field">
<label style="font-weight: 600;">目标应用</label>
<label style="font-weight: 600;">鐩爣搴旂敤</label>
</div>
<div class="field">
<select class="ui dropdown auto-width" v-model="selectedAppId" @change="onAppChange">
<option value="">[请选择应用]</option>
<option value="">[璇烽€夋嫨搴旂敤]</option>
<option v-for="app in apps" :value="app.appId">{{app.name}} ({{app.appId}})</option>
</select>
</div>
@@ -35,7 +35,7 @@
<div v-show="selectedAppId.length == 0" class="ui segment center aligned" style="padding:4em 1em; color:#999;">
<i class="icon cogs" style="font-size:3em;"></i>
<p style="margin-top:1em; font-size:1.1em;">请先选择应用,然后查看配置并完成 SDK 接入。</p>
<p style="margin-top:1em; font-size:1.1em;">璇峰厛閫夋嫨搴旂敤锛岀劧鍚庢煡鐪嬮厤缃苟瀹屾垚 SDK 鎺ュ叆銆?/p>
</div>
<div v-show="selectedAppId.length > 0">
@@ -43,106 +43,104 @@
<a class="step" :class="{active: currentStep == 1}" @click.prevent="currentStep=1">
<i class="icon file alternate outline"></i>
<div class="content">
<div class="title">01 查看配置</div>
<div class="description">查看 SDK 初始化参数</div>
<div class="title">01 鏌ョ湅閰嶇疆</div>
<div class="description">鏌ョ湅 SDK 鍒濆鍖栧弬鏁?/div>
</div>
</a>
<a class="step" :class="{active: currentStep == 2}" @click.prevent="currentStep=2">
<i class="icon code"></i>
<div class="content">
<div class="title">02 开发接入</div>
<div class="description">下载 SDK 并集成项目</div>
<div class="title">02 寮€鍙戞帴鍏?/div>
<div class="description">涓嬭浇 SDK 骞堕泦鎴愰」鐩?/div>
</div>
</a>
</div>
<div class="ui segment" v-show="currentStep == 1"
style="border-top:none; margin-top:0; border-top-left-radius:0; border-top-right-radius:0;">
<h4 class="ui header">查看配置</h4>
<h4 class="ui header">鏌ョ湅閰嶇疆</h4>
<table class="ui table definition">
<tr>
<td class="four wide">App ID</td>
<td>
<code>{{selectedApp.appId}}</code>
<a href="" class="httpdns-mini-action" title="复制 App ID" @click.prevent="copyText(selectedApp.appId, 'App ID')"><i class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-action" title="澶嶅埗 App ID" @click.prevent="copyText(selectedApp.appId, 'App ID')"><i class="copy outline icon"></i></a>
</td>
</tr>
<tr>
<td>应用名称</td>
<td>搴旂敤鍚嶇О</td>
<td><strong>{{selectedApp.name}}</strong></td>
</tr>
<tr>
<td>集群服务地址</td>
<td>闆嗙兢鏈嶅姟鍦板潃</td>
<td>
<code>{{selectedApp.gatewayDomainDisplay}}</code>
<a href="" class="httpdns-mini-action" title="复制服务地址" @click.prevent="copyText(selectedApp.gatewayDomainDisplay, '服务地址')"><i class="copy outline icon"></i></a>
<p class="comment" v-if="selectedApp.gatewayDomains && selectedApp.gatewayDomains.length > 1">已启用主备:第一个为主集群,后续为备集群。</p>
<a href="" class="httpdns-mini-action" title="澶嶅埗鏈嶅姟鍦板潃" @click.prevent="copyText(selectedApp.gatewayDomainDisplay, '鏈嶅姟鍦板潃')"><i class="copy outline icon"></i></a>
<p class="comment" v-if="selectedApp.gatewayDomains && selectedApp.gatewayDomains.length > 1">宸插惎鐢ㄤ富澶囷細绗竴涓负涓婚泦缇わ紝鍚庣画涓哄闆嗙兢銆?/p>
</td>
</tr>
<tr>
<td>加签 Secret</td>
<td>鍔犵 Secret</td>
<td>
<code>{{signSecretVisible ? selectedApp.signSecret : selectedApp.signSecretMasked}}</code>
<a href="" class="httpdns-mini-action" @click.prevent="signSecretVisible = !signSecretVisible" :title="signSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="signSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-action" title="复制加签 Secret" @click.prevent="copyText(selectedApp.signSecret, '加签 Secret')"><i class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-action" @click.prevent="signSecretVisible = !signSecretVisible" :title="signSecretVisible ? '闅愯棌鏄庢枃' : '鏌ョ湅鏄庢枃'"><i class="icon" :class="signSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-action" title="澶嶅埗鍔犵 Secret" @click.prevent="copyText(selectedApp.signSecret, '鍔犵 Secret')"><i class="copy outline icon"></i></a>
</td>
</tr>
<tr>
<td>请求验签</td>
<td>璇锋眰楠岀</td>
<td>
<span class="green" v-if="selectedApp.signEnabled">已开启</span>
<span class="grey" v-else>已关闭</span>
<span class="green" v-if="selectedApp.signEnabled">宸插紑鍚?/span>
<span class="grey" v-else>宸插叧闂?/span>
</td>
</tr>
</table>
<a href="" class="ui button small" @click.prevent="currentStep=2">
下一步 <i class="icon arrow right"></i>
涓嬩竴姝?<i class="icon arrow right"></i>
</a>
</div>
<div class="ui segment" v-show="currentStep == 2"
style="border-top:none; margin-top:0; border-top-left-radius:0; border-top-right-radius:0;">
<h4 class="ui header">开发接入</h4>
<p class="grey">选择对应平台 SDK 下载并查阅集成文档。</p>
<h4 class="ui header">寮€鍙戞帴鍏?/h4>
<p class="grey">閫夋嫨瀵瑰簲骞冲彴 SDK 涓嬭浇骞舵煡闃呴泦鎴愭枃妗c€?/p>
<div class="ui three cards" style="margin-top: 1.5em;">
<div class="card">
<div class="content">
<div class="header"><i class="icon android green"></i> Android SDK</div>
<div class="description" style="margin-top:.5em;">
适用于 Android 5.0+ 的原生 SDK支持 Java / Kotlin。
</div>
閫傜敤浜?Android 5.0+ 鐨勫師鐢?SDK锛屾敮鎸?Java / Kotlin銆? </div>
</div>
<div class="extra content">
<a class="ui button primary mini"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button basic mini"><i class="icon book"></i> 集成帮助文档</a>
<a class="ui button primary mini" href="https://github.com/aliyun/alibabacloud-httpdns-android-sdk" target="_blank" rel="noopener noreferrer"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button basic mini" href="https://github.com/aliyun/alibabacloud-httpdns-android-sdk/blob/master/README.md" target="_blank" rel="noopener noreferrer"><i class="icon book"></i> 集成帮助文档</a>
</div>
</div>
<div class="card">
<div class="content">
<div class="header"><i class="icon apple grey"></i> iOS SDK</div>
<div class="description" style="margin-top:.5em;">
适用于 iOS 12+ 的原生 SDK支持 Swift / Objective-C
</div>
閫傜敤浜?iOS 12+ 鐨勫師鐢?SDK锛屾敮鎸?Swift / Objective-C銆? </div>
</div>
<div class="extra content">
<a class="ui button primary mini"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button basic mini"><i class="icon book"></i> 集成帮助文档</a>
<a class="ui button primary mini" href="https://github.com/aliyun/alibabacloud-httpdns-ios-sdk" target="_blank" rel="noopener noreferrer"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button basic mini" href="https://github.com/aliyun/alibabacloud-httpdns-ios-sdk/blob/master/README.md" target="_blank" rel="noopener noreferrer"><i class="icon book"></i> 集成帮助文档</a>
</div>
</div>
<div class="card">
<div class="content">
<div class="header"><i class="icon mobile alternate blue"></i> Flutter SDK</div>
<div class="description" style="margin-top:.5em;">
跨平台 Flutter 插件,同时支持 Android iOS
</div>
璺ㄥ钩鍙?Flutter 鎻掍欢锛屽悓鏃舵敮鎸?Android 鍜?iOS銆? </div>
</div>
<div class="extra content">
<a class="ui button primary mini"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button basic mini"><i class="icon book"></i> 集成帮助文档</a>
<a class="ui button primary mini" href="https://pub.dev/packages/aliyun_httpdns" target="_blank" rel="noopener noreferrer"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button basic mini" href="https://pub.dev/packages/aliyun_httpdns" target="_blank" rel="noopener noreferrer"><i class="icon book"></i> 集成帮助文档</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,30 +1,28 @@
{$layout}
{$template "menu"}
{$layout}
<div class="margin"></div>
{$template "/datepicker"}
<style>
.httpdns-runtime-level {
font-weight: 600;
}
.httpdns-runtime-level {
font-weight: 600;
}
.httpdns-runtime-message {
word-break: break-all;
}
.httpdns-runtime-time {
margin-right: 0.5em;
}
.httpdns-runtime-tag {
margin-right: 0.5em;
}
</style>
<form method="get" action="/httpdns/runtimeLogs" class="ui form small" autocomplete="off">
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown" name="clusterId" v-model="clusterId">
<option value="">[集群]</option>
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
</div>
<div class="ui field">
<select class="ui dropdown" name="nodeId" v-model="nodeId">
<option value="">[节点]</option>
<option v-for="node in nodes" :value="node.id" v-if="clusterId == '' || clusterId == node.clusterId">
{{node.name}}</option>
</select>
</div>
<div class="ui field">
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" style="width:7.8em"
id="day-from-picker" />
@@ -35,10 +33,10 @@
<div class="ui field">
<select class="ui dropdown" name="level" v-model="level">
<option value="">[级别]</option>
<option value="error">error</option>
<option value="warning">warning</option>
<option value="info">info</option>
<option value="success">success</option>
<option value="error">错误</option>
<option value="warning">警告</option>
<option value="info">信息</option>
<option value="success">成功</option>
</select>
</div>
<div class="ui field">
@@ -47,8 +45,7 @@
<div class="ui field">
<button type="submit" class="ui button small">查询</button>
</div>
<div class="ui field"
v-if="clusterId.toString().length > 0 || nodeId.toString().length > 0 || dayFrom.length > 0 || dayTo.length > 0 || keyword.length > 0 || level.length > 0">
<div class="ui field" v-if="dayFrom.length > 0 || dayTo.length > 0 || keyword.length > 0 || level.length > 0">
<a href="/httpdns/runtimeLogs">[清除条件]</a>
</div>
</div>
@@ -58,31 +55,44 @@
<not-found-box v-if="runtimeLogs.length == 0">暂时还没有运行日志。</not-found-box>
<table class="ui table selectable celled" v-if="runtimeLogs.length > 0">
<table class="ui table celled" v-if="runtimeLogs.length > 0">
<thead>
<tr>
<th>时间</th>
<th>集群</th>
<th>节点</th>
<th>级别</th>
<th>类型</th>
<th>详情</th>
<th>次数</th>
<th>信息</th>
</tr>
</thead>
<tbody>
<tr v-for="log in runtimeLogs">
<td>{{log.createdTime}}</td>
<td>{{log.clusterName}}</td>
<td>{{log.nodeName}}</td>
<td>
<span
class="httpdns-runtime-level"
:class="{red:log.level == 'error', orange:log.level == 'warning', green:log.level == 'success'}">{{log.level}}</span>
<td nowrap="">
<span v-if="log.clusterName">{{log.clusterName}}
<a :href="'/httpdns/clusters/cluster?clusterId=' + log.clusterId" target="_blank"
v-if="log.clusterId > 0"><i class="icon external small"></i></a>
</span>
<span v-else class="disabled">-</span>
</td>
<td nowrap="">
<span v-if="log.nodeName">{{log.nodeName}}
<a :href="'/httpdns/clusters/cluster/node?clusterId=' + log.clusterId + '&nodeId=' + log.nodeId"
target="_blank" v-if="log.nodeId > 0"><i class="icon external small"></i></a>
</span>
<span v-else class="disabled">-</span>
</td>
<td>
<div>
<span
:class="{red:log.level == 'error', orange:log.level == 'warning', green:log.level == 'success' || log.level == 'info'}"
class="httpdns-runtime-message">
<span class="httpdns-runtime-time">[{{log.createdTime}}]</span>
<span class="httpdns-runtime-tag" v-if="log.tag && log.tag.length > 0">[{{log.tag}}]</span>
<span class="httpdns-runtime-level">{{log.description}}</span>
</span>
<span v-if="log.count > 1" class="ui label tiny basic"
:class="{red:log.level == 'error', orange:log.level == 'warning', green:log.level == 'success' || log.level == 'info'}"
style="margin-left: 0.5em">共{{log.count}}条</span>
</div>
</td>
<td><code>{{log.tag}}</code></td>
<td>{{log.description}}</td>
<td>{{log.count}}</td>
</tr>
</tbody>
</table>
</table>

View File

@@ -1,4 +1,7 @@
Tea.context(function () {
this.clusterId = "";
this.nodeId = "";
this.$delay(function () {
teaweb.datepicker("day-from-picker");
teaweb.datepicker("day-to-picker");

Some files were not shown because too many files have changed in this diff Show More