feat: sync httpdns sdk/platform updates without large binaries

This commit is contained in:
robin
2026-03-04 17:59:14 +08:00
parent 853897a6f8
commit 532891fad0
700 changed files with 6096 additions and 2712 deletions

View File

@@ -22,7 +22,14 @@
"Bash(where protoc:*)",
"Bash(/c/Users/robin/AppData/Local/Temp/protoc-install/bin/protoc.exe:*)",
"Bash(protoc-gen-go:*)",
"Bash(cp:*)"
"Bash(cp:*)",
"Bash(ls:*)",
"Bash(choco list --local-only)",
"Bash(scoop list)",
"Bash(go install:*)",
"Bash(go env:*)",
"Bash(xargs:*)",
"Bash(sed:*)"
]
}
}

6
.gitignore vendored
View File

@@ -5,3 +5,9 @@ deploy/fluent-bit/logs.db
deploy/fluent-bit/logs.db-shm
deploy/fluent-bit/logs.db-wal
deploy/fluent-bit/storage/
pkg/
.claude
# Local large build artifacts
EdgeAdmin/edge-admin.exe
EdgeAPI/deploy/edge-node-linux-amd64-v1.4.9.zip

View File

@@ -3,5 +3,10 @@
# generate 'internal/setup/sql.json' file
CWD="$(dirname "$0")"
SQL_JSON="${CWD}/../internal/setup/sql.json"
if [ -f "$SQL_JSON" ]; then
echo "sql.json already exists, skipping sql-dump (delete it manually to regenerate)"
else
go run "${CWD}"/../cmd/sql-dump/main.go -dir="${CWD}"
fi

View File

@@ -12,6 +12,8 @@ import (
)
func main() {
Tea.Env = "prod"
db, err := dbs.Default()
if err != nil {
fmt.Println("[ERROR]" + err.Error())

View File

@@ -1,7 +1,7 @@
package teaconst
const (
Version = "1.4.8" //1.3.9
Version = "1.4.9" //1.3.9
ProductName = "Edge API"
ProcessName = "edge-api"
@@ -17,6 +17,6 @@ const (
// 其他节点版本号,用来检测是否有需要升级的节点
NodeVersion = "1.4.8" //1.3.8.2
NodeVersion = "1.4.9" //1.3.8.2
)

View File

@@ -4,8 +4,8 @@
package teaconst
const (
DNSNodeVersion = "1.4.8" //1.3.8.2
UserNodeVersion = "1.4.8" //1.3.8.2
DNSNodeVersion = "1.4.9" //1.3.8.2
UserNodeVersion = "1.4.9" //1.3.8.2
ReportNodeVersion = "0.1.5"
DefaultMaxNodes int32 = 50

View File

@@ -34,7 +34,7 @@ func init() {
})
}
func (this *HTTPDNSClusterDAO) CreateCluster(tx *dbs.Tx, name string, serviceDomain string, defaultTTL int32, fallbackTimeoutMs int32, installDir string, tlsPolicyJSON []byte, isOn bool, isDefault bool, autoRemoteStart bool, accessLogIsOn bool) (int64, error) {
func (this *HTTPDNSClusterDAO) CreateCluster(tx *dbs.Tx, name string, serviceDomain string, defaultTTL int32, fallbackTimeoutMs int32, installDir string, tlsPolicyJSON []byte, isOn bool, isDefault bool, autoRemoteStart bool, accessLogIsOn bool, timeZone string) (int64, error) {
if isDefault {
err := this.Query(tx).
State(HTTPDNSClusterStateEnabled).
@@ -55,6 +55,7 @@ func (this *HTTPDNSClusterDAO) CreateCluster(tx *dbs.Tx, name string, serviceDom
op.IsDefault = isDefault
op.AutoRemoteStart = autoRemoteStart
op.AccessLogIsOn = accessLogIsOn
op.TimeZone = timeZone
op.CreatedAt = time.Now().Unix()
op.UpdatedAt = time.Now().Unix()
op.State = HTTPDNSClusterStateEnabled
@@ -68,7 +69,7 @@ func (this *HTTPDNSClusterDAO) CreateCluster(tx *dbs.Tx, name string, serviceDom
return types.Int64(op.Id), nil
}
func (this *HTTPDNSClusterDAO) UpdateCluster(tx *dbs.Tx, clusterId int64, name string, serviceDomain string, defaultTTL int32, fallbackTimeoutMs int32, installDir string, tlsPolicyJSON []byte, isOn bool, isDefault bool, autoRemoteStart bool, accessLogIsOn bool) error {
func (this *HTTPDNSClusterDAO) UpdateCluster(tx *dbs.Tx, clusterId int64, name string, serviceDomain string, defaultTTL int32, fallbackTimeoutMs int32, installDir string, tlsPolicyJSON []byte, isOn bool, isDefault bool, autoRemoteStart bool, accessLogIsOn bool, timeZone string) error {
if isDefault {
err := this.Query(tx).
State(HTTPDNSClusterStateEnabled).
@@ -91,6 +92,7 @@ func (this *HTTPDNSClusterDAO) UpdateCluster(tx *dbs.Tx, clusterId int64, name s
op.IsDefault = isDefault
op.AutoRemoteStart = autoRemoteStart
op.AccessLogIsOn = accessLogIsOn
op.TimeZone = timeZone
op.UpdatedAt = time.Now().Unix()
if len(tlsPolicyJSON) > 0 {
op.TLSPolicy = tlsPolicyJSON

View File

@@ -15,6 +15,7 @@ type HTTPDNSCluster struct {
TLSPolicy dbs.JSON `field:"tlsPolicy"` // TLS策略
AutoRemoteStart bool `field:"autoRemoteStart"` // 自动远程启动
AccessLogIsOn bool `field:"accessLogIsOn"` // 访问日志是否开启
TimeZone string `field:"timeZone"` // 时区
CreatedAt uint64 `field:"createdAt"` // 创建时间
UpdatedAt uint64 `field:"updatedAt"` // 修改时间
State uint8 `field:"state"` // 记录状态
@@ -32,6 +33,7 @@ type HTTPDNSClusterOperator struct {
TLSPolicy any // TLS策略
AutoRemoteStart any // 自动远程启动
AccessLogIsOn any // 访问日志是否开启
TimeZone any // 时区
CreatedAt any // 创建时间
UpdatedAt any // 修改时间
State any // 记录状态

View File

@@ -2,6 +2,7 @@ package models
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
@@ -12,8 +13,8 @@ import (
)
const (
HTTPDNSNodeStateEnabled = 1 // 已启用
HTTPDNSNodeStateDisabled = 0 // 已禁用
HTTPDNSNodeStateEnabled = 1
HTTPDNSNodeStateDisabled = 0
)
type HTTPDNSNodeDAO dbs.DAO
@@ -37,7 +38,7 @@ func init() {
})
}
// FindEnabledNodeIdWithUniqueId 根据唯一ID获取启用中的HTTPDNS节点ID
// FindEnabledNodeIdWithUniqueId 鏍规嵁鍞竴ID鑾峰彇鍚敤涓殑HTTPDNS鑺傜偣ID
func (this *HTTPDNSNodeDAO) FindEnabledNodeIdWithUniqueId(tx *dbs.Tx, uniqueId string) (int64, error) {
return this.Query(tx).
Attr("uniqueId", uniqueId).
@@ -46,7 +47,7 @@ func (this *HTTPDNSNodeDAO) FindEnabledNodeIdWithUniqueId(tx *dbs.Tx, uniqueId s
FindInt64Col(0)
}
// CreateNode 创建节点
// CreateNode 鍒涘缓鑺傜偣
func (this *HTTPDNSNodeDAO) CreateNode(tx *dbs.Tx, clusterId int64, name string, installDir string, isOn bool) (int64, error) {
uniqueId := rands.HexString(32)
secret := rands.String(32)
@@ -75,7 +76,7 @@ func (this *HTTPDNSNodeDAO) CreateNode(tx *dbs.Tx, clusterId int64, name string,
return types.Int64(op.Id), nil
}
// UpdateNode 更新节点
// UpdateNode 鏇存柊鑺傜偣
func (this *HTTPDNSNodeDAO) UpdateNode(tx *dbs.Tx, nodeId int64, name string, installDir string, isOn bool) error {
var op = NewHTTPDNSNodeOperator()
op.Id = nodeId
@@ -86,7 +87,7 @@ func (this *HTTPDNSNodeDAO) UpdateNode(tx *dbs.Tx, nodeId int64, name string, in
return this.Save(tx, op)
}
// DisableNode 禁用节点
// DisableNode 绂佺敤鑺傜偣
func (this *HTTPDNSNodeDAO) DisableNode(tx *dbs.Tx, nodeId int64) error {
node, err := this.FindEnabledNode(tx, nodeId)
if err != nil {
@@ -112,7 +113,7 @@ func (this *HTTPDNSNodeDAO) DisableNode(tx *dbs.Tx, nodeId int64) error {
return err
}
// FindEnabledNode 查找启用节点
// FindEnabledNode 鏌ユ壘鍚敤鑺傜偣
func (this *HTTPDNSNodeDAO) FindEnabledNode(tx *dbs.Tx, nodeId int64) (*HTTPDNSNode, error) {
one, err := this.Query(tx).
Pk(nodeId).
@@ -124,7 +125,7 @@ func (this *HTTPDNSNodeDAO) FindEnabledNode(tx *dbs.Tx, nodeId int64) (*HTTPDNSN
return one.(*HTTPDNSNode), nil
}
// FindNodeClusterId 查询节点所属集群ID
// FindNodeClusterId 鏌ヨ鑺傜偣鎵€灞為泦缇D
func (this *HTTPDNSNodeDAO) FindNodeClusterId(tx *dbs.Tx, nodeId int64) (int64, error) {
return this.Query(tx).
Pk(nodeId).
@@ -133,7 +134,7 @@ func (this *HTTPDNSNodeDAO) FindNodeClusterId(tx *dbs.Tx, nodeId int64) (int64,
FindInt64Col(0)
}
// ListEnabledNodes 列出节点
// ListEnabledNodes 鍒楀嚭鑺傜偣
func (this *HTTPDNSNodeDAO) ListEnabledNodes(tx *dbs.Tx, clusterId int64) (result []*HTTPDNSNode, err error) {
query := this.Query(tx).
State(HTTPDNSNodeStateEnabled).
@@ -145,6 +146,20 @@ func (this *HTTPDNSNodeDAO) ListEnabledNodes(tx *dbs.Tx, clusterId int64) (resul
return
}
// FindAllInactiveNodesWithClusterId 取得一个集群离线的HTTPDNS节点
func (this *HTTPDNSNodeDAO) FindAllInactiveNodesWithClusterId(tx *dbs.Tx, clusterId int64) (result []*HTTPDNSNode, err error) {
_, err = this.Query(tx).
State(HTTPDNSNodeStateEnabled).
Attr("clusterId", clusterId).
Attr("isOn", true). // 只监控启用的节点
Attr("isInstalled", true). // 只监控已经安装的节点
Attr("isActive", false). // 当前处于离线状态
Result("id", "name").
Slice(&result).
FindAll()
return
}
// UpdateNodeStatus 更新节点状态
func (this *HTTPDNSNodeDAO) UpdateNodeStatus(tx *dbs.Tx, nodeId int64, isUp bool, isInstalled bool, isActive bool, statusJSON []byte, installStatusJSON []byte) error {
var op = NewHTTPDNSNodeOperator()
@@ -261,7 +276,46 @@ func (this *HTTPDNSNodeDAO) FindNodeInstallStatus(tx *dbs.Tx, nodeId int64) (*No
return installStatus, nil
}
// UpdateNodeIsInstalled 更新节点安装状态位
// CountAllLowerVersionNodesWithClusterId 璁$畻鍗曚釜闆嗙兢涓墍鏈変綆浜庢煇涓増鏈殑鑺傜偣鏁伴噺
func (this *HTTPDNSNodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (int64, error) {
return this.Query(tx).
State(HTTPDNSNodeStateEnabled).
Attr("clusterId", clusterId).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").
Where("JSON_EXTRACT(status, '$.arch')=:arch").
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
Param("os", os).
Param("arch", arch).
Param("version", utils.VersionToLong(version)).
Count()
}
// FindAllLowerVersionNodesWithClusterId 鏌ユ壘鍗曚釜闆嗙兢涓墍鏈変綆浜庢煇涓増鏈殑鑺傜偣
func (this *HTTPDNSNodeDAO) FindAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (result []*HTTPDNSNode, err error) {
_, err = this.Query(tx).
State(HTTPDNSNodeStateEnabled).
Attr("clusterId", clusterId).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").
Where("JSON_EXTRACT(status, '$.arch')=:arch").
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
Param("os", os).
Param("arch", arch).
Param("version", utils.VersionToLong(version)).
DescPk().
Slice(&result).
FindAll()
return
}
// UpdateNodeIsInstalled 鏇存柊鑺傜偣瀹夎鐘舵€佷綅
func (this *HTTPDNSNodeDAO) UpdateNodeIsInstalled(tx *dbs.Tx, nodeId int64, isInstalled bool) error {
_, err := this.Query(tx).
Pk(nodeId).

View File

@@ -1521,6 +1521,8 @@ func (this *NodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterI
return this.Query(tx).
State(NodeStateEnabled).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Attr("clusterId", clusterId).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").
@@ -1536,6 +1538,9 @@ func (this *NodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterI
func (this *NodeDAO) FindAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (result []*Node, err error) {
_, err = this.Query(tx).
State(NodeStateEnabled).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Attr("clusterId", clusterId).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").

View File

@@ -94,6 +94,8 @@ func (this *NSNodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, cluste
State(NSNodeStateEnabled).
Attr("clusterId", clusterId).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").
Where("JSON_EXTRACT(status, '$.arch')=:arch").
@@ -104,6 +106,27 @@ func (this *NSNodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, cluste
Count()
}
// FindAllLowerVersionNodesWithClusterId 查找单个集群中所有低于某个版本的节点
func (this *NSNodeDAO) FindAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (result []*NSNode, err error) {
_, err = this.Query(tx).
State(NSNodeStateEnabled).
Attr("clusterId", clusterId).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").
Where("JSON_EXTRACT(status, '$.arch')=:arch").
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
Param("os", os).
Param("arch", arch).
Param("version", utils.VersionToLong(version)).
DescPk().
Slice(&result).
FindAll()
return
}
// FindEnabledNodeIdWithUniqueId 根据唯一ID获取节点ID
func (this *NSNodeDAO) FindEnabledNodeIdWithUniqueId(tx *dbs.Tx, uniqueId string) (int64, error) {
return this.Query(tx).

View File

@@ -209,6 +209,8 @@ func (this *NSNodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, cluste
return this.Query(tx).
State(NSNodeStateEnabled).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Attr("clusterId", clusterId).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").
@@ -412,6 +414,27 @@ func (this *NSNodeDAO) CountAllLowerVersionNodes(tx *dbs.Tx, version string) (in
Count()
}
// FindAllLowerVersionNodesWithClusterId 查找单个集群中所有低于某个版本的节点
func (this *NSNodeDAO) FindAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (result []*NSNode, err error) {
_, err = this.Query(tx).
State(NSNodeStateEnabled).
Attr("clusterId", clusterId).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").
Where("JSON_EXTRACT(status, '$.arch')=:arch").
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
Param("os", os).
Param("arch", arch).
Param("version", utils.VersionToLong(version)).
DescPk().
Slice(&result).
FindAll()
return
}
// ComposeNodeConfig 组合节点配置
func (this *NSNodeDAO) ComposeNodeConfig(tx *dbs.Tx, nodeId int64) (*dnsconfigs.NSNodeConfig, error) {
if nodeId <= 0 {

View File

@@ -187,6 +187,100 @@ func (q *HTTPDNSNodeQueue) InstallNode(nodeId int64, installStatus *models.NodeI
return installer.Install(installDir, params, installStatus)
}
// StartNode 启动HTTPDNS节点
func (q *HTTPDNSNodeQueue) StartNode(nodeId int64) error {
node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(nil, nodeId)
if err != nil {
return err
}
if node == nil {
return errors.New("can not find node, ID '" + numberutils.FormatInt64(nodeId) + "'")
}
// 登录信息
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleHTTPDNS, nodeId)
if err != nil {
return err
}
if login == nil {
return newGrantError("can not find node login information")
}
loginParams, err := login.DecodeSSHParams()
if err != nil {
return newGrantError(err.Error())
}
if len(strings.TrimSpace(loginParams.Host)) == 0 {
return newGrantError("ssh host should not be empty")
}
if loginParams.Port <= 0 {
loginParams.Port = 22
}
if loginParams.GrantId <= 0 {
return newGrantError("can not find node grant")
}
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
if err != nil {
return err
}
if grant == nil {
return newGrantError("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
}
installer := &HTTPDNSNodeInstaller{}
err = installer.Login(&Credentials{
Host: strings.TrimSpace(loginParams.Host),
Port: loginParams.Port,
Username: grant.Username,
Password: grant.Password,
PrivateKey: grant.PrivateKey,
Passphrase: grant.Passphrase,
Method: grant.Method,
Sudo: grant.Su == 1,
})
if err != nil {
return err
}
defer func() {
_ = installer.Close()
}()
installDir := strings.TrimSpace(node.InstallDir)
if len(installDir) == 0 {
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(nil, int64(node.ClusterId))
if err != nil {
return err
}
if cluster == nil {
return errors.New("can not find cluster, ID '" + numberutils.FormatInt64(int64(node.ClusterId)) + "'")
}
installDir = strings.TrimSpace(cluster.InstallDir)
if len(installDir) == 0 {
installDir = installer.client.UserHome() + "/edge-httpdns"
}
}
_, appDir := resolveHTTPDNSInstallPaths(installDir)
exeFile := appDir + "/bin/edge-httpdns"
_, err = installer.client.Stat(exeFile)
if err != nil {
return errors.New("httpdns node is not installed correctly, can not find executable file: " + exeFile)
}
// 先尝试 systemd 拉起
_, _, _ = installer.client.Exec("/usr/bin/systemctl start edge-httpdns")
_, stderr, err := installer.client.Exec(exeFile + " start")
if err != nil {
return fmt.Errorf("start failed: %w", err)
}
if len(strings.TrimSpace(stderr)) > 0 {
return errors.New("start failed: " + strings.TrimSpace(stderr))
}
return nil
}
func (q *HTTPDNSNodeQueue) resolveClusterTLSCertPair(cluster *models.HTTPDNSCluster) ([]byte, []byte, error) {
if cluster == nil {
return nil, nil, errors.New("cluster not found")

View File

@@ -0,0 +1,25 @@
package installers
// UpgradeQueue 升级队列,控制并发数
type UpgradeQueue struct {
sem chan struct{}
}
// SharedUpgradeQueue 全局升级队列最多5个并发
var SharedUpgradeQueue = NewUpgradeQueue(5)
// NewUpgradeQueue 创建升级队列
func NewUpgradeQueue(maxConcurrent int) *UpgradeQueue {
return &UpgradeQueue{
sem: make(chan struct{}, maxConcurrent),
}
}
// SubmitNodeUpgrade 提交节点升级任务(异步执行,超过并发限制自动排队)
func (q *UpgradeQueue) SubmitNodeUpgrade(nodeId int64, upgradeFunc func(int64) error) {
go func() {
q.sem <- struct{}{}
defer func() { <-q.sem }()
_ = upgradeFunc(nodeId)
}()
}

View File

@@ -2,9 +2,12 @@ package httpdns
import (
"encoding/json"
"log"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/dbs"
)
func toPBCluster(cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
@@ -25,9 +28,94 @@ func toPBCluster(cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
UpdatedAt: int64(cluster.UpdatedAt),
AutoRemoteStart: cluster.AutoRemoteStart,
AccessLogIsOn: cluster.AccessLogIsOn,
TimeZone: cluster.TimeZone,
}
}
// toPBClusterWithResolvedCerts 转换集群并解析证书引用为实际 PEM 数据
// 供节点调用的 RPC 使用,确保节点能拿到完整的证书内容
func toPBClusterWithResolvedCerts(tx *dbs.Tx, cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
pbCluster := toPBCluster(cluster)
if pbCluster == nil {
return nil
}
resolved := resolveTLSPolicyCerts(tx, cluster.TLSPolicy)
if resolved != nil {
pbCluster.TlsPolicyJSON = resolved
}
return pbCluster
}
// resolveTLSPolicyCerts 将 tlsPolicyJSON 中的 certRefs 解析为带实际 PEM 数据的 certs
func resolveTLSPolicyCerts(tx *dbs.Tx, tlsPolicyJSON []byte) []byte {
if len(tlsPolicyJSON) == 0 {
return nil
}
// 解析外层结构: {"listen": [...], "sslPolicy": {...}}
var tlsConfig map[string]json.RawMessage
if err := json.Unmarshal(tlsPolicyJSON, &tlsConfig); err != nil {
return nil
}
sslPolicyData, ok := tlsConfig["sslPolicy"]
if !ok || len(sslPolicyData) == 0 {
return nil
}
var sslPolicy sslconfigs.SSLPolicy
if err := json.Unmarshal(sslPolicyData, &sslPolicy); err != nil {
return nil
}
// 检查 certs 是否已经有实际数据
for _, cert := range sslPolicy.Certs {
if cert != nil && len(cert.CertData) > 128 && len(cert.KeyData) > 128 {
return nil // 已有完整 PEM 数据,无需处理
}
}
// 从 certRefs 解析实际证书数据
if len(sslPolicy.CertRefs) == 0 {
return nil
}
var resolvedCerts []*sslconfigs.SSLCertConfig
for _, ref := range sslPolicy.CertRefs {
if ref == nil || ref.CertId <= 0 {
continue
}
certConfig, err := models.SharedSSLCertDAO.ComposeCertConfig(tx, ref.CertId, false, nil, nil)
if err != nil {
log.Println("[HTTPDNS]resolve cert", ref.CertId, "failed:", err.Error())
continue
}
if certConfig == nil || len(certConfig.CertData) == 0 || len(certConfig.KeyData) == 0 {
continue
}
resolvedCerts = append(resolvedCerts, certConfig)
}
if len(resolvedCerts) == 0 {
return nil
}
// 把解析后的证书写回 sslPolicy.Certs
sslPolicy.Certs = resolvedCerts
newPolicyData, err := json.Marshal(&sslPolicy)
if err != nil {
return nil
}
tlsConfig["sslPolicy"] = newPolicyData
result, err := json.Marshal(tlsConfig)
if err != nil {
return nil
}
return result
}
func toPBNode(node *models.HTTPDNSNode) *pb.HTTPDNSNode {
if node == nil {
return nil

View File

@@ -67,10 +67,10 @@ func (this *HTTPDNSAppService) CreateHTTPDNSApp(ctx context.Context, req *pb.Cre
return errors.New("appId already exists")
}
// 使用 clusterIdsJSON若为空则优先从用户关联集群获取,再 fallback 到全局默认
// 使用 clusterIdsJSON若为空则从用户关联集群获取
clusterIdsJSON := req.ClusterIdsJSON
if len(clusterIdsJSON) == 0 || string(clusterIdsJSON) == "[]" || string(clusterIdsJSON) == "null" {
// 优先读取用户关联的 HTTPDNS 集群
// 读取用户关联的 HTTPDNS 集群
if req.UserId > 0 {
user, userErr := models.SharedUserDAO.FindEnabledUser(tx, req.UserId, nil)
if userErr != nil {
@@ -83,16 +83,11 @@ func (this *HTTPDNSAppService) CreateHTTPDNSApp(ctx context.Context, req *pb.Cre
}
}
}
// fallback 到全局默认
}
// 如果仍然没有集群,则不允许创建
if len(clusterIdsJSON) == 0 || string(clusterIdsJSON) == "[]" || string(clusterIdsJSON) == "null" {
defaultClusterIds, defaultErr := readHTTPDNSDefaultClusterIdList(tx)
if defaultErr != nil {
return defaultErr
}
if len(defaultClusterIds) > 0 {
clusterIdsJSON, _ = json.Marshal(defaultClusterIds)
}
}
return errors.New("用户尚未分配 HTTPDNS 集群,无法创建应用")
}
appDbId, err = models.SharedHTTPDNSAppDAO.CreateApp(tx, appName, appId, clusterIdsJSON, req.IsOn, req.UserId)
@@ -143,14 +138,6 @@ func readHTTPDNSDefaultClusterIdList(tx *dbs.Tx) ([]int64, error) {
}
}
// fallback默认主集群
primaryClusterId, err := models.SharedHTTPDNSClusterDAO.FindDefaultPrimaryClusterId(tx)
if err != nil {
return nil, err
}
if primaryClusterId > 0 {
return []int64{primaryClusterId}, nil
}
return nil, nil
}

View File

@@ -3,10 +3,14 @@ package httpdns
import (
"context"
"errors"
"fmt"
"strings"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// HTTPDNSClusterService HTTPDNS集群服务
@@ -25,7 +29,7 @@ func (this *HTTPDNSClusterService) CreateHTTPDNSCluster(ctx context.Context, req
}
var clusterId int64
err = this.RunTx(func(tx *dbs.Tx) error {
clusterId, err = models.SharedHTTPDNSClusterDAO.CreateCluster(tx, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault, req.AutoRemoteStart, req.AccessLogIsOn)
clusterId, err = models.SharedHTTPDNSClusterDAO.CreateCluster(tx, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault, req.AutoRemoteStart, req.AccessLogIsOn, req.TimeZone)
if err != nil {
return err
}
@@ -42,13 +46,41 @@ func (this *HTTPDNSClusterService) UpdateHTTPDNSCluster(ctx context.Context, req
if err != nil {
return nil, err
}
// Compatibility fallback:
// If protobuf schemas between edge-admin and edge-api are inconsistent,
// these newly-added fields may be lost on the wire. Read gRPC metadata as fallback.
if md, ok := metadata.FromIncomingContext(ctx); ok {
if values := md.Get("x-httpdns-auto-remote-start"); len(values) > 0 {
raw := strings.ToLower(strings.TrimSpace(values[0]))
req.AutoRemoteStart = raw == "1" || raw == "true" || raw == "on" || raw == "yes" || raw == "enabled"
}
if values := md.Get("x-httpdns-access-log-is-on"); len(values) > 0 {
raw := strings.ToLower(strings.TrimSpace(values[0]))
req.AccessLogIsOn = raw == "1" || raw == "true" || raw == "on" || raw == "yes" || raw == "enabled"
}
if values := md.Get("x-httpdns-time-zone"); len(values) > 0 {
raw := strings.TrimSpace(values[0])
if len(raw) > 0 {
req.TimeZone = raw
}
}
}
err = this.RunTx(func(tx *dbs.Tx) error {
err = models.SharedHTTPDNSClusterDAO.UpdateCluster(tx, req.ClusterId, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault, req.AutoRemoteStart, req.AccessLogIsOn)
// 先读取旧的 TLS 配置,用于判断是否真正发生了变化
var oldTLSJSON string
oldCluster, findErr := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(tx, req.ClusterId)
if findErr == nil && oldCluster != nil {
oldTLSJSON = string(oldCluster.TLSPolicy)
}
err = models.SharedHTTPDNSClusterDAO.UpdateCluster(tx, req.ClusterId, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault, req.AutoRemoteStart, req.AccessLogIsOn, req.TimeZone)
if err != nil {
return err
}
taskType := models.HTTPDNSNodeTaskTypeConfigChanged
if len(req.TlsPolicyJSON) > 0 {
if len(req.TlsPolicyJSON) > 0 && string(req.TlsPolicyJSON) != oldTLSJSON {
taskType = models.HTTPDNSNodeTaskTypeTLSChanged
}
return notifyHTTPDNSClusterTask(tx, req.ClusterId, taskType)
@@ -86,6 +118,13 @@ func (this *HTTPDNSClusterService) FindHTTPDNSCluster(ctx context.Context, req *
if err != nil {
return nil, err
}
if cluster != nil {
_ = grpc.SetHeader(ctx, metadata.Pairs(
"x-httpdns-auto-remote-start", fmt.Sprintf("%t", cluster.AutoRemoteStart),
"x-httpdns-access-log-is-on", fmt.Sprintf("%t", cluster.AccessLogIsOn),
"x-httpdns-time-zone", cluster.TimeZone,
))
}
return &pb.FindHTTPDNSClusterResponse{Cluster: toPBCluster(cluster)}, nil
}
@@ -107,10 +146,12 @@ func (this *HTTPDNSClusterService) ListHTTPDNSClusters(ctx context.Context, req
func (this *HTTPDNSClusterService) FindAllHTTPDNSClusters(ctx context.Context, req *pb.FindAllHTTPDNSClustersRequest) (*pb.FindAllHTTPDNSClustersResponse, error) {
_, _, validateErr := this.ValidateAdminAndUser(ctx, true)
isNode := false
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
isNode = true
}
clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(this.NullTx())
if err != nil {
@@ -118,8 +159,13 @@ func (this *HTTPDNSClusterService) FindAllHTTPDNSClusters(ctx context.Context, r
}
var pbClusters []*pb.HTTPDNSCluster
for _, cluster := range clusters {
if isNode {
// 节点调用时解析证书引用,嵌入实际 PEM 数据
pbClusters = append(pbClusters, toPBClusterWithResolvedCerts(this.NullTx(), cluster))
} else {
pbClusters = append(pbClusters, toPBCluster(cluster))
}
}
return &pb.FindAllHTTPDNSClustersResponse{Clusters: pbClusters}, nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/TeaOSLab/EdgeAPI/internal/goman"
"github.com/TeaOSLab/EdgeAPI/internal/installers"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeAPI/internal/setup"
rpcutils "github.com/TeaOSLab/EdgeAPI/internal/rpc/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -247,6 +248,12 @@ func (this *HTTPDNSNodeService) DownloadHTTPDNSNodeInstallationFile(ctx context.
return nil, err
}
// 检查自动升级开关
upgradeConfig, _ := setup.LoadUpgradeConfig()
if upgradeConfig != nil && !upgradeConfig.AutoUpgrade {
return &pb.DownloadHTTPDNSNodeInstallationFileResponse{}, nil
}
var file = installers.SharedDeployManager.FindHTTPDNSNodeFile(req.Os, req.Arch)
if file == nil {
return &pb.DownloadHTTPDNSNodeInstallationFileResponse{}, nil
@@ -274,6 +281,120 @@ func (this *HTTPDNSNodeService) DownloadHTTPDNSNodeInstallationFile(ctx context.
}, nil
}
// CountAllUpgradeHTTPDNSNodesWithClusterId 计算需要升级的HTTPDNS节点数量
func (this *HTTPDNSNodeService) CountAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, req *pb.CountAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*pb.RPCCountResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
deployFiles := installers.SharedDeployManager.LoadHTTPDNSNodeFiles()
total := int64(0)
for _, deployFile := range deployFiles {
count, err := models.SharedHTTPDNSNodeDAO.CountAllLowerVersionNodesWithClusterId(tx, req.ClusterId, deployFile.OS, deployFile.Arch, deployFile.Version)
if err != nil {
return nil, err
}
total += count
}
return this.SuccessCount(total)
}
// FindAllUpgradeHTTPDNSNodesWithClusterId 列出所有需要升级的HTTPDNS节点
func (this *HTTPDNSNodeService) FindAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, req *pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
deployFiles := installers.SharedDeployManager.LoadHTTPDNSNodeFiles()
var result []*pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse_HTTPDNSNodeUpgrade
for _, deployFile := range deployFiles {
nodes, err := models.SharedHTTPDNSNodeDAO.FindAllLowerVersionNodesWithClusterId(tx, req.ClusterId, deployFile.OS, deployFile.Arch, deployFile.Version)
if err != nil {
return nil, err
}
for _, node := range nodes {
// 解析状态获取当前版本
var oldVersion string
if len(node.Status) > 0 {
var statusMap map[string]interface{}
if json.Unmarshal(node.Status, &statusMap) == nil {
if v, ok := statusMap["buildVersion"]; ok {
oldVersion, _ = v.(string)
}
}
}
pbNode := toPBNode(node)
// 认证信息
login, loginErr := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(tx, nodeconfigs.NodeRoleHTTPDNS, int64(node.Id))
if loginErr != nil {
return nil, loginErr
}
if login != nil && pbNode != nil {
pbNode.NodeLogin = &pb.NodeLogin{
Id: int64(login.Id),
Name: login.Name,
Type: login.Type,
Params: login.Params,
}
}
result = append(result, &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse_HTTPDNSNodeUpgrade{
Node: pbNode,
Os: deployFile.OS,
Arch: deployFile.Arch,
OldVersion: oldVersion,
NewVersion: deployFile.Version,
})
}
}
return &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse{Nodes: result}, nil
}
// UpgradeHTTPDNSNode 升级单个HTTPDNS节点
func (this *HTTPDNSNodeService) UpgradeHTTPDNSNode(ctx context.Context, req *pb.UpgradeHTTPDNSNodeRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
err = models.SharedHTTPDNSNodeDAO.UpdateNodeIsInstalled(tx, req.NodeId, false)
if err != nil {
return nil, err
}
// 重置安装状态
installStatus, err := models.SharedHTTPDNSNodeDAO.FindNodeInstallStatus(tx, req.NodeId)
if err != nil {
return nil, err
}
if installStatus == nil {
installStatus = &models.NodeInstallStatus{}
}
installStatus.IsOk = false
installStatus.IsFinished = false
err = models.SharedHTTPDNSNodeDAO.UpdateNodeInstallStatus(tx, req.NodeId, installStatus)
if err != nil {
return nil, err
}
goman.New(func() {
installErr := installers.SharedHTTPDNSNodeQueue().InstallNodeProcess(req.NodeId, true)
if installErr != nil {
logs.Println("[RPC][HTTPDNS]upgrade node failed:", installErr.Error())
}
})
return this.Success()
}
func shouldTriggerHTTPDNSInstall(installStatusJSON []byte) bool {
if len(installStatusJSON) == 0 {
return false

View File

@@ -12,6 +12,7 @@ import (
"github.com/TeaOSLab/EdgeAPI/internal/goman"
"github.com/TeaOSLab/EdgeAPI/internal/installers"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeAPI/internal/setup"
rpcutils "github.com/TeaOSLab/EdgeAPI/internal/rpc/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
@@ -484,6 +485,12 @@ func (this *NSNodeService) DownloadNSNodeInstallationFile(ctx context.Context, r
return nil, err
}
// 检查自动升级开关
upgradeConfig, _ := setup.LoadUpgradeConfig()
if upgradeConfig != nil && !upgradeConfig.AutoUpgrade {
return &pb.DownloadNSNodeInstallationFileResponse{}, nil
}
var file = installers.SharedDeployManager.FindNSNodeFile(req.Os, req.Arch)
if file == nil {
return &pb.DownloadNSNodeInstallationFileResponse{}, nil
@@ -738,3 +745,109 @@ func (this *NSNodeService) UpdateNSNodeAPIConfig(ctx context.Context, req *pb.Up
return this.Success()
}
// FindAllUpgradeNSNodesWithNSClusterId 列出所有需要升级的NS节点
func (this *NSNodeService) FindAllUpgradeNSNodesWithNSClusterId(ctx context.Context, req *pb.FindAllUpgradeNSNodesWithNSClusterIdRequest) (*pb.FindAllUpgradeNSNodesWithNSClusterIdResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
deployFiles := installers.SharedDeployManager.LoadNSNodeFiles()
var result []*pb.FindAllUpgradeNSNodesWithNSClusterIdResponse_NSNodeUpgrade
for _, deployFile := range deployFiles {
nodes, err := models.SharedNSNodeDAO.FindAllLowerVersionNodesWithClusterId(tx, req.NsClusterId, deployFile.OS, deployFile.Arch, deployFile.Version)
if err != nil {
return nil, err
}
for _, node := range nodes {
// 解析状态获取当前版本
var oldVersion string
if len(node.Status) > 0 {
var statusMap map[string]interface{}
if json.Unmarshal(node.Status, &statusMap) == nil {
if v, ok := statusMap["buildVersion"]; ok {
oldVersion, _ = v.(string)
}
}
}
// 安装信息
installStatus, installErr := node.DecodeInstallStatus()
if installErr != nil {
return nil, installErr
}
pbInstallStatus := &pb.NodeInstallStatus{}
if installStatus != nil {
pbInstallStatus = &pb.NodeInstallStatus{
IsRunning: installStatus.IsRunning,
IsFinished: installStatus.IsFinished,
IsOk: installStatus.IsOk,
Error: installStatus.Error,
ErrorCode: installStatus.ErrorCode,
UpdatedAt: installStatus.UpdatedAt,
}
}
// 认证信息
login, loginErr := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(tx, nodeconfigs.NodeRoleDNS, int64(node.Id))
if loginErr != nil {
return nil, loginErr
}
var pbLogin *pb.NodeLogin
if login != nil {
pbLogin = &pb.NodeLogin{
Id: int64(login.Id),
Name: login.Name,
Type: login.Type,
Params: login.Params,
}
}
result = append(result, &pb.FindAllUpgradeNSNodesWithNSClusterIdResponse_NSNodeUpgrade{
NsNode: &pb.NSNode{
Id: int64(node.Id),
Name: node.Name,
IsOn: node.IsOn,
UniqueId: node.UniqueId,
IsInstalled: node.IsInstalled,
IsUp: node.IsUp,
IsActive: node.IsActive,
StatusJSON: node.Status,
InstallStatus: pbInstallStatus,
NodeLogin: pbLogin,
},
Os: deployFile.OS,
Arch: deployFile.Arch,
OldVersion: oldVersion,
NewVersion: deployFile.Version,
})
}
}
return &pb.FindAllUpgradeNSNodesWithNSClusterIdResponse{Nodes: result}, nil
}
// UpgradeNSNode 升级单个NS节点
func (this *NSNodeService) UpgradeNSNode(ctx context.Context, req *pb.UpgradeNSNodeRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
err = models.SharedNSNodeDAO.UpdateNodeIsInstalled(tx, req.NsNodeId, false)
if err != nil {
return nil, err
}
goman.New(func() {
installErr := installers.SharedNSNodeQueue().InstallNodeProcess(req.NsNodeId, true)
if installErr != nil {
logs.Println("[RPC]upgrade dns node:" + installErr.Error())
}
})
return this.Success()
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/TeaOSLab/EdgeAPI/internal/installers"
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
rpcutils "github.com/TeaOSLab/EdgeAPI/internal/rpc/utils"
"github.com/TeaOSLab/EdgeAPI/internal/setup"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
@@ -1716,6 +1717,12 @@ func (this *NodeService) DownloadNodeInstallationFile(ctx context.Context, req *
return nil, err
}
// 检查自动升级开关
upgradeConfig, _ := setup.LoadUpgradeConfig()
if upgradeConfig != nil && !upgradeConfig.AutoUpgrade {
return &pb.DownloadNodeInstallationFileResponse{}, nil
}
var file = installers.SharedDeployManager.FindNodeFile(req.Os, req.Arch)
if file == nil {
return &pb.DownloadNodeInstallationFileResponse{}, nil

View File

@@ -113,6 +113,9 @@ var upgradeFuncs = []*upgradeVersion{
{
"1.4.8", upgradeV1_4_8,
},
{
"1.4.9", upgradeV1_4_9,
},
}
// UpgradeSQLData 升级SQL数据
@@ -1274,10 +1277,25 @@ func upgradeV1_4_8(db *dbs.DB) error {
return nil
}
// 1.4.9
func upgradeV1_4_9(db *dbs.DB) error {
_, err := db.Exec("ALTER TABLE `edgeHTTPDNSClusters` ALTER COLUMN `installDir` SET DEFAULT '/root/edge-httpdns'")
if err != nil {
return err
}
_, err = db.Exec("ALTER TABLE `edgeHTTPDNSNodes` ALTER COLUMN `installDir` SET DEFAULT '/root/edge-httpdns'")
if err != nil {
return err
}
return nil
}
func createHTTPDNSTables(db *dbs.DB) error {
sqls := []string{
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSClusters` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isDefault` tinyint unsigned DEFAULT '0',`serviceDomain` varchar(255) DEFAULT NULL,`defaultTTL` int unsigned DEFAULT '30',`fallbackTimeoutMs` int unsigned DEFAULT '300',`installDir` varchar(255) DEFAULT '/opt/edge-httpdns',`tlsPolicy` json DEFAULT NULL,`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),KEY `name` (`name`),KEY `isDefault` (`isDefault`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS集群配置表默认TTL、回退超时、服务域名等'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSNodes` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`clusterId` bigint unsigned DEFAULT '0',`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isUp` tinyint unsigned DEFAULT '0',`isInstalled` tinyint unsigned DEFAULT '0',`isActive` tinyint unsigned DEFAULT '0',`uniqueId` varchar(64) DEFAULT NULL,`secret` varchar(64) DEFAULT NULL,`installDir` varchar(255) DEFAULT '/opt/edge-httpdns',`status` json DEFAULT NULL,`installStatus` json DEFAULT NULL,`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `uniqueId` (`uniqueId`),KEY `clusterId` (`clusterId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS节点表节点基础信息与运行状态'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSClusters` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isDefault` tinyint unsigned DEFAULT '0',`serviceDomain` varchar(255) DEFAULT NULL,`defaultTTL` int unsigned DEFAULT '30',`fallbackTimeoutMs` int unsigned DEFAULT '300',`installDir` varchar(255) DEFAULT '/root/edge-httpdns',`tlsPolicy` json DEFAULT NULL,`autoRemoteStart` tinyint unsigned DEFAULT '0',`accessLogIsOn` tinyint unsigned DEFAULT '0',`timeZone` varchar(128) NOT NULL DEFAULT '',`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),KEY `name` (`name`),KEY `isDefault` (`isDefault`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS集群配置表默认TTL、回退超时、服务域名等'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSNodes` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`clusterId` bigint unsigned DEFAULT '0',`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isUp` tinyint unsigned DEFAULT '0',`isInstalled` tinyint unsigned DEFAULT '0',`isActive` tinyint unsigned DEFAULT '0',`uniqueId` varchar(64) DEFAULT NULL,`secret` varchar(64) DEFAULT NULL,`installDir` varchar(255) DEFAULT '/root/edge-httpdns',`status` json DEFAULT NULL,`installStatus` json DEFAULT NULL,`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `uniqueId` (`uniqueId`),KEY `clusterId` (`clusterId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS节点表节点基础信息与运行状态'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSApps` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`appId` varchar(64) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`clusterIdsJSON` text DEFAULT NULL,`sniMode` varchar(64) DEFAULT 'fixed_hide',`userId` bigint unsigned DEFAULT '0',`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `appId` (`appId`),KEY `name` (`name`),KEY `userId` (`userId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS应用表应用与集群绑定关系'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSAppSecrets` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`appId` bigint unsigned DEFAULT '0',`signEnabled` tinyint unsigned DEFAULT '0',`signSecret` varchar(255) DEFAULT NULL,`signUpdatedAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `appId` (`appId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS应用密钥表请求验签开关与加签Secret'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSDomains` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`appId` bigint unsigned DEFAULT '0',`domain` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `appId_domain` (`appId`,`domain`),KEY `domain` (`domain`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS应用域名表应用绑定的业务域名'",

View File

@@ -0,0 +1,45 @@
package setup
import (
"encoding/json"
"sync"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
)
var (
sharedUpgradeConfig *systemconfigs.UpgradeConfig
sharedUpgradeConfigTime time.Time
sharedUpgradeConfigMu sync.Mutex
)
const upgradeConfigTTL = 5 * time.Minute
// LoadUpgradeConfig 读取升级配置带5分钟内存缓存
func LoadUpgradeConfig() (*systemconfigs.UpgradeConfig, error) {
sharedUpgradeConfigMu.Lock()
defer sharedUpgradeConfigMu.Unlock()
if sharedUpgradeConfig != nil && time.Since(sharedUpgradeConfigTime) < upgradeConfigTTL {
return sharedUpgradeConfig, nil
}
valueJSON, err := models.SharedSysSettingDAO.ReadSetting(nil, systemconfigs.SettingCodeUpgradeConfig)
if err != nil {
return nil, err
}
config := systemconfigs.NewUpgradeConfig()
if len(valueJSON) > 0 {
err = json.Unmarshal(valueJSON, config)
if err != nil {
return config, nil
}
}
sharedUpgradeConfig = config
sharedUpgradeConfigTime = time.Now()
return config, nil
}

View File

@@ -0,0 +1,107 @@
package tasks
import (
"time"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/goman"
"github.com/TeaOSLab/EdgeAPI/internal/installers"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/iwind/TeaGo/dbs"
)
func init() {
dbs.OnReadyDone(func() {
goman.New(func() {
NewHTTPDNSNodeMonitorTask(1 * time.Minute).Start()
})
})
}
type httpdnsNodeStartingTry struct {
count int
timestamp int64
}
// HTTPDNSNodeMonitorTask monitors HTTPDNS node activity and optionally tries to start offline nodes.
type HTTPDNSNodeMonitorTask struct {
BaseTask
ticker *time.Ticker
recoverMap map[int64]*httpdnsNodeStartingTry // nodeId => retry info
}
func NewHTTPDNSNodeMonitorTask(duration time.Duration) *HTTPDNSNodeMonitorTask {
return &HTTPDNSNodeMonitorTask{
ticker: time.NewTicker(duration),
recoverMap: map[int64]*httpdnsNodeStartingTry{},
}
}
func (t *HTTPDNSNodeMonitorTask) Start() {
for range t.ticker.C {
if err := t.Loop(); err != nil {
t.logErr("HTTPDNS_NODE_MONITOR", err.Error())
}
}
}
func (t *HTTPDNSNodeMonitorTask) Loop() error {
// only run on primary api node
if !t.IsPrimaryNode() {
return nil
}
clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(nil)
if err != nil {
return err
}
for _, cluster := range clusters {
if cluster == nil || !cluster.IsOn || !cluster.AutoRemoteStart {
continue
}
clusterID := int64(cluster.Id)
inactiveNodes, err := models.SharedHTTPDNSNodeDAO.FindAllInactiveNodesWithClusterId(nil, clusterID)
if err != nil {
return err
}
if len(inactiveNodes) == 0 {
continue
}
nodeQueue := installers.NewHTTPDNSNodeQueue()
for _, node := range inactiveNodes {
nodeID := int64(node.Id)
tryInfo, ok := t.recoverMap[nodeID]
if !ok {
tryInfo = &httpdnsNodeStartingTry{
count: 1,
timestamp: time.Now().Unix(),
}
t.recoverMap[nodeID] = tryInfo
} else {
if tryInfo.count >= 3 {
if tryInfo.timestamp+10*60 > time.Now().Unix() {
continue
}
tryInfo.timestamp = time.Now().Unix()
tryInfo.count = 0
}
tryInfo.count++
}
err = nodeQueue.StartNode(nodeID)
if err != nil {
if !installers.IsGrantError(err) {
_ = models.SharedNodeLogDAO.CreateLog(nil, nodeconfigs.NodeRoleHTTPDNS, nodeID, 0, 0, models.LevelError, "NODE", "start node from remote API failed: "+err.Error(), time.Now().Unix(), "", nil)
}
continue
}
_ = models.SharedNodeLogDAO.CreateLog(nil, nodeconfigs.NodeRoleHTTPDNS, nodeID, 0, 0, models.LevelSuccess, "NODE", "start node from remote API successfully", time.Now().Unix(), "", nil)
}
}
return nil
}

View File

@@ -1,4 +1,28 @@
#!/usr/bin/env bash
set -e
function verify_components_bundle() {
local file_path="$1"
if [ ! -f "$file_path" ]; then
echo "[error] components.js not found: $file_path"
return 1
fi
local file_size
file_size=$(wc -c < "$file_path")
if [ "$file_size" -lt 100000 ]; then
echo "[error] components.js looks too small ($file_size bytes), generate likely failed"
return 1
fi
if ! grep -q 'Vue.component("csrf-token"' "$file_path"; then
echo "[error] components.js missing csrf-token component, generate likely failed"
return 1
fi
echo "verify components.js: ok ($file_size bytes)"
return 0
}
function build() {
ROOT=$(dirname "$0")
@@ -58,7 +82,7 @@ function build() {
# generate files
echo "generating files ..."
env CGO_ENABLED=0 go run -tags $TAG "$ROOT"/../cmd/edge-admin/main.go generate
env TEAROOT="$ROOT" CGO_ENABLED=0 go run -tags "$TAG" "$ROOT"/../cmd/edge-admin/main.go generate
if [ "$(which uglifyjs)" ]; then
echo "compress to component.js ..."
uglifyjs --compress --mangle -- "${JS_ROOT}"/components.src.js > "${JS_ROOT}"/components.js
@@ -69,6 +93,8 @@ function build() {
cp "${JS_ROOT}"/utils.js "${JS_ROOT}"/utils.min.js
fi
verify_components_bundle "${JS_ROOT}/components.js"
# create dir & copy files
echo "copying ..."
if [ ! -d "$DIST" ]; then

View File

@@ -1,22 +1,49 @@
#!/usr/bin/env bash
set -e
JS_ROOT=../web/public/js
ROOT=$(cd "$(dirname "$0")" && pwd)
JS_ROOT="$ROOT"/../web/public/js
function verify_components_bundle() {
local file_path="$1"
if [ ! -f "$file_path" ]; then
echo "[error] components.js not found: $file_path"
return 1
fi
local file_size
file_size=$(wc -c < "$file_path")
if [ "$file_size" -lt 100000 ]; then
echo "[error] components.js looks too small ($file_size bytes), generate likely failed"
return 1
fi
if ! grep -q 'Vue.component("csrf-token"' "$file_path"; then
echo "[error] components.js missing csrf-token component, generate likely failed"
return 1
fi
echo "verify components.js: ok ($file_size bytes)"
return 0
}
echo "generating component.src.js ..."
env CGO_ENABLED=0 go run -tags=community ../cmd/edge-admin/main.go generate
env TEAROOT="$ROOT" CGO_ENABLED=0 go run -tags=community "$ROOT"/../cmd/edge-admin/main.go generate
if [ "$(which uglifyjs)" ]; then
echo "compress to component.js ..."
uglifyjs --compress --mangle -- ${JS_ROOT}/components.src.js > ${JS_ROOT}/components.js
uglifyjs --compress --mangle -- "${JS_ROOT}"/components.src.js > "${JS_ROOT}"/components.js
echo "compress to utils.min.js ..."
uglifyjs --compress --mangle -- ${JS_ROOT}/utils.js > ${JS_ROOT}/utils.min.js
uglifyjs --compress --mangle -- "${JS_ROOT}"/utils.js > "${JS_ROOT}"/utils.min.js
else
echo "copy to component.js ..."
cp ${JS_ROOT}/components.src.js ${JS_ROOT}/components.js
cp "${JS_ROOT}"/components.src.js "${JS_ROOT}"/components.js
echo "copy to utils.min.js ..."
cp ${JS_ROOT}/utils.js ${JS_ROOT}/utils.min.js
cp "${JS_ROOT}"/utils.js "${JS_ROOT}"/utils.min.js
fi
verify_components_bundle "${JS_ROOT}/components.js"
echo "ok"

View File

@@ -22,6 +22,7 @@ import (
"log"
"os"
"os/exec"
"path/filepath"
"time"
)
@@ -112,10 +113,12 @@ func main() {
}
})
app.On("generate", func() {
prepareGenerateRoot()
err := gen.Generate()
if err != nil {
fmt.Println("generate failed: " + err.Error())
return
os.Exit(1)
}
})
app.On("dev", func() {
@@ -214,3 +217,32 @@ func main() {
adminNode.Run()
})
}
func prepareGenerateRoot() {
wd, err := os.Getwd()
if err != nil {
return
}
candidates := []string{
wd,
filepath.Clean(filepath.Join(wd, "..")),
}
for _, root := range candidates {
componentsDir := filepath.Join(root, "web", "public", "js", "components")
stat, statErr := os.Stat(componentsDir)
if statErr != nil || !stat.IsDir() {
continue
}
// In testing mode, generator reads from Tea.Root + "/../web/...",
// so keep Root under build dir to make relative path stable.
buildRoot := filepath.Join(root, "build")
Tea.UpdateRoot(buildRoot)
Tea.SetPublicDir(filepath.Join(root, "web", "public"))
Tea.SetViewsDir(filepath.Join(root, "web", "views"))
Tea.SetTmpDir(filepath.Join(root, "web", "tmp"))
return
}
}

View File

@@ -0,0 +1,69 @@
package configloaders
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
)
const UpgradeSettingName = "upgradeConfig"
var sharedUpgradeConfig *systemconfigs.UpgradeConfig
func LoadUpgradeConfig() (*systemconfigs.UpgradeConfig, error) {
locker.Lock()
defer locker.Unlock()
if sharedUpgradeConfig != nil {
return sharedUpgradeConfig, nil
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return nil, err
}
resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{
Code: UpgradeSettingName,
})
if err != nil {
return nil, err
}
if len(resp.ValueJSON) == 0 {
sharedUpgradeConfig = systemconfigs.NewUpgradeConfig()
return sharedUpgradeConfig, nil
}
config := systemconfigs.NewUpgradeConfig()
err = json.Unmarshal(resp.ValueJSON, config)
if err != nil {
sharedUpgradeConfig = systemconfigs.NewUpgradeConfig()
return sharedUpgradeConfig, nil
}
sharedUpgradeConfig = config
return sharedUpgradeConfig, nil
}
func UpdateUpgradeConfig(config *systemconfigs.UpgradeConfig) error {
locker.Lock()
defer locker.Unlock()
rpcClient, err := rpc.SharedRPC()
if err != nil {
return err
}
valueJSON, err := json.Marshal(config)
if err != nil {
return err
}
_, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{
Code: UpgradeSettingName,
ValueJSON: valueJSON,
})
if err != nil {
return err
}
sharedUpgradeConfig = config
return nil
}

View File

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

View File

@@ -28,19 +28,20 @@ func (this *SdkCheckAction) RunGet(params struct {
return
}
version := strings.TrimSpace(params.Version)
t := strings.ToLower(strings.TrimSpace(params.Type))
if t == "doc" {
docPath := findUploadedSDKDocPath(platform, params.Version)
docPath := findUploadedSDKDocPath(platform, version)
if len(docPath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "Documentation is unavailable, please upload first"
this.Data["message"] = "当前平台/版本尚未上传集成文档"
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))
if len(version) > 0 {
downloadURL += "&version=" + url.QueryEscape(version)
}
this.Data["exists"] = true
this.Data["url"] = downloadURL
@@ -48,17 +49,17 @@ func (this *SdkCheckAction) RunGet(params struct {
return
}
archivePath := findSDKArchivePath(filename, params.Version)
archivePath := findSDKArchivePath(filename, version)
if len(archivePath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "SDK package is unavailable, please upload first"
this.Data["message"] = "当前平台/版本尚未上传 SDK 安装包"
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))
if len(version) > 0 {
downloadURL += "&version=" + url.QueryEscape(version)
}
downloadURL += "&raw=1"
this.Data["exists"] = true

View File

@@ -3,7 +3,6 @@ package apps
import (
"os"
"path/filepath"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
)
@@ -20,7 +19,7 @@ func (this *SdkDocAction) RunGet(params struct {
Platform string
Version string
}) {
platform, _, readmeRelativePath, _, err := resolveSDKPlatform(params.Platform)
platform, _, _, _, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Data["exists"] = false
this.Data["message"] = err.Error()
@@ -28,35 +27,25 @@ func (this *SdkDocAction) RunGet(params struct {
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 {
docPath := findUploadedSDKDocPath(platform, params.Version)
if len(docPath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "SDK documentation is not found on server, please upload first"
this.Data["message"] = "当前平台/版本尚未上传集成文档"
this.Success()
return
}
downloadName := filepath.Base(uploadedDocPath)
data, err := os.ReadFile(docPath)
if err != nil || len(data) == 0 {
this.Data["exists"] = false
this.Data["message"] = "读取集成文档失败"
this.Success()
return
}
downloadName := filepath.Base(docPath)
if len(downloadName) == 0 || downloadName == "." || downloadName == string(filepath.Separator) {
downloadName = "httpdns-sdk-" + strings.ToLower(platform) + ".md"
downloadName = "sdk-doc.md"
}
this.AddHeader("Content-Type", "text/markdown; charset=utf-8")

View File

@@ -32,7 +32,7 @@ func (this *SdkDownloadAction) RunGet(params struct {
archivePath := findSDKArchivePath(filename, params.Version)
if len(archivePath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "SDK archive not found on server, please upload first: " + filename
this.Data["message"] = "当前平台/版本尚未上传 SDK 安装包"
this.Success()
return
}
@@ -40,7 +40,7 @@ func (this *SdkDownloadAction) RunGet(params struct {
fp, err := os.Open(archivePath)
if err != nil {
this.Data["exists"] = false
this.Data["message"] = "failed to open SDK archive: " + err.Error()
this.Data["message"] = "打开 SDK 安装包失败: " + err.Error()
this.Success()
return
}

View File

@@ -1,16 +1,183 @@
package apps
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/iwind/TeaGo/Tea"
)
type sdkUploadMeta struct {
Platform string `json:"platform"`
Version string `json:"version"`
FileType string `json:"fileType"` // sdk | doc
Filename string `json:"filename"`
UpdatedAt int64 `json:"updatedAt"`
}
type sdkUploadMetaRecord struct {
Meta sdkUploadMeta
Dir string
FilePath string
}
func sdkUploadMetaFilename(platform string, version string, fileType string) string {
platform = strings.ToLower(strings.TrimSpace(platform))
version = strings.TrimSpace(version)
fileType = strings.ToLower(strings.TrimSpace(fileType))
return ".httpdns-sdk-meta-" + platform + "-v" + version + "-" + fileType + ".json"
}
func isSDKUploadMetaFile(name string) bool {
name = strings.ToLower(strings.TrimSpace(name))
return strings.HasPrefix(name, ".httpdns-sdk-meta-") && strings.HasSuffix(name, ".json")
}
func parseSDKPlatformFromDownloadFilename(downloadFilename string) string {
name := strings.ToLower(strings.TrimSpace(downloadFilename))
if !strings.HasPrefix(name, "httpdns-sdk-") || !strings.HasSuffix(name, ".zip") {
return ""
}
platform := strings.TrimSuffix(strings.TrimPrefix(name, "httpdns-sdk-"), ".zip")
switch platform {
case "android", "ios", "flutter":
return platform
default:
return ""
}
}
func listSDKUploadMetaRecords() []sdkUploadMetaRecord {
type wrapped struct {
record sdkUploadMetaRecord
modTime time.Time
}
byKey := map[string]wrapped{}
for _, dir := range sdkUploadSearchDirs() {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !isSDKUploadMetaFile(name) {
continue
}
metaPath := filepath.Join(dir, name)
data, err := os.ReadFile(metaPath)
if err != nil || len(data) == 0 {
continue
}
var meta sdkUploadMeta
if err = json.Unmarshal(data, &meta); err != nil {
continue
}
meta.Platform = strings.ToLower(strings.TrimSpace(meta.Platform))
meta.Version = strings.TrimSpace(meta.Version)
meta.FileType = strings.ToLower(strings.TrimSpace(meta.FileType))
meta.Filename = filepath.Base(strings.TrimSpace(meta.Filename))
if len(meta.Platform) == 0 || len(meta.Version) == 0 || len(meta.Filename) == 0 {
continue
}
if meta.FileType != "sdk" && meta.FileType != "doc" {
continue
}
if strings.Contains(meta.Filename, "..") || strings.Contains(meta.Filename, "/") || strings.Contains(meta.Filename, "\\") {
continue
}
filePath := filepath.Join(dir, meta.Filename)
fileStat, err := os.Stat(filePath)
if err != nil || fileStat.IsDir() || fileStat.Size() <= 0 {
continue
}
metaStat, err := os.Stat(metaPath)
if err != nil {
continue
}
if meta.UpdatedAt <= 0 {
meta.UpdatedAt = metaStat.ModTime().Unix()
}
key := meta.Platform + "|" + meta.Version + "|" + meta.FileType
current := wrapped{
record: sdkUploadMetaRecord{
Meta: meta,
Dir: dir,
FilePath: filePath,
},
modTime: metaStat.ModTime(),
}
old, ok := byKey[key]
if !ok ||
current.record.Meta.UpdatedAt > old.record.Meta.UpdatedAt ||
(current.record.Meta.UpdatedAt == old.record.Meta.UpdatedAt && current.modTime.After(old.modTime)) ||
(current.record.Meta.UpdatedAt == old.record.Meta.UpdatedAt && current.modTime.Equal(old.modTime) && current.record.FilePath > old.record.FilePath) {
byKey[key] = current
}
}
}
result := make([]sdkUploadMetaRecord, 0, len(byKey))
for _, item := range byKey {
result = append(result, item.record)
}
return result
}
func findSDKUploadFileByMeta(platform string, version string, fileType string) string {
platform = strings.ToLower(strings.TrimSpace(platform))
version = strings.TrimSpace(version)
fileType = strings.ToLower(strings.TrimSpace(fileType))
if len(platform) == 0 || len(version) == 0 {
return ""
}
for _, record := range listSDKUploadMetaRecords() {
if record.Meta.Platform == platform && record.Meta.Version == version && record.Meta.FileType == fileType {
return record.FilePath
}
}
return ""
}
func findNewestSDKUploadFileByMeta(platform string, fileType string) string {
platform = strings.ToLower(strings.TrimSpace(platform))
fileType = strings.ToLower(strings.TrimSpace(fileType))
if len(platform) == 0 {
return ""
}
var foundPath string
var foundUpdatedAt int64
for _, record := range listSDKUploadMetaRecords() {
if record.Meta.Platform != platform || record.Meta.FileType != fileType {
continue
}
if len(foundPath) == 0 || record.Meta.UpdatedAt > foundUpdatedAt || (record.Meta.UpdatedAt == foundUpdatedAt && record.FilePath > foundPath) {
foundPath = record.FilePath
foundUpdatedAt = record.Meta.UpdatedAt
}
}
return foundPath
}
func sdkUploadDir() string {
dirs := sdkUploadDirs()
if len(dirs) > 0 {
@@ -27,6 +194,7 @@ func sdkUploadDirs() []string {
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 {
@@ -54,67 +222,6 @@ func sdkUploadSearchDirs() []string {
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":
@@ -122,52 +229,23 @@ func resolveSDKPlatform(platform string) (key string, relativeDir string, readme
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
return "flutter", "flutter/new_httpdns", "flutter/new_httpdns/README.md", "httpdns-sdk-flutter.zip", nil
default:
return "", "", "", "", errors.New("invalid platform, expected one of: android, ios, flutter")
return "", "", "", "", errors.New("不支持的平台,可选值:androidiosflutter")
}
}
func findSDKArchivePath(downloadFilename string, version string) string {
searchDirs := sdkUploadSearchDirs()
platform := parseSDKPlatformFromDownloadFilename(downloadFilename)
if len(platform) == 0 {
return ""
}
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"))
return findSDKUploadFileByMeta(platform, normalizedVersion, "sdk")
}
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 ""
return findNewestSDKUploadFileByMeta(platform, "sdk")
}
func findUploadedSDKDocPath(platform string, version string) string {
@@ -176,42 +254,9 @@ func findUploadedSDKDocPath(platform string, version string) string {
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"))
return findSDKUploadFileByMeta(platform, normalizedVersion, "doc")
}
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 + "/EdgeAdmin/web/views/@default/httpdns/apps/docs/" + filename),
}
return findFirstExistingFile(candidates)
return findNewestSDKUploadFileByMeta(platform, "doc")
}

View File

@@ -1,6 +1,7 @@
package apps
import (
"encoding/json"
"errors"
"os"
"path/filepath"
@@ -14,6 +15,8 @@ import (
"github.com/iwind/TeaGo/actions"
)
const sdkUploadMaxFileSize = 20 * 1024 * 1024 // 20MB
type SdkUploadAction struct {
actionutils.ParentAction
}
@@ -52,7 +55,7 @@ func (this *SdkUploadAction) RunPost(params struct {
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
platform, _, _, downloadFilename, err := resolveSDKPlatform(params.Platform)
platform, _, _, _, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Fail(err.Error())
return
@@ -70,57 +73,21 @@ func (this *SdkUploadAction) RunPost(params struct {
}
uploadDir := sdkUploadDir()
err = os.MkdirAll(uploadDir, 0755)
if err != nil {
if err = os.MkdirAll(uploadDir, 0755); err != nil {
this.Fail("创建上传目录失败: " + err.Error())
return
}
if params.SdkFile != nil {
filename := strings.ToLower(strings.TrimSpace(params.SdkFile.Filename))
if !strings.HasSuffix(filename, ".zip") {
this.Fail("SDK 包仅支持 .zip 文件")
return
}
sdkData, readErr := params.SdkFile.Read()
if readErr != nil {
this.Fail("读取 SDK 包失败: " + readErr.Error())
return
}
if len(sdkData) == 0 {
this.Fail("SDK 包文件为空,请重新上传")
return
}
baseName := strings.TrimSuffix(downloadFilename, ".zip")
err = saveSDKUploadFile(uploadDir, baseName+"-v"+version+".zip", sdkData)
if err != nil {
this.Fail("保存 SDK 包失败: " + err.Error())
if err = this.saveUploadedItem(uploadDir, platform, version, "sdk", params.SdkFile); err != nil {
this.Fail(err.Error())
return
}
}
if params.DocFile != nil {
docName := strings.ToLower(strings.TrimSpace(params.DocFile.Filename))
if !strings.HasSuffix(docName, ".md") {
this.Fail("集成文档仅支持 .md 文件")
return
}
docData, readErr := params.DocFile.Read()
if readErr != nil {
this.Fail("读取集成文档失败: " + readErr.Error())
return
}
if len(docData) == 0 {
this.Fail("集成文档文件为空,请重新上传")
return
}
err = saveSDKUploadFile(uploadDir, "httpdns-sdk-"+platform+"-v"+version+".md", docData)
if err != nil {
this.Fail("保存集成文档失败: " + err.Error())
if err = this.saveUploadedItem(uploadDir, platform, version, "doc", params.DocFile); err != nil {
this.Fail(err.Error())
return
}
}
@@ -128,6 +95,52 @@ func (this *SdkUploadAction) RunPost(params struct {
this.Success()
}
func (this *SdkUploadAction) saveUploadedItem(uploadDir string, platform string, version string, fileType string, file *actions.File) error {
expectedExt := ".md"
displayType := "集成文档"
if fileType == "sdk" {
expectedExt = ".zip"
displayType = "SDK 包"
}
filename, err := normalizeUploadedFilename(file.Filename, expectedExt)
if err != nil {
return err
}
if file.Size > sdkUploadMaxFileSize {
return errors.New(displayType + "文件不能超过 20MB")
}
data, err := file.Read()
if err != nil {
return errors.New("读取" + displayType + "失败: " + err.Error())
}
if len(data) == 0 {
return errors.New(displayType + "文件为空,请重新上传")
}
if len(data) > sdkUploadMaxFileSize {
return errors.New(displayType + "文件不能超过 20MB")
}
if err = saveSDKUploadFile(uploadDir, filename, data); err != nil {
return errors.New("保存" + displayType + "失败: " + err.Error())
}
err = saveSDKUploadMetaRecord(uploadDir, sdkUploadMeta{
Platform: platform,
Version: version,
FileType: fileType,
Filename: filename,
UpdatedAt: time.Now().Unix(),
})
if err != nil {
return errors.New("保存上传元信息失败: " + err.Error())
}
return nil
}
func normalizeSDKVersion(version string) (string, error) {
version = strings.TrimSpace(version)
if len(version) == 0 {
@@ -142,6 +155,26 @@ func normalizeSDKVersion(version string) (string, error) {
return version, nil
}
func normalizeUploadedFilename(raw string, expectedExt string) (string, error) {
filename := filepath.Base(strings.TrimSpace(raw))
if len(filename) == 0 || filename == "." || filename == string(filepath.Separator) {
return "", errors.New("文件名不能为空")
}
if strings.Contains(filename, "/") || strings.Contains(filename, "\\") || strings.Contains(filename, "..") {
return "", errors.New("文件名不合法")
}
actualExt := strings.ToLower(filepath.Ext(filename))
if actualExt != strings.ToLower(expectedExt) {
if expectedExt == ".zip" {
return "", errors.New("SDK 包仅支持 .zip 文件")
}
return "", errors.New("集成文档仅支持 .md 文件")
}
return filename, nil
}
func saveSDKUploadFile(baseDir string, filename string, data []byte) error {
targetPath := filepath.Join(baseDir, filename)
tmpPath := targetPath + ".tmp"
@@ -152,6 +185,35 @@ func saveSDKUploadFile(baseDir string, filename string, data []byte) error {
return os.Rename(tmpPath, targetPath)
}
func saveSDKUploadMetaRecord(baseDir string, meta sdkUploadMeta) error {
meta.Platform = strings.ToLower(strings.TrimSpace(meta.Platform))
meta.Version = strings.TrimSpace(meta.Version)
meta.FileType = strings.ToLower(strings.TrimSpace(meta.FileType))
meta.Filename = filepath.Base(strings.TrimSpace(meta.Filename))
if len(meta.Platform) == 0 || len(meta.Version) == 0 || len(meta.FileType) == 0 || len(meta.Filename) == 0 {
return errors.New("上传元信息不完整")
}
metaFilename := sdkUploadMetaFilename(meta.Platform, meta.Version, meta.FileType)
metaPath := filepath.Join(baseDir, metaFilename)
if data, err := os.ReadFile(metaPath); err == nil && len(data) > 0 {
var oldMeta sdkUploadMeta
if json.Unmarshal(data, &oldMeta) == nil {
oldFile := filepath.Base(strings.TrimSpace(oldMeta.Filename))
if len(oldFile) > 0 && oldFile != meta.Filename {
_ = os.Remove(filepath.Join(baseDir, oldFile))
}
}
}
data, err := json.Marshal(meta)
if err != nil {
return err
}
return saveSDKUploadFile(baseDir, metaFilename, data)
}
func listUploadedSDKFiles() []map[string]interface{} {
type item struct {
Name string
@@ -162,45 +224,26 @@ func listUploadedSDKFiles() []map[string]interface{} {
UpdatedAt int64
}
byName := map[string]item{}
for _, dir := range sdkUploadSearchDirs() {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
platform, version, fileType, ok := parseSDKUploadFilename(name)
if !ok {
items := make([]item, 0)
for _, record := range listSDKUploadMetaRecords() {
stat, err := os.Stat(record.FilePath)
if err != nil || stat.IsDir() {
continue
}
info, statErr := entry.Info()
if statErr != nil {
continue
fileType := "SDK 包"
if record.Meta.FileType == "doc" {
fileType = "集成文档"
}
current := item{
Name: name,
Platform: platform,
items = append(items, item{
Name: filepath.Base(record.FilePath),
Platform: record.Meta.Platform,
FileType: fileType,
Version: version,
SizeBytes: info.Size(),
UpdatedAt: info.ModTime().Unix(),
}
old, exists := byName[name]
if !exists || current.UpdatedAt >= old.UpdatedAt {
byName[name] = current
}
}
}
items := make([]item, 0, len(byName))
for _, it := range byName {
items = append(items, it)
Version: record.Meta.Version,
SizeBytes: stat.Size(),
UpdatedAt: stat.ModTime().Unix(),
})
}
sort.Slice(items, func(i, j int) bool {
@@ -224,55 +267,21 @@ func listUploadedSDKFiles() []map[string]interface{} {
return result
}
func parseSDKUploadFilename(filename string) (platform string, version string, fileType string, ok bool) {
if !strings.HasPrefix(filename, "httpdns-sdk-") {
return "", "", "", false
}
ext := ""
switch {
case strings.HasSuffix(filename, ".zip"):
ext = ".zip"
fileType = "SDK包"
case strings.HasSuffix(filename, ".md"):
ext = ".md"
fileType = "集成文档"
default:
return "", "", "", false
}
main := strings.TrimSuffix(strings.TrimPrefix(filename, "httpdns-sdk-"), ext)
version = ""
if idx := strings.Index(main, "-v"); idx > 0 && idx+2 < len(main) {
version = main[idx+2:]
main = main[:idx]
}
main = strings.ToLower(strings.TrimSpace(main))
switch main {
case "android", "ios", "flutter":
platform = main
if len(version) == 0 {
version = "-"
}
return platform, version, fileType, true
default:
return "", "", "", false
}
}
func formatSDKFileSize(size int64) string {
if size < 1024 {
return strconv.FormatInt(size, 10) + " B"
}
sizeKB := float64(size) / 1024
if sizeKB < 1024 {
return strconv.FormatFloat(sizeKB, 'f', 1, 64) + " KB"
}
sizeMB := sizeKB / 1024
if sizeMB < 1024 {
return strconv.FormatFloat(sizeMB, 'f', 1, 64) + " MB"
}
sizeGB := sizeMB / 1024
return strconv.FormatFloat(sizeGB, 'f', 1, 64) + " GB"
}

View File

@@ -1,6 +1,7 @@
package apps
import (
"encoding/json"
"os"
"path/filepath"
"strings"
@@ -34,19 +35,16 @@ func (this *SdkUploadDeleteAction) RunPost(params struct {
this.Fail("文件名不合法")
return
}
if !strings.HasPrefix(filename, "httpdns-sdk-") {
this.Fail("不允许删除该文件")
return
}
if !(strings.HasSuffix(filename, ".zip") || strings.HasSuffix(filename, ".md")) {
this.Fail("不允许删除该文件")
lowName := strings.ToLower(filename)
if !strings.HasSuffix(lowName, ".zip") && !strings.HasSuffix(lowName, ".md") {
this.Fail("仅允许删除 .zip 或 .md 文件")
return
}
for _, dir := range sdkUploadDirs() {
fullPath := filepath.Join(dir, filename)
_, err := os.Stat(fullPath)
if err != nil {
stat, err := os.Stat(fullPath)
if err != nil || stat.IsDir() {
continue
}
if err = os.Remove(fullPath); err != nil {
@@ -55,5 +53,38 @@ func (this *SdkUploadDeleteAction) RunPost(params struct {
}
}
// 删除引用该文件的元数据
for _, dir := range sdkUploadDirs() {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
if !isSDKUploadMetaFile(entry.Name()) {
continue
}
metaPath := filepath.Join(dir, entry.Name())
data, err := os.ReadFile(metaPath)
if err != nil || len(data) == 0 {
continue
}
var meta sdkUploadMeta
if err = json.Unmarshal(data, &meta); err != nil {
continue
}
if filepath.Base(strings.TrimSpace(meta.Filename)) != filename {
continue
}
_ = os.Remove(metaPath)
}
}
this.Success()
}

View File

@@ -1,9 +1,11 @@
package node
import (
"strings"
"time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
)
@@ -50,7 +52,22 @@ func (this *IndexAction) RunGet(params struct {
this.Data["nodeDatetime"] = nodeDatetime
this.Data["nodeTimeDiff"] = nodeTimeDiff
osName := strings.TrimSpace(status.GetString("os"))
if len(osName) > 0 {
checkVersionResp, err := this.RPC().HTTPDNSNodeRPC().CheckHTTPDNSNodeLatestVersion(this.AdminContext(), &pb.CheckHTTPDNSNodeLatestVersionRequest{
Os: osName,
Arch: strings.TrimSpace(status.GetString("arch")),
CurrentVersion: strings.TrimSpace(status.GetString("buildVersion")),
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["shouldUpgrade"] = checkVersionResp.GetHasNewVersion()
this.Data["newVersion"] = checkVersionResp.GetNewVersion()
} else {
this.Data["shouldUpgrade"] = false
this.Data["newVersion"] = ""
}
this.Show()
}

View File

@@ -22,14 +22,14 @@ func findHTTPDNSClusterMap(parent *actionutils.ParentAction, clusterID int64) (m
return maps.Map{
"id": clusterID,
"name": "",
"installDir": "/opt/edge-httpdns",
"installDir": "/root/edge-httpdns",
}, nil
}
cluster := resp.GetCluster()
installDir := strings.TrimSpace(cluster.GetInstallDir())
if len(installDir) == 0 {
installDir = "/opt/edge-httpdns"
installDir = "/root/edge-httpdns"
}
return maps.Map{
"id": cluster.GetId(),
@@ -93,7 +93,7 @@ func findHTTPDNSNodeMap(parent *actionutils.ParentAction, nodeID int64) (maps.Ma
installDir := strings.TrimSpace(node.GetInstallDir())
if len(installDir) == 0 {
installDir = "/opt/edge-httpdns"
installDir = "/root/edge-httpdns"
}
clusterMap, err := findHTTPDNSClusterMap(parent, node.GetClusterId())
@@ -167,6 +167,8 @@ func decodeNodeStatus(raw []byte) maps.Map {
return maps.Map{
"isActive": status.IsActive,
"updatedAt": status.UpdatedAt,
"os": status.OS,
"arch": status.Arch,
"hostname": status.Hostname,
"hostIP": status.HostIP,
"cpuUsage": status.CPUUsage,

View File

@@ -195,7 +195,7 @@ func (this *UpdateAction) RunPost(params struct {
installDir := strings.TrimSpace(node.GetInstallDir())
if len(installDir) == 0 {
installDir = "/opt/edge-httpdns"
installDir = "/root/edge-httpdns"
}
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNode(this.AdminContext(), &pb.UpdateHTTPDNSNodeRequest{

View File

@@ -1,17 +1,20 @@
package clusters
package clusters
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"google.golang.org/grpc/metadata"
)
type ClusterSettingsAction struct {
@@ -49,6 +52,7 @@ func (this *ClusterSettingsAction) RunGet(params struct {
"isOn": cluster.GetBool("isOn"),
"autoRemoteStart": cluster.GetBool("autoRemoteStart"),
"accessLogIsOn": cluster.GetBool("accessLogIsOn"),
"timeZone": cluster.GetString("timeZone"),
}
if settings.GetInt("cacheTtl") <= 0 {
settings["cacheTtl"] = 60
@@ -57,7 +61,10 @@ func (this *ClusterSettingsAction) RunGet(params struct {
settings["fallbackTimeout"] = 300
}
if len(settings.GetString("installDir")) == 0 {
settings["installDir"] = "/opt/edge-httpdns"
settings["installDir"] = "/root/edge-httpdns"
}
if len(settings.GetString("timeZone")) == 0 {
settings["timeZone"] = "Asia/Shanghai"
}
listenAddresses := []*serverconfigs.NetworkAddressConfig{
@@ -101,6 +108,15 @@ func (this *ClusterSettingsAction) RunGet(params struct {
"listen": listenAddresses,
"sslPolicy": sslPolicy,
}
this.Data["timeZoneGroups"] = nodeconfigs.FindAllTimeZoneGroups()
this.Data["timeZoneLocations"] = nodeconfigs.FindAllTimeZoneLocations()
timeZoneStr := settings.GetString("timeZone")
if len(timeZoneStr) == 0 {
timeZoneStr = nodeconfigs.DefaultTimeZoneLocation
}
this.Data["timeZoneLocation"] = nodeconfigs.FindTimeZoneLocation(timeZoneStr)
this.Show()
}
@@ -114,6 +130,7 @@ func (this *ClusterSettingsAction) RunPost(params struct {
IsOn bool
AutoRemoteStart bool
AccessLogIsOn bool
TimeZone string
Addresses []byte
SslPolicyJSON []byte
@@ -129,6 +146,31 @@ func (this *ClusterSettingsAction) RunPost(params struct {
params.Must.Field("name", params.Name).Require("请输入集群名称")
params.Must.Field("gatewayDomain", params.GatewayDomain).Require("请输入服务域名")
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
// 开关项按请求值强制覆盖:未提交/空值都视为 false支持取消勾选
autoRemoteStartRaw := strings.ToLower(strings.TrimSpace(this.ParamString("autoRemoteStart")))
params.AutoRemoteStart = autoRemoteStartRaw == "1" || autoRemoteStartRaw == "true" || autoRemoteStartRaw == "on" || autoRemoteStartRaw == "yes" || autoRemoteStartRaw == "enabled"
accessLogIsOnRaw := strings.ToLower(strings.TrimSpace(this.ParamString("accessLogIsOn")))
params.AccessLogIsOn = accessLogIsOnRaw == "1" || accessLogIsOnRaw == "true" || accessLogIsOnRaw == "on" || accessLogIsOnRaw == "yes" || accessLogIsOnRaw == "enabled"
isOnRaw := strings.ToLower(strings.TrimSpace(this.ParamString("isOn")))
params.IsOn = isOnRaw == "1" || isOnRaw == "true" || isOnRaw == "on" || isOnRaw == "yes" || isOnRaw == "enabled"
// 时区为空时继承当前值,再兜底默认值
params.TimeZone = strings.TrimSpace(this.ParamString("timeZone"))
if len(params.TimeZone) == 0 {
params.TimeZone = strings.TrimSpace(cluster.GetString("timeZone"))
}
if len(params.TimeZone) == 0 {
params.TimeZone = "Asia/Shanghai"
}
if params.CacheTtl <= 0 {
params.CacheTtl = 60
}
@@ -136,20 +178,13 @@ func (this *ClusterSettingsAction) RunPost(params struct {
params.FallbackTimeout = 300
}
if len(params.InstallDir) == 0 {
params.InstallDir = "/opt/edge-httpdns"
}
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
params.InstallDir = "/root/edge-httpdns"
}
tlsConfig := maps.Map{}
if rawTLS := strings.TrimSpace(cluster.GetString("tlsPolicyJSON")); len(rawTLS) > 0 {
_ = json.Unmarshal([]byte(rawTLS), &tlsConfig)
}
if len(params.Addresses) > 0 {
var addresses []*serverconfigs.NetworkAddressConfig
if err := json.Unmarshal(params.Addresses, &addresses); err != nil {
@@ -158,7 +193,6 @@ func (this *ClusterSettingsAction) RunPost(params struct {
}
tlsConfig["listen"] = addresses
}
if len(params.SslPolicyJSON) > 0 {
sslPolicy := &sslconfigs.SSLPolicy{}
if err := json.Unmarshal(params.SslPolicyJSON, sslPolicy); err != nil {
@@ -177,7 +211,7 @@ func (this *ClusterSettingsAction) RunPost(params struct {
}
}
_, err = this.RPC().HTTPDNSClusterRPC().UpdateHTTPDNSCluster(this.AdminContext(), &pb.UpdateHTTPDNSClusterRequest{
updateReq := &pb.UpdateHTTPDNSClusterRequest{
ClusterId: params.ClusterId,
Name: params.Name,
ServiceDomain: params.GatewayDomain,
@@ -189,7 +223,16 @@ func (this *ClusterSettingsAction) RunPost(params struct {
IsDefault: false,
AutoRemoteStart: params.AutoRemoteStart,
AccessLogIsOn: params.AccessLogIsOn,
})
TimeZone: params.TimeZone,
}
updateCtx := metadata.AppendToOutgoingContext(
this.AdminContext(),
"x-httpdns-auto-remote-start", fmt.Sprintf("%t", updateReq.GetAutoRemoteStart()),
"x-httpdns-access-log-is-on", fmt.Sprintf("%t", updateReq.GetAccessLogIsOn()),
"x-httpdns-time-zone", updateReq.GetTimeZone(),
)
_, err = this.RPC().HTTPDNSClusterRPC().UpdateHTTPDNSCluster(updateCtx, updateReq)
if err != nil {
this.ErrorPage(err)
return

View File

@@ -1,6 +1,7 @@
package clusters
import (
"strconv"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
@@ -36,7 +37,7 @@ func (this *CreateAction) RunPost(params struct {
params.GatewayDomain = strings.TrimSpace(params.GatewayDomain)
params.InstallDir = strings.TrimSpace(params.InstallDir)
if len(params.InstallDir) == 0 {
params.InstallDir = "/opt/edge-httpdns"
params.InstallDir = "/root/edge-httpdns"
}
if params.CacheTtl <= 0 {
params.CacheTtl = 60
@@ -56,6 +57,9 @@ func (this *CreateAction) RunPost(params struct {
InstallDir: params.InstallDir,
IsOn: params.IsOn,
IsDefault: false,
AutoRemoteStart: true,
AccessLogIsOn: true,
TimeZone: "Asia/Shanghai",
})
if err != nil {
this.ErrorPage(err)
@@ -63,5 +67,12 @@ func (this *CreateAction) RunPost(params struct {
}
this.Data["clusterId"] = resp.GetClusterId()
// fallback: if frontend JS doesn't intercept form submit, redirect instead of showing raw JSON
if len(this.Request.Header.Get("X-Requested-With")) == 0 {
this.RedirectURL("/httpdns/clusters/cluster?clusterId=" + strconv.FormatInt(resp.GetClusterId(), 10))
return
}
this.Success()
}

View File

@@ -51,7 +51,7 @@ func (this *CreateNodeAction) RunPost(params struct {
params.InstallDir = strings.TrimSpace(cluster.GetString("installDir"))
}
if len(params.InstallDir) == 0 {
params.InstallDir = "/opt/edge-httpdns"
params.InstallDir = "/root/edge-httpdns"
}
}

View File

@@ -22,7 +22,9 @@ func init() {
// Node level
GetPost("/createNode", new(CreateNodeAction)).
Post("/deleteNode", new(DeleteNodeAction)).
Get("/upgradeRemote", new(UpgradeRemoteAction)).
GetPost("/cluster/upgradeRemote", new(UpgradeRemoteAction)).
GetPost("/upgradeRemote", new(UpgradeRemoteAction)).
Post("/upgradeStatus", new(UpgradeStatusAction)).
GetPost("/updateNodeSSH", new(UpdateNodeSSHAction)).
Post("/checkPorts", new(CheckPortsAction)).

View File

@@ -9,6 +9,10 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map, error) {
@@ -37,6 +41,11 @@ func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.M
}
}
countUpgradeNodes, err := countUpgradeHTTPDNSNodes(parent, cluster.GetId())
if err != nil {
return nil, err
}
port := "443"
if rawTLS := cluster.GetTlsPolicyJSON(); len(rawTLS) > 0 {
tlsConfig := maps.Map{}
@@ -63,25 +72,80 @@ func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.M
"defaultTTL": cluster.GetDefaultTTL(),
"fallbackTimeout": cluster.GetFallbackTimeoutMs(),
"installDir": cluster.GetInstallDir(),
"timeZone": cluster.GetTimeZone(),
"isOn": cluster.GetIsOn(),
"isDefault": cluster.GetIsDefault(),
"autoRemoteStart": cluster.GetAutoRemoteStart(),
"accessLogIsOn": cluster.GetAccessLogIsOn(),
"tlsPolicyJSON": cluster.GetTlsPolicyJSON(),
"countAllNodes": countAllNodes,
"countActiveNodes": countActiveNodes,
"countUpgradeNodes": countUpgradeNodes,
})
}
return result, nil
}
func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map, error) {
if clusterID > 0 {
resp, err := parent.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(parent.AdminContext(), &pb.FindHTTPDNSClusterRequest{
func countUpgradeHTTPDNSNodes(parent *actionutils.ParentAction, clusterID int64) (int64, error) {
countResp, err := parent.RPC().HTTPDNSNodeRPC().CountAllUpgradeHTTPDNSNodesWithClusterId(parent.AdminContext(), &pb.CountAllUpgradeHTTPDNSNodesWithClusterIdRequest{
ClusterId: clusterID,
})
if err == nil {
return countResp.GetCount(), nil
}
grpcStatus, ok := status.FromError(err)
if !ok || grpcStatus.Code() != codes.Unimplemented {
return 0, err
}
// Compatibility fallback: old edge-api may not implement countAllUpgradeHTTPDNSNodesWithClusterId yet.
listResp, listErr := parent.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(parent.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
ClusterId: clusterID,
})
if listErr == nil {
return int64(len(listResp.GetNodes())), nil
}
listStatus, ok := status.FromError(listErr)
if ok && listStatus.Code() == codes.Unimplemented {
// Compatibility fallback: both methods missing on old edge-api, don't block page rendering.
return 0, nil
}
return 0, listErr
}
func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map, error) {
if clusterID > 0 {
var headerMD metadata.MD
resp, err := parent.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(parent.AdminContext(), &pb.FindHTTPDNSClusterRequest{
ClusterId: clusterID,
}, grpc.Header(&headerMD))
if err != nil {
return nil, err
}
if resp.GetCluster() != nil {
cluster := resp.GetCluster()
autoRemoteStart := cluster.GetAutoRemoteStart()
accessLogIsOn := cluster.GetAccessLogIsOn()
timeZone := cluster.GetTimeZone()
// Compatibility fallback:
// Some deployed admin binaries may decode newly-added protobuf fields incorrectly.
// Read values from grpc response headers as a source of truth.
if values := headerMD.Get("x-httpdns-auto-remote-start"); len(values) > 0 {
autoRemoteStart = parseBoolLike(values[0], autoRemoteStart)
}
if values := headerMD.Get("x-httpdns-access-log-is-on"); len(values) > 0 {
accessLogIsOn = parseBoolLike(values[0], accessLogIsOn)
}
if values := headerMD.Get("x-httpdns-time-zone"); len(values) > 0 {
if tz := strings.TrimSpace(values[0]); len(tz) > 0 {
timeZone = tz
}
}
return maps.Map{
"id": cluster.GetId(),
"name": cluster.GetName(),
@@ -92,8 +156,9 @@ func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map
"isOn": cluster.GetIsOn(),
"isDefault": cluster.GetIsDefault(),
"tlsPolicyJSON": cluster.GetTlsPolicyJSON(),
"autoRemoteStart": cluster.GetAutoRemoteStart(),
"accessLogIsOn": cluster.GetAccessLogIsOn(),
"autoRemoteStart": autoRemoteStart,
"accessLogIsOn": accessLogIsOn,
"timeZone": timeZone,
}, nil
}
}
@@ -109,12 +174,25 @@ func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map
"gatewayDomain": "",
"defaultTTL": 60,
"fallbackTimeout": 300,
"installDir": "/opt/edge-httpdns",
"installDir": "/root/edge-httpdns",
"timeZone": "Asia/Shanghai",
}, nil
}
return clusters[0], nil
}
func parseBoolLike(raw string, defaultValue bool) bool {
s := strings.ToLower(strings.TrimSpace(raw))
switch s {
case "1", "true", "on", "yes", "enabled":
return true
case "0", "false", "off", "no", "disabled":
return false
default:
return defaultValue
}
}
func listNodeMaps(parent *actionutils.ParentAction, clusterID int64) ([]maps.Map, error) {
resp, err := parent.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(parent.AdminContext(), &pb.ListHTTPDNSNodesRequest{
ClusterId: clusterID,

View File

@@ -1,16 +1,108 @@
package clusters
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
import (
"encoding/json"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type UpgradeRemoteAction struct {
actionutils.ParentAction
}
func (this *UpgradeRemoteAction) Init() {
this.Nav("httpdns", "cluster", "index")
}
func (this *UpgradeRemoteAction) RunGet(params struct {
NodeId int64
ClusterId int64
}) {
this.Data["nodeId"] = params.NodeId
httpdnsutils.AddLeftMenu(this.Parent())
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "node")
this.Data["clusterId"] = params.ClusterId
this.Data["cluster"] = cluster
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
ClusterId: params.ClusterId,
})
if err != nil {
this.ErrorPage(err)
return
}
nodes := make([]maps.Map, 0, len(resp.GetNodes()))
for _, upgradeNode := range resp.GetNodes() {
node := upgradeNode.Node
if node == nil {
continue
}
loginParams := maps.Map{}
if node.GetNodeLogin() != nil && len(node.GetNodeLogin().GetParams()) > 0 {
_ = json.Unmarshal(node.GetNodeLogin().GetParams(), &loginParams)
}
status := decodeNodeStatus(node.GetStatusJSON())
accessIP := strings.TrimSpace(status.GetString("hostIP"))
if len(accessIP) == 0 {
accessIP = strings.TrimSpace(node.GetName())
}
nodes = append(nodes, maps.Map{
"id": node.GetId(),
"name": node.GetName(),
"accessIP": accessIP,
"oldVersion": upgradeNode.OldVersion,
"newVersion": upgradeNode.NewVersion,
"login": node.GetNodeLogin(),
"loginParams": loginParams,
"installStatus": decodeUpgradeInstallStatus(node.GetInstallStatusJSON()),
})
}
this.Data["nodes"] = nodes
this.Show()
}
func (this *UpgradeRemoteAction) RunPost(params struct {
NodeId int64
Must *actions.Must
}) {
_, err := this.RPC().HTTPDNSNodeRPC().UpgradeHTTPDNSNode(this.AdminContext(), &pb.UpgradeHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}
func decodeUpgradeInstallStatus(raw []byte) maps.Map {
result := maps.Map{
"isRunning": false,
"isFinished": false,
"isOk": false,
"error": "",
"errorCode": "",
}
if len(raw) == 0 {
return result
}
_ = json.Unmarshal(raw, &result)
return result
}

View File

@@ -0,0 +1,48 @@
package clusters
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type UpgradeStatusAction struct {
actionutils.ParentAction
}
func (this *UpgradeStatusAction) RunPost(params struct {
NodeId int64
}) {
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
NodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
if resp.GetNode() == nil {
this.Data["status"] = nil
this.Success()
return
}
this.Data["status"] = decodeUpgradeInstallStatusMap(resp.GetNode().GetInstallStatusJSON())
this.Success()
}
func decodeUpgradeInstallStatusMap(raw []byte) map[string]interface{} {
result := map[string]interface{}{
"isRunning": false,
"isFinished": false,
"isOk": false,
"error": "",
"errorCode": "",
}
if len(raw) == 0 {
return result
}
_ = json.Unmarshal(raw, &result)
return result
}

View File

@@ -39,6 +39,7 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool)
if configloaders.AllowModule(adminId, configloaders.AdminModuleCodeSetting) {
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminServer), "", "/settings/server", "", this.tab == "server")
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminUI), "", "/settings/ui", "", this.tab == "ui")
tabbar.Add("升级设置", "", "/settings/upgrade", "", this.tab == "upgrade")
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminSecuritySettings), "", "/settings/security", "", this.tab == "security")
if teaconst.IsPlus {
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabIPLibrary), "", "/settings/ip-library", "", this.tab == "ipLibrary")

View File

@@ -42,6 +42,7 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool)
if teaconst.IsPlus {
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabUserUI), "", "/settings/user-ui", "", this.tab == "userUI")
}
tabbar.Add("升级设置", "", "/settings/upgrade", "", this.tab == "upgrade")
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminSecuritySettings), "", "/settings/security", "", this.tab == "security")
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabIPLibrary), "", "/settings/ip-library", "", this.tab == "ipLibrary")
}

View File

@@ -56,9 +56,6 @@ func (this *IndexAction) RunPost(params struct {
TimeZone string
DnsResolverType string
SupportModuleCDN bool
SupportModuleNS bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
@@ -93,13 +90,7 @@ func (this *IndexAction) RunPost(params struct {
config.DefaultPageSize = 10
}
config.Modules = []userconfigs.UserModule{}
if params.SupportModuleCDN {
config.Modules = append(config.Modules, userconfigs.UserModuleCDN)
}
if params.SupportModuleNS {
config.Modules = append(config.Modules, userconfigs.UserModuleNS)
}
config.Modules = []userconfigs.UserModule{userconfigs.UserModuleCDN, userconfigs.UserModuleNS}
// 上传Favicon文件
if params.FaviconFile != nil {

View File

@@ -0,0 +1,23 @@
//go:build !plus
package upgrade
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
// loadDNSUpgradeModules 非Plus版本不支持DNS模块
func loadDNSUpgradeModules(parent *actionutils.ParentAction) []maps.Map {
return nil
}
// upgradeDNSNode 非Plus版本不支持DNS节点升级
func upgradeDNSNode(parent *actionutils.ParentAction, nodeId int64) error {
return nil
}
// loadDNSNodeStatus 非Plus版本不支持DNS节点状态查询
func loadDNSNodeStatus(parent *actionutils.ParentAction, nodeIds []int64) []maps.Map {
return nil
}

View File

@@ -0,0 +1,100 @@
//go:build plus
package upgrade
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
// loadDNSUpgradeModules 加载DNS模块的待升级节点信息
func loadDNSUpgradeModules(parent *actionutils.ParentAction) []maps.Map {
clustersResp, err := parent.RPC().NSClusterRPC().ListNSClusters(parent.AdminContext(), &pb.ListNSClustersRequest{
Offset: 0,
Size: 10000,
})
if err != nil {
return nil
}
var clusterMaps []maps.Map
for _, cluster := range clustersResp.NsClusters {
nodesResp, err := parent.RPC().NSNodeRPC().FindAllUpgradeNSNodesWithNSClusterId(parent.AdminContext(), &pb.FindAllUpgradeNSNodesWithNSClusterIdRequest{
NsClusterId: cluster.Id,
})
if err != nil {
continue
}
var nodeMaps []maps.Map
for _, nodeUpgrade := range nodesResp.Nodes {
if nodeUpgrade.NsNode == nil {
continue
}
installStatusMap := decodeInstallStatusFromPB(nodeUpgrade.NsNode.InstallStatus)
accessIP, login, loginParams := decodeNodeAccessInfo(nodeUpgrade.NsNode.StatusJSON, nodeUpgrade.NsNode.NodeLogin, nodeUpgrade.NsNode.Name)
nodeMaps = append(nodeMaps, maps.Map{
"id": nodeUpgrade.NsNode.Id,
"name": nodeUpgrade.NsNode.Name,
"os": nodeUpgrade.Os,
"arch": nodeUpgrade.Arch,
"oldVersion": nodeUpgrade.OldVersion,
"newVersion": nodeUpgrade.NewVersion,
"isOn": nodeUpgrade.NsNode.IsOn,
"isUp": nodeUpgrade.NsNode.IsUp,
"accessIP": accessIP,
"login": login,
"loginParams": loginParams,
"installStatus": installStatusMap,
})
}
if len(nodeMaps) == 0 {
continue
}
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"nodes": nodeMaps,
"count": len(nodeMaps),
})
}
return clusterMaps
}
// upgradeDNSNode 升级DNS节点
func upgradeDNSNode(parent *actionutils.ParentAction, nodeId int64) error {
_, err := parent.RPC().NSNodeRPC().UpgradeNSNode(parent.AdminContext(), &pb.UpgradeNSNodeRequest{
NsNodeId: nodeId,
})
return err
}
// loadDNSNodeStatus 加载DNS节点安装状态
func loadDNSNodeStatus(parent *actionutils.ParentAction, nodeIds []int64) []maps.Map {
var result []maps.Map
for _, nodeId := range nodeIds {
resp, err := parent.RPC().NSNodeRPC().FindNSNode(parent.AdminContext(), &pb.FindNSNodeRequest{
NsNodeId: nodeId,
})
if err != nil || resp.NsNode == nil {
continue
}
var installStatusMap maps.Map
if resp.NsNode.InstallStatus != nil {
installStatusMap = maps.Map{
"isRunning": resp.NsNode.InstallStatus.IsRunning,
"isFinished": resp.NsNode.InstallStatus.IsFinished,
"isOk": resp.NsNode.InstallStatus.IsOk,
"error": resp.NsNode.InstallStatus.Error,
"errorCode": resp.NsNode.InstallStatus.ErrorCode,
"updatedAt": resp.NsNode.InstallStatus.UpdatedAt,
}
}
result = append(result, maps.Map{
"id": nodeId,
"installStatus": installStatusMap,
})
}
return result
}

View File

@@ -1,6 +1,15 @@
package upgrade
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
import (
"encoding/json"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
@@ -11,5 +20,264 @@ func (this *IndexAction) Init() {
}
func (this *IndexAction) RunGet(params struct{}) {
// 加载升级配置
config, err := configloaders.LoadUpgradeConfig()
if err != nil {
this.ErrorPage(err)
return
}
this.Data["config"] = config
// 模块列表
modules := []maps.Map{}
// 1. 边缘节点 (EdgeNode)
nodeModule := this.loadEdgeNodeModule()
if nodeModule != nil {
modules = append(modules, nodeModule)
}
// 2. DNS节点 (EdgeDNS) — 仅Plus版本
if teaconst.IsPlus {
dnsClusters := loadDNSUpgradeModules(&this.ParentAction)
if len(dnsClusters) > 0 {
totalCount := 0
for _, c := range dnsClusters {
totalCount += c.GetInt("count")
}
modules = append(modules, maps.Map{
"name": "DNS节点",
"code": "dns",
"clusters": dnsClusters,
"count": totalCount,
})
}
}
// 3. HTTPDNS节点
httpdnsModule := this.loadHTTPDNSModule()
if httpdnsModule != nil {
modules = append(modules, httpdnsModule)
}
this.Data["modules"] = modules
this.Show()
}
func (this *IndexAction) RunPost(params struct {
AutoUpgrade bool
}) {
config, err := configloaders.LoadUpgradeConfig()
if err != nil {
this.ErrorPage(err)
return
}
config.AutoUpgrade = params.AutoUpgrade
err = configloaders.UpdateUpgradeConfig(config)
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}
// loadEdgeNodeModule 加载边缘节点模块的待升级信息
func (this *IndexAction) loadEdgeNodeModule() maps.Map {
// 获取所有集群
clustersResp, err := this.RPC().NodeClusterRPC().ListEnabledNodeClusters(this.AdminContext(), &pb.ListEnabledNodeClustersRequest{
Offset: 0,
Size: 10000,
})
if err != nil {
return nil
}
var clusterMaps []maps.Map
totalCount := 0
for _, cluster := range clustersResp.NodeClusters {
resp, err := this.RPC().NodeRPC().FindAllUpgradeNodesWithNodeClusterId(this.AdminContext(), &pb.FindAllUpgradeNodesWithNodeClusterIdRequest{
NodeClusterId: cluster.Id,
})
if err != nil {
continue
}
if len(resp.Nodes) == 0 {
continue
}
var nodeMaps []maps.Map
for _, nodeUpgrade := range resp.Nodes {
if nodeUpgrade.Node == nil {
continue
}
installStatusMap := decodeInstallStatusFromPB(nodeUpgrade.Node.InstallStatus)
accessIP, login, loginParams := decodeNodeAccessInfo(nodeUpgrade.Node.StatusJSON, nodeUpgrade.Node.NodeLogin, nodeUpgrade.Node.Name)
nodeMaps = append(nodeMaps, maps.Map{
"id": nodeUpgrade.Node.Id,
"name": nodeUpgrade.Node.Name,
"os": nodeUpgrade.Os,
"arch": nodeUpgrade.Arch,
"oldVersion": nodeUpgrade.OldVersion,
"newVersion": nodeUpgrade.NewVersion,
"isOn": nodeUpgrade.Node.IsOn,
"isUp": nodeUpgrade.Node.IsUp,
"accessIP": accessIP,
"login": login,
"loginParams": loginParams,
"installStatus": installStatusMap,
})
}
totalCount += len(nodeMaps)
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"nodes": nodeMaps,
"count": len(nodeMaps),
})
}
if len(clusterMaps) == 0 {
return nil
}
return maps.Map{
"name": "边缘节点",
"code": "node",
"clusters": clusterMaps,
"count": totalCount,
}
}
// loadHTTPDNSModule 加载HTTPDNS模块的待升级信息
func (this *IndexAction) loadHTTPDNSModule() maps.Map {
clustersResp, err := this.RPC().HTTPDNSClusterRPC().ListHTTPDNSClusters(this.AdminContext(), &pb.ListHTTPDNSClustersRequest{
Offset: 0,
Size: 10000,
})
if err != nil {
return nil
}
var clusterMaps []maps.Map
totalCount := 0
for _, cluster := range clustersResp.Clusters {
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
ClusterId: cluster.Id,
})
if err != nil {
continue
}
if len(resp.Nodes) == 0 {
continue
}
var nodeMaps []maps.Map
for _, nodeUpgrade := range resp.Nodes {
if nodeUpgrade.Node == nil {
continue
}
installStatusMap := decodeInstallStatusFromJSON(nodeUpgrade.Node.InstallStatusJSON)
accessIP, login, loginParams := decodeNodeAccessInfo(nodeUpgrade.Node.StatusJSON, nodeUpgrade.Node.NodeLogin, nodeUpgrade.Node.Name)
nodeMaps = append(nodeMaps, maps.Map{
"id": nodeUpgrade.Node.Id,
"name": nodeUpgrade.Node.Name,
"os": nodeUpgrade.Os,
"arch": nodeUpgrade.Arch,
"oldVersion": nodeUpgrade.OldVersion,
"newVersion": nodeUpgrade.NewVersion,
"isOn": nodeUpgrade.Node.IsOn,
"isUp": nodeUpgrade.Node.IsUp,
"accessIP": accessIP,
"login": login,
"loginParams": loginParams,
"installStatus": installStatusMap,
})
}
totalCount += len(nodeMaps)
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"nodes": nodeMaps,
"count": len(nodeMaps),
})
}
if len(clusterMaps) == 0 {
return nil
}
return maps.Map{
"name": "HTTPDNS节点",
"code": "httpdns",
"clusters": clusterMaps,
"count": totalCount,
}
}
// decodeInstallStatusFromPB 从 protobuf InstallStatus 解码安装状态
func decodeInstallStatusFromPB(status *pb.NodeInstallStatus) maps.Map {
if status == nil {
return nil
}
// 历史成功状态,在待升级列表中忽略
if status.IsFinished && status.IsOk {
return nil
}
return maps.Map{
"isRunning": status.IsRunning,
"isFinished": status.IsFinished,
"isOk": status.IsOk,
"error": status.Error,
"errorCode": status.ErrorCode,
"updatedAt": status.UpdatedAt,
}
}
// decodeInstallStatusFromJSON 从 JSON 字节解码安装状态
func decodeInstallStatusFromJSON(raw []byte) maps.Map {
if len(raw) == 0 {
return nil
}
result := maps.Map{}
_ = json.Unmarshal(raw, &result)
isFinished, _ := result["isFinished"].(bool)
isOk, _ := result["isOk"].(bool)
if isFinished && isOk {
return nil
}
return result
}
// decodeNodeAccessInfo 从节点状态和登录信息中提取 accessIP、login、loginParams
func decodeNodeAccessInfo(statusJSON []byte, nodeLogin *pb.NodeLogin, nodeName string) (accessIP string, login maps.Map, loginParams maps.Map) {
// 从 statusJSON 中提取 hostIP 作为 accessIP
if len(statusJSON) > 0 {
statusMap := maps.Map{}
_ = json.Unmarshal(statusJSON, &statusMap)
accessIP = strings.TrimSpace(statusMap.GetString("hostIP"))
}
if len(accessIP) == 0 {
accessIP = strings.TrimSpace(nodeName)
}
// 解码 login 信息
if nodeLogin != nil {
login = maps.Map{
"id": nodeLogin.Id,
"name": nodeLogin.Name,
"type": nodeLogin.Type,
}
if len(nodeLogin.Params) > 0 {
loginParams = maps.Map{}
_ = json.Unmarshal(nodeLogin.Params, &loginParams)
}
}
return
}

View File

@@ -13,7 +13,9 @@ func init() {
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
Helper(settingutils.NewHelper("upgrade")).
Prefix("/settings/upgrade").
Get("", new(IndexAction)).
GetPost("", new(IndexAction)).
Post("/upgradeNode", new(UpgradeNodeAction)).
Post("/status", new(StatusAction)).
EndAll()
})
}

View File

@@ -0,0 +1,80 @@
package upgrade
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
type StatusAction struct {
actionutils.ParentAction
}
func (this *StatusAction) RunPost(params struct {
NodeIdsJSON []byte // JSON: {"node": [1,2], "dns": [3], "httpdns": [4,5]}
}) {
var nodeIdsMap map[string][]int64
if len(params.NodeIdsJSON) > 0 {
_ = json.Unmarshal(params.NodeIdsJSON, &nodeIdsMap)
}
result := maps.Map{}
// EdgeNode 状态
if nodeIds, ok := nodeIdsMap["node"]; ok && len(nodeIds) > 0 {
var nodeStatuses []maps.Map
for _, nodeId := range nodeIds {
resp, err := this.RPC().NodeRPC().FindEnabledNode(this.AdminContext(), &pb.FindEnabledNodeRequest{NodeId: nodeId})
if err != nil || resp.Node == nil {
continue
}
var installStatusMap maps.Map
if resp.Node.InstallStatus != nil {
installStatusMap = maps.Map{
"isRunning": resp.Node.InstallStatus.IsRunning,
"isFinished": resp.Node.InstallStatus.IsFinished,
"isOk": resp.Node.InstallStatus.IsOk,
"error": resp.Node.InstallStatus.Error,
"errorCode": resp.Node.InstallStatus.ErrorCode,
"updatedAt": resp.Node.InstallStatus.UpdatedAt,
}
}
nodeStatuses = append(nodeStatuses, maps.Map{
"id": nodeId,
"installStatus": installStatusMap,
})
}
result["node"] = nodeStatuses
}
// DNS 状态
if nodeIds, ok := nodeIdsMap["dns"]; ok && len(nodeIds) > 0 {
result["dns"] = loadDNSNodeStatus(&this.ParentAction, nodeIds)
}
// HTTPDNS 状态
if nodeIds, ok := nodeIdsMap["httpdns"]; ok && len(nodeIds) > 0 {
var nodeStatuses []maps.Map
for _, nodeId := range nodeIds {
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{NodeId: nodeId})
if err != nil || resp.Node == nil {
continue
}
var installStatusMap maps.Map
if len(resp.Node.InstallStatusJSON) > 0 {
installStatusMap = maps.Map{}
_ = json.Unmarshal(resp.Node.InstallStatusJSON, &installStatusMap)
}
nodeStatuses = append(nodeStatuses, maps.Map{
"id": nodeId,
"installStatus": installStatusMap,
})
}
result["httpdns"] = nodeStatuses
}
this.Data["statuses"] = result
this.Success()
}

View File

@@ -0,0 +1,150 @@
package upgrade
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
type UpgradeNodeAction struct {
actionutils.ParentAction
}
func (this *UpgradeNodeAction) RunPost(params struct {
Module string // node, dns, httpdns
Scope string // all, module, cluster, node
ClusterId int64
NodeId int64
}) {
switch params.Scope {
case "node":
err := this.upgradeSingleNode(params.Module, params.NodeId)
if err != nil {
this.ErrorPage(err)
return
}
case "cluster":
err := this.upgradeCluster(params.Module, params.ClusterId)
if err != nil {
this.ErrorPage(err)
return
}
case "module":
err := this.upgradeModule(params.Module)
if err != nil {
this.ErrorPage(err)
return
}
case "all":
_ = this.upgradeModule("node")
_ = this.upgradeModule("dns")
_ = this.upgradeModule("httpdns")
}
this.Success()
}
func (this *UpgradeNodeAction) upgradeSingleNode(module string, nodeId int64) error {
switch module {
case "node":
_, err := this.RPC().NodeRPC().UpgradeNode(this.AdminContext(), &pb.UpgradeNodeRequest{NodeId: nodeId})
return err
case "dns":
return upgradeDNSNode(&this.ParentAction, nodeId)
case "httpdns":
_, err := this.RPC().HTTPDNSNodeRPC().UpgradeHTTPDNSNode(this.AdminContext(), &pb.UpgradeHTTPDNSNodeRequest{NodeId: nodeId})
return err
}
return nil
}
func (this *UpgradeNodeAction) upgradeCluster(module string, clusterId int64) error {
switch module {
case "node":
resp, err := this.RPC().NodeRPC().FindAllUpgradeNodesWithNodeClusterId(this.AdminContext(), &pb.FindAllUpgradeNodesWithNodeClusterIdRequest{
NodeClusterId: clusterId,
})
if err != nil {
return err
}
for _, nodeUpgrade := range resp.Nodes {
if nodeUpgrade.Node == nil {
continue
}
_, _ = this.RPC().NodeRPC().UpgradeNode(this.AdminContext(), &pb.UpgradeNodeRequest{NodeId: nodeUpgrade.Node.Id})
}
case "dns":
this.upgradeDNSCluster(clusterId)
case "httpdns":
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
ClusterId: clusterId,
})
if err != nil {
return err
}
for _, nodeUpgrade := range resp.Nodes {
if nodeUpgrade.Node == nil {
continue
}
_, _ = this.RPC().HTTPDNSNodeRPC().UpgradeHTTPDNSNode(this.AdminContext(), &pb.UpgradeHTTPDNSNodeRequest{NodeId: nodeUpgrade.Node.Id})
}
}
return nil
}
func (this *UpgradeNodeAction) upgradeModule(module string) error {
switch module {
case "node":
clustersResp, err := this.RPC().NodeClusterRPC().ListEnabledNodeClusters(this.AdminContext(), &pb.ListEnabledNodeClustersRequest{
Offset: 0,
Size: 10000,
})
if err != nil {
return err
}
for _, cluster := range clustersResp.NodeClusters {
_ = this.upgradeCluster("node", cluster.Id)
}
case "dns":
dnsClusters := loadDNSUpgradeModules(&this.ParentAction)
for _, c := range dnsClusters {
this.upgradeDNSClusterFromMap(c)
}
case "httpdns":
clustersResp, err := this.RPC().HTTPDNSClusterRPC().ListHTTPDNSClusters(this.AdminContext(), &pb.ListHTTPDNSClustersRequest{
Offset: 0,
Size: 10000,
})
if err != nil {
return err
}
for _, cluster := range clustersResp.Clusters {
_ = this.upgradeCluster("httpdns", cluster.Id)
}
}
return nil
}
// upgradeDNSCluster 根据集群ID升级DNS节点
func (this *UpgradeNodeAction) upgradeDNSCluster(clusterId int64) {
dnsClusters := loadDNSUpgradeModules(&this.ParentAction)
for _, c := range dnsClusters {
if c.GetInt64("id") == clusterId {
this.upgradeDNSClusterFromMap(c)
break
}
}
}
// upgradeDNSClusterFromMap 从maps.Map中提取节点ID并升级
func (this *UpgradeNodeAction) upgradeDNSClusterFromMap(c maps.Map) {
nodesVal := c.Get("nodes")
if nodeMaps, ok := nodesVal.([]maps.Map); ok {
for _, n := range nodeMaps {
nodeId := n.GetInt64("id")
if nodeId > 0 {
_ = upgradeDNSNode(&this.ParentAction, nodeId)
}
}
}
}

View File

@@ -25,20 +25,16 @@ func (this *CreatePopupAction) Init() {
func (this *CreatePopupAction) RunGet(params struct{}) {
// 检查是否启用了 HTTPDNS 功能(全局用户注册设置中)
var hasHTTPDNSFeature = false
var defaultHttpdnsClusterId int64 = 0
resp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.AdminContext(), &pb.ReadSysSettingRequest{Code: systemconfigs.SettingCodeUserRegisterConfig})
if err == nil && len(resp.ValueJSON) > 0 {
var config = userconfigs.DefaultUserRegisterConfig()
if json.Unmarshal(resp.ValueJSON, config) == nil {
hasHTTPDNSFeature = config.HTTPDNSIsOn
if len(config.HTTPDNSDefaultClusterIds) > 0 {
defaultHttpdnsClusterId = config.HTTPDNSDefaultClusterIds[0]
}
}
}
this.Data["hasHTTPDNSFeature"] = hasHTTPDNSFeature
this.Data["httpdnsClusterId"] = defaultHttpdnsClusterId
this.Data["httpdnsClusterId"] = 0
// 加载所有 HTTPDNS 集群
var httpdnsClusters = []maps.Map{}

View File

@@ -22046,15 +22046,16 @@ Vue.component("ssl-certs-box", {
<span class="grey" v-if="description.length > 0">{{description}}</span>
<div class="ui divider" v-if="buttonsVisible()"></div>
</div>
<div v-if="buttonsVisible()">
<div v-if="buttonsVisible()" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:nowrap;white-space:nowrap;">
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp;
<span class="disabled">|</span> &nbsp;
<span class="disabled" style="margin:0 0.5rem;">|</span>
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button> &nbsp;
</div>
</div>`
})
Vue.component("ssl-certs-view", {
props: ["v-certs"],
data: function () {
@@ -22518,12 +22519,16 @@ Vue.component("ssl-config-box", {
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
<div class="ui divider"></div>
</div>
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp;
<span class="disabled">|</span> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button> &nbsp;
<span class="disabled">|</span> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; white-space: nowrap;">
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
<span class="disabled" style="margin: 0 0.5rem;">|</span>
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传证书</button>
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
<template v-if="vServerId != null && vServerId > 0">
<span class="disabled" style="margin: 0 0.5rem;">|</span>
<button class="ui button tiny" type="button" @click.prevent="requestCert()">申请免费证书</button>
</template>
</div>
</td>
</tr>
<tr>

View File

@@ -22046,15 +22046,16 @@ Vue.component("ssl-certs-box", {
<span class="grey" v-if="description.length > 0">{{description}}</span>
<div class="ui divider" v-if="buttonsVisible()"></div>
</div>
<div v-if="buttonsVisible()">
<div v-if="buttonsVisible()" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:nowrap;white-space:nowrap;">
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp;
<span class="disabled">|</span> &nbsp;
<span class="disabled" style="margin:0 0.5rem;">|</span>
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button> &nbsp;
</div>
</div>`
})
Vue.component("ssl-certs-view", {
props: ["v-certs"],
data: function () {
@@ -22518,12 +22519,16 @@ Vue.component("ssl-config-box", {
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
<div class="ui divider"></div>
</div>
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp;
<span class="disabled">|</span> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button> &nbsp;
<span class="disabled">|</span> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; white-space: nowrap;">
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
<span class="disabled" style="margin: 0 0.5rem;">|</span>
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传证书</button>
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
<template v-if="vServerId != null && vServerId > 0">
<span class="disabled" style="margin: 0 0.5rem;">|</span>
<button class="ui button tiny" type="button" @click.prevent="requestCert()">申请免费证书</button>
</template>
</div>
</td>
</tr>
<tr>

View File

@@ -160,9 +160,9 @@ Vue.component("ssl-certs-box", {
<span class="grey" v-if="description.length > 0">{{description}}</span>
<div class="ui divider" v-if="buttonsVisible()"></div>
</div>
<div v-if="buttonsVisible()">
<div v-if="buttonsVisible()" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:nowrap;white-space:nowrap;">
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp;
<span class="disabled">|</span> &nbsp;
<span class="disabled" style="margin:0 0.5rem;">|</span>
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button> &nbsp;
</div>

View File

@@ -427,12 +427,16 @@ Vue.component("ssl-config-box", {
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
<div class="ui divider"></div>
</div>
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp;
<span class="disabled">|</span> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button> &nbsp;
<span class="disabled">|</span> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; white-space: nowrap;">
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
<span class="disabled" style="margin: 0 0.5rem;">|</span>
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传证书</button>
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
<template v-if="vServerId != null && vServerId > 0">
<span class="disabled" style="margin: 0 0.5rem;">|</span>
<button class="ui button tiny" type="button" @click.prevent="requestCert()">申请免费证书</button>
</template>
</div>
</td>
</tr>
<tr>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>{$ htmlEncode .teaTitle}</title>
<meta charset="UTF-8" />
@@ -46,34 +47,47 @@
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all" />
</head>
<body>
<div>
<!-- 顶部导航 -->
<div class="ui menu top-nav blue inverted small borderless" :class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
<div class="ui menu top-nav blue inverted small borderless"
:class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
<a href="/" class="item">
<i class="ui icon leaf" v-if="teaLogoFileId == 0"></i><img alt="logo" v-if="teaLogoFileId > 0" :src="'/ui/image/' + teaLogoFileId" style="width: auto;height: 1.6em"/> &nbsp; {{teaTitle}}&nbsp;<sup v-if="teaShowVersion">v{{teaVersion}}<span v-if="teaVersion.split('.').length == 4" title="当前版本为测试版,再次感谢您参与测试"> beta</span></sup> &nbsp;
<i class="ui icon leaf" v-if="teaLogoFileId == 0"></i><img alt="logo" v-if="teaLogoFileId > 0"
:src="'/ui/image/' + teaLogoFileId" style="width: auto;height: 1.6em" /> &nbsp;
{{teaTitle}}&nbsp;<sup v-if="teaShowVersion">v{{teaVersion}}<span
v-if="teaVersion.split('.').length == 4" title="当前版本为测试版,再次感谢您参与测试"> beta</span></sup> &nbsp;
</a>
<div class="right menu">
<!-- 集群同步 -->
<a href="" class="item" v-if="teaCheckNodeTasks && doingNodeTasks.isUpdated" @click.prevent="showNodeTasks()">
<span v-if="!doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span"><i class="icon cloud disabled"></i><span class="disabled">已同步节点</span></span>
<span v-if="doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span rotate"><i class="icon cloud"></i><span>正在同步节点...</span></span>
<a href="" class="item" v-if="teaCheckNodeTasks && doingNodeTasks.isUpdated"
@click.prevent="showNodeTasks()">
<span v-if="!doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span"><i
class="icon cloud disabled"></i><span class="disabled">已同步节点</span></span>
<span v-if="doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span rotate"><i
class="icon cloud"></i><span>正在同步节点...</span></span>
<span v-if="doingNodeTasks.hasError" class="red"><i class="icon cloud"></i>节点同步失败</span>
</a>
<!-- DNS同步 -->
<a href="" class="item" v-if="teaCheckDNSTasks && doingDNSTasks.isUpdated" @click.prevent="showDNSTasks()">
<span v-if="!doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span"><i class="icon globe disabled"></i><span class="disabled">已同步DNS</span></span>
<span v-if="doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span rotate"><i class="icon globe"></i><span>正在同步DNS...</span></span>
<a href="" class="item" v-if="teaCheckDNSTasks && doingDNSTasks.isUpdated"
@click.prevent="showDNSTasks()">
<span v-if="!doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span"><i
class="icon globe disabled"></i><span class="disabled">已同步DNS</span></span>
<span v-if="doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span rotate"><i
class="icon globe"></i><span>正在同步DNS...</span></span>
<span v-if="doingDNSTasks.hasError" class="red"><i class="icon globe"></i>DNS同步失败</span>
</a>
<!-- 消息 -->
<a href="" class="item" :class="{active:teaMenu == 'message'}" @click.prevent="showMessages()">
<span v-if="globalMessageBadge > 0" class="blink hover-span"><i class="icon bell"></i><span>消息({{globalMessageBadge}}) </span></span>
<span v-if="globalMessageBadge == 0" class="hover-span"><i class="icon bell disabled"></i><span class="disabled">消息(0)</span></span>
<span v-if="globalMessageBadge > 0" class="blink hover-span"><i
class="icon bell"></i><span>消息({{globalMessageBadge}}) </span></span>
<span v-if="globalMessageBadge == 0" class="hover-span"><i class="icon bell disabled"></i><span
class="disabled">消息(0)</span></span>
</a>
<!-- 用户信息 -->
@@ -83,10 +97,12 @@
<span class="hover-span"><span class="disabled">{{teaUsername}}</span></span>
</a>
<a href="" class="item" title="switch language" @click.prevent="switchLang" v-show="false"><i class="icon language"></i> </a>
<a href="" class="item" title="switch language" @click.prevent="switchLang" v-show="false"><i
class="icon language"></i> </a>
<!-- 背景颜色 -->
<a href="" class="item" title="点击切换界面风格" @click.prevent="changeTheme()" v-if="false"><i class="icon adjust"></i></a>
<a href="" class="item" title="点击切换界面风格" @click.prevent="changeTheme()" v-if="false"><i
class="icon adjust"></i></a>
<!-- 企业版 -->
<!-- <a :href="'/settings/authority'" class="item" title="商业版" :v-if="teaIsPlus"><i class="icon gem outline yellow"></i></a>-->
@@ -109,16 +125,24 @@
<!-- 模块 -->
<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, expend: teaMenu == module.code}" :style="(teaMenu == module.code && teaSubMenu.length == 0) ? 'background: rgba(230, 230, 230, 0.45) !important;' : ''" v-if="module.isOn !== false">
<a class="item" :href="Tea.url(module.code)"
:class="{active:teaMenu == module.code && teaSubMenu.length == 0, separator:module.code.length == 0, expend: teaMenu == module.code}"
:style="(teaMenu == module.code && teaSubMenu.length == 0) ? 'background: rgba(230, 230, 230, 0.45) !important;' : ''"
v-if="module.isOn !== false">
<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 class="module-name">{{module.name}}</span>
</span>
<div class="subtitle" v-if="module.subtitle != null && module.subtitle.length > 0">{{module.subtitle}}</div>
<div class="subtitle" v-if="module.subtitle != null && module.subtitle.length > 0">
{{module.subtitle}}</div>
</a>
<div v-if="teaMenu == module.code" class="sub-items">
<a class="item" :class="{separator:subItem.name == '-', active: subItem.code == teaSubMenu}" v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url" :style="(subItem.code == teaSubMenu) ? 'background: rgba(230, 230, 230, 0.55) !important;' : ''"><i class="icon angle right" v-if="subItem.name != '-' && subItem.code == teaSubMenu"></i> <span v-if="subItem.name != '-'">{{subItem.name}}</span>
<a class="item" :class="{separator:subItem.name == '-', active: subItem.code == teaSubMenu}"
v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url"
:style="(subItem.code == teaSubMenu) ? 'background: rgba(230, 230, 230, 0.55) !important;' : ''"><i
class="icon angle right" v-if="subItem.name != '-' && subItem.code == teaSubMenu"></i>
<span v-if="subItem.name != '-'">{{subItem.name}}</span>
<span class="ui label tiny red" v-if="subItem.badge != null && subItem.badge > 0">
<span v-if="subItem.badge < 100">{{subItem.badge}}</span>
<span v-else>99+</span>
@@ -130,11 +154,17 @@
</div>
<!-- 右侧主操作栏 -->
<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':!teaShowOpenSourceInfo}" v-cloak="">
<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':!teaShowOpenSourceInfo}"
v-cloak="">
<!-- 操作菜单 -->
<div class="ui top menu tabular tab-menu small" v-if="teaTabbar.length > 1">
<a class="item" v-for="item in teaTabbar" :class="{'active':item.isActive && !item.isDisabled, right:item.isRight, title: item.isTitle, icon: item.icon != null && item.icon.length > 0, disabled: item.isDisabled}" :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>
<a class="item" v-for="item in teaTabbar"
:class="{'active':item.isActive && !item.isDisabled, right:item.isRight, title: item.isTitle, icon: item.icon != null && item.icon.length > 0, disabled: item.isDisabled}"
: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>
<var v-if="item.isTitle && typeof _data.node == 'object'">{{node.name}}</var>
<div class="bottom-indicator" v-if="item.isActive && !item.isTitle"></div>
</a>
@@ -154,4 +184,5 @@
{$echo "footer"}
</body>
</html>

View File

@@ -3,60 +3,76 @@
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
scrollbar-width: thin;
}
.clear {
clear: both;
}
.hidden {
display: none;
}
pre {
white-space: pre-wrap;
}
a.disabled,
a.disabled:hover,
a.disabled:active,
span.disabled {
color: #ccc !important;
}
a.enabled,
span.enabled,
span.green {
color: #21ba45;
}
span.grey,
label.grey,
p.grey {
color: grey !important;
}
p.grey {
margin-top: 0.8em;
}
span.red,
pre.red {
color: #db2828;
}
span.blue {
color: #4183c4;
}
pre:not(.CodeMirror-line) {
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important;
}
tbody {
background: transparent;
}
.table.width30 {
width: 30em !important;
}
.table.width35 {
width: 35em !important;
}
.table.width40 {
width: 40em !important;
}
.table th,
.table td {
font-size: 0.9em !important;
}
p.comment,
div.comment {
color: #959da6;
@@ -65,37 +81,46 @@ div.comment {
word-break: break-all;
line-height: 1.8;
}
p.comment em,
div.comment em {
font-style: italic !important;
}
.truncate {
white-space: nowrap;
-ms-text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
}
div.margin,
p.margin {
margin-top: 1em;
}
/** 操作按钮容器 **/
.op.one {
width: 4em;
}
.op.two {
width: 7.4em;
}
.op.three {
width: 9em;
}
.op.four {
width: 10em;
}
/** 扩展UI **/
.field.text {
padding: 0.5em;
}
/** 右侧主操作区 **/
.main {
position: absolute;
@@ -105,166 +130,210 @@ p.margin {
padding-right: 1em;
right: 1em;
}
@media screen and (max-width: 512px) {
.main {
left: 4em;
}
}
.main.without-menu {
left: 9em;
}
.main.without-secondary-menu {
top: 2.9em;
}
@media screen and (max-width: 512px) {
.main.without-menu {
left: 4em;
}
}
.main table td.title {
width: 10em;
}
.main table td.middle-title {
width: 14em;
}
.main table td {
vertical-align: top;
}
table td.color-border {
border-left: 1px #276ac6 solid !important;
}
.main table td.vertical-top {
vertical-align: top;
}
.main table td.vertical-middle {
vertical-align: middle;
}
.main table td[colspan="2"] a {
font-weight: normal;
}
.main table td em {
font-weight: normal;
font-style: normal;
font-size: 0.9em;
}
.main h3 {
font-weight: normal;
margin-top: 0.5em !important;
}
.main h3 span {
font-size: 0.8em;
}
.main h3 span.label {
color: #6435c9;
}
.main h3 a {
margin-left: 1em;
font-size: 14px !important;
right: 1em;
}
.main h3 a::before {
content: "[";
}
.main h3 a::after {
content: "]";
}
.main h4 {
font-weight: normal;
}
.main td span.small {
font-size: 0.8em;
}
.main .button.mini {
font-size: 0.8em;
padding: 0.2em;
margin-left: 1em;
}
/** 右侧文本子菜单 **/
.text.menu {
overflow-x: auto;
}
.text.menu::-webkit-scrollbar {
width: 4px;
height: 4px;
}
/** Vue **/
[v-cloak] {
display: none !important;
}
/** auto complete **/
.autocomplete-box .menu {
background: #eee !important;
}
.autocomplete-box .menu::-webkit-scrollbar {
width: 4px;
}
.autocomplete-box .menu .item {
border-top: none !important;
}
select.auto-width {
width: auto !important;
}
/** column **/
@media screen and (max-width: 512px) {
.column:not(.one) {
width: 100% !important;
}
}
/** label **/
label[for] {
cursor: pointer !important;
}
label.blue {
color: #2185d0 !important;
}
/** Menu **/
.first-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.first-menu .divider {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text em {
font-style: normal;
}
.second-menu .divider {
margin-top: 0 !important;
}
.menu a {
outline: none;
}
/** var **/
span.olive,
var.olive {
color: #b5cc18 !important;
}
span.dash {
border-bottom: 1px dashed grey;
}
span.hover:hover {
background: #eee;
}
/** Message **/
.message .gopher {
width: 30px;
margin-right: 10px;
}
/** checkbox **/
.checkbox label a,
.checkbox label {
font-size: 0.8em !important;
}
/** page **/
.page {
margin-top: 1em;
border-left: 1px solid #ddd;
}
.page a {
display: inline-block;
background: #fafafa;
@@ -275,40 +344,50 @@ span.hover:hover {
border: 1px solid #ddd;
border-left: 0;
}
.page a.active {
background: #2185d0 !important;
color: white;
}
.page a:hover {
background: #eee;
}
.page select {
padding-top: 0.3em !important;
padding-bottom: 0.3em !important;
}
.swal2-html-container {
overflow-x: hidden;
}
.swal2-confirm:focus,
.swal2-cancel:focus,
.swal2-close:focus {
border: 3px #ddd solid !important;
}
.swal2-confirm,
.swal2-cancel,
.swal2-close {
border: 3px #fff solid !important;
}
.swal2-cancel {
margin-left: 2em !important;
}
input.error {
border: 1px #e0b4b4 solid !important;
}
textarea.wide-code {
font-family: Menlo, Monaco, "Courier New", monospace !important;
line-height: 1.6 !important;
}
.combo-box .menu {
max-height: 17em;
overflow-y: auto;
@@ -317,9 +396,11 @@ textarea.wide-code {
border-top: 0;
z-index: 100;
}
.combo-box .menu::-webkit-scrollbar {
width: 4px;
}
code-label {
background: #fff;
border: 1px solid rgba(34, 36, 38, 0.15);
@@ -333,4 +414,62 @@ code-label {
font-weight: 700;
vertical-align: baseline;
}
/*# sourceMappingURL=@layout_popup.css.map */
/* Override Primary Button Color for WAF Platform */
.ui.primary.button,
.ui.primary.buttons .button {
background-color: #0f2c54 !important;
color: #ffffff !important;
}
.ui.primary.button:hover,
.ui.primary.buttons .button:hover {
background-color: #0a1f3a !important;
}
.ui.primary.button:focus,
.ui.primary.buttons .button:focus {
background-color: #08192e !important;
}
.ui.primary.button:active,
.ui.primary.buttons .button:active,
.ui.primary.active.button {
background-color: #050d18 !important;
}
.text-primary,
.blue {
color: #0f2c54 !important;
}
/* Override Semantic UI Default Blue */
.ui.blue.button,
.ui.blue.buttons .button {
background-color: #0f2c54 !important;
color: #ffffff !important;
}
.ui.blue.button:hover,
.ui.blue.buttons .button:hover {
background-color: #0a1f3a !important;
}
.ui.basic.blue.button,
.ui.basic.blue.buttons .button {
box-shadow: 0 0 0 1px #0f2c54 inset !important;
color: #0f2c54 !important;
}
.ui.basic.blue.button:hover,
.ui.basic.blue.buttons .button:hover {
background: transparent !important;
box-shadow: 0 0 0 1px #0a1f3a inset !important;
color: #0a1f3a !important;
}
.ui.menu .active.item {
border-color: #2185d0 !important;
color: #2185d0 !important;
}

View File

@@ -8,14 +8,8 @@
<div class="item"><strong>上传 SDK</strong></div>
</second-menu>
<form method="post"
enctype="multipart/form-data"
class="ui form"
data-tea-action="$"
data-tea-timeout="300"
data-tea-before="beforeUpload"
data-tea-done="doneUpload"
data-tea-success="successUpload">
<form method="post" enctype="multipart/form-data" class="ui form" data-tea-action="$" data-tea-timeout="300"
data-tea-before="beforeUpload" data-tea-done="doneUpload" data-tea-success="successUpload">
<csrf-token></csrf-token>
<input type="hidden" name="appId" :value="app.id" />
@@ -41,14 +35,14 @@
<td class="title">SDK 包</td>
<td>
<input type="file" name="sdkFile" accept=".zip" />
<p class="comment">支持 zip 包,例如 `httpdns-sdk-android.zip`。</p>
<p class="comment">支持 zip 包,例如 `httpdns-sdk-android.zip`,单文件最大 20MB</p>
</td>
</tr>
<tr>
<td class="title">集成文档</td>
<td>
<input type="file" name="docFile" accept=".md,text/markdown" />
<p class="comment">支持 Markdown 文件(`.md`)。</p>
<p class="comment">支持 Markdown 文件(`.md`,单文件最大 20MB</p>
</td>
</tr>
</table>

View File

@@ -8,6 +8,17 @@
}
this.beforeUpload = function () {
var maxSize = 20 * 1024 * 1024; // 20MB
var inputs = document.querySelectorAll("input[type=file]");
for (var i = 0; i < inputs.length; i++) {
var files = inputs[i].files;
if (files != null && files.length > 0) {
if (files[0].size > maxSize) {
teaweb.warn("文件 \"" + files[0].name + "\" 超过 20MB 限制(" + (files[0].size / 1024 / 1024).toFixed(1) + "MB请压缩后重试");
return false;
}
}
}
this.isUploading = true;
};

View File

@@ -127,7 +127,7 @@
<td>
v{{node.status.buildVersion}}
&nbsp;
<a :href="'/httpdns/clusters/upgradeRemote?clusterId=' + clusterId + '&nodeId=' + node.id" v-if="shouldUpgrade"><span class="red">发现新版本 v{{newVersion}} &raquo;</span></a>
<a :href="'/httpdns/clusters/cluster/upgradeRemote?clusterId=' + clusterId" v-if="shouldUpgrade"><span class="red">发现新版本 v{{newVersion}} &raquo;</span></a>
</td>
</tr>
<tr v-if="node.status.exePath != null && node.status.exePath.length > 0">

View File

@@ -66,30 +66,37 @@
<tr>
<td>自动远程启动</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="autoRemoteStart" value="1" v-model="settings.autoRemoteStart" />
<label></label>
</div>
<checkbox name="autoRemoteStart" v-model="settings.autoRemoteStart"></checkbox>
<p class="comment">当检测到节点离线时自动尝试远程启动前提是节点已经设置了SSH登录认证</p>
</td>
</tr>
<tr>
<td>访问日志</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="accessLogIsOn" value="1" v-model="settings.accessLogIsOn" />
<label></label>
</div>
<checkbox name="accessLogIsOn" v-model="settings.accessLogIsOn"></checkbox>
<p class="comment">启用后HTTPDNS 节点将会记录客户端的请求访问日志。</p>
</td>
</tr>
<tr>
<td>时区</td>
<td>
<div>
<span class="ui label basic small" v-if="timeZoneLocation != null">当前:{{timeZoneLocation.name}}
({{timeZoneLocation.offset}})</span>
</div>
<div style="margin-top: 0.5em">
<select class="ui dropdown auto-width" name="timeZone" v-model="settings.timeZone">
<option v-for="tz in timeZoneLocations" :value="tz.name">{{tz.name}} ({{tz.offset}})
</option>
</select>
</div>
<p class="comment">HTTPDNS 节点进程使用的时区设置,默认为 Asia/Shanghai。</p>
</td>
</tr>
<tr>
<td>启用当前集群</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" v-model="settings.isOn" />
<label></label>
</div>
<checkbox name="isOn" v-model="settings.isOn"></checkbox>
<p class="comment">取消启用后,该集群不会参与 HTTPDNS 服务。</p>
</td>
</tr>

View File

@@ -7,4 +7,27 @@ Tea.context(function () {
if (!this.settings) {
this.settings = {}
}
let toBool = function (v) {
if (typeof v === "boolean") {
return v
}
if (typeof v === "number") {
return v === 1
}
if (typeof v === "string") {
let s = v.toLowerCase().trim()
return s === "1" || s === "true" || s === "on" || s === "yes" || s === "enabled"
}
return false
}
this.settings.autoRemoteStart = toBool(this.settings.autoRemoteStart)
this.settings.accessLogIsOn = toBool(this.settings.accessLogIsOn)
this.settings.isOn = toBool(this.settings.isOn)
if (!this.settings.timeZone || this.settings.timeZone.length == 0) {
this.settings.timeZone = "Asia/Shanghai"
}
})

View File

@@ -43,7 +43,7 @@
<tr>
<td>节点安装根目录</td>
<td>
<input type="text" name="installDir" maxlength="255" value="/opt/edge-httpdns" />
<input type="text" name="installDir" maxlength="255" value="/root/edge-httpdns" />
<p class="comment">边缘节点安装 HTTPDNS 服务的默认目录。</p>
</td>
</tr>

View File

@@ -19,7 +19,7 @@
<tr>
<td>安装目录</td>
<td>
<input type="text" name="installDir" maxlength="100" :value="cluster.installDir || '/opt/edge-httpdns'" />
<input type="text" name="installDir" maxlength="100" :value="cluster.installDir || '/root/edge-httpdns'" />
<p class="comment">默认使用集群配置目录。</p>
</td>
</tr>

View File

@@ -51,6 +51,12 @@
:class="{red:cluster.countAllNodes > cluster.countActiveNodes}">{{cluster.countAllNodes}}</span>
</a>
<span class="disabled" v-else="">-</span>
<div v-if="cluster.countUpgradeNodes > 0" style="margin-top:0.5em">
<a :href="'/httpdns/clusters/cluster/upgradeRemote?clusterId=' + cluster.id" title="点击进入远程升级页面">
<span class="red">有{{cluster.countUpgradeNodes}}个节点需要升级</span>
</a>
</div>
</td>
<td class="center">
<a :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id" v-if="cluster.countActiveNodes > 0">

View File

@@ -1,3 +1,62 @@
{$layout}
{$layout}
<p class="ui message error">姝ゅ姛鑳芥殏鏈紑鏀俱€?/p>
<p class="comment" v-if="nodes.length == 0">暂时没有需要升级的节点。</p>
<div v-if="nodes.length > 0">
<h3>
所有需要升级的节点
<button class="ui button primary tiny" v-if="countCheckedNodes() > 0" @click.prevent="installBatch()">批量升级({{countCheckedNodes()}})</button>
</h3>
<table class="ui table selectable celled">
<thead>
<tr>
<th style="width:3em">
<checkbox @input="checkNodes"></checkbox>
</th>
<th>节点名</th>
<th>访问IP</th>
<th>SSH地址</th>
<th>版本变化</th>
<th class="four wide">节点状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="node in nodes">
<td>
<checkbox v-model="node.isChecked" v-if="node.installStatus == null || !node.installStatus.isOk"></checkbox>
</td>
<td>
<a :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id">{{node.name}}</a>
<a :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id" title="节点详情" style="margin-left: 0.4em">[详情]</a>
</td>
<td>
<span v-if="node.accessIP.length > 0" class="ui label tiny basic">{{node.accessIP}}</span>
<span v-else class="disabled">-</span>
</td>
<td>
<span v-if="node.login != null && node.login.type == 'ssh' && node.loginParams != null && node.loginParams.host != null && node.loginParams.host.length > 0">
{{node.loginParams.host}}:{{node.loginParams.port}}
</span>
<span v-else class="disabled">没有设置</span>
</td>
<td>v{{node.oldVersion}} -&gt; v{{node.newVersion}}</td>
<td>
<div v-if="node.installStatus != null && (node.installStatus.isRunning || node.installStatus.isFinished)">
<div v-if="node.installStatus.isRunning" class="blue">升级中...</div>
<div v-if="node.installStatus.isFinished">
<span v-if="node.installStatus.isOk" class="green">已升级成功</span>
<span v-if="!node.installStatus.isOk" class="red">升级过程中发生错误:{{node.installStatus.error}}</span>
</div>
</div>
<span v-else class="disabled">等待升级</span>
</td>
<td>
<a href="" @click.prevent="installNode(node)" v-if="!isInstalling">升级</a>
<span v-if="isInstalling && node.isInstalling">升级中...</span>
<span v-if="isInstalling && !node.isInstalling" class="disabled">升级</span>
</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,147 @@
Tea.context(function () {
this.isInstalling = false
this.isBatch = false
let installingNode = null
this.nodes.forEach(function (v) {
v.isChecked = false
})
this.$delay(function () {
this.reload()
})
let that = this
this.checkNodes = function (isChecked) {
this.nodes.forEach(function (v) {
v.isChecked = isChecked
})
}
this.countCheckedNodes = function () {
return that.nodes.$count(function (k, v) {
return v.isChecked
})
}
this.installNode = function (node) {
let that = this
if (this.isBatch) {
installingNode = node
that.isInstalling = true
node.isInstalling = true
that.$post("$")
.params({
nodeId: node.id
})
} else {
teaweb.confirm("确定要开始升级此节点吗?", function () {
installingNode = node
that.isInstalling = true
node.isInstalling = true
that.$post("$")
.params({
nodeId: node.id
})
})
}
}
this.installBatch = function () {
let that = this
this.isBatch = true
teaweb.confirm("确定要批量升级选中的节点吗?", function () {
that.installNext()
})
}
this.installNext = function () {
let nextNode = this.nodes.$find(function (k, v) {
return v.isChecked
})
if (nextNode == null) {
teaweb.success("全部升级成功", function () {
teaweb.reload()
})
} else {
this.installNode(nextNode)
}
return
}
this.reload = function () {
let that = this
if (installingNode != null) {
this.$post("/httpdns/clusters/upgradeStatus")
.params({
nodeId: installingNode.id
})
.success(function (resp) {
if (resp.data.status != null) {
installingNode.installStatus = resp.data.status
if (installingNode.installStatus.isFinished) {
if (installingNode.installStatus.isOk) {
installingNode.isChecked = false
installingNode = null
if (that.isBatch) {
that.installNext()
} else {
teaweb.success("升级成功", function () {
teaweb.reload()
})
}
} else {
let nodeId = installingNode.id
let errMsg = installingNode.installStatus.error
that.isInstalling = false
installingNode.isInstalling = false
installingNode = null
switch (resp.data.status.errorCode) {
case "EMPTY_LOGIN":
case "EMPTY_SSH_HOST":
case "EMPTY_SSH_PORT":
case "EMPTY_GRANT":
teaweb.warn("需要填写SSH登录信息", function () {
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + nodeId, {
height: "20em",
callback: function () {
teaweb.reload()
}
})
})
return
case "CREATE_ROOT_DIRECTORY_FAILED":
teaweb.warn("创建根目录失败,请检查目录权限或手工创建:" + errMsg)
return
case "INSTALL_HELPER_FAILED":
teaweb.warn("安装助手失败:" + errMsg)
return
case "TEST_FAILED":
teaweb.warn("环境测试失败:" + errMsg)
return
case "RPC_TEST_FAILED":
teaweb.confirm("html:要升级的节点到API服务之间的RPC通讯测试失败具体错误" + errMsg + "<br/>现在修改API信息", function () {
window.location = "/settings/api"
})
return
default:
teaweb.warn("升级失败:" + errMsg)
}
}
}
}
})
.done(function () {
setTimeout(this.reload, 3000)
})
} else {
setTimeout(this.reload, 3000)
}
}
})

View File

@@ -52,7 +52,7 @@
</thead>
<tr v-for="node in nodes">
<td class="node-name-td"><a :href="'/ns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id"><keyword :v-word="keyword">{{node.name}}</keyword></a>
<div v-if="node.status != null && node.status.version.length > 0 && node.status.version != latestVersion">
<div v-if="node.status != null && node.status.version.length > 0 && latestVersion.length > 0 && Tea.versionCompare(latestVersion, node.status.version) > 0">
<span class="small red">v{{node.status.version}} -&gt; v{{latestVersion}}</span>
</div>

View File

@@ -32,14 +32,7 @@
<p class="comment">定制自己的版本号,留空表示使用系统自带的版本号。</p>
</td>
</tr>
<tr v-show="teaIsPlus">
<td>显示模块</td>
<td>
<checkbox name="supportModuleCDN" v-model="supportModuleCDN">CDN</checkbox> &nbsp; &nbsp; &nbsp;
<checkbox name="supportModuleNS" v-model="supportModuleNS" v-show="nsIsVisible">智能DNS</checkbox>
<p class="comment">当前管理系统中可以显示的模块,不能为空。</p>
</td>
</tr>
<tr>
<td>显示财务相关功能</td>
<td>
@@ -51,7 +44,8 @@
<td>浏览器图标</td>
<td>
<div v-if="config.faviconFileId > 0">
<a :href="'/ui/image/' + config.faviconFileId" target="_blank"><img alt="" :src="'/ui/image/' + config.faviconFileId" style="width:32px;border:1px #ccc solid;"/></a>
<a :href="'/ui/image/' + config.faviconFileId" target="_blank"><img alt=""
:src="'/ui/image/' + config.faviconFileId" style="width:32px;border:1px #ccc solid;" /></a>
</div>
<div v-else>
<span class="disabled">还没有上传。</span>
@@ -66,7 +60,8 @@
<td>Logo</td>
<td>
<div v-if="config.logoFileId > 0">
<a :href="'/ui/image/' + config.logoFileId" target="_blank"><img :src="'/ui/image/' + config.logoFileId" style="width:32px;border:1px #ccc solid;"/></a>
<a :href="'/ui/image/' + config.logoFileId" target="_blank"><img
:src="'/ui/image/' + config.logoFileId" style="width:32px;border:1px #ccc solid;" /></a>
</div>
<div v-else>
<span class="disabled">还没有上传。</span>
@@ -84,7 +79,8 @@
<tr>
<td class="title">每页显示数</td>
<td>
<input type="text" name="defaultPageSize" v-model="config.defaultPageSize" maxlength="3" style="width: 4em"/>
<input type="text" name="defaultPageSize" v-model="config.defaultPageSize" maxlength="3"
style="width: 4em" />
<p class="comment">在有分页的地方每页显示数量不能超过100。</p>
</td>
</tr>
@@ -94,12 +90,15 @@
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown" v-model="timeZoneGroupCode">
<option v-for="timeZoneGroup in timeZoneGroups" :value="timeZoneGroup.code">{{timeZoneGroup.name}}</option>
<option v-for="timeZoneGroup in timeZoneGroups" :value="timeZoneGroup.code">
{{timeZoneGroup.name}}</option>
</select>
</div>
<div class="ui field">
<select class="ui dropdown" name="timeZone" v-model="config.timeZone">
<option v-for="timeZoneLocation in timeZoneLocations" :value="timeZoneLocation.name" v-if="timeZoneLocation.group == timeZoneGroupCode">{{timeZoneLocation.name}} ({{timeZoneLocation.offset}})</option>
<option v-for="timeZoneLocation in timeZoneLocations" :value="timeZoneLocation.name"
v-if="timeZoneLocation.group == timeZoneGroupCode">{{timeZoneLocation.name}}
({{timeZoneLocation.offset}})</option>
</select>
</div>
</div>

View File

@@ -1,3 +1,139 @@
{$layout}
<p class="comment">此功能暂未开放,敬请期待。</p>
<div class="margin"></div>
<!-- 自动升级设置 -->
<div class="ui segment">
<h3>自动升级</h3>
<table class="ui table definition">
<tr>
<td class="title">开启自动升级</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="autoUpgrade" v-model="config.autoUpgrade" @change="updateAutoUpgrade">
<label></label>
</div>
<p class="comment">开启后边缘节点、DNS节点、HTTPDNS节点每分钟检查新版本并自动下载升级。关闭后节点不会自动升级但管理员仍可在下方手动升级。</p>
</td>
</tr>
</table>
</div>
<div class="margin"></div>
<!-- 手动升级 -->
<div class="ui segment">
<h3 style="display: flex; justify-content: space-between; align-items: center;">
<span>手动升级</span>
<button class="ui button primary tiny" v-if="totalUpgradeCount > 0"
@click.prevent="upgradeAll()">全部升级({{totalUpgradeCount}})</button>
</h3>
<div v-if="modules.length == 0">
<p class="comment">暂无需要升级的节点。</p>
</div>
<div v-for="mod in modules" class="ui segment" style="margin-bottom: 1em; padding: 0;">
<h4 style="cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 0.8em 1em; margin: 0; background: #f9fafb; border-bottom: 1px solid rgba(34,36,38,.15);"
@click.prevent="mod.expanded = !mod.expanded">
<div>
<i class="icon angle down" v-if="mod.expanded"></i>
<i class="icon angle right" v-else></i>
{{mod.name}}
<span class="ui label tiny basic" v-if="mod.count > 0"
style="margin-left: 0.5em;">{{mod.count}}个待升级</span>
</div>
<button class="ui button primary tiny" v-if="mod.count > 0"
@click.stop.prevent="upgradeModule(mod.code)">升级所有{{mod.name}}</button>
</h4>
<div v-show="mod.expanded" style="padding: 1em;">
<div v-for="cluster in mod.clusters" style="margin-bottom: 1em;">
<h5 style="cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 0.6em; background: #f3f4f5; border-radius: 4px; margin: 0;"
@click.prevent="cluster.expanded = !cluster.expanded">
<div>
<i class="icon angle down" v-if="cluster.expanded"></i>
<i class="icon angle right" v-else></i>
{{cluster.name}}
<span class="ui label tiny basic" style="margin-left: 0.5em;">{{cluster.count}}个待升级</span>
</div>
<div>
<button class="ui button tiny primary" v-if="countCheckedNodesInCluster(cluster) > 0"
@click.stop.prevent="upgradeBatchInCluster(mod.code, cluster)">批量升级({{countCheckedNodesInCluster(cluster)}})</button>
<button class="ui button tiny"
@click.stop.prevent="upgradeCluster(mod.code, cluster.id)">升级集群内所有节点</button>
</div>
</h5>
<div v-show="cluster.expanded" style="margin-top: 0.5em;">
<table class="ui table selectable celled small" style="margin: 0;">
<thead>
<tr>
<th style="width:3em">
<div class="ui checkbox" @click.prevent="toggleCheckAll(cluster)">
<input type="checkbox" :checked="isAllChecked(cluster)">
<label></label>
</div>
</th>
<th>节点名</th>
<th>访问IP</th>
<th>SSH地址</th>
<th>版本变化</th>
<th class="four wide">节点状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="node in cluster.nodes">
<td>
<div class="ui checkbox" v-if="!isNodeUpgradeFinished(node)">
<input type="checkbox" v-model="node.isChecked">
<label></label>
</div>
</td>
<td>
{{node.name}}
<a :href="nodeDetailURL(mod.code, cluster.id, node.id)" title="节点详情" style="margin-left: 0.4em"><i class="icon external alternate small"></i></a>
</td>
<td>
<span v-if="node.accessIP && node.accessIP.length > 0" class="ui label tiny basic">{{node.accessIP}}</span>
<span v-else class="disabled">-</span>
</td>
<td>
<span v-if="node.login != null && node.login.type == 'ssh' && node.loginParams != null && node.loginParams.host != null && node.loginParams.host.length > 0">
{{node.loginParams.host}}:{{node.loginParams.port}}
</span>
<span v-else class="disabled">没有设置</span>
</td>
<td>
<span v-if="node.oldVersion">v{{node.oldVersion}}</span>
<span v-else class="ui label tiny">未知</span>
-&gt; <strong>v{{node.newVersion}}</strong>
</td>
<td>
<div v-if="node.installStatus != null && (node.installStatus.isRunning || node.installStatus.isFinished)">
<div v-if="node.installStatus.isRunning && !node.installStatus.isFinished"
class="blue">
<i class="notched circle loading icon"></i> 升级中...
</div>
<div v-if="node.installStatus.isFinished">
<span v-if="node.installStatus.isOk" class="green"><i
class="icon check circle"></i>
已升级成功</span>
<span v-if="!node.installStatus.isOk" class="red"><i
class="icon warning circle"></i>
升级过程中发生错误:{{node.installStatus.error}}</span>
</div>
</div>
<span v-else class="disabled">等待升级</span>
</td>
<td>
<a href="" @click.prevent="upgradeNode(mod.code, node)" v-if="!node.isUpgrading">升级</a>
<span v-if="node.isUpgrading" class="blue">升级中...</span>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,457 @@
Tea.context(function () {
let that = this
// 计算总待升级数
this.totalUpgradeCount = 0
this.modules.forEach(function (mod) {
mod.expanded = true
that.totalUpgradeCount += mod.count
if (mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
cluster.expanded = true
if (cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
node.isUpgrading = false
node.isChecked = false
})
}
})
}
})
// 正在升级的节点ID集合按模块
this.upgradingNodeIds = {
"node": [],
"dns": [],
"httpdns": []
}
// 启动状态轮询
this.$delay(function () {
this.pollStatus()
})
/**
* 获取节点详情页URL
*/
this.nodeDetailURL = function (moduleCode, clusterId, nodeId) {
switch (moduleCode) {
case "node":
return "/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
case "dns":
return "/ns/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
case "httpdns":
return "/httpdns/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
default:
return "#"
}
}
/**
* 判断节点升级是否已完成(成功)
*/
this.isNodeUpgradeFinished = function (node) {
return node.installStatus != null && node.installStatus.isFinished && node.installStatus.isOk
}
/**
* 全选/取消全选
*/
this.toggleCheckAll = function (cluster) {
if (cluster.nodes == null) return
let allChecked = that.isAllChecked(cluster)
cluster.nodes.forEach(function (node) {
if (!that.isNodeUpgradeFinished(node)) {
node.isChecked = !allChecked
}
})
}
/**
* 是否全部选中
*/
this.isAllChecked = function (cluster) {
if (cluster.nodes == null || cluster.nodes.length == 0) return false
let checkableNodes = cluster.nodes.filter(function (node) {
return !that.isNodeUpgradeFinished(node)
})
if (checkableNodes.length == 0) return false
return checkableNodes.every(function (node) { return node.isChecked })
}
/**
* 计算集群内选中的节点数
*/
this.countCheckedNodesInCluster = function (cluster) {
if (cluster.nodes == null) return 0
return cluster.nodes.filter(function (node) { return node.isChecked }).length
}
/**
* 全部升级
*/
this.upgradeAll = function () {
var vue = this
teaweb.confirm("确定要升级所有模块的所有待升级节点吗?", function () {
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: "",
scope: "all",
clusterId: 0,
nodeId: 0
})
.success(function () {
// 标记所有节点为升级中
that.modules.forEach(function (mod) {
if (mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
if (cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
that.trackNode(mod.code, node.id)
})
}
})
}
})
})
.fail(function (resp) {
teaweb.warn("升级请求失败:" + resp.message)
})
})
}
/**
* 按模块升级
*/
this.upgradeModule = function (moduleCode) {
var vue = this
teaweb.confirm("确定要升级该模块的所有待升级节点吗?", function () {
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: moduleCode,
scope: "module",
clusterId: 0,
nodeId: 0
})
.success(function () {
that.markModuleUpgrading(moduleCode)
})
.fail(function (resp) {
teaweb.warn("升级请求失败:" + resp.message)
})
})
}
/**
* 按集群升级
*/
this.upgradeCluster = function (moduleCode, clusterId) {
var vue = this
teaweb.confirm("确定要升级该集群的所有待升级节点吗?", function () {
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: moduleCode,
scope: "cluster",
clusterId: clusterId,
nodeId: 0
})
.success(function () {
that.markClusterUpgrading(moduleCode, clusterId)
})
.fail(function (resp) {
teaweb.warn("升级请求失败:" + resp.message)
})
})
}
/**
* 批量升级集群内选中的节点
*/
this.upgradeBatchInCluster = function (moduleCode, cluster) {
if (cluster.nodes == null) return
var checkedNodes = cluster.nodes.filter(function (node) { return node.isChecked })
if (checkedNodes.length == 0) return
var vue = this
teaweb.confirm("确定要批量升级选中的 " + checkedNodes.length + " 个节点吗?", function () {
checkedNodes.forEach(function (node) {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
node.isChecked = false
that.trackNode(moduleCode, node.id)
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: moduleCode,
scope: "node",
clusterId: 0,
nodeId: node.id
})
})
})
}
/**
* 升级单个节点
*/
this.upgradeNode = function (moduleCode, node) {
var vue = this
teaweb.confirm("确定要升级节点 \"" + node.name + "\" 吗?", function () {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
that.trackNode(moduleCode, node.id)
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: moduleCode,
scope: "node",
clusterId: 0,
nodeId: node.id
})
.success(function () { })
.fail(function (resp) {
node.isUpgrading = false
teaweb.warn("升级请求失败:" + resp.message)
})
})
}
/**
* 标记模块下所有节点为升级中
*/
this.markModuleUpgrading = function (moduleCode) {
that.modules.forEach(function (mod) {
if (mod.code == moduleCode && mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
if (cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
that.trackNode(moduleCode, node.id)
})
}
})
}
})
}
/**
* 标记集群下所有节点为升级中
*/
this.markClusterUpgrading = function (moduleCode, clusterId) {
that.modules.forEach(function (mod) {
if (mod.code == moduleCode && mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
if (cluster.id == clusterId && cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
that.trackNode(moduleCode, node.id)
})
}
})
}
})
}
/**
* 追踪节点升级状态
*/
this.trackNode = function (moduleCode, nodeId) {
if (that.upgradingNodeIds[moduleCode] == null) {
that.upgradingNodeIds[moduleCode] = []
}
if (that.upgradingNodeIds[moduleCode].indexOf(nodeId) < 0) {
that.upgradingNodeIds[moduleCode].push(nodeId)
}
}
/**
* 状态轮询
*/
this.pollStatus = function () {
var vue = this
// 检查是否有正在升级的节点
let hasUpgrading = false
for (let key in that.upgradingNodeIds) {
if (that.upgradingNodeIds[key].length > 0) {
hasUpgrading = true
break
}
}
if (!hasUpgrading) {
setTimeout(function () { that.pollStatus() }, 5000)
return
}
vue.$post("/settings/upgrade/status")
.params({
nodeIdsJSON: JSON.stringify(that.upgradingNodeIds)
})
.success(function (resp) {
let statuses = resp.data.statuses
if (statuses == null) {
return
}
// 更新各模块节点状态
for (let moduleCode in statuses) {
let nodeStatuses = statuses[moduleCode]
if (nodeStatuses == null) {
continue
}
nodeStatuses.forEach(function (ns) {
that.updateNodeStatus(moduleCode, ns.id, ns.installStatus)
})
}
})
.done(function () {
setTimeout(function () { that.pollStatus() }, 3000)
})
}
/**
* 更新节点安装状态
*/
this.updateNodeStatus = function (moduleCode, nodeId, installStatus) {
that.modules.forEach(function (mod) {
if (mod.code == moduleCode && mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
if (cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
if (node.id == nodeId && installStatus != null) {
node.installStatus = installStatus
// 升级完成后移除跟踪
if (installStatus.isFinished) {
node.isUpgrading = false
let idx = that.upgradingNodeIds[moduleCode].indexOf(nodeId)
if (idx >= 0) {
that.upgradingNodeIds[moduleCode].splice(idx, 1)
}
if (installStatus.isOk) {
// 升级成功,延迟后从列表中移除
setTimeout(function () {
let nIdx = cluster.nodes.indexOf(node)
if (nIdx >= 0) {
cluster.nodes.splice(nIdx, 1)
cluster.count--
mod.count--
that.totalUpgradeCount--
if (cluster.count <= 0) {
let cIdx = mod.clusters.indexOf(cluster)
if (cIdx >= 0) {
mod.clusters.splice(cIdx, 1)
}
}
if (mod.count <= 0) {
let mIdx = that.modules.indexOf(mod)
if (mIdx >= 0) {
that.modules.splice(mIdx, 1)
}
}
}
}, 2000)
} else {
// 升级失败,根据 errorCode 给出具体提示
that.handleUpgradeError(moduleCode, node, installStatus)
}
}
}
})
}
})
}
})
}
/**
* 处理升级错误,根据 errorCode 提供更友好的提示
*/
this.handleUpgradeError = function (moduleCode, node, installStatus) {
let errorCode = installStatus.errorCode || ""
let errMsg = installStatus.error || ""
switch (errorCode) {
case "EMPTY_LOGIN":
case "EMPTY_SSH_HOST":
case "EMPTY_SSH_PORT":
case "EMPTY_GRANT":
// SSH 信息未配置的错误,不弹窗(页面上已有"没有设置"提示)
break
case "CREATE_ROOT_DIRECTORY_FAILED":
teaweb.warn("节点 \"" + node.name + "\" 创建根目录失败:" + errMsg)
break
case "INSTALL_HELPER_FAILED":
teaweb.warn("节点 \"" + node.name + "\" 安装助手失败:" + errMsg)
break
case "TEST_FAILED":
teaweb.warn("节点 \"" + node.name + "\" 环境测试失败:" + errMsg)
break
case "RPC_TEST_FAILED":
teaweb.warn("节点 \"" + node.name + "\" RPC通讯测试失败" + errMsg)
break
}
}
/**
* 更新自动升级状态
*/
this.updateAutoUpgrade = function () {
var vue = this
// @change 已经翻转了值,先记录新值并立即恢复
let newValue = that.config.autoUpgrade
that.config.autoUpgrade = !newValue
let msg = newValue ? "确定要开启自动升级功能吗?开启后节点会自动下载安装新版本。" : "确定要关闭自动升级功能吗?关闭后只能通过这里手动执行升级。"
teaweb.confirm(msg, function () {
vue.$post("/settings/upgrade")
.params({
autoUpgrade: newValue ? 1 : 0
})
.success(function () {
that.config.autoUpgrade = newValue
teaweb.successToast("设置保存成功")
})
.fail(function (resp) {
teaweb.warn("设置保存失败:" + resp.message)
})
})
}
})

View File

@@ -39,7 +39,7 @@
</td>
</tr>
<tr v-if="hasHTTPDNSFeature">
<td>HTTPDNS关联集群</td>
<td style="white-space: nowrap">HTTPDNS关联集群</td>
<td>
<select class="ui dropdown auto-width" name="httpdnsClusterId" v-model="httpdnsClusterId">
<option value="0">[未选择]</option>

View File

@@ -1,3 +1,6 @@
.ui.table.definition td.title {
min-width: 12em;
}
.feature-boxes .feature-box {
margin-bottom: 1em;
width: 24em;

View File

@@ -6,7 +6,7 @@
<table class="ui table definition selectable">
<tr>
<td class="title">允许注册</td>
<td class="title" style="white-space: nowrap">允许注册</td>
<td>
<checkbox name="isOn" v-model="config.isOn"></checkbox>
<p class="comment">选中表示允许用户自行注册。</p>
@@ -40,7 +40,7 @@
<h4>登录设置</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">检查客户端区域</td>
<td class="title" style="white-space: nowrap">检查客户端区域</td>
<td>
<checkbox name="checkClientRegion" v-model="config.checkClientRegion"></checkbox>
<p class="comment">选中后,表示每次用户访问时都检查客户端所在地理区域是否和登录时一致,以提升安全性;如果当前系统下游有反向代理设置,请在<a
@@ -53,7 +53,7 @@
<h4>电子邮箱相关</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">启用电子邮箱绑定功能</td>
<td class="title" style="white-space: nowrap">启用电子邮箱绑定功能</td>
<td>
<checkbox name="emailVerificationIsOn" v-model="config.emailVerification.isOn"></checkbox>
<p class="comment">选中后,电子邮箱需要激活之后可以使用邮箱登录、找回密码等。此功能需要事先设置 <a href="/users/setting/email"
@@ -107,7 +107,7 @@
<h4>通过邮箱找回密码</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">启用找回密码功能</td>
<td class="title" style="white-space: nowrap">启用找回密码功能</td>
<td>
<checkbox name="emailResetPasswordIsOn" v-model="config.emailResetPassword.isOn"></checkbox>
<p class="comment">选中后,用户可以通过已绑定的电子邮箱找回密码;此功能需要同时开启电子邮箱绑定功能。</p>
@@ -146,7 +146,7 @@
<h4>手机号码相关</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">启用手机号码绑定功能</td>
<td class="title" style="white-space: nowrap">启用手机号码绑定功能</td>
<td>
<checkbox name="mobileVerificationIsOn" v-model="config.mobileVerification.isOn"></checkbox>
<p class="comment">选中后,手机号码需要激活之后可以使用手机号码、找回密码等。此功能需要事先设置 <a href="/users/setting/sms"
@@ -196,7 +196,7 @@
<h4>CDN服务</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">开通CDN服务</td>
<td class="title" style="white-space: nowrap">开通CDN服务</td>
<td>
<checkbox name="cdnIsOn" v-model="config.cdnIsOn"></checkbox>
<p class="comment">选中表示自动为用户开通CDN服务。 </p>
@@ -254,7 +254,7 @@
<h4>DDoS高防</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">开通DDoS高防管理</td>
<td class="title" style="white-space: nowrap">开通DDoS高防管理</td>
<td>
<checkbox name="adIsOn" v-model="config.adIsOn"></checkbox>
<p class="comment">选中表示自动为用户开通DDoS高防IP使用服务。</p>
@@ -269,7 +269,7 @@
<h4>智能DNS服务</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">开通智能DNS服务</td>
<td class="title" style="white-space: nowrap">开通智能DNS服务</td>
<td>
<checkbox name="nsIsOn" v-model="config.nsIsOn"></checkbox>
<p class="comment">选中表示自动为用户开通智能DNS服务。使用默认集群资源如需使用其他集群请到用户新增和业务设置页面中进行选择。</p>
@@ -284,7 +284,7 @@
<h4>HTTPDNS服务</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">开通HTTPDNS服务</td>
<td class="title" style="white-space: nowrap">开通HTTPDNS服务</td>
<td>
<checkbox name="httpdnsIsOn" v-model="config.httpdnsIsOn"></checkbox>
<p class="comment">选中表示自动为用户开通HTTPDNS服务。使用默认集群资源如需使用其他集群请到用户新增和业务设置页面中进行选择。</p>

View File

@@ -7,9 +7,7 @@ import (
"github.com/oschwald/geoip2-golang"
"net"
"os"
"path/filepath"
"sync"
"time"
)
// MaxMindReader MaxMind GeoIP2 Reader
@@ -19,6 +17,9 @@ type MaxMindReader struct {
meta *Meta
initialized bool
mutex sync.RWMutex
// 临时文件路径Destroy 时自动清理
tmpFiles []string
}
// NewMaxMindReader 创建 MaxMind Reader
@@ -66,27 +67,9 @@ func NewMaxMindReaderFromBytes(cityDBData, asnDBData []byte) (*MaxMindReader, er
return nil, fmt.Errorf("city database data is required")
}
// 创建临时文件,使用更唯一的文件名避免冲突
tmpDir := os.TempDir()
pid := os.Getpid()
// 使用时间戳增加唯一性,避免同一进程多次调用时的冲突
timestamp := time.Now().UnixNano()
cityTmpFile := filepath.Join(tmpDir, fmt.Sprintf("geolite2-city-%d-%d.mmdb", pid, timestamp))
asnTmpFile := filepath.Join(tmpDir, fmt.Sprintf("geolite2-asn-%d-%d.mmdb", pid, timestamp))
// 如果临时文件已存在,先删除(可能是之前崩溃留下的)
os.Remove(cityTmpFile)
os.Remove(asnTmpFile)
// 写入 City 数据库到临时文件
if err := os.WriteFile(cityTmpFile, cityDBData, 0644); err != nil {
return nil, fmt.Errorf("write city database to temp file failed: %w", err)
}
// 打开 City 数据库
db, err := geoip2.Open(cityTmpFile)
// 直接从内存字节加载,避免在 /tmp 持续生成 mmdb 临时文件。
db, err := geoip2.FromBytes(cityDBData)
if err != nil {
os.Remove(cityTmpFile)
return nil, fmt.Errorf("open MaxMind city database failed: %w", err)
}
@@ -94,16 +77,11 @@ func NewMaxMindReaderFromBytes(cityDBData, asnDBData []byte) (*MaxMindReader, er
db: db,
}
// 写入并打开 ASN 数据库(可选)
// 加载 ASN 数据库(可选)
if len(asnDBData) > 0 {
if err := os.WriteFile(asnTmpFile, asnDBData, 0644); err == nil {
dbASN, err := geoip2.Open(asnTmpFile)
dbASN, err := geoip2.FromBytes(asnDBData)
if err == nil {
reader.dbASN = dbASN
} else {
// ASN 数据库打开失败,清理临时文件但不影响主功能
os.Remove(asnTmpFile)
}
}
}
@@ -174,7 +152,7 @@ func (this *MaxMindReader) Meta() *Meta {
return this.meta
}
// Destroy 销毁 Reader
// Destroy 销毁 Reader 并清理临时文件
func (this *MaxMindReader) Destroy() {
this.mutex.Lock()
defer this.mutex.Unlock()
@@ -187,6 +165,10 @@ func (this *MaxMindReader) Destroy() {
this.dbASN.Close()
this.dbASN = nil
}
for _, f := range this.tmpFiles {
os.Remove(f)
}
this.tmpFiles = nil
this.initialized = false
}

View File

@@ -38,6 +38,7 @@ type HTTPDNSCluster struct {
UpdatedAt int64 `protobuf:"varint,11,opt,name=updatedAt,proto3" json:"updatedAt,omitempty"`
AutoRemoteStart bool `protobuf:"varint,12,opt,name=autoRemoteStart,proto3" json:"autoRemoteStart,omitempty"`
AccessLogIsOn bool `protobuf:"varint,13,opt,name=accessLogIsOn,proto3" json:"accessLogIsOn,omitempty"`
TimeZone string `protobuf:"bytes,14,opt,name=timeZone,proto3" json:"timeZone,omitempty"`
}
func (x *HTTPDNSCluster) Reset() {
@@ -161,6 +162,13 @@ func (x *HTTPDNSCluster) GetAccessLogIsOn() bool {
return false
}
func (x *HTTPDNSCluster) GetTimeZone() string {
if x != nil {
return x.TimeZone
}
return ""
}
var File_models_model_httpdns_cluster_proto protoreflect.FileDescriptor
var file_models_model_httpdns_cluster_proto_rawDesc = []byte{

View File

@@ -35,6 +35,7 @@ type CreateHTTPDNSClusterRequest struct {
IsDefault bool `protobuf:"varint,8,opt,name=isDefault,proto3" json:"isDefault,omitempty"`
AutoRemoteStart bool `protobuf:"varint,9,opt,name=autoRemoteStart,proto3" json:"autoRemoteStart,omitempty"`
AccessLogIsOn bool `protobuf:"varint,10,opt,name=accessLogIsOn,proto3" json:"accessLogIsOn,omitempty"`
TimeZone string `protobuf:"bytes,11,opt,name=timeZone,proto3" json:"timeZone,omitempty"`
}
func (x *CreateHTTPDNSClusterRequest) Reset() {
@@ -137,6 +138,13 @@ func (x *CreateHTTPDNSClusterRequest) GetAccessLogIsOn() bool {
return false
}
func (x *CreateHTTPDNSClusterRequest) GetTimeZone() string {
if x != nil {
return x.TimeZone
}
return ""
}
type CreateHTTPDNSClusterResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -198,6 +206,7 @@ type UpdateHTTPDNSClusterRequest struct {
IsDefault bool `protobuf:"varint,9,opt,name=isDefault,proto3" json:"isDefault,omitempty"`
AutoRemoteStart bool `protobuf:"varint,10,opt,name=autoRemoteStart,proto3" json:"autoRemoteStart,omitempty"`
AccessLogIsOn bool `protobuf:"varint,11,opt,name=accessLogIsOn,proto3" json:"accessLogIsOn,omitempty"`
TimeZone string `protobuf:"bytes,12,opt,name=timeZone,proto3" json:"timeZone,omitempty"`
}
func (x *UpdateHTTPDNSClusterRequest) Reset() {
@@ -307,6 +316,13 @@ func (x *UpdateHTTPDNSClusterRequest) GetAccessLogIsOn() bool {
return false
}
func (x *UpdateHTTPDNSClusterRequest) GetTimeZone() string {
if x != nil {
return x.TimeZone
}
return ""
}
type DeleteHTTPDNSClusterRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache

View File

@@ -28,6 +28,9 @@ const (
HTTPDNSNodeService_UpdateHTTPDNSNodeLogin_FullMethodName = "/pb.HTTPDNSNodeService/updateHTTPDNSNodeLogin"
HTTPDNSNodeService_CheckHTTPDNSNodeLatestVersion_FullMethodName = "/pb.HTTPDNSNodeService/checkHTTPDNSNodeLatestVersion"
HTTPDNSNodeService_DownloadHTTPDNSNodeInstallationFile_FullMethodName = "/pb.HTTPDNSNodeService/downloadHTTPDNSNodeInstallationFile"
HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName = "/pb.HTTPDNSNodeService/countAllUpgradeHTTPDNSNodesWithClusterId"
HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName = "/pb.HTTPDNSNodeService/findAllUpgradeHTTPDNSNodesWithClusterId"
HTTPDNSNodeService_UpgradeHTTPDNSNode_FullMethodName = "/pb.HTTPDNSNodeService/upgradeHTTPDNSNode"
)
// HTTPDNSNodeServiceClient is the client API for HTTPDNSNodeService service.
@@ -46,6 +49,12 @@ type HTTPDNSNodeServiceClient interface {
CheckHTTPDNSNodeLatestVersion(ctx context.Context, in *CheckHTTPDNSNodeLatestVersionRequest, opts ...grpc.CallOption) (*CheckHTTPDNSNodeLatestVersionResponse, error)
// 下载最新HTTPDNS节点安装文件
DownloadHTTPDNSNodeInstallationFile(ctx context.Context, in *DownloadHTTPDNSNodeInstallationFileRequest, opts ...grpc.CallOption) (*DownloadHTTPDNSNodeInstallationFileResponse, error)
// 计算需要升级的HTTPDNS节点数量
CountAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, in *CountAllUpgradeHTTPDNSNodesWithClusterIdRequest, opts ...grpc.CallOption) (*RPCCountResponse, error)
// 列出所有需要升级的HTTPDNS节点
FindAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, in *FindAllUpgradeHTTPDNSNodesWithClusterIdRequest, opts ...grpc.CallOption) (*FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error)
// 升级单个HTTPDNS节点
UpgradeHTTPDNSNode(ctx context.Context, in *UpgradeHTTPDNSNodeRequest, opts ...grpc.CallOption) (*RPCSuccess, error)
}
type hTTPDNSNodeServiceClient struct {
@@ -146,6 +155,36 @@ func (c *hTTPDNSNodeServiceClient) DownloadHTTPDNSNodeInstallationFile(ctx conte
return out, nil
}
func (c *hTTPDNSNodeServiceClient) CountAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, in *CountAllUpgradeHTTPDNSNodesWithClusterIdRequest, opts ...grpc.CallOption) (*RPCCountResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RPCCountResponse)
err := c.cc.Invoke(ctx, HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *hTTPDNSNodeServiceClient) FindAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, in *FindAllUpgradeHTTPDNSNodesWithClusterIdRequest, opts ...grpc.CallOption) (*FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(FindAllUpgradeHTTPDNSNodesWithClusterIdResponse)
err := c.cc.Invoke(ctx, HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *hTTPDNSNodeServiceClient) UpgradeHTTPDNSNode(ctx context.Context, in *UpgradeHTTPDNSNodeRequest, opts ...grpc.CallOption) (*RPCSuccess, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RPCSuccess)
err := c.cc.Invoke(ctx, HTTPDNSNodeService_UpgradeHTTPDNSNode_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// HTTPDNSNodeServiceServer is the server API for HTTPDNSNodeService service.
// All implementations must embed UnimplementedHTTPDNSNodeServiceServer
// for forward compatibility.
@@ -162,6 +201,12 @@ type HTTPDNSNodeServiceServer interface {
CheckHTTPDNSNodeLatestVersion(context.Context, *CheckHTTPDNSNodeLatestVersionRequest) (*CheckHTTPDNSNodeLatestVersionResponse, error)
// 下载最新HTTPDNS节点安装文件
DownloadHTTPDNSNodeInstallationFile(context.Context, *DownloadHTTPDNSNodeInstallationFileRequest) (*DownloadHTTPDNSNodeInstallationFileResponse, error)
// 计算需要升级的HTTPDNS节点数量
CountAllUpgradeHTTPDNSNodesWithClusterId(context.Context, *CountAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*RPCCountResponse, error)
// 列出所有需要升级的HTTPDNS节点
FindAllUpgradeHTTPDNSNodesWithClusterId(context.Context, *FindAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error)
// 升级单个HTTPDNS节点
UpgradeHTTPDNSNode(context.Context, *UpgradeHTTPDNSNodeRequest) (*RPCSuccess, error)
mustEmbedUnimplementedHTTPDNSNodeServiceServer()
}
@@ -199,6 +244,15 @@ func (UnimplementedHTTPDNSNodeServiceServer) CheckHTTPDNSNodeLatestVersion(conte
func (UnimplementedHTTPDNSNodeServiceServer) DownloadHTTPDNSNodeInstallationFile(context.Context, *DownloadHTTPDNSNodeInstallationFileRequest) (*DownloadHTTPDNSNodeInstallationFileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DownloadHTTPDNSNodeInstallationFile not implemented")
}
func (UnimplementedHTTPDNSNodeServiceServer) CountAllUpgradeHTTPDNSNodesWithClusterId(context.Context, *CountAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*RPCCountResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CountAllUpgradeHTTPDNSNodesWithClusterId not implemented")
}
func (UnimplementedHTTPDNSNodeServiceServer) FindAllUpgradeHTTPDNSNodesWithClusterId(context.Context, *FindAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error) {
return nil, status.Error(codes.Unimplemented, "method FindAllUpgradeHTTPDNSNodesWithClusterId not implemented")
}
func (UnimplementedHTTPDNSNodeServiceServer) UpgradeHTTPDNSNode(context.Context, *UpgradeHTTPDNSNodeRequest) (*RPCSuccess, error) {
return nil, status.Error(codes.Unimplemented, "method UpgradeHTTPDNSNode not implemented")
}
func (UnimplementedHTTPDNSNodeServiceServer) mustEmbedUnimplementedHTTPDNSNodeServiceServer() {}
func (UnimplementedHTTPDNSNodeServiceServer) testEmbeddedByValue() {}
@@ -382,6 +436,60 @@ func _HTTPDNSNodeService_DownloadHTTPDNSNodeInstallationFile_Handler(srv interfa
return interceptor(ctx, in, info, handler)
}
func _HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CountAllUpgradeHTTPDNSNodesWithClusterIdRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HTTPDNSNodeServiceServer).CountAllUpgradeHTTPDNSNodesWithClusterId(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HTTPDNSNodeServiceServer).CountAllUpgradeHTTPDNSNodesWithClusterId(ctx, req.(*CountAllUpgradeHTTPDNSNodesWithClusterIdRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FindAllUpgradeHTTPDNSNodesWithClusterIdRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HTTPDNSNodeServiceServer).FindAllUpgradeHTTPDNSNodesWithClusterId(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HTTPDNSNodeServiceServer).FindAllUpgradeHTTPDNSNodesWithClusterId(ctx, req.(*FindAllUpgradeHTTPDNSNodesWithClusterIdRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HTTPDNSNodeService_UpgradeHTTPDNSNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpgradeHTTPDNSNodeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HTTPDNSNodeServiceServer).UpgradeHTTPDNSNode(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: HTTPDNSNodeService_UpgradeHTTPDNSNode_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HTTPDNSNodeServiceServer).UpgradeHTTPDNSNode(ctx, req.(*UpgradeHTTPDNSNodeRequest))
}
return interceptor(ctx, in, info, handler)
}
// HTTPDNSNodeService_ServiceDesc is the grpc.ServiceDesc for HTTPDNSNodeService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -425,6 +533,18 @@ var HTTPDNSNodeService_ServiceDesc = grpc.ServiceDesc{
MethodName: "downloadHTTPDNSNodeInstallationFile",
Handler: _HTTPDNSNodeService_DownloadHTTPDNSNodeInstallationFile_Handler,
},
{
MethodName: "countAllUpgradeHTTPDNSNodesWithClusterId",
Handler: _HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_Handler,
},
{
MethodName: "findAllUpgradeHTTPDNSNodesWithClusterId",
Handler: _HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_Handler,
},
{
MethodName: "upgradeHTTPDNSNode",
Handler: _HTTPDNSNodeService_UpgradeHTTPDNSNode_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "service_httpdns_node.proto",

View File

@@ -46,6 +46,8 @@ const (
NSNodeService_UpdateNSNodeDDoSProtection_FullMethodName = "/pb.NSNodeService/updateNSNodeDDoSProtection"
NSNodeService_FindNSNodeAPIConfig_FullMethodName = "/pb.NSNodeService/findNSNodeAPIConfig"
NSNodeService_UpdateNSNodeAPIConfig_FullMethodName = "/pb.NSNodeService/updateNSNodeAPIConfig"
NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_FullMethodName = "/pb.NSNodeService/findAllUpgradeNSNodesWithNSClusterId"
NSNodeService_UpgradeNSNode_FullMethodName = "/pb.NSNodeService/upgradeNSNode"
)
// NSNodeServiceClient is the client API for NSNodeService service.
@@ -108,6 +110,10 @@ type NSNodeServiceClient interface {
FindNSNodeAPIConfig(ctx context.Context, in *FindNSNodeAPIConfigRequest, opts ...grpc.CallOption) (*FindNSNodeAPIConfigResponse, error)
// 修改某个节点的API相关配置
UpdateNSNodeAPIConfig(ctx context.Context, in *UpdateNSNodeAPIConfigRequest, opts ...grpc.CallOption) (*RPCSuccess, error)
// 列出所有需要升级的NS节点
FindAllUpgradeNSNodesWithNSClusterId(ctx context.Context, in *FindAllUpgradeNSNodesWithNSClusterIdRequest, opts ...grpc.CallOption) (*FindAllUpgradeNSNodesWithNSClusterIdResponse, error)
// 升级单个NS节点
UpgradeNSNode(ctx context.Context, in *UpgradeNSNodeRequest, opts ...grpc.CallOption) (*RPCSuccess, error)
}
type nSNodeServiceClient struct {
@@ -391,6 +397,26 @@ func (c *nSNodeServiceClient) UpdateNSNodeAPIConfig(ctx context.Context, in *Upd
return out, nil
}
func (c *nSNodeServiceClient) FindAllUpgradeNSNodesWithNSClusterId(ctx context.Context, in *FindAllUpgradeNSNodesWithNSClusterIdRequest, opts ...grpc.CallOption) (*FindAllUpgradeNSNodesWithNSClusterIdResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(FindAllUpgradeNSNodesWithNSClusterIdResponse)
err := c.cc.Invoke(ctx, NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *nSNodeServiceClient) UpgradeNSNode(ctx context.Context, in *UpgradeNSNodeRequest, opts ...grpc.CallOption) (*RPCSuccess, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RPCSuccess)
err := c.cc.Invoke(ctx, NSNodeService_UpgradeNSNode_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// NSNodeServiceServer is the server API for NSNodeService service.
// All implementations should embed UnimplementedNSNodeServiceServer
// for forward compatibility.
@@ -451,6 +477,10 @@ type NSNodeServiceServer interface {
FindNSNodeAPIConfig(context.Context, *FindNSNodeAPIConfigRequest) (*FindNSNodeAPIConfigResponse, error)
// 修改某个节点的API相关配置
UpdateNSNodeAPIConfig(context.Context, *UpdateNSNodeAPIConfigRequest) (*RPCSuccess, error)
// 列出所有需要升级的NS节点
FindAllUpgradeNSNodesWithNSClusterId(context.Context, *FindAllUpgradeNSNodesWithNSClusterIdRequest) (*FindAllUpgradeNSNodesWithNSClusterIdResponse, error)
// 升级单个NS节点
UpgradeNSNode(context.Context, *UpgradeNSNodeRequest) (*RPCSuccess, error)
}
// UnimplementedNSNodeServiceServer should be embedded to have
@@ -541,6 +571,12 @@ func (UnimplementedNSNodeServiceServer) FindNSNodeAPIConfig(context.Context, *Fi
func (UnimplementedNSNodeServiceServer) UpdateNSNodeAPIConfig(context.Context, *UpdateNSNodeAPIConfigRequest) (*RPCSuccess, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateNSNodeAPIConfig not implemented")
}
func (UnimplementedNSNodeServiceServer) FindAllUpgradeNSNodesWithNSClusterId(context.Context, *FindAllUpgradeNSNodesWithNSClusterIdRequest) (*FindAllUpgradeNSNodesWithNSClusterIdResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method FindAllUpgradeNSNodesWithNSClusterId not implemented")
}
func (UnimplementedNSNodeServiceServer) UpgradeNSNode(context.Context, *UpgradeNSNodeRequest) (*RPCSuccess, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpgradeNSNode not implemented")
}
func (UnimplementedNSNodeServiceServer) testEmbeddedByValue() {}
// UnsafeNSNodeServiceServer may be embedded to opt out of forward compatibility for this service.
@@ -1036,6 +1072,42 @@ func _NSNodeService_UpdateNSNodeAPIConfig_Handler(srv interface{}, ctx context.C
return interceptor(ctx, in, info, handler)
}
func _NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FindAllUpgradeNSNodesWithNSClusterIdRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NSNodeServiceServer).FindAllUpgradeNSNodesWithNSClusterId(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NSNodeServiceServer).FindAllUpgradeNSNodesWithNSClusterId(ctx, req.(*FindAllUpgradeNSNodesWithNSClusterIdRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NSNodeService_UpgradeNSNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpgradeNSNodeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NSNodeServiceServer).UpgradeNSNode(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NSNodeService_UpgradeNSNode_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NSNodeServiceServer).UpgradeNSNode(ctx, req.(*UpgradeNSNodeRequest))
}
return interceptor(ctx, in, info, handler)
}
// NSNodeService_ServiceDesc is the grpc.ServiceDesc for NSNodeService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -1147,6 +1219,14 @@ var NSNodeService_ServiceDesc = grpc.ServiceDesc{
MethodName: "updateNSNodeAPIConfig",
Handler: _NSNodeService_UpdateNSNodeAPIConfig_Handler,
},
{
MethodName: "findAllUpgradeNSNodesWithNSClusterId",
Handler: _NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_Handler,
},
{
MethodName: "upgradeNSNode",
Handler: _NSNodeService_UpgradeNSNode_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@@ -17,4 +17,5 @@ message HTTPDNSCluster {
int64 updatedAt = 11;
bool autoRemoteStart = 12;
bool accessLogIsOn = 13;
string timeZone = 14;
}

View File

@@ -29,6 +29,7 @@ message CreateHTTPDNSClusterRequest {
bool isDefault = 8;
bool autoRemoteStart = 9;
bool accessLogIsOn = 10;
string timeZone = 11;
}
message CreateHTTPDNSClusterResponse {
@@ -47,6 +48,7 @@ message UpdateHTTPDNSClusterRequest {
bool isDefault = 9;
bool autoRemoteStart = 10;
bool accessLogIsOn = 11;
string timeZone = 12;
}
message DeleteHTTPDNSClusterRequest {

View File

@@ -22,6 +22,15 @@ service HTTPDNSNodeService {
// 下载最新HTTPDNS节点安装文件
rpc downloadHTTPDNSNodeInstallationFile (DownloadHTTPDNSNodeInstallationFileRequest) returns (DownloadHTTPDNSNodeInstallationFileResponse);
// 计算需要升级的HTTPDNS节点数量
rpc countAllUpgradeHTTPDNSNodesWithClusterId (CountAllUpgradeHTTPDNSNodesWithClusterIdRequest) returns (RPCCountResponse);
// 列出所有需要升级的HTTPDNS节点
rpc findAllUpgradeHTTPDNSNodesWithClusterId (FindAllUpgradeHTTPDNSNodesWithClusterIdRequest) returns (FindAllUpgradeHTTPDNSNodesWithClusterIdResponse);
// 升级单个HTTPDNS节点
rpc upgradeHTTPDNSNode (UpgradeHTTPDNSNodeRequest) returns (RPCSuccess);
}
message CreateHTTPDNSNodeRequest {
@@ -103,3 +112,30 @@ message DownloadHTTPDNSNodeInstallationFileResponse {
string version = 4;
string filename = 5;
}
// 计算需要升级的HTTPDNS节点数量
message CountAllUpgradeHTTPDNSNodesWithClusterIdRequest {
int64 clusterId = 1;
}
// 列出所有需要升级的HTTPDNS节点
message FindAllUpgradeHTTPDNSNodesWithClusterIdRequest {
int64 clusterId = 1;
}
message FindAllUpgradeHTTPDNSNodesWithClusterIdResponse {
repeated HTTPDNSNodeUpgrade nodes = 1;
message HTTPDNSNodeUpgrade {
HTTPDNSNode node = 1;
string os = 2;
string arch = 3;
string oldVersion = 4;
string newVersion = 5;
}
}
// 升级单个HTTPDNS节点
message UpgradeHTTPDNSNodeRequest {
int64 nodeId = 1;
}

View File

@@ -25,6 +25,12 @@ service NSNodeService {
// 计算需要升级的NS节点数量
rpc countAllUpgradeNSNodesWithNSClusterId (CountAllUpgradeNSNodesWithNSClusterIdRequest) returns (RPCCountResponse);
// 列出所有需要升级的NS节点
rpc findAllUpgradeNSNodesWithNSClusterId (FindAllUpgradeNSNodesWithNSClusterIdRequest) returns (FindAllUpgradeNSNodesWithNSClusterIdResponse);
// 升级单个NS节点
rpc upgradeNSNode (UpgradeNSNodeRequest) returns (RPCSuccess);
// 创建NS节点
rpc createNSNode (CreateNSNodeRequest) returns (CreateNSNodeResponse);
@@ -317,3 +323,25 @@ message UpdateNSNodeAPIConfigRequest {
int64 nsNodeId = 1;
bytes apiNodeAddrsJSON = 2;
}
// 列出所有需要升级的NS节点
message FindAllUpgradeNSNodesWithNSClusterIdRequest {
int64 nsClusterId = 1;
}
message FindAllUpgradeNSNodesWithNSClusterIdResponse {
repeated NSNodeUpgrade nodes = 1;
message NSNodeUpgrade {
NSNode nsNode = 1;
string os = 2;
string arch = 3;
string oldVersion = 4;
string newVersion = 5;
}
}
// 升级单个NS节点
message UpgradeNSNodeRequest {
int64 nsNodeId = 1;
}

View File

@@ -17,4 +17,5 @@ const (
SettingCodeStandaloneInstanceInitialized SettingCode = "standaloneInstanceInitialized" // 单体实例初始化状态
SettingCodeHTTPDNSDefaultBackupClusterId SettingCode = "httpdnsDefaultBackupClusterId" // HTTPDNS默认备用集群ID
SettingCodeUpgradeConfig SettingCode = "upgradeConfig" // 升级设置
)

View File

@@ -58,7 +58,7 @@ func (this *UserFeature) ToPB() *pb.UserFeature {
func FindAllUserFeatures() []*UserFeature {
return []*UserFeature{
{
Name: "记录访问日志",
Name: "访问日志记录",
Code: UserFeatureCodeServerAccessLog,
Description: "用户可以开启服务的访问日志。",
SupportPlan: true,
@@ -106,7 +106,7 @@ func FindAllUserFeatures() []*UserFeature {
SupportPlan: false,
},
{
Name: "开启WAF",
Name: "WAF",
Code: UserFeatureCodeServerWAF,
Description: "用户可以开启WAF功能并可以设置黑白名单等。",
SupportPlan: true,
@@ -136,7 +136,7 @@ func FindAllUserFeatures() []*UserFeature {
SupportPlan: true,
},
{
Name: "页面优化",
Name: "页面动态加密",
Code: UserFeatureCodeServerOptimization,
Description: "用户可以开启页面优化功能。",
SupportPlan: true,

View File

@@ -1,7 +1,7 @@
package teaconst
const (
Version = "1.4.8" //1.3.8.2
Version = "1.4.9" //1.3.8.2
ProductName = "Edge DNS"
ProcessName = "edge-dns"

View File

@@ -1,64 +0,0 @@
1.4.8版本ChangelogHTTPDNS功能全量发布
1、通过智能解析把用户就近调度到最优节点显著降低首包时延与连接抖动。
2、支持按地域、运营商、国内/海外精细分流,提升弱网与跨网访问稳定性。
3、解析链路内置签名鉴权与请求追踪增强安全性与可观测性。
4、无命中规则时自动回源兜底保障解析连续可用。
5、支持 A/AAAA 双栈与多记录返回,兼容不同终端网络环境。
6、提供Android、iOS、Flutter 多端SDK 开箱可用,支持预解析、缓存与同步/非阻塞解析。
7、提供 IP 直连适配能力(含 Host 保留与 No-SNI 模式),适配复杂 HTTPS 场景。
8、控制台支持应用/域名/规则全流程配置与在线验证,缩短问题定位和发布周期。
9、节点支持在线安装升级与日志上报降低运维复杂度并提升可维护性。
1.4.8版本升级步骤
1、备份现有配置与数据
将 edge-api、edge-admin、edge-user 等组件目录下的 configs 文件夹,以及平台的 MySQL 数据库进行全量备份;
2、停止旧版本进程管理端各组件
killall -9 edge-api
killall -9 edge-admin
killall -9 edge-user
3、上传并解压新版本包以 Linux x64 环境为例):
unzip -o edge-admin-linux-amd64-v1.4.8.zip -d /data/
unzip -o edge-user-linux-amd64-v1.4.8.zip -d /data/
4、依次运行edge-api、edge-admin、edge-user
# 启动 API 服务
cd /data/edge-api/bin
chmod +x edge-api
nohup ./edge-api 2>&1 &
# 启动管理后台
cd /data/edge-admin/bin
chmod +x edge-admin
nohup ./edge-admin 2>&1 &
# 启动租户控制台
cd /data/edge-user/bin
chmod +x edge-user
nohup ./edge-user 2>&1 &
5、检查版本状态
登录管理后台,确认系统版本显示为 1.4.8
6、配置主备集群
进入“HTTPDNS -> 集群列表 -> 集群设置”,按需勾选“默认主集群”或“默认备用集群”角色,以便后续应用自动关联;
7、在线升级 HTTPDNS 节点:
进入“HTTPDNS -> 节点列表”,点击对应节点的“详情”,在“安装信息”页面点击 **[在线安装]** 或 **[升级]**。系统会自动下发最新的 edge-httpdns 二进制文件并完成重启。
8、验证节点在线状态
等待 30 秒左右,确认节点状态恢复为“在线”,并验证硬件负载监控数据是否正常上报。
9、业务解析验证
使用控制台“解析测试”工具,验证域名在当前环境下是否能正确返回调度的 IP 地址。
10、完成升级。
特别说明:
1、在线升级模式Edge HTTPDNS 节点支持通过管理平台一键在线升级,无需手动上传文件和重启进程。
2、离线安装模式如节点服务器无法连接控制台可手动上传 edge-httpdns 压缩包并解压,更新 bin 目录下的程序文件后手动执行 `./edge-httpdns restart` 即可。
3、SNI 隐匿功能:请确保关联的 CDN 边缘节点也已同步更新至配套版本(会自动升级)。

View File

@@ -1,147 +0,0 @@
# HTTPDNS 主计划V1.2 现行设计)
## 1. 目标
1. 构建独立的 HTTPDNS 管理闭环:集群、应用、域名、自定义解析、访问日志、运行日志、解析测试。
2. 方案以当前已确认的页面与交互为准,不保留历史分支设计。
3. 优先保证可运维、可灰度、可观测,先落地稳定版本。
## 2. 信息架构(菜单)
1. 左侧 HTTPDNS 菜单顺序:
- 集群管理
- 应用管理
- 访问日志
- 运行日志
- 解析测试
2. 不再保留独立“全局配置”菜单。
3. 不再保留独立“SDK接入引导”菜单。
## 3. 核心设计约束
1. SNI 防护策略固定为“隐匿 SNI”不提供 level1/level3、mask/empty切换入口
2. 服务入口按“集群服务域名”管理,不使用全局单入口。
3. 回源协议默认 HTTPS不提供开关。
4. 域名校验默认开启,不在用户侧暴露开关。
5. 自定义解析先做精简版:不支持 SDNS 参数匹配。
## 4. 集群管理
### 4.1 集群列表
1. 字段:集群名称、服务域名、节点数、在线节点数、状态、操作。
2. 操作:节点列表、集群设置、删除集群。
### 4.2 节点列表
1. 字段节点名称、IP、CPU、内存、负载、状态、操作。
2. IP 列不展示“[下线]/[宕机]”附加标识。
### 4.3 集群设置
1. 页面布局对齐智能DNS左侧菜单 + 右侧配置区(`left-box with-menu` / `right-box with-menu`)。
2. 配置分组:
- 基础设置
- TLS
3. 基础设置字段:
- 集群名称
- 服务域名
- 默认解析 TTL
- 降级超时容忍度(毫秒)
- 节点安装根目录(默认 `/opt/edge-httpdns`
- 启用当前集群
- 默认集群(勾选)
4. TLS 配置:
- 样式与交互对齐智能DNS TLS页
- 维护并绑定该集群服务域名使用的证书
- 保证节点回源链路为 HTTPS
## 5. 应用管理
### 5.1 应用列表
1. 字段应用名称、AppID、绑定域名数、状态、操作。
2. 操作域名列表、应用设置、SDK集成、删除应用。
3. 删除应用页面交互风格对齐“删除集群”页面。
### 5.2 应用设置
1. 页面布局对齐智能DNS设置页风格左侧菜单 + 右侧配置区)。
2. 分组:
- 基础配置
- 认证与密钥
3. 基础配置字段:
- AppID只读
- 主集群
- 备集群(可选)
- 应用启用
- SNI 防护配置(文案展示:隐匿 SNI
4. 认证与密钥字段:
- 请求验签(状态展示 + 独立启停按钮 + 确认提示)
- 加签 Secret查看、复制、重置、最近更新时间
5. 交互约束:
- 强制 HTTPS 传输,不再提供独立“数据加密 Secret”配置项。
- 密钥操作区使用紧凑图标样式,减少视觉噪音。
## 6. 域名管理与自定义解析
### 6.1 域名管理
1. 页面采用框架标准顶部 tab + 面包屑样式。
2. 字段:
- 域名列表
- 规则策略(仅展示数字,表示该域名规则数,可点击)
- 操作(自定义解析、解绑)
3. 入口:在域名行操作中直接进入“自定义解析”。
### 6.2 自定义解析(精简版)
1. 规则字段:
- 规则名称
- 线路
- 解析记录值
- TTL
- 状态
2. 线路联动:
- 第一层:`中国地区` / `境外`
- 中国地区:运营商 -> 大区 -> 省份
- 境外:洲 -> 国家/地区(亚洲内使用中国香港/中国澳门/中国台湾)
3. 解析记录:
- 每条规则最多 10 条
- 支持 A / AAAA
- 可开启权重调度
4. 不包含 SDNS 参数配置。
## 7. 访问日志
1. 菜单与页面文案统一为“访问日志”(不再使用“解析日志”)。
2. 页面结构筛选区与列表区分离间距与智能DNS访问日志风格一致。
3. 列字段:集群、节点、域名、类型、概要。
4. 概要展示:
- 使用单行拼接
- 按“访问信息 -> 解析结果”顺序
- 不显示字段名堆叠
## 8. 运行日志
1. 字段与智能DNS运行日志保持一致。
2. 级别样式使用文字着色,不使用大块背景色。
## 9. 解析测试
1. 去掉“API在线沙盒”标题与右侧等待占位图标区。
2. 解析配置区标题统一为“解析测试”。
3. 测试参数保留目标应用、所属集群、解析域名、模拟客户端IP、解析类型A/AAAA
4. 所属集群使用下拉框选择。
5. 解析域名使用下拉框选择(按当前目标应用联动展示)。
6. 去掉 SDNS 参数。
7. 结果区保留核心结果展示,参考阿里云风格的简洁结果布局。
## 10. SDK 集成
1. SDK 集成不作为左侧独立菜单。
2. 在“应用管理”操作列进入 SDK 集成页。
3. 页面只保留:
- SDK 下载
- 集成文档
4. 卡片与按钮采用紧凑布局,避免按钮区域拥挤。
## 11. SDK 与服务端接口(现行)
1. 解析接口:`/resolve`
- SDK 请求域名解析结果的主接口。
2. 启动配置接口:`/bootstrap`(规划/联调口径)
- 用于 SDK 获取可用服务域名与策略参数替代节点IP调度模式
3. SDK 地址策略:优先主集群服务域名,不可用时切备集群;失败后按客户端降级策略处理。
## 12. 本文明确不包含
1. 不包含全局配置独立页面设计。
2. 不包含 SDK 接入向导独立菜单设计。
3. 不包含 SNI level1/level3、ECH 控制台、Public SNI 池分级配置。
4. 不包含 SDNS 参数匹配链路。
5. 不包含第三方 DNS 依赖与复用方案。

View File

@@ -1,255 +0,0 @@
# HTTPDNS后端开发计划V1.0
## 0. 文档信息
- 目标文件:`EdgeHttpDNS/HTTPDNS后端开发计划.md`
- 交付范围:`EdgeAdmin + EdgeAPI + EdgeHttpDNS + SDK对接接口`
- 交付策略:一次性全量交付(非分阶段)
- 核心约束:
- 仅新协议
-`/bootstrap`
- `/resolve` 使用 GET 参数
- 线路匹配仅基于客户端 IP 归属
- 独立 `edgeHTTPDNS*` 数据表
- 新增独立节点角色 `NodeRoleHTTPDNS`
- 访问日志 MySQL + ClickHouse 双写(查询优先 ClickHouse
- 不复用智能DNSEdgeDNS/NS模块的 DAO/Service/StoreHTTPDNS 独立实现(可参考并复制所需能力)
## 1. 目标与成功标准
1. 将 HTTPDNS 从当前 Admin 侧 mock/store 方案落地为真实后端能力。
2. 打通“配置 -> 下发 -> 解析 -> 日志 -> 查询”闭环。
3. 与当前前端设计严格对齐:
- 菜单:集群管理、应用管理、访问日志、运行日志、解析测试
- SNI 固定为“隐匿 SNI”
- 自定义解析不含 SDNS 参数
4. 成功标准:
- 管理页面均通过 RPC 读取真实数据,无本地 mock 依赖
- `/resolve` 可按应用/域名/线路返回解析结果
- 访问日志与运行日志可查询、可筛选、可分页
- 节点配置与状态可下发和回传
- 主备集群服务域名可在应用设置中配置并生效
- EdgeHttpDNS 支持 SNI 与 Host 解耦路由,并可执行 WAF 动态验签与隐匿 SNI 转发
## 2. 架构与边界
### 2.1 服务边界
1. EdgeAdmin仅负责页面动作与 RPC 编排,不存业务状态。
2. EdgeAPI负责数据存储、策略匹配、接口服务、日志汇聚。
3. EdgeHttpDNSHTTPDNS节点负责执行解析、上报运行日志/访问日志、接收任务。
4. SDK手动配置应用关联主备服务域名调用 `/resolve` 获取结果。
### 2.2 不做项
1. 不做 `/bootstrap` 接口。
2. 不做 ECH / SNI 分级策略。
3. 不做 SDNS 参数匹配。
4. 不做第三方 DNS 依赖复用。
## 3. 公开接口与契约(需新增/调整)
### 3.1 HTTP 解析接口
1. `GET /resolve`
2. 请求参数:
- `appId`(必填)
- `dn`(必填)
- `qtype`可选A/AAAA默认 A
- `cip`(可选)
- `sid``sdk_version``os`(可选,用于日志)
- `nonce``exp``sign`(可选,验签开启时必需)
3. 响应结构(统一):
- `code``message``requestId`
- `data`
- `domain`
- `qtype`
- `ttl`
- `records[]``type`,`ip`,`weight?`,`line?`,`region?`
- `client``ip`,`region`,`carrier`,`country`
- `summary`(命中规则摘要)
4. 错误码最低集合:
- app 无效/禁用
- 域名未绑定
- 验签失败
- 无可用解析记录
- 内部解析失败/超时
### 3.2 管理 RPCEdgeAdmin -> EdgeAPI
1. 新增服务:
- `HTTPDNSClusterService`
- `HTTPDNSNodeService`
- `HTTPDNSAppService`
- `HTTPDNSDomainService`
- `HTTPDNSRuleService`
- `HTTPDNSAccessLogService`
- `HTTPDNSRuntimeLogService`
- `HTTPDNSSandboxService`
2. 最小方法集:
- 集群增删改查、设置默认集群、TLS证书绑定、节点列表/状态
- 应用:增删改查、主备集群设置、启停、验签开关、密钥重置
- 域名:绑定/解绑/列表
- 自定义解析:规则增删改查、启停、排序
- 日志:访问日志分页查询、运行日志分页查询
- 测试:在线解析测试调用(入参包含 appId、clusterId、domain、qtype、clientIp
### 3.3 节点日志上报 RPCEdgeHttpDNS -> EdgeAPI
1. `CreateHTTPDNSAccessLogs`(批量)
2. `CreateHTTPDNSRuntimeLogs`(批量)
3. 幂等键:`requestId + nodeId`
4. 支持高吞吐批量提交和失败重试
### 3.4 节点路由与 WAF 策略下发契约EdgeAPI -> EdgeHttpDNS
1. 下发内容最小集合:
- `appId/domain/serviceDomain`
- `sniMode`(固定为隐匿 SNI
- `hostRouteMode`SNI 与 Host 解耦)
- `wafVerifyEnabled`
- `wafVerifyPolicy`(验签字段、时效窗口、失败动作)
2. 节点执行口径:
- 入站按 `serviceDomain` 接入,按 Host/业务域名做路由匹配
- TLS 握手与业务 Host 解耦,避免真实业务域名暴露在 SNI
- 命中需要验签的应用时,先执行 WAF 动态验签,再继续解析链路
3. 失败处置:
- 验签失败按策略拒绝并记录运行日志、访问日志错误码
- 路由未命中返回统一错误码并上报审计日志
## 4. 数据模型设计(独立 HTTPDNS 表)
1. `edgeHTTPDNSClusters`
- `id,name,isOn,isDefault,serviceDomain,defaultTTL,fallbackTimeoutMs,installDir,tlsPolicyJSON,createdAt,updatedAt`
2. `edgeHTTPDNSNodes`
- `id,clusterId,name,isOn,isUp,isInstalled,isActive,statusJSON,installStatusJSON,installDir,uniqueId,secret,createdAt,updatedAt`
3. `edgeHTTPDNSApps`
- `id,name,appId,isOn,primaryClusterId,backupClusterId,sniMode(fixed_hide),createdAt,updatedAt`
4. `edgeHTTPDNSAppSecrets`
- `id,appId,signEnabled,signSecretEnc,signUpdatedAt,updatedAt`
5. `edgeHTTPDNSDomains`
- `id,appId,domain,isOn,createdAt,updatedAt`
6. `edgeHTTPDNSCustomRules`
- `id,appId,domainId,ruleName,lineScope,lineCarrier,lineRegion,lineProvince,lineContinent,lineCountry,ttl,isOn,priority,updatedAt`
7. `edgeHTTPDNSCustomRuleRecords`
- `id,ruleId,recordType,recordValue,weight,sort`
8. `edgeHTTPDNSAccessLogs`
- `id,requestId,clusterId,nodeId,appId,domain,qtype,clientIP,clientRegion,carrier,sdkVersion,os,resultIPs,status,errorCode,costMs,createdAt,day`
9. `edgeHTTPDNSRuntimeLogs`
- `id,clusterId,nodeId,level,type,tag,description,count,createdAt,day`
### 4.1 索引与唯一约束
1. 唯一:`edgeHTTPDNSApps.appId`
2. 唯一:`edgeHTTPDNSDomains(appId,domain)`
3. 唯一:`edgeHTTPDNSAccessLogs(requestId,nodeId)`
4. 索引:
- 访问日志:`day,clusterId,nodeId,domain,status,createdAt`
- 规则匹配:`domainId,isOn,priority,lineScope,...`
- 应用查询:`name,appId,isOn`
## 5. 解析引擎实现EdgeAPI
1. 输入校验:`appId/dn/qtype`
2. 应用与域名校验:
- app 存在且启用
- 域名已绑定到 app
3. 线路归属:
- `cip` 优先,其次 remote IP
- 映射字段:运营商/大区/省份/洲/国家
4. 规则匹配:
- 精确匹配 > 半精确 > 默认
- 同级按 `priority` 从小到大
5. 记录返回:
- 权重关闭:返回全部记录
- 权重开启:按权重算法返回单条或子集(固定口径)
6. TTL 取值:
- 命中规则取规则 TTL
- 未命中规则取集群默认 TTL
7. 验签:
- `signEnabled=true` 时必须通过签名校验
- `signEnabled=false` 时跳过签名校验
8. 访问日志:
- 解析结束异步写日志
- 双写 MySQL/ClickHouse
## 6. 节点与任务链路
1.`EdgeCommon/pkg/nodeconfigs` 增加 `NodeRoleHTTPDNS`
2. 在任务系统增加 HTTPDNS 任务类型:
- 配置变更
- 应用变更
- 域名变更
- 规则变更
- 证书变更
- 路由与 WAF 策略变更
3. EdgeHttpDNS 增加 HTTPDNS 子服务:
- 接收配置快照
- 执行解析
- 上报运行/访问日志
- 执行 SNI/Host 解耦路由
- 执行 WAF 动态验签
- 执行隐匿 SNI 转发
4. 复用现有安装升级框架,但节点角色、任务通道、日志 tag 独立。
## 7. EdgeAdmin 后端改造
1.`internal/web/actions/default/httpdns/*` 的 store/mock 读写改为 RPC。
2. 删除/停用旧能力路由:
- `/httpdns/policies`
- `/httpdns/guide`
- `/httpdns/ech`
3. 保留必要跳转,避免旧链接 404。
4. `sandbox/test` 改为调用真实解析服务(可保留测试开关)。
5. 解析测试页面交互固定为:
- 配置区标题“解析测试”
- 所属集群使用下拉框
- 解析域名使用下拉框并按目标应用联动
## 8. 安全与审计
1. Secret 持久化使用加密存储(至少密文列),返回时脱敏。
2. 操作审计记录:
- 验签开关启停
- Sign Secret 重置
- 应用主备集群修改
3. 验签失败日志保留 `requestId,errorCode,sourceIP`
4. 防滥用:
- `/resolve` 增加基础限流与异常请求过滤(按 appId + IP 维度)。
5. 节点侧安全执行:
- WAF 动态验签失败必须留存审计日志(含 requestId/appId/sourceIP
- SNI/Host 解耦路由命中结果需可追踪(用于问题回溯)
## 9. 测试与验收
### 9.1 单元测试
1. 规则匹配优先级与线路匹配
2. 验签成功/失败路径
3. 权重返回算法
4. DAO CRUD 与唯一约束
### 9.2 集成测试
1. EdgeAdmin -> RPC -> DB 全链路
2. `/resolve` 各错误码分支
3. 节点日志上报双写MySQL+CH
4. CH 不可用时 MySQL 回退查询
5. EdgeAPI 策略下发 -> EdgeHttpDNS 路由/WAF 执行 -> 日志落库全链路
### 9.3 回归测试
1. 智能DNS功能不受影响
2. 菜单与权限不串模块
3. 旧入口跳转正确,无新增 404
### 9.4 验收用例
1. 应用配置主备服务域名后SDK可解析成功
2. 主集群故障时可切备集群
3. 自定义解析按线路返回预期 IP
4. 访问日志筛选与概要展示正确
5. 运行日志级别/字段与智能DNS一致
6. EdgeHttpDNS 可在 SNI 与 Host 解耦场景下正确路由到目标应用
7. 开启 WAF 动态验签后,合法请求通过、非法签名请求被拒绝且有审计日志
## 10. 发布与回滚
1. 发布顺序:
- DB migration
- EdgeAPIDAO+RPC+resolve
- EdgeHttpDNS角色+上报)
- EdgeAdminRPC切换
2. 开关控制:
- `httpdns.resolve.enabled`
- `httpdns.log.clickhouse.enabled`
3. 回滚策略:
- 关闭 `resolve` 新实现开关
- 访问日志读取切回 MySQL
- Admin 保留只读能力
## 11. 默认值与固定决策
1.`/bootstrap`SDK手动配置主备服务域名。
2. `SNI` 固定“隐匿 SNI”。
3. 自定义解析无 SDNS 参数。
4. 线路仅客户端 IP 归属。
5. 日志双写,查询优先 ClickHouse。
6. 节点角色独立:`NodeRoleHTTPDNS`

View File

@@ -1,98 +0,0 @@
# Edge HTTPDNS 用户使用手册
欢迎使用 Edge HTTPDNS 服务。本文档旨在帮助您快速完成应用创建、域名配置及解析测试,实现精准、安全的业务调度。
---
## 1. 快速入门流程
1. **创建应用**获取接入凭证AppId 和 SecretKey
2. **添加域名**:登记需要通过 HTTPDNS 解析的业务域名。
3. **自定义解析规则**:设置域名对应的 IP 地址及智能分流规则。
4. **解析测试**:通过沙箱工具验证解析是否生效。
5. **集成 SDK**:将解析功能集成至您的 App 中。
---
## 2. 应用管理
应用是您接入 HTTPDNS 的基础单元。
### 2.1 创建应用
1. 登录用户控制台,点击 **HTTPDNS -> 应用列表**
2. 点击 **创建应用**
3. 填写应用名称如“我的安卓App”
4. 系统会自动关联默认的服务域名,无需手动选择。
### 2.2 获取接入凭证
在应用详情页面,您可以找到:
* **AppId**:应用的唯一识别 ID。
* **SecretKey**:签名密钥。**请务必妥善保管,切勿泄露。** 在 SDK 初始化时使用此密钥可开启“签名鉴权”,防止解析接口被他人盗刷。
---
## 3. 域名与记录配置
### 3.1 添加域名
1. 进入应用详情页,切换至 **域名管理** 标签。
2. 点击 **添加域名**,输入您的业务域名(如 `api.example.com`)。
### 3.2 自定义解析规则
**作用**:自定义解析规则允许您根据终端用户的网络环境(如运营商)或物理位置,为其分配最优的访问地址。通过精细化的线路调度,可以有效降低跨境或跨网访问带来的延迟,提升 App 的响应速度。
点击域名后的 **解析规则**,进入详细设置:
* **解析类型**:支持 **A 记录 (IPv4)****AAAA 记录 (IPv6)**
* **线路选择**:可选择针对特定运营商(如:移动、电信、联通)或特定地域(如:浙江省、海外)进行精准匹配。若不选择则代表全局默认配置。
* **解析结果**:填写您的服务器目标 IP 地址。
* **TTL**:解析结果在客户端缓存的时间。默认为 30 秒,建议保持默认以兼顾调度灵活性。
---
## 4. 配合 CDN 实现网络加速与安全
Edge HTTPDNS 不仅仅提供域名解析功能,更通过与 CDN 节点的深度集成,解决了移动端常见的 **HTTPS SNI 隐匿访问**及 **跨运营商加速**问题。
### 4.1 自动获取 CDN 边缘节点
如果您在系统内开通了 **CDN 加速服务**,只需将业务域名配置在 CDN 平台中,并将其 CNAME 解析指向 CDN 提供的地址:
* **智能调度**HTTPDNS 会自动识别该域名已接入 CDN并针对终端用户的地理位置和运营商智能返回最优的 **CDN 边缘节点 IP**
* **无感知兜底**:如果域名未接入 CDN 或未配置解析规则HTTPDNS 将自动回源查询 **权威 DNS**,并返回业务真实的源站 IP确保解析永不中断。
### 4.2 解决 HTTPS SNI 隐匿问题
在使用 IP 直连(如 `https://1.2.3.4/path`)访问 HTTPS 业务时,传统的网络库会因为无法获取正确的 Host 导致 SSL 握手失败。
**我们的方案:**
* **配合 CDN 节点**:我们的 CDN 节点已针对 HTTPDNS 进行了特殊适配。
* **SDK 自动适配**SDK 内部集成了标准适配器。在您的代码中,只需配合解析出的 IP 设置 HTTP 请求头的 `Host` 字段,即可透明地完成 SNI 握手,无需复杂的 SSL 改写逻辑。
* **稳定性保障**:通过 CDN 节点的全局负载均衡即使某个节点异常HTTPDNS 也会实时踢除并将流量导向其他可用节点,确保业务高可用。
---
## 4. 调试与验证
### 4.1 在线解析测试
在左侧菜单进入 **解析测试**
1. 选择您创建的 **应用**、**HTTPDNS 服务域名** 和 **待解析域名**
2. **模拟客户端 IP**(可选):输入特定地区的 IP验证该地区的解析结果是否符合预期地域调度验证
3. 点击 **在线解析**,查看返回的具体 IP 列表。
### 4.2 访问日志查询
**访问日志** 中,您可以实时监控解析请求:
* 查看各个 AppId 下域名的解析成功率。
* 查看请求的来源 IP、耗时以及命中的路由规则。
---
## 5. 获取 SDK
**应用详情 -> SDK下载** 中:
* 您可以下载最新版本的 Android、iOS 或 Flutter SDK 压缩包。
* 查看配套的 **SDK 集成文档**
---
## 6. 常见问题 (FAQ)
* **Q为什么我设置了记录解析测试却返回为空**
* A请检查记录是否已启用或者检查该域名是否已被添加到对应的 AppId 允许列表下。
* **Q如何应对冷启动时的解析延迟**
* A建议在 SDK 初始化后调用“解析预热”接口,提前将热点域名加载至缓存。
* **QSecretKey 泄露了怎么办?**
* A请在应用设置中重置 SecretKey并在 App 代码中同步更新。

View File

@@ -1,82 +0,0 @@
# Edge HTTPDNS 管理员配置手册
本文档汇总了 Edge HTTPDNS 的核心配置流程,重点介绍了集群管理、节点在线安装以及多集群调度的详细操作。
---
## 1. 集群管理
集群是 HTTPDNS 服务的基本组织单元。通过设置“默认”角色,可以实现应用配置的自动关联与 SDK 侧的高可用容灾。
### 1.1 默认集群角色定义
在“集群设置”中,您可以将集群开启为 **“设为默认集群”**,并指派以下角色:
* **默认主集群**
* **自动关联**:当在控制台“添加应用”时,系统会自动将该应用关联到此默认主集群。
* **服务首选**SDK 初始化后,会优先使用该集群的服务域名进行解析请求。
* **默认备用集群**
* **自动容灾**当默认主集群的节点全部宕机或网络不可达时SDK 会自动切换至默认备用集群进行解析,确保业务不中断。
* **自动关联**:与主集群一样,新应用创建时也会自动关联此备用集群信息。
> **注意**:同一时刻,系统内仅允许存在一个“默认主集群”和一个“默认备用集群”。新设置的默认集群会自动取消之前的旧设置。
### 1.2 核心参数配置
* **服务域名**:该集群对外提供 HTTPDNS 服务的接入地址。
* **降级超时容忍度**:指节点回源查询上游 DNS 时的最大等待时间(单位:毫秒)。若超过此阈值未获得结果,将视作解析失败。该选项可在“集群设置”中统一调整。
* **默认 TTL**:解析结果在客户端缓存的缺省时长(默认 30s
* **TLS 设置**
* **端口绑定**:通常绑定 `443`
* **SSL 证书**:必须配置合法证书,否则 SDK 的 HTTPS 请求将失败。
* **TLS 版本**:默认支持 TLS 1.1 及以上版本。
---
## 2. 节点安装与维护
节点是处理解析请求的实体。Edge HTTPDNS 支持“在线安装”。
### 2.1 在线安装
1. **创建节点**:在集群下点击“创建节点”,填写名称及公网 IP。
2. **配置 SSH**:在节点详情中点击“设置 SSH”输入服务器的 Host、Port 及登录授权。
3. **启动安装**:在“安装信息”页面点击 **[开始安装]**。
* 自动下发二进制文件及服务脚本。
* 自动生成 `configs/api_httpdns.yaml`(包含节点识别需要的 `nodeId``secret`)。
4. **实时状态**:安装成功后,节点状态变为 **[在线]**,控制台每 30 秒更新一次节点的 CPU、内存及负载数据。
---
## 3. 应用与解析规则
### 3.1 接入应用管理
* **AppId/SecretKey**:创建应用后生成的凭证。应用在创建时已根据上述“默认集群”设定自动关联了服务入口。
* **鉴权配置**:开启“签名鉴权”可配合 SDK 的 `setSecretKey` 接口,杜绝接口被盗刷风险。
### 3.2 智能解析策略
* **线路/地域匹配**:根据来源运营商和地理位置返回最优 IP。
---
## 4. 调试与监控
### 4.1 解析测试
**调试工具 -> 解析测试** 中,您可以模拟客户端请求:
* 支持指定 **目标应用** 与 **所属集群**
* 支持模拟 **客户端 IP** 以验证 ECS 掩码及地域调度效果。
* 实时展示请求 URL、客户端归属地、命中线路以及解析结果IP 列表与 TTL
### 4.2 访问日志
**访问日志** 菜单中,可实时查阅所有终端发起的解析请求:
* 记录包括:请求时间、客户端 IP/操作系统、SDK 版本、解析域名、耗时以及最终返回的 IP 结果。
* 支持按 AppID、域名、状态成功/失败)或关键字搜索排查。
### 4.3 运行日志
记录服务端及节点的底层运行事件:
* 包括节点心跳、SSL 证书加载、API 连接状态等系统级信息。
* 分为 Error、Warning、Info 等级别,是排查节点离线或连接故障的首要工具。
---
## 5. 常见问题
* **节点与 API 时间偏离**:若节点时间与 API Server 相差超过 30s会导致鉴权失败SIGN_INVALID。请务必开启 NTP 时间同步。
* **SDK 无法连接备用集群**:请检查默认备用集群的 SSL 证书是否有效,以及防火墙端口是否开放。

View File

@@ -1,92 +0,0 @@
# SNI隐匿开发计划HTTPDNS专项
## 0. 目标
在明文网络层DNS 与 TLS ClientHello中不出现真实业务域名真实业务域名仅出现在加密后的 HTTP 层Host / :authority中传输。
## 1. 固定原则
1. 只做 SNI 隐匿能力,不引入其他无关能力描述。
2. 客户端不走系统 DNS 解析业务域名。
3. TLS 握手阶段不发送真实业务域名 SNI。
4. CDN/WAF 节点必须支持“空SNI接入 + Host路由”。
5. 真实域名只在 HTTPS 加密通道内携带。
## 2. 端到端链路(目标形态)
1. App 调用 SDK请求解析业务域名。
2. SDK 从 HTTPDNS 获取业务域名对应的“接入 IP 列表”(不是业务域名 DNS
3. SDK 直连接入 IP 发起 TLS
- SNI 置空(或不发送)
- 不出现真实业务域名
4. TLS 建立后发起 HTTPS 请求:
- Host / :authority = 真实业务域名
5. CDN/WAF 节点在解密后读取 Host将流量路由到对应业务源站。
## 3. SDK 改造要求
### 3.1 连接行为
1. 提供“按 IP 直连”的请求通道。
2. TLS 握手时固定空 SNI不允许带真实域名。
3. HTTP 层强制写入真实 Host / :authority。
### 3.2 证书校验
1. 仍必须做证书链校验(不允许关闭 TLS 安全校验)。
2. 证书校验目标为接入层证书CDN/WAF 对外证书),而非业务源站证书。
### 3.3 多IP与容错
1. HTTPDNS 返回多个接入 IP 时SDK 按顺序/策略重试。
2. 连接失败可切换下一 IP。
3. 缓存与过期策略保持稳定,避免频繁抖动。
### 3.4 多端一致性
1. Android/iOS/Flutter 需保证一致行为:
- 空 SNI
- Host 注入
- 失败重试策略
2. 文档与示例代码同步更新。
## 4. CDN/WAF 节点改造要求
### 4.1 TLS 接入
1. 支持无 SNI ClientHello 的 TLS 握手。
2. 为接入域名部署有效证书(覆盖客户端连接目标)。
### 4.2 路由逻辑
1. 以 Host / :authority 作为业务路由主键。
2. 路由匹配前做标准化:
- 小写化
- 去端口
3. Host 未命中时返回明确错误4xx禁止兜底到默认站点。
### 4.3 回源行为
1. 节点到源站可继续使用 HTTPS 回源。
2. 回源主机名与证书校验按现有网关策略执行。
### 4.4 可观测性
1. 增加日志字段:
- `tlsSniPresent`(是否携带 SNI
- `host`
- `routeResult`
2. 可按“空SNI请求占比、Host路由命中率”监控。
## 5. 控制面(管理端)要求
1. 页面仅展示“已启用 SNI 隐匿空SNI不提供策略切换。
2. 集群侧需可检查“节点是否支持空SNI接入”。
3. 发布配置时支持灰度与回滚。
## 6. 验收标准
1. 抓包验证:
- DNS 明文流量中不出现真实业务域名
- TLS ClientHello 中不出现真实业务域名 SNI
2. 请求验证:
- HTTPS 请求 Host 为真实业务域名
- CDN/WAF 按 Host 正确路由
3. 稳定性验证:
- 多 IP 切换成功
- 节点故障时请求可恢复
## 7. 上线顺序
1. 先升级 CDN/WAF 节点能力空SNI接入 + Host路由
2. 再升级 SDK空SNI + Host注入
3. 最后按应用灰度开启,观察指标后全量。
## 8. 风险与约束
1. 若 CDN/WAF 不支持空 SNI链路会在握手阶段失败。
2. 若 Host 路由不严格,可能出现串站风险。
3. 若客户端错误关闭证书校验,会引入严重安全风险。

View File

@@ -1,7 +1,7 @@
package teaconst
const (
Version = "1.4.8"
Version = "1.4.9"
ProductName = "Edge HTTPDNS"
ProcessName = "edge-httpdns"

View File

@@ -270,23 +270,24 @@ func (s *ResolveServer) desiredAddrs(snapshot *LoadedSnapshot) []string {
func (s *ResolveServer) reloadCertFromSnapshot(snapshot *LoadedSnapshot) {
cfg := s.parseTLSConfig(snapshot)
if cfg == nil || cfg.SSLPolicy == nil || len(cfg.SSLPolicy.Certs) == 0 {
// 没有TLS配置标记已处理不需要重试
s.certMu.Lock()
s.certSnapshotAt = snapshot.LoadedAt
s.certMu.Unlock()
reportRuntimeLog("info", "tls", "resolve", "no TLS policy in cluster snapshot, skipped cert reload", fmt.Sprintf("cert-skip-%d", snapshot.LoadedAt))
return
}
if err := cfg.SSLPolicy.Init(context.Background()); err != nil {
log.Println("[HTTPDNS_NODE][resolve]init SSLPolicy failed:", err.Error())
s.certMu.Lock()
s.certSnapshotAt = snapshot.LoadedAt
s.certMu.Unlock()
reportRuntimeLog("error", "tls", "resolve", "init SSLPolicy failed: "+err.Error(), fmt.Sprintf("cert-err-%d", snapshot.LoadedAt))
// 不更新 certSnapshotAt,下次 watchLoop 会重试
return
}
cert := cfg.SSLPolicy.FirstCert()
if cert == nil {
s.certMu.Lock()
s.certSnapshotAt = snapshot.LoadedAt
s.certMu.Unlock()
log.Println("[HTTPDNS_NODE][resolve]SSLPolicy has no valid certificate after Init")
reportRuntimeLog("error", "tls", "resolve", "SSLPolicy has no valid certificate after Init", fmt.Sprintf("cert-err-%d", snapshot.LoadedAt))
// 不更新 certSnapshotAt下次 watchLoop 会重试
return
}
@@ -295,6 +296,7 @@ func (s *ResolveServer) reloadCertFromSnapshot(snapshot *LoadedSnapshot) {
s.certSnapshotAt = snapshot.LoadedAt
s.certMu.Unlock()
log.Println("[HTTPDNS_NODE][resolve]TLS certificate reloaded from snapshot")
reportRuntimeLog("info", "tls", "resolve", "TLS certificate reloaded from snapshot successfully", fmt.Sprintf("cert-ok-%d", snapshot.LoadedAt))
}
func (s *ResolveServer) startListener(addr string) error {
@@ -561,6 +563,7 @@ func (s *ResolveServer) handleResolve(writer http.ResponseWriter, request *http.
},
})
if s.isAccessLogEnabled(snapshot) {
s.enqueueAccessLog(&pb.HTTPDNSAccessLog{
RequestId: requestID,
ClusterId: snapshot.ClusterID,
@@ -583,6 +586,7 @@ func (s *ResolveServer) handleResolve(writer http.ResponseWriter, request *http.
Summary: summary,
})
}
}
func pickDefaultTTL(snapshot *LoadedSnapshot, app *pb.HTTPDNSApp) int32 {
if snapshot == nil {
@@ -655,6 +659,7 @@ func (s *ResolveServer) writeFailedResolve(
nodeID = snapshot.NodeID
}
if s.isAccessLogEnabled(snapshot) {
s.enqueueAccessLog(&pb.HTTPDNSAccessLog{
RequestId: requestID,
ClusterId: clusterID,
@@ -677,6 +682,7 @@ func (s *ResolveServer) writeFailedResolve(
Summary: summary,
})
}
}
func (s *ResolveServer) writeResolveJSON(writer http.ResponseWriter, status int, resp *resolveResponse) {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
@@ -1424,6 +1430,17 @@ func ruleRegionSummary(rule *pb.HTTPDNSCustomRule) string {
return ""
}
func (s *ResolveServer) isAccessLogEnabled(snapshot *LoadedSnapshot) bool {
if snapshot == nil || snapshot.ClusterID <= 0 {
return true
}
cluster := snapshot.Clusters[snapshot.ClusterID]
if cluster == nil {
return true
}
return cluster.GetAccessLogIsOn()
}
func (s *ResolveServer) enqueueAccessLog(item *pb.HTTPDNSAccessLog) {
if item == nil {
return

View File

@@ -37,6 +37,8 @@ type SnapshotManager struct {
locker sync.RWMutex
snapshot *LoadedSnapshot
timezone string
}
func NewSnapshotManager(quitCh <-chan struct{}) *SnapshotManager {
@@ -144,7 +146,7 @@ func (m *SnapshotManager) RefreshNow(reason string) error {
}
snapshot := &LoadedSnapshot{
LoadedAt: time.Now().Unix(),
LoadedAt: time.Now().UnixNano(),
NodeID: nodeResp.GetNode().GetId(),
ClusterID: nodeResp.GetNode().GetClusterId(),
Clusters: clusters,
@@ -156,5 +158,36 @@ func (m *SnapshotManager) RefreshNow(reason string) error {
m.locker.Unlock()
reportRuntimeLog("info", "config", "snapshot", "snapshot refreshed: "+reason, fmt.Sprintf("snapshot-%d", time.Now().UnixNano()))
// timezone sync - prefer current node's cluster timezone
var timeZone string
if snapshot.ClusterID > 0 {
if cluster := clusters[snapshot.ClusterID]; cluster != nil && len(cluster.GetTimeZone()) > 0 {
timeZone = cluster.GetTimeZone()
}
}
// fallback to any non-empty cluster timezone for compatibility
if len(timeZone) == 0 {
for _, cluster := range clusters {
if cluster != nil && len(cluster.GetTimeZone()) > 0 {
timeZone = cluster.GetTimeZone()
break
}
}
}
if len(timeZone) == 0 {
timeZone = "Asia/Shanghai"
}
if m.timezone != timeZone {
location, err := time.LoadLocation(timeZone)
if err != nil {
log.Println("[HTTPDNS_NODE][TIMEZONE]change time zone failed:", err.Error())
} else {
log.Println("[HTTPDNS_NODE][TIMEZONE]change time zone to '" + timeZone + "'")
time.Local = location
m.timezone = timeZone
}
}
return nil
}

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