带阿里标识的版本
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
123
EdgeUser/internal/web/actions/default/httpdns/addPortPopup.go
Normal file
123
EdgeUser/internal/web/actions/default/httpdns/addPortPopup.go
Normal 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
|
||||
}
|
||||
26
EdgeUser/internal/web/actions/default/httpdns/apps/app.go
Normal file
26
EdgeUser/internal/web/actions/default/httpdns/apps/app.go
Normal 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))
|
||||
}
|
||||
@@ -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 + "§ion=basic",
|
||||
"isActive": section == "basic",
|
||||
},
|
||||
{
|
||||
"name": "认证与密钥",
|
||||
"url": "/httpdns/apps/app/settings?appId=" + appIDStr + "§ion=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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
47
EdgeUser/internal/web/actions/default/httpdns/apps/create.go
Normal file
47
EdgeUser/internal/web/actions/default/httpdns/apps/create.go
Normal 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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
48
EdgeUser/internal/web/actions/default/httpdns/apps/delete.go
Normal file
48
EdgeUser/internal/web/actions/default/httpdns/apps/delete.go
Normal 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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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, ", ")
|
||||
}
|
||||
28
EdgeUser/internal/web/actions/default/httpdns/apps/index.go
Normal file
28
EdgeUser/internal/web/actions/default/httpdns/apps/index.go
Normal 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()
|
||||
}
|
||||
35
EdgeUser/internal/web/actions/default/httpdns/apps/init.go
Normal file
35
EdgeUser/internal/web/actions/default/httpdns/apps/init.go
Normal 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()
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
30
EdgeUser/internal/web/actions/default/httpdns/apps/sdk.go
Normal file
30
EdgeUser/internal/web/actions/default/httpdns/apps/sdk.go
Normal 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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
11
EdgeUser/internal/web/actions/default/httpdns/index.go
Normal file
11
EdgeUser/internal/web/actions/default/httpdns/index.go
Normal 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")
|
||||
}
|
||||
18
EdgeUser/internal/web/actions/default/httpdns/init.go
Normal file
18
EdgeUser/internal/web/actions/default/httpdns/init.go
Normal 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()
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
154
EdgeUser/internal/web/actions/default/httpdns/sandbox/test.go
Normal file
154
EdgeUser/internal/web/actions/default/httpdns/sandbox/test.go
Normal 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))
|
||||
}
|
||||
@@ -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": "财务管理",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
{$echo "header"}
|
||||
<!-- 品牌配置 -->
|
||||
<script type="text/javascript">
|
||||
window.BRAND_OFFICIAL_SITE = { $ jsonEncode .brandConfig.officialSite };
|
||||
window.BRAND_DOCS_SITE = { $ jsonEncode .brandConfig.docsSite };
|
||||
window.BRAND_DOCS_PREFIX = { $ jsonEncode .brandConfig.docsPathPrefix };
|
||||
window.BRAND_PRODUCT_NAME = { $ jsonEncode .brandConfig.productName };
|
||||
window.BRAND_OFFICIAL_SITE = {$ jsonEncode .brandConfig.officialSite};
|
||||
window.BRAND_DOCS_SITE = {$ jsonEncode .brandConfig.docsSite};
|
||||
window.BRAND_DOCS_PREFIX = {$ jsonEncode .brandConfig.docsPathPrefix};
|
||||
window.BRAND_PRODUCT_NAME = {$ jsonEncode .brandConfig.productName};
|
||||
</script>
|
||||
<script type="text/javascript" src="/js/config/brand.js"></script>
|
||||
<script type="text/javascript" src="/_/@default/@layout.js"></script>
|
||||
@@ -80,8 +80,8 @@
|
||||
class="disabled">消息(0)</span></span>
|
||||
</a>
|
||||
<a href="/settings/profile" class="item">
|
||||
<i class="icon user" v-if="teaUserAvatar.length == 0"></i>
|
||||
<img class="avatar" alt="" :src="teaUserAvatar" v-if="teaUserAvatar.length > 0" />
|
||||
<i class="icon user" v-if="(teaUserAvatar || '').length == 0"></i>
|
||||
<img class="avatar" alt="" :src="teaUserAvatar" v-if="(teaUserAvatar || '').length > 0" />
|
||||
<span class="hover-span"><span class="disabled">{{teaUsername}}</span></span>
|
||||
</a>
|
||||
<a href="/docs" class="item" :class="{active: teaMenu == 'docs'}"><i class="icon file"></i><span
|
||||
@@ -109,9 +109,9 @@
|
||||
<!-- 模块 -->
|
||||
<div v-for="module in teaModules">
|
||||
<a class="item" :href="Tea.url(module.code)"
|
||||
:class="{active:teaMenu == module.code && teaSubMenu.length == 0, separator:module.code.length == 0}"
|
||||
:style="(teaMenu == module.code && teaSubMenu.length == 0) ? 'background: rgba(230, 230, 230, 0.45) !important;' : ''">
|
||||
<span v-if="module.code.length > 0">
|
||||
:class="{active:teaMenu == module.code && (teaSubMenu || '').length == 0, separator:(module.code || '').length == 0}"
|
||||
:style="(teaMenu == module.code && (teaSubMenu || '').length == 0) ? 'background: rgba(230, 230, 230, 0.45) !important;' : ''">
|
||||
<span v-if="(module.code || '').length > 0">
|
||||
<i class="window restore outline icon" v-if="module.icon == null"></i>
|
||||
<i class="ui icon" v-if="module.icon != null" :class="module.icon"></i>
|
||||
<span>{{module.name}}</span>
|
||||
@@ -131,14 +131,14 @@
|
||||
|
||||
<!-- 右侧主操作栏 -->
|
||||
<div class="main"
|
||||
:class="{'without-menu':teaSubMenus.menus == null || teaSubMenus.menus.length == 0 || (teaSubMenus.menus.length == 1 && teaSubMenus.menus[0].alwaysActive), 'without-secondary-menu':teaSubMenus.alwaysMenu == null || teaSubMenus.alwaysMenu.items.length <= 1, 'without-footer':!teaShowPageFooter}"
|
||||
:class="{'without-menu':teaSubMenus == null || teaSubMenus.menus == null || teaSubMenus.menus.length == 0 || (teaSubMenus.menus.length == 1 && teaSubMenus.menus[0].alwaysActive), 'without-secondary-menu':teaSubMenus == null || teaSubMenus.alwaysMenu == null || teaSubMenus.alwaysMenu.items == null || teaSubMenus.alwaysMenu.items.length <= 1, 'without-footer':!teaShowPageFooter}"
|
||||
v-cloak="">
|
||||
<!-- 操作菜单 -->
|
||||
<div class="ui top menu tabular tab-menu small" v-if="teaTabbar.length > 0">
|
||||
<div class="ui top menu tabular tab-menu small" v-if="teaTabbar != null && teaTabbar.length > 0">
|
||||
<a class="item" v-for="item in teaTabbar" :class="{'active':item.active,right:item.right}"
|
||||
:href="item.url">
|
||||
<var>{{item.name}}<span v-if="item.subName.length > 0">({{item.subName}})</span><i
|
||||
class="icon small" :class="item.icon" v-if="item.icon != null && item.icon.length > 0"></i>
|
||||
<var>{{item.name}}<span v-if="(item.subName || '').length > 0">({{item.subName}})</span><i
|
||||
class="icon small" :class="item.icon" v-if="item.icon != null && (item.icon || '').length > 0"></i>
|
||||
</var>
|
||||
</a>
|
||||
</div>
|
||||
@@ -149,11 +149,11 @@
|
||||
|
||||
<!-- 底部 -->
|
||||
<div id="footer" class="ui menu inverted light-blue borderless small"
|
||||
v-if="teaShowPageFooter && teaPageFooterHTML.length == 0" v-cloak>
|
||||
v-if="teaShowPageFooter && (teaPageFooterHTML || '').length == 0" v-cloak>
|
||||
<a class="item" title="点击进入检查版本更新页面">{{teaName}} v{{teaVersion}}</a>
|
||||
</div>
|
||||
<div id="footer" class="ui menu inverted light-blue borderless small"
|
||||
v-if="teaShowPageFooter && teaPageFooterHTML.length > 0" v-html="teaPageFooterHTML" v-cloak> </div>
|
||||
v-if="teaShowPageFooter && (teaPageFooterHTML || '').length > 0" v-html="teaPageFooterHTML" v-cloak> </div>
|
||||
</div>
|
||||
|
||||
{$echo "footer"}
|
||||
|
||||
4
EdgeUser/web/views/@default/httpdns/apps/@menu.html
Normal file
4
EdgeUser/web/views/@default/httpdns/apps/@menu.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<first-menu>
|
||||
<menu-item href="/httpdns/apps" code="index">应用列表</menu-item>
|
||||
<a href="/httpdns/apps/create" class="item">[添加应用]</a>
|
||||
</first-menu>
|
||||
134
EdgeUser/web/views/@default/httpdns/apps/appSettings.html
Normal file
134
EdgeUser/web/views/@default/httpdns/apps/appSettings.html
Normal file
@@ -0,0 +1,134 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
{$template "/left_menu_with_menu"}
|
||||
|
||||
<style>
|
||||
.httpdns-mini-action {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-left: .55em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.httpdns-mini-action:hover {
|
||||
color: #1e70bf;
|
||||
}
|
||||
|
||||
.httpdns-mini-action .icon {
|
||||
margin-right: 0 !important;
|
||||
font-size: .92em !important;
|
||||
}
|
||||
|
||||
.httpdns-note.comment {
|
||||
color: #8f9aa6 !important;
|
||||
font-size: 12px;
|
||||
margin-top: .45em !important;
|
||||
}
|
||||
|
||||
.httpdns-auth-table td.title {
|
||||
width: 260px !important;
|
||||
min-width: 260px !important;
|
||||
white-space: nowrap;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.httpdns-secret-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .35em;
|
||||
}
|
||||
|
||||
.httpdns-mini-icon {
|
||||
color: #8c96a3 !important;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.httpdns-mini-icon:hover {
|
||||
color: #5e6c7b !important;
|
||||
}
|
||||
|
||||
.httpdns-state-on {
|
||||
color: #21ba45;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.httpdns-state-off {
|
||||
color: #8f9aa6;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="right-box with-menu">
|
||||
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
|
||||
<csrf-token></csrf-token>
|
||||
<input type="hidden" name="appId" :value="app.id" />
|
||||
|
||||
<table class="ui table selectable definition" v-show="activeSection == 'basic'">
|
||||
<tr>
|
||||
<td class="title">应用启用</td>
|
||||
<td>
|
||||
<checkbox name="appStatus" value="1" v-model="settings.appStatus"></checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">SNI 防护配置</td>
|
||||
<td>
|
||||
<span class="green">隐匿 SNI</span>
|
||||
<p class="comment httpdns-note">当前默认采用隐匿 SNI 策略,避免在 TLS 握手阶段暴露业务域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="ui table selectable definition httpdns-auth-table" v-show="activeSection == 'auth'">
|
||||
<tr>
|
||||
<td class="title">App ID</td>
|
||||
<td>
|
||||
<code>{{settings.appId}}</code>
|
||||
<a href="" class="httpdns-mini-icon" title="复制 App ID" @click.prevent="copySecret(settings.appId, 'App ID')"><i class="copy outline icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">主服务域名</td>
|
||||
<td>
|
||||
<code v-if="settings.primaryServiceDomain && settings.primaryServiceDomain.length > 0">{{settings.primaryServiceDomain}}</code>
|
||||
<span class="grey" v-else>未配置</span>
|
||||
<a v-if="settings.primaryServiceDomain && settings.primaryServiceDomain.length > 0" href="" class="httpdns-mini-icon" title="复制主服务域名" @click.prevent="copySecret(settings.primaryServiceDomain, '主服务域名')"><i class="copy outline icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">备用服务域名</td>
|
||||
<td>
|
||||
<code v-if="settings.backupServiceDomain && settings.backupServiceDomain.length > 0">{{settings.backupServiceDomain}}</code>
|
||||
<span class="grey" v-else>未配置</span>
|
||||
<a v-if="settings.backupServiceDomain && settings.backupServiceDomain.length > 0" href="" class="httpdns-mini-icon" title="复制备用服务域名" @click.prevent="copySecret(settings.backupServiceDomain, '备用服务域名')"><i class="copy outline icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">请求验签</td>
|
||||
<td>
|
||||
<span :class="settings.signEnabled ? 'httpdns-state-on' : 'httpdns-state-off'">{{settings.signEnabled ? "已开启" : "已关闭"}}</span>
|
||||
<a href="" class="ui mini button basic" style="margin-left: .8em;" @click.prevent="toggleSignEnabled">{{settings.signEnabled ? "关闭请求验签" : "开启请求验签"}}</a>
|
||||
<p class="comment httpdns-note">打开后,服务端会对请求进行签名校验。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">加签 Secret</td>
|
||||
<td>
|
||||
<div class="httpdns-secret-line">
|
||||
<code>{{signSecretVisible ? settings.signSecretPlain : settings.signSecretMasked}}</code>
|
||||
<a href="" class="httpdns-mini-icon" @click.prevent="signSecretVisible = !signSecretVisible" :title="signSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="signSecretVisible ? 'eye slash' : 'eye'"></i></a>
|
||||
<a href="" class="httpdns-mini-icon" title="复制加签 Secret" @click.prevent="copySecret(settings.signSecretPlain, '加签 Secret')"><i class="copy outline icon"></i></a>
|
||||
<a href="" class="httpdns-mini-icon" title="重置加签 Secret" @click.prevent="resetSignSecret"><i class="redo icon"></i></a>
|
||||
</div>
|
||||
<p class="comment httpdns-note">最近更新:{{settings.signSecretUpdatedAt}}</p>
|
||||
<p class="comment httpdns-note" v-if="!settings.signEnabled">请求验签已关闭,当前不使用加签 Secret。</p>
|
||||
<p class="comment httpdns-note">用于生成鉴权接口的安全密钥。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
</div>
|
||||
95
EdgeUser/web/views/@default/httpdns/apps/appSettings.js
Normal file
95
EdgeUser/web/views/@default/httpdns/apps/appSettings.js
Normal file
@@ -0,0 +1,95 @@
|
||||
Tea.context(function () {
|
||||
this.activeSection = this.activeSection || "basic";
|
||||
this.success = NotifyReloadSuccess("保存成功");
|
||||
this.signSecretVisible = false;
|
||||
|
||||
this.toggleSignEnabled = function () {
|
||||
let that = this;
|
||||
let targetIsOn = !this.settings.signEnabled;
|
||||
|
||||
if (targetIsOn) {
|
||||
teaweb.confirm("html:开启后,服务端会对解析请求进行签名鉴权,<span class='red'>未签名、签名无效或过期的请求都会解析失败</span>,确认开启吗?", 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:关闭后,服务端不会对解析请求进行签名鉴权,可能<span class='red'>存在被刷风险</span>,确认关闭吗?", 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
16
EdgeUser/web/views/@default/httpdns/apps/create.html
Normal file
16
EdgeUser/web/views/@default/httpdns/apps/create.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
|
||||
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
|
||||
<csrf-token></csrf-token>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">应用名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="name" maxlength="64" ref="focus" />
|
||||
<p class="comment">为该 HTTPDNS 应用设置一个便于识别的名称。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
BIN
EdgeUser/web/views/@default/httpdns/apps/create.js
Normal file
BIN
EdgeUser/web/views/@default/httpdns/apps/create.js
Normal file
Binary file not shown.
55
EdgeUser/web/views/@default/httpdns/apps/customRecords.html
Normal file
55
EdgeUser/web/views/@default/httpdns/apps/customRecords.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{$layout}
|
||||
|
||||
<style>
|
||||
.httpdns-col-ttl {
|
||||
width: 72px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.httpdns-col-actions {
|
||||
width: 130px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<second-menu>
|
||||
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
|
||||
<span class="item disabled" style="padding-left: 0; padding-right: 0">»</span>
|
||||
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">域名列表</a>
|
||||
<span class="item disabled" style="padding-left: 0; padding-right: 0">»</span>
|
||||
<div class="item"><strong>{{domain.name}}</strong></div>
|
||||
<a href="" class="item" @click.prevent="createRecord" :class="{disabled: !domain || !domain.id}">创建规则</a>
|
||||
</second-menu>
|
||||
|
||||
<table class="ui table selectable celled" v-if="records.length > 0" style="margin-top: .8em;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>线路</th>
|
||||
<th>规则名称</th>
|
||||
<th>解析记录</th>
|
||||
<th class="httpdns-col-ttl">TTL</th>
|
||||
<th class="width10">状态</th>
|
||||
<th class="httpdns-col-actions">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="record in records">
|
||||
<td>{{record.lineText}}</td>
|
||||
<td>{{record.ruleName}}</td>
|
||||
<td>{{record.recordValueText}}</td>
|
||||
<td class="httpdns-col-ttl">{{record.ttl}}s</td>
|
||||
<td>
|
||||
<span class="green" v-if="record.isOn">已启用</span>
|
||||
<span class="grey" v-else>已停用</span>
|
||||
</td>
|
||||
<td class="httpdns-col-actions">
|
||||
<a href="" @click.prevent="editRecord(record.id)">编辑</a> |
|
||||
<a href="" @click.prevent="toggleRecord(record)">{{record.isOn ? "停用" : "启用"}}</a> |
|
||||
<a href="" @click.prevent="deleteRecord(record.id)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<not-found-box v-if="!domain || !domain.id">当前应用暂无可用域名,请先到域名管理中添加域名。</not-found-box>
|
||||
<not-found-box v-else-if="records.length == 0">当前域名还没有自定义解析规则。</not-found-box>
|
||||
60
EdgeUser/web/views/@default/httpdns/apps/customRecords.js
Normal file
60
EdgeUser/web/views/@default/httpdns/apps/customRecords.js
Normal file
@@ -0,0 +1,60 @@
|
||||
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: "42em",
|
||||
height: "33em",
|
||||
title: "新增自定义解析规则",
|
||||
callback: function () {
|
||||
teaweb.success("保存成功", function () {
|
||||
teaweb.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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: "42em",
|
||||
height: "33em",
|
||||
title: "编辑自定义解析规则",
|
||||
callback: function () {
|
||||
teaweb.success("保存成功", function () {
|
||||
teaweb.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
{$layout "layout_popup"}
|
||||
|
||||
<style>
|
||||
.httpdns-inline-actions {
|
||||
margin-top: .6em;
|
||||
}
|
||||
|
||||
.httpdns-inline-actions .count {
|
||||
color: #8f9aa6;
|
||||
margin-left: .4em;
|
||||
}
|
||||
|
||||
.httpdns-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
margin-bottom: .45em;
|
||||
}
|
||||
|
||||
.httpdns-row .field {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.httpdns-row .field.flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.httpdns-line-row {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: .5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3 v-if="isEditing">编辑自定义解析规则</h3>
|
||||
<h3 v-else>新增自定义解析规则</h3>
|
||||
|
||||
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
|
||||
<csrf-token></csrf-token>
|
||||
<input type="hidden" name="appId" :value="app.id" />
|
||||
<input type="hidden" name="domainId" :value="domain.id" />
|
||||
<input type="hidden" name="domain" :value="record.domain" />
|
||||
<input type="hidden" name="recordId" :value="record.id" />
|
||||
<input type="hidden" name="recordItemsJSON" :value="JSON.stringify(recordItems)" />
|
||||
|
||||
<table class="ui table definition selectable small">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">域名 *</td>
|
||||
<td><strong>{{record.domain}}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">规则名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="ruleName" maxlength="50" v-model="record.ruleName" ref="focus" />
|
||||
<p class="comment">例如:上海电信灰度-v2</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">线路</td>
|
||||
<td>
|
||||
<div class="httpdns-line-row">
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineScope" v-model="record.lineScope" @change="onLineScopeChange">
|
||||
<option value="china">中国地区</option>
|
||||
<option value="overseas">境外</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineCarrier" v-if="record.lineScope == 'china'" v-model="record.lineCarrier">
|
||||
<option v-for="carrier in chinaCarriers" :value="carrier">{{carrier}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineRegion" v-if="record.lineScope == 'china'" v-model="record.lineRegion"
|
||||
@change="onChinaRegionChange">
|
||||
<option v-for="region in chinaRegions" :value="region">{{region}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineProvince" v-if="record.lineScope == 'china'" v-model="record.lineProvince">
|
||||
<option v-for="province in provinceOptions" :value="province">{{province}}</option>
|
||||
</select>
|
||||
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineContinent" v-if="record.lineScope == 'overseas'" v-model="record.lineContinent"
|
||||
@change="onContinentChange">
|
||||
<option v-for="continent in continents" :value="continent">{{continent}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineCountry" v-if="record.lineScope == 'overseas'" v-model="record.lineCountry">
|
||||
<option v-for="country in countryOptions" :value="country">{{country}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">解析记录值 *</td>
|
||||
<td>
|
||||
<div style="margin-bottom: 0.5em;">
|
||||
<div class="ui checkbox" style="margin-bottom: 0.5em;">
|
||||
<input type="checkbox" name="weightEnabled" value="1" v-model="record.weightEnabled" />
|
||||
<label>启用权重调度设置</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="httpdns-row" v-for="(item, index) in recordItems">
|
||||
<div class="field">
|
||||
<select class="ui dropdown auto-width" v-model="item.type">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field flex">
|
||||
<input type="text" placeholder="记录值" v-model="item.value" />
|
||||
</div>
|
||||
<div class="field" v-if="record.weightEnabled">
|
||||
<input type="text" style="width:7em;" placeholder="权重1-100" v-model="item.weight" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<a href="" @click.prevent="removeRecordItem(index)" title="删除"><i
|
||||
class="icon trash alternate outline"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="httpdns-inline-actions">
|
||||
<a href="" @click.prevent="addRecordItem" :class="{disabled: recordItems.length >= 10}">
|
||||
<i class="icon plus circle"></i>添加记录值
|
||||
</a>
|
||||
<span class="count">{{recordItems.length}}/10</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">TTL *</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" name="ttl" maxlength="5" style="width:8em;" v-model="record.ttl" />
|
||||
<span class="ui basic label">秒</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">状态</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="isOn" value="1" v-model="record.isOn" />
|
||||
<label>启用当前规则</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
@@ -0,0 +1,155 @@
|
||||
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.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.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();
|
||||
});
|
||||
12
EdgeUser/web/views/@default/httpdns/apps/delete.html
Normal file
12
EdgeUser/web/views/@default/httpdns/apps/delete.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{$layout}
|
||||
|
||||
<second-menu>
|
||||
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
|
||||
<span class="item disabled" style="padding-left: 0; padding-right: 0">»</span>
|
||||
<div class="item"><strong>删除应用</strong></div>
|
||||
</second-menu>
|
||||
|
||||
<div class="buttons-box">
|
||||
<button class="ui button red" type="button" @click.prevent="deleteApp(app.id)">删除当前应用</button>
|
||||
<p class="comment" style="margin-top: .8em;">包含{{domainCount}}域名</p>
|
||||
</div>
|
||||
16
EdgeUser/web/views/@default/httpdns/apps/delete.js
Normal file
16
EdgeUser/web/views/@default/httpdns/apps/delete.js
Normal file
@@ -0,0 +1,16 @@
|
||||
Tea.context(function () {
|
||||
this.deleteApp = function (appId) {
|
||||
let that = this;
|
||||
teaweb.confirm("确定要删除此应用吗?", function () {
|
||||
that.$post("/httpdns/apps/delete")
|
||||
.params({
|
||||
appId: appId
|
||||
})
|
||||
.success(function () {
|
||||
teaweb.success("删除成功", function () {
|
||||
window.location = "/httpdns/apps";
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
36
EdgeUser/web/views/@default/httpdns/apps/docs/android.md
Normal file
36
EdgeUser/web/views/@default/httpdns/apps/docs/android.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Android SDK
|
||||
|
||||
## 初始化
|
||||
|
||||
```java
|
||||
new InitConfig.Builder()
|
||||
.setContext(context)
|
||||
.setPrimaryServiceHost("httpdns-a.example.com")
|
||||
.setBackupServiceHost("httpdns-b.example.com")
|
||||
.setServicePort(443)
|
||||
.setSecretKey("your-sign-secret")
|
||||
.setEnableHttps(true)
|
||||
.buildFor("app1f1ndpo9");
|
||||
```
|
||||
|
||||
## 解析
|
||||
|
||||
```java
|
||||
HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(
|
||||
"api.business.com",
|
||||
RequestIpType.auto,
|
||||
null,
|
||||
null
|
||||
);
|
||||
```
|
||||
|
||||
## 官方业务适配器
|
||||
|
||||
```java
|
||||
HttpDnsHttpAdapter adapter = HttpDns.buildHttpClientAdapter(httpDnsService);
|
||||
HttpDnsAdapterResponse resp = adapter.execute(
|
||||
new HttpDnsAdapterRequest("GET", "https://api.business.com/v1/ping")
|
||||
);
|
||||
```
|
||||
|
||||
固定策略:IP 直连 + 空 SNI + Host=真实域名,不回退到带 SNI。
|
||||
35
EdgeUser/web/views/@default/httpdns/apps/docs/flutter.md
Normal file
35
EdgeUser/web/views/@default/httpdns/apps/docs/flutter.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Flutter SDK
|
||||
|
||||
## 初始化
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.init(
|
||||
appId: 'app1f1ndpo9',
|
||||
primaryServiceHost: 'httpdns-a.example.com',
|
||||
backupServiceHost: 'httpdns-b.example.com',
|
||||
servicePort: 443,
|
||||
secretKey: 'your-sign-secret',
|
||||
);
|
||||
await AliyunHttpdns.build();
|
||||
```
|
||||
|
||||
## 解析
|
||||
|
||||
```dart
|
||||
final result = await AliyunHttpdns.resolveHostSyncNonBlocking(
|
||||
'api.business.com',
|
||||
ipType: 'both',
|
||||
);
|
||||
```
|
||||
|
||||
## 官方业务适配器
|
||||
|
||||
```dart
|
||||
final adapter = AliyunHttpdns.createHttpAdapter();
|
||||
final resp = await adapter.request(
|
||||
Uri.parse('https://api.business.com/v1/ping'),
|
||||
method: 'GET',
|
||||
);
|
||||
```
|
||||
|
||||
固定策略:IP 直连 + 空 SNI + Host=真实域名,不回退到带 SNI。
|
||||
31
EdgeUser/web/views/@default/httpdns/apps/docs/ios.md
Normal file
31
EdgeUser/web/views/@default/httpdns/apps/docs/ios.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# iOS SDK
|
||||
|
||||
## 初始化
|
||||
|
||||
```objc
|
||||
HttpdnsEdgeService *service = [[HttpdnsEdgeService alloc]
|
||||
initWithAppId:@"app1f1ndpo9"
|
||||
primaryServiceHost:@"httpdns-a.example.com"
|
||||
backupServiceHost:@"httpdns-b.example.com"
|
||||
servicePort:443
|
||||
signSecret:@"your-sign-secret"];
|
||||
```
|
||||
|
||||
## 解析
|
||||
|
||||
```objc
|
||||
[service resolveHost:@"api.business.com" queryType:@"A" completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) {
|
||||
// result.ipv4s / result.ipv6s
|
||||
}];
|
||||
```
|
||||
|
||||
## 官方业务适配器
|
||||
|
||||
```objc
|
||||
NSURL *url = [NSURL URLWithString:@"https://api.business.com/v1/ping"];
|
||||
[service requestURL:url method:@"GET" headers:nil body:nil completion:^(NSData * _Nullable data, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
// handle
|
||||
}];
|
||||
```
|
||||
|
||||
固定策略:IP 直连 + 空 SNI + Host=真实域名,不回退到带 SNI。
|
||||
62
EdgeUser/web/views/@default/httpdns/apps/domains.html
Normal file
62
EdgeUser/web/views/@default/httpdns/apps/domains.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{$layout}
|
||||
|
||||
<div class="margin"></div>
|
||||
|
||||
<style>
|
||||
.httpdns-domains-table .httpdns-domains-op {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
<second-menu>
|
||||
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
|
||||
<span class="item disabled" style="padding-left: 0; padding-right: 0">»</span>
|
||||
<div class="item"><strong>域名列表</strong></div>
|
||||
<a href="" class="item" @click.prevent="bindDomain">创建域名</a>
|
||||
</second-menu>
|
||||
|
||||
<form class="ui form small" @submit.prevent="doSearch">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" v-model.trim="keywordInput" placeholder="按域名筛选..." />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button type="submit" class="ui button small">搜索</button>
|
||||
|
||||
<a href="" v-if="keyword && keyword.length > 0" @click.prevent="clearSearch">[清除条件]</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="ui table selectable celled httpdns-domains-table" v-if="filteredDomains().length > 0">
|
||||
<colgroup>
|
||||
<col style="width:60%;" />
|
||||
<col style="width:20%;" />
|
||||
<col style="width:20%;" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>服务域名</th>
|
||||
<th class="width10">规则策略</th>
|
||||
<th class="httpdns-domains-op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="domain in filteredDomains()">
|
||||
<td>
|
||||
<div><strong>{{domain.name}}</strong></div>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
:href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">{{domain.customRecordCount}}</a>
|
||||
</td>
|
||||
<td class="httpdns-domains-op">
|
||||
<a :href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">自定义解析</a>
|
||||
|
|
||||
<a href="" @click.prevent="deleteDomain(domain.id)">解绑</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<not-found-box v-if="!domains || domains.length == 0">当前应用尚未绑定域名。</not-found-box>
|
||||
<not-found-box v-if="domains && domains.length > 0 && filteredDomains().length == 0">没有匹配的域名。</not-found-box>
|
||||
72
EdgeUser/web/views/@default/httpdns/apps/domains.js
Normal file
72
EdgeUser/web/views/@default/httpdns/apps/domains.js
Normal file
@@ -0,0 +1,72 @@
|
||||
Tea.context(function () {
|
||||
if (typeof this.keywordInput == "undefined" || this.keywordInput == null) {
|
||||
this.keywordInput = "";
|
||||
}
|
||||
if (typeof this.keyword == "undefined" || this.keyword == null) {
|
||||
this.keyword = "";
|
||||
}
|
||||
|
||||
if (typeof this.domains == "undefined" || this.domains == null) {
|
||||
this.domains = [];
|
||||
}
|
||||
|
||||
this.keywordInput = String(this.keyword);
|
||||
|
||||
this.doSearch = function () {
|
||||
this.keyword = String(this.keywordInput || "").trim();
|
||||
};
|
||||
|
||||
this.clearSearch = function () {
|
||||
this.keywordInput = "";
|
||||
this.keyword = "";
|
||||
};
|
||||
|
||||
this.filteredDomains = function () {
|
||||
let domains = Array.isArray(this.domains) ? this.domains : [];
|
||||
let keyword = String(this.keyword || "").trim().toLowerCase();
|
||||
if (keyword.length == 0) {
|
||||
return domains;
|
||||
}
|
||||
return domains.filter(function (domain) {
|
||||
let name = (domain.name || "").toLowerCase();
|
||||
return name.indexOf(keyword) >= 0;
|
||||
});
|
||||
};
|
||||
|
||||
this.bindDomain = function () {
|
||||
teaweb.popup("/httpdns/apps/domains/createPopup?appId=" + this.app.id, {
|
||||
height: "24em",
|
||||
width: "46em",
|
||||
title: "添加域名",
|
||||
callback: function () {
|
||||
teaweb.success("保存成功", function () {
|
||||
teaweb.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteApp = function () {
|
||||
let that = this;
|
||||
teaweb.confirm("确定要删除当前应用吗?", function () {
|
||||
that.$post("/httpdns/apps/delete")
|
||||
.params({
|
||||
appId: that.app.id
|
||||
})
|
||||
.success(function () {
|
||||
window.location = "/httpdns/apps";
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteDomain = function (domainId) {
|
||||
let that = this;
|
||||
teaweb.confirm("确定要解绑这个域名吗?", function () {
|
||||
that.$post("/httpdns/apps/domains/delete")
|
||||
.params({
|
||||
domainId: domainId
|
||||
})
|
||||
.refresh();
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
{$layout "layout_popup"}
|
||||
|
||||
<h3>添加域名</h3>
|
||||
|
||||
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
|
||||
<csrf-token></csrf-token>
|
||||
<input type="hidden" name="appId" :value="app.id" />
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">域名 *</td>
|
||||
<td>
|
||||
<input type="text" name="domain" maxlength="255" placeholder="api.example.com" ref="focus" />
|
||||
<p class="comment">请输入完整 FQDN,例如 <code>api.example.com</code>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
71
EdgeUser/web/views/@default/httpdns/apps/index.html
Normal file
71
EdgeUser/web/views/@default/httpdns/apps/index.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
|
||||
<div class="ui margin"></div>
|
||||
|
||||
<style>
|
||||
.httpdns-apps-table .httpdns-apps-op {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<form method="get" class="ui form" action="/httpdns/apps">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" name="keyword" style="width:16em" placeholder="AppID 或应用名称..." :value="keyword" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button type="submit" class="ui button">查询</button>
|
||||
<a href="/httpdns/apps" v-if="keyword.length > 0">[清除条件]</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="ui message" v-if="apps.length == 0">暂时没有符合条件的 HTTPDNS 应用。</p>
|
||||
|
||||
<table class="ui table selectable celled httpdns-apps-table" v-if="apps.length > 0">
|
||||
<colgroup>
|
||||
<col style="width:32%;" />
|
||||
<col style="width:26%;" />
|
||||
<col style="width:12%;" />
|
||||
<col style="width:10%;" />
|
||||
<col style="width:20%;" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>应用名称</th>
|
||||
<th>AppID</th>
|
||||
<th class="center">绑定域名数</th>
|
||||
<th class="center">状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="app in apps">
|
||||
<td>
|
||||
<a :href="'/httpdns/apps/app?appId=' + app.id">
|
||||
<strong>
|
||||
<keyword :v-word="keyword">{{app.name}}</keyword>
|
||||
</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{app.appId}}</code>
|
||||
<copy-icon :text="app.appId"></copy-icon>
|
||||
</td>
|
||||
<td class="center"><a :href="'/httpdns/apps/domains?appId=' + app.id">{{app.domainCount}}</a></td>
|
||||
<td class="center">
|
||||
<label-on :v-is-on="app.isOn"></label-on>
|
||||
</td>
|
||||
<td class="httpdns-apps-op">
|
||||
<a :href="'/httpdns/apps/app?appId=' + app.id">域名管理</a> |
|
||||
<a :href="'/httpdns/apps/app/settings?appId=' + app.id">应用设置</a> |
|
||||
<a :href="'/httpdns/apps/sdk?appId=' + app.id">SDK集成</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="page" v-html="page"></div>
|
||||
</div>
|
||||
9
EdgeUser/web/views/@default/httpdns/apps/index.js
Normal file
9
EdgeUser/web/views/@default/httpdns/apps/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Tea.context(function () {
|
||||
if (typeof this.apps == "undefined") {
|
||||
this.apps = [];
|
||||
}
|
||||
|
||||
this.createApp = function () {
|
||||
window.location = "/httpdns/apps/create";
|
||||
};
|
||||
});
|
||||
78
EdgeUser/web/views/@default/httpdns/apps/policies.html
Normal file
78
EdgeUser/web/views/@default/httpdns/apps/policies.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
|
||||
<div class="ui margin"></div>
|
||||
|
||||
<h3 class="ui header">HTTPDNS 鍏ㄥ眬绛栫暐</h3>
|
||||
|
||||
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
|
||||
<csrf-token></csrf-token>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">鍏ㄥ眬榛樿瑙f瀽 TTL</td>
|
||||
<td><input type="text" name="defaultTTL" maxlength="8" v-model="policies.defaultTTL" /> 绉?/td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">鍏ㄥ眬榛樿 SNI 绛夌骇</td>
|
||||
<td>
|
||||
<select name="defaultSniPolicy" class="ui dropdown auto-width" v-model="policies.defaultSniPolicy">
|
||||
<option value="level1">level1锛堝浐瀹?SNI锛?/option>
|
||||
<option value="level2">level2锛堥殣鍖?SNI锛?/option>
|
||||
<option value="level3">level3锛圗CH锛?/option>
|
||||
</select>
|
||||
<div class="grey small" v-if="policies.defaultSniPolicy == 'level1'" style="margin-top:.5em;">
|
||||
level1 浠呬娇鐢ㄥ浐瀹?SNI锛屼笉鍚敤 ECS 鍜岃瘉涔︽牎楠岀瓥鐣ャ€? </div>
|
||||
<div class="grey small" v-else-if="policies.defaultSniPolicy == 'level2'" style="margin-top:.5em;">
|
||||
level2 浠呭惎鐢ㄩ殣鍖?SNI锛屼笉瑕佹眰閰嶇疆 ECS 涓庤瘉涔︾瓥鐣ャ€? </div>
|
||||
<div class="grey small" v-else style="margin-top:.5em;">
|
||||
level3 鍚敤 ECH锛屽缓璁悓鏃堕厤缃?ECS 涓庤瘉涔︽牎楠岀瓥鐣ャ€? </div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">鍏ㄥ眬闄嶇骇瓒呮椂</td>
|
||||
<td><input type="text" name="defaultFallbackMs" maxlength="8" v-model="policies.defaultFallbackMs" /> 姣</td>
|
||||
</tr>
|
||||
|
||||
<tr v-show="policies.defaultSniPolicy == 'level3'">
|
||||
<td class="title">全局 ECS 掩码策略</td>
|
||||
<td>
|
||||
<select name="ecsMode" class="ui dropdown auto-width" v-model="policies.ecsMode">
|
||||
<option value="off">鍏抽棴</option>
|
||||
<option value="auto">鑷姩</option>
|
||||
<option value="custom">鑷畾涔?/option>
|
||||
</select>
|
||||
<span v-if="policies.ecsMode == 'custom'">
|
||||
<span class="grey" style="margin-left:.8em;">IPv4 /</span>
|
||||
<input type="text" name="ecsIPv4Prefix" maxlength="3" v-model="policies.ecsIPv4Prefix" style="width:4.5em;" />
|
||||
<span class="grey" style="margin-left:.8em;">IPv6 /</span>
|
||||
<input type="text" name="ecsIPv6Prefix" maxlength="3" v-model="policies.ecsIPv6Prefix" style="width:4.5em;" />
|
||||
</span>
|
||||
<span v-else class="grey" style="margin-left:.8em;">仅在“自定义”模式下配置掩码。</span>
|
||||
|
||||
<input type="hidden" name="ecsIPv4Prefix" v-model="policies.ecsIPv4Prefix" v-if="policies.ecsMode != 'custom'" />
|
||||
<input type="hidden" name="ecsIPv6Prefix" v-model="policies.ecsIPv6Prefix" v-if="policies.ecsMode != 'custom'" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-show="policies.defaultSniPolicy == 'level3'">
|
||||
<td class="title">鍏ㄥ眬璇佷功鏍¢獙绛栫暐</td>
|
||||
<td>
|
||||
<span class="grey">证书指纹校验(Pinning)</span>
|
||||
<select name="pinningMode" class="ui dropdown auto-width" v-model="policies.pinningMode" style="margin-left:.4em;">
|
||||
<option value="off">鍏抽棴</option>
|
||||
<option value="report">瑙傚療妯″紡</option>
|
||||
<option value="enforce">寮哄埗鏍¢獙</option>
|
||||
</select>
|
||||
<span class="grey" style="margin-left:1.2em;">证书 SAN 域名校验</span>
|
||||
<select name="sanMode" class="ui dropdown auto-width" v-model="policies.sanMode" style="margin-left:.4em;">
|
||||
<option value="off">鍏抽棴</option>
|
||||
<option value="report">瑙傚療妯″紡</option>
|
||||
<option value="strict">涓ユ牸鏍¢獙</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
|
||||
40
EdgeUser/web/views/@default/httpdns/apps/policies.js
Normal file
40
EdgeUser/web/views/@default/httpdns/apps/policies.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
79
EdgeUser/web/views/@default/httpdns/apps/sdk.html
Normal file
79
EdgeUser/web/views/@default/httpdns/apps/sdk.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{$layout}
|
||||
|
||||
<style>
|
||||
.httpdns-sdk-cards {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.httpdns-sdk-cards > .card {
|
||||
min-height: 168px;
|
||||
}
|
||||
.httpdns-sdk-desc {
|
||||
margin-top: .6em;
|
||||
color: #5e6c7b;
|
||||
}
|
||||
.httpdns-sdk-meta {
|
||||
margin-top: .35em;
|
||||
color: #9aa6b2;
|
||||
font-size: 12px;
|
||||
}
|
||||
.httpdns-sdk-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .6em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.httpdns-sdk-actions .button {
|
||||
padding-left: .8em !important;
|
||||
padding-right: .8em !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<second-menu>
|
||||
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
|
||||
<span class="item disabled" style="padding-left: 0; padding-right: 0">»</span>
|
||||
<div class="item"><strong>SDK 集成</strong></div>
|
||||
</second-menu>
|
||||
|
||||
<div class="ui three stackable cards httpdns-sdk-cards">
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header"><i class="icon android green"></i> Android SDK</div>
|
||||
<div class="httpdns-sdk-meta">Java / Kotlin</div>
|
||||
<div class="description httpdns-sdk-desc">适用于 Android 客户端接入。</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<div class="httpdns-sdk-actions">
|
||||
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=android" @click="downloadSDK('android', $event)"><i class="icon download"></i> 下载 SDK</a>
|
||||
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=android" @click="downloadDoc('android', $event)"><i class="icon book"></i> 下载文档</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header"><i class="icon apple grey"></i> iOS SDK</div>
|
||||
<div class="httpdns-sdk-meta">Objective-C / Swift</div>
|
||||
<div class="description httpdns-sdk-desc">适用于 iOS 客户端接入。</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<div class="httpdns-sdk-actions">
|
||||
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=ios" @click="downloadSDK('ios', $event)"><i class="icon download"></i> 下载 SDK</a>
|
||||
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=ios" @click="downloadDoc('ios', $event)"><i class="icon book"></i> 下载文档</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header"><i class="icon mobile alternate blue"></i> Flutter SDK</div>
|
||||
<div class="httpdns-sdk-meta">Dart / Plugin</div>
|
||||
<div class="description httpdns-sdk-desc">适用于 Flutter 跨平台接入。</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<div class="httpdns-sdk-actions">
|
||||
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=flutter" @click="downloadSDK('flutter', $event)"><i class="icon download"></i> 下载 SDK</a>
|
||||
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=flutter" @click="downloadDoc('flutter', $event)"><i class="icon book"></i> 下载文档</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
123
EdgeUser/web/views/@default/httpdns/apps/sdk.js
Normal file
123
EdgeUser/web/views/@default/httpdns/apps/sdk.js
Normal file
@@ -0,0 +1,123 @@
|
||||
Tea.context(function () {
|
||||
this.downloadSDK = function (platform, event) {
|
||||
this.checkAndDownload(platform, "sdk", event)
|
||||
}
|
||||
|
||||
this.downloadDoc = function (platform, event) {
|
||||
this.checkAndDownload(platform, "doc", event)
|
||||
}
|
||||
|
||||
this.checkAndDownload = function (platform, type, event) {
|
||||
if (event != null && typeof event.preventDefault == "function") {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
this.$get("/httpdns/apps/sdk/check")
|
||||
.params({
|
||||
platform: platform,
|
||||
type: type
|
||||
})
|
||||
.success(function (resp) {
|
||||
let data = (resp != null && resp.data != null) ? resp.data : {}
|
||||
if (!data.exists) {
|
||||
teaweb.warn(data.message || "当前暂无可下载文件")
|
||||
return
|
||||
}
|
||||
if (typeof data.url == "string" && data.url.length > 0) {
|
||||
this.downloadByBlob(data.url, platform, type)
|
||||
return
|
||||
}
|
||||
teaweb.warn("下载地址生成失败,请稍后重试")
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.downloadByBlob = function (url, platform, type) {
|
||||
let defaultFileName = "httpdns-sdk-" + platform + (type == "doc" ? ".md" : ".zip")
|
||||
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open("GET", url, true)
|
||||
xhr.responseType = "blob"
|
||||
|
||||
xhr.onload = function () {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
teaweb.warn("下载失败(HTTP " + xhr.status + ")")
|
||||
return
|
||||
}
|
||||
if (xhr.status == 204) {
|
||||
teaweb.warn("下载被浏览器扩展或下载工具拦截,请暂时关闭相关扩展后重试")
|
||||
return
|
||||
}
|
||||
|
||||
let contentType = (xhr.getResponseHeader("Content-Type") || "").toLowerCase()
|
||||
if (contentType.indexOf("application/json") >= 0) {
|
||||
let reader = new FileReader()
|
||||
reader.onload = function () {
|
||||
try {
|
||||
let json = JSON.parse(reader.result)
|
||||
teaweb.warn((json && json.message) ? json.message : "下载失败,请稍后重试")
|
||||
} catch (_e) {
|
||||
teaweb.warn("下载失败,请稍后重试")
|
||||
}
|
||||
}
|
||||
reader.readAsText(xhr.response)
|
||||
return
|
||||
}
|
||||
|
||||
let disposition = xhr.getResponseHeader("Content-Disposition") || ""
|
||||
let fileName = xhr.getResponseHeader("X-SDK-Filename") || this.parseFileName(disposition) || defaultFileName
|
||||
if (xhr.response == null || xhr.response.size <= 0) {
|
||||
teaweb.warn("下载文件为空,请检查已上传 SDK 包是否完整")
|
||||
return
|
||||
}
|
||||
this.saveBlob(xhr.response, fileName)
|
||||
}.bind(this)
|
||||
|
||||
xhr.onerror = function () {
|
||||
teaweb.warn("下载失败,请检查网络后重试")
|
||||
}
|
||||
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
this.parseFileName = function (disposition) {
|
||||
if (typeof disposition != "string" || disposition.length == 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i)
|
||||
if (utf8Match != null && utf8Match.length > 1) {
|
||||
try {
|
||||
return decodeURIComponent(utf8Match[1])
|
||||
} catch (_e) {
|
||||
}
|
||||
}
|
||||
|
||||
let plainMatch = disposition.match(/filename="?([^";]+)"?/i)
|
||||
if (plainMatch != null && plainMatch.length > 1) {
|
||||
return plainMatch[1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
this.saveBlob = function (blob, fileName) {
|
||||
if (window.navigator != null && typeof window.navigator.msSaveOrOpenBlob == "function") {
|
||||
window.navigator.msSaveOrOpenBlob(blob, fileName)
|
||||
return
|
||||
}
|
||||
|
||||
let objectURL = window.URL.createObjectURL(blob)
|
||||
let a = document.createElement("a")
|
||||
a.style.display = "none"
|
||||
a.href = objectURL
|
||||
a.download = fileName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
setTimeout(function () {
|
||||
window.URL.revokeObjectURL(objectURL)
|
||||
if (a.parentNode != null) {
|
||||
a.parentNode.removeChild(a)
|
||||
}
|
||||
}, 30000)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
<first-menu>
|
||||
<menu-item href="/httpdns/resolveLogs" code="index">访问日志</menu-item>
|
||||
</first-menu>
|
||||
94
EdgeUser/web/views/@default/httpdns/resolveLogs/index.html
Normal file
94
EdgeUser/web/views/@default/httpdns/resolveLogs/index.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{$layout}
|
||||
|
||||
<div class="margin"></div>
|
||||
|
||||
<style>
|
||||
.httpdns-log-summary {
|
||||
color: #556070;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.httpdns-log-summary code {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<form method="get" action="/httpdns/resolveLogs" class="ui form small" autocomplete="off">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" name="clusterId" v-model="clusterId">
|
||||
<option value="">[HTTPDNS服务域名]</option>
|
||||
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.displayName}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<input type="text" name="appId" v-model="appId" placeholder="AppID" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<input type="text" name="domain" v-model="domain" placeholder="域名" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" name="status" v-model="status">
|
||||
<option value="">[状态]</option>
|
||||
<option value="success">解析成功</option>
|
||||
<option value="failed">解析失败</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<input type="text" name="keyword" v-model="keyword" placeholder="应用/域名/IP/结果IP" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button type="submit" class="ui button small">查询</button>
|
||||
</div>
|
||||
<div class="ui field"
|
||||
v-if="clusterId.toString().length > 0 || appId.length > 0 || domain.length > 0 || status.length > 0 || keyword.length > 0">
|
||||
<a href="/httpdns/resolveLogs">[清除条件]</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="margin"></div>
|
||||
|
||||
<not-found-box v-if="resolveLogs.length == 0">暂时还没有访问日志。</not-found-box>
|
||||
|
||||
<div v-if="resolveLogs.length > 0">
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="ui table selectable celled">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTPDNS服务域名</th>
|
||||
<th>域名</th>
|
||||
<th>类型</th>
|
||||
<th>概要</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in resolveLogs">
|
||||
<td>{{log.serviceDomain}}</td>
|
||||
<td>{{log.domain}}</td>
|
||||
<td>{{log.query}}</td>
|
||||
<td>
|
||||
<div class="httpdns-log-summary">
|
||||
{{log.time}}
|
||||
| {{log.appName}} (<code>{{log.appId}}</code>)
|
||||
| <code>{{log.clientIp}}</code>
|
||||
| {{log.os}}/{{log.sdkVersion}}
|
||||
| {{log.query}} {{log.domain}} ->
|
||||
<code v-if="log.ips && log.ips.length > 0">{{log.ips}}</code>
|
||||
<span class="grey" v-else>[无记录]</span>
|
||||
|
|
||||
<span class="green" v-if="log.status == 'success'"><strong>成功</strong></span>
|
||||
<span class="red" v-else><strong>失败</strong></span>
|
||||
<span class="grey" v-if="log.errorCode != 'none'">({{log.errorCode}})</span>
|
||||
| {{log.costMs}}ms
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
4
EdgeUser/web/views/@default/httpdns/sandbox/@menu.html
Normal file
4
EdgeUser/web/views/@default/httpdns/sandbox/@menu.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- 左侧菜单由 Go Backend 自动生成注入,此处加首行子菜单使其符合标准平台样式 -->
|
||||
<first-menu>
|
||||
<menu-item href="/httpdns/sandbox" code="index">解析测试</menu-item>
|
||||
</first-menu>
|
||||
147
EdgeUser/web/views/@default/httpdns/sandbox/index.html
Normal file
147
EdgeUser/web/views/@default/httpdns/sandbox/index.html
Normal file
@@ -0,0 +1,147 @@
|
||||
{$layout}
|
||||
|
||||
<div class="ui margin"></div>
|
||||
|
||||
<div>
|
||||
<div class="ui grid stackable">
|
||||
<!-- Left: 解析测试 -->
|
||||
<div class="six wide column">
|
||||
<div class="ui segment">
|
||||
<h4 class="ui header" style="margin-top:0;">解析测试</h4>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>目标应用 *</label>
|
||||
<select class="ui dropdown" name="appId" v-model="request.appId" @change="onAppChanged">
|
||||
<option value="">[请选择应用]</option>
|
||||
<option v-for="app in apps" :value="app.appId">{{app.name}} ({{app.appId}})</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>HTTPDNS服务域名 *</label>
|
||||
<select class="ui dropdown" name="clusterId" v-model="request.clusterId">
|
||||
<option value="">[请选择HTTPDNS服务域名]</option>
|
||||
<option v-for="cluster in currentClusters" :value="String(cluster.id)">{{cluster.displayName || cluster.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>解析域名 *</label>
|
||||
<select class="ui dropdown" name="domain" v-model="request.domain">
|
||||
<option value="">[请选择域名]</option>
|
||||
<option v-for="domain in currentDomains" :value="domain">{{domain}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>模拟客户端 IP</label>
|
||||
<input type="text" v-model="request.clientIp" placeholder="留空使用当前 IP" />
|
||||
<p class="comment">用于测试 ECS 掩码与区域调度效果。</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>解析类型</label>
|
||||
<div class="ui buttons mini">
|
||||
<button class="ui button" :class="{primary: request.qtype=='A'}"
|
||||
@click.prevent="request.qtype='A'" type="button">A</button>
|
||||
<button class="ui button" :class="{primary: request.qtype=='AAAA'}"
|
||||
@click.prevent="request.qtype='AAAA'" type="button">AAAA</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<button class="ui button primary" type="button" @click.prevent="sendTestRequest"
|
||||
:class="{loading: isRequesting, disabled: isRequesting}">
|
||||
<i class="icon search"></i> 在线解析
|
||||
</button>
|
||||
<button class="ui button basic" type="button" @click.prevent="resetForm">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: 解析结果 -->
|
||||
<div class="ten wide column">
|
||||
<!-- Empty state -->
|
||||
<div v-show="!response.hasResult && !isRequesting" class="ui segment placeholder">
|
||||
<p class="grey" style="text-align:center;">请在左侧配置参数后点击「在线解析」。</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-show="isRequesting" class="ui segment" style="min-height:200px;">
|
||||
<div class="ui active dimmer">
|
||||
<div class="ui text loader">解析中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div v-show="response.hasResult && !isRequesting">
|
||||
<!-- Status banner -->
|
||||
<div class="ui message small" :class="{positive: response.code === 0, negative: response.code !== 0}">
|
||||
<div class="header">
|
||||
<span v-if="response.code === 0"><i class="icon check circle"></i> 解析成功</span>
|
||||
<span v-else><i class="icon times circle"></i> 解析失败 ({{response.code}})</span>
|
||||
</div>
|
||||
<p v-if="response.code !== 0">{{response.message}}</p>
|
||||
<p class="grey small" style="margin-top:.3em;">Request ID: <code>{{response.requestId}}</code></p>
|
||||
</div>
|
||||
|
||||
<!-- Result details -->
|
||||
<div v-if="response.code === 0 && response.data" style="margin-top: 1em;">
|
||||
<h5 class="ui header">解析结果</h5>
|
||||
<div class="ui segment" style="margin-top:.4em;">
|
||||
<div style="font-weight: 600; margin-bottom: .5em;">请求URL</div>
|
||||
<div style="word-break: break-all;">
|
||||
<code>{{response.data.request_url || '-'}}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="ui header" style="margin-top:1.2em;">解析结果详情</h5>
|
||||
<div class="ui three column stackable grid" style="margin-top:0;">
|
||||
<div class="column">
|
||||
<div class="ui segment">
|
||||
<div class="grey">客户端 IP</div>
|
||||
<div style="margin-top:.4em;">
|
||||
<code>{{response.data.client_ip || request.clientIp || '-'}}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ui segment">
|
||||
<div class="grey">地区</div>
|
||||
<div style="margin-top:.4em;">{{response.data.client_region || '-'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ui segment">
|
||||
<div class="grey">线路</div>
|
||||
<div style="margin-top:.4em;">{{response.data.line_name || '-'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP results table -->
|
||||
<h5 class="ui header" style="margin-top:1.2em;">解析记录</h5>
|
||||
<table class="ui table celled compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>解析域名</th>
|
||||
<th>解析类型</th>
|
||||
<th>IP地址</th>
|
||||
<th>TTL</th>
|
||||
<th>地区</th>
|
||||
<th>线路</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in response.resultRows">
|
||||
<td v-if="idx === 0" :rowspan="response.resultRows.length">{{row.domain ||
|
||||
request.domain}}</td>
|
||||
<td>{{row.type || request.qtype}}</td>
|
||||
<td><code>{{row.ip}}</code></td>
|
||||
<td>{{row.ttl}}s</td>
|
||||
<td>{{row.region || '-'}}</td>
|
||||
<td>{{row.line || '-'}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
161
EdgeUser/web/views/@default/httpdns/sandbox/index.js
Normal file
161
EdgeUser/web/views/@default/httpdns/sandbox/index.js
Normal file
@@ -0,0 +1,161 @@
|
||||
Tea.context(function () {
|
||||
this.newRequest = function () {
|
||||
return {
|
||||
appId: "",
|
||||
clusterId: "",
|
||||
domain: "",
|
||||
clientIp: "",
|
||||
qtype: "A"
|
||||
}
|
||||
}
|
||||
|
||||
this.request = this.newRequest()
|
||||
|
||||
this.response = {
|
||||
hasResult: false,
|
||||
code: -1,
|
||||
message: "",
|
||||
data: null,
|
||||
requestId: "",
|
||||
resultRows: []
|
||||
}
|
||||
|
||||
this.isRequesting = false
|
||||
this.currentDomains = []
|
||||
this.currentClusters = []
|
||||
|
||||
if (typeof this.apps === "undefined" || this.apps == null) {
|
||||
this.apps = []
|
||||
}
|
||||
if (typeof this.clusters === "undefined" || this.clusters == null) {
|
||||
this.clusters = []
|
||||
}
|
||||
|
||||
this.onAppChanged = function () {
|
||||
let selectedApp = null
|
||||
for (let i = 0; i < this.apps.length; i++) {
|
||||
if (this.apps[i].appId === this.request.appId) {
|
||||
selectedApp = this.apps[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedApp == null) {
|
||||
this.currentDomains = []
|
||||
this.currentClusters = []
|
||||
this.request.domain = ""
|
||||
this.request.clusterId = ""
|
||||
return
|
||||
}
|
||||
|
||||
this.currentDomains = Array.isArray(selectedApp.domains) ? selectedApp.domains : []
|
||||
|
||||
if (this.currentDomains.length > 0) {
|
||||
if (this.currentDomains.indexOf(this.request.domain) < 0) {
|
||||
this.request.domain = this.currentDomains[0]
|
||||
}
|
||||
} else {
|
||||
this.request.domain = ""
|
||||
}
|
||||
|
||||
let primaryClusterId = (typeof selectedApp.primaryClusterId !== "undefined" && selectedApp.primaryClusterId !== null) ? Number(selectedApp.primaryClusterId) : 0
|
||||
let backupClusterId = (typeof selectedApp.backupClusterId !== "undefined" && selectedApp.backupClusterId !== null) ? Number(selectedApp.backupClusterId) : 0
|
||||
|
||||
let allowed = []
|
||||
for (let i = 0; i < this.clusters.length; i++) {
|
||||
let cluster = this.clusters[i]
|
||||
let clusterId = Number(cluster.id)
|
||||
if (clusterId <= 0) {
|
||||
continue
|
||||
}
|
||||
if (clusterId === primaryClusterId || clusterId === backupClusterId) {
|
||||
allowed.push(cluster)
|
||||
}
|
||||
}
|
||||
this.currentClusters = allowed
|
||||
|
||||
if (allowed.length > 0) {
|
||||
if (!allowed.some((c) => String(c.id) === String(this.request.clusterId))) {
|
||||
this.request.clusterId = String(allowed[0].id)
|
||||
}
|
||||
} else {
|
||||
this.request.clusterId = ""
|
||||
}
|
||||
}
|
||||
|
||||
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 = Array.isArray(data.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.clusterId.length === 0) {
|
||||
teaweb.warn("当前应用未绑定可用的 HTTPDNS 服务域名,请先在应用设置中配置主/备集群")
|
||||
return
|
||||
}
|
||||
if (this.request.domain.length === 0) {
|
||||
teaweb.warn("请选择要解析的域名")
|
||||
return
|
||||
}
|
||||
|
||||
this.isRequesting = true
|
||||
this.response.hasResult = false
|
||||
|
||||
let payload = Object.assign({}, this.request)
|
||||
|
||||
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.currentDomains = []
|
||||
this.currentClusters = []
|
||||
this.response = {
|
||||
hasResult: false,
|
||||
code: -1,
|
||||
message: "",
|
||||
data: null,
|
||||
requestId: "",
|
||||
resultRows: []
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user