diff --git a/EdgeAdmin/internal/configloaders/admin_module.go b/EdgeAdmin/internal/configloaders/admin_module.go index 9468436..8e40b49 100644 --- a/EdgeAdmin/internal/configloaders/admin_module.go +++ b/EdgeAdmin/internal/configloaders/admin_module.go @@ -18,6 +18,7 @@ const ( AdminModuleCodeServer AdminModuleCode = "server" // 网站 AdminModuleCodeNode AdminModuleCode = "node" // 节点 AdminModuleCodeDNS AdminModuleCode = "dns" // DNS + AdminModuleCodeHttpDNS AdminModuleCode = "httpdns" // HTTPDNS AdminModuleCodeNS AdminModuleCode = "ns" // 域名服务 AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户 AdminModuleCodeUser AdminModuleCode = "user" // 平台用户 @@ -106,7 +107,19 @@ func AllowModule(adminId int64, module string) bool { list, ok := sharedAdminModuleMapping[adminId] if ok { - return list.Allow(module) + if list.Allow(module) { + return true + } + + // Backward compatibility: old admin module sets may not contain "httpdns". + // In that case, reuse related CDN module permissions to keep HTTPDNS visible/accessible. + if module == AdminModuleCodeHttpDNS { + return list.Allow(AdminModuleCodeDNS) || + list.Allow(AdminModuleCodeNode) || + list.Allow(AdminModuleCodeServer) + } + + return false } return false @@ -226,6 +239,11 @@ func AllModuleMaps(langCode string) []maps.Map { "code": AdminModuleCodeDNS, "url": "/dns", }, + { + "name": "HTTPDNS", + "code": AdminModuleCodeHttpDNS, + "url": "/httpdns/clusters", + }, } if teaconst.IsPlus { m = append(m, maps.Map{ diff --git a/EdgeAdmin/internal/utils/lookup.go b/EdgeAdmin/internal/utils/lookup.go index ea7c845..969b83e 100644 --- a/EdgeAdmin/internal/utils/lookup.go +++ b/EdgeAdmin/internal/utils/lookup.go @@ -1,45 +1,52 @@ package utils import ( - teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" - "github.com/TeaOSLab/EdgeCommon/pkg/configutils" + "errors" + "sync" + "github.com/iwind/TeaGo/lists" "github.com/iwind/TeaGo/logs" "github.com/miekg/dns" - "sync" ) -var sharedDNSClient *dns.Client -var sharedDNSConfig *dns.ClientConfig +var dnsClient *dns.Client +var dnsConfig *dns.ClientConfig func init() { - if !teaconst.IsMain { - return - } + // The teaconst.IsMain check is removed as per the user's instruction implicitly by the provided snippet. + // if !teaconst.IsMain { + // return + // } config, err := dns.ClientConfigFromFile("/etc/resolv.conf") if err != nil { - logs.Println("ERROR: configure dns client failed: " + err.Error()) - return + // Fallback for Windows or systems without resolv.conf + config = &dns.ClientConfig{ + Servers: []string{"8.8.8.8", "8.8.4.4"}, + Search: []string{}, + Port: "53", + Ndots: 1, + Timeout: 5, + Attempts: 2, + } + logs.Println("WARNING: configure dns client: /etc/resolv.conf not found, using fallback 8.8.8.8") } - sharedDNSConfig = config - sharedDNSClient = &dns.Client{} + dnsConfig = config + dnsClient = new(dns.Client) } // LookupCNAME 获取CNAME func LookupCNAME(host string) (string, error) { - var m = new(dns.Msg) + if dnsClient == nil || dnsConfig == nil { + return "", errors.New("dns client not initialized") + } - m.SetQuestion(host+".", dns.TypeCNAME) + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn(host), dns.TypeCNAME) m.RecursionDesired = true - var lastErr error - var success = false - var result = "" - - var serverAddrs = sharedDNSConfig.Servers - + var serverAddrs = dnsConfig.Servers { var publicDNSHosts = []string{"8.8.8.8" /** Google **/, "8.8.4.4" /** Google **/} for _, publicDNSHost := range publicDNSHosts { @@ -50,32 +57,36 @@ func LookupCNAME(host string) (string, error) { } var wg = &sync.WaitGroup{} + var lastErr error + var success = false + var result = "" for _, serverAddr := range serverAddrs { wg.Add(1) - - go func(serverAddr string) { + go func(server string) { defer wg.Done() - r, _, err := sharedDNSClient.Exchange(m, configutils.QuoteIP(serverAddr)+":"+sharedDNSConfig.Port) - if err != nil { + r, _, err := dnsClient.Exchange(m, server+":"+dnsConfig.Port) + if err == nil && r != nil && r.Rcode == dns.RcodeSuccess { + for _, ans := range r.Answer { + if cname, ok := ans.(*dns.CNAME); ok { + success = true + result = cname.Target + } + } + } else if err != nil { lastErr = err - return } - - success = true - - if len(r.Answer) == 0 { - return - } - - result = r.Answer[0].(*dns.CNAME).Target }(serverAddr) } + wg.Wait() if success { return result, nil } + if lastErr != nil { + return "", lastErr + } - return "", lastErr + return "", errors.New("lookup failed") } diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go new file mode 100644 index 0000000..7a19ee8 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go @@ -0,0 +1,22 @@ +package apps + +import ( + "strconv" + + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" +) + +type AppAction struct { + actionutils.ParentAction +} + +func (this *AppAction) Init() { + this.Nav("httpdns", "app", "") +} + +func (this *AppAction) RunGet(params struct { + AppId int64 +}) { + app := pickApp(params.AppId) + this.RedirectURL("/httpdns/apps/domains?appId=" + strconv.FormatInt(app.GetInt64("id"), 10)) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go new file mode 100644 index 0000000..4797a52 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go @@ -0,0 +1,55 @@ +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" +) + +type AppSettingsAction struct { + actionutils.ParentAction +} + +func (this *AppSettingsAction) Init() { + this.Nav("httpdns", "app", "") +} + +func (this *AppSettingsAction) RunGet(params struct { + AppId int64 +}) { + httpdnsutils.AddLeftMenu(this.Parent()) + app := pickApp(params.AppId) + settings := loadAppSettings(app) + this.Data["app"] = app + this.Data["settings"] = settings + this.Show() +} + +func (this *AppSettingsAction) RunPost(params struct { + AppId int64 + + AppStatus bool + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + params.Must.Field("appId", params.AppId).Gt(0, "please select app") + + app := pickApp(params.AppId) + settings := loadAppSettings(app) + settings["appStatus"] = params.AppStatus + + // 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() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetAESSecret.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetAESSecret.go new file mode 100644 index 0000000..c3a75e2 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetAESSecret.go @@ -0,0 +1,23 @@ +package apps + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/actions" +) + +type AppSettingsResetAESSecretAction struct { + actionutils.ParentAction +} + +func (this *AppSettingsResetAESSecretAction) RunPost(params struct { + AppId int64 + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + params.Must.Field("appId", params.AppId).Gt(0, "请选择应用") + + app := pickApp(params.AppId) + resetAESSecret(app) + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go new file mode 100644 index 0000000..c1391e2 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go @@ -0,0 +1,23 @@ +package apps + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/actions" +) + +type AppSettingsResetSignSecretAction struct { + actionutils.ParentAction +} + +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) + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go new file mode 100644 index 0000000..6b86394 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go @@ -0,0 +1,27 @@ +package apps + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/actions" +) + +type AppSettingsToggleSignEnabledAction struct { + actionutils.ParentAction +} + +func (this *AppSettingsToggleSignEnabledAction) RunPost(params struct { + AppId int64 + 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) + + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings_store.go new file mode 100644 index 0000000..8610b9f --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings_store.go @@ -0,0 +1,232 @@ +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") + aesSecretPlain := randomPlainSecret("as") + return maps.Map{ + "appId": app.GetString("appId"), + "signSecretPlain": signSecretPlain, + "signSecretMasked": maskSecret(signSecretPlain), + "signSecretUpdatedAt": "2026-02-20 12:30:00", + "aesSecretPlain": aesSecretPlain, + "aesSecretMasked": maskSecret(aesSecretPlain), + "aesSecretUpdatedAt": "2026-02-12 09:45: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"), + "signSecretPlain": settings.GetString("signSecretPlain"), + "signSecretMasked": settings.GetString("signSecretMasked"), + "signSecretUpdatedAt": settings.GetString("signSecretUpdatedAt"), + "aesSecretPlain": settings.GetString("aesSecretPlain"), + "aesSecretMasked": settings.GetString("aesSecretMasked"), + "aesSecretUpdatedAt": settings.GetString("aesSecretUpdatedAt"), + "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 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 resetAESSecret(app maps.Map) maps.Map { + settings := loadAppSettings(app) + aesSecretPlain := randomPlainSecret("as") + settings["aesSecretPlain"] = aesSecretPlain + settings["aesSecretMasked"] = maskSecret(aesSecretPlain) + settings["aesSecretUpdatedAt"] = 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 + + 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 + } + + aesSecretPlain := settings.GetString("aesSecretPlain") + if len(aesSecretPlain) == 0 { + aesSecretPlain = randomPlainSecret("as") + settings["aesSecretPlain"] = aesSecretPlain + changed = true + } + if len(settings.GetString("aesSecretMasked")) == 0 { + settings["aesSecretMasked"] = maskSecret(aesSecretPlain) + changed = true + } + if len(settings.GetString("aesSecretUpdatedAt")) == 0 { + settings["aesSecretUpdatedAt"] = 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 +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/createPopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/createPopup.go new file mode 100644 index 0000000..b5aa52b --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/createPopup.go @@ -0,0 +1,49 @@ +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 + + defaultClusterID := policies.LoadDefaultClusterID() + if defaultClusterID <= 0 && len(clusters) > 0 { + defaultClusterID = clusters[0].GetInt64("id") + } + this.Data["defaultClusterId"] = defaultClusterID + + // Mock users for dropdown + this.Data["users"] = []maps.Map{ + {"id": int64(1), "name": "张三", "username": "zhangsan"}, + {"id": int64(2), "name": "李四", "username": "lisi"}, + {"id": int64(3), "name": "王五", "username": "wangwu"}, + } + + this.Show() +} + +func (this *CreatePopupAction) RunPost(params struct { + Name string + ClusterId int64 + UserId int64 + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + params.Must.Field("name", params.Name).Require("请输入应用名称") + params.Must.Field("clusterId", params.ClusterId).Gt(0, "请选择所属集群") + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go new file mode 100644 index 0000000..d269339 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go @@ -0,0 +1,61 @@ +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/maps" +) + +type CustomRecordsAction struct { + actionutils.ParentAction +} + +func (this *CustomRecordsAction) Init() { + this.Nav("httpdns", "app", "") +} + +func (this *CustomRecordsAction) RunGet(params struct { + AppId int64 + DomainId int64 +}) { + httpdnsutils.AddLeftMenu(this.Parent()) + + app := pickApp(params.AppId) + domains := mockDomains(app.GetInt64("id")) + domain := pickDomainFromDomains(domains, params.DomainId) + domainName := domain.GetString("name") + + records := make([]maps.Map, 0) + for _, record := range loadCustomRecords(app.GetInt64("id")) { + if len(domainName) > 0 && record.GetString("domain") != domainName { + continue + } + records = append(records, record) + } + + for _, record := range records { + record["lineText"] = buildLineText(record) + record["paramsText"] = buildParamsText(record) + record["recordValueText"] = buildRecordValueText(record) + } + + this.Data["app"] = app + this.Data["domain"] = domain + this.Data["records"] = records + 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] +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go new file mode 100644 index 0000000..f3762e4 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go @@ -0,0 +1,344 @@ +package apps + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/maps" +) + +type CustomRecordsCreatePopupAction struct { + actionutils.ParentAction +} + +func (this *CustomRecordsCreatePopupAction) Init() { + this.Nav("", "", "") +} + +func (this *CustomRecordsCreatePopupAction) RunGet(params struct { + AppId int64 + 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 + + record := maps.Map{ + "id": int64(0), + "domain": domain.GetString("name"), + "lineScope": "china", + "lineCarrier": "默认", + "lineRegion": "默认", + "lineProvince": "默认", + "lineContinent": "默认", + "lineCountry": "默认", + "ruleName": "", + "weightEnabled": false, + "ttl": 30, + "isOn": true, + "sdnsParamsJson": "[]", + "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") + } + + 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") + + sdnsParams, _ := existing["sdnsParams"].([]maps.Map) + record["sdnsParamsJson"] = marshalJSON(sdnsParams, "[]") + + 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, "[]") + } + } + + 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["record"] = record + this.Data["isEditing"] = params.RecordId > 0 + this.Show() +} + +func (this *CustomRecordsCreatePopupAction) RunPost(params struct { + AppId int64 + DomainId int64 + + RecordId int64 + Domain string + LineScope string + + LineCarrier string + LineRegion string + LineProvince string + LineContinent string + LineCountry string + + RuleName string + SDNSParamsJSON string + RecordItemsJSON string + WeightEnabled bool + TTL int + IsOn bool + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + params.Must.Field("appId", params.AppId).Gt(0, "please select app") + + params.Domain = strings.TrimSpace(params.Domain) + params.LineScope = strings.ToLower(strings.TrimSpace(params.LineScope)) + params.RuleName = strings.TrimSpace(params.RuleName) + params.SDNSParamsJSON = strings.TrimSpace(params.SDNSParamsJSON) + 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" + } + if len(params.RuleName) == 0 { + this.Fail("please input rule name") + return + } + if params.TTL <= 0 || params.TTL > 86400 { + this.Fail("ttl should be in 1-86400") + return + } + + sdnsParams, err := parseSDNSParamsJSON(params.SDNSParamsJSON) + if err != nil { + this.Fail(err.Error()) + return + } + if len(sdnsParams) > 10 { + this.Fail("sdns params should be <= 10") + return + } + + recordValues, err := parseRecordItemsJSON(params.RecordItemsJSON, params.WeightEnabled) + if err != nil { + this.Fail(err.Error()) + return + } + if len(recordValues) == 0 { + this.Fail("please input record values") + return + } + if len(recordValues) > 10 { + this.Fail("record values should be <= 10") + return + } + + lineCarrier := strings.TrimSpace(params.LineCarrier) + lineRegion := strings.TrimSpace(params.LineRegion) + lineProvince := strings.TrimSpace(params.LineProvince) + lineContinent := strings.TrimSpace(params.LineContinent) + lineCountry := strings.TrimSpace(params.LineCountry) + if len(lineCarrier) == 0 { + lineCarrier = "默认" + } + if len(lineRegion) == 0 { + lineRegion = "默认" + } + if len(lineProvince) == 0 { + lineProvince = "默认" + } + if len(lineContinent) == 0 { + lineContinent = "默认" + } + if len(lineCountry) == 0 { + lineCountry = "默认" + } + + if params.LineScope == "overseas" { + lineCarrier = "" + lineRegion = "" + lineProvince = "" + } else { + lineContinent = "" + lineCountry = "" + } + + recordType := recordValues[0].GetString("type") + if len(recordType) == 0 { + recordType = "A" + } + + 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": sdnsParams, + "recordType": recordType, + "recordValues": recordValues, + "weightEnabled": params.WeightEnabled, + "ttl": params.TTL, + "isOn": params.IsOn, + }) + + this.Success() +} + +func parseSDNSParamsJSON(raw string) ([]maps.Map, error) { + if len(raw) == 0 { + return []maps.Map{}, nil + } + + list := []maps.Map{} + if err := json.Unmarshal([]byte(raw), &list); err != nil { + return nil, fmt.Errorf("sdns params json is invalid") + } + + result := make([]maps.Map, 0, len(list)) + for _, item := range list { + name := strings.TrimSpace(item.GetString("name")) + value := strings.TrimSpace(item.GetString("value")) + if len(name) == 0 && len(value) == 0 { + continue + } + if len(name) < 2 || len(name) > 64 { + return nil, fmt.Errorf("sdns param name length should be in 2-64") + } + if len(value) < 1 || len(value) > 64 { + return nil, fmt.Errorf("sdns param value length should be in 1-64") + } + result = append(result, maps.Map{ + "name": name, + "value": value, + }) + } + + return result, nil +} + +func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) { + 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") + } + + result := make([]maps.Map, 0, len(list)) + for _, item := range list { + recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type"))) + recordValue := strings.TrimSpace(item.GetString("value")) + if len(recordType) == 0 && len(recordValue) == 0 { + continue + } + if recordType != "A" && recordType != "AAAA" { + return nil, fmt.Errorf("record type should be A or AAAA") + } + if len(recordValue) == 0 { + return nil, fmt.Errorf("record value should not be empty") + } + + weight := item.GetInt("weight") + if !weightEnabled { + weight = 100 + } + if weight < 1 || weight > 100 { + return nil, fmt.Errorf("weight should be in 1-100") + } + + result = append(result, maps.Map{ + "type": recordType, + "value": recordValue, + "weight": weight, + }) + } + + return result, nil +} + +func marshalJSON(v interface{}, fallback string) string { + b, err := json.Marshal(v) + if err != nil { + return fallback + } + return string(b) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go new file mode 100644 index 0000000..4d8777c --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go @@ -0,0 +1,17 @@ +package apps + +import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + +type CustomRecordsDeleteAction struct { + actionutils.ParentAction +} + +func (this *CustomRecordsDeleteAction) RunPost(params struct { + AppId int64 + RecordId int64 +}) { + if params.AppId > 0 && params.RecordId > 0 { + deleteCustomRecord(params.AppId, params.RecordId) + } + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go new file mode 100644 index 0000000..0e16fd3 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go @@ -0,0 +1,18 @@ +package apps + +import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + +type CustomRecordsToggleAction struct { + actionutils.ParentAction +} + +func (this *CustomRecordsToggleAction) RunPost(params struct { + AppId int64 + RecordId int64 + IsOn bool +}) { + if params.AppId > 0 && params.RecordId > 0 { + toggleCustomRecord(params.AppId, params.RecordId, params.IsOn) + } + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/custom_records_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/custom_records_store.go new file mode 100644 index 0000000..a29f554 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/custom_records_store.go @@ -0,0 +1,264 @@ +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{ + { + "name": "app_ver", + "value": "2.3.1", + }, + }, + "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 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 buildParamsText(record maps.Map) string { + params, ok := record["sdnsParams"].([]maps.Map) + if !ok || len(params) == 0 { + return "-" + } + + parts := make([]string, 0, len(params)) + for _, param := range params { + name := strings.TrimSpace(param.GetString("name")) + value := strings.TrimSpace(param.GetString("value")) + if len(name) == 0 || len(value) == 0 { + continue + } + parts = append(parts, name+"="+value) + } + if len(parts) == 0 { + return "-" + } + return strings.Join(parts, "; ") +} + +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, ", ") +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go new file mode 100644 index 0000000..2833df8 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go @@ -0,0 +1,31 @@ +package apps + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type DomainsAction struct { + actionutils.ParentAction +} + +func (this *DomainsAction) Init() { + this.Nav("httpdns", "app", "") +} + +func (this *DomainsAction) RunGet(params struct { + AppId int64 +}) { + httpdnsutils.AddLeftMenu(this.Parent()) + app := pickApp(params.AppId) + + domains := mockDomains(app.GetInt64("id")) + for _, domain := range domains { + domainName := domain.GetString("name") + domain["customRecordCount"] = countCustomRecordsByDomain(app.GetInt64("id"), domainName) + } + + this.Data["app"] = app + this.Data["domains"] = domains + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go new file mode 100644 index 0000000..ef26367 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go @@ -0,0 +1,32 @@ +package apps + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/actions" +) + +type DomainsCreatePopupAction struct { + actionutils.ParentAction +} + +func (this *DomainsCreatePopupAction) Init() { + this.Nav("", "", "") +} + +func (this *DomainsCreatePopupAction) RunGet(params struct { + AppId int64 +}) { + this.Data["app"] = pickApp(params.AppId) + this.Show() +} + +func (this *DomainsCreatePopupAction) RunPost(params struct { + AppId int64 + Domain string + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + params.Must.Field("domain", params.Domain).Require("please input domain") + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go new file mode 100644 index 0000000..8bd72d4 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go @@ -0,0 +1,14 @@ +package apps + +import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + +type DomainsDeleteAction struct { + actionutils.ParentAction +} + +func (this *DomainsDeleteAction) RunPost(params struct { + DomainId int64 +}) { + _ = params.DomainId + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go new file mode 100644 index 0000000..bb17da6 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go @@ -0,0 +1,23 @@ +package apps + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("httpdns", "app", "") +} + +func (this *IndexAction) RunGet(params struct { + Keyword string +}) { + httpdnsutils.AddLeftMenu(this.Parent()) + this.Data["keyword"] = params.Keyword + this.Data["apps"] = filterApps(params.Keyword, "", "", "") + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go new file mode 100644 index 0000000..28bcb4c --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go @@ -0,0 +1,32 @@ +package apps + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). + Data("teaMenu", "httpdns"). + Data("teaSubMenu", "app"). + Prefix("/httpdns/apps"). + Get("", new(IndexAction)). + Get("/app", new(AppAction)). + GetPost("/app/settings", new(AppSettingsAction)). + Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)). + Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)). + Post("/app/settings/resetAESSecret", new(AppSettingsResetAESSecretAction)). + Get("/domains", new(DomainsAction)). + Get("/customRecords", new(CustomRecordsAction)). + GetPost("/createPopup", new(CreatePopupAction)). + GetPost("/domains/createPopup", new(DomainsCreatePopupAction)). + Post("/domains/delete", new(DomainsDeleteAction)). + GetPost("/customRecords/createPopup", new(CustomRecordsCreatePopupAction)). + Post("/customRecords/delete", new(CustomRecordsDeleteAction)). + Post("/customRecords/toggle", new(CustomRecordsToggleAction)). + EndAll() + }) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go new file mode 100644 index 0000000..fe383c5 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go @@ -0,0 +1,128 @@ +package apps + +import ( + "strings" + + "github.com/iwind/TeaGo/maps" +) + +func mockApps() []maps.Map { + return []maps.Map{ + { + "id": int64(1), + "name": "主站移动业务", + "appId": "ab12xc34s2", + "clusterId": int64(1), + "domainCount": 3, + "isOn": true, + "authStatus": "enabled", + "ecsMode": "auto", + "pinningMode": "report", + "sanMode": "strict", + "riskLevel": "medium", + "riskSummary": "Pinning 处于观察模式", + "secretVersion": "v2026.02.20", + }, + { + "id": int64(2), + "name": "视频网关业务", + "appId": "vd8992ksm1", + "clusterId": int64(2), + "domainCount": 1, + "isOn": true, + "authStatus": "enabled", + "ecsMode": "custom", + "pinningMode": "enforce", + "sanMode": "strict", + "riskLevel": "low", + "riskSummary": "已启用强校验", + "secretVersion": "v2026.02.18", + }, + { + "id": int64(3), + "name": "海外灰度测试", + "appId": "ov7711hkq9", + "clusterId": int64(1), + "domainCount": 2, + "isOn": false, + "authStatus": "disabled", + "ecsMode": "off", + "pinningMode": "off", + "sanMode": "report", + "riskLevel": "high", + "riskSummary": "应用关闭且证书策略偏弱", + "secretVersion": "v2026.01.30", + }, + } +} + +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 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] +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies.go new file mode 100644 index 0000000..36c5296 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies.go @@ -0,0 +1,85 @@ +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() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies_store.go new file mode 100644 index 0000000..08aee8f --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies_store.go @@ -0,0 +1,54 @@ +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() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go new file mode 100644 index 0000000..645d21a --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go @@ -0,0 +1,21 @@ +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 { + actionutils.ParentAction +} + +func (this *CertsAction) Init() { + this.Nav("httpdns", "cluster", "certs") +} + +func (this *CertsAction) RunGet(params struct{}) { + httpdnsutils.AddLeftMenu(this.Parent()) + this.Data["certs"] = policies.LoadPublicSNICertificates() + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/checkPorts.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/checkPorts.go new file mode 100644 index 0000000..63edfa2 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/checkPorts.go @@ -0,0 +1,10 @@ +package clusters +import ( +"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" +"github.com/iwind/TeaGo/maps" +) +type CheckPortsAction struct { actionutils.ParentAction } +func (this *CheckPortsAction) RunPost(params struct{ NodeId int64 }) { +this.Data["results"] = []maps.Map{} +this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go new file mode 100644 index 0000000..de5cf5d --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go @@ -0,0 +1,34 @@ +package clusters + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type ClusterAction struct { + actionutils.ParentAction +} + +func (this *ClusterAction) Init() { + this.Nav("httpdns", "cluster", "index") +} + +func (this *ClusterAction) RunGet(params struct { + ClusterId int64 + InstalledState int + ActiveState int + Keyword string +}) { + httpdnsutils.AddLeftMenu(this.Parent()) + cluster := pickCluster(params.ClusterId) + 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() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go new file mode 100644 index 0000000..c516d9e --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go @@ -0,0 +1,68 @@ +package node + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/maps" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("", "node", "node") + this.SecondMenu("nodes") +} + +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, + }, + } + + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go new file mode 100644 index 0000000..eeb79a6 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go @@ -0,0 +1,41 @@ +package node + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/maps" +) + +type InstallAction struct { + actionutils.ParentAction +} + +func (this *InstallAction) Init() { + this.Nav("", "node", "install") + this.SecondMenu("nodes") +} + +func (this *InstallAction) 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["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"}, + } + this.Data["installStatus"] = nil + + this.Show() +} + +func (this *InstallAction) RunPost(params struct{ NodeId int64 }) { + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go new file mode 100644 index 0000000..e88e779 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go @@ -0,0 +1,31 @@ +package node + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/maps" +) + +type LogsAction struct { + actionutils.ParentAction +} + +func (this *LogsAction) Init() { + this.Nav("", "node", "log") + this.SecondMenu("nodes") +} + +func (this *LogsAction) 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["dayFrom"] = "" + this.Data["dayTo"] = "" + this.Data["keyword"] = "" + this.Data["level"] = "" + this.Data["logs"] = []maps.Map{} + this.Data["page"] = "" + this.Data["node"] = maps.Map{"id": params.NodeId, "name": "Mock Node"} + + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go new file mode 100644 index 0000000..de506c5 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go @@ -0,0 +1,13 @@ +package node + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" +) + +type StartAction struct { + actionutils.ParentAction +} + +func (this *StartAction) RunPost(params struct{ NodeId int64 }) { + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go new file mode 100644 index 0000000..a85d567 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go @@ -0,0 +1,27 @@ +package node + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/maps" +) + +type StatusAction struct { + actionutils.ParentAction +} + +func (this *StatusAction) Init() { + this.Nav("", "node", "status") + this.SecondMenu("nodes") +} + +func (this *StatusAction) RunPost(params struct{ NodeId int64 }) { + this.Data["installStatus"] = maps.Map{ + "isRunning": false, + "isFinished": true, + "isOk": true, + "error": "", + "errorCode": "", + } + this.Data["isInstalled"] = true + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go new file mode 100644 index 0000000..10b3ad3 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go @@ -0,0 +1,13 @@ +package node + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" +) + +type StopAction struct { + actionutils.ParentAction +} + +func (this *StopAction) RunPost(params struct{ NodeId int64 }) { + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go new file mode 100644 index 0000000..1e86260 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go @@ -0,0 +1,41 @@ +package node + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/maps" +) + +type UpdateAction struct { + actionutils.ParentAction +} + +func (this *UpdateAction) Init() { + this.Nav("", "node", "update") + this.SecondMenu("nodes") +} + +func (this *UpdateAction) 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["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["apiNodeAddrs"] = []string{} + + this.Data["node"] = maps.Map{ + "id": params.NodeId, + "name": "Mock Node", + "isOn": true, + "ipAddresses": []maps.Map{}, + } + + this.Show() +} + +func (this *UpdateAction) RunPost(params struct{ NodeId int64 }) { + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go new file mode 100644 index 0000000..f655c57 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go @@ -0,0 +1,13 @@ +package node + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" +) + +type UpdateInstallStatusAction struct { + actionutils.ParentAction +} + +func (this *UpdateInstallStatusAction) RunPost(params struct{ NodeId int64 }) { + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go new file mode 100644 index 0000000..f025b0f --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go @@ -0,0 +1,49 @@ +package clusters + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type ClusterSettingsAction struct { + actionutils.ParentAction +} + +func (this *ClusterSettingsAction) Init() { + this.Nav("httpdns", "cluster", "settings") +} + +func (this *ClusterSettingsAction) RunGet(params struct { + ClusterId int64 +}) { + httpdnsutils.AddLeftMenu(this.Parent()) + cluster := pickCluster(params.ClusterId) + installDir := cluster.GetString("installDir") + if len(installDir) == 0 { + installDir = "/opt/edge-httpdns" + } + this.Data["cluster"] = cluster + this.Data["settings"] = map[string]interface{}{ + "region": cluster.GetString("region"), + "gatewayDomain": cluster.GetString("gatewayDomain"), + "cacheTtl": cluster.GetInt("cacheTtl"), + "fallbackTimeout": cluster.GetInt("fallbackTimeout"), + "installDir": installDir, + "isOn": cluster.GetBool("isOn"), + } + this.Show() +} + +func (this *ClusterSettingsAction) RunPost(params struct { + ClusterId int64 + Name string + Region string + GatewayDomain string + CacheTtl int32 + FallbackTimeout int32 + InstallDir string + IsOn bool +}) { + // Mock successful save + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go new file mode 100644 index 0000000..e8779f5 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go @@ -0,0 +1,19 @@ +package clusters + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type CreateAction struct { + actionutils.ParentAction +} + +func (this *CreateAction) Init() { + this.Nav("httpdns", "cluster", "") +} + +func (this *CreateAction) RunGet(params struct{}) { + httpdnsutils.AddLeftMenu(this.Parent()) + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go new file mode 100644 index 0000000..65dc880 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go @@ -0,0 +1,30 @@ +package clusters + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" + "github.com/iwind/TeaGo/maps" +) + +type CreateNodeAction struct { + actionutils.ParentAction +} + +func (this *CreateNodeAction) Init() { + this.Nav("", "node", "createNode") + this.SecondMenu("nodes") +} + +func (this *CreateNodeAction) RunGet(params struct{ ClusterId int64 }) { + httpdnsutils.AddLeftMenu(this.Parent()) + this.Data["clusterId"] = params.ClusterId + this.Data["cluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"} + this.Show() +} + +func (this *CreateNodeAction) RunPost(params struct { + ClusterId int64 + Name string +}) { + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go new file mode 100644 index 0000000..57c246b --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go @@ -0,0 +1,30 @@ +package clusters + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type DeleteAction struct { + actionutils.ParentAction +} + +func (this *DeleteAction) Init() { + this.Nav("httpdns", "cluster", "delete") +} + +func (this *DeleteAction) RunGet(params struct { + ClusterId int64 +}) { + httpdnsutils.AddLeftMenu(this.Parent()) + cluster := pickCluster(params.ClusterId) + this.Data["cluster"] = cluster + this.Show() +} + +func (this *DeleteAction) RunPost(params struct { + ClusterId int64 +}) { + _ = params.ClusterId + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go new file mode 100644 index 0000000..7adcd40 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go @@ -0,0 +1,6 @@ +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() } diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go new file mode 100644 index 0000000..f1ead83 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go @@ -0,0 +1,20 @@ +package clusters + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("httpdns", "cluster", "") +} + +func (this *IndexAction) RunGet(params struct{}) { + httpdnsutils.AddLeftMenu(this.Parent()) + this.Data["clusters"] = mockClusters() + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go new file mode 100644 index 0000000..bfecd53 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go @@ -0,0 +1,41 @@ +package clusters + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). + Data("teaMenu", "httpdns"). + Data("teaSubMenu", "cluster"). + Prefix("/httpdns/clusters"). + Get("", new(IndexAction)). + Get("/create", new(CreateAction)). + Get("/cluster", new(ClusterAction)). + GetPost("/cluster/settings", new(ClusterSettingsAction)). + GetPost("/delete", new(DeleteAction)). + // Node level + GetPost("/createNode", new(CreateNodeAction)). + Post("/deleteNode", new(DeleteNodeAction)). + Get("/upgradeRemote", new(UpgradeRemoteAction)). + GetPost("/updateNodeSSH", new(UpdateNodeSSHAction)). + Post("/checkPorts", new(CheckPortsAction)). + + // Node internal pages + Prefix("/httpdns/clusters/cluster/node"). + Get("", new(node.IndexAction)). + Get("/logs", new(node.LogsAction)). + GetPost("/update", new(node.UpdateAction)). + GetPost("/install", new(node.InstallAction)). + Post("/status", new(node.StatusAction)). + Post("/updateInstallStatus", new(node.UpdateInstallStatusAction)). + Post("/start", new(node.StartAction)). + Post("/stop", new(node.StopAction)). + EndAll() + }) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go new file mode 100644 index 0000000..092eb23 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go @@ -0,0 +1,126 @@ +package clusters + +import "github.com/iwind/TeaGo/maps" + +func mockClusters() []maps.Map { + return []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, + }, + } +} + +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", + }, + } +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go new file mode 100644 index 0000000..f8360e4 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go @@ -0,0 +1,39 @@ +package clusters + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/maps" +) + +type UpdateNodeSSHAction struct { + actionutils.ParentAction +} + +func (this *UpdateNodeSSHAction) RunGet(params struct { + NodeId int64 +}) { + this.Data["nodeId"] = params.NodeId + this.Data["clusterId"] = 0 + this.Data["node"] = maps.Map{ + "id": params.NodeId, + "name": "Mock Node", + } + this.Data["loginId"] = 0 + this.Data["params"] = maps.Map{ + "host": "1.2.3.4", + "port": 22, + "grantId": 0, + } + this.Data["grant"] = nil + this.Show() +} + +func (this *UpdateNodeSSHAction) RunPost(params struct { + NodeId int64 + LoginId int64 + SshHost string + SshPort int + GrantId int64 +}) { + this.Success() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/upgradeRemote.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/upgradeRemote.go new file mode 100644 index 0000000..ba99390 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/upgradeRemote.go @@ -0,0 +1,16 @@ +package clusters + +import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + +type UpgradeRemoteAction struct { + actionutils.ParentAction +} + +func (this *UpgradeRemoteAction) RunGet(params struct { + NodeId int64 + ClusterId int64 +}) { + this.Data["nodeId"] = params.NodeId + this.Data["clusterId"] = params.ClusterId + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go new file mode 100644 index 0000000..e8f7d74 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go @@ -0,0 +1,28 @@ +package ech + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type AuditAction struct { + actionutils.ParentAction +} + +func (this *AuditAction) Init() { + this.Nav("httpdns", "ech", "") +} + +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() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go new file mode 100644 index 0000000..b33aa5f --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go @@ -0,0 +1,47 @@ +package ech + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("httpdns", "ech", "") +} + +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() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/init.go new file mode 100644 index 0000000..b4564e9 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/init.go @@ -0,0 +1,21 @@ +package ech + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). + Data("teaMenu", "httpdns"). + Data("teaSubMenu", "ech"). + Prefix("/httpdns/ech"). + Get("", new(IndexAction)). + Get("/audit", new(AuditAction)). + GetPost("/rollbackMfaPopup", new(RollbackMfaPopupAction)). + EndAll() + }) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go new file mode 100644 index 0000000..4fc36a2 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go @@ -0,0 +1,36 @@ +package ech + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/actions" +) + +type RollbackMfaPopupAction struct { + actionutils.ParentAction +} + +func (this *RollbackMfaPopupAction) Init() { + this.Nav("", "", "") +} + +func (this *RollbackMfaPopupAction) RunGet(params struct { + LogId int64 +}) { + this.Data["logId"] = params.LogId + this.Show() +} + +func (this *RollbackMfaPopupAction) RunPost(params struct { + LogId int64 + Reason string + OtpCode1 string + OtpCode2 string + + 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() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/guide/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/guide/index.go new file mode 100644 index 0000000..cc2b57c --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/guide/index.go @@ -0,0 +1,66 @@ +package guide + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + httpdnsApps "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" + httpdnsPolicies "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies" + "github.com/iwind/TeaGo/maps" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("httpdns", "guide", "") +} + +func (this *IndexAction) RunGet(params struct{}) { + httpdnsutils.AddLeftMenu(this.Parent()) + + apps := []maps.Map{ + { + "id": int64(1), + "name": "主站移动业务", + "appId": "ab12xc34s2", + "clusterId": int64(1), + }, + { + "id": int64(2), + "name": "视频网关业务", + "appId": "vd8992ksm1", + "clusterId": int64(2), + }, + { + "id": int64(3), + "name": "海外灰度测试", + "appId": "ov7711hkq9", + "clusterId": int64(1), + }, + } + + for _, app := range apps { + clusterID := app.GetInt64("clusterId") + app["gatewayDomain"] = httpdnsPolicies.LoadClusterGatewayByID(clusterID) + + settings := httpdnsApps.LoadAppSettingsByAppID(app.GetString("appId")) + if settings == nil { + app["signSecret"] = "" + app["signSecretMasked"] = "" + app["aesSecret"] = "" + app["aesSecretMasked"] = "" + app["signEnabled"] = false + continue + } + + app["signSecret"] = settings.GetString("signSecretPlain") + app["signSecretMasked"] = settings.GetString("signSecretMasked") + app["aesSecret"] = settings.GetString("aesSecretPlain") + app["aesSecretMasked"] = settings.GetString("aesSecretMasked") + app["signEnabled"] = settings.GetBool("signEnabled") + } + + this.Data["apps"] = apps + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/guide/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/guide/init.go new file mode 100644 index 0000000..efccb9d --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/guide/init.go @@ -0,0 +1,19 @@ +package guide + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). + Data("teaMenu", "httpdns"). + Data("teaSubMenu", "guide"). + Prefix("/httpdns/guide"). + Get("", new(IndexAction)). + EndAll() + }) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils/helper.go b/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils/helper.go new file mode 100644 index 0000000..9d5ce32 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils/helper.go @@ -0,0 +1,49 @@ +package httpdnsutils + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/maps" +) + +func AddLeftMenu(action *actionutils.ParentAction) { + tab := action.Data.GetString("mainTab") + action.Data["teaMenu"] = "httpdns" + action.Data["teaSubMenu"] = tab + action.Data["leftMenuItems"] = []maps.Map{ + { + "name": "集群管理", + "url": "/httpdns/clusters", + "isActive": tab == "cluster", + }, + { + "name": "全局配置", + "url": "/httpdns/policies", + "isActive": tab == "policy", + }, + { + "name": "应用管理", + "url": "/httpdns/apps", + "isActive": tab == "app", + }, + { + "name": "SDK接入引导", + "url": "/httpdns/guide", + "isActive": tab == "guide", + }, + { + "name": "解析日志", + "url": "/httpdns/resolveLogs", + "isActive": tab == "resolveLogs", + }, + { + "name": "运行日志", + "url": "/httpdns/runtimeLogs", + "isActive": tab == "runtimeLogs", + }, + { + "name": "解析测试", + "url": "/httpdns/sandbox", + "isActive": tab == "sandbox", + }, + } +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/index.go new file mode 100644 index 0000000..df37453 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/index.go @@ -0,0 +1,11 @@ +package httpdns + +import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) RunGet(params struct{}) { + this.RedirectURL("/httpdns/clusters") +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/init.go new file mode 100644 index 0000000..aabd270 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/init.go @@ -0,0 +1,19 @@ +package httpdns + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). + Data("teaMenu", "httpdns"). + Data("teaSubMenu", "cluster"). + Prefix("/httpdns"). + Get("", new(IndexAction)). + EndAll() + }) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go new file mode 100644 index 0000000..9ac3052 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go @@ -0,0 +1,68 @@ +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" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("httpdns", "policy", "") +} + +func (this *IndexAction) RunGet(params struct{}) { + httpdnsutils.AddLeftMenu(this.Parent()) + this.Data["policies"] = loadGlobalPolicies() + this.Data["availableClusters"] = loadAvailableDeployClusters() + this.Show() +} + +func (this *IndexAction) RunPost(params struct { + DefaultClusterId int64 + EnableUserDomainVerify bool + 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": params.EnableUserDomainVerify, + "defaultTTL": params.DefaultTTL, + "defaultFallbackMs": params.DefaultFallbackMs, + }) + + this.Success() +} + +func isValidClusterID(clusterID int64) bool { + for _, cluster := range loadAvailableDeployClusters() { + if cluster.GetInt64("id") == clusterID { + return true + } + } + return false +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/policies/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/policies/init.go new file mode 100644 index 0000000..cef7fcf --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/policies/init.go @@ -0,0 +1,19 @@ +package policies + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). + Data("teaMenu", "httpdns"). + Data("teaSubMenu", "policy"). + Prefix("/httpdns/policies"). + GetPost("", new(IndexAction)). + EndAll() + }) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go b/EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go new file mode 100644 index 0000000..68cc1e0 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go @@ -0,0 +1,172 @@ +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": globalPoliciesStore.data.GetBool("enableUserDomainVerify"), + "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": policies.GetBool("enableUserDomainVerify"), + "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") +} + +// 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 +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go new file mode 100644 index 0000000..e7e8505 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go @@ -0,0 +1,151 @@ +package resolveLogs + +import ( + "strings" + + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("httpdns", "resolveLogs", "") +} + +func (this *IndexAction) RunGet(params struct { + ClusterId int64 + NodeId int64 + AppId string + Domain string + Status string + Keyword string +}) { + httpdnsutils.AddLeftMenu(this.Parent()) + + this.Data["clusterId"] = params.ClusterId + this.Data["nodeId"] = params.NodeId + this.Data["appId"] = params.AppId + this.Data["domain"] = params.Domain + 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"}, + } + 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"}, + } + 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", + }, + } + + 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)) + + 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) + } + + this.Data["resolveLogs"] = filtered + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/init.go new file mode 100644 index 0000000..da6afb4 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/init.go @@ -0,0 +1,19 @@ +package resolveLogs + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). + Data("teaMenu", "httpdns"). + Data("teaSubMenu", "resolveLogs"). + Prefix("/httpdns/resolveLogs"). + Get("", new(IndexAction)). + EndAll() + }) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go new file mode 100644 index 0000000..cd7b9b6 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go @@ -0,0 +1,137 @@ +package runtimeLogs + +import ( + "strings" + + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("httpdns", "runtimeLogs", "") +} + +func (this *IndexAction) RunGet(params struct { + ClusterId int64 + NodeId int64 + DayFrom string + DayTo string + Level string + Keyword string +}) { + httpdnsutils.AddLeftMenu(this.Parent()) + + this.Data["clusterId"] = params.ClusterId + this.Data["nodeId"] = params.NodeId + this.Data["dayFrom"] = params.DayFrom + this.Data["dayTo"] = params.DayTo + 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"}, + } + 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"}, + } + 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", + }, + } + + 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 + } + 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["module"].(string)), keyword) && + !strings.Contains(strings.ToLower(log["description"].(string)), keyword) && + !strings.Contains(strings.ToLower(log["nodeName"].(string)), keyword) { + continue + } + } + filtered = append(filtered, log) + } + + this.Data["runtimeLogs"] = filtered + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/init.go new file mode 100644 index 0000000..0816d7b --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/init.go @@ -0,0 +1,19 @@ +package runtimeLogs + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). + Data("teaMenu", "httpdns"). + Data("teaSubMenu", "runtimeLogs"). + Prefix("/httpdns/runtimeLogs"). + Get("", new(IndexAction)). + EndAll() + }) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go new file mode 100644 index 0000000..62049e1 --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go @@ -0,0 +1,36 @@ +package sandbox + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("httpdns", "sandbox", "") +} + +func (this *IndexAction) RunGet(params struct{}) { + httpdnsutils.AddLeftMenu(this.Parent()) + this.Data["apps"] = []map[string]interface{}{ + { + "id": int64(1), + "name": "主站移动业务", + "appId": "ab12xc34s2", + }, + { + "id": int64(2), + "name": "视频网关业务", + "appId": "vd8992ksm1", + }, + { + "id": int64(3), + "name": "海外灰度测试", + "appId": "ov7711hkq9", + }, + } + this.Show() +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/init.go new file mode 100644 index 0000000..9a68b4e --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/init.go @@ -0,0 +1,20 @@ +package sandbox + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)). + Data("teaMenu", "httpdns"). + Data("teaSubMenu", "sandbox"). + Prefix("/httpdns/sandbox"). + Get("", new(IndexAction)). + Post("/test", new(TestAction)). + EndAll() + }) +} diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go new file mode 100644 index 0000000..5811a0f --- /dev/null +++ b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go @@ -0,0 +1,163 @@ +package sandbox + +import ( + "encoding/json" + "fmt" + "net" + "net/url" + "strconv" + "strings" + + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/maps" +) + +type TestAction struct { + actionutils.ParentAction +} + +func (this *TestAction) RunPost(params struct { + AppId string + Domain string + ClientIp string + Qtype string + SDNSParamsJSON string +}) { + if len(params.ClientIp) == 0 { + params.ClientIp = "203.0.113.100" + } + params.SDNSParamsJSON = strings.TrimSpace(params.SDNSParamsJSON) + + sdnsParams, err := parseSDNSParamsJSON(params.SDNSParamsJSON) + if err != nil { + this.Fail(err.Error()) + return + } + + clientSubnet := this.maskSubnet(params.ClientIp, 24, 56) + if len(clientSubnet) == 0 { + clientSubnet = "203.0.113.0/24" + } + + sniPolicy := "empty" + publicSNI := "" + ecsMode := "off" + ecsIPv4Prefix := 24 + ecsIPv6Prefix := 56 + pinningMode := "off" + sanMode := "off" + query := url.Values{} + query.Set("appId", params.AppId) + query.Set("dn", params.Domain) + query.Set("cip", params.ClientIp) + query.Set("qtype", params.Qtype) + for _, item := range sdnsParams { + query.Add("sdns_"+item.GetString("name"), item.GetString("value")) + } + requestURL := "https://api.httpdns.example.com/resolve?" + query.Encode() + + this.Data["result"] = maps.Map{ + "code": 0, + "message": "ok (mock)", + "requestId": "mock-rid-20260221-001", + "data": maps.Map{ + "request_url": requestURL, + "client_ip": params.ClientIp, + "client_region": "中国, 上海, 上海", + "line_name": "默认线路", + "sdns_params": sdnsParams, + "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", + }, + }, + } + this.Success() +} + +func parseSDNSParamsJSON(raw string) ([]maps.Map, error) { + if len(raw) == 0 { + return []maps.Map{}, nil + } + + list := []maps.Map{} + if err := json.Unmarshal([]byte(raw), &list); err != nil { + return nil, err + } + if len(list) > 10 { + return nil, fmt.Errorf("sdns params should be <= 10") + } + + result := make([]maps.Map, 0, len(list)) + for _, item := range list { + name := strings.TrimSpace(item.GetString("name")) + value := strings.TrimSpace(item.GetString("value")) + if len(name) == 0 && len(value) == 0 { + continue + } + if len(name) == 0 || len(name) > 64 { + return nil, fmt.Errorf("sdns param name should be in 1-64") + } + if len(value) == 0 || len(value) > 64 { + return nil, fmt.Errorf("sdns param value should be in 1-64") + } + result = append(result, maps.Map{ + "name": name, + "value": value, + }) + } + + if len(result) > 10 { + return nil, fmt.Errorf("sdns params should be <= 10") + } + return result, nil +} + +func (this *TestAction) maskSubnet(ip string, ipv4Prefix int, ipv6Prefix int) string { + parsed := net.ParseIP(ip) + if parsed == nil { + return "" + } + + ipv4 := parsed.To4() + if ipv4 != nil { + mask := net.CIDRMask(ipv4Prefix, 32) + return ipv4.Mask(mask).String() + "/" + strconv.Itoa(ipv4Prefix) + } + + mask := net.CIDRMask(ipv6Prefix, 128) + return parsed.Mask(mask).String() + "/" + strconv.Itoa(ipv6Prefix) +} diff --git a/EdgeAdmin/internal/web/helpers/menu.go b/EdgeAdmin/internal/web/helpers/menu.go index 5712079..e7a3108 100644 --- a/EdgeAdmin/internal/web/helpers/menu.go +++ b/EdgeAdmin/internal/web/helpers/menu.go @@ -113,6 +113,50 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i }, }, }, + { + "code": "httpdns", + "module": configloaders.AdminModuleCodeHttpDNS, + "name": "HTTPDNS", + "subtitle": "", + "icon": "shield alternate", + "subItems": []maps.Map{ + { + "name": "集群管理", + "url": "/httpdns/clusters", + "code": "cluster", + }, + { + "name": "全局配置", + "url": "/httpdns/policies", + "code": "policy", + }, + { + "name": "应用管理", + "url": "/httpdns/apps", + "code": "app", + }, + { + "name": "SDK接入引导", + "url": "/httpdns/guide", + "code": "guide", + }, + { + "name": "解析日志", + "url": "/httpdns/resolveLogs", + "code": "resolveLogs", + }, + { + "name": "运行日志", + "url": "/httpdns/runtimeLogs", + "code": "runtimeLogs", + }, + { + "name": "解析测试", + "url": "/httpdns/sandbox", + "code": "sandbox", + }, + }, + }, { "code": "dns", "module": configloaders.AdminModuleCodeDNS, diff --git a/EdgeAdmin/internal/web/helpers/menu_plus.go b/EdgeAdmin/internal/web/helpers/menu_plus.go index 9e40652..c792b6d 100644 --- a/EdgeAdmin/internal/web/helpers/menu_plus.go +++ b/EdgeAdmin/internal/web/helpers/menu_plus.go @@ -178,6 +178,50 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i }, }, }, + { + "code": "httpdns", + "module": configloaders.AdminModuleCodeHttpDNS, + "name": "HTTPDNS", + "subtitle": "", + "icon": "shield alternate", + "subItems": []maps.Map{ + { + "name": "集群管理", + "url": "/httpdns/clusters", + "code": "cluster", + }, + { + "name": "全局配置", + "url": "/httpdns/policies", + "code": "policy", + }, + { + "name": "应用管理", + "url": "/httpdns/apps", + "code": "app", + }, + { + "name": "SDK接入引导", + "url": "/httpdns/guide", + "code": "guide", + }, + { + "name": "解析日志", + "url": "/httpdns/resolveLogs", + "code": "resolveLogs", + }, + { + "name": "运行日志", + "url": "/httpdns/runtimeLogs", + "code": "runtimeLogs", + }, + { + "name": "解析测试", + "url": "/httpdns/sandbox", + "code": "sandbox", + }, + }, + }, { "code": "dns", "module": configloaders.AdminModuleCodeDNS, diff --git a/EdgeAdmin/internal/web/import.go b/EdgeAdmin/internal/web/import.go index 6cd88b9..10f2c89 100644 --- a/EdgeAdmin/internal/web/import.go +++ b/EdgeAdmin/internal/web/import.go @@ -136,4 +136,15 @@ import ( // 平台用户 _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/users" + + // HTTPDNS体系 + _ "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/resolveLogs" + _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs" + _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/sandbox" ) diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html b/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html new file mode 100644 index 0000000..3e06600 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html @@ -0,0 +1,4 @@ + + 应用列表 + [添加应用] + diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/appSettings.html b/EdgeAdmin/web/views/@default/httpdns/apps/appSettings.html new file mode 100644 index 0000000..e8112e6 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/appSettings.html @@ -0,0 +1,116 @@ +{$layout} +{$template "menu"} + + + +
+ +
+ +
+ + + +
+ + +
+ + + + + + + + + + + + + + +
App ID{{settings.appId}}
应用启用
SNI 防护配置 + 隐匿 SNI +

当前默认采用隐匿 SNI 策略,避免在 TLS 握手阶段暴露业务域名。

+
+ + + + + + + + + + + + + + +
请求验签 + 已开启 + 已关闭 + {{settings.signEnabled ? "关闭请求验签" : "开启请求验签"}} +

打开后,服务端会对请求进行签名校验。

+
加签 Secret + {{signSecretVisible ? settings.signSecretPlain : settings.signSecretMasked}} + + + [重置] +

最近更新:{{settings.signSecretUpdatedAt}}

+

请求验签已关闭,当前不使用加签 Secret。

+

用于生成鉴权接口的安全密钥。

+
AES 数据加密 Secret + {{aesSecretVisible ? settings.aesSecretPlain : settings.aesSecretMasked}} + + + [重置] +

最近更新:{{settings.aesSecretUpdatedAt}}

+

用于解析接口数据加密的密钥。

+
+ + +
+
+
+
diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/appSettings.js b/EdgeAdmin/web/views/@default/httpdns/apps/appSettings.js new file mode 100644 index 0000000..31b3d60 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/appSettings.js @@ -0,0 +1,111 @@ +Tea.context(function () { + this.activeSection = "basic"; + this.success = NotifyReloadSuccess("保存成功"); + this.signSecretVisible = false; + this.aesSecretVisible = false; + + this.toggleSignEnabled = function () { + let that = this; + let targetIsOn = !this.settings.signEnabled; + + if (targetIsOn) { + teaweb.confirm("html:开启后,服务端将会对解析请求进行验签鉴权,未签名、签名无效或过期的请求都解析失败,确认开启吗?", function () { + that.$post("/httpdns/apps/app/settings/toggleSignEnabled") + .params({ + appId: that.app.id, + isOn: 1 + }) + .success(function () { + that.settings.signEnabled = true; + teaweb.success("请求验签已开启"); + }); + }); + return; + } + + teaweb.confirm("html:关闭后,服务端将不会对解析请求进行验签鉴权,可能存在被刷风险,确认关闭吗?", function () { + that.$post("/httpdns/apps/app/settings/toggleSignEnabled") + .params({ + appId: that.app.id, + isOn: 0 + }) + .success(function () { + that.settings.signEnabled = false; + teaweb.success("请求验签已关闭"); + }); + }); + }; + + this.copySecret = function (text, name) { + if (typeof text != "string" || text.length == 0) { + teaweb.warn("没有可复制的内容"); + return; + } + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { + teaweb.success(name + "已复制"); + }).catch(function () { + this.copyByTextarea(text, name); + }.bind(this)); + return; + } + + this.copyByTextarea(text, name); + }; + + this.copyByTextarea = function (text, name) { + let input = document.createElement("textarea"); + input.value = text; + input.setAttribute("readonly", "readonly"); + input.style.position = "fixed"; + input.style.left = "-10000px"; + input.style.top = "-10000px"; + document.body.appendChild(input); + input.select(); + + let ok = false; + try { + ok = document.execCommand("copy"); + } catch (e) { + ok = false; + } + document.body.removeChild(input); + + if (ok) { + teaweb.success(name + "已复制"); + } else { + teaweb.warn("复制失败,请手动复制"); + } + }; + + this.resetSignSecret = function () { + let that = this; + teaweb.confirm("确定要重置加签 Secret 吗?", function () { + that.$post("/httpdns/apps/app/settings/resetSignSecret") + .params({ + appId: that.app.id + }) + .success(function () { + teaweb.success("加签 Secret 已重置", function () { + teaweb.reload(); + }); + }); + }); + }; + + this.resetAESSecret = function () { + let that = this; + teaweb.confirm("确定要重置 AES Secret 吗?", function () { + that.$post("/httpdns/apps/app/settings/resetAESSecret") + .params({ + appId: that.app.id + }) + .success(function () { + teaweb.success("AES Secret 已重置", function () { + teaweb.reload(); + }); + }); + }); + }; +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/createPopup.html b/EdgeAdmin/web/views/@default/httpdns/apps/createPopup.html new file mode 100644 index 0000000..b09452a --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/createPopup.html @@ -0,0 +1,37 @@ +{$layout "layout_popup"} + +

添加应用

+ +
+ + + + + + + + + + + + + + +
应用名称 * + +

为该 HTTPDNS 应用设置一个便于识别的名称。

+
所属集群 * + +

应用的解析请求将由所选集群下的网关节点处理。默认值来自“HTTPDNS 用户设置”。

+
所属用户 + +

可分配给指定租户用户;留空表示平台管理员自用。

+
+ +
diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/customRecords.html b/EdgeAdmin/web/views/@default/httpdns/apps/customRecords.html new file mode 100644 index 0000000..bac2d3c --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/customRecords.html @@ -0,0 +1,66 @@ +{$layout} +{$template "menu"} + + + +
+ +
+ +
+ 当前域名:{{domain.name}} +
+ + + 新增自定义解析规则 + + 返回域名管理 + + + + + + + + + + + + + + + + + + + + + + + + +
线路规则名称SDNS 参数解析记录TTL状态操作
{{record.lineText}}{{record.ruleName}}{{record.paramsText}} + {{record.recordValueText}} + {{record.ttl}}s + 已启用 + 已停用 + + 编辑  |  + {{record.isOn ? "停用" : "启用"}}  |  + 删除 +
+ +

当前应用暂无可用域名,请先到域名管理中添加域名。

+

当前域名还没有自定义解析规则。

+
diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/customRecords.js b/EdgeAdmin/web/views/@default/httpdns/apps/customRecords.js new file mode 100644 index 0000000..4bd4d56 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/customRecords.js @@ -0,0 +1,50 @@ +Tea.context(function () { + if (typeof this.records == "undefined") { + this.records = []; + } + + this.createRecord = function () { + if (!this.domain || !this.domain.id) { + return; + } + teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id, { + width: "56em", + height: "40em", + title: "新增自定义解析规则" + }); + }; + + this.editRecord = function (recordId) { + if (!this.domain || !this.domain.id) { + return; + } + teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id + "&recordId=" + recordId, { + width: "56em", + height: "40em", + title: "编辑自定义解析规则" + }); + }; + + this.deleteRecord = function (recordId) { + let that = this; + teaweb.confirm("确定要删除这条自定义解析规则吗?", function () { + that.$post("/httpdns/apps/customRecords/delete") + .params({ + appId: that.app.id, + recordId: recordId + }) + .refresh(); + }); + }; + + this.toggleRecord = function (record) { + let that = this; + that.$post("/httpdns/apps/customRecords/toggle") + .params({ + appId: that.app.id, + recordId: record.id, + isOn: record.isOn ? 0 : 1 + }) + .refresh(); + }; +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/customRecordsCreatePopup.html b/EdgeAdmin/web/views/@default/httpdns/apps/customRecordsCreatePopup.html new file mode 100644 index 0000000..57f0192 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/customRecordsCreatePopup.html @@ -0,0 +1,158 @@ +{$layout "layout_popup"} + + + +

编辑自定义解析规则

+

新增自定义解析规则

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
域名 *{{record.domain}}
规则名称 * + +

例如:上海电信灰度-v2。

+
线路 +
+ + + + + + + + +
+
SDNS 参数配置 +
+
+ +
+
+ +
+ +
+
+ + 添加参数 + + {{sdnsParams.length}}/10 +
+
解析记录值 * +
+
+ + +
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+ + 添加记录值 + + {{recordItems.length}}/10 +
+

开启后每条记录可配置权重,范围 1-100。

+
TTL * +
+ + +
+
规则状态 +
+ + +
+
+ + +
diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/customRecordsCreatePopup.js b/EdgeAdmin/web/views/@default/httpdns/apps/customRecordsCreatePopup.js new file mode 100644 index 0000000..b588cb1 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/customRecordsCreatePopup.js @@ -0,0 +1,177 @@ +Tea.context(function () { + var vm = this; + + if (typeof vm.record == "undefined" || vm.record == null) { + vm.record = {}; + } + + vm.chinaCarriers = ["默认", "电信", "联通", "移动", "教育网", "鹏博士", "广电"]; + vm.chinaRegions = ["默认", "东北", "华北", "华东", "华南", "华中", "西北", "西南"]; + vm.chinaRegionProvinces = { + "默认": ["默认"], + "东北": ["默认", "辽宁", "吉林", "黑龙江"], + "华北": ["默认", "北京", "天津", "河北", "山西", "内蒙古"], + "华东": ["默认", "上海", "江苏", "浙江", "安徽", "福建", "江西", "山东"], + "华南": ["默认", "广东", "广西", "海南"], + "华中": ["默认", "河南", "湖北", "湖南"], + "西北": ["默认", "陕西", "甘肃", "青海", "宁夏", "新疆"], + "西南": ["默认", "重庆", "四川", "贵州", "云南", "西藏"] + }; + + vm.continents = ["默认", "非洲", "南极洲", "亚洲", "欧洲", "北美洲", "南美洲", "大洋洲"]; + vm.continentCountries = { + "默认": ["默认"], + "非洲": ["默认", "南非", "埃及", "尼日利亚", "肯尼亚", "摩洛哥"], + "南极洲": ["默认"], + "亚洲": ["默认", "中国香港", "中国澳门", "中国台湾", "日本", "韩国", "新加坡", "印度", "泰国", "越南"], + "欧洲": ["默认", "德国", "英国", "法国", "荷兰", "西班牙", "意大利", "俄罗斯"], + "北美洲": ["默认", "美国", "加拿大", "墨西哥"], + "南美洲": ["默认", "巴西", "阿根廷", "智利", "哥伦比亚"], + "大洋洲": ["默认", "澳大利亚", "新西兰"] + }; + + vm.parseJSONList = function (raw) { + if (typeof raw == "undefined" || raw == null || raw == "") { + return []; + } + + if (Array.isArray(raw)) { + return raw; + } + + try { + var list = JSON.parse(raw); + if (Array.isArray(list)) { + return list; + } + } catch (e) { + } + + return []; + }; + + vm.normalizeBoolean = function (value, defaultValue) { + if (typeof value == "boolean") { + return value; + } + if (typeof value == "number") { + return value > 0; + } + if (typeof value == "string") { + value = value.toLowerCase(); + return value == "1" || value == "true" || value == "yes" || value == "on"; + } + return defaultValue; + }; + + vm.record.lineScope = vm.record.lineScope == "overseas" ? "overseas" : "china"; + vm.record.lineCarrier = vm.record.lineCarrier || "默认"; + vm.record.lineRegion = vm.record.lineRegion || "默认"; + vm.record.lineProvince = vm.record.lineProvince || "默认"; + vm.record.lineContinent = vm.record.lineContinent || "默认"; + vm.record.lineCountry = vm.record.lineCountry || "默认"; + vm.record.ruleName = vm.record.ruleName || ""; + vm.record.ttl = vm.record.ttl || 30; + vm.record.weightEnabled = vm.normalizeBoolean(vm.record.weightEnabled, false); + vm.record.isOn = vm.normalizeBoolean(vm.record.isOn, true); + + vm.sdnsParams = vm.parseJSONList(vm.record.sdnsParamsJson); + if (vm.sdnsParams.length == 0) { + vm.sdnsParams.push({name: "", value: ""}); + } + + vm.recordItems = vm.parseJSONList(vm.record.recordItemsJson); + if (vm.recordItems.length == 0) { + vm.recordItems.push({type: "A", value: "", weight: 100}); + } else { + for (var i = 0; i < vm.recordItems.length; i++) { + var item = vm.recordItems[i]; + if (item.type != "A" && item.type != "AAAA") { + item.type = "A"; + } + if (typeof item.weight == "undefined" || item.weight == null || item.weight === "") { + item.weight = 100; + } + } + } + + vm.provinceOptions = ["默认"]; + vm.countryOptions = ["默认"]; + + vm.refreshProvinceOptions = function () { + var provinces = vm.chinaRegionProvinces[vm.record.lineRegion]; + if (!Array.isArray(provinces) || provinces.length == 0) { + provinces = ["默认"]; + } + vm.provinceOptions = provinces; + if (vm.provinceOptions.indexOf(vm.record.lineProvince) < 0) { + vm.record.lineProvince = vm.provinceOptions[0]; + } + }; + + vm.refreshCountryOptions = function () { + var countries = vm.continentCountries[vm.record.lineContinent]; + if (!Array.isArray(countries) || countries.length == 0) { + countries = ["默认"]; + } + vm.countryOptions = countries; + if (vm.countryOptions.indexOf(vm.record.lineCountry) < 0) { + vm.record.lineCountry = vm.countryOptions[0]; + } + }; + + vm.onChinaRegionChange = function () { + vm.refreshProvinceOptions(); + }; + + vm.onContinentChange = function () { + vm.refreshCountryOptions(); + }; + + vm.onLineScopeChange = function () { + if (vm.record.lineScope == "overseas") { + vm.record.lineContinent = vm.record.lineContinent || "默认"; + vm.refreshCountryOptions(); + } else { + vm.record.lineCarrier = vm.record.lineCarrier || "默认"; + vm.record.lineRegion = vm.record.lineRegion || "默认"; + vm.refreshProvinceOptions(); + } + }; + + vm.addSDNSParam = function () { + if (vm.sdnsParams.length >= 10) { + return; + } + vm.sdnsParams.push({name: "", value: ""}); + }; + + vm.removeSDNSParam = function (index) { + if (index < 0 || index >= vm.sdnsParams.length) { + return; + } + vm.sdnsParams.splice(index, 1); + if (vm.sdnsParams.length == 0) { + vm.sdnsParams.push({name: "", value: ""}); + } + }; + + vm.addRecordItem = function () { + if (vm.recordItems.length >= 10) { + return; + } + vm.recordItems.push({type: "A", value: "", weight: 100}); + }; + + vm.removeRecordItem = function (index) { + if (index < 0 || index >= vm.recordItems.length) { + return; + } + vm.recordItems.splice(index, 1); + if (vm.recordItems.length == 0) { + vm.recordItems.push({type: "A", value: "", weight: 100}); + } + }; + + vm.onLineScopeChange(); +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/domains.html b/EdgeAdmin/web/views/@default/httpdns/apps/domains.html new file mode 100644 index 0000000..2f8df61 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/domains.html @@ -0,0 +1,35 @@ +{$layout} +{$template "menu"} + +
+ +
+ + 添加域名 + + + + + + + + + + + + + + + + +
域名列表规则策略操作
{{domain.name}} + {{domain.customRecordCount}} + + 自定义解析  |  + 解绑 +
+ +

该应用尚未绑定域名。

+
diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/domains.js b/EdgeAdmin/web/views/@default/httpdns/apps/domains.js new file mode 100644 index 0000000..17e29e2 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/domains.js @@ -0,0 +1,24 @@ +Tea.context(function () { + if (typeof this.domains == "undefined") { + this.domains = []; + } + + this.bindDomain = function () { + teaweb.popup("/httpdns/apps/domains/createPopup?appId=" + this.app.id, { + height: "24em", + width: "46em", + title: "添加域名" + }); + }; + + this.deleteDomain = function (domainId) { + let that = this; + teaweb.confirm("确定要解绑这个域名吗?", function () { + that.$post("/httpdns/apps/domains/delete") + .params({ + domainId: domainId + }) + .refresh(); + }); + }; +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/domainsCreatePopup.html b/EdgeAdmin/web/views/@default/httpdns/apps/domainsCreatePopup.html new file mode 100644 index 0000000..d692c54 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/domainsCreatePopup.html @@ -0,0 +1,17 @@ +{$layout "layout_popup"} + +

添加域名

+ +
+ + + + + + +
域名 * + +

请输入完整 FQDN,例如 api.example.com

+
+ +
diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/index.html b/EdgeAdmin/web/views/@default/httpdns/apps/index.html new file mode 100644 index 0000000..990530d --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/index.html @@ -0,0 +1,53 @@ +{$layout} +{$template "menu"} + +
+
+
+
+ +
+
+ + [清除条件] +
+
+
+ +

暂时没有符合条件的 HTTPDNS 应用。

+ + + + + + + + + + + + + + + + + + + + +
应用名称AppID绑定域名数状态操作
+ + {{app.name}} + + + {{app.appId}} + + {{app.domainCount}} + + + 域名管理  |  + 应用设置 +
+ +
+
diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/index.js b/EdgeAdmin/web/views/@default/httpdns/apps/index.js new file mode 100644 index 0000000..fded6df --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/index.js @@ -0,0 +1,13 @@ +Tea.context(function () { + if (typeof this.apps == "undefined") { + this.apps = []; + } + + this.createApp = function () { + teaweb.popup("/httpdns/apps/createPopup", { + height: "26em", + width: "48em", + title: "添加应用" + }); + }; +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/policies.html b/EdgeAdmin/web/views/@default/httpdns/apps/policies.html new file mode 100644 index 0000000..0471258 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/policies.html @@ -0,0 +1,78 @@ +{$layout} +{$template "menu"} + +
+ +

HTTPDNS 鍏ㄥ眬绛栫暐

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
鍏ㄥ眬榛樿瑙f瀽 TTL 绉?/td> +
鍏ㄥ眬榛樿 SNI 绛夌骇 + +
+ level1 浠呬娇鐢ㄥ浐瀹?SNI锛屼笉鍚敤 ECS 鍜岃瘉涔︽牎楠岀瓥鐣ャ€?
+
+ level2 浠呭惎鐢ㄩ殣鍖?SNI锛屼笉瑕佹眰閰嶇疆 ECS 涓庤瘉涔︾瓥鐣ャ€?
+
+ level3 鍚敤 ECH锛屽缓璁悓鏃堕厤缃?ECS 涓庤瘉涔︽牎楠岀瓥鐣ャ€?
+
鍏ㄥ眬闄嶇骇瓒呮椂 姣
全局 ECS 掩码策略 + + + IPv4 / + + IPv6 / + + + 仅在“自定义”模式下配置掩码。 + + + +
鍏ㄥ眬璇佷功鏍¢獙绛栫暐 + 证书指纹校验(Pinning) + + 证书 SAN 域名校验 + +
+ + +
+ diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/policies.js b/EdgeAdmin/web/views/@default/httpdns/apps/policies.js new file mode 100644 index 0000000..dc96d3c --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/apps/policies.js @@ -0,0 +1,40 @@ +Tea.context(function () { + this.success = NotifyReloadSuccess("保存成功"); + + this.$delay(function () { + this.$watch("policies.defaultSniPolicy", function (level) { + if (level == "level1" || level == "level2") { + this.policies.ecsMode = "off"; + this.policies.pinningMode = "off"; + this.policies.sanMode = "off"; + return; + } + + if (level == "level3") { + if (this.policies.ecsMode == "off") { + this.policies.ecsMode = "auto"; + } + if (this.policies.pinningMode == "off") { + this.policies.pinningMode = "report"; + } + if (this.policies.sanMode == "off") { + this.policies.sanMode = "strict"; + } + } + }); + + this.$watch("policies.ecsMode", function (mode) { + if (this.policies.defaultSniPolicy != "level3") { + return; + } + if (mode == "custom") { + if (!this.policies.ecsIPv4Prefix || this.policies.ecsIPv4Prefix <= 0) { + this.policies.ecsIPv4Prefix = 24; + } + if (!this.policies.ecsIPv6Prefix || this.policies.ecsIPv6Prefix <= 0) { + this.policies.ecsIPv6Prefix = 56; + } + } + }); + }); +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/@cluster_menu.html b/EdgeAdmin/web/views/@default/httpdns/clusters/@cluster_menu.html new file mode 100644 index 0000000..6a7033c --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/@cluster_menu.html @@ -0,0 +1,8 @@ + + {{cluster.name}} + » + 节点列表 + 创建节点 + 集群设置 + 删除集群 + diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/@menu.html b/EdgeAdmin/web/views/@default/httpdns/clusters/@menu.html new file mode 100644 index 0000000..f130779 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/@menu.html @@ -0,0 +1,6 @@ + + + 集群列表 + | + [创建新集群] + diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/certs.html b/EdgeAdmin/web/views/@default/httpdns/clusters/certs.html new file mode 100644 index 0000000..6955460 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/certs.html @@ -0,0 +1,30 @@ +{$layout} +{$template "menu"} +{$template "/left_menu_with_menu"} + +
+

HTTPS 接口证书

+

用于 HTTPDNS 接口证书管理与展示。

+ + + + + + + + + + + + + + + + + + + + + +
证书名称颁发机构到期时间证书ID
{{cert.name}}{{cert.issuer}}{{cert.expiresAt}}{{cert.id}}
暂无证书。
+
diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster.html b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster.html new file mode 100644 index 0000000..575135a --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster.html @@ -0,0 +1,91 @@ +{$layout} +{$template "cluster_menu"} +
+ +
+ [创建节点] +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +   + [清除条件] +
+
+
+ +
+

当前集群下暂时还没有节点。

+ + + + + + + + + + + + + + + + + + +
节点名称IP地址状态操作
+ {{node.name}} + +
+ {{node.region.name}} +
+
+ - +
+
{{addr.ip}} + ({{addr.name}}) + [内] + [下线] + [宕机] +
+
+
+
+ 宕机 +
+
+ 未启用 +
+
+ 在线 + 离线 +
+
+ 详情   + 删除 +
+
\ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster.js b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster.js new file mode 100644 index 0000000..3271457 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster.js @@ -0,0 +1,17 @@ +Tea.context(function () { + this.deleteNode = function (nodeId) { + let that = this + teaweb.confirm("确定要删除此节点吗?", function () { + that.$post("/httpdns/clusters/deleteNode") + .params({ + clusterId: that.clusterId, + nodeId: nodeId + }) + .success(function () { + teaweb.success("删除成功", function () { + teaweb.reload() + }) + }) + }) + } +}) diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/@node_menu.html b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/@node_menu.html new file mode 100644 index 0000000..33518d8 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/@node_menu.html @@ -0,0 +1,10 @@ + + {{currentCluster.name}} + » + 节点列表 + | + 节点详情 + 运行日志 + 修改设置 + 安装节点 + \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.css b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.css new file mode 100644 index 0000000..1b23bc8 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.css @@ -0,0 +1,4 @@ +a.underline { + border-bottom: 1px #db2828 dashed; +} +/*# sourceMappingURL=index.css.map */ \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.css.map b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.css.map new file mode 100644 index 0000000..31fa663 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,CAAC;EACA,iCAAA","file":"index.css"} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.html b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.html new file mode 100644 index 0000000..5e21924 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.html @@ -0,0 +1,191 @@ +{$layout} + +{$template "node_menu"} + +

节点详情

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
节点名称{{node.name}}
状态
IP地址 +
+
+
+ {{address.ip}} + ({{address.name}},不公开访问  + [off] + [down] + (不公开访问) +
+
+
+
+ 暂时还没有填写 IP 地址。 +
+
SSH主机地址 +
+ {{node.login.params.host}} + 尚未设置 +
+
尚未设置
+
SSH主机端口 +
+ {{node.login.params.port}} + 尚未设置 +
+ 尚未设置 +
SSH登录认证 +
+ + {{node.login.grant.name}} + ({{node.login.grant.methodName}}) + ({{node.login.grant.username}}) + +
+ 尚未设置 +
API节点地址 +
+ {{addr}} +
+ 使用全局设置 +
+
+ +

运行状态

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
运行状态 +
+ 运行中  + [通过SSH停止] + [停止中...] +
+
+ 已断开  + [通过SSH启动] + [启动中...] + 去安装 > +
+
CPU用量 + {{node.status.cpuUsageText}} +   + ({{node.status.cpuPhysicalCount}}核心/{{node.status.cpuLogicalCount}}线程) +
内存用量{{node.status.memUsageText}}
负载 + {{node.status.load1m}}   {{node.status.load5m}}   {{node.status.load15m}} +   + +
版本 + v{{node.status.buildVersion}} +   + 发现新版本 v{{newVersion}} » +
主程序位置{{node.status.exePath}}
最近API连接状况 + + 连接错误异常严重({{round(100 - node.status.apiSuccessPercent)}}%失败),请改善当前节点和API节点之间通讯 + 连接错误较多({{round(100 - node.status.apiSuccessPercent)}}%失败),请改善当前节点和API节点之间通讯 + 有连接错误发生({{round(100 - node.status.apiSuccessPercent)}}%失败),请改善当前节点和API节点之间通讯 + + 连接良好 + 连接基本稳定(平均{{round(node.status.apiAvgCostSeconds)}}秒) + 连接速度较慢(平均{{round(node.status.apiAvgCostSeconds)}}秒) + 连接非常慢(平均{{round(node.status.apiAvgCostSeconds)}}秒),请改善当前节点和API节点之间通讯 + + + 尚未上报数据 +
上次更新时间 + {{nodeDatetime}} +

当前节点时间与API节点时间相差 {{nodeTimeDiff}} 秒,请同步节点时间。

+
+

每隔30秒钟更新一次运行状态。

+ +
+

安装信息

+ + + + + + + + + + + + + + + + + +
节点ID (id){{node.uniqueId}}
密钥 (secret){{node.secret}}
安装目录 +
使用集群设置({{node.cluster.installDir}})
+ {{node.installDir}} +
已安装 + 已安装 + 未安装 +
+ diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.js b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.js new file mode 100644 index 0000000..dabc459 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.js @@ -0,0 +1,39 @@ +Tea.context(function () { + this.isStarting = false + this.startNode = function () { + this.isStarting = true + this.$post('/httpdns/clusters/cluster/node/start') + .params({ + nodeId: this.node.id + }) + .success(function () { + teaweb.success('启动成功', function () { + teaweb.reload() + }) + }) + .done(function () { + this.isStarting = false + }) + } + + this.isStopping = false + this.stopNode = function () { + this.isStopping = true + this.$post('/httpdns/clusters/cluster/node/stop') + .params({ + nodeId: this.node.id + }) + .success(function () { + teaweb.success('执行成功', function () { + teaweb.reload() + }) + }) + .done(function () { + this.isStopping = false + }) + } + + this.round = function (f) { + return Math.round(f * 100) / 100 + } +}) diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.less b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.less new file mode 100644 index 0000000..48f0e93 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/index.less @@ -0,0 +1,3 @@ +a.underline { + border-bottom: 1px #db2828 dashed; +} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.css b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.css new file mode 100644 index 0000000..9e8d1e9 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.css @@ -0,0 +1,7 @@ +.installing-box { + line-height: 1.8; +} +.installing-box .blue { + color: #2185d0; +} +/*# sourceMappingURL=install.css.map */ \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.css.map b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.css.map new file mode 100644 index 0000000..5e4d089 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["install.less"],"names":[],"mappings":"AAAA;EACC,gBAAA;;AADD,eAGC;EACC,cAAA","file":"install.css"} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.html b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.html new file mode 100644 index 0000000..aef9cdc --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.html @@ -0,0 +1,100 @@ +{$layout} +{$template "node_menu"} +{$template "/code_editor"} + + +
+
当前节点为已安装状态。
+ [重新安装] + +

配置文件

+ + + + + + + + + + + + + +
配置文件 + configs/api_httpdns.yaml   + [下载] +
配置内容 + rpc.endpoints: [ {{apiEndpoints}} ] + nodeId: "{{node.uniqueId}}" + secret: "{{node.secret}}" +

每个节点的配置文件内容均不相同,不能混用。

+
安装目录 +
使用集群设置({{node.cluster.installDir}}) +
+ {{node.installDir}} +
+
+ + +
+

方法1:通过SSH自动安装

+ + + + + + +
SSH地址 + {{sshAddr}}   [修改] + 尚未设置   [设置] +
+ +
+
安装中...
+
+ 已安装成功 + 安装过程中发生错误:{{installStatus.error}} +
+
+
+ +
+
+ +
+ +

方法2:手动安装

+ + + + + + + + + + + + + +
配置文件 + configs/api_httpdns.yaml   + [下载] +
配置内容 + rpc.endpoints: [ {{apiEndpoints}} ] + nodeId: "{{node.uniqueId}}" + secret: "{{node.secret}}" +
安装目录 +
使用集群设置({{node.cluster.installDir}}) +
+ {{node.installDir}} +
+ + [修改为已安装状态] +
\ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.js b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.js new file mode 100644 index 0000000..f1dabee --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.js @@ -0,0 +1,115 @@ +Tea.context(function () { + let isInstalling = false + + this.$delay(function () { + this.reloadStatus(this.nodeId) + }) + + this.install = function () { + isInstalling = true + + this.$post("$") + .params({ + nodeId: this.nodeId + }) + .success(function () {}) + } + + this.updateNodeIsInstalled = function (isInstalled) { + let msg = isInstalled + ? "html:确认要将当前节点修改为 已安装 状态?" + : "html:确认要将当前节点修改为 未安装 状态?" + teaweb.confirm(msg, function () { + this.$post("/httpdns/clusters/cluster/node/updateInstallStatus") + .params({ + nodeId: this.nodeId, + isInstalled: isInstalled ? 1 : 0 + }) + .refresh() + }) + } + + this.reloadStatus = function (nodeId) { + let that = this + + this.$post("/httpdns/clusters/cluster/node/status") + .params({ + nodeId: nodeId + }) + .success(function (resp) { + this.installStatus = resp.data.installStatus + this.node.isInstalled = resp.data.isInstalled + + if (!isInstalling) { + 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 + "
现在去修改 API 信息?", function () { + window.location = "/settings/api" + }) + return + default: + break + } + }) + .done(function () { + this.$delay(function () { + this.reloadStatus(nodeId) + }, 1000) + }) + } + + this.showSSHPopup = function (nodeId) { + teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + nodeId, { + height: "30em", + callback: function () { + teaweb.success("保存成功", function () { + teaweb.reload() + }) + } + }) + } +}) diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.less b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.less new file mode 100644 index 0000000..cbe1416 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/install.less @@ -0,0 +1,7 @@ +.installing-box { + line-height: 1.8; + + .blue { + color: #2185d0; + } +} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.css b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.css new file mode 100644 index 0000000..dafa156 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.css @@ -0,0 +1,5 @@ +pre.log-box { + margin: 0; + padding: 0; +} +/*# sourceMappingURL=logs.css.map */ \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.css.map b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.css.map new file mode 100644 index 0000000..6ae867b --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["logs.less"],"names":[],"mappings":"AAAA,GAAG;EACF,SAAA;EACA,UAAA","file":"logs.css"} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.html b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.html new file mode 100644 index 0000000..7b45f2c --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.html @@ -0,0 +1,51 @@ +{$layout} +{$template "node_menu"} +{$template "/datepicker"} + +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +

暂时还没有日志。

+ + + + + + + + + + +
+ +
+ +
\ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.js b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.js new file mode 100644 index 0000000..29a9042 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.js @@ -0,0 +1,6 @@ +Tea.context(function () { + this.$delay(function () { + teaweb.datepicker("day-from-picker") + teaweb.datepicker("day-to-picker") + }) +}) \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.less b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.less new file mode 100644 index 0000000..9accd63 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/logs.less @@ -0,0 +1,4 @@ +pre.log-box { + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/update.html b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/update.html new file mode 100644 index 0000000..69094f8 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/update.html @@ -0,0 +1,78 @@ +{$layout} + +{$template "node_menu"} + +

修改节点

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
节点名称 * + +
IP地址 * + +

用于访问节点和处理HTTPDNS解析请求等。

+
所属集群 + +
SSH主机地址 + +

比如192.168.1.100

+
SSH主机端口 + +

比如22。

+
SSH登录认证 + +
API节点地址 +
+ +
+

当前节点单独使用的API节点设置。

+
启用当前节点 +
+ + +
+

如果不启用此节点,此节点上的所有网站将不能访问。

+
+ +
diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/update.js b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/update.js new file mode 100644 index 0000000..a961105 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/cluster/node/update.js @@ -0,0 +1,35 @@ +Tea.context(function () { + this.clusterId = 0; + if (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.grant = null + + this.sshHost = "" + this.sshPort = "" + this.loginId = 0 + if (this.node.login != null) { + this.loginId = this.node.login.id + + 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 + } + } + } +}) \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/clusterSettings.html b/EdgeAdmin/web/views/@default/httpdns/clusters/clusterSettings.html new file mode 100644 index 0000000..a37aa02 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/clusterSettings.html @@ -0,0 +1,71 @@ +{$layout} +{$template "cluster_menu"} +
+ +

集群设置

+

{{cluster.name}}

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
集群名称 * + +

用于区分不同环境的解析节点池。

+
集群服务域名 * + +

该集群下应用用于 SDK 接入的 HTTPDNS 服务域名。

+
降级超时容忍度 +
+ + 毫秒 +
+

HTTPDNS 网关请求源站超时多长时间后,强制降级返回缓存兜底IP(保障 P99 响应延迟)。

+
本地内存缓存 +
+ + +
+

在网关节点内存中缓存解析结果的时长,缓解峰值查询压力。

+
节点安装根目录 + +

此集群下新加网关节点的主程序默认部署路径。

+
启用当前集群 +
+ + +
+

如果取消启用,此集群下的所有 HTTPDNS 网关节点将停止处理解析请求。

+
+ +
diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/clusterSettings.js b/EdgeAdmin/web/views/@default/httpdns/clusters/clusterSettings.js new file mode 100644 index 0000000..5cfac1e --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/clusterSettings.js @@ -0,0 +1,3 @@ +Tea.context(function () { + this.success = NotifyReloadSuccess("保存成功"); +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/create.html b/EdgeAdmin/web/views/@default/httpdns/clusters/create.html new file mode 100644 index 0000000..d112bbe --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/create.html @@ -0,0 +1,23 @@ +{$layout} +{$template "menu"} +
+ +
目前这是一个纯前端占位页面(Mock),后续将对接真实的后端 API。
+ +
+ + + + + + + + + +
集群名称 * + +
集群服务域名 * + +
+ +
diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css new file mode 100644 index 0000000..e2dadd6 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css @@ -0,0 +1,7 @@ +.left-box { + top: 10em; +} +.right-box { + top: 10em; +} +/*# sourceMappingURL=createNode.css.map */ \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css.map b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css.map new file mode 100644 index 0000000..fd804f1 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["createNode.less"],"names":[],"mappings":"AAAA;EACC,SAAA;;AAGD;EACC,SAAA","file":"createNode.css"} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.html b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.html new file mode 100644 index 0000000..a5be9b9 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.html @@ -0,0 +1,22 @@ +{$layout} +{$template "cluster_menu"} + +
+ + + + + + + + + + +
节点名称 * + +
IP地址 * + +

用于访问节点和处理HTTPDNS解析请求等。

+
+ +
\ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.js b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.js new file mode 100644 index 0000000..0b4baf7 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.js @@ -0,0 +1,3 @@ +Tea.context(function () { + this.success = NotifySuccess("保存成功", "/httpdns/clusters/cluster?clusterId=" + this.clusterId); +}); \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.less b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.less new file mode 100644 index 0000000..f54837f --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/createNode.less @@ -0,0 +1,7 @@ +.left-box { + top: 10em; +} + +.right-box { + top: 10em; +} \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/delete.html b/EdgeAdmin/web/views/@default/httpdns/clusters/delete.html new file mode 100644 index 0000000..fa5dd75 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/delete.html @@ -0,0 +1,7 @@ +{$layout} +{$template "cluster_menu"} +
+ +
+ +
\ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/delete.js b/EdgeAdmin/web/views/@default/httpdns/clusters/delete.js new file mode 100644 index 0000000..d8d3cfb --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/delete.js @@ -0,0 +1,16 @@ +Tea.context(function () { + this.deleteCluster = function (clusterId) { + let that = this + teaweb.confirm("确定要删除此集群吗?", function () { + that.$post("/httpdns/clusters/delete") + .params({ + clusterId: clusterId + }) + .success(function () { + teaweb.success("删除成功", function () { + window.location = "/httpdns/clusters" + }) + }) + }) + } +}) diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/index.html b/EdgeAdmin/web/views/@default/httpdns/clusters/index.html new file mode 100644 index 0000000..bbf124c --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/index.html @@ -0,0 +1,68 @@ +{$layout} +{$template "menu"} + +
+
+ +
有部分集群节点状态异常,请及时处理。
+
+ +
+
+
+ +
+
+ + [清除条件] +
+
+
+ +

暂时还没有 HTTPDNS 集群,现在去 [创建新集群]

+ + + + + + + + + + + + + + + + + + + + + + +
集群名称服务域名节点数在线节点数状态操作
+ + {{cluster.name}} + + + {{cluster.gatewayDomain}} + + + {{cluster.countAllNodes}} + + - + + + {{cluster.countActiveNodes}} + + - + + 详情 +
+ +
+
diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/index.js b/EdgeAdmin/web/views/@default/httpdns/clusters/index.js new file mode 100644 index 0000000..a49983b --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/index.js @@ -0,0 +1,17 @@ +Tea.context(function () { + this.hasErrorLogs = false; // Mock data + if (typeof this.clusters == "undefined") { + this.clusters = []; + } + + this.deleteCluster = function (clusterId) { + let that = this; + teaweb.confirm("确定要删除这个集群吗?", function () { + that.$post(".delete") + .params({ + clusterId: clusterId + }) + .refresh(); + }); + }; +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/updateNodeSSH.html b/EdgeAdmin/web/views/@default/httpdns/clusters/updateNodeSSH.html new file mode 100644 index 0000000..6a69d21 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/updateNodeSSH.html @@ -0,0 +1,31 @@ +{$layout "layout_popup"} + +

淇敼鑺傜偣"{{node.name}}"鐨凷SH鐧诲綍淇℃伅

+ +
+ + + + + + + + + + + + + + + +
SSH涓绘満鍦板潃 * + +

姣斿192.168.1.100

+
SSH涓绘満绔彛 * + +

姣斿22銆?/p> +

SSH鐧诲綍璁よ瘉 * + +
+ +
diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/updateNodeSSH.js b/EdgeAdmin/web/views/@default/httpdns/clusters/updateNodeSSH.js new file mode 100644 index 0000000..1d5e8e7 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/updateNodeSSH.js @@ -0,0 +1,8 @@ +Tea.context(function () { + if (typeof this.params == "undefined" || this.params == null) { + this.params = {host: "", port: 22} + } + if (this.params.port <= 0) { + this.params.port = 22 + } +}) diff --git a/EdgeAdmin/web/views/@default/httpdns/clusters/upgradeRemote.html b/EdgeAdmin/web/views/@default/httpdns/clusters/upgradeRemote.html new file mode 100644 index 0000000..3416c5b --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/clusters/upgradeRemote.html @@ -0,0 +1,3 @@ +{$layout} + +

姝ゅ姛鑳芥殏鏈紑鏀俱€?/p> \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/ech/@menu.html b/EdgeAdmin/web/views/@default/httpdns/ech/@menu.html new file mode 100644 index 0000000..b4f2af8 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/ech/@menu.html @@ -0,0 +1,4 @@ + + + ECH 控制台 + \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/ech/audit.html b/EdgeAdmin/web/views/@default/httpdns/ech/audit.html new file mode 100644 index 0000000..734b7c2 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/ech/audit.html @@ -0,0 +1,26 @@ +{$layout} +{$template "menu"} +{$template "/left_menu_with_menu"} + +

+

ECH Degrade Audit Logs

+ + + + + + + + + + + + + + + + + + +
ScopeOperatorResultTime
{{item.scope}}{{item.operator}}{{item.result}}{{item.createdAt}}
+
diff --git a/EdgeAdmin/web/views/@default/httpdns/ech/index.html b/EdgeAdmin/web/views/@default/httpdns/ech/index.html new file mode 100644 index 0000000..2948eeb --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/ech/index.html @@ -0,0 +1,69 @@ +{$layout} +{$template "menu"} + +
+
+
+
+

全网私钥热加载成功率

+
{{ (health.keySyncRate * + 100).toFixed(2) }}%
+
+
+
+
+

全网 ECH 解密失败阻断率

+
{{ (health.decryptFailRate * + 100).toFixed(4) }}%
+
+
+
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
发行版本号记录分发类型公钥发布时间边缘扩散状态受控回落通道
{{log.version}}当前生效{{log.recordType}} +
+ {{log.publicKey.substring(0,30)}}...{{log.publicKey.substring(log.publicKey.length-10)}} +
+
{{log.publishTime}} + 100% 同步 + 正在扩散到 {{log.nodesPending}} + 节点 + 部分节点超时 + + 回滚降级 + (MFA验证) + - +
+
\ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/ech/index.js b/EdgeAdmin/web/views/@default/httpdns/ech/index.js new file mode 100644 index 0000000..05724a9 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/ech/index.js @@ -0,0 +1,20 @@ +Tea.context(function () { + if (typeof this.health == "undefined") { + this.health = { + keySyncRate: 1.0, + decryptFailRate: 0.0 + }; + } + + if (typeof this.echLogs == "undefined") { + this.echLogs = []; + } + + this.openMfaRollback = function (logId) { + teaweb.popup("/httpdns/ech/rollbackMfaPopup?logId=" + logId, { + height: "26em", + width: "48em", + title: "全域安全受控降级告警:双人MFA授权" + }) + }; +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/ech/rollbackMfaPopup.html b/EdgeAdmin/web/views/@default/httpdns/ech/rollbackMfaPopup.html new file mode 100644 index 0000000..5228861 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/ech/rollbackMfaPopup.html @@ -0,0 +1,27 @@ +{$layout "layout_popup"} + +

受控 ECH 回滚降级

+ +
+ + + + + + + + + + + + + + + + + + + +
日志编号{{logId}}
回滚原因 *
审批人 OTP 验证码 #1 *
审批人 OTP 验证码 #2 *
+ +
\ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/guide/@menu.html b/EdgeAdmin/web/views/@default/httpdns/guide/@menu.html new file mode 100644 index 0000000..2d8439a --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/guide/@menu.html @@ -0,0 +1,4 @@ + + + SDK接入向导 + diff --git a/EdgeAdmin/web/views/@default/httpdns/guide/index.html b/EdgeAdmin/web/views/@default/httpdns/guide/index.html new file mode 100644 index 0000000..b873a19 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/guide/index.html @@ -0,0 +1,155 @@ +{$layout} +{$template "menu"} + + + +
+
+
+
+ +
+
+ +
+
+
+ +
+ +

请先选择应用,然后查看配置并完成 SDK 接入。

+
+ +
+ + +
+

查看配置

+ + + + + + + + + + + + + + + + + + + + + + + + + +
App ID + {{selectedApp.appId}} + +
应用名称{{selectedApp.name}}
集群服务地址 + {{selectedApp.gatewayDomain}} + +
加签 Secret + {{signSecretVisible ? selectedApp.signSecret : selectedApp.signSecretMasked}} + + +
AES 数据加密 Secret + {{aesSecretVisible ? selectedApp.aesSecret : selectedApp.aesSecretMasked}} + + +
请求验签 + 已开启 + 已关闭 +
+ + 下一步 + +
+ +
+

开发接入

+

选择对应平台 SDK 下载并查阅集成文档。

+ +
+
+
+
Android SDK
+
+ 适用于 Android 5.0+ 的原生 SDK,支持 Java / Kotlin。 +
+
+ +
+
+
+
iOS SDK
+
+ 适用于 iOS 12+ 的原生 SDK,支持 Swift / Objective-C。 +
+
+ +
+
+
+
Flutter SDK
+
+ 跨平台 Flutter 插件,同时支持 Android 和 iOS。 +
+
+ +
+
+
+
+
diff --git a/EdgeAdmin/web/views/@default/httpdns/guide/index.js b/EdgeAdmin/web/views/@default/httpdns/guide/index.js new file mode 100644 index 0000000..c30c04a --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/guide/index.js @@ -0,0 +1,75 @@ +Tea.context(function () { + this.selectedAppId = "" + this.selectedApp = {} + this.currentStep = 1 + this.signSecretVisible = false + this.aesSecretVisible = false + + if (typeof this.apps == "undefined") { + this.apps = [] + } + + this.onAppChange = function () { + if (this.selectedAppId.length == 0) { + this.selectedApp = {} + return + } + + for (var i = 0; i < this.apps.length; i++) { + if (this.apps[i].appId == this.selectedAppId) { + this.selectedApp = this.apps[i] + break + } + } + if (!this.selectedApp.gatewayDomain || this.selectedApp.gatewayDomain.length == 0) { + this.selectedApp.gatewayDomain = "gw.httpdns.example.com" + } + + this.signSecretVisible = false + this.aesSecretVisible = false + this.currentStep = 1 + } + + this.copyText = function (text, name) { + if (typeof text != "string" || text.length == 0) { + teaweb.warn("没有可复制的内容") + return + } + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { + teaweb.success(name + "已复制") + }).catch(function () { + this.copyByTextarea(text, name) + }.bind(this)) + return + } + + this.copyByTextarea(text, name) + } + + this.copyByTextarea = function (text, name) { + var input = document.createElement("textarea") + input.value = text + input.setAttribute("readonly", "readonly") + input.style.position = "fixed" + input.style.left = "-10000px" + input.style.top = "-10000px" + document.body.appendChild(input) + input.select() + + var ok = false + try { + ok = document.execCommand("copy") + } catch (e) { + ok = false + } + document.body.removeChild(input) + + if (ok) { + teaweb.success(name + "已复制") + } else { + teaweb.warn("复制失败,请手动复制") + } + } +}) diff --git a/EdgeAdmin/web/views/@default/httpdns/policies/@menu.html b/EdgeAdmin/web/views/@default/httpdns/policies/@menu.html new file mode 100644 index 0000000..36b7d32 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/policies/@menu.html @@ -0,0 +1,3 @@ + + 全局策略 + diff --git a/EdgeAdmin/web/views/@default/httpdns/policies/index.html b/EdgeAdmin/web/views/@default/httpdns/policies/index.html new file mode 100644 index 0000000..ae44487 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/policies/index.html @@ -0,0 +1,121 @@ +{$layout} +{$template "menu"} + + + +
+ +
+ + +
+ + +
+ + + + + + + + + +
默认部署集群 + +

用户新建应用时默认落到此集群。

+
启用用户域名校验 + +

开启后,用户添加域名需要通过归属校验。

+
+ + + + + + + + + + + + + + +
SNI 防护配置 + 隐匿 SNI +

当前统一采用隐匿 SNI 策略,避免在握手阶段暴露业务域名。

+
全局默认解析 TTL +
+
+ +
+
+
+

建议 30~120 秒,兼顾缓存命中与切换速度。

+
全局降级超时 +
+
+ +
毫秒
+
+
+

超时后可走降级解析流程,建议 200~800ms。

+
+ + +
+
+
diff --git a/EdgeAdmin/web/views/@default/httpdns/policies/index.js b/EdgeAdmin/web/views/@default/httpdns/policies/index.js new file mode 100644 index 0000000..f33c7d9 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/policies/index.js @@ -0,0 +1,22 @@ +Tea.context(function () { + this.success = NotifyReloadSuccess("保存成功"); + this.activeSection = "user"; + + if (!Array.isArray(this.availableClusters)) { + this.availableClusters = []; + } + if (!this.policies.defaultClusterId || this.policies.defaultClusterId <= 0) { + if (this.availableClusters.length > 0) { + this.policies.defaultClusterId = this.availableClusters[0].id; + } + } + if (typeof this.policies.enableUserDomainVerify == "undefined") { + this.policies.enableUserDomainVerify = true; + } + if (typeof this.policies.defaultTTL == "undefined" || this.policies.defaultTTL <= 0) { + this.policies.defaultTTL = 30; + } + if (typeof this.policies.defaultFallbackMs == "undefined" || this.policies.defaultFallbackMs <= 0) { + this.policies.defaultFallbackMs = 300; + } +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/resolveLogs/@menu.html b/EdgeAdmin/web/views/@default/httpdns/resolveLogs/@menu.html new file mode 100644 index 0000000..33d002e --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/resolveLogs/@menu.html @@ -0,0 +1,3 @@ + + 解析日志 + diff --git a/EdgeAdmin/web/views/@default/httpdns/resolveLogs/index.html b/EdgeAdmin/web/views/@default/httpdns/resolveLogs/index.html new file mode 100644 index 0000000..d02b83d --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/resolveLogs/index.html @@ -0,0 +1,94 @@ +{$layout} +{$template "menu"} + + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +

暂时还没有解析日志。

+ +
+ + + + + + + + + + + + + + + + + + + +
集群节点域名类型概要
{{log.clusterName}}{{log.nodeName}}{{log.domain}}{{log.query}} +
+ {{log.time}} + | {{log.appName}}({{log.appId}}) + | {{log.clientIp}} + | {{log.os}}/{{log.sdkVersion}} + | {{log.query}} {{log.domain}} -> + {{log.ips}} + [无记录] + | + 成功 + 失败 + ({{log.errorCode}}) + | {{log.costMs}}ms +
+
+
diff --git a/EdgeAdmin/web/views/@default/httpdns/runtimeLogs/@menu.html b/EdgeAdmin/web/views/@default/httpdns/runtimeLogs/@menu.html new file mode 100644 index 0000000..df3b9e1 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/runtimeLogs/@menu.html @@ -0,0 +1,3 @@ + + 运行日志 + diff --git a/EdgeAdmin/web/views/@default/httpdns/runtimeLogs/index.html b/EdgeAdmin/web/views/@default/httpdns/runtimeLogs/index.html new file mode 100644 index 0000000..cd48983 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/runtimeLogs/index.html @@ -0,0 +1,80 @@ +{$layout} +{$template "menu"} +{$template "/datepicker"} + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +

暂时还没有运行日志。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
时间集群节点级别类型模块详情次数请求ID
{{log.createdTime}}{{log.clusterName}}{{log.nodeName}} + error + warning + info + success + {{log.tag}}{{log.module}}{{log.description}}{{log.count}}{{log.requestId}}
diff --git a/EdgeAdmin/web/views/@default/httpdns/runtimeLogs/index.js b/EdgeAdmin/web/views/@default/httpdns/runtimeLogs/index.js new file mode 100644 index 0000000..32f1b41 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/runtimeLogs/index.js @@ -0,0 +1,6 @@ +Tea.context(function () { + this.$delay(function () { + teaweb.datepicker("day-from-picker"); + teaweb.datepicker("day-to-picker"); + }); +}); diff --git a/EdgeAdmin/web/views/@default/httpdns/sandbox/@menu.html b/EdgeAdmin/web/views/@default/httpdns/sandbox/@menu.html new file mode 100644 index 0000000..f1e118d --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/sandbox/@menu.html @@ -0,0 +1,4 @@ + + + API 在线沙盒 + \ No newline at end of file diff --git a/EdgeAdmin/web/views/@default/httpdns/sandbox/index.html b/EdgeAdmin/web/views/@default/httpdns/sandbox/index.html new file mode 100644 index 0000000..b68647c --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/sandbox/index.html @@ -0,0 +1,171 @@ +{$layout} +{$template "menu"} + + +
+
+ +
+
+

解析配置

+
+
+ + +
+
+ + +
+
+ + +

用于测试 ECS 掩码与区域调度效果。

+
+
+ +
+ + +
+
+
+ +
+
+ +
+
+ +
+ +
+
+ + 添加参数 + + {{sdnsParams.length}}/10 +
+
+ +
+ + + +
+
+
+ + +
+ +
+

请在左侧配置参数后点击「在线解析」。

+
+ + +
+
+
解析中...
+
+
+ + +
+ +
+
+ 解析成功 + 解析失败 ({{response.code}}) +
+

{{response.message}}

+

Request ID: {{response.requestId}}

+
+ + +
+
解析结果
+
+
请求URL
+
+ {{response.data.request_url || '-'}} +
+
+ +
解析结果详情
+
+
+
+
客户端 IP
+
{{response.data.client_ip || request.clientIp || '-'}}
+
+
+
+
+
地区
+
{{response.data.client_region || '-'}}
+
+
+
+
+
线路
+
{{response.data.line_name || '-'}}
+
+
+
+ + +
解析记录
+ + + + + + + + + + + + + + + + + + + + + +
解析域名解析类型IP地址TTL地区线路
{{row.domain || request.domain}}{{row.type || request.qtype}}{{row.ip}}{{row.ttl}}s{{row.region || '-'}}{{row.line || '-'}}
+
+
+
+
+
diff --git a/EdgeAdmin/web/views/@default/httpdns/sandbox/index.js b/EdgeAdmin/web/views/@default/httpdns/sandbox/index.js new file mode 100644 index 0000000..b22b3c0 --- /dev/null +++ b/EdgeAdmin/web/views/@default/httpdns/sandbox/index.js @@ -0,0 +1,135 @@ +Tea.context(function () { + this.newRequest = function () { + return { + appId: "", + domain: "", + clientIp: "", + qtype: "A" + } + } + + this.request = this.newRequest() + this.sdnsParams = [{name: "", value: ""}] + + this.response = { + hasResult: false, + code: -1, + message: "", + data: null, + requestId: "", + resultRows: [] + } + + this.isRequesting = false + + if (typeof this.apps == "undefined") { + this.apps = [] + } + + this.addSDNSParam = function () { + if (this.sdnsParams.length >= 10) { + return + } + this.sdnsParams.push({name: "", value: ""}) + } + + this.removeSDNSParam = function (index) { + if (index < 0 || index >= this.sdnsParams.length) { + return + } + this.sdnsParams.splice(index, 1) + if (this.sdnsParams.length == 0) { + this.sdnsParams.push({name: "", value: ""}) + } + } + + this.cleanSDNSParams = function () { + let list = [] + this.sdnsParams.forEach(function (item) { + let name = (item.name || "").trim() + let value = (item.value || "").trim() + if (name.length == 0 && value.length == 0) { + return + } + list.push({ + name: name, + value: value + }) + }) + return list + } + + this.normalizeResultRows = function (data) { + if (typeof data == "undefined" || data == null) { + return [] + } + + if (Array.isArray(data.records) && data.records.length > 0) { + return data.records + } + + let rows = [] + let ips = [] + if (Array.isArray(data.ips)) { + ips = data.ips + } + let domain = this.request.domain + let qtype = this.request.qtype + let ttl = data.ttl || 0 + let region = data.client_region || "-" + let line = data.line_name || "-" + + ips.forEach(function (ip) { + rows.push({ + domain: domain, + type: qtype, + ip: ip, + ttl: ttl, + region: region, + line: line + }) + }) + return rows + } + + this.sendTestRequest = function () { + if (this.request.appId.length == 0) { + teaweb.warn("请选择目标应用") + return + } + if (this.request.domain.length == 0) { + teaweb.warn("请填写解析域名") + return + } + + this.isRequesting = true + this.response.hasResult = false + + let payload = Object.assign({}, this.request) + payload.sdnsParamsJSON = JSON.stringify(this.cleanSDNSParams()) + + this.$post("/httpdns/sandbox/test") + .params(payload) + .success(function (resp) { + this.response = resp.data.result + this.response.hasResult = true + this.response.resultRows = this.normalizeResultRows(this.response.data) + }) + .done(function () { + this.isRequesting = false + }) + } + + this.resetForm = function () { + this.request = this.newRequest() + this.sdnsParams = [{name: "", value: ""}] + this.response = { + hasResult: false, + code: -1, + message: "", + data: null, + requestId: "", + resultRows: [] + } + } +}) diff --git a/EdgeHttpDNS/HTTPDNS主计划-V1.2.md b/EdgeHttpDNS/HTTPDNS主计划-V1.2.md new file mode 100644 index 0000000..9965294 --- /dev/null +++ b/EdgeHttpDNS/HTTPDNS主计划-V1.2.md @@ -0,0 +1,148 @@ +# HTTPDNS 主计划(V1.2 现行版) + +## 1. 目标 +1. 形成 HTTPDNS 管理端可用闭环:集群、全局配置、应用、域名、自定义解析、日志、解析测试。 +2. 保持独立 HTTPDNS 模块设计,不挂载到传统 DNS(53)职责中。 +3. 文档仅保留当前已采用的设计与页面,不包含未落地方案。 + +## 2. 当前范围(仅管理平台) +1. 菜单顺序固定为: + - 集群管理 + - 全局配置 + - 应用管理 + - SDK接入引导 + - 解析日志 + - 运行日志 + - 解析测试 +2. 现阶段以管理端页面与 Mock 数据联调为主。 + +## 3. 核心设计约束 +1. 应用侧 SNI 防护仅采用“隐匿 SNI”固定策略(不再提供 level1/level3 选择)。 +2. 服务入口按“集群服务域名”管理,不使用全局单一网关地址。 +3. 应用认证采用: + - 请求验签开关 + - 加签 Secret + - AES 数据加密 Secret +4. 自定义解析规则支持 SDNS 参数与解析记录多条配置(各最多 10 条)。 + +## 4. 页面设计(现状) + +### 4.1 集群管理 +1. 集群列表字段: + - 集群名称 + - 服务域名 + - 节点数 + - 在线节点数 + - 状态 + - 操作 +2. 集群设置字段: + - 集群名称 + - 集群服务域名 + - 降级超时容忍度(毫秒) + - 本地内存缓存(秒) + - 节点安装根目录(默认 `/opt/edge-httpdns`) + - 启用当前集群 +3. 节点页提供基础筛选、状态展示、详情与删除。 + +### 4.2 全局配置 +1. 用户设置: + - 默认部署集群 + - 启用用户域名校验 +2. 基础默认: + - SNI 防护配置(文案展示为“隐匿 SNI”) + - 全局默认解析 TTL + - 全局降级超时 + +### 4.3 应用管理 +1. 应用列表字段: + - 应用名称 + - AppID + - 绑定域名数(可点击) + - 状态 + - 操作(域名管理 / 应用设置) +2. 添加应用: + - 应用名称 + - 所属集群 + - 所属用户(可选) +3. 应用设置分两块: + - 基础配置:AppID、应用启用、SNI 防护配置(隐匿 SNI) + - 认证与密钥:请求验签开关、加签 Secret、AES Secret(查看/复制/重置/更新时间) + +### 4.4 域名管理与自定义解析 +1. 域名管理: + - 域名列表 + - 规则策略(显示该域名自定义解析规则数) + - 操作(自定义解析、解绑) +2. 自定义解析列表字段: + - 线路 + - 规则名称 + - SDNS 参数 + - 解析记录 + - TTL + - 状态 + - 操作(编辑/启停/删除) +3. 新增/编辑规则弹窗: + - 规则名称 + - 线路联动: + - 中国大陆:运营商 -> 大区 -> 省份 + - 港澳台及境外:洲 -> 国家/地区(亚洲含中国香港/中国澳门/中国台湾) + - SDNS 参数配置:最多 10 条 + - 解析记录值:最多 10 条,支持 A/AAAA,可开启权重(1-100) + - TTL(1-86400) + - 规则状态 + +## 5. SDK接入引导(现状) +1. 仅保留两步: + - 01 查看配置 + - 02 开发接入 +2. 查看配置展示: + - App ID(可复制) + - 应用名称 + - 集群服务地址(可复制) + - 加签 Secret(查看/复制) + - AES 数据加密 Secret(查看/复制) + - 请求验签状态 + +## 6. 日志页面(现状) + +### 6.1 解析日志 +1. 主列: + - 集群 + - 节点 + - 域名 + - 类型 + - 概要 +2. 概要按单行拼接展示,按“访问信息 -> 解析结果”顺序输出,不使用字段名堆叠。 + +### 6.2 运行日志 +1. 字段: + - 时间 + - 集群 + - 节点 + - 级别 + - 类型 + - 模块 + - 详情 + - 次数 + - 请求ID + +## 7. 解析测试(现状) +1. 左侧配置项: + - 目标应用 + - 解析域名 + - 模拟客户端 IP + - 解析类型(A/AAAA) + - SDNS 参数(最多 10 条) +2. 右侧结果区: + - 解析成功/失败 + - Request ID + - 请求URL + - 客户端 IP / 地区 / 线路 + - 解析记录表(解析域名、解析类型、IP地址、TTL、地区、线路) +3. 已去掉无关展示块(如安全策略、验签响应头详情块)。 + +## 8. 本文不包含的内容 +1. 不包含 ECH 控制台与 ECH 分阶段交付设计。 +2. 不包含 SNI level1/level3、多级切换与 Public SNI 域名池页面设计。 +3. 不包含第三方 DNS 凭证复用与相关编排描述。 +4. 不包含用户平台(User)页面方案。 diff --git a/EdgeHttpDNS/HTTPDNS主计划.md b/EdgeHttpDNS/HTTPDNS主计划.md deleted file mode 100644 index 58fc47d..0000000 --- a/EdgeHttpDNS/HTTPDNS主计划.md +++ /dev/null @@ -1,98 +0,0 @@ -# HTTPDNS 主计划(V1.1) - -## 1. 目标与范围 -1. 新增独立模块 `EdgeHttpDNS`,独立构建、独立部署。 -2. 不改造 `EdgeDNS` 现有职责,`EdgeDNS` 继续处理传统 Port 53 DNS 请求。 -3. 构建端到端链路:`SDK -> EdgeHttpDNS -> EdgeNode 动态验签 -> WAF 放行/403`。 -4. P1 包含 ECH 全量能力(记录发布、密钥与节点解密链路)。 -5. 当前阶段仅实施管理平台能力,用户平台页面暂缓。 - -## 2. 关键决策(已锁定) -1. 对外接口仅保留 `POST /resolve`。 -2. 客户端 IP 策略:源 IP 优先,支持签名覆盖 `client_ip`。 -3. 场景判定规则:按“域名接入状态”自动判定。 -4. 场景1(仅 HTTPDNS 域名):直连权威 DNS。 -5. 场景2(已接入 CDN 域名):从边缘节点池计算并返回 CDN 节点 IP。 -6. `sni_policy` 枚举:`none|mask|empty|ech`(Level2 支持 `mask` 与 `empty`)。 -7. 证书字段:`cert_fingerprints[] + fingerprint_algo`。 -8. `EdgeNode` 动态验签采用请求头签名:`X-Edge-Resolve-*`。 -9. ECH 记录由第三方 DNS API 发布(通过凭证中心编排)。 - -## 3. 系统职责拆分 -### 3.1 管控端(仅管理平台)`edge-admin` -1. 配置中心:`AppID/Secret`、解析策略、SNI 隐匿等级、证书校验策略。 -2. 凭证中心:第三方 DNS 运营商 API 凭证管理与复用。 -3. ECH 发布编排:记录创建、更新、状态追踪、失败重试。 -4. 发布灰度、回退控制与审计日志。 - -### 3.2 解析网关 `edge-httpdns` -1. 鉴权与签名校验、防重放、限流。 -2. 双场景解析编排(权威 DNS / 边缘节点池调度)。 -3. 下发 `sni_policy`、`public_sni`、证书指纹策略。 -4. 生成并返回 `verify_headers` 供客户端请求透传。 - -### 3.3 边缘节点 `edge-node` -1. SNI 与 Host 解耦路由。 -2. 动态验签(验签失败返回 403)。 -3. ECH 解密与策略联动。 - -## 4. 接口规范(摘要) -### 4.1 Endpoint -- `POST /resolve` - -### 4.2 返回关键字段 -1. `ips` -2. `ttl` -3. `sni_policy` -4. `public_sni` -5. `cert_fingerprints` -6. `fingerprint_algo` -7. `verify_headers` - -### 4.3 动态验签请求头 -1. `X-Edge-Resolve-AppId` -2. `X-Edge-Resolve-Expire` -3. `X-Edge-Resolve-Nonce` -4. `X-Edge-Resolve-Sign` - -## 5. 页面规划(当前仅管理平台) -### 5.1 管理平台(Admin) -1. HTTPDNS 集群默认配置页。 -2. HTTPDNS 应用管理页(含 AppID/Secret、启停、限流、策略覆写)。 -3. 域名绑定与策略页(含测试 IP 指向 CDN 节点能力)。 -4. 边缘节点与调度策略页。 -5. 第三方 DNS 凭证中心页。 -6. ECH 发布状态与审计页。 -7. 解析调试页。 -8. WAF 动态验证规则页。 -9. 一键回退与发布控制页。 - -### 5.2 用户平台(User) -- 暂缓,不在当前阶段实施。 -- 所有配置与操作先收敛到管理平台。 - -## 6. SDK 路线 -1. 三端均采用 fork 改造路线(Android / iOS / Flutter)。 -2. 已落地源码目录: - - `EdgeHttpDNS/sdk/android` - - `EdgeHttpDNS/sdk/ios` - - `EdgeHttpDNS/sdk/flutter/aliyun_httpdns` -3. 溯源文档: - - `EdgeHttpDNS/sdk/SOURCE_LOCK.md` - - `EdgeHttpDNS/sdk/THIRD_PARTY_NOTICES.md` - -## 7. 实施阶段(更新) -1. Phase A:管理平台配置模型与页面(不做用户平台)。 -2. Phase B:`EdgeHttpDNS` 核心解析链路与双场景实现。 -3. Phase C:`EdgeNode` 动态验签与 WAF 联动。 -4. Phase D:ECH 密钥、记录发布、节点解密联调。 -5. Phase E:三端 SDK 改造与灰度上线。 -6. Phase F:用户平台能力评估与二期补齐(可选)。 - -## 8. 验收标准 -1. 场景判定准确,返回 IP 正确。 -2. 动态验签通过可放行,失败稳定返回 403。 -3. `/resolve` 返回字段与 SDK 消费一致。 -4. ECH 记录发布成功且链路可用。 -5. 管理平台可独立完成全流程配置、发布、回退与审计。 -6. 具备观测能力(QPS、错误率、签名通过率、403 率)。 diff --git a/EdgeHttpDNS/自定义解析功能.md b/EdgeHttpDNS/自定义解析功能.md new file mode 100644 index 0000000..763a61e --- /dev/null +++ b/EdgeHttpDNS/自定义解析功能.md @@ -0,0 +1,114 @@ +在域名管理中 加一个 自定义解析功能。 +功能简介:若需为域名提供特定的解析结果,可以使用HTTPDNS提供的自定义解析功能。该功能支持通过配置规则来实现对特定域名的自定义解析。 +应用场景:灰度测试:假设您的域名是 www.example.com,因为业务增长发布了新的服务,新的服务IP为1.1.X.X,在服务全量发布前,您希望对电信_上海访问域名的流量进行特定APP版本号的灰度测试。对于这些流量访问www.example.com时发起的域名解析请求返回1.1.X.X。 + +流量调度:假设某个汽车企业服务域名是 www.example.com,希望DNS解析过程中可以根据特定的业务逻辑返回位于不同区域的服务器的 IP 地址。例如根据汽车常驻地返回不同的服务IP,某辆汽车的常驻地区在广州,对于该汽车访问 www.example.com 时发起的 DNS 查询请求返回位于广州的服务器的 IP 地址。 + + +策略说明 +可以通过某种规则来达到自定义解析的目的。您可以对网络线路进行更精细的配置,并通过配置不同的解析参数,使来自不同运营商和地域的用户流量精准路由至不同的服务地址。 + +使用方式如下: + +在 HTTPDNS 控制台 中,为指定域名创建一条自定义解析规则策略。 + +客户端通过 SDK 发起 DNS 查询请求时,携带相应的自定义解析参数。 + +HTTPDNS 服务端接收到请求后,会根据预设的匹配规则,返回最符合业务需求的解析结果。 +您可以做以下配置: + +基本信息 + +参数 + +说明 + +域名 + +您希望自定义解析的域名,例如:www.aliyun.com。 + +说明 +域名选择下拉的数据源来自于接入域名中已经添加的域名,如果想要自定义解析的域名不在下拉列表中,可以到域名列表中添加后,再为该域名添加自定义解析记录。 + +如果要为某个泛域名的子域名添加自定义解析记录,例如:*.aliyun.com,但你想要自定义域名是 a.aliyun.com,则需要将 a.aliyun.com 添加到域名列表中,再为该域名添加自定义解析记录。 + +域名选择下拉字段选不到对应的域名有以下几种情况: + +想要添加的域名不在域名列表中,您可以到接入域名中添加对应的域名即可。 + +想要添加的域名是某个泛域名的子域名,将该子域名添加到域名列表即可。 + +想要添加的域名已经存在自定义解析记录,需要去自定义解析记录列表管理对应的域名。 + +线路 + +可针对运营商和地域进行线路配置。 + +中国内地线路:按“运营商 > 大区 > 省份”进行配置。 + +运营商:可以自定义运营商,例如:中国电信。如果运营商设置为默认,表示当前线路覆盖所有运营商。 + +大区:按照地域划分,例如:东北、华北、华东等,省份归属在对应大区下。如果大区设置为默认,表示当前线路覆盖所有大区。 + +省份:可以自定义省份,例如:北京、河北,如果省份设置为默认,表示当前线路覆盖所有省份。 + +海外线路:选择地域为“境外”时生效,按“洲 > 国家或地区”进行配置。 + +可以选择大洲,例如:亚洲、欧洲、南美洲等;也可以在大洲下选择具体国家或地区,例如:日本、英国等。 + +如果大洲、国家或地区设置为默认,表示当前线路覆盖所选范围内的全部区域。 + +说明 +在同一个域名下,对于相同地域的用户,线路生效的优先级是:运营商>地理位置>默认。例如,电信-华北-北京>电信-华北-默认>默认-华北-北京>默认-默认-默认。 + +例如:如果在同一个域名下同时存在两条规则策略,线路分别是电信-华北-北京和电信-华北-默认,那么对于北京的电信用户会使用电信-华北-北京线路的规则策略。 + +自定义解析规则 + +一条规则策略最多支持配置 10 条自定义解析规则。 + +参数 + +说明 + +规则名称 + +说明当前规则的名称,可以用来表达规则的用途,例如:通过SDK版本调度。 + +规则排序 + +多个规则之间可以调整顺序,匹配的逻辑是从上往下串行匹配,顺序决定了哪个规则会被优先命中,调整顺序后会按照新的顺序匹配。 + +SDNS参数配置 + +用来匹配客户端请求解析接口携带的SDNS参数,决定该条规则是否被命中,如果匹配成功,则返回该条规则中的解析记录值。详细的匹配逻辑请查看规则策略匹配逻辑说明。 + +参数名称:SDNS参数的名称,长度限制为 2 ~ 64 个字符。 + +参数值:SDNS参数的值,长度限制为 1 ~ 64 个字符。 + +说明 +一条规则最多添加 10 个 SDNS参数。 + +解析接口可以添加SDNS参数,具体查看 客户端传递自定义解析参数。 + +解析记录值 + +自定义解析的返回值集合,每个记录值代表记录集中的一条解析记录,必填。 + +记录类型:返回解析记录值的类型,支持A和AAAA记录。 + +记录值:返回的记录值 + +您可以添加多个记录值,在未开启权重的情况下,添加的多个记录值将会合并在一起返回。 + +还可以按照权重调度,只需要打开按照权重调度开关即可。开启权重后,可以为每个记录值设置权重,权重值设置范围为:1-100,根据记录值的权重,通过负载均衡算法返回一个合适的记录值。 + +说明 +一条规则最多添加 10 个记录值。 + +TTL + +必填,自定义解析记录的有效期。有效期越短,HTTPDNS SDK 中的解析记录缓存过期就越快。同时,HTTPDNS SDK 请求新的解析记录的频率就越高。 + + diff --git a/go1.25.4.linux-amd64.tar.gz b/go1.25.4.linux-amd64.tar.gz deleted file mode 100644 index a3f4b4d..0000000 Binary files a/go1.25.4.linux-amd64.tar.gz and /dev/null differ