带阿里标识的版本

This commit is contained in:
robin
2026-02-28 18:55:33 +08:00
parent 150799f41d
commit 5d0b7c7e91
477 changed files with 10813 additions and 4044 deletions

View File

@@ -1,7 +1,7 @@
package teaconst
const (
Version = "1.4.7" //1.3.8.2
Version = "1.4.8" //1.3.8.2
ProductName = "Edge User"
ProcessName = "edge-user"

View File

@@ -420,6 +420,34 @@ func (this *RPCClient) NSPlanRPC() pb.NSPlanServiceClient {
return pb.NewNSPlanServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSClusterRPC() pb.HTTPDNSClusterServiceClient {
return pb.NewHTTPDNSClusterServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSNodeRPC() pb.HTTPDNSNodeServiceClient {
return pb.NewHTTPDNSNodeServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSAppRPC() pb.HTTPDNSAppServiceClient {
return pb.NewHTTPDNSAppServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSDomainRPC() pb.HTTPDNSDomainServiceClient {
return pb.NewHTTPDNSDomainServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSRuleRPC() pb.HTTPDNSRuleServiceClient {
return pb.NewHTTPDNSRuleServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSAccessLogRPC() pb.HTTPDNSAccessLogServiceClient {
return pb.NewHTTPDNSAccessLogServiceClient(this.pickConn())
}
func (this *RPCClient) HTTPDNSSandboxRPC() pb.HTTPDNSSandboxServiceClient {
return pb.NewHTTPDNSSandboxServiceClient(this.pickConn())
}
func (this *RPCClient) ServerBandwidthStatRPC() pb.ServerBandwidthStatServiceClient {
return pb.NewServerBandwidthStatServiceClient(this.pickConn())
}

View File

@@ -0,0 +1,123 @@
package httpdns
import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"regexp"
"strings"
)
type AddPortPopupAction struct {
actionutils.ParentAction
}
func (this *AddPortPopupAction) Init() {
this.Nav("", "", "")
}
func (this *AddPortPopupAction) RunGet(params struct {
Protocol string
From string
SupportRange bool
}) {
this.Data["from"] = params.From
var protocols = serverconfigs.FindAllServerProtocols()
if len(params.Protocol) > 0 {
result := []maps.Map{}
for _, p := range protocols {
if p.GetString("code") == params.Protocol {
result = append(result, p)
}
}
protocols = result
}
this.Data["protocols"] = protocols
this.Data["supportRange"] = params.SupportRange
this.Show()
}
func (this *AddPortPopupAction) RunPost(params struct {
SupportRange bool
Protocol string
Address string
Must *actions.Must
}) {
// 鏍¢獙鍦板潃
var addr = maps.Map{
"protocol": params.Protocol,
"host": "",
"portRange": "",
"minPort": 0,
"maxPort": 0,
}
var portRegexp = regexp.MustCompile(`^\d+$`)
if portRegexp.MatchString(params.Address) { // 鍗曚釜绔彛
addr["portRange"] = this.checkPort(params.Address)
} else if params.SupportRange && regexp.MustCompile(`^\d+\s*-\s*\d+$`).MatchString(params.Address) { // Port1-Port2
addr["portRange"], addr["minPort"], addr["maxPort"] = this.checkPortRange(params.Address)
} else if strings.Contains(params.Address, ":") { // IP:Port
index := strings.LastIndex(params.Address, ":")
addr["host"] = strings.TrimSpace(params.Address[:index])
port := strings.TrimSpace(params.Address[index+1:])
if portRegexp.MatchString(port) {
addr["portRange"] = this.checkPort(port)
} else if params.SupportRange && regexp.MustCompile(`^\d+\s*-\s*\d+$`).MatchString(port) { // Port1-Port2
addr["portRange"], addr["minPort"], addr["maxPort"] = this.checkPortRange(port)
} else {
this.FailField("address", "璇疯緭鍏ユ纭殑绔彛鎴栬€呯綉缁滃湴鍧€")
}
} else {
this.FailField("address", "璇疯緭鍏ユ纭殑绔彛鎴栬€呯綉缁滃湴鍧€")
}
this.Data["address"] = addr
this.Success()
}
func (this *AddPortPopupAction) checkPort(port string) (portRange string) {
var intPort = types.Int(port)
if intPort < 1 {
this.FailField("address", "绔彛鍙蜂笉鑳藉皬浜?")
}
if intPort > 65535 {
this.FailField("address", "绔彛鍙蜂笉鑳藉ぇ浜?5535")
}
return port
}
func (this *AddPortPopupAction) checkPortRange(port string) (portRange string, minPort int, maxPort int) {
var pieces = strings.Split(port, "-")
var piece1 = strings.TrimSpace(pieces[0])
var piece2 = strings.TrimSpace(pieces[1])
var port1 = types.Int(piece1)
var port2 = types.Int(piece2)
if port1 < 1 {
this.FailField("address", "绔彛鍙蜂笉鑳藉皬浜?")
}
if port1 > 65535 {
this.FailField("address", "绔彛鍙蜂笉鑳藉ぇ浜?5535")
}
if port2 < 1 {
this.FailField("address", "绔彛鍙蜂笉鑳藉皬浜?")
}
if port2 > 65535 {
this.FailField("address", "绔彛鍙蜂笉鑳藉ぇ浜?5535")
}
if port1 > port2 {
port1, port2 = port2, port1
}
return types.String(port1) + "-" + types.String(port2), port1, port2
}

View File

@@ -0,0 +1,26 @@
package apps
import (
"strconv"
"github.com/TeaOSLab/EdgeUser/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, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
this.RedirectURL("/httpdns/apps/domains?appId=" + strconv.FormatInt(app.GetInt64("id"), 10))
}

View File

@@ -0,0 +1,120 @@
package apps
import (
"strconv"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type AppSettingsAction struct {
actionutils.ParentAction
}
func (this *AppSettingsAction) Init() {
this.Nav("httpdns", "app", "settings")
}
func (this *AppSettingsAction) RunGet(params struct {
AppId int64
Section string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "settings")
section := params.Section
if len(section) == 0 {
section = "basic"
}
this.Data["activeSection"] = section
appIDStr := strconv.FormatInt(app.GetInt64("id"), 10)
this.Data["leftMenuItems"] = []maps.Map{
{
"name": "基础配置",
"url": "/httpdns/apps/app/settings?appId=" + appIDStr + "&section=basic",
"isActive": section == "basic",
},
{
"name": "认证与密钥",
"url": "/httpdns/apps/app/settings?appId=" + appIDStr + "&section=auth",
"isActive": section == "auth",
},
}
clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.UserContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
clusterDomainMap := map[int64]string{}
for _, cluster := range clusterResp.GetClusters() {
clusterDomainMap[cluster.GetId()] = cluster.GetServiceDomain()
}
primaryClusterId := app.GetInt64("primaryClusterId")
backupClusterId := app.GetInt64("backupClusterId")
settings := maps.Map{
"appId": app.GetString("appId"),
"appStatus": app.GetBool("isOn"),
"primaryClusterId": primaryClusterId,
"backupClusterId": backupClusterId,
"primaryServiceDomain": clusterDomainMap[primaryClusterId],
"backupServiceDomain": clusterDomainMap[backupClusterId],
"signEnabled": app.GetBool("signEnabled"),
"signSecretPlain": app.GetString("signSecretPlain"),
"signSecretMasked": app.GetString("signSecretMasked"),
"signSecretUpdatedAt": app.GetString("signSecretUpdated"),
}
this.Data["app"] = app
this.Data["settings"] = settings
this.Show()
}
func (this *AppSettingsAction) RunPost(params struct {
AppId int64
AppStatus bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
appResp, err := this.RPC().HTTPDNSAppRPC().FindHTTPDNSApp(this.UserContext(), &pb.FindHTTPDNSAppRequest{
AppDbId: params.AppId,
})
if err != nil {
this.ErrorPage(err)
return
}
if appResp.GetApp() == nil {
this.Fail("找不到对应的应用")
return
}
_, err = this.RPC().HTTPDNSAppRPC().UpdateHTTPDNSApp(this.UserContext(), &pb.UpdateHTTPDNSAppRequest{
AppDbId: params.AppId,
Name: appResp.GetApp().GetName(),
PrimaryClusterId: appResp.GetApp().GetPrimaryClusterId(),
BackupClusterId: appResp.GetApp().GetBackupClusterId(),
IsOn: params.AppStatus,
UserId: appResp.GetApp().GetUserId(),
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,29 @@
package apps
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/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
}) {
params.Must.Field("appId", params.AppId).Gt(0, "璇烽€夋嫨搴旂敤")
_, err := this.RPC().HTTPDNSAppRPC().ResetHTTPDNSAppSignSecret(this.UserContext(), &pb.ResetHTTPDNSAppSignSecretRequest{
AppDbId: params.AppId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,31 @@
package apps
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/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
}) {
params.Must.Field("appId", params.AppId).Gt(0, "璇烽€夋嫨搴旂敤")
_, err := this.RPC().HTTPDNSAppRPC().UpdateHTTPDNSAppSignEnabled(this.UserContext(), &pb.UpdateHTTPDNSAppSignEnabledRequest{
AppDbId: params.AppId,
SignEnabled: params.IsOn == 1,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,47 @@
package apps
import (
"strconv"
"time"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
)
type CreateAction struct {
actionutils.ParentAction
}
func (this *CreateAction) Init() {
this.Nav("", "", "create")
}
func (this *CreateAction) RunGet(params struct{}) {
this.Show()
}
func (this *CreateAction) RunPost(params struct {
Name string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("name", params.Name).Require("请输入应用名称")
createResp, err := this.RPC().HTTPDNSAppRPC().CreateHTTPDNSApp(this.UserContext(), &pb.CreateHTTPDNSAppRequest{
Name: params.Name,
AppId: "app" + strconv.FormatInt(time.Now().UnixNano()%1_000_000_000_000, 36),
PrimaryClusterId: 0,
BackupClusterId: 0,
IsOn: true,
SignEnabled: true,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["appId"] = createResp.GetAppDbId()
this.Success()
}

View File

@@ -0,0 +1,55 @@
package apps
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/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, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "domains")
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
domain := findDomainMap(domains, params.DomainId)
records := make([]maps.Map, 0)
if domain.GetInt64("id") > 0 {
records, err = listCustomRuleMaps(this.Parent(), domain.GetInt64("id"))
if err != nil {
this.ErrorPage(err)
return
}
for _, record := range records {
record["domain"] = domain.GetString("name")
record["lineText"] = buildLineText(record)
record["recordValueText"] = buildRecordValueText(record)
}
}
this.Data["app"] = app
this.Data["domain"] = domain
this.Data["records"] = records
this.Show()
}

View File

@@ -0,0 +1,263 @@
package apps
import (
"encoding/json"
"fmt"
"strings"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/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, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
domain := findDomainMap(domains, params.DomainId)
record := maps.Map{
"id": int64(0),
"domain": domain.GetString("name"),
"lineScope": "china",
"lineCarrier": "榛樿",
"lineRegion": "榛樿",
"lineProvince": "榛樿",
"lineContinent": "榛樿",
"lineCountry": "榛樿",
"ruleName": "",
"weightEnabled": false,
"ttl": 30,
"isOn": true,
"recordItemsJson": `[{"type":"A","value":"","weight":100}]`,
}
if params.RecordId > 0 && domain.GetInt64("id") > 0 {
rules, err := listCustomRuleMaps(this.Parent(), domain.GetInt64("id"))
if err != nil {
this.ErrorPage(err)
return
}
for _, rule := range rules {
if rule.GetInt64("id") != params.RecordId {
continue
}
record["id"] = rule.GetInt64("id")
record["domain"] = domain.GetString("name")
record["lineScope"] = rule.GetString("lineScope")
record["lineCarrier"] = defaultLineField(rule.GetString("lineCarrier"))
record["lineRegion"] = defaultLineField(rule.GetString("lineRegion"))
record["lineProvince"] = defaultLineField(rule.GetString("lineProvince"))
record["lineContinent"] = defaultLineField(rule.GetString("lineContinent"))
record["lineCountry"] = defaultLineField(rule.GetString("lineCountry"))
record["ruleName"] = rule.GetString("ruleName")
record["weightEnabled"] = rule.GetBool("weightEnabled")
record["ttl"] = rule.GetInt("ttl")
record["isOn"] = rule.GetBool("isOn")
record["recordItemsJson"] = marshalJSON(rule["recordValues"], "[]")
break
}
}
this.Data["app"] = app
this.Data["domain"] = domain
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
RecordItemsJSON string
WeightEnabled bool
Ttl int
IsOn bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "璇烽€夋嫨搴旂敤")
params.Must.Field("domainId", params.DomainId).Gt(0, "请选择所属域名")
params.LineScope = strings.ToLower(strings.TrimSpace(params.LineScope))
if params.LineScope != "china" && params.LineScope != "overseas" {
params.LineScope = "china"
}
params.RuleName = strings.TrimSpace(params.RuleName)
if len(params.RuleName) == 0 {
this.Fail("请输入规则名称")
return
}
if params.Ttl <= 0 || params.Ttl > 86400 {
this.Fail("TTL值必须在 1-86400 范围内")
return
}
recordValues, err := parseRecordItemsJSON(params.RecordItemsJSON, params.WeightEnabled)
if err != nil {
this.Fail(err.Error())
return
}
if len(recordValues) == 0 {
this.Fail("请输入解析记录值")
return
}
if len(recordValues) > 10 {
this.Fail("单个规则最多只能添加 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 = ""
}
records := make([]*pb.HTTPDNSRuleRecord, 0, len(recordValues))
for i, item := range recordValues {
records = append(records, &pb.HTTPDNSRuleRecord{
Id: 0,
RuleId: 0,
RecordType: item.GetString("type"),
RecordValue: item.GetString("value"),
Weight: int32(item.GetInt("weight")),
Sort: int32(i + 1),
})
}
rule := &pb.HTTPDNSCustomRule{
Id: params.RecordId,
AppId: params.AppId,
DomainId: params.DomainId,
RuleName: params.RuleName,
LineScope: params.LineScope,
LineCarrier: lineCarrier,
LineRegion: lineRegion,
LineProvince: lineProvince,
LineContinent: lineContinent,
LineCountry: lineCountry,
Ttl: int32(params.Ttl),
IsOn: params.IsOn,
Priority: 100,
Records: records,
}
if params.RecordId > 0 {
err = updateCustomRule(this.Parent(), rule)
} else {
_, err = createCustomRule(this.Parent(), rule)
}
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}
func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
raw = strings.TrimSpace(raw)
if len(raw) == 0 {
return []maps.Map{}, nil
}
list := []maps.Map{}
if err := json.Unmarshal([]byte(raw), &list); err != nil {
return nil, fmt.Errorf("解析记录格式不正确")
}
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("记录类型只能是 A 或 AAAA")
}
if len(recordValue) == 0 {
return nil, fmt.Errorf("记录值不能为空")
}
weight := item.GetInt("weight")
if !weightEnabled {
weight = 100
}
if weight < 1 || weight > 100 {
return nil, fmt.Errorf("鏉冮噸鍊煎繀椤诲湪 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)
}

View File

@@ -0,0 +1,21 @@
package apps
import "github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
type CustomRecordsDeleteAction struct {
actionutils.ParentAction
}
func (this *CustomRecordsDeleteAction) RunPost(params struct {
AppId int64
RecordId int64
}) {
if params.RecordId > 0 {
err := deleteCustomRule(this.Parent(), params.RecordId)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

@@ -0,0 +1,22 @@
package apps
import "github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
type CustomRecordsToggleAction struct {
actionutils.ParentAction
}
func (this *CustomRecordsToggleAction) RunPost(params struct {
AppId int64
RecordId int64
IsOn bool
}) {
if params.RecordId > 0 {
err := toggleCustomRule(this.Parent(), params.RecordId, params.IsOn)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

@@ -0,0 +1,48 @@
package apps
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns/httpdnsutils"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) Init() {
this.Nav("httpdns", "app", "delete")
}
func (this *DeleteAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "delete")
this.Data["app"] = app
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
this.Data["domainCount"] = len(domains)
this.Show()
}
func (this *DeleteAction) RunPost(params struct {
AppId int64
}) {
if params.AppId > 0 {
err := deleteAppByID(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

@@ -0,0 +1,39 @@
package apps
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns/httpdnsutils"
)
type DomainsAction struct {
actionutils.ParentAction
}
func (this *DomainsAction) Init() {
this.Nav("httpdns", "app", "domains")
}
func (this *DomainsAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
// 鏋勫缓椤堕儴 tabbar
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "domains")
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
this.Data["app"] = app
this.Data["domains"] = domains
this.Data["keyword"] = ""
this.Show()
}

View File

@@ -0,0 +1,44 @@
package apps
import (
"github.com/TeaOSLab/EdgeUser/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
}) {
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["app"] = app
this.Show()
}
func (this *DomainsCreatePopupAction) RunPost(params struct {
AppId int64
Domain string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "璇烽€夋嫨搴旂敤")
params.Must.Field("domain", params.Domain).Require("请输入域名")
err := createDomain(this.Parent(), params.AppId, params.Domain)
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,20 @@
package apps
import "github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
type DomainsDeleteAction struct {
actionutils.ParentAction
}
func (this *DomainsDeleteAction) RunPost(params struct {
DomainId int64
}) {
if params.DomainId > 0 {
err := deleteDomain(this.Parent(), params.DomainId)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

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

View File

@@ -0,0 +1,28 @@
package apps
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/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
apps, err := listAppMaps(this.Parent(), params.Keyword)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["apps"] = apps
this.Show()
}

View File

@@ -0,0 +1,35 @@
package apps
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "httpdns").
Data("teaSubMenu", "app").
Prefix("/httpdns/apps").
Get("", new(IndexAction)).
Get("/app", new(AppAction)).
Get("/sdk", new(SdkAction)).
Get("/sdk/check", new(SdkCheckAction)).
Get("/sdk/download", new(SdkDownloadAction)).
Get("/sdk/doc", new(SdkDocAction)).
GetPost("/app/settings", new(AppSettingsAction)).
Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)).
Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)).
Get("/domains", new(DomainsAction)).
Get("/customRecords", new(CustomRecordsAction)).
GetPost("/create", new(CreateAction)).
GetPost("/delete", new(DeleteAction)).
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()
})
}

View File

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

View File

@@ -0,0 +1,30 @@
package apps
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns/httpdnsutils"
)
type SdkAction struct {
actionutils.ParentAction
}
func (this *SdkAction) Init() {
this.Nav("httpdns", "app", "sdk")
}
func (this *SdkAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "sdk")
this.Data["app"] = app
this.Show()
}

View File

@@ -0,0 +1,67 @@
package apps
import (
"net/url"
"strings"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type SdkCheckAction struct {
actionutils.ParentAction
}
func (this *SdkCheckAction) Init() {
this.Nav("", "", "")
}
func (this *SdkCheckAction) RunGet(params struct {
Platform string
Version string
Type string
}) {
platform, _, _, filename, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Data["exists"] = false
this.Data["message"] = err.Error()
this.Success()
return
}
t := strings.ToLower(strings.TrimSpace(params.Type))
if t == "doc" {
docPath := findUploadedSDKDocPath(platform, params.Version)
if len(docPath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "Documentation is unavailable, please contact admin"
this.Success()
return
}
downloadURL := "/httpdns/apps/sdk/doc?platform=" + url.QueryEscape(platform)
if len(strings.TrimSpace(params.Version)) > 0 {
downloadURL += "&version=" + url.QueryEscape(strings.TrimSpace(params.Version))
}
this.Data["exists"] = true
this.Data["url"] = downloadURL
this.Success()
return
}
archivePath := findSDKArchivePath(filename, params.Version)
if len(archivePath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "SDK package is unavailable, please contact admin"
this.Success()
return
}
downloadURL := "/httpdns/apps/sdk/download?platform=" + url.QueryEscape(platform)
if len(strings.TrimSpace(params.Version)) > 0 {
downloadURL += "&version=" + url.QueryEscape(strings.TrimSpace(params.Version))
}
downloadURL += "&raw=1"
this.Data["exists"] = true
this.Data["url"] = downloadURL
this.Success()
}

View File

@@ -0,0 +1,65 @@
package apps
import (
"os"
"path/filepath"
"strings"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type SdkDocAction struct {
actionutils.ParentAction
}
func (this *SdkDocAction) Init() {
this.Nav("", "", "")
}
func (this *SdkDocAction) RunGet(params struct {
Platform string
Version string
}) {
platform, _, readmeRelativePath, _, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Data["exists"] = false
this.Data["message"] = err.Error()
this.Success()
return
}
var data []byte
uploadedDocPath := findUploadedSDKDocPath(platform, params.Version)
if len(uploadedDocPath) > 0 {
data, err = os.ReadFile(uploadedDocPath)
}
sdkRoot, sdkRootErr := findSDKRoot()
if len(data) == 0 && sdkRootErr == nil {
readmePath := filepath.Join(sdkRoot, readmeRelativePath)
data, err = os.ReadFile(readmePath)
}
if len(data) == 0 {
localDocPath := findLocalSDKDocPath(platform)
if len(localDocPath) > 0 {
data, err = os.ReadFile(localDocPath)
}
}
if len(data) == 0 || err != nil {
this.Data["exists"] = false
this.Data["message"] = "SDK documentation is unavailable for current platform, please contact admin"
this.Success()
return
}
downloadName := filepath.Base(uploadedDocPath)
if len(downloadName) == 0 || downloadName == "." || downloadName == string(filepath.Separator) {
downloadName = "httpdns-sdk-" + strings.ToLower(platform) + ".md"
}
this.AddHeader("Content-Type", "text/markdown; charset=utf-8")
this.AddHeader("Content-Disposition", "attachment; filename=\""+downloadName+"\";")
_, _ = this.ResponseWriter.Write(data)
}

View File

@@ -0,0 +1,65 @@
package apps
import (
"io"
"os"
"path/filepath"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type SdkDownloadAction struct {
actionutils.ParentAction
}
func (this *SdkDownloadAction) Init() {
this.Nav("", "", "")
}
func (this *SdkDownloadAction) RunGet(params struct {
Platform string
Version string
Raw int
}) {
_, _, _, filename, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Data["exists"] = false
this.Data["message"] = err.Error()
this.Success()
return
}
archivePath := findSDKArchivePath(filename, params.Version)
if len(archivePath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "SDK archive is unavailable for current platform, please contact admin: " + filename
this.Success()
return
}
fp, err := os.Open(archivePath)
if err != nil {
this.Data["exists"] = false
this.Data["message"] = "failed to open SDK archive: " + err.Error()
this.Success()
return
}
defer func() {
_ = fp.Close()
}()
downloadName := filepath.Base(archivePath)
if len(downloadName) == 0 || downloadName == "." || downloadName == string(filepath.Separator) {
downloadName = filename
}
if params.Raw == 1 {
this.AddHeader("Content-Type", "application/octet-stream")
this.AddHeader("X-SDK-Filename", downloadName)
} else {
this.AddHeader("Content-Type", "application/zip")
this.AddHeader("Content-Disposition", "attachment; filename=\""+downloadName+"\";")
}
this.AddHeader("X-Accel-Buffering", "no")
_, _ = io.Copy(this.ResponseWriter, fp)
}

View File

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

View File

@@ -0,0 +1,57 @@
package httpdnsutils
import (
"strconv"
"github.com/TeaOSLab/EdgeUser/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": "\u5e94\u7528\u7ba1\u7406",
"url": "/httpdns/apps",
"isActive": tab == "app",
},
{
"name": "\u8bbf\u95ee\u65e5\u5fd7",
"url": "/httpdns/resolveLogs",
"isActive": tab == "resolveLogs",
},
{
"name": "\u89e3\u6790\u6d4b\u8bd5",
"url": "/httpdns/sandbox",
"isActive": tab == "sandbox",
},
}
}
// AddClusterTabbar builds the top tabbar on cluster pages.
func AddClusterTabbar(action *actionutils.ParentAction, clusterName string, clusterId int64, selectedTab string) {
cid := strconv.FormatInt(clusterId, 10)
tabbar := actionutils.NewTabbar()
tabbar.Add("", "", "/httpdns/clusters", "arrow left", false)
titleItem := tabbar.Add(clusterName, "", "/httpdns/clusters/cluster?clusterId="+cid, "angle right", true)
titleItem["isTitle"] = true
tabbar.Add("\u8282\u70b9\u5217\u8868", "", "/httpdns/clusters/cluster?clusterId="+cid, "server", selectedTab == "node")
tabbar.Add("\u96c6\u7fa4\u8bbe\u7f6e", "", "/httpdns/clusters/cluster/settings?clusterId="+cid, "setting", selectedTab == "setting")
tabbar.Add("\u5220\u9664\u96c6\u7fa4", "", "/httpdns/clusters/delete?clusterId="+cid, "trash", selectedTab == "delete")
actionutils.SetTabbar(action, tabbar)
}
// AddAppTabbar builds the top tabbar on app pages.
func AddAppTabbar(action *actionutils.ParentAction, appName string, appId int64, selectedTab string) {
aid := strconv.FormatInt(appId, 10)
tabbar := actionutils.NewTabbar()
tabbar.Add("", "", "/httpdns/apps", "arrow left", false)
titleItem := tabbar.Add(appName, "", "/httpdns/apps/domains?appId="+aid, "angle right", true)
titleItem["isTitle"] = true
tabbar.Add("\u57df\u540d\u5217\u8868", "", "/httpdns/apps/domains?appId="+aid, "list", selectedTab == "domains")
tabbar.Add("\u5e94\u7528\u8bbe\u7f6e", "", "/httpdns/apps/app/settings?appId="+aid, "setting", selectedTab == "settings")
tabbar.Add("\u5220\u9664\u5e94\u7528", "", "/httpdns/apps/delete?appId="+aid, "trash", selectedTab == "delete")
actionutils.SetTabbar(action, tabbar)
}

View File

@@ -0,0 +1,11 @@
package httpdns
import "github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) RunGet(params struct{}) {
this.RedirectURL("/httpdns/apps")
}

View File

@@ -0,0 +1,18 @@
package httpdns
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "httpdns").
Data("teaSubMenu", "app").
Prefix("/httpdns").
Get("", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,123 @@
package resolveLogs
import (
"strings"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns/httpdnsutils"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("httpdns", "resolveLogs", "")
}
func (this *IndexAction) RunGet(params struct {
ClusterId int64
AppId string
Domain string
Status string
Keyword string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
if params.ClusterId > 0 {
this.Data["clusterId"] = params.ClusterId
} else {
this.Data["clusterId"] = ""
}
this.Data["appId"] = params.AppId
this.Data["domain"] = params.Domain
this.Data["status"] = params.Status
this.Data["keyword"] = params.Keyword
clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.UserContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
clusters := make([]map[string]interface{}, 0, len(clusterResp.GetClusters()))
clusterDomainMap := map[int64]string{}
for _, cluster := range clusterResp.GetClusters() {
serviceDomain := strings.TrimSpace(cluster.GetServiceDomain())
displayName := serviceDomain
if len(displayName) == 0 {
displayName = cluster.GetName()
}
clusters = append(clusters, map[string]interface{}{
"id": cluster.GetId(),
"name": cluster.GetName(),
"serviceDomain": serviceDomain,
"displayName": displayName,
})
clusterDomainMap[cluster.GetId()] = serviceDomain
}
this.Data["clusters"] = clusters
logResp, err := this.RPC().HTTPDNSAccessLogRPC().ListHTTPDNSAccessLogs(this.UserContext(), &pb.ListHTTPDNSAccessLogsRequest{
Day: "",
ClusterId: params.ClusterId,
NodeId: 0,
AppId: strings.TrimSpace(params.AppId),
Domain: strings.TrimSpace(params.Domain),
Status: strings.TrimSpace(params.Status),
Keyword: strings.TrimSpace(params.Keyword),
Offset: 0,
Size: 100,
})
if err != nil {
this.ErrorPage(err)
return
}
logs := make([]map[string]interface{}, 0, len(logResp.GetLogs()))
for _, item := range logResp.GetLogs() {
createdTime := ""
if item.GetCreatedAt() > 0 {
createdTime = timeutil.FormatTime("Y-m-d H:i:s", item.GetCreatedAt())
}
status := item.GetStatus()
if len(status) == 0 {
status = "failed"
}
errorCode := item.GetErrorCode()
if len(errorCode) == 0 {
errorCode = "none"
}
logs = append(logs, map[string]interface{}{
"time": createdTime,
"clusterId": item.GetClusterId(),
"serviceDomain": func() string {
serviceDomain := strings.TrimSpace(clusterDomainMap[item.GetClusterId()])
if len(serviceDomain) > 0 {
return serviceDomain
}
if len(strings.TrimSpace(item.GetClusterName())) > 0 {
return item.GetClusterName()
}
return "-"
}(),
"appName": item.GetAppName(),
"appId": item.GetAppId(),
"domain": item.GetDomain(),
"query": item.GetQtype(),
"clientIp": item.GetClientIP(),
"os": item.GetOs(),
"sdkVersion": item.GetSdkVersion(),
"ips": item.GetResultIPs(),
"status": status,
"errorCode": errorCode,
"costMs": item.GetCostMs(),
})
}
this.Data["resolveLogs"] = logs
this.Show()
}

View File

@@ -0,0 +1,18 @@
package resolveLogs
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "httpdns").
Data("teaSubMenu", "resolveLogs").
Prefix("/httpdns/resolveLogs").
Get("", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,74 @@
package sandbox
import (
"strings"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("httpdns", "sandbox", "")
}
func (this *IndexAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.UserContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
clusters := make([]maps.Map, 0, len(clusterResp.GetClusters()))
for _, cluster := range clusterResp.GetClusters() {
serviceDomain := strings.TrimSpace(cluster.GetServiceDomain())
displayName := serviceDomain
if len(displayName) == 0 {
displayName = cluster.GetName()
}
clusters = append(clusters, maps.Map{
"id": cluster.GetId(),
"name": cluster.GetName(),
"serviceDomain": serviceDomain,
"displayName": displayName,
})
}
this.Data["clusters"] = clusters
appResp, err := this.RPC().HTTPDNSAppRPC().FindAllHTTPDNSApps(this.UserContext(), &pb.FindAllHTTPDNSAppsRequest{})
if err != nil {
this.ErrorPage(err)
return
}
apps := make([]maps.Map, 0, len(appResp.GetApps()))
for _, app := range appResp.GetApps() {
domainResp, err := this.RPC().HTTPDNSDomainRPC().ListHTTPDNSDomainsWithAppId(this.UserContext(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
AppDbId: app.GetId(),
})
if err != nil {
this.ErrorPage(err)
return
}
domains := make([]string, 0, len(domainResp.GetDomains()))
for _, domain := range domainResp.GetDomains() {
domains = append(domains, domain.GetDomain())
}
apps = append(apps, maps.Map{
"id": app.GetId(),
"name": app.GetName(),
"appId": app.GetAppId(),
"clusterId": app.GetPrimaryClusterId(),
"primaryClusterId": app.GetPrimaryClusterId(),
"backupClusterId": app.GetBackupClusterId(),
"domains": domains,
})
}
this.Data["apps"] = apps
this.Show()
}

View File

@@ -0,0 +1,19 @@
package sandbox
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "httpdns").
Data("teaSubMenu", "sandbox").
Prefix("/httpdns/sandbox").
Get("", new(IndexAction)).
Post("/test", new(TestAction)).
EndAll()
})
}

View File

@@ -0,0 +1,154 @@
package sandbox
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/url"
"strconv"
"strings"
"time"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/login/loginutils"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
)
type TestAction struct {
actionutils.ParentAction
}
func (this *TestAction) RunPost(params struct {
AppId string
ClusterId int64
Domain string
ClientIp string
Qtype string
}) {
clientIP := strings.TrimSpace(params.ClientIp)
if len(clientIP) == 0 {
clientIP = strings.TrimSpace(loginutils.RemoteIP(&this.ActionObject))
}
qtype := strings.ToUpper(strings.TrimSpace(params.Qtype))
if len(qtype) == 0 {
qtype = "A"
}
resp, err := this.RPC().HTTPDNSSandboxRPC().TestHTTPDNSResolve(this.UserContext(), &pb.TestHTTPDNSResolveRequest{
ClusterId: params.ClusterId,
AppId: strings.TrimSpace(params.AppId),
Domain: strings.TrimSpace(params.Domain),
Qtype: qtype,
ClientIP: clientIP,
Sid: "",
SdkVersion: "",
Os: "",
})
if err != nil {
this.ErrorPage(err)
return
}
clusterDomain := ""
if params.ClusterId > 0 {
clusterResp, findErr := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.UserContext(), &pb.FindAllHTTPDNSClustersRequest{})
if findErr == nil {
for _, cluster := range clusterResp.GetClusters() {
if cluster.GetId() == params.ClusterId {
clusterDomain = strings.TrimSpace(cluster.GetServiceDomain())
break
}
}
}
}
if len(clusterDomain) == 0 {
clusterDomain = "httpdns.example.com"
}
query := url.Values{}
query.Set("appId", params.AppId)
query.Set("dn", params.Domain)
query.Set("qtype", qtype)
if len(clientIP) > 0 {
query.Set("cip", clientIP)
}
signEnabled, signSecret := this.findAppSignConfig(params.AppId)
if signEnabled && len(signSecret) > 0 {
exp := strconv.FormatInt(time.Now().Unix()+300, 10)
nonce := "sandbox-" + rands.HexString(16)
sign := buildSandboxResolveSign(signSecret, params.AppId, params.Domain, qtype, exp, nonce)
query.Set("exp", exp)
query.Set("nonce", nonce)
query.Set("sign", sign)
}
requestURL := "https://" + clusterDomain + "/resolve?" + query.Encode()
resultCode := 1
if strings.EqualFold(resp.GetCode(), "SUCCESS") {
resultCode = 0
}
rows := make([]maps.Map, 0, len(resp.GetRecords()))
ips := make([]string, 0, len(resp.GetRecords()))
lineName := strings.TrimSpace(resp.GetClientCarrier())
if len(lineName) == 0 {
lineName = "-"
}
for _, record := range resp.GetRecords() {
ips = append(ips, record.GetIp())
if lineName == "-" && len(record.GetLine()) > 0 {
lineName = record.GetLine()
}
rows = append(rows, maps.Map{
"domain": resp.GetDomain(),
"type": record.GetType(),
"ip": record.GetIp(),
"ttl": record.GetTtl(),
"region": record.GetRegion(),
"line": record.GetLine(),
})
}
this.Data["result"] = maps.Map{
"code": resultCode,
"message": resp.GetMessage(),
"requestId": resp.GetRequestId(),
"data": maps.Map{
"request_url": requestURL,
"client_ip": resp.GetClientIP(),
"client_region": resp.GetClientRegion(),
"line_name": lineName,
"ips": ips,
"records": rows,
"ttl": resp.GetTtl(),
},
}
this.Success()
}
func (this *TestAction) findAppSignConfig(appId string) (bool, string) {
appId = strings.TrimSpace(appId)
if len(appId) == 0 {
return false, ""
}
resp, err := this.RPC().HTTPDNSAppRPC().FindAllHTTPDNSApps(this.UserContext(), &pb.FindAllHTTPDNSAppsRequest{})
if err != nil {
return false, ""
}
for _, app := range resp.GetApps() {
if strings.EqualFold(strings.TrimSpace(app.GetAppId()), appId) {
return app.GetSignEnabled(), strings.TrimSpace(app.GetSignSecret())
}
}
return false, ""
}
func buildSandboxResolveSign(signSecret string, appID string, domain string, qtype string, exp string, nonce string) string {
raw := strings.TrimSpace(appID) + "|" + strings.ToLower(strings.TrimSpace(domain)) + "|" + strings.ToUpper(strings.TrimSpace(qtype)) + "|" + strings.TrimSpace(exp) + "|" + strings.TrimSpace(nonce)
mac := hmac.New(sha256.New, []byte(strings.TrimSpace(signSecret)))
_, _ = mac.Write([]byte(raw))
return hex.EncodeToString(mac.Sum(nil))
}

View File

@@ -426,6 +426,31 @@ func (this *userMustAuth) modules(userId int64, isVerified bool, isIdentified bo
},**/
},
},
{
"code": "httpdns",
"name": "HTTPDNS",
"icon": "shield alternate",
"isOn": registerConfig != nil &&
registerConfig.HTTPDNSIsOn &&
lists.ContainsString(featureCodes, userconfigs.UserFeatureCodeHTTPDNS),
"subItems": []maps.Map{
{
"name": "应用管理",
"code": "app",
"url": "/httpdns/apps",
},
{
"name": "访问日志",
"code": "resolveLogs",
"url": "/httpdns/resolveLogs",
},
{
"name": "解析测试",
"code": "sandbox",
"url": "/httpdns/sandbox",
},
},
},
{
"code": "finance",
"name": "财务管理",

View File

@@ -106,6 +106,12 @@ import (
_ "github.com/TeaOSLab/EdgeUser/internal/web/actions/default/ns"
_ "github.com/TeaOSLab/EdgeUser/internal/web/actions/default/ns/routes"
// httpdns
_ "github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns"
_ "github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns/apps"
_ "github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns/resolveLogs"
_ "github.com/TeaOSLab/EdgeUser/internal/web/actions/default/httpdns/sandbox"
// api
_ "github.com/TeaOSLab/EdgeUser/internal/web/actions/default/api"