diff --git a/EdgeAPI/build/build.sh b/EdgeAPI/build/build.sh
index dade19e..9c58075 100644
--- a/EdgeAPI/build/build.sh
+++ b/EdgeAPI/build/build.sh
@@ -94,6 +94,32 @@ function build() {
fi
fi
+ # build edge-httpdns
+ HTTPDNS_ROOT=$ROOT"/../../EdgeHttpDNS"
+ if [ -d "$HTTPDNS_ROOT" ]; then
+ HTTPDNSNodeVersion=$(lookup-version "$ROOT""/../../EdgeHttpDNS/internal/const/const.go")
+ echo "building edge-httpdns v${HTTPDNSNodeVersion} ..."
+ EDGE_HTTPDNS_NODE_BUILD_SCRIPT=$ROOT"/../../EdgeHttpDNS/build/build.sh"
+ if [ ! -f "$EDGE_HTTPDNS_NODE_BUILD_SCRIPT" ]; then
+ echo "unable to find edge-httpdns build script 'EdgeHttpDNS/build/build.sh'"
+ exit
+ fi
+ cd "$ROOT""/../../EdgeHttpDNS/build" || exit
+ echo "=============================="
+ architects=("amd64")
+ #architects=("amd64" "arm64")
+ for arch in "${architects[@]}"; do
+ # always rebuild to avoid reusing stale zip when version number is unchanged
+ ./build.sh linux "$arch"
+ done
+ echo "=============================="
+ cd - || exit
+
+ for arch in "${architects[@]}"; do
+ cp "$ROOT""/../../EdgeHttpDNS/dist/edge-httpdns-linux-${arch}-v${HTTPDNSNodeVersion}.zip" "$ROOT"/deploy/edge-httpdns-linux-"${arch}"-v"${HTTPDNSNodeVersion}".zip
+ done
+ fi
+
# build sql
if [ $TAG = "plus" ]; then
echo "building sql ..."
diff --git a/EdgeAPI/internal/clickhouse/client.go b/EdgeAPI/internal/clickhouse/client.go
index 96fe903..dba0895 100644
--- a/EdgeAPI/internal/clickhouse/client.go
+++ b/EdgeAPI/internal/clickhouse/client.go
@@ -1,6 +1,7 @@
package clickhouse
import (
+ "bytes"
"context"
"crypto/tls"
"encoding/json"
@@ -12,13 +13,11 @@ import (
"time"
)
-// Client 通过 HTTP 接口执行只读查询(SELECT),返回 JSONEachRow 解析为 map 或结构体
type Client struct {
cfg *Config
httpCli *http.Client
}
-// NewClient 使用共享配置创建客户端
func NewClient() *Client {
cfg := SharedConfig()
transport := &http.Transport{}
@@ -28,6 +27,7 @@ func NewClient() *Client {
ServerName: cfg.TLSServerName,
}
}
+
return &Client{
cfg: cfg,
httpCli: &http.Client{
@@ -37,21 +37,20 @@ func NewClient() *Client {
}
}
-// IsConfigured 是否已配置
func (c *Client) IsConfigured() bool {
return c.cfg != nil && c.cfg.IsConfigured()
}
-// Query 执行 SELECT,将每行 JSON 解析到 dest 切片;dest 元素类型需为 *struct 或 map
func (c *Client) Query(ctx context.Context, query string, dest interface{}) error {
if !c.IsConfigured() {
return fmt.Errorf("clickhouse: not configured")
}
- // 强制 JSONEachRow 便于解析
+
q := strings.TrimSpace(query)
if !strings.HasSuffix(strings.ToUpper(q), "FORMAT JSONEACHROW") {
query = q + " FORMAT JSONEachRow"
}
+
u := c.buildURL(query)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
@@ -60,28 +59,32 @@ func (c *Client) Query(ctx context.Context, query string, dest interface{}) erro
if c.cfg.User != "" || c.cfg.Password != "" {
req.SetBasicAuth(c.cfg.User, c.cfg.Password)
}
+
resp, err := c.httpCli.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
+
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
}
+
dec := json.NewDecoder(resp.Body)
return decodeRows(dec, dest)
}
-// QueryRow 执行仅返回一行的查询,将结果解析到 dest(*struct 或 *map)
func (c *Client) QueryRow(ctx context.Context, query string, dest interface{}) error {
if !c.IsConfigured() {
return fmt.Errorf("clickhouse: not configured")
}
+
q := strings.TrimSpace(query)
if !strings.HasSuffix(strings.ToUpper(q), "FORMAT JSONEACHROW") {
query = q + " FORMAT JSONEachRow"
}
+
u := c.buildURL(query)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
@@ -90,32 +93,109 @@ func (c *Client) QueryRow(ctx context.Context, query string, dest interface{}) e
if c.cfg.User != "" || c.cfg.Password != "" {
req.SetBasicAuth(c.cfg.User, c.cfg.Password)
}
+
resp, err := c.httpCli.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
+
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
}
+
dec := json.NewDecoder(resp.Body)
return decodeOneRow(dec, dest)
}
+func (c *Client) Execute(ctx context.Context, query string) error {
+ if !c.IsConfigured() {
+ return fmt.Errorf("clickhouse: not configured")
+ }
+
+ u := c.buildURL(strings.TrimSpace(query))
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil)
+ if err != nil {
+ return err
+ }
+ if c.cfg.User != "" || c.cfg.Password != "" {
+ req.SetBasicAuth(c.cfg.User, c.cfg.Password)
+ }
+
+ resp, err := c.httpCli.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
+ }
+ return nil
+}
+
+func (c *Client) InsertJSONEachRow(ctx context.Context, insertSQL string, rows []map[string]interface{}) error {
+ if len(rows) == 0 {
+ return nil
+ }
+ if !c.IsConfigured() {
+ return fmt.Errorf("clickhouse: not configured")
+ }
+
+ query := strings.TrimSpace(insertSQL)
+ if !strings.HasSuffix(strings.ToUpper(query), "FORMAT JSONEACHROW") {
+ query += " FORMAT JSONEachRow"
+ }
+
+ var payload bytes.Buffer
+ for _, row := range rows {
+ if row == nil {
+ continue
+ }
+ data, err := json.Marshal(row)
+ if err != nil {
+ return err
+ }
+ payload.Write(data)
+ payload.WriteByte('\n')
+ }
+
+ u := c.buildURL(query)
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, &payload)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ if c.cfg.User != "" || c.cfg.Password != "" {
+ req.SetBasicAuth(c.cfg.User, c.cfg.Password)
+ }
+
+ resp, err := c.httpCli.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
+ }
+
+ return nil
+}
+
func (c *Client) buildURL(query string) string {
scheme := "http"
if c.cfg != nil && strings.EqualFold(c.cfg.Scheme, "https") {
scheme = "https"
}
- rawURL := fmt.Sprintf("%s://%s:%d/?query=%s&database=%s",
+ return fmt.Sprintf("%s://%s:%d/?query=%s&database=%s",
scheme, c.cfg.Host, c.cfg.Port, url.QueryEscape(query), url.QueryEscape(c.cfg.Database))
- return rawURL
}
-// decodeRows 将 JSONEachRow 流解析到 slice;元素类型须为 *struct 或 *[]map[string]interface{}
func decodeRows(dec *json.Decoder, dest interface{}) error {
- // dest 应为 *[]*SomeStruct 或 *[]map[string]interface{}
switch d := dest.(type) {
case *[]map[string]interface{}:
*d = (*d)[:0]
@@ -130,7 +210,7 @@ func decodeRows(dec *json.Decoder, dest interface{}) error {
*d = append(*d, row)
}
default:
- return fmt.Errorf("clickhouse: unsupported dest type for Query (use *[]map[string]interface{} or implement decoder)")
+ return fmt.Errorf("clickhouse: unsupported dest type for Query (use *[]map[string]interface{})")
}
}
diff --git a/EdgeAPI/internal/clickhouse/config.go b/EdgeAPI/internal/clickhouse/config.go
index c2fe014..31add08 100644
--- a/EdgeAPI/internal/clickhouse/config.go
+++ b/EdgeAPI/internal/clickhouse/config.go
@@ -12,15 +12,15 @@ import (
)
const (
- envHost = "CLICKHOUSE_HOST"
- envPort = "CLICKHOUSE_PORT"
- envUser = "CLICKHOUSE_USER"
- envPassword = "CLICKHOUSE_PASSWORD"
- envDatabase = "CLICKHOUSE_DATABASE"
- envScheme = "CLICKHOUSE_SCHEME"
- defaultPort = 8443
- defaultDB = "default"
- defaultScheme = "https"
+ envHost = "CLICKHOUSE_HOST"
+ envPort = "CLICKHOUSE_PORT"
+ envUser = "CLICKHOUSE_USER"
+ envPassword = "CLICKHOUSE_PASSWORD"
+ envDatabase = "CLICKHOUSE_DATABASE"
+ envScheme = "CLICKHOUSE_SCHEME"
+ defaultPort = 8443
+ defaultDB = "default"
+ defaultScheme = "https"
)
var (
diff --git a/EdgeAPI/internal/clickhouse/httpdns_access_logs_store.go b/EdgeAPI/internal/clickhouse/httpdns_access_logs_store.go
new file mode 100644
index 0000000..14f76ce
--- /dev/null
+++ b/EdgeAPI/internal/clickhouse/httpdns_access_logs_store.go
@@ -0,0 +1,279 @@
+package clickhouse
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+)
+
+const httpDNSAccessLogsTable = "httpdns_access_logs_ingest"
+
+type HTTPDNSAccessLogRow struct {
+ RequestId string
+ ClusterId int64
+ NodeId int64
+ AppId string
+ AppName string
+ Domain string
+ QType string
+ ClientIP string
+ ClientRegion string
+ Carrier string
+ SDKVersion string
+ OS string
+ ResultIPs string
+ Status string
+ ErrorCode string
+ CostMs int32
+ CreatedAt int64
+ Day string
+ Summary string
+}
+
+type HTTPDNSAccessLogListFilter struct {
+ Day string
+ ClusterId int64
+ NodeId int64
+ AppId string
+ Domain string
+ Status string
+ Keyword string
+ Offset int64
+ Size int64
+}
+
+type HTTPDNSAccessLogsStore struct {
+ client *Client
+}
+
+func NewHTTPDNSAccessLogsStore() *HTTPDNSAccessLogsStore {
+ return &HTTPDNSAccessLogsStore{client: NewClient()}
+}
+
+func (s *HTTPDNSAccessLogsStore) Client() *Client {
+ return s.client
+}
+
+func (s *HTTPDNSAccessLogsStore) Insert(ctx context.Context, logs []*pb.HTTPDNSAccessLog) error {
+ if len(logs) == 0 {
+ return nil
+ }
+ if !s.client.IsConfigured() {
+ return fmt.Errorf("clickhouse: not configured")
+ }
+
+ rows := make([]map[string]interface{}, 0, len(logs))
+ for _, item := range logs {
+ if item == nil {
+ continue
+ }
+ rows = append(rows, map[string]interface{}{
+ "request_id": item.GetRequestId(),
+ "cluster_id": item.GetClusterId(),
+ "node_id": item.GetNodeId(),
+ "app_id": item.GetAppId(),
+ "app_name": item.GetAppName(),
+ "domain": item.GetDomain(),
+ "qtype": item.GetQtype(),
+ "client_ip": item.GetClientIP(),
+ "client_region": item.GetClientRegion(),
+ "carrier": item.GetCarrier(),
+ "sdk_version": item.GetSdkVersion(),
+ "os": item.GetOs(),
+ "result_ips": item.GetResultIPs(),
+ "status": item.GetStatus(),
+ "error_code": item.GetErrorCode(),
+ "cost_ms": item.GetCostMs(),
+ "created_at": item.GetCreatedAt(),
+ "day": item.GetDay(),
+ "summary": item.GetSummary(),
+ })
+ }
+
+ query := fmt.Sprintf("INSERT INTO %s (request_id, cluster_id, node_id, app_id, app_name, domain, qtype, client_ip, client_region, carrier, sdk_version, os, result_ips, status, error_code, cost_ms, created_at, day, summary)",
+ s.tableName())
+ return s.client.InsertJSONEachRow(ctx, query, rows)
+}
+
+func (s *HTTPDNSAccessLogsStore) Count(ctx context.Context, f HTTPDNSAccessLogListFilter) (int64, error) {
+ if !s.client.IsConfigured() {
+ return 0, fmt.Errorf("clickhouse: not configured")
+ }
+
+ conditions := s.buildConditions(f)
+ query := fmt.Sprintf("SELECT count() AS count FROM %s", s.tableName())
+ if len(conditions) > 0 {
+ query += " WHERE " + strings.Join(conditions, " AND ")
+ }
+
+ row := map[string]interface{}{}
+ if err := s.client.QueryRow(ctx, query, &row); err != nil {
+ return 0, err
+ }
+ return toInt64(row["count"]), nil
+}
+
+func (s *HTTPDNSAccessLogsStore) List(ctx context.Context, f HTTPDNSAccessLogListFilter) ([]*HTTPDNSAccessLogRow, error) {
+ if !s.client.IsConfigured() {
+ return nil, fmt.Errorf("clickhouse: not configured")
+ }
+
+ size := f.Size
+ if size <= 0 {
+ size = 20
+ }
+ if size > 1000 {
+ size = 1000
+ }
+ offset := f.Offset
+ if offset < 0 {
+ offset = 0
+ }
+
+ conditions := s.buildConditions(f)
+ query := fmt.Sprintf("SELECT request_id, cluster_id, node_id, app_id, app_name, domain, qtype, client_ip, client_region, carrier, sdk_version, os, result_ips, status, error_code, cost_ms, created_at, day, summary FROM %s",
+ s.tableName())
+ if len(conditions) > 0 {
+ query += " WHERE " + strings.Join(conditions, " AND ")
+ }
+ query += " ORDER BY created_at DESC, request_id DESC"
+ query += fmt.Sprintf(" LIMIT %d OFFSET %d", size, offset)
+
+ rawRows := []map[string]interface{}{}
+ if err := s.client.Query(ctx, query, &rawRows); err != nil {
+ return nil, err
+ }
+
+ result := make([]*HTTPDNSAccessLogRow, 0, len(rawRows))
+ for _, row := range rawRows {
+ result = append(result, &HTTPDNSAccessLogRow{
+ RequestId: toString(row["request_id"]),
+ ClusterId: toInt64(row["cluster_id"]),
+ NodeId: toInt64(row["node_id"]),
+ AppId: toString(row["app_id"]),
+ AppName: toString(row["app_name"]),
+ Domain: toString(row["domain"]),
+ QType: toString(row["qtype"]),
+ ClientIP: toString(row["client_ip"]),
+ ClientRegion: toString(row["client_region"]),
+ Carrier: toString(row["carrier"]),
+ SDKVersion: toString(row["sdk_version"]),
+ OS: toString(row["os"]),
+ ResultIPs: toString(row["result_ips"]),
+ Status: toString(row["status"]),
+ ErrorCode: toString(row["error_code"]),
+ CostMs: int32(toInt64(row["cost_ms"])),
+ CreatedAt: toInt64(row["created_at"]),
+ Day: toString(row["day"]),
+ Summary: toString(row["summary"]),
+ })
+ }
+ return result, nil
+}
+
+func HTTPDNSRowToPB(row *HTTPDNSAccessLogRow) *pb.HTTPDNSAccessLog {
+ if row == nil {
+ return nil
+ }
+ return &pb.HTTPDNSAccessLog{
+ RequestId: row.RequestId,
+ ClusterId: row.ClusterId,
+ NodeId: row.NodeId,
+ AppId: row.AppId,
+ AppName: row.AppName,
+ Domain: row.Domain,
+ Qtype: row.QType,
+ ClientIP: row.ClientIP,
+ ClientRegion: row.ClientRegion,
+ Carrier: row.Carrier,
+ SdkVersion: row.SDKVersion,
+ Os: row.OS,
+ ResultIPs: row.ResultIPs,
+ Status: row.Status,
+ ErrorCode: row.ErrorCode,
+ CostMs: row.CostMs,
+ CreatedAt: row.CreatedAt,
+ Day: row.Day,
+ Summary: row.Summary,
+ }
+}
+
+func (s *HTTPDNSAccessLogsStore) buildConditions(f HTTPDNSAccessLogListFilter) []string {
+ conditions := []string{}
+ if day := strings.TrimSpace(f.Day); day != "" {
+ conditions = append(conditions, "day = '"+escapeString(day)+"'")
+ }
+ if f.ClusterId > 0 {
+ conditions = append(conditions, "cluster_id = "+strconv.FormatInt(f.ClusterId, 10))
+ }
+ if f.NodeId > 0 {
+ conditions = append(conditions, "node_id = "+strconv.FormatInt(f.NodeId, 10))
+ }
+ if appID := strings.TrimSpace(f.AppId); appID != "" {
+ conditions = append(conditions, "app_id = '"+escapeString(appID)+"'")
+ }
+ if domain := strings.TrimSpace(f.Domain); domain != "" {
+ conditions = append(conditions, "domain = '"+escapeString(domain)+"'")
+ }
+ if status := strings.TrimSpace(f.Status); status != "" {
+ conditions = append(conditions, "status = '"+escapeString(status)+"'")
+ }
+ if keyword := strings.TrimSpace(f.Keyword); keyword != "" {
+ kw := escapeString(keyword)
+ conditions = append(conditions, "(summary LIKE '%"+kw+"%' OR app_name LIKE '%"+kw+"%' OR client_ip LIKE '%"+kw+"%' OR result_ips LIKE '%"+kw+"%')")
+ }
+ return conditions
+}
+
+func (s *HTTPDNSAccessLogsStore) tableName() string {
+ if s.client != nil && s.client.cfg != nil && s.client.cfg.Database != "" && s.client.cfg.Database != "default" {
+ return quoteIdent(s.client.cfg.Database) + "." + quoteIdent(httpDNSAccessLogsTable)
+ }
+ return quoteIdent(httpDNSAccessLogsTable)
+}
+
+func toString(value interface{}) string {
+ if value == nil {
+ return ""
+ }
+ switch v := value.(type) {
+ case string:
+ return v
+ case json.Number:
+ return v.String()
+ default:
+ return fmt.Sprintf("%v", v)
+ }
+}
+
+func toInt64(value interface{}) int64 {
+ if value == nil {
+ return 0
+ }
+ switch v := value.(type) {
+ case int:
+ return int64(v)
+ case int32:
+ return int64(v)
+ case int64:
+ return v
+ case uint32:
+ return int64(v)
+ case uint64:
+ return int64(v)
+ case float64:
+ return int64(v)
+ case json.Number:
+ n, _ := v.Int64()
+ return n
+ case string:
+ n, _ := strconv.ParseInt(v, 10, 64)
+ return n
+ default:
+ return 0
+ }
+}
diff --git a/EdgeAPI/internal/clickhouse/logs_ingest_store.go b/EdgeAPI/internal/clickhouse/logs_ingest_store.go
index 4cbc965..84f7a56 100644
--- a/EdgeAPI/internal/clickhouse/logs_ingest_store.go
+++ b/EdgeAPI/internal/clickhouse/logs_ingest_store.go
@@ -351,8 +351,8 @@ func RowToPB(r *LogsIngestRow) *pb.HTTPAccessLog {
RemoteAddr: r.IP,
RequestMethod: r.Method,
RequestPath: r.Path,
- RequestURI: r.Path, // 前端使用 requestURI 显示完整路径
- Scheme: "http", // 默认 http,日志中未存储实际值
+ RequestURI: r.Path, // 前端使用 requestURI 显示完整路径
+ Scheme: "http", // 默认 http,日志中未存储实际值
Proto: "HTTP/1.1", // 默认值,日志中未存储实际值
Status: int32(r.Status),
RequestLength: int64(r.BytesIn),
diff --git a/EdgeAPI/internal/const/const.go b/EdgeAPI/internal/const/const.go
index 89f1e42..14d8d93 100644
--- a/EdgeAPI/internal/const/const.go
+++ b/EdgeAPI/internal/const/const.go
@@ -1,7 +1,7 @@
package teaconst
const (
- Version = "1.4.7" //1.3.9
+ Version = "1.4.8" //1.3.9
ProductName = "Edge API"
ProcessName = "edge-api"
@@ -17,6 +17,6 @@ const (
// 其他节点版本号,用来检测是否有需要升级的节点
- NodeVersion = "1.4.7" //1.3.8.2
+ NodeVersion = "1.4.8" //1.3.8.2
)
diff --git a/EdgeAPI/internal/const/const_plus.go b/EdgeAPI/internal/const/const_plus.go
index 44ea4f5..56e5324 100644
--- a/EdgeAPI/internal/const/const_plus.go
+++ b/EdgeAPI/internal/const/const_plus.go
@@ -4,8 +4,8 @@
package teaconst
const (
- DNSNodeVersion = "1.4.7" //1.3.8.2
- UserNodeVersion = "1.4.7" //1.3.8.2
+ DNSNodeVersion = "1.4.8" //1.3.8.2
+ UserNodeVersion = "1.4.8" //1.3.8.2
ReportNodeVersion = "0.1.5"
DefaultMaxNodes int32 = 50
diff --git a/EdgeAPI/internal/db/models/httpdns_access_log_dao.go b/EdgeAPI/internal/db/models/httpdns_access_log_dao.go
new file mode 100644
index 0000000..9bcbf17
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_access_log_dao.go
@@ -0,0 +1,101 @@
+package models
+
+import (
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/iwind/TeaGo/Tea"
+ "github.com/iwind/TeaGo/dbs"
+)
+
+type HTTPDNSAccessLogDAO dbs.DAO
+
+func NewHTTPDNSAccessLogDAO() *HTTPDNSAccessLogDAO {
+ return dbs.NewDAO(&HTTPDNSAccessLogDAO{
+ DAOObject: dbs.DAOObject{
+ DB: Tea.Env,
+ Table: "edgeHTTPDNSAccessLogs",
+ Model: new(HTTPDNSAccessLog),
+ PkName: "id",
+ },
+ }).(*HTTPDNSAccessLogDAO)
+}
+
+var SharedHTTPDNSAccessLogDAO *HTTPDNSAccessLogDAO
+
+func init() {
+ dbs.OnReady(func() {
+ SharedHTTPDNSAccessLogDAO = NewHTTPDNSAccessLogDAO()
+ })
+}
+
+func (this *HTTPDNSAccessLogDAO) CreateLog(tx *dbs.Tx, log *HTTPDNSAccessLog) error {
+ var op = NewHTTPDNSAccessLogOperator()
+ op.RequestId = log.RequestId
+ op.ClusterId = log.ClusterId
+ op.NodeId = log.NodeId
+ op.AppId = log.AppId
+ op.AppName = log.AppName
+ op.Domain = log.Domain
+ op.QType = log.QType
+ op.ClientIP = log.ClientIP
+ op.ClientRegion = log.ClientRegion
+ op.Carrier = log.Carrier
+ op.SDKVersion = log.SDKVersion
+ op.OS = log.OS
+ op.ResultIPs = log.ResultIPs
+ op.Status = log.Status
+ op.ErrorCode = log.ErrorCode
+ op.CostMs = log.CostMs
+ op.CreatedAt = log.CreatedAt
+ op.Day = log.Day
+ op.Summary = log.Summary
+ return this.Save(tx, op)
+}
+
+func (this *HTTPDNSAccessLogDAO) BuildListQuery(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, domain string, status string, keyword string) *dbs.Query {
+ query := this.Query(tx).DescPk()
+ if len(day) > 0 {
+ query = query.Attr("day", day)
+ }
+ if clusterId > 0 {
+ query = query.Attr("clusterId", clusterId)
+ }
+ if nodeId > 0 {
+ query = query.Attr("nodeId", nodeId)
+ }
+ if len(appId) > 0 {
+ query = query.Attr("appId", appId)
+ }
+ if len(domain) > 0 {
+ query = query.Attr("domain", domain)
+ }
+ if len(status) > 0 {
+ query = query.Attr("status", status)
+ }
+ if len(keyword) > 0 {
+ query = query.Where("(summary LIKE :kw OR appName LIKE :kw OR clientIP LIKE :kw OR resultIPs LIKE :kw)").Param("kw", "%"+keyword+"%")
+ }
+ return query
+}
+
+func (this *HTTPDNSAccessLogDAO) CountLogs(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, domain string, status string, keyword string) (int64, error) {
+ return this.BuildListQuery(tx, day, clusterId, nodeId, appId, domain, status, keyword).Count()
+}
+
+func (this *HTTPDNSAccessLogDAO) ListLogs(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, domain string, status string, keyword string, offset int64, size int64) (result []*HTTPDNSAccessLog, err error) {
+ _, err = this.BuildListQuery(tx, day, clusterId, nodeId, appId, domain, status, keyword).
+ Offset(offset).
+ Limit(size).
+ Slice(&result).
+ FindAll()
+ return
+}
+
+func (this *HTTPDNSAccessLogDAO) DeleteLogsWithAppId(tx *dbs.Tx, appId string) error {
+ if len(appId) == 0 {
+ return nil
+ }
+ _, err := this.Query(tx).
+ Attr("appId", appId).
+ Delete()
+ return err
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_access_log_model.go b/EdgeAPI/internal/db/models/httpdns_access_log_model.go
new file mode 100644
index 0000000..0c8da61
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_access_log_model.go
@@ -0,0 +1,52 @@
+package models
+
+// HTTPDNSAccessLog 访问日志
+type HTTPDNSAccessLog struct {
+ Id uint64 `field:"id"` // ID
+ RequestId string `field:"requestId"` // 请求ID
+ ClusterId uint32 `field:"clusterId"` // 集群ID
+ NodeId uint32 `field:"nodeId"` // 节点ID
+ AppId string `field:"appId"` // AppID
+ AppName string `field:"appName"` // 应用名
+ Domain string `field:"domain"` // 域名
+ QType string `field:"qtype"` // 查询类型
+ ClientIP string `field:"clientIP"` // 客户端IP
+ ClientRegion string `field:"clientRegion"` // 客户端区域
+ Carrier string `field:"carrier"` // 运营商
+ SDKVersion string `field:"sdkVersion"` // SDK版本
+ OS string `field:"os"` // 系统
+ ResultIPs string `field:"resultIPs"` // 结果IP
+ Status string `field:"status"` // 状态
+ ErrorCode string `field:"errorCode"` // 错误码
+ CostMs int32 `field:"costMs"` // 耗时
+ CreatedAt uint64 `field:"createdAt"` // 创建时间
+ Day string `field:"day"` // YYYYMMDD
+ Summary string `field:"summary"` // 概要
+}
+
+type HTTPDNSAccessLogOperator struct {
+ Id any // ID
+ RequestId any // 请求ID
+ ClusterId any // 集群ID
+ NodeId any // 节点ID
+ AppId any // AppID
+ AppName any // 应用名
+ Domain any // 域名
+ QType any // 查询类型
+ ClientIP any // 客户端IP
+ ClientRegion any // 客户端区域
+ Carrier any // 运营商
+ SDKVersion any // SDK版本
+ OS any // 系统
+ ResultIPs any // 结果IP
+ Status any // 状态
+ ErrorCode any // 错误码
+ CostMs any // 耗时
+ CreatedAt any // 创建时间
+ Day any // YYYYMMDD
+ Summary any // 概要
+}
+
+func NewHTTPDNSAccessLogOperator() *HTTPDNSAccessLogOperator {
+ return &HTTPDNSAccessLogOperator{}
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_app_dao.go b/EdgeAPI/internal/db/models/httpdns_app_dao.go
new file mode 100644
index 0000000..0af29b7
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_app_dao.go
@@ -0,0 +1,128 @@
+package models
+
+import (
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/iwind/TeaGo/Tea"
+ "github.com/iwind/TeaGo/dbs"
+ "github.com/iwind/TeaGo/types"
+ "time"
+)
+
+const (
+ HTTPDNSAppStateEnabled = 1
+ HTTPDNSAppStateDisabled = 0
+ HTTPDNSSNIModeFixedHide = "fixed_hide"
+)
+
+type HTTPDNSAppDAO dbs.DAO
+
+func NewHTTPDNSAppDAO() *HTTPDNSAppDAO {
+ return dbs.NewDAO(&HTTPDNSAppDAO{
+ DAOObject: dbs.DAOObject{
+ DB: Tea.Env,
+ Table: "edgeHTTPDNSApps",
+ Model: new(HTTPDNSApp),
+ PkName: "id",
+ },
+ }).(*HTTPDNSAppDAO)
+}
+
+var SharedHTTPDNSAppDAO *HTTPDNSAppDAO
+
+func init() {
+ dbs.OnReady(func() {
+ SharedHTTPDNSAppDAO = NewHTTPDNSAppDAO()
+ })
+}
+
+func (this *HTTPDNSAppDAO) CreateApp(tx *dbs.Tx, name string, appId string, primaryClusterId int64, backupClusterId int64, isOn bool, userId int64) (int64, error) {
+ var op = NewHTTPDNSAppOperator()
+ op.Name = name
+ op.AppId = appId
+ op.PrimaryClusterId = primaryClusterId
+ op.BackupClusterId = backupClusterId
+ op.IsOn = isOn
+ op.UserId = userId
+ op.SNIMode = HTTPDNSSNIModeFixedHide
+ op.CreatedAt = time.Now().Unix()
+ op.UpdatedAt = time.Now().Unix()
+ op.State = HTTPDNSAppStateEnabled
+ err := this.Save(tx, op)
+ if err != nil {
+ return 0, err
+ }
+ return types.Int64(op.Id), nil
+}
+
+func (this *HTTPDNSAppDAO) UpdateApp(tx *dbs.Tx, appDbId int64, name string, primaryClusterId int64, backupClusterId int64, isOn bool, userId int64) error {
+ var op = NewHTTPDNSAppOperator()
+ op.Id = appDbId
+ op.Name = name
+ op.PrimaryClusterId = primaryClusterId
+ op.BackupClusterId = backupClusterId
+ op.IsOn = isOn
+ op.UserId = userId
+ op.UpdatedAt = time.Now().Unix()
+ return this.Save(tx, op)
+}
+
+func (this *HTTPDNSAppDAO) DisableApp(tx *dbs.Tx, appDbId int64) error {
+ _, err := this.Query(tx).
+ Pk(appDbId).
+ Set("state", HTTPDNSAppStateDisabled).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSAppDAO) FindEnabledApp(tx *dbs.Tx, appDbId int64) (*HTTPDNSApp, error) {
+ one, err := this.Query(tx).
+ Pk(appDbId).
+ State(HTTPDNSAppStateEnabled).
+ Find()
+ if one == nil {
+ return nil, err
+ }
+ return one.(*HTTPDNSApp), nil
+}
+
+func (this *HTTPDNSAppDAO) FindEnabledAppWithAppId(tx *dbs.Tx, appId string) (*HTTPDNSApp, error) {
+ one, err := this.Query(tx).
+ State(HTTPDNSAppStateEnabled).
+ Attr("appId", appId).
+ Find()
+ if one == nil {
+ return nil, err
+ }
+ return one.(*HTTPDNSApp), nil
+}
+
+func (this *HTTPDNSAppDAO) ListEnabledApps(tx *dbs.Tx, offset int64, size int64, keyword string) (result []*HTTPDNSApp, err error) {
+ query := this.Query(tx).
+ State(HTTPDNSAppStateEnabled).
+ AscPk()
+ if len(keyword) > 0 {
+ query = query.Where("(name LIKE :kw OR appId LIKE :kw)").Param("kw", "%"+keyword+"%")
+ }
+ if size > 0 {
+ query = query.Offset(offset).Limit(size)
+ }
+ _, err = query.Slice(&result).FindAll()
+ return
+}
+
+func (this *HTTPDNSAppDAO) CountEnabledApps(tx *dbs.Tx, keyword string) (int64, error) {
+ query := this.Query(tx).State(HTTPDNSAppStateEnabled)
+ if len(keyword) > 0 {
+ query = query.Where("(name LIKE :kw OR appId LIKE :kw)").Param("kw", "%"+keyword+"%")
+ }
+ return query.Count()
+}
+
+func (this *HTTPDNSAppDAO) FindAllEnabledApps(tx *dbs.Tx) (result []*HTTPDNSApp, err error) {
+ _, err = this.Query(tx).
+ State(HTTPDNSAppStateEnabled).
+ AscPk().
+ Slice(&result).
+ FindAll()
+ return
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_app_model.go b/EdgeAPI/internal/db/models/httpdns_app_model.go
new file mode 100644
index 0000000..113fe9f
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_app_model.go
@@ -0,0 +1,36 @@
+package models
+
+// HTTPDNSApp maps to edgeHTTPDNSApps.
+type HTTPDNSApp struct {
+ Id uint32 `field:"id"` // id
+ Name string `field:"name"` // app name
+ AppId string `field:"appId"` // external app id
+ IsOn bool `field:"isOn"` // enabled
+ PrimaryClusterId uint32 `field:"primaryClusterId"` // primary cluster id
+ BackupClusterId uint32 `field:"backupClusterId"` // backup cluster id
+ SNIMode string `field:"sniMode"` // sni mode
+ UserId int64 `field:"userId"` // owner user id
+ CreatedAt uint64 `field:"createdAt"` // created unix ts
+ UpdatedAt uint64 `field:"updatedAt"` // updated unix ts
+ State uint8 `field:"state"` // state
+}
+
+// HTTPDNSAppOperator is used by DAO save/update.
+type HTTPDNSAppOperator struct {
+ Id any // id
+ Name any // app name
+ AppId any // external app id
+ IsOn any // enabled
+ PrimaryClusterId any // primary cluster id
+ BackupClusterId any // backup cluster id
+ SNIMode any // sni mode
+ UserId any // owner user id
+ CreatedAt any // created unix ts
+ UpdatedAt any // updated unix ts
+ State any // state
+}
+
+func NewHTTPDNSAppOperator() *HTTPDNSAppOperator {
+ return &HTTPDNSAppOperator{}
+}
+
diff --git a/EdgeAPI/internal/db/models/httpdns_app_secret_dao.go b/EdgeAPI/internal/db/models/httpdns_app_secret_dao.go
new file mode 100644
index 0000000..b2d1934
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_app_secret_dao.go
@@ -0,0 +1,125 @@
+package models
+
+import (
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/iwind/TeaGo/Tea"
+ "github.com/iwind/TeaGo/dbs"
+ "github.com/iwind/TeaGo/rands"
+ "github.com/iwind/TeaGo/types"
+ "time"
+)
+
+const (
+ HTTPDNSAppSecretStateEnabled = 1
+ HTTPDNSAppSecretStateDisabled = 0
+)
+
+type HTTPDNSAppSecretDAO dbs.DAO
+
+func NewHTTPDNSAppSecretDAO() *HTTPDNSAppSecretDAO {
+ return dbs.NewDAO(&HTTPDNSAppSecretDAO{
+ DAOObject: dbs.DAOObject{
+ DB: Tea.Env,
+ Table: "edgeHTTPDNSAppSecrets",
+ Model: new(HTTPDNSAppSecret),
+ PkName: "id",
+ },
+ }).(*HTTPDNSAppSecretDAO)
+}
+
+var SharedHTTPDNSAppSecretDAO *HTTPDNSAppSecretDAO
+
+func init() {
+ dbs.OnReady(func() {
+ SharedHTTPDNSAppSecretDAO = NewHTTPDNSAppSecretDAO()
+ })
+}
+
+func (this *HTTPDNSAppSecretDAO) InitAppSecret(tx *dbs.Tx, appDbId int64, signEnabled bool) (string, uint64, error) {
+ signSecret := "ss_" + rands.HexString(12)
+ now := uint64(time.Now().Unix())
+ var op = NewHTTPDNSAppSecretOperator()
+ op.AppId = appDbId
+ op.SignEnabled = signEnabled
+ op.SignSecret = signSecret
+ op.SignUpdatedAt = now
+ op.UpdatedAt = now
+ op.State = HTTPDNSAppSecretStateEnabled
+ err := this.Save(tx, op)
+ return signSecret, now, err
+}
+
+func (this *HTTPDNSAppSecretDAO) FindEnabledAppSecret(tx *dbs.Tx, appDbId int64) (*HTTPDNSAppSecret, error) {
+ one, err := this.Query(tx).
+ Attr("appId", appDbId).
+ State(HTTPDNSAppSecretStateEnabled).
+ Find()
+ if one == nil {
+ return nil, err
+ }
+ return one.(*HTTPDNSAppSecret), nil
+}
+
+func (this *HTTPDNSAppSecretDAO) UpdateSignEnabled(tx *dbs.Tx, appDbId int64, signEnabled bool) error {
+ _, err := this.Query(tx).
+ Attr("appId", appDbId).
+ State(HTTPDNSAppSecretStateEnabled).
+ Set("signEnabled", signEnabled).
+ Set("updatedAt", time.Now().Unix()).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSAppSecretDAO) ResetSignSecret(tx *dbs.Tx, appDbId int64) (string, int64, error) {
+ signSecret := "ss_" + rands.HexString(12)
+ now := time.Now().Unix()
+ _, err := this.Query(tx).
+ Attr("appId", appDbId).
+ State(HTTPDNSAppSecretStateEnabled).
+ Set("signSecret", signSecret).
+ Set("signUpdatedAt", now).
+ Set("updatedAt", now).
+ Update()
+ if err != nil {
+ return "", 0, err
+ }
+ return signSecret, now, nil
+}
+
+func (this *HTTPDNSAppSecretDAO) FindSignEnabled(tx *dbs.Tx, appDbId int64) (bool, error) {
+ one, err := this.FindEnabledAppSecret(tx, appDbId)
+ if err != nil || one == nil {
+ return false, err
+ }
+ return one.SignEnabled, nil
+}
+
+func (this *HTTPDNSAppSecretDAO) FindSignSecretWithAppDbId(tx *dbs.Tx, appDbId int64) (string, error) {
+ return this.Query(tx).
+ Attr("appId", appDbId).
+ State(HTTPDNSAppSecretStateEnabled).
+ Result("signSecret").
+ FindStringCol("")
+}
+
+func (this *HTTPDNSAppSecretDAO) FindSignUpdatedAt(tx *dbs.Tx, appDbId int64) (int64, error) {
+ col, err := this.Query(tx).
+ Attr("appId", appDbId).
+ State(HTTPDNSAppSecretStateEnabled).
+ Result("signUpdatedAt").
+ FindCol(nil)
+ if err != nil {
+ return 0, err
+ }
+ return types.Int64(col), nil
+}
+
+func (this *HTTPDNSAppSecretDAO) DisableAppSecret(tx *dbs.Tx, appDbId int64) error {
+ _, err := this.Query(tx).
+ Attr("appId", appDbId).
+ State(HTTPDNSAppSecretStateEnabled).
+ Set("state", HTTPDNSAppSecretStateDisabled).
+ Set("updatedAt", time.Now().Unix()).
+ Update()
+ return err
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_app_secret_model.go b/EdgeAPI/internal/db/models/httpdns_app_secret_model.go
new file mode 100644
index 0000000..92563a7
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_app_secret_model.go
@@ -0,0 +1,26 @@
+package models
+
+// HTTPDNSAppSecret 应用验签密钥配置
+type HTTPDNSAppSecret struct {
+ Id uint32 `field:"id"` // ID
+ AppId uint32 `field:"appId"` // 应用DB ID
+ SignEnabled bool `field:"signEnabled"` // 是否启用验签
+ SignSecret string `field:"signSecret"` // 验签密钥(当前先明文存储)
+ SignUpdatedAt uint64 `field:"signUpdatedAt"` // 验签密钥更新时间
+ UpdatedAt uint64 `field:"updatedAt"` // 修改时间
+ State uint8 `field:"state"` // 记录状态
+}
+
+type HTTPDNSAppSecretOperator struct {
+ Id any // ID
+ AppId any // 应用DB ID
+ SignEnabled any // 是否启用验签
+ SignSecret any // 验签密钥
+ SignUpdatedAt any // 验签密钥更新时间
+ UpdatedAt any // 修改时间
+ State any // 记录状态
+}
+
+func NewHTTPDNSAppSecretOperator() *HTTPDNSAppSecretOperator {
+ return &HTTPDNSAppSecretOperator{}
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_cluster_dao.go b/EdgeAPI/internal/db/models/httpdns_cluster_dao.go
new file mode 100644
index 0000000..872b5d0
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_cluster_dao.go
@@ -0,0 +1,169 @@
+package models
+
+import (
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/iwind/TeaGo/Tea"
+ "github.com/iwind/TeaGo/dbs"
+ "github.com/iwind/TeaGo/types"
+ "time"
+)
+
+const (
+ HTTPDNSClusterStateEnabled = 1
+ HTTPDNSClusterStateDisabled = 0
+)
+
+type HTTPDNSClusterDAO dbs.DAO
+
+func NewHTTPDNSClusterDAO() *HTTPDNSClusterDAO {
+ return dbs.NewDAO(&HTTPDNSClusterDAO{
+ DAOObject: dbs.DAOObject{
+ DB: Tea.Env,
+ Table: "edgeHTTPDNSClusters",
+ Model: new(HTTPDNSCluster),
+ PkName: "id",
+ },
+ }).(*HTTPDNSClusterDAO)
+}
+
+var SharedHTTPDNSClusterDAO *HTTPDNSClusterDAO
+
+func init() {
+ dbs.OnReady(func() {
+ SharedHTTPDNSClusterDAO = NewHTTPDNSClusterDAO()
+ })
+}
+
+func (this *HTTPDNSClusterDAO) CreateCluster(tx *dbs.Tx, name string, serviceDomain string, defaultTTL int32, fallbackTimeoutMs int32, installDir string, tlsPolicyJSON []byte, isOn bool, isDefault bool) (int64, error) {
+ if isDefault {
+ err := this.Query(tx).
+ State(HTTPDNSClusterStateEnabled).
+ Set("isDefault", false).
+ UpdateQuickly()
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ var op = NewHTTPDNSClusterOperator()
+ op.Name = name
+ op.ServiceDomain = serviceDomain
+ op.DefaultTTL = defaultTTL
+ op.FallbackTimeoutMs = fallbackTimeoutMs
+ op.InstallDir = installDir
+ op.IsOn = isOn
+ op.IsDefault = isDefault
+ op.CreatedAt = time.Now().Unix()
+ op.UpdatedAt = time.Now().Unix()
+ op.State = HTTPDNSClusterStateEnabled
+ if len(tlsPolicyJSON) > 0 {
+ op.TLSPolicy = tlsPolicyJSON
+ }
+ err := this.Save(tx, op)
+ if err != nil {
+ return 0, err
+ }
+ 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) error {
+ if isDefault {
+ err := this.Query(tx).
+ State(HTTPDNSClusterStateEnabled).
+ Neq("id", clusterId).
+ Set("isDefault", false).
+ UpdateQuickly()
+ if err != nil {
+ return err
+ }
+ }
+
+ var op = NewHTTPDNSClusterOperator()
+ op.Id = clusterId
+ op.Name = name
+ op.ServiceDomain = serviceDomain
+ op.DefaultTTL = defaultTTL
+ op.FallbackTimeoutMs = fallbackTimeoutMs
+ op.InstallDir = installDir
+ op.IsOn = isOn
+ op.IsDefault = isDefault
+ op.UpdatedAt = time.Now().Unix()
+ if len(tlsPolicyJSON) > 0 {
+ op.TLSPolicy = tlsPolicyJSON
+ }
+ return this.Save(tx, op)
+}
+
+func (this *HTTPDNSClusterDAO) DisableCluster(tx *dbs.Tx, clusterId int64) error {
+ _, err := this.Query(tx).
+ Pk(clusterId).
+ Set("state", HTTPDNSClusterStateDisabled).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSClusterDAO) FindEnabledCluster(tx *dbs.Tx, clusterId int64) (*HTTPDNSCluster, error) {
+ one, err := this.Query(tx).
+ Pk(clusterId).
+ State(HTTPDNSClusterStateEnabled).
+ Find()
+ if one == nil {
+ return nil, err
+ }
+ return one.(*HTTPDNSCluster), nil
+}
+
+func (this *HTTPDNSClusterDAO) FindEnabledClusterName(tx *dbs.Tx, clusterId int64) (string, error) {
+ return this.Query(tx).
+ Pk(clusterId).
+ State(HTTPDNSClusterStateEnabled).
+ Result("name").
+ FindStringCol("")
+}
+
+func (this *HTTPDNSClusterDAO) ListEnabledClusters(tx *dbs.Tx, offset int64, size int64, keyword string) (result []*HTTPDNSCluster, err error) {
+ query := this.Query(tx).
+ State(HTTPDNSClusterStateEnabled).
+ AscPk()
+ if len(keyword) > 0 {
+ query = query.Where("(name LIKE :kw OR serviceDomain LIKE :kw)").Param("kw", "%"+keyword+"%")
+ }
+ if size > 0 {
+ query = query.Offset(offset).Limit(size)
+ }
+ _, err = query.Slice(&result).FindAll()
+ return
+}
+
+func (this *HTTPDNSClusterDAO) CountEnabledClusters(tx *dbs.Tx, keyword string) (int64, error) {
+ query := this.Query(tx).State(HTTPDNSClusterStateEnabled)
+ if len(keyword) > 0 {
+ query = query.Where("(name LIKE :kw OR serviceDomain LIKE :kw)").Param("kw", "%"+keyword+"%")
+ }
+ return query.Count()
+}
+
+func (this *HTTPDNSClusterDAO) FindAllEnabledClusters(tx *dbs.Tx) (result []*HTTPDNSCluster, err error) {
+ _, err = this.Query(tx).
+ State(HTTPDNSClusterStateEnabled).
+ AscPk().
+ Slice(&result).
+ FindAll()
+ return
+}
+
+func (this *HTTPDNSClusterDAO) UpdateDefaultCluster(tx *dbs.Tx, clusterId int64) error {
+ err := this.Query(tx).
+ State(HTTPDNSClusterStateEnabled).
+ Set("isDefault", false).
+ UpdateQuickly()
+ if err != nil {
+ return err
+ }
+ _, err = this.Query(tx).
+ Pk(clusterId).
+ State(HTTPDNSClusterStateEnabled).
+ Set("isDefault", true).
+ Update()
+ return err
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_cluster_model.go b/EdgeAPI/internal/db/models/httpdns_cluster_model.go
new file mode 100644
index 0000000..00aeaf1
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_cluster_model.go
@@ -0,0 +1,38 @@
+package models
+
+import "github.com/iwind/TeaGo/dbs"
+
+// HTTPDNSCluster HTTPDNS集群
+type HTTPDNSCluster struct {
+ Id uint32 `field:"id"` // ID
+ Name string `field:"name"` // 集群名称
+ IsOn bool `field:"isOn"` // 是否启用
+ IsDefault bool `field:"isDefault"` // 默认集群
+ ServiceDomain string `field:"serviceDomain"` // 服务域名
+ DefaultTTL int32 `field:"defaultTTL"` // 默认TTL
+ FallbackTimeoutMs int32 `field:"fallbackTimeoutMs"` // 降级超时
+ InstallDir string `field:"installDir"` // 安装目录
+ TLSPolicy dbs.JSON `field:"tlsPolicy"` // TLS策略
+ CreatedAt uint64 `field:"createdAt"` // 创建时间
+ UpdatedAt uint64 `field:"updatedAt"` // 修改时间
+ State uint8 `field:"state"` // 记录状态
+}
+
+type HTTPDNSClusterOperator struct {
+ Id any // ID
+ Name any // 集群名称
+ IsOn any // 是否启用
+ IsDefault any // 默认集群
+ ServiceDomain any // 服务域名
+ DefaultTTL any // 默认TTL
+ FallbackTimeoutMs any // 降级超时
+ InstallDir any // 安装目录
+ TLSPolicy any // TLS策略
+ CreatedAt any // 创建时间
+ UpdatedAt any // 修改时间
+ State any // 记录状态
+}
+
+func NewHTTPDNSClusterOperator() *HTTPDNSClusterOperator {
+ return &HTTPDNSClusterOperator{}
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_custom_rule_dao.go b/EdgeAPI/internal/db/models/httpdns_custom_rule_dao.go
new file mode 100644
index 0000000..0df5629
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_custom_rule_dao.go
@@ -0,0 +1,143 @@
+package models
+
+import (
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/iwind/TeaGo/Tea"
+ "github.com/iwind/TeaGo/dbs"
+ "github.com/iwind/TeaGo/types"
+ "time"
+)
+
+const (
+ HTTPDNSCustomRuleStateEnabled = 1
+ HTTPDNSCustomRuleStateDisabled = 0
+)
+
+type HTTPDNSCustomRuleDAO dbs.DAO
+
+func NewHTTPDNSCustomRuleDAO() *HTTPDNSCustomRuleDAO {
+ return dbs.NewDAO(&HTTPDNSCustomRuleDAO{
+ DAOObject: dbs.DAOObject{
+ DB: Tea.Env,
+ Table: "edgeHTTPDNSCustomRules",
+ Model: new(HTTPDNSCustomRule),
+ PkName: "id",
+ },
+ }).(*HTTPDNSCustomRuleDAO)
+}
+
+var SharedHTTPDNSCustomRuleDAO *HTTPDNSCustomRuleDAO
+
+func init() {
+ dbs.OnReady(func() {
+ SharedHTTPDNSCustomRuleDAO = NewHTTPDNSCustomRuleDAO()
+ })
+}
+
+func (this *HTTPDNSCustomRuleDAO) CreateRule(tx *dbs.Tx, rule *HTTPDNSCustomRule) (int64, error) {
+ var op = NewHTTPDNSCustomRuleOperator()
+ op.AppId = rule.AppId
+ op.DomainId = rule.DomainId
+ op.RuleName = rule.RuleName
+ op.LineScope = rule.LineScope
+ op.LineCarrier = rule.LineCarrier
+ op.LineRegion = rule.LineRegion
+ op.LineProvince = rule.LineProvince
+ op.LineContinent = rule.LineContinent
+ op.LineCountry = rule.LineCountry
+ op.TTL = rule.TTL
+ op.IsOn = rule.IsOn
+ op.Priority = rule.Priority
+ op.UpdatedAt = time.Now().Unix()
+ op.State = HTTPDNSCustomRuleStateEnabled
+ err := this.Save(tx, op)
+ if err != nil {
+ return 0, err
+ }
+ return types.Int64(op.Id), nil
+}
+
+func (this *HTTPDNSCustomRuleDAO) UpdateRule(tx *dbs.Tx, rule *HTTPDNSCustomRule) error {
+ var op = NewHTTPDNSCustomRuleOperator()
+ op.Id = rule.Id
+ op.RuleName = rule.RuleName
+ op.LineScope = rule.LineScope
+ op.LineCarrier = rule.LineCarrier
+ op.LineRegion = rule.LineRegion
+ op.LineProvince = rule.LineProvince
+ op.LineContinent = rule.LineContinent
+ op.LineCountry = rule.LineCountry
+ op.TTL = rule.TTL
+ op.IsOn = rule.IsOn
+ op.Priority = rule.Priority
+ op.UpdatedAt = time.Now().Unix()
+ return this.Save(tx, op)
+}
+
+func (this *HTTPDNSCustomRuleDAO) DisableRule(tx *dbs.Tx, ruleId int64) error {
+ _, err := this.Query(tx).
+ Pk(ruleId).
+ Set("state", HTTPDNSCustomRuleStateDisabled).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSCustomRuleDAO) DisableRulesWithAppId(tx *dbs.Tx, appDbId int64) error {
+ _, err := this.Query(tx).
+ Attr("appId", appDbId).
+ State(HTTPDNSCustomRuleStateEnabled).
+ Set("state", HTTPDNSCustomRuleStateDisabled).
+ Set("updatedAt", time.Now().Unix()).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSCustomRuleDAO) UpdateRuleStatus(tx *dbs.Tx, ruleId int64, isOn bool) error {
+ _, err := this.Query(tx).
+ Pk(ruleId).
+ State(HTTPDNSCustomRuleStateEnabled).
+ Set("isOn", isOn).
+ Set("updatedAt", time.Now().Unix()).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSCustomRuleDAO) FindEnabledRule(tx *dbs.Tx, ruleId int64) (*HTTPDNSCustomRule, error) {
+ one, err := this.Query(tx).
+ Pk(ruleId).
+ State(HTTPDNSCustomRuleStateEnabled).
+ Find()
+ if one == nil {
+ return nil, err
+ }
+ return one.(*HTTPDNSCustomRule), nil
+}
+
+func (this *HTTPDNSCustomRuleDAO) ListEnabledRulesWithDomainId(tx *dbs.Tx, domainId int64) (result []*HTTPDNSCustomRule, err error) {
+ _, err = this.Query(tx).
+ State(HTTPDNSCustomRuleStateEnabled).
+ Attr("domainId", domainId).
+ Asc("priority").
+ AscPk().
+ Slice(&result).
+ FindAll()
+ return
+}
+
+func (this *HTTPDNSCustomRuleDAO) ListEnabledRulesWithAppId(tx *dbs.Tx, appDbId int64) (result []*HTTPDNSCustomRule, err error) {
+ _, err = this.Query(tx).
+ State(HTTPDNSCustomRuleStateEnabled).
+ Attr("appId", appDbId).
+ Asc("priority").
+ AscPk().
+ Slice(&result).
+ FindAll()
+ return
+}
+
+func (this *HTTPDNSCustomRuleDAO) CountEnabledRulesWithDomainId(tx *dbs.Tx, domainId int64) (int64, error) {
+ return this.Query(tx).
+ State(HTTPDNSCustomRuleStateEnabled).
+ Attr("domainId", domainId).
+ Count()
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_custom_rule_model.go b/EdgeAPI/internal/db/models/httpdns_custom_rule_model.go
new file mode 100644
index 0000000..8120152
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_custom_rule_model.go
@@ -0,0 +1,42 @@
+package models
+
+// HTTPDNSCustomRule 自定义解析规则
+type HTTPDNSCustomRule struct {
+ Id uint32 `field:"id"` // ID
+ AppId uint32 `field:"appId"` // 应用DB ID
+ DomainId uint32 `field:"domainId"` // 域名ID
+ RuleName string `field:"ruleName"` // 规则名称
+ LineScope string `field:"lineScope"` // 线路范围
+ LineCarrier string `field:"lineCarrier"` // 运营商
+ LineRegion string `field:"lineRegion"` // 区域
+ LineProvince string `field:"lineProvince"` // 省份
+ LineContinent string `field:"lineContinent"` // 大洲
+ LineCountry string `field:"lineCountry"` // 国家
+ TTL int32 `field:"ttl"` // TTL
+ IsOn bool `field:"isOn"` // 启用状态
+ Priority int32 `field:"priority"` // 优先级
+ UpdatedAt uint64 `field:"updatedAt"` // 修改时间
+ State uint8 `field:"state"` // 记录状态
+}
+
+type HTTPDNSCustomRuleOperator struct {
+ Id any // ID
+ AppId any // 应用DB ID
+ DomainId any // 域名ID
+ RuleName any // 规则名称
+ LineScope any // 线路范围
+ LineCarrier any // 运营商
+ LineRegion any // 区域
+ LineProvince any // 省份
+ LineContinent any // 大洲
+ LineCountry any // 国家
+ TTL any // TTL
+ IsOn any // 启用状态
+ Priority any // 优先级
+ UpdatedAt any // 修改时间
+ State any // 记录状态
+}
+
+func NewHTTPDNSCustomRuleOperator() *HTTPDNSCustomRuleOperator {
+ return &HTTPDNSCustomRuleOperator{}
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_custom_rule_record_dao.go b/EdgeAPI/internal/db/models/httpdns_custom_rule_record_dao.go
new file mode 100644
index 0000000..58aad00
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_custom_rule_record_dao.go
@@ -0,0 +1,69 @@
+package models
+
+import (
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/iwind/TeaGo/Tea"
+ "github.com/iwind/TeaGo/dbs"
+ "github.com/iwind/TeaGo/types"
+)
+
+const (
+ HTTPDNSCustomRuleRecordStateEnabled = 1
+ HTTPDNSCustomRuleRecordStateDisabled = 0
+)
+
+type HTTPDNSCustomRuleRecordDAO dbs.DAO
+
+func NewHTTPDNSCustomRuleRecordDAO() *HTTPDNSCustomRuleRecordDAO {
+ return dbs.NewDAO(&HTTPDNSCustomRuleRecordDAO{
+ DAOObject: dbs.DAOObject{
+ DB: Tea.Env,
+ Table: "edgeHTTPDNSCustomRuleRecords",
+ Model: new(HTTPDNSCustomRuleRecord),
+ PkName: "id",
+ },
+ }).(*HTTPDNSCustomRuleRecordDAO)
+}
+
+var SharedHTTPDNSCustomRuleRecordDAO *HTTPDNSCustomRuleRecordDAO
+
+func init() {
+ dbs.OnReady(func() {
+ SharedHTTPDNSCustomRuleRecordDAO = NewHTTPDNSCustomRuleRecordDAO()
+ })
+}
+
+func (this *HTTPDNSCustomRuleRecordDAO) CreateRecord(tx *dbs.Tx, ruleId int64, recordType string, recordValue string, weight int32, sort int32) (int64, error) {
+ var op = NewHTTPDNSCustomRuleRecordOperator()
+ op.RuleId = ruleId
+ op.RecordType = recordType
+ op.RecordValue = recordValue
+ op.Weight = weight
+ op.Sort = sort
+ op.State = HTTPDNSCustomRuleRecordStateEnabled
+ err := this.Save(tx, op)
+ if err != nil {
+ return 0, err
+ }
+ return types.Int64(op.Id), nil
+}
+
+func (this *HTTPDNSCustomRuleRecordDAO) DisableRecordsWithRuleId(tx *dbs.Tx, ruleId int64) error {
+ _, err := this.Query(tx).
+ Attr("ruleId", ruleId).
+ State(HTTPDNSCustomRuleRecordStateEnabled).
+ Set("state", HTTPDNSCustomRuleRecordStateDisabled).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSCustomRuleRecordDAO) ListEnabledRecordsWithRuleId(tx *dbs.Tx, ruleId int64) (result []*HTTPDNSCustomRuleRecord, err error) {
+ _, err = this.Query(tx).
+ State(HTTPDNSCustomRuleRecordStateEnabled).
+ Attr("ruleId", ruleId).
+ Asc("sort").
+ AscPk().
+ Slice(&result).
+ FindAll()
+ return
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_custom_rule_record_model.go b/EdgeAPI/internal/db/models/httpdns_custom_rule_record_model.go
new file mode 100644
index 0000000..c5128d0
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_custom_rule_record_model.go
@@ -0,0 +1,26 @@
+package models
+
+// HTTPDNSCustomRuleRecord 自定义规则记录值
+type HTTPDNSCustomRuleRecord struct {
+ Id uint32 `field:"id"` // ID
+ RuleId uint32 `field:"ruleId"` // 规则ID
+ RecordType string `field:"recordType"` // 记录类型
+ RecordValue string `field:"recordValue"` // 记录值
+ Weight int32 `field:"weight"` // 权重
+ Sort int32 `field:"sort"` // 顺序
+ State uint8 `field:"state"` // 记录状态
+}
+
+type HTTPDNSCustomRuleRecordOperator struct {
+ Id any // ID
+ RuleId any // 规则ID
+ RecordType any // 记录类型
+ RecordValue any // 记录值
+ Weight any // 权重
+ Sort any // 顺序
+ State any // 记录状态
+}
+
+func NewHTTPDNSCustomRuleRecordOperator() *HTTPDNSCustomRuleRecordOperator {
+ return &HTTPDNSCustomRuleRecordOperator{}
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_domain_dao.go b/EdgeAPI/internal/db/models/httpdns_domain_dao.go
new file mode 100644
index 0000000..2330869
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_domain_dao.go
@@ -0,0 +1,115 @@
+package models
+
+import (
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/iwind/TeaGo/Tea"
+ "github.com/iwind/TeaGo/dbs"
+ "github.com/iwind/TeaGo/types"
+ "strings"
+ "time"
+)
+
+const (
+ HTTPDNSDomainStateEnabled = 1
+ HTTPDNSDomainStateDisabled = 0
+)
+
+type HTTPDNSDomainDAO dbs.DAO
+
+func NewHTTPDNSDomainDAO() *HTTPDNSDomainDAO {
+ return dbs.NewDAO(&HTTPDNSDomainDAO{
+ DAOObject: dbs.DAOObject{
+ DB: Tea.Env,
+ Table: "edgeHTTPDNSDomains",
+ Model: new(HTTPDNSDomain),
+ PkName: "id",
+ },
+ }).(*HTTPDNSDomainDAO)
+}
+
+var SharedHTTPDNSDomainDAO *HTTPDNSDomainDAO
+
+func init() {
+ dbs.OnReady(func() {
+ SharedHTTPDNSDomainDAO = NewHTTPDNSDomainDAO()
+ })
+}
+
+func (this *HTTPDNSDomainDAO) CreateDomain(tx *dbs.Tx, appDbId int64, domain string, isOn bool) (int64, error) {
+ domain = strings.ToLower(strings.TrimSpace(domain))
+ var op = NewHTTPDNSDomainOperator()
+ op.AppId = appDbId
+ op.Domain = domain
+ op.IsOn = isOn
+ op.CreatedAt = time.Now().Unix()
+ op.UpdatedAt = time.Now().Unix()
+ op.State = HTTPDNSDomainStateEnabled
+ err := this.Save(tx, op)
+ if err != nil {
+ return 0, err
+ }
+ return types.Int64(op.Id), nil
+}
+
+func (this *HTTPDNSDomainDAO) DisableDomain(tx *dbs.Tx, domainId int64) error {
+ _, err := this.Query(tx).
+ Pk(domainId).
+ Set("state", HTTPDNSDomainStateDisabled).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSDomainDAO) DisableDomainsWithAppId(tx *dbs.Tx, appDbId int64) error {
+ _, err := this.Query(tx).
+ Attr("appId", appDbId).
+ State(HTTPDNSDomainStateEnabled).
+ Set("state", HTTPDNSDomainStateDisabled).
+ Set("updatedAt", time.Now().Unix()).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSDomainDAO) UpdateDomainStatus(tx *dbs.Tx, domainId int64, isOn bool) error {
+ _, err := this.Query(tx).
+ Pk(domainId).
+ State(HTTPDNSDomainStateEnabled).
+ Set("isOn", isOn).
+ Set("updatedAt", time.Now().Unix()).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSDomainDAO) FindEnabledDomain(tx *dbs.Tx, domainId int64) (*HTTPDNSDomain, error) {
+ one, err := this.Query(tx).
+ Pk(domainId).
+ State(HTTPDNSDomainStateEnabled).
+ Find()
+ if one == nil {
+ return nil, err
+ }
+ return one.(*HTTPDNSDomain), nil
+}
+
+func (this *HTTPDNSDomainDAO) FindEnabledDomainWithAppAndName(tx *dbs.Tx, appDbId int64, domain string) (*HTTPDNSDomain, error) {
+ one, err := this.Query(tx).
+ State(HTTPDNSDomainStateEnabled).
+ Attr("appId", appDbId).
+ Attr("domain", strings.ToLower(strings.TrimSpace(domain))).
+ Find()
+ if one == nil {
+ return nil, err
+ }
+ return one.(*HTTPDNSDomain), nil
+}
+
+func (this *HTTPDNSDomainDAO) ListEnabledDomainsWithAppId(tx *dbs.Tx, appDbId int64, keyword string) (result []*HTTPDNSDomain, err error) {
+ query := this.Query(tx).
+ State(HTTPDNSDomainStateEnabled).
+ Attr("appId", appDbId).
+ AscPk()
+ if len(keyword) > 0 {
+ query = query.Where("domain LIKE :kw").Param("kw", "%"+keyword+"%")
+ }
+ _, err = query.Slice(&result).FindAll()
+ return
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_domain_model.go b/EdgeAPI/internal/db/models/httpdns_domain_model.go
new file mode 100644
index 0000000..1b96478
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_domain_model.go
@@ -0,0 +1,26 @@
+package models
+
+// HTTPDNSDomain 应用绑定域名
+type HTTPDNSDomain struct {
+ Id uint32 `field:"id"` // ID
+ AppId uint32 `field:"appId"` // 应用DB ID
+ Domain string `field:"domain"` // 业务域名
+ IsOn bool `field:"isOn"` // 是否启用
+ CreatedAt uint64 `field:"createdAt"` // 创建时间
+ UpdatedAt uint64 `field:"updatedAt"` // 修改时间
+ State uint8 `field:"state"` // 记录状态
+}
+
+type HTTPDNSDomainOperator struct {
+ Id any // ID
+ AppId any // 应用DB ID
+ Domain any // 业务域名
+ IsOn any // 是否启用
+ CreatedAt any // 创建时间
+ UpdatedAt any // 修改时间
+ State any // 记录状态
+}
+
+func NewHTTPDNSDomainOperator() *HTTPDNSDomainOperator {
+ return &HTTPDNSDomainOperator{}
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_node_dao.go b/EdgeAPI/internal/db/models/httpdns_node_dao.go
new file mode 100644
index 0000000..a5bf0d3
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_node_dao.go
@@ -0,0 +1,273 @@
+package models
+
+import (
+ "encoding/json"
+ "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/iwind/TeaGo/Tea"
+ "github.com/iwind/TeaGo/dbs"
+ "github.com/iwind/TeaGo/rands"
+ "github.com/iwind/TeaGo/types"
+ "time"
+)
+
+const (
+ HTTPDNSNodeStateEnabled = 1 // 已启用
+ HTTPDNSNodeStateDisabled = 0 // 已禁用
+)
+
+type HTTPDNSNodeDAO dbs.DAO
+
+func NewHTTPDNSNodeDAO() *HTTPDNSNodeDAO {
+ return dbs.NewDAO(&HTTPDNSNodeDAO{
+ DAOObject: dbs.DAOObject{
+ DB: Tea.Env,
+ Table: "edgeHTTPDNSNodes",
+ Model: new(HTTPDNSNode),
+ PkName: "id",
+ },
+ }).(*HTTPDNSNodeDAO)
+}
+
+var SharedHTTPDNSNodeDAO *HTTPDNSNodeDAO
+
+func init() {
+ dbs.OnReady(func() {
+ SharedHTTPDNSNodeDAO = NewHTTPDNSNodeDAO()
+ })
+}
+
+// FindEnabledNodeIdWithUniqueId 根据唯一ID获取启用中的HTTPDNS节点ID
+func (this *HTTPDNSNodeDAO) FindEnabledNodeIdWithUniqueId(tx *dbs.Tx, uniqueId string) (int64, error) {
+ return this.Query(tx).
+ Attr("uniqueId", uniqueId).
+ Attr("state", HTTPDNSNodeStateEnabled).
+ ResultPk().
+ FindInt64Col(0)
+}
+
+// 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)
+ err := SharedApiTokenDAO.CreateAPIToken(tx, uniqueId, secret, nodeconfigs.NodeRoleHTTPDNS)
+ if err != nil {
+ return 0, err
+ }
+
+ var op = NewHTTPDNSNodeOperator()
+ op.ClusterId = clusterId
+ op.Name = name
+ op.IsOn = isOn
+ op.IsUp = false
+ op.IsInstalled = false
+ op.IsActive = false
+ op.UniqueId = uniqueId
+ op.Secret = secret
+ op.InstallDir = installDir
+ op.CreatedAt = time.Now().Unix()
+ op.UpdatedAt = time.Now().Unix()
+ op.State = HTTPDNSNodeStateEnabled
+ err = this.Save(tx, op)
+ if err != nil {
+ return 0, err
+ }
+ return types.Int64(op.Id), nil
+}
+
+// UpdateNode 更新节点
+func (this *HTTPDNSNodeDAO) UpdateNode(tx *dbs.Tx, nodeId int64, name string, installDir string, isOn bool) error {
+ var op = NewHTTPDNSNodeOperator()
+ op.Id = nodeId
+ op.Name = name
+ op.InstallDir = installDir
+ op.IsOn = isOn
+ op.UpdatedAt = time.Now().Unix()
+ return this.Save(tx, op)
+}
+
+// DisableNode 禁用节点
+func (this *HTTPDNSNodeDAO) DisableNode(tx *dbs.Tx, nodeId int64) error {
+ node, err := this.FindEnabledNode(tx, nodeId)
+ if err != nil {
+ return err
+ }
+ if node == nil {
+ return nil
+ }
+
+ _, err = this.Query(tx).
+ Pk(nodeId).
+ Set("state", HTTPDNSNodeStateDisabled).
+ Update()
+ if err != nil {
+ return err
+ }
+
+ _, err = SharedApiTokenDAO.Query(tx).
+ Attr("nodeId", node.UniqueId).
+ Attr("role", nodeconfigs.NodeRoleHTTPDNS).
+ Set("state", ApiTokenStateDisabled).
+ Update()
+ return err
+}
+
+// FindEnabledNode 查找启用节点
+func (this *HTTPDNSNodeDAO) FindEnabledNode(tx *dbs.Tx, nodeId int64) (*HTTPDNSNode, error) {
+ one, err := this.Query(tx).
+ Pk(nodeId).
+ Attr("state", HTTPDNSNodeStateEnabled).
+ Find()
+ if one == nil {
+ return nil, err
+ }
+ return one.(*HTTPDNSNode), nil
+}
+
+// FindNodeClusterId 查询节点所属集群ID
+func (this *HTTPDNSNodeDAO) FindNodeClusterId(tx *dbs.Tx, nodeId int64) (int64, error) {
+ return this.Query(tx).
+ Pk(nodeId).
+ Attr("state", HTTPDNSNodeStateEnabled).
+ Result("clusterId").
+ FindInt64Col(0)
+}
+
+// ListEnabledNodes 列出节点
+func (this *HTTPDNSNodeDAO) ListEnabledNodes(tx *dbs.Tx, clusterId int64) (result []*HTTPDNSNode, err error) {
+ query := this.Query(tx).
+ State(HTTPDNSNodeStateEnabled).
+ AscPk()
+ if clusterId > 0 {
+ query = query.Attr("clusterId", clusterId)
+ }
+ _, err = query.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()
+ op.Id = nodeId
+ op.IsUp = isUp
+ op.IsInstalled = isInstalled
+ op.IsActive = isActive
+ op.UpdatedAt = time.Now().Unix()
+ if len(statusJSON) > 0 {
+ op.Status = statusJSON
+ }
+ if len(installStatusJSON) > 0 {
+ mergedStatusJSON, mergeErr := this.mergeInstallStatusJSON(tx, nodeId, installStatusJSON)
+ if mergeErr != nil {
+ return mergeErr
+ }
+ op.InstallStatus = mergedStatusJSON
+ }
+ return this.Save(tx, op)
+}
+
+// UpdateNodeInstallStatus 更新节点安装状态
+func (this *HTTPDNSNodeDAO) UpdateNodeInstallStatus(tx *dbs.Tx, nodeId int64, installStatus *NodeInstallStatus) error {
+ if installStatus == nil {
+ return nil
+ }
+
+ // Read existing installStatus to preserve custom fields like 'ssh' and 'ipAddr'
+ raw, err := this.Query(tx).Pk(nodeId).Result("installStatus").FindBytesCol()
+ if err != nil {
+ return err
+ }
+
+ var m = map[string]interface{}{}
+ if len(raw) > 0 {
+ _ = json.Unmarshal(raw, &m)
+ }
+
+ // Overlay standard install status fields
+ statusData, err := json.Marshal(installStatus)
+ if err != nil {
+ return err
+ }
+ var newStatusMap = map[string]interface{}{}
+ _ = json.Unmarshal(statusData, &newStatusMap)
+
+ for k, v := range newStatusMap {
+ m[k] = v
+ }
+
+ // Re-marshal the merged map
+ mergedData, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+
+ _, err = this.Query(tx).
+ Pk(nodeId).
+ Set("installStatus", mergedData).
+ Set("updatedAt", time.Now().Unix()).
+ Update()
+ return err
+}
+
+func (this *HTTPDNSNodeDAO) mergeInstallStatusJSON(tx *dbs.Tx, nodeId int64, patch []byte) ([]byte, error) {
+ if len(patch) == 0 {
+ return patch, nil
+ }
+
+ raw, err := this.Query(tx).Pk(nodeId).Result("installStatus").FindBytesCol()
+ if err != nil {
+ return nil, err
+ }
+
+ merged := map[string]interface{}{}
+ if len(raw) > 0 {
+ _ = json.Unmarshal(raw, &merged)
+ }
+ patchMap := map[string]interface{}{}
+ if len(patch) > 0 {
+ _ = json.Unmarshal(patch, &patchMap)
+ }
+
+ for k, v := range patchMap {
+ merged[k] = v
+ }
+
+ data, err := json.Marshal(merged)
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+}
+
+// FindNodeInstallStatus 读取节点安装状态
+func (this *HTTPDNSNodeDAO) FindNodeInstallStatus(tx *dbs.Tx, nodeId int64) (*NodeInstallStatus, error) {
+ raw, err := this.Query(tx).
+ Pk(nodeId).
+ State(HTTPDNSNodeStateEnabled).
+ Result("installStatus").
+ FindBytesCol()
+ if err != nil {
+ return nil, err
+ }
+ if len(raw) == 0 {
+ return nil, nil
+ }
+
+ installStatus := &NodeInstallStatus{}
+ err = json.Unmarshal(raw, installStatus)
+ if err != nil {
+ return nil, err
+ }
+ return installStatus, nil
+}
+
+// UpdateNodeIsInstalled 更新节点安装状态位
+func (this *HTTPDNSNodeDAO) UpdateNodeIsInstalled(tx *dbs.Tx, nodeId int64, isInstalled bool) error {
+ _, err := this.Query(tx).
+ Pk(nodeId).
+ State(HTTPDNSNodeStateEnabled).
+ Set("isInstalled", isInstalled).
+ Set("updatedAt", time.Now().Unix()).
+ Update()
+ return err
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_node_model.go b/EdgeAPI/internal/db/models/httpdns_node_model.go
new file mode 100644
index 0000000..80a15de
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_node_model.go
@@ -0,0 +1,44 @@
+package models
+
+import "github.com/iwind/TeaGo/dbs"
+
+// HTTPDNSNode HTTPDNS节点
+type HTTPDNSNode struct {
+ Id uint32 `field:"id"` // ID
+ ClusterId uint32 `field:"clusterId"` // 集群ID
+ Name string `field:"name"` // 节点名称
+ IsOn bool `field:"isOn"` // 是否启用
+ IsUp bool `field:"isUp"` // 是否在线
+ IsInstalled bool `field:"isInstalled"` // 是否已安装
+ IsActive bool `field:"isActive"` // 是否活跃
+ UniqueId string `field:"uniqueId"` // 节点唯一ID
+ Secret string `field:"secret"` // 节点密钥
+ InstallDir string `field:"installDir"` // 安装目录
+ Status dbs.JSON `field:"status"` // 运行状态快照
+ InstallStatus dbs.JSON `field:"installStatus"` // 安装状态
+ CreatedAt uint64 `field:"createdAt"` // 创建时间
+ UpdatedAt uint64 `field:"updatedAt"` // 修改时间
+ State uint8 `field:"state"` // 记录状态
+}
+
+type HTTPDNSNodeOperator struct {
+ Id any // ID
+ ClusterId any // 集群ID
+ Name any // 节点名称
+ IsOn any // 是否启用
+ IsUp any // 是否在线
+ IsInstalled any // 是否已安装
+ IsActive any // 是否活跃
+ UniqueId any // 节点唯一ID
+ Secret any // 节点密钥
+ InstallDir any // 安装目录
+ Status any // 运行状态快照
+ InstallStatus any // 安装状态
+ CreatedAt any // 创建时间
+ UpdatedAt any // 修改时间
+ State any // 记录状态
+}
+
+func NewHTTPDNSNodeOperator() *HTTPDNSNodeOperator {
+ return &HTTPDNSNodeOperator{}
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_runtime_log_dao.go b/EdgeAPI/internal/db/models/httpdns_runtime_log_dao.go
new file mode 100644
index 0000000..4d53581
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_runtime_log_dao.go
@@ -0,0 +1,108 @@
+package models
+
+import (
+ "strconv"
+ "time"
+
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/iwind/TeaGo/Tea"
+ "github.com/iwind/TeaGo/dbs"
+)
+
+type HTTPDNSRuntimeLogDAO dbs.DAO
+
+func NewHTTPDNSRuntimeLogDAO() *HTTPDNSRuntimeLogDAO {
+ return dbs.NewDAO(&HTTPDNSRuntimeLogDAO{
+ DAOObject: dbs.DAOObject{
+ DB: Tea.Env,
+ Table: "edgeHTTPDNSRuntimeLogs",
+ Model: new(HTTPDNSRuntimeLog),
+ PkName: "id",
+ },
+ }).(*HTTPDNSRuntimeLogDAO)
+}
+
+var SharedHTTPDNSRuntimeLogDAO *HTTPDNSRuntimeLogDAO
+
+func init() {
+ dbs.OnReady(func() {
+ SharedHTTPDNSRuntimeLogDAO = NewHTTPDNSRuntimeLogDAO()
+ })
+}
+
+func (this *HTTPDNSRuntimeLogDAO) CreateLog(tx *dbs.Tx, log *HTTPDNSRuntimeLog) error {
+ lastLog, err := this.Query(tx).
+ Result("id", "clusterId", "nodeId", "level", "type", "module", "description", "createdAt").
+ DescPk().
+ Find()
+ if err != nil {
+ return err
+ }
+ if lastLog != nil {
+ nodeLog := lastLog.(*HTTPDNSRuntimeLog)
+ if nodeLog.ClusterId == log.ClusterId &&
+ nodeLog.NodeId == log.NodeId &&
+ nodeLog.Level == log.Level &&
+ nodeLog.Type == log.Type &&
+ nodeLog.Module == log.Module &&
+ nodeLog.Description == log.Description &&
+ time.Now().Unix()-int64(nodeLog.CreatedAt) < 1800 {
+
+ count := log.Count
+ if count <= 0 {
+ count = 1
+ }
+
+ return this.Query(tx).
+ Pk(nodeLog.Id).
+ Set("count", dbs.SQL("count+"+strconv.FormatInt(count, 10))).
+ UpdateQuickly()
+ }
+ }
+
+ var op = NewHTTPDNSRuntimeLogOperator()
+ op.ClusterId = log.ClusterId
+ op.NodeId = log.NodeId
+ op.Level = log.Level
+ op.Type = log.Type
+ op.Module = log.Module
+ op.Description = log.Description
+ op.Count = log.Count
+ op.RequestId = log.RequestId
+ op.CreatedAt = log.CreatedAt
+ op.Day = log.Day
+ return this.Save(tx, op)
+}
+
+func (this *HTTPDNSRuntimeLogDAO) BuildListQuery(tx *dbs.Tx, day string, clusterId int64, nodeId int64, level string, keyword string) *dbs.Query {
+ query := this.Query(tx).DescPk()
+ if len(day) > 0 {
+ query = query.Attr("day", day)
+ }
+ if clusterId > 0 {
+ query = query.Attr("clusterId", clusterId)
+ }
+ if nodeId > 0 {
+ query = query.Attr("nodeId", nodeId)
+ }
+ if len(level) > 0 {
+ query = query.Attr("level", level)
+ }
+ if len(keyword) > 0 {
+ query = query.Where("(type LIKE :kw OR module LIKE :kw OR description LIKE :kw OR requestId LIKE :kw)").Param("kw", "%"+keyword+"%")
+ }
+ return query
+}
+
+func (this *HTTPDNSRuntimeLogDAO) CountLogs(tx *dbs.Tx, day string, clusterId int64, nodeId int64, level string, keyword string) (int64, error) {
+ return this.BuildListQuery(tx, day, clusterId, nodeId, level, keyword).Count()
+}
+
+func (this *HTTPDNSRuntimeLogDAO) ListLogs(tx *dbs.Tx, day string, clusterId int64, nodeId int64, level string, keyword string, offset int64, size int64) (result []*HTTPDNSRuntimeLog, err error) {
+ _, err = this.BuildListQuery(tx, day, clusterId, nodeId, level, keyword).
+ Offset(offset).
+ Limit(size).
+ Slice(&result).
+ FindAll()
+ return
+}
diff --git a/EdgeAPI/internal/db/models/httpdns_runtime_log_model.go b/EdgeAPI/internal/db/models/httpdns_runtime_log_model.go
new file mode 100644
index 0000000..fe26dda
--- /dev/null
+++ b/EdgeAPI/internal/db/models/httpdns_runtime_log_model.go
@@ -0,0 +1,34 @@
+package models
+
+// HTTPDNSRuntimeLog 运行日志
+type HTTPDNSRuntimeLog struct {
+ Id uint64 `field:"id"` // ID
+ ClusterId uint32 `field:"clusterId"` // 集群ID
+ NodeId uint32 `field:"nodeId"` // 节点ID
+ Level string `field:"level"` // 级别
+ Type string `field:"type"` // 类型
+ Module string `field:"module"` // 模块
+ Description string `field:"description"` // 详情
+ Count int64 `field:"count"` // 次数
+ RequestId string `field:"requestId"` // 请求ID
+ CreatedAt uint64 `field:"createdAt"` // 创建时间
+ Day string `field:"day"` // YYYYMMDD
+}
+
+type HTTPDNSRuntimeLogOperator struct {
+ Id any // ID
+ ClusterId any // 集群ID
+ NodeId any // 节点ID
+ Level any // 级别
+ Type any // 类型
+ Module any // 模块
+ Description any // 详情
+ Count any // 次数
+ RequestId any // 请求ID
+ CreatedAt any // 创建时间
+ Day any // YYYYMMDD
+}
+
+func NewHTTPDNSRuntimeLogOperator() *HTTPDNSRuntimeLogOperator {
+ return &HTTPDNSRuntimeLogOperator{}
+}
diff --git a/EdgeAPI/internal/db/models/node_task_dao.go b/EdgeAPI/internal/db/models/node_task_dao.go
index df317dd..1a3d90a 100644
--- a/EdgeAPI/internal/db/models/node_task_dao.go
+++ b/EdgeAPI/internal/db/models/node_task_dao.go
@@ -15,35 +15,41 @@ import (
type NodeTaskType = string
const (
- // CDN相关
+ // CDN鐩稿叧
- NodeTaskTypeConfigChanged NodeTaskType = "configChanged" // 节点整体配置变化
- NodeTaskTypeDDosProtectionChanged NodeTaskType = "ddosProtectionChanged" // 节点DDoS配置变更
- NodeTaskTypeGlobalServerConfigChanged NodeTaskType = "globalServerConfigChanged" // 全局服务设置变化
- NodeTaskTypeIPListDeleted NodeTaskType = "ipListDeleted" // IPList被删除
- NodeTaskTypeIPItemChanged NodeTaskType = "ipItemChanged" // IP条目变更
- NodeTaskTypeNodeVersionChanged NodeTaskType = "nodeVersionChanged" // 节点版本变化
- NodeTaskTypeScriptsChanged NodeTaskType = "scriptsChanged" // 脚本配置变化
- NodeTaskTypeNodeLevelChanged NodeTaskType = "nodeLevelChanged" // 节点级别变化
- NodeTaskTypeUserServersStateChanged NodeTaskType = "userServersStateChanged" // 用户服务状态变化
- NodeTaskTypeUAMPolicyChanged NodeTaskType = "uamPolicyChanged" // UAM策略变化
- NodeTaskTypeHTTPPagesPolicyChanged NodeTaskType = "httpPagesPolicyChanged" // 自定义页面变化
- NodeTaskTypeHTTPCCPolicyChanged NodeTaskType = "httpCCPolicyChanged" // CC策略变化
- NodeTaskTypeHTTP3PolicyChanged NodeTaskType = "http3PolicyChanged" // HTTP3策略变化
- NodeTaskTypeNetworkSecurityPolicyChanged NodeTaskType = "networkSecurityPolicyChanged" // 网络安全策略变化
- NodeTaskTypeWebPPolicyChanged NodeTaskType = "webPPolicyChanged" // WebP策略变化
- NodeTaskTypeUpdatingServers NodeTaskType = "updatingServers" // 更新一组服务
- NodeTaskTypeTOAChanged NodeTaskType = "toaChanged" // TOA配置变化
- NodeTaskTypePlanChanged NodeTaskType = "planChanged" // 套餐变化
+ NodeTaskTypeConfigChanged NodeTaskType = "configChanged" // 鑺傜偣鏁翠綋閰嶇疆鍙樺寲
+ NodeTaskTypeDDosProtectionChanged NodeTaskType = "ddosProtectionChanged" // 鑺傜偣DDoS閰嶇疆鍙樻洿
+ NodeTaskTypeGlobalServerConfigChanged NodeTaskType = "globalServerConfigChanged" // 鍏ㄥ眬鏈嶅姟璁剧疆鍙樺寲
+ NodeTaskTypeIPListDeleted NodeTaskType = "ipListDeleted" // IPList琚垹闄?
+ NodeTaskTypeIPItemChanged NodeTaskType = "ipItemChanged" // IP鏉$洰鍙樻洿
+ NodeTaskTypeNodeVersionChanged NodeTaskType = "nodeVersionChanged" // 鑺傜偣鐗堟湰鍙樺寲
+ NodeTaskTypeScriptsChanged NodeTaskType = "scriptsChanged" // 鑴氭湰閰嶇疆鍙樺寲
+ NodeTaskTypeNodeLevelChanged NodeTaskType = "nodeLevelChanged" // 鑺傜偣绾у埆鍙樺寲
+ NodeTaskTypeUserServersStateChanged NodeTaskType = "userServersStateChanged" // 鐢ㄦ埛鏈嶅姟鐘舵€佸彉鍖?
+ NodeTaskTypeUAMPolicyChanged NodeTaskType = "uamPolicyChanged" // UAM绛栫暐鍙樺寲
+ NodeTaskTypeHTTPPagesPolicyChanged NodeTaskType = "httpPagesPolicyChanged" // 鑷畾涔夐〉闈㈠彉鍖?
+ NodeTaskTypeHTTPCCPolicyChanged NodeTaskType = "httpCCPolicyChanged" // CC绛栫暐鍙樺寲
+ NodeTaskTypeHTTP3PolicyChanged NodeTaskType = "http3PolicyChanged" // HTTP3绛栫暐鍙樺寲
+ NodeTaskTypeNetworkSecurityPolicyChanged NodeTaskType = "networkSecurityPolicyChanged" // 缃戠粶瀹夊叏绛栫暐鍙樺寲
+ NodeTaskTypeWebPPolicyChanged NodeTaskType = "webPPolicyChanged" // WebP绛栫暐鍙樺寲
+ NodeTaskTypeUpdatingServers NodeTaskType = "updatingServers" // 鏇存柊涓€缁勬湇鍔?
+ NodeTaskTypeTOAChanged NodeTaskType = "toaChanged" // TOA閰嶇疆鍙樺寲
+ NodeTaskTypePlanChanged NodeTaskType = "planChanged" // 濂楅鍙樺寲
- // NS相关
+ // NS鐩稿叧
NSNodeTaskTypeConfigChanged NodeTaskType = "nsConfigChanged"
NSNodeTaskTypeDomainChanged NodeTaskType = "nsDomainChanged"
NSNodeTaskTypeRecordChanged NodeTaskType = "nsRecordChanged"
NSNodeTaskTypeRouteChanged NodeTaskType = "nsRouteChanged"
NSNodeTaskTypeKeyChanged NodeTaskType = "nsKeyChanged"
- NSNodeTaskTypeDDosProtectionChanged NodeTaskType = "nsDDoSProtectionChanged" // 节点DDoS配置变更
+ NSNodeTaskTypeDDosProtectionChanged NodeTaskType = "nsDDoSProtectionChanged" // 鑺傜偣DDoS閰嶇疆鍙樻洿
+ // HTTPDNS相关
+ HTTPDNSNodeTaskTypeConfigChanged NodeTaskType = "httpdnsConfigChanged"
+ HTTPDNSNodeTaskTypeAppChanged NodeTaskType = "httpdnsAppChanged"
+ HTTPDNSNodeTaskTypeDomainChanged NodeTaskType = "httpdnsDomainChanged"
+ HTTPDNSNodeTaskTypeRuleChanged NodeTaskType = "httpdnsRuleChanged"
+ HTTPDNSNodeTaskTypeTLSChanged NodeTaskType = "httpdnsTLSChanged"
)
type NodeTaskDAO dbs.DAO
@@ -67,15 +73,15 @@ func init() {
})
}
-// CreateNodeTask 创建单个节点任务
+// CreateNodeTask 鍒涘缓鍗曚釜鑺傜偣浠诲姟
func (this *NodeTaskDAO) CreateNodeTask(tx *dbs.Tx, role string, clusterId int64, nodeId int64, userId int64, serverId int64, taskType NodeTaskType) error {
if clusterId <= 0 || nodeId <= 0 {
return nil
}
var uniqueId = role + "@" + types.String(nodeId) + "@node@" + types.String(serverId) + "@" + taskType
- // 用户信息
- // 没有直接加入到 uniqueId 中,是为了兼容以前的字段值
+ // 鐢ㄦ埛淇℃伅
+ // 娌℃湁鐩存帴鍔犲叆鍒?uniqueId 涓紝鏄负浜嗗吋瀹逛互鍓嶇殑瀛楁鍊?
if userId > 0 {
uniqueId += "@" + types.String(userId)
}
@@ -113,7 +119,7 @@ func (this *NodeTaskDAO) CreateNodeTask(tx *dbs.Tx, role string, clusterId int64
return err
}
-// CreateClusterTask 创建集群任务
+// CreateClusterTask 鍒涘缓闆嗙兢浠诲姟
func (this *NodeTaskDAO) CreateClusterTask(tx *dbs.Tx, role string, clusterId int64, userId int64, serverId int64, taskType NodeTaskType) error {
if clusterId <= 0 {
return nil
@@ -121,8 +127,8 @@ func (this *NodeTaskDAO) CreateClusterTask(tx *dbs.Tx, role string, clusterId in
var uniqueId = role + "@" + types.String(clusterId) + "@" + types.String(serverId) + "@cluster@" + taskType
- // 用户信息
- // 没有直接加入到 uniqueId 中,是为了兼容以前的字段值
+ // 鐢ㄦ埛淇℃伅
+ // 娌℃湁鐩存帴鍔犲叆鍒?uniqueId 涓紝鏄负浜嗗吋瀹逛互鍓嶇殑瀛楁鍊?
if userId > 0 {
uniqueId += "@" + types.String(userId)
}
@@ -155,7 +161,7 @@ func (this *NodeTaskDAO) CreateClusterTask(tx *dbs.Tx, role string, clusterId in
return err
}
-// ExtractNodeClusterTask 分解边缘节点集群任务
+// ExtractNodeClusterTask 鍒嗚В杈圭紭鑺傜偣闆嗙兢浠诲姟
func (this *NodeTaskDAO) ExtractNodeClusterTask(tx *dbs.Tx, clusterId int64, userId int64, serverId int64, taskType NodeTaskType) error {
nodeIds, err := SharedNodeDAO.FindAllNodeIdsMatch(tx, clusterId, true, configutils.BoolStateYes)
if err != nil {
@@ -193,7 +199,7 @@ func (this *NodeTaskDAO) ExtractNodeClusterTask(tx *dbs.Tx, clusterId int64, use
return nil
}
-// ExtractAllClusterTasks 分解所有集群任务
+// ExtractAllClusterTasks 鍒嗚В鎵€鏈夐泦缇や换鍔?
func (this *NodeTaskDAO) ExtractAllClusterTasks(tx *dbs.Tx, role string) error {
ones, err := this.Query(tx).
Attr("role", role).
@@ -216,12 +222,17 @@ func (this *NodeTaskDAO) ExtractAllClusterTasks(tx *dbs.Tx, role string) error {
if err != nil {
return err
}
+ case nodeconfigs.NodeRoleHTTPDNS:
+ err = this.ExtractHTTPDNSClusterTask(tx, clusterId, one.(*NodeTask).Type)
+ if err != nil {
+ return err
+ }
}
}
return nil
}
-// DeleteAllClusterTasks 删除集群所有相关任务
+// DeleteAllClusterTasks 鍒犻櫎闆嗙兢鎵€鏈夌浉鍏充换鍔?
func (this *NodeTaskDAO) DeleteAllClusterTasks(tx *dbs.Tx, role string, clusterId int64) error {
_, err := this.Query(tx).
Attr("role", role).
@@ -230,7 +241,7 @@ func (this *NodeTaskDAO) DeleteAllClusterTasks(tx *dbs.Tx, role string, clusterI
return err
}
-// DeleteNodeTasks 删除节点相关任务
+// DeleteNodeTasks 鍒犻櫎鑺傜偣鐩稿叧浠诲姟
func (this *NodeTaskDAO) DeleteNodeTasks(tx *dbs.Tx, role string, nodeId int64) error {
_, err := this.Query(tx).
Attr("role", role).
@@ -239,13 +250,13 @@ func (this *NodeTaskDAO) DeleteNodeTasks(tx *dbs.Tx, role string, nodeId int64)
return err
}
-// DeleteAllNodeTasks 删除所有节点相关任务
+// DeleteAllNodeTasks 鍒犻櫎鎵€鏈夎妭鐐圭浉鍏充换鍔?
func (this *NodeTaskDAO) DeleteAllNodeTasks(tx *dbs.Tx) error {
return this.Query(tx).
DeleteQuickly()
}
-// FindDoingNodeTasks 查询一个节点的所有任务
+// FindDoingNodeTasks 鏌ヨ涓€涓妭鐐圭殑鎵€鏈変换鍔?
func (this *NodeTaskDAO) FindDoingNodeTasks(tx *dbs.Tx, role string, nodeId int64, version int64) (result []*NodeTask, err error) {
if nodeId <= 0 {
return
@@ -256,10 +267,10 @@ func (this *NodeTaskDAO) FindDoingNodeTasks(tx *dbs.Tx, role string, nodeId int6
UseIndex("nodeId").
Asc("version")
if version > 0 {
- query.Lt("LENGTH(version)", 19) // 兼容以往版本
+ query.Lt("LENGTH(version)", 19) // 鍏煎浠ュ線鐗堟湰
query.Gt("version", version)
} else {
- // 第一次访问时只取当前正在执行的或者执行失败的
+ // 绗竴娆¤闂椂鍙彇褰撳墠姝e湪鎵ц鐨勬垨鑰呮墽琛屽け璐ョ殑
query.Where("(isDone=0 OR (isDone=1 AND isOk=0))")
}
_, err = query.
@@ -268,10 +279,10 @@ func (this *NodeTaskDAO) FindDoingNodeTasks(tx *dbs.Tx, role string, nodeId int6
return
}
-// UpdateNodeTaskDone 修改节点任务的完成状态
+// UpdateNodeTaskDone 淇敼鑺傜偣浠诲姟鐨勫畬鎴愮姸鎬?
func (this *NodeTaskDAO) UpdateNodeTaskDone(tx *dbs.Tx, taskId int64, isOk bool, errorMessage string) error {
if isOk {
- // 特殊任务删除
+ // 鐗规畩浠诲姟鍒犻櫎
taskType, err := this.Query(tx).
Pk(taskId).
Result("type").
@@ -286,7 +297,7 @@ func (this *NodeTaskDAO) UpdateNodeTaskDone(tx *dbs.Tx, taskId int64, isOk bool,
}
}
- // 其他任务标记为完成
+ // 鍏朵粬浠诲姟鏍囪涓哄畬鎴?
var query = this.Query(tx).
Pk(taskId)
if !isOk {
@@ -305,7 +316,7 @@ func (this *NodeTaskDAO) UpdateNodeTaskDone(tx *dbs.Tx, taskId int64, isOk bool,
return err
}
-// FindAllDoingTaskClusterIds 查找正在更新的集群IDs
+// FindAllDoingTaskClusterIds 鏌ユ壘姝e湪鏇存柊鐨勯泦缇Ds
func (this *NodeTaskDAO) FindAllDoingTaskClusterIds(tx *dbs.Tx, role string) ([]int64, error) {
ones, _, err := this.Query(tx).
Result("DISTINCT(clusterId) AS clusterId").
@@ -322,7 +333,7 @@ func (this *NodeTaskDAO) FindAllDoingTaskClusterIds(tx *dbs.Tx, role string) ([]
return result, nil
}
-// FindAllDoingNodeTasksWithClusterId 查询某个集群下所有的任务
+// FindAllDoingNodeTasksWithClusterId 鏌ヨ鏌愪釜闆嗙兢涓嬫墍鏈夌殑浠诲姟
func (this *NodeTaskDAO) FindAllDoingNodeTasksWithClusterId(tx *dbs.Tx, role string, clusterId int64) (result []*NodeTask, err error) {
_, err = this.Query(tx).
Attr("role", role).
@@ -337,7 +348,7 @@ func (this *NodeTaskDAO) FindAllDoingNodeTasksWithClusterId(tx *dbs.Tx, role str
return
}
-// FindAllDoingNodeIds 查询有任务的节点IDs
+// FindAllDoingNodeIds 鏌ヨ鏈変换鍔$殑鑺傜偣IDs
func (this *NodeTaskDAO) FindAllDoingNodeIds(tx *dbs.Tx, role string) ([]int64, error) {
ones, err := this.Query(tx).
Result("DISTINCT(nodeId) AS nodeId").
@@ -356,7 +367,7 @@ func (this *NodeTaskDAO) FindAllDoingNodeIds(tx *dbs.Tx, role string) ([]int64,
return result, nil
}
-// ExistsDoingNodeTasks 检查是否有正在执行的任务
+// ExistsDoingNodeTasks 妫€鏌ユ槸鍚︽湁姝e湪鎵ц鐨勪换鍔?
func (this *NodeTaskDAO) ExistsDoingNodeTasks(tx *dbs.Tx, role string, excludeTypes []NodeTaskType) (bool, error) {
var query = this.Query(tx).
Attr("role", role).
@@ -370,7 +381,7 @@ func (this *NodeTaskDAO) ExistsDoingNodeTasks(tx *dbs.Tx, role string, excludeTy
return query.Exist()
}
-// ExistsErrorNodeTasks 是否有错误的任务
+// ExistsErrorNodeTasks 鏄惁鏈夐敊璇殑浠诲姟
func (this *NodeTaskDAO) ExistsErrorNodeTasks(tx *dbs.Tx, role string, excludeTypes []NodeTaskType) (bool, error) {
var query = this.Query(tx).
Attr("role", role).
@@ -383,7 +394,7 @@ func (this *NodeTaskDAO) ExistsErrorNodeTasks(tx *dbs.Tx, role string, excludeTy
return query.Exist()
}
-// DeleteNodeTask 删除任务
+// DeleteNodeTask 鍒犻櫎浠诲姟
func (this *NodeTaskDAO) DeleteNodeTask(tx *dbs.Tx, taskId int64) error {
_, err := this.Query(tx).
Pk(taskId).
@@ -391,7 +402,7 @@ func (this *NodeTaskDAO) DeleteNodeTask(tx *dbs.Tx, taskId int64) error {
return err
}
-// CountDoingNodeTasks 计算正在执行的任务
+// CountDoingNodeTasks 璁$畻姝e湪鎵ц鐨勪换鍔?
func (this *NodeTaskDAO) CountDoingNodeTasks(tx *dbs.Tx, role string) (int64, error) {
return this.Query(tx).
Attr("isDone", 0).
@@ -400,7 +411,7 @@ func (this *NodeTaskDAO) CountDoingNodeTasks(tx *dbs.Tx, role string) (int64, er
Count()
}
-// FindNotifyingNodeTasks 查找需要通知的任务
+// FindNotifyingNodeTasks 鏌ユ壘闇€瑕侀€氱煡鐨勪换鍔?
func (this *NodeTaskDAO) FindNotifyingNodeTasks(tx *dbs.Tx, role string, size int64) (result []*NodeTask, err error) {
_, err = this.Query(tx).
Attr("role", role).
@@ -413,7 +424,7 @@ func (this *NodeTaskDAO) FindNotifyingNodeTasks(tx *dbs.Tx, role string, size in
return
}
-// UpdateTasksNotified 设置任务已通知
+// UpdateTasksNotified 璁剧疆浠诲姟宸查€氱煡
func (this *NodeTaskDAO) UpdateTasksNotified(tx *dbs.Tx, taskIds []int64) error {
if len(taskIds) == 0 {
return nil
@@ -430,7 +441,7 @@ func (this *NodeTaskDAO) UpdateTasksNotified(tx *dbs.Tx, taskIds []int64) error
return nil
}
-// 生成一个版本号
+// 鐢熸垚涓€涓増鏈彿
func (this *NodeTaskDAO) increaseVersion(tx *dbs.Tx) (version int64, err error) {
return SharedSysLockerDAO.Increase(tx, "NODE_TASK_VERSION", 0)
}
diff --git a/EdgeAPI/internal/db/models/node_task_dao_httpdns.go b/EdgeAPI/internal/db/models/node_task_dao_httpdns.go
new file mode 100644
index 0000000..b07a685
--- /dev/null
+++ b/EdgeAPI/internal/db/models/node_task_dao_httpdns.go
@@ -0,0 +1,47 @@
+package models
+
+import (
+ "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
+ "github.com/iwind/TeaGo/dbs"
+)
+
+// ExtractHTTPDNSClusterTask 分解HTTPDNS节点集群任务
+func (this *NodeTaskDAO) ExtractHTTPDNSClusterTask(tx *dbs.Tx, clusterId int64, taskType NodeTaskType) error {
+ nodes, err := SharedHTTPDNSNodeDAO.ListEnabledNodes(tx, clusterId)
+ if err != nil {
+ return err
+ }
+
+ _, err = this.Query(tx).
+ Attr("role", nodeconfigs.NodeRoleHTTPDNS).
+ Attr("clusterId", clusterId).
+ Gt("nodeId", 0).
+ Attr("type", taskType).
+ Delete()
+ if err != nil {
+ return err
+ }
+
+ for _, node := range nodes {
+ if !node.IsOn {
+ continue
+ }
+
+ err = this.CreateNodeTask(tx, nodeconfigs.NodeRoleHTTPDNS, clusterId, int64(node.Id), 0, 0, taskType)
+ if err != nil {
+ return err
+ }
+ }
+
+ _, err = this.Query(tx).
+ Attr("role", nodeconfigs.NodeRoleHTTPDNS).
+ Attr("clusterId", clusterId).
+ Attr("nodeId", 0).
+ Attr("type", taskType).
+ Delete()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/EdgeAPI/internal/installers/fluent_bit.go b/EdgeAPI/internal/installers/fluent_bit.go
index 2272230..09ab0ff 100644
--- a/EdgeAPI/internal/installers/fluent_bit.go
+++ b/EdgeAPI/internal/installers/fluent_bit.go
@@ -343,6 +343,8 @@ func mapNodeRole(role nodeconfigs.NodeRole) (string, error) {
return fluentBitRoleNode, nil
case nodeconfigs.NodeRoleDNS:
return fluentBitRoleDNS, nil
+ case nodeconfigs.NodeRoleHTTPDNS:
+ return fluentBitRoleDNS, nil
default:
return "", fmt.Errorf("unsupported fluent-bit role '%s'", role)
}
diff --git a/EdgeAPI/internal/installers/installer_httpdns_node.go b/EdgeAPI/internal/installers/installer_httpdns_node.go
new file mode 100644
index 0000000..67ff300
--- /dev/null
+++ b/EdgeAPI/internal/installers/installer_httpdns_node.go
@@ -0,0 +1,230 @@
+package installers
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/TeaOSLab/EdgeAPI/internal/db/models"
+ "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
+)
+
+type HTTPDNSNodeInstaller struct {
+ BaseInstaller
+}
+
+func (i *HTTPDNSNodeInstaller) Install(dir string, params interface{}, installStatus *models.NodeInstallStatus) error {
+ if params == nil {
+ return errors.New("'params' required for node installation")
+ }
+ nodeParams, ok := params.(*NodeParams)
+ if !ok {
+ return errors.New("'params' should be *NodeParams")
+ }
+ err := nodeParams.Validate()
+ if err != nil {
+ return fmt.Errorf("params validation: %w", err)
+ }
+
+ installRootDir, appDir := resolveHTTPDNSInstallPaths(dir)
+
+ _, err = i.client.Stat(installRootDir)
+ if err != nil {
+ err = i.client.MkdirAll(installRootDir)
+ if err != nil {
+ installStatus.ErrorCode = "CREATE_ROOT_DIRECTORY_FAILED"
+ return fmt.Errorf("create directory '%s' failed: %w", installRootDir, err)
+ }
+ }
+
+ env, err := i.InstallHelper(installRootDir, nodeconfigs.NodeRoleHTTPDNS)
+ if err != nil {
+ installStatus.ErrorCode = "INSTALL_HELPER_FAILED"
+ return err
+ }
+
+ filePrefix := "edge-httpdns-" + env.OS + "-" + env.Arch
+ zipFile, err := i.LookupLatestInstallerForTarget(filePrefix, env)
+ if err != nil {
+ return err
+ }
+ if len(zipFile) == 0 {
+ return errors.New("can not find installer file for " + env.OS + "/" + env.Arch + ", expected '" + filePrefix + "-v*.zip' or distro-specific '" + filePrefix + "-{ubuntu22.04|amzn2023}-v*.zip'")
+ }
+
+ targetZip, err := i.copyZipToRemote(installRootDir, zipFile)
+ if err != nil {
+ return err
+ }
+
+ if !nodeParams.IsUpgrading {
+ _, stderr, testErr := i.client.Exec(env.HelperPath + " -cmd=test")
+ if testErr != nil {
+ return fmt.Errorf("test failed: %w", testErr)
+ }
+ if len(stderr) > 0 {
+ return errors.New("test failed: " + stderr)
+ }
+ }
+
+ exePath := appDir + "/bin/edge-httpdns"
+ if nodeParams.IsUpgrading {
+ _, err = i.client.Stat(exePath)
+ if err == nil {
+ _, _, _ = i.client.Exec(exePath + " stop")
+ removeErr := i.client.Remove(exePath)
+ if removeErr != nil && removeErr != os.ErrNotExist {
+ return fmt.Errorf("remove old file failed: %w", removeErr)
+ }
+ }
+ }
+
+ _, stderr, err := i.client.Exec(env.HelperPath + " -cmd=unzip -zip=\"" + targetZip + "\" -target=\"" + installRootDir + "\"")
+ if err != nil {
+ return err
+ }
+ if len(stderr) > 0 {
+ return errors.New("unzip installer failed: " + stderr)
+ }
+
+ certFile := appDir + "/configs/tls/server.crt"
+ keyFile := appDir + "/configs/tls/server.key"
+ err = i.writeTLSCertificate(certFile, keyFile, nodeParams.TLSCertData, nodeParams.TLSKeyData)
+ if err != nil {
+ installStatus.ErrorCode = "WRITE_TLS_CERT_FAILED"
+ return err
+ }
+
+ configFile := appDir + "/configs/api_httpdns.yaml"
+ if i.client.sudo {
+ _, _, _ = i.client.Exec("chown " + i.client.User() + " " + filepath.Dir(configFile))
+ }
+
+ configData := []byte(`rpc.endpoints: [ ${endpoints} ]
+nodeId: "${nodeId}"
+secret: "${nodeSecret}"
+
+https.listenAddr: ":443"
+https.cert: "${certFile}"
+https.key: "${keyFile}"`)
+ certFileClean := strings.ReplaceAll(certFile, "\\", "/")
+ keyFileClean := strings.ReplaceAll(keyFile, "\\", "/")
+
+ configData = bytes.ReplaceAll(configData, []byte("${endpoints}"), []byte(nodeParams.QuoteEndpoints()))
+ configData = bytes.ReplaceAll(configData, []byte("${nodeId}"), []byte(nodeParams.NodeId))
+ configData = bytes.ReplaceAll(configData, []byte("${nodeSecret}"), []byte(nodeParams.Secret))
+ configData = bytes.ReplaceAll(configData, []byte("${certFile}"), []byte(certFileClean))
+ configData = bytes.ReplaceAll(configData, []byte("${keyFile}"), []byte(keyFileClean))
+
+ _, err = i.client.WriteFile(configFile, configData)
+ if err != nil {
+ return fmt.Errorf("write '%s': %w", configFile, err)
+ }
+
+ err = i.SetupFluentBit(nodeconfigs.NodeRoleHTTPDNS)
+ if err != nil {
+ installStatus.ErrorCode = "SETUP_FLUENT_BIT_FAILED"
+ return fmt.Errorf("setup fluent-bit failed: %w", err)
+ }
+
+ startCmdPrefix := "cd " + shQuote(appDir+"/configs") + " && ../bin/edge-httpdns "
+
+ stdout, stderr, err := i.client.Exec(startCmdPrefix + "test")
+ if err != nil {
+ installStatus.ErrorCode = "TEST_FAILED"
+ return fmt.Errorf("test edge-httpdns failed: %w, stdout: %s, stderr: %s", err, stdout, stderr)
+ }
+ if len(stderr) > 0 {
+ if regexp.MustCompile(`(?i)rpc`).MatchString(stderr) || regexp.MustCompile(`(?i)rpc`).MatchString(stdout) {
+ installStatus.ErrorCode = "RPC_TEST_FAILED"
+ }
+ return errors.New("test edge-httpdns failed, stdout: " + stdout + ", stderr: " + stderr)
+ }
+
+ stdout, stderr, err = i.client.Exec(startCmdPrefix + "start")
+ if err != nil {
+ return fmt.Errorf("start edge-httpdns failed: %w, stdout: %s, stderr: %s", err, stdout, stderr)
+ }
+ if len(stderr) > 0 {
+ return errors.New("start edge-httpdns failed, stdout: " + stdout + ", stderr: " + stderr)
+ }
+
+ return nil
+}
+
+func resolveHTTPDNSInstallPaths(rawDir string) (installRootDir string, appDir string) {
+ dir := strings.TrimSpace(rawDir)
+ dir = strings.TrimRight(dir, "/")
+ if len(dir) == 0 {
+ return rawDir, rawDir + "/edge-httpdns"
+ }
+
+ if strings.HasSuffix(dir, "/edge-httpdns") {
+ root := strings.TrimSuffix(dir, "/edge-httpdns")
+ if len(root) == 0 {
+ root = "/"
+ }
+ return root, dir
+ }
+
+ return dir, dir + "/edge-httpdns"
+}
+
+func (i *HTTPDNSNodeInstaller) copyZipToRemote(dir string, zipFile string) (string, error) {
+ targetZip := ""
+ var firstCopyErr error
+ zipName := filepath.Base(zipFile)
+ for _, candidate := range []string{
+ dir + "/" + zipName,
+ i.client.UserHome() + "/" + zipName,
+ "/tmp/" + zipName,
+ } {
+ err := i.client.Copy(zipFile, candidate, 0777)
+ if err != nil {
+ if firstCopyErr == nil {
+ firstCopyErr = err
+ }
+ continue
+ }
+ targetZip = candidate
+ firstCopyErr = nil
+ break
+ }
+ if firstCopyErr != nil {
+ return "", fmt.Errorf("upload httpdns file failed: %w", firstCopyErr)
+ }
+ return targetZip, nil
+}
+
+func (i *HTTPDNSNodeInstaller) writeTLSCertificate(certFile string, keyFile string, certData []byte, keyData []byte) error {
+ if len(certData) == 0 || len(keyData) == 0 {
+ return errors.New("cluster tls certificate is empty")
+ }
+ certDir := filepath.Dir(certFile)
+ _, stderr, err := i.client.Exec("mkdir -p " + shQuote(certDir))
+ if err != nil {
+ return fmt.Errorf("create tls directory failed: %w, stderr: %s", err, stderr)
+ }
+ if i.client.sudo {
+ _, _, _ = i.client.Exec("chown " + i.client.User() + " " + shQuote(certDir))
+ }
+
+ _, err = i.client.WriteFile(certFile, certData)
+ if err != nil {
+ return fmt.Errorf("write cert file failed: %w", err)
+ }
+ _, err = i.client.WriteFile(keyFile, keyData)
+ if err != nil {
+ return fmt.Errorf("write key file failed: %w", err)
+ }
+
+ _, stderr, err = i.client.Exec("chmod 0644 " + shQuote(certFile) + " && chmod 0600 " + shQuote(keyFile))
+ if err != nil {
+ return fmt.Errorf("chmod tls files failed: %w, stderr: %s", err, stderr)
+ }
+ return nil
+}
diff --git a/EdgeAPI/internal/installers/params_node.go b/EdgeAPI/internal/installers/params_node.go
index b825150..72222f4 100644
--- a/EdgeAPI/internal/installers/params_node.go
+++ b/EdgeAPI/internal/installers/params_node.go
@@ -9,6 +9,8 @@ type NodeParams struct {
Endpoints []string
NodeId string
Secret string
+ TLSCertData []byte
+ TLSKeyData []byte
IsUpgrading bool // 是否为升级
}
diff --git a/EdgeAPI/internal/installers/queue_httpdns_node.go b/EdgeAPI/internal/installers/queue_httpdns_node.go
new file mode 100644
index 0000000..0cfc743
--- /dev/null
+++ b/EdgeAPI/internal/installers/queue_httpdns_node.go
@@ -0,0 +1,280 @@
+package installers
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/TeaOSLab/EdgeAPI/internal/db/models"
+ "github.com/TeaOSLab/EdgeAPI/internal/goman"
+ "github.com/TeaOSLab/EdgeAPI/internal/utils"
+ "github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
+ "github.com/iwind/TeaGo/logs"
+ "github.com/iwind/TeaGo/maps"
+)
+
+var sharedHTTPDNSNodeQueue = NewHTTPDNSNodeQueue()
+
+type HTTPDNSNodeQueue struct{}
+
+func NewHTTPDNSNodeQueue() *HTTPDNSNodeQueue {
+ return &HTTPDNSNodeQueue{}
+}
+
+func SharedHTTPDNSNodeQueue() *HTTPDNSNodeQueue {
+ return sharedHTTPDNSNodeQueue
+}
+
+// InstallNodeProcess 鍦ㄧ嚎瀹夎 HTTPDNS 鑺傜偣娴佺▼鎺у埗
+func (q *HTTPDNSNodeQueue) InstallNodeProcess(nodeId int64, isUpgrading bool) error {
+ installStatus := models.NewNodeInstallStatus()
+ installStatus.IsRunning = true
+ installStatus.IsFinished = false
+ installStatus.IsOk = false
+ installStatus.Error = ""
+ installStatus.ErrorCode = ""
+ installStatus.UpdatedAt = time.Now().Unix()
+
+ err := models.SharedHTTPDNSNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
+ if err != nil {
+ return err
+ }
+
+ ticker := utils.NewTicker(3 * time.Second)
+ goman.New(func() {
+ for ticker.Wait() {
+ installStatus.UpdatedAt = time.Now().Unix()
+ updateErr := models.SharedHTTPDNSNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
+ if updateErr != nil {
+ logs.Println("[HTTPDNS_INSTALL]" + updateErr.Error())
+ }
+ }
+ })
+ defer ticker.Stop()
+
+ err = q.InstallNode(nodeId, installStatus, isUpgrading)
+
+ installStatus.IsRunning = false
+ installStatus.IsFinished = true
+ if err != nil {
+ installStatus.IsOk = false
+ installStatus.Error = err.Error()
+ } else {
+ installStatus.IsOk = true
+ }
+ installStatus.UpdatedAt = time.Now().Unix()
+
+ updateErr := models.SharedHTTPDNSNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
+ if updateErr != nil {
+ return updateErr
+ }
+
+ if installStatus.IsOk {
+ return models.SharedHTTPDNSNodeDAO.UpdateNodeIsInstalled(nil, nodeId, true)
+ }
+ return nil
+}
+
+// InstallNode 鍦ㄧ嚎瀹夎 HTTPDNS 鑺傜偣
+func (q *HTTPDNSNodeQueue) InstallNode(nodeId int64, installStatus *models.NodeInstallStatus, isUpgrading bool) 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) + "'")
+ }
+ cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(nil, int64(node.ClusterId))
+ if err != nil {
+ return err
+ }
+ if cluster == nil {
+ return errors.New("can not find cluster")
+ }
+
+ sshHost, sshPort, grantId, err := q.parseSSHInfo(node)
+ if err != nil {
+ installStatus.ErrorCode = "EMPTY_SSH"
+ return err
+ }
+
+ grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, grantId)
+ if err != nil {
+ return err
+ }
+ if grant == nil {
+ installStatus.ErrorCode = "EMPTY_GRANT"
+ return errors.New("can not find user grant with id '" + numberutils.FormatInt64(grantId) + "'")
+ }
+
+ apiNodes, err := models.SharedAPINodeDAO.FindAllEnabledAndOnAPINodes(nil)
+ if err != nil {
+ return err
+ }
+ if len(apiNodes) == 0 {
+ return errors.New("no available api nodes")
+ }
+
+ apiEndpoints := make([]string, 0, 8)
+ for _, apiNode := range apiNodes {
+ addrConfigs, decodeErr := apiNode.DecodeAccessAddrs()
+ if decodeErr != nil {
+ return fmt.Errorf("decode api node access addresses failed: %w", decodeErr)
+ }
+ for _, addrConfig := range addrConfigs {
+ apiEndpoints = append(apiEndpoints, addrConfig.FullAddresses()...)
+ }
+ }
+ if len(apiEndpoints) == 0 {
+ return errors.New("no available api endpoints")
+ }
+
+ tlsCertData, tlsKeyData, err := q.resolveClusterTLSCertPair(cluster)
+ if err != nil {
+ installStatus.ErrorCode = "EMPTY_TLS_CERT"
+ return err
+ }
+
+ params := &NodeParams{
+ Endpoints: apiEndpoints,
+ NodeId: node.UniqueId,
+ Secret: node.Secret,
+ TLSCertData: tlsCertData,
+ TLSKeyData: tlsKeyData,
+ IsUpgrading: isUpgrading,
+ }
+
+ installer := &HTTPDNSNodeInstaller{}
+ err = installer.Login(&Credentials{
+ Host: sshHost,
+ Port: sshPort,
+ Username: grant.Username,
+ Password: grant.Password,
+ PrivateKey: grant.PrivateKey,
+ Passphrase: grant.Passphrase,
+ Method: grant.Method,
+ Sudo: grant.Su == 1,
+ })
+ if err != nil {
+ installStatus.ErrorCode = "SSH_LOGIN_FAILED"
+ return err
+ }
+ defer func() {
+ _ = installer.Close()
+ }()
+
+ installDir := node.InstallDir
+ if len(installDir) == 0 {
+ if cluster != nil && len(cluster.InstallDir) > 0 {
+ installDir = cluster.InstallDir
+ }
+ if len(installDir) == 0 {
+ installDir = installer.client.UserHome() + "/edge-httpdns"
+ }
+ }
+
+ return installer.Install(installDir, params, installStatus)
+}
+
+func (q *HTTPDNSNodeQueue) resolveClusterTLSCertPair(cluster *models.HTTPDNSCluster) ([]byte, []byte, error) {
+ if cluster == nil {
+ return nil, nil, errors.New("cluster not found")
+ }
+ if len(cluster.TLSPolicy) == 0 {
+ return nil, nil, errors.New("cluster tls policy is empty")
+ }
+
+ tlsConfig := map[string]json.RawMessage{}
+ if err := json.Unmarshal(cluster.TLSPolicy, &tlsConfig); err != nil {
+ return nil, nil, fmt.Errorf("decode cluster tls policy failed: %w", err)
+ }
+
+ sslPolicyData := tlsConfig["sslPolicy"]
+ if len(sslPolicyData) == 0 {
+ // Compatible with old data where TLSPolicy stores SSLPolicy directly.
+ sslPolicyData = json.RawMessage(cluster.TLSPolicy)
+ }
+ sslPolicy := &sslconfigs.SSLPolicy{}
+ if err := json.Unmarshal(sslPolicyData, sslPolicy); err != nil {
+ return nil, nil, fmt.Errorf("decode ssl policy failed: %w", err)
+ }
+
+ for _, cert := range sslPolicy.Certs {
+ if cert == nil {
+ continue
+ }
+ if len(cert.CertData) > 0 && len(cert.KeyData) > 0 {
+ return cert.CertData, cert.KeyData, nil
+ }
+ }
+
+ for _, certRef := range sslPolicy.CertRefs {
+ if certRef == nil || certRef.CertId <= 0 {
+ continue
+ }
+ certConfig, err := models.SharedSSLCertDAO.ComposeCertConfig(nil, certRef.CertId, false, nil, nil)
+ if err != nil {
+ return nil, nil, fmt.Errorf("load ssl cert %d failed: %w", certRef.CertId, err)
+ }
+ if certConfig == nil {
+ continue
+ }
+ if len(certConfig.CertData) > 0 && len(certConfig.KeyData) > 0 {
+ return certConfig.CertData, certConfig.KeyData, nil
+ }
+ }
+
+ if sslPolicy.Id > 0 {
+ policyConfig, err := models.SharedSSLPolicyDAO.ComposePolicyConfig(nil, sslPolicy.Id, false, nil, nil)
+ if err != nil {
+ return nil, nil, fmt.Errorf("load ssl policy %d failed: %w", sslPolicy.Id, err)
+ }
+ if policyConfig != nil {
+ for _, cert := range policyConfig.Certs {
+ if cert == nil {
+ continue
+ }
+ if len(cert.CertData) > 0 && len(cert.KeyData) > 0 {
+ return cert.CertData, cert.KeyData, nil
+ }
+ }
+ }
+ }
+
+ return nil, nil, errors.New("cluster tls certificate is not configured")
+}
+
+func (q *HTTPDNSNodeQueue) parseSSHInfo(node *models.HTTPDNSNode) (string, int, int64, error) {
+ if node == nil {
+ return "", 0, 0, errors.New("node should not be nil")
+ }
+ if len(node.InstallStatus) == 0 {
+ return "", 0, 0, errors.New("ssh config should not be empty")
+ }
+
+ statusMap := maps.Map{}
+ err := json.Unmarshal(node.InstallStatus, &statusMap)
+ if err != nil {
+ return "", 0, 0, errors.New("invalid install status data")
+ }
+ sshMap := statusMap.GetMap("ssh")
+ if sshMap == nil {
+ return "", 0, 0, errors.New("ssh config should not be empty")
+ }
+
+ host := sshMap.GetString("host")
+ port := sshMap.GetInt("port")
+ grantId := sshMap.GetInt64("grantId")
+ if len(host) == 0 {
+ return "", 0, 0, errors.New("ssh host should not be empty")
+ }
+ if port <= 0 {
+ port = 22
+ }
+ if grantId <= 0 {
+ return "", 0, 0, errors.New("grant id should not be empty")
+ }
+ return host, port, grantId, nil
+}
diff --git a/EdgeAPI/internal/nodes/api_node.go b/EdgeAPI/internal/nodes/api_node.go
index c9ea2fc..e6b05e3 100644
--- a/EdgeAPI/internal/nodes/api_node.go
+++ b/EdgeAPI/internal/nodes/api_node.go
@@ -144,6 +144,17 @@ func (this *APINode) Start() {
this.processTableNames()
dbs.NotifyReady()
+ // 自动确保 ClickHouse 日志表存在(不阻断主流程)
+ this.setProgress("CLICKHOUSE", "正在检查 ClickHouse 日志表")
+ logs.Println("[API_NODE]ensuring clickhouse tables ...")
+ err = setup.EnsureClickHouseTables()
+ if err != nil {
+ logs.Println("[API_NODE]WARNING: ensure clickhouse tables failed: " + err.Error())
+ remotelogs.Error("API_NODE", "ensure clickhouse tables failed: "+err.Error())
+ } else {
+ logs.Println("[API_NODE]ensure clickhouse tables done")
+ }
+
// 设置时区
this.setProgress("TIMEZONE", "正在设置时区")
this.setupTimeZone()
diff --git a/EdgeAPI/internal/nodes/api_node_services.go b/EdgeAPI/internal/nodes/api_node_services.go
index be28ac3..b7df91a 100644
--- a/EdgeAPI/internal/nodes/api_node_services.go
+++ b/EdgeAPI/internal/nodes/api_node_services.go
@@ -5,6 +5,7 @@ package nodes
import (
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/clients"
+ httpdnsservices "github.com/TeaOSLab/EdgeAPI/internal/rpc/services/httpdns"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/users"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"google.golang.org/grpc"
@@ -425,6 +426,46 @@ func (this *APINode) registerServices(server *grpc.Server) {
pb.RegisterDNSTaskServiceServer(server, instance)
this.rest(instance)
}
+ {
+ var instance = this.serviceInstance(&httpdnsservices.HTTPDNSClusterService{}).(*httpdnsservices.HTTPDNSClusterService)
+ pb.RegisterHTTPDNSClusterServiceServer(server, instance)
+ this.rest(instance)
+ }
+ {
+ var instance = this.serviceInstance(&httpdnsservices.HTTPDNSNodeService{}).(*httpdnsservices.HTTPDNSNodeService)
+ pb.RegisterHTTPDNSNodeServiceServer(server, instance)
+ this.rest(instance)
+ }
+ {
+ var instance = this.serviceInstance(&httpdnsservices.HTTPDNSAppService{}).(*httpdnsservices.HTTPDNSAppService)
+ pb.RegisterHTTPDNSAppServiceServer(server, instance)
+ this.rest(instance)
+ }
+ {
+ var instance = this.serviceInstance(&httpdnsservices.HTTPDNSDomainService{}).(*httpdnsservices.HTTPDNSDomainService)
+ pb.RegisterHTTPDNSDomainServiceServer(server, instance)
+ this.rest(instance)
+ }
+ {
+ var instance = this.serviceInstance(&httpdnsservices.HTTPDNSRuleService{}).(*httpdnsservices.HTTPDNSRuleService)
+ pb.RegisterHTTPDNSRuleServiceServer(server, instance)
+ this.rest(instance)
+ }
+ {
+ var instance = this.serviceInstance(&httpdnsservices.HTTPDNSAccessLogService{}).(*httpdnsservices.HTTPDNSAccessLogService)
+ pb.RegisterHTTPDNSAccessLogServiceServer(server, instance)
+ this.rest(instance)
+ }
+ {
+ var instance = this.serviceInstance(&httpdnsservices.HTTPDNSRuntimeLogService{}).(*httpdnsservices.HTTPDNSRuntimeLogService)
+ pb.RegisterHTTPDNSRuntimeLogServiceServer(server, instance)
+ this.rest(instance)
+ }
+ {
+ var instance = this.serviceInstance(&httpdnsservices.HTTPDNSSandboxService{}).(*httpdnsservices.HTTPDNSSandboxService)
+ pb.RegisterHTTPDNSSandboxServiceServer(server, instance)
+ this.rest(instance)
+ }
{
var instance = this.serviceInstance(&services.NodeClusterFirewallActionService{}).(*services.NodeClusterFirewallActionService)
pb.RegisterNodeClusterFirewallActionServiceServer(server, instance)
diff --git a/EdgeAPI/internal/rpc/services/httpdns/converters.go b/EdgeAPI/internal/rpc/services/httpdns/converters.go
new file mode 100644
index 0000000..cd57a78
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/converters.go
@@ -0,0 +1,125 @@
+package httpdns
+
+import (
+ "github.com/TeaOSLab/EdgeAPI/internal/db/models"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+)
+
+func toPBCluster(cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
+ if cluster == nil {
+ return nil
+ }
+ return &pb.HTTPDNSCluster{
+ Id: int64(cluster.Id),
+ IsOn: cluster.IsOn,
+ IsDefault: cluster.IsDefault,
+ Name: cluster.Name,
+ ServiceDomain: cluster.ServiceDomain,
+ DefaultTTL: cluster.DefaultTTL,
+ FallbackTimeoutMs: cluster.FallbackTimeoutMs,
+ InstallDir: cluster.InstallDir,
+ TlsPolicyJSON: cluster.TLSPolicy,
+ CreatedAt: int64(cluster.CreatedAt),
+ UpdatedAt: int64(cluster.UpdatedAt),
+ }
+}
+
+func toPBNode(node *models.HTTPDNSNode) *pb.HTTPDNSNode {
+ if node == nil {
+ return nil
+ }
+ return &pb.HTTPDNSNode{
+ Id: int64(node.Id),
+ ClusterId: int64(node.ClusterId),
+ Name: node.Name,
+ IsOn: node.IsOn,
+ IsUp: node.IsUp,
+ IsInstalled: node.IsInstalled,
+ IsActive: node.IsActive,
+ UniqueId: node.UniqueId,
+ Secret: node.Secret,
+ InstallDir: node.InstallDir,
+ StatusJSON: node.Status,
+ InstallStatusJSON: node.InstallStatus,
+ CreatedAt: int64(node.CreatedAt),
+ UpdatedAt: int64(node.UpdatedAt),
+ }
+}
+
+func toPBApp(app *models.HTTPDNSApp, secret *models.HTTPDNSAppSecret) *pb.HTTPDNSApp {
+ if app == nil {
+ return nil
+ }
+ var signEnabled bool
+ var signSecret string
+ var signUpdatedAt int64
+ if secret != nil {
+ signEnabled = secret.SignEnabled
+ signSecret = secret.SignSecret
+ signUpdatedAt = int64(secret.SignUpdatedAt)
+ }
+ return &pb.HTTPDNSApp{
+ Id: int64(app.Id),
+ Name: app.Name,
+ AppId: app.AppId,
+ IsOn: app.IsOn,
+ PrimaryClusterId: int64(app.PrimaryClusterId),
+ BackupClusterId: int64(app.BackupClusterId),
+ SniMode: app.SNIMode,
+ SignEnabled: signEnabled,
+ SignSecret: signSecret,
+ SignUpdatedAt: signUpdatedAt,
+ CreatedAt: int64(app.CreatedAt),
+ UpdatedAt: int64(app.UpdatedAt),
+ UserId: int64(app.UserId),
+ }
+}
+
+func toPBDomain(domain *models.HTTPDNSDomain, ruleCount int64) *pb.HTTPDNSDomain {
+ if domain == nil {
+ return nil
+ }
+ return &pb.HTTPDNSDomain{
+ Id: int64(domain.Id),
+ AppId: int64(domain.AppId),
+ Domain: domain.Domain,
+ IsOn: domain.IsOn,
+ CreatedAt: int64(domain.CreatedAt),
+ UpdatedAt: int64(domain.UpdatedAt),
+ RuleCount: ruleCount,
+ }
+}
+
+func toPBRule(rule *models.HTTPDNSCustomRule, records []*models.HTTPDNSCustomRuleRecord) *pb.HTTPDNSCustomRule {
+ if rule == nil {
+ return nil
+ }
+ var pbRecords []*pb.HTTPDNSRuleRecord
+ for _, record := range records {
+ pbRecords = append(pbRecords, &pb.HTTPDNSRuleRecord{
+ Id: int64(record.Id),
+ RuleId: int64(record.RuleId),
+ RecordType: record.RecordType,
+ RecordValue: record.RecordValue,
+ Weight: record.Weight,
+ Sort: record.Sort,
+ })
+ }
+ return &pb.HTTPDNSCustomRule{
+ Id: int64(rule.Id),
+ AppId: int64(rule.AppId),
+ DomainId: int64(rule.DomainId),
+ RuleName: rule.RuleName,
+ LineScope: rule.LineScope,
+ LineCarrier: rule.LineCarrier,
+ LineRegion: rule.LineRegion,
+ LineProvince: rule.LineProvince,
+ LineContinent: rule.LineContinent,
+ LineCountry: rule.LineCountry,
+ Ttl: rule.TTL,
+ IsOn: rule.IsOn,
+ Priority: rule.Priority,
+ UpdatedAt: int64(rule.UpdatedAt),
+ Records: pbRecords,
+ }
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_access_log.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_access_log.go
new file mode 100644
index 0000000..6a415ef
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_access_log.go
@@ -0,0 +1,258 @@
+package httpdns
+
+import (
+ "context"
+ "log"
+ "strconv"
+ "time"
+
+ "github.com/TeaOSLab/EdgeAPI/internal/clickhouse"
+ "github.com/TeaOSLab/EdgeAPI/internal/db/models"
+ "github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ timeutil "github.com/iwind/TeaGo/utils/time"
+)
+
+type HTTPDNSAccessLogService struct {
+ services.BaseService
+ pb.UnimplementedHTTPDNSAccessLogServiceServer
+}
+
+func (s *HTTPDNSAccessLogService) CreateHTTPDNSAccessLogs(ctx context.Context, req *pb.CreateHTTPDNSAccessLogsRequest) (*pb.CreateHTTPDNSAccessLogsResponse, error) {
+ nodeIdInContext, err := s.ValidateHTTPDNSNode(ctx)
+ if err != nil {
+ _, err = s.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if len(req.GetLogs()) == 0 {
+ return &pb.CreateHTTPDNSAccessLogsResponse{}, nil
+ }
+
+ mysqlLogs := make([]*models.HTTPDNSAccessLog, 0, len(req.GetLogs()))
+ chLogs := make([]*pb.HTTPDNSAccessLog, 0, len(req.GetLogs()))
+ seen := map[string]struct{}{}
+ for _, item := range req.GetLogs() {
+ if item == nil {
+ continue
+ }
+ nodeId := item.GetNodeId()
+ // When called by HTTPDNS node, trust node id parsed from RPC token.
+ if nodeIdInContext > 0 {
+ nodeId = nodeIdInContext
+ }
+ clusterId := item.GetClusterId()
+ if clusterId <= 0 && nodeId > 0 {
+ clusterId, _ = models.SharedHTTPDNSNodeDAO.FindNodeClusterId(s.NullTx(), nodeId)
+ }
+
+ key := item.GetRequestId() + "#" + strconv.FormatInt(nodeId, 10)
+ if _, ok := seen[key]; ok {
+ continue
+ }
+ seen[key] = struct{}{}
+
+ createdAt := item.GetCreatedAt()
+ if createdAt <= 0 {
+ createdAt = time.Now().Unix()
+ }
+ day := item.GetDay()
+ if len(day) == 0 {
+ day = timeutil.Format("Ymd")
+ }
+
+ mysqlLogs = append(mysqlLogs, &models.HTTPDNSAccessLog{
+ RequestId: item.GetRequestId(),
+ ClusterId: uint32(clusterId),
+ NodeId: uint32(nodeId),
+ AppId: item.GetAppId(),
+ AppName: item.GetAppName(),
+ Domain: item.GetDomain(),
+ QType: item.GetQtype(),
+ ClientIP: item.GetClientIP(),
+ ClientRegion: item.GetClientRegion(),
+ Carrier: item.GetCarrier(),
+ SDKVersion: item.GetSdkVersion(),
+ OS: item.GetOs(),
+ ResultIPs: item.GetResultIPs(),
+ Status: item.GetStatus(),
+ ErrorCode: item.GetErrorCode(),
+ CostMs: item.GetCostMs(),
+ CreatedAt: uint64(createdAt),
+ Day: day,
+ Summary: item.GetSummary(),
+ })
+
+ chLogs = append(chLogs, &pb.HTTPDNSAccessLog{
+ RequestId: item.GetRequestId(),
+ ClusterId: clusterId,
+ NodeId: nodeId,
+ AppId: item.GetAppId(),
+ AppName: item.GetAppName(),
+ Domain: item.GetDomain(),
+ Qtype: item.GetQtype(),
+ ClientIP: item.GetClientIP(),
+ ClientRegion: item.GetClientRegion(),
+ Carrier: item.GetCarrier(),
+ SdkVersion: item.GetSdkVersion(),
+ Os: item.GetOs(),
+ ResultIPs: item.GetResultIPs(),
+ Status: item.GetStatus(),
+ ErrorCode: item.GetErrorCode(),
+ CostMs: item.GetCostMs(),
+ CreatedAt: createdAt,
+ Day: day,
+ Summary: item.GetSummary(),
+ })
+ }
+
+ if s.canWriteHTTPDNSAccessLogsToMySQL() {
+ for _, item := range mysqlLogs {
+ err = models.SharedHTTPDNSAccessLogDAO.CreateLog(s.NullTx(), item)
+ if err != nil {
+ if models.CheckSQLDuplicateErr(err) {
+ continue
+ }
+ return nil, err
+ }
+ }
+ }
+
+ store := clickhouse.NewHTTPDNSAccessLogsStore()
+ if s.canWriteHTTPDNSAccessLogsToClickHouse() && store.Client().IsConfigured() && len(chLogs) > 0 {
+ err = store.Insert(ctx, chLogs)
+ if err != nil {
+ log.Println("[HTTPDNS_ACCESS_LOG]write clickhouse failed, keep mysql success:", err.Error())
+ }
+ }
+
+ return &pb.CreateHTTPDNSAccessLogsResponse{}, nil
+}
+
+func (s *HTTPDNSAccessLogService) ListHTTPDNSAccessLogs(ctx context.Context, req *pb.ListHTTPDNSAccessLogsRequest) (*pb.ListHTTPDNSAccessLogsResponse, error) {
+ _, _, err := s.ValidateAdminAndUser(ctx, true)
+ if err != nil {
+ return nil, err
+ }
+
+ store := clickhouse.NewHTTPDNSAccessLogsStore()
+ canReadFromClickHouse := s.shouldReadHTTPDNSAccessLogsFromClickHouse() && store.Client().IsConfigured()
+ canReadFromMySQL := s.shouldReadHTTPDNSAccessLogsFromMySQL()
+ if canReadFromClickHouse {
+ resp, listErr := s.listFromClickHouse(ctx, store, req)
+ if listErr == nil {
+ return resp, nil
+ }
+ log.Println("[HTTPDNS_ACCESS_LOG]read clickhouse failed, fallback mysql:", listErr.Error())
+ if !canReadFromMySQL {
+ return nil, listErr
+ }
+ }
+
+ if !canReadFromMySQL {
+ return &pb.ListHTTPDNSAccessLogsResponse{
+ Logs: []*pb.HTTPDNSAccessLog{},
+ Total: 0,
+ }, nil
+ }
+
+ total, err := models.SharedHTTPDNSAccessLogDAO.CountLogs(s.NullTx(), req.GetDay(), req.GetClusterId(), req.GetNodeId(), req.GetAppId(), req.GetDomain(), req.GetStatus(), req.GetKeyword())
+ if err != nil {
+ return nil, err
+ }
+ logs, err := models.SharedHTTPDNSAccessLogDAO.ListLogs(s.NullTx(), req.GetDay(), req.GetClusterId(), req.GetNodeId(), req.GetAppId(), req.GetDomain(), req.GetStatus(), req.GetKeyword(), req.GetOffset(), req.GetSize())
+ if err != nil {
+ return nil, err
+ }
+
+ result := make([]*pb.HTTPDNSAccessLog, 0, len(logs))
+ for _, item := range logs {
+ if item == nil {
+ continue
+ }
+
+ clusterName, _ := models.SharedHTTPDNSClusterDAO.FindEnabledClusterName(s.NullTx(), int64(item.ClusterId))
+ nodeName := ""
+ node, _ := models.SharedHTTPDNSNodeDAO.FindEnabledNode(s.NullTx(), int64(item.NodeId))
+ if node != nil {
+ nodeName = node.Name
+ }
+
+ result = append(result, &pb.HTTPDNSAccessLog{
+ Id: int64(item.Id),
+ RequestId: item.RequestId,
+ ClusterId: int64(item.ClusterId),
+ NodeId: int64(item.NodeId),
+ AppId: item.AppId,
+ AppName: item.AppName,
+ Domain: item.Domain,
+ Qtype: item.QType,
+ ClientIP: item.ClientIP,
+ ClientRegion: item.ClientRegion,
+ Carrier: item.Carrier,
+ SdkVersion: item.SDKVersion,
+ Os: item.OS,
+ ResultIPs: item.ResultIPs,
+ Status: item.Status,
+ ErrorCode: item.ErrorCode,
+ CostMs: item.CostMs,
+ CreatedAt: int64(item.CreatedAt),
+ Day: item.Day,
+ Summary: item.Summary,
+ NodeName: nodeName,
+ ClusterName: clusterName,
+ })
+ }
+
+ return &pb.ListHTTPDNSAccessLogsResponse{
+ Logs: result,
+ Total: total,
+ }, nil
+}
+
+func (s *HTTPDNSAccessLogService) listFromClickHouse(ctx context.Context, store *clickhouse.HTTPDNSAccessLogsStore, req *pb.ListHTTPDNSAccessLogsRequest) (*pb.ListHTTPDNSAccessLogsResponse, error) {
+ filter := clickhouse.HTTPDNSAccessLogListFilter{
+ Day: req.GetDay(),
+ ClusterId: req.GetClusterId(),
+ NodeId: req.GetNodeId(),
+ AppId: req.GetAppId(),
+ Domain: req.GetDomain(),
+ Status: req.GetStatus(),
+ Keyword: req.GetKeyword(),
+ Offset: req.GetOffset(),
+ Size: req.GetSize(),
+ }
+
+ total, err := store.Count(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ rows, err := store.List(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+
+ result := make([]*pb.HTTPDNSAccessLog, 0, len(rows))
+ for _, row := range rows {
+ item := clickhouse.HTTPDNSRowToPB(row)
+ if item == nil {
+ continue
+ }
+ clusterName, _ := models.SharedHTTPDNSClusterDAO.FindEnabledClusterName(s.NullTx(), item.GetClusterId())
+ nodeName := ""
+ node, _ := models.SharedHTTPDNSNodeDAO.FindEnabledNode(s.NullTx(), item.GetNodeId())
+ if node != nil {
+ nodeName = node.Name
+ }
+ item.ClusterName = clusterName
+ item.NodeName = nodeName
+ result = append(result, item)
+ }
+
+ return &pb.ListHTTPDNSAccessLogsResponse{
+ Logs: result,
+ Total: total,
+ }, nil
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_access_log_ext.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_access_log_ext.go
new file mode 100644
index 0000000..8a2c01d
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_access_log_ext.go
@@ -0,0 +1,19 @@
+//go:build !plus
+
+package httpdns
+
+func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToMySQL() bool {
+ return true
+}
+
+func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToClickHouse() bool {
+ return true
+}
+
+func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromClickHouse() bool {
+ return true
+}
+
+func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromMySQL() bool {
+ return true
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_access_log_ext_plus.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_access_log_ext_plus.go
new file mode 100644
index 0000000..cda9276
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_access_log_ext_plus.go
@@ -0,0 +1,124 @@
+//go:build plus
+
+package httpdns
+
+import (
+ "log"
+ "sync"
+ "time"
+
+ "github.com/TeaOSLab/EdgeAPI/internal/db/models"
+ "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
+)
+
+var (
+ httpDNSAccessLogWriteTargetsLocker sync.RWMutex
+ httpDNSAccessLogWriteTargetsCache = &serverconfigs.AccessLogWriteTargets{
+ File: true,
+ MySQL: true,
+ ClickHouse: false,
+ }
+ httpDNSAccessLogWriteTargetsExpireAt int64
+)
+
+const httpDNSAccessLogWriteTargetsCacheTTL = 10 * time.Second
+
+func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToMySQL() bool {
+ targets := s.readHTTPDNSAccessLogWriteTargets()
+ if targets == nil {
+ return true
+ }
+ return targets.MySQL
+}
+
+func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToClickHouse() bool {
+ targets := s.readHTTPDNSAccessLogWriteTargets()
+ if targets == nil {
+ return false
+ }
+ return targets.ClickHouse
+}
+
+func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromClickHouse() bool {
+ targets := s.readHTTPDNSAccessLogWriteTargets()
+ if targets == nil {
+ return false
+ }
+ return targets.ClickHouse
+}
+
+func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromMySQL() bool {
+ targets := s.readHTTPDNSAccessLogWriteTargets()
+ if targets == nil {
+ return true
+ }
+ return targets.MySQL
+}
+
+func (s *HTTPDNSAccessLogService) readHTTPDNSAccessLogWriteTargets() *serverconfigs.AccessLogWriteTargets {
+ now := time.Now().Unix()
+
+ httpDNSAccessLogWriteTargetsLocker.RLock()
+ if now < httpDNSAccessLogWriteTargetsExpireAt && httpDNSAccessLogWriteTargetsCache != nil {
+ targets := *httpDNSAccessLogWriteTargetsCache
+ httpDNSAccessLogWriteTargetsLocker.RUnlock()
+ return &targets
+ }
+ httpDNSAccessLogWriteTargetsLocker.RUnlock()
+
+ httpDNSAccessLogWriteTargetsLocker.Lock()
+ defer httpDNSAccessLogWriteTargetsLocker.Unlock()
+
+ // double-check
+ now = time.Now().Unix()
+ if now < httpDNSAccessLogWriteTargetsExpireAt && httpDNSAccessLogWriteTargetsCache != nil {
+ targets := *httpDNSAccessLogWriteTargetsCache
+ return &targets
+ }
+
+ targets := s.loadHTTPDNSAccessLogWriteTargetsFromPolicy()
+ if targets == nil {
+ targets = &serverconfigs.AccessLogWriteTargets{
+ File: true,
+ MySQL: true,
+ ClickHouse: false,
+ }
+ }
+
+ httpDNSAccessLogWriteTargetsCache = targets
+ httpDNSAccessLogWriteTargetsExpireAt = time.Now().Add(httpDNSAccessLogWriteTargetsCacheTTL).Unix()
+
+ copyTargets := *targets
+ return ©Targets
+}
+
+func (s *HTTPDNSAccessLogService) loadHTTPDNSAccessLogWriteTargetsFromPolicy() *serverconfigs.AccessLogWriteTargets {
+ tx := s.NullTx()
+ publicPolicyId, err := models.SharedHTTPAccessLogPolicyDAO.FindCurrentPublicPolicyId(tx)
+ if err != nil {
+ log.Println("[HTTPDNS_ACCESS_LOG]load public access log policy failed:", err.Error())
+ return nil
+ }
+ if publicPolicyId <= 0 {
+ return &serverconfigs.AccessLogWriteTargets{
+ File: true,
+ MySQL: true,
+ ClickHouse: false,
+ }
+ }
+
+ policy, err := models.SharedHTTPAccessLogPolicyDAO.FindEnabledHTTPAccessLogPolicy(tx, publicPolicyId)
+ if err != nil {
+ log.Println("[HTTPDNS_ACCESS_LOG]load access log policy detail failed:", err.Error())
+ return nil
+ }
+ if policy == nil {
+ return &serverconfigs.AccessLogWriteTargets{
+ File: true,
+ MySQL: true,
+ ClickHouse: false,
+ }
+ }
+
+ return serverconfigs.ParseWriteTargetsFromPolicy(policy.WriteTargets, policy.Type, policy.DisableDefaultDB)
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_app.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_app.go
new file mode 100644
index 0000000..41c238e
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_app.go
@@ -0,0 +1,246 @@
+package httpdns
+
+import (
+ "context"
+ "errors"
+ "github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ "github.com/iwind/TeaGo/dbs"
+ "strings"
+
+ "github.com/TeaOSLab/EdgeAPI/internal/db/models"
+)
+
+// HTTPDNSAppService HTTPDNS应用服务
+type HTTPDNSAppService struct {
+ services.BaseService
+ pb.UnimplementedHTTPDNSAppServiceServer
+}
+
+func (this *HTTPDNSAppService) CreateHTTPDNSApp(ctx context.Context, req *pb.CreateHTTPDNSAppRequest) (*pb.CreateHTTPDNSAppResponse, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if len(req.Name) == 0 || len(req.AppId) == 0 {
+ return nil, errors.New("required 'name' and 'appId'")
+ }
+ if req.PrimaryClusterId <= 0 {
+ return nil, errors.New("required 'primaryClusterId'")
+ }
+ var appDbId int64
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ exists, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(tx, strings.TrimSpace(req.AppId))
+ if err != nil {
+ return err
+ }
+ if exists != nil {
+ return errors.New("appId already exists")
+ }
+
+ appDbId, err = models.SharedHTTPDNSAppDAO.CreateApp(tx, req.Name, strings.TrimSpace(req.AppId), req.PrimaryClusterId, req.BackupClusterId, req.IsOn, req.UserId)
+ if err != nil {
+ return err
+ }
+ _, _, err = models.SharedHTTPDNSAppSecretDAO.InitAppSecret(tx, appDbId, req.SignEnabled)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, appDbId, models.HTTPDNSNodeTaskTypeAppChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &pb.CreateHTTPDNSAppResponse{AppDbId: appDbId}, nil
+}
+
+func (this *HTTPDNSAppService) UpdateHTTPDNSApp(ctx context.Context, req *pb.UpdateHTTPDNSAppRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ oldApp, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(tx, req.AppDbId)
+ if err != nil {
+ return err
+ }
+ if oldApp == nil {
+ return errors.New("app not found")
+ }
+
+ err = models.SharedHTTPDNSAppDAO.UpdateApp(tx, req.AppDbId, req.Name, req.PrimaryClusterId, req.BackupClusterId, req.IsOn, req.UserId)
+ if err != nil {
+ return err
+ }
+
+ err = notifyHTTPDNSAppTasksByApp(tx, oldApp, models.HTTPDNSNodeTaskTypeAppChanged)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeAppChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSAppService) DeleteHTTPDNSApp(ctx context.Context, req *pb.DeleteHTTPDNSAppRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ app, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(tx, req.AppDbId)
+ if err != nil {
+ return err
+ }
+ if app == nil {
+ return nil
+ }
+
+ // 1) 先停用规则记录
+ rules, err := models.SharedHTTPDNSCustomRuleDAO.ListEnabledRulesWithAppId(tx, req.AppDbId)
+ if err != nil {
+ return err
+ }
+ for _, rule := range rules {
+ err = models.SharedHTTPDNSCustomRuleRecordDAO.DisableRecordsWithRuleId(tx, int64(rule.Id))
+ if err != nil {
+ return err
+ }
+ }
+
+ // 2) 停用规则、域名、密钥
+ err = models.SharedHTTPDNSCustomRuleDAO.DisableRulesWithAppId(tx, req.AppDbId)
+ if err != nil {
+ return err
+ }
+ err = models.SharedHTTPDNSDomainDAO.DisableDomainsWithAppId(tx, req.AppDbId)
+ if err != nil {
+ return err
+ }
+ err = models.SharedHTTPDNSAppSecretDAO.DisableAppSecret(tx, req.AppDbId)
+ if err != nil {
+ return err
+ }
+
+ // 3) 删除该应用的 MySQL 访问日志,避免残留
+ err = models.SharedHTTPDNSAccessLogDAO.DeleteLogsWithAppId(tx, app.AppId)
+ if err != nil {
+ return err
+ }
+
+ // 4) 最后停用应用
+ err = models.SharedHTTPDNSAppDAO.DisableApp(tx, req.AppDbId)
+ if err != nil {
+ return err
+ }
+
+ return notifyHTTPDNSAppTasksByApp(tx, app, models.HTTPDNSNodeTaskTypeAppChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSAppService) FindHTTPDNSApp(ctx context.Context, req *pb.FindHTTPDNSAppRequest) (*pb.FindHTTPDNSAppResponse, error) {
+ _, _, err := this.ValidateAdminAndUser(ctx, true)
+ if err != nil {
+ return nil, err
+ }
+ app, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(this.NullTx(), req.AppDbId)
+ if err != nil {
+ return nil, err
+ }
+ secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), req.AppDbId)
+ if err != nil {
+ return nil, err
+ }
+ return &pb.FindHTTPDNSAppResponse{App: toPBApp(app, secret)}, nil
+}
+
+func (this *HTTPDNSAppService) ListHTTPDNSApps(ctx context.Context, req *pb.ListHTTPDNSAppsRequest) (*pb.ListHTTPDNSAppsResponse, error) {
+ _, _, err := this.ValidateAdminAndUser(ctx, true)
+ if err != nil {
+ return nil, err
+ }
+ apps, err := models.SharedHTTPDNSAppDAO.ListEnabledApps(this.NullTx(), req.Offset, req.Size, req.Keyword)
+ if err != nil {
+ return nil, err
+ }
+ var pbApps []*pb.HTTPDNSApp
+ for _, app := range apps {
+ secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
+ if err != nil {
+ return nil, err
+ }
+ pbApps = append(pbApps, toPBApp(app, secret))
+ }
+ return &pb.ListHTTPDNSAppsResponse{Apps: pbApps}, nil
+}
+
+func (this *HTTPDNSAppService) FindAllHTTPDNSApps(ctx context.Context, req *pb.FindAllHTTPDNSAppsRequest) (*pb.FindAllHTTPDNSAppsResponse, error) {
+ _, _, validateErr := this.ValidateAdminAndUser(ctx, true)
+ if validateErr != nil {
+ if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
+ return nil, validateErr
+ }
+ }
+ apps, err := models.SharedHTTPDNSAppDAO.FindAllEnabledApps(this.NullTx())
+ if err != nil {
+ return nil, err
+ }
+ var pbApps []*pb.HTTPDNSApp
+ for _, app := range apps {
+ secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
+ if err != nil {
+ return nil, err
+ }
+ pbApps = append(pbApps, toPBApp(app, secret))
+ }
+ return &pb.FindAllHTTPDNSAppsResponse{Apps: pbApps}, nil
+}
+
+func (this *HTTPDNSAppService) UpdateHTTPDNSAppSignEnabled(ctx context.Context, req *pb.UpdateHTTPDNSAppSignEnabledRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ err := models.SharedHTTPDNSAppSecretDAO.UpdateSignEnabled(tx, req.AppDbId, req.SignEnabled)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeAppChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSAppService) ResetHTTPDNSAppSignSecret(ctx context.Context, req *pb.ResetHTTPDNSAppSignSecretRequest) (*pb.ResetHTTPDNSAppSignSecretResponse, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var signSecret string
+ var updatedAt int64
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ var err error
+ signSecret, updatedAt, err = models.SharedHTTPDNSAppSecretDAO.ResetSignSecret(tx, req.AppDbId)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeAppChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &pb.ResetHTTPDNSAppSignSecretResponse{
+ SignSecret: signSecret,
+ UpdatedAt: updatedAt,
+ }, nil
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_cluster.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_cluster.go
new file mode 100644
index 0000000..fda859d
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_cluster.go
@@ -0,0 +1,170 @@
+package httpdns
+
+import (
+ "context"
+ "errors"
+ "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"
+)
+
+// HTTPDNSClusterService HTTPDNS集群服务
+type HTTPDNSClusterService struct {
+ services.BaseService
+ pb.UnimplementedHTTPDNSClusterServiceServer
+}
+
+func (this *HTTPDNSClusterService) CreateHTTPDNSCluster(ctx context.Context, req *pb.CreateHTTPDNSClusterRequest) (*pb.CreateHTTPDNSClusterResponse, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if len(req.Name) == 0 {
+ return nil, errors.New("required 'name'")
+ }
+ 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)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSClusterTask(tx, clusterId, models.HTTPDNSNodeTaskTypeConfigChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &pb.CreateHTTPDNSClusterResponse{ClusterId: clusterId}, nil
+}
+
+func (this *HTTPDNSClusterService) UpdateHTTPDNSCluster(ctx context.Context, req *pb.UpdateHTTPDNSClusterRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ 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)
+ if err != nil {
+ return err
+ }
+ taskType := models.HTTPDNSNodeTaskTypeConfigChanged
+ if len(req.TlsPolicyJSON) > 0 {
+ taskType = models.HTTPDNSNodeTaskTypeTLSChanged
+ }
+ return notifyHTTPDNSClusterTask(tx, req.ClusterId, taskType)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSClusterService) DeleteHTTPDNSCluster(ctx context.Context, req *pb.DeleteHTTPDNSClusterRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ err = models.SharedHTTPDNSClusterDAO.DisableCluster(tx, req.ClusterId)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSClusterTask(tx, req.ClusterId, models.HTTPDNSNodeTaskTypeConfigChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSClusterService) FindHTTPDNSCluster(ctx context.Context, req *pb.FindHTTPDNSClusterRequest) (*pb.FindHTTPDNSClusterResponse, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(this.NullTx(), req.ClusterId)
+ if err != nil {
+ return nil, err
+ }
+ return &pb.FindHTTPDNSClusterResponse{Cluster: toPBCluster(cluster)}, nil
+}
+
+func (this *HTTPDNSClusterService) ListHTTPDNSClusters(ctx context.Context, req *pb.ListHTTPDNSClustersRequest) (*pb.ListHTTPDNSClustersResponse, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ clusters, err := models.SharedHTTPDNSClusterDAO.ListEnabledClusters(this.NullTx(), req.Offset, req.Size, req.Keyword)
+ if err != nil {
+ return nil, err
+ }
+ var pbClusters []*pb.HTTPDNSCluster
+ for _, cluster := range clusters {
+ pbClusters = append(pbClusters, toPBCluster(cluster))
+ }
+ return &pb.ListHTTPDNSClustersResponse{Clusters: pbClusters}, nil
+}
+
+func (this *HTTPDNSClusterService) FindAllHTTPDNSClusters(ctx context.Context, req *pb.FindAllHTTPDNSClustersRequest) (*pb.FindAllHTTPDNSClustersResponse, error) {
+ _, _, validateErr := this.ValidateAdminAndUser(ctx, true)
+ if validateErr != nil {
+ if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
+ return nil, validateErr
+ }
+ }
+ clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(this.NullTx())
+ if err != nil {
+ return nil, err
+ }
+ var pbClusters []*pb.HTTPDNSCluster
+ for _, cluster := range clusters {
+ pbClusters = append(pbClusters, toPBCluster(cluster))
+ }
+ return &pb.FindAllHTTPDNSClustersResponse{Clusters: pbClusters}, nil
+}
+
+func (this *HTTPDNSClusterService) UpdateHTTPDNSClusterDefault(ctx context.Context, req *pb.UpdateHTTPDNSClusterDefaultRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ err = models.SharedHTTPDNSClusterDAO.UpdateDefaultCluster(tx, req.ClusterId)
+ if err != nil {
+ return err
+ }
+
+ clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(tx)
+ if err != nil {
+ return err
+ }
+
+ for _, cluster := range clusters {
+ err = notifyHTTPDNSClusterTask(tx, int64(cluster.Id), models.HTTPDNSNodeTaskTypeConfigChanged)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSClusterService) ListHTTPDNSNodesWithClusterId(ctx context.Context, req *pb.ListHTTPDNSNodesWithClusterIdRequest) (*pb.ListHTTPDNSNodesWithClusterIdResponse, error) {
+ _, _, err := this.ValidateAdminAndUser(ctx, true)
+ if err != nil {
+ return nil, err
+ }
+ nodes, err := models.SharedHTTPDNSNodeDAO.ListEnabledNodes(this.NullTx(), req.ClusterId)
+ if err != nil {
+ return nil, err
+ }
+ var pbNodes []*pb.HTTPDNSNode
+ for _, node := range nodes {
+ pbNodes = append(pbNodes, toPBNode(node))
+ }
+ return &pb.ListHTTPDNSNodesWithClusterIdResponse{Nodes: pbNodes}, nil
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_domain.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_domain.go
new file mode 100644
index 0000000..1a43d0d
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_domain.go
@@ -0,0 +1,112 @@
+package httpdns
+
+import (
+ "context"
+ "errors"
+ "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"
+)
+
+// HTTPDNSDomainService HTTPDNS域名服务
+type HTTPDNSDomainService struct {
+ services.BaseService
+ pb.UnimplementedHTTPDNSDomainServiceServer
+}
+
+func (this *HTTPDNSDomainService) CreateHTTPDNSDomain(ctx context.Context, req *pb.CreateHTTPDNSDomainRequest) (*pb.CreateHTTPDNSDomainResponse, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if req.AppDbId <= 0 || len(req.Domain) == 0 {
+ return nil, errors.New("required 'appDbId' and 'domain'")
+ }
+ var domainId int64
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ domainId, err = models.SharedHTTPDNSDomainDAO.CreateDomain(tx, req.AppDbId, req.Domain, req.IsOn)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeDomainChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &pb.CreateHTTPDNSDomainResponse{DomainId: domainId}, nil
+}
+
+func (this *HTTPDNSDomainService) DeleteHTTPDNSDomain(ctx context.Context, req *pb.DeleteHTTPDNSDomainRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ domain, err := models.SharedHTTPDNSDomainDAO.FindEnabledDomain(tx, req.DomainId)
+ if err != nil {
+ return err
+ }
+ if domain == nil {
+ return nil
+ }
+
+ err = models.SharedHTTPDNSDomainDAO.DisableDomain(tx, req.DomainId)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, int64(domain.AppId), models.HTTPDNSNodeTaskTypeDomainChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSDomainService) UpdateHTTPDNSDomainStatus(ctx context.Context, req *pb.UpdateHTTPDNSDomainStatusRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ domain, err := models.SharedHTTPDNSDomainDAO.FindEnabledDomain(tx, req.DomainId)
+ if err != nil {
+ return err
+ }
+ if domain == nil {
+ return nil
+ }
+
+ err = models.SharedHTTPDNSDomainDAO.UpdateDomainStatus(tx, req.DomainId, req.IsOn)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, int64(domain.AppId), models.HTTPDNSNodeTaskTypeDomainChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSDomainService) ListHTTPDNSDomainsWithAppId(ctx context.Context, req *pb.ListHTTPDNSDomainsWithAppIdRequest) (*pb.ListHTTPDNSDomainsWithAppIdResponse, error) {
+ _, _, validateErr := this.ValidateAdminAndUser(ctx, true)
+ if validateErr != nil {
+ if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
+ return nil, validateErr
+ }
+ }
+ domains, err := models.SharedHTTPDNSDomainDAO.ListEnabledDomainsWithAppId(this.NullTx(), req.AppDbId, req.Keyword)
+ if err != nil {
+ return nil, err
+ }
+ var pbDomains []*pb.HTTPDNSDomain
+ for _, domain := range domains {
+ ruleCount, err := models.SharedHTTPDNSCustomRuleDAO.CountEnabledRulesWithDomainId(this.NullTx(), int64(domain.Id))
+ if err != nil {
+ return nil, err
+ }
+ pbDomains = append(pbDomains, toPBDomain(domain, ruleCount))
+ }
+ return &pb.ListHTTPDNSDomainsWithAppIdResponse{Domains: pbDomains}, nil
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_node.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_node.go
new file mode 100644
index 0000000..463159b
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_node.go
@@ -0,0 +1,185 @@
+package httpdns
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "github.com/TeaOSLab/EdgeAPI/internal/db/models"
+ "github.com/TeaOSLab/EdgeAPI/internal/goman"
+ "github.com/TeaOSLab/EdgeAPI/internal/installers"
+ "github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ "github.com/iwind/TeaGo/dbs"
+ "github.com/iwind/TeaGo/logs"
+)
+
+// HTTPDNSNodeService HTTPDNS节点服务
+type HTTPDNSNodeService struct {
+ services.BaseService
+ pb.UnimplementedHTTPDNSNodeServiceServer
+}
+
+func (this *HTTPDNSNodeService) CreateHTTPDNSNode(ctx context.Context, req *pb.CreateHTTPDNSNodeRequest) (*pb.CreateHTTPDNSNodeResponse, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if req.ClusterId <= 0 {
+ return nil, errors.New("required 'clusterId'")
+ }
+ var nodeId int64
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ nodeId, err = models.SharedHTTPDNSNodeDAO.CreateNode(tx, req.ClusterId, req.Name, req.InstallDir, req.IsOn)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSClusterTask(tx, req.ClusterId, models.HTTPDNSNodeTaskTypeConfigChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &pb.CreateHTTPDNSNodeResponse{NodeId: nodeId}, nil
+}
+
+func (this *HTTPDNSNodeService) UpdateHTTPDNSNode(ctx context.Context, req *pb.UpdateHTTPDNSNodeRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, req.NodeId)
+ if err != nil {
+ return err
+ }
+ if node == nil {
+ return errors.New("node not found")
+ }
+
+ err = models.SharedHTTPDNSNodeDAO.UpdateNode(tx, req.NodeId, req.Name, req.InstallDir, req.IsOn)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSClusterTask(tx, int64(node.ClusterId), models.HTTPDNSNodeTaskTypeConfigChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSNodeService) DeleteHTTPDNSNode(ctx context.Context, req *pb.DeleteHTTPDNSNodeRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, req.NodeId)
+ if err != nil {
+ return err
+ }
+ if node == nil {
+ return nil
+ }
+
+ err = models.SharedHTTPDNSNodeDAO.DisableNode(tx, req.NodeId)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSClusterTask(tx, int64(node.ClusterId), models.HTTPDNSNodeTaskTypeConfigChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSNodeService) FindHTTPDNSNode(ctx context.Context, req *pb.FindHTTPDNSNodeRequest) (*pb.FindHTTPDNSNodeResponse, error) {
+ nodeId := req.NodeId
+ if nodeId <= 0 {
+ parsedNodeId, nodeErr := this.ValidateHTTPDNSNode(ctx)
+ if nodeErr != nil {
+ return nil, errors.New("invalid 'nodeId'")
+ }
+ nodeId = parsedNodeId
+ } else {
+ _, _, validateErr := this.ValidateAdminAndUser(ctx, true)
+ if validateErr != nil {
+ if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
+ return nil, validateErr
+ }
+ }
+ }
+
+ node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(this.NullTx(), nodeId)
+ if err != nil {
+ return nil, err
+ }
+ return &pb.FindHTTPDNSNodeResponse{Node: toPBNode(node)}, nil
+}
+
+func (this *HTTPDNSNodeService) ListHTTPDNSNodes(ctx context.Context, req *pb.ListHTTPDNSNodesRequest) (*pb.ListHTTPDNSNodesResponse, error) {
+ _, _, err := this.ValidateAdminAndUser(ctx, true)
+ if err != nil {
+ return nil, err
+ }
+ nodes, err := models.SharedHTTPDNSNodeDAO.ListEnabledNodes(this.NullTx(), req.ClusterId)
+ if err != nil {
+ return nil, err
+ }
+ var pbNodes []*pb.HTTPDNSNode
+ for _, node := range nodes {
+ pbNodes = append(pbNodes, toPBNode(node))
+ }
+ return &pb.ListHTTPDNSNodesResponse{Nodes: pbNodes}, nil
+}
+
+func (this *HTTPDNSNodeService) UpdateHTTPDNSNodeStatus(ctx context.Context, req *pb.UpdateHTTPDNSNodeStatusRequest) (*pb.RPCSuccess, error) {
+ nodeId := req.GetNodeId()
+ isAdminCaller := false
+ if nodeId > 0 {
+ if _, adminErr := this.ValidateAdmin(ctx); adminErr == nil {
+ isAdminCaller = true
+ }
+ }
+ if !isAdminCaller {
+ if nodeId <= 0 {
+ parsedNodeId, err := this.ValidateHTTPDNSNode(ctx)
+ if err != nil {
+ return nil, err
+ }
+ nodeId = parsedNodeId
+ }
+ }
+ if nodeId <= 0 {
+ return nil, errors.New("invalid 'nodeId'")
+ }
+
+ err := models.SharedHTTPDNSNodeDAO.UpdateNodeStatus(this.NullTx(), nodeId, req.GetIsUp(), req.GetIsInstalled(), req.GetIsActive(), req.GetStatusJSON(), req.GetInstallStatusJSON())
+ if err != nil {
+ return nil, err
+ }
+
+ if isAdminCaller && shouldTriggerHTTPDNSInstall(req.GetInstallStatusJSON()) {
+ goman.New(func() {
+ installErr := installers.SharedHTTPDNSNodeQueue().InstallNodeProcess(nodeId, false)
+ if installErr != nil {
+ logs.Println("[RPC][HTTPDNS]install node failed:", installErr.Error())
+ }
+ })
+ }
+
+ return this.Success()
+}
+
+func shouldTriggerHTTPDNSInstall(installStatusJSON []byte) bool {
+ if len(installStatusJSON) == 0 {
+ return false
+ }
+
+ installStatus := &models.NodeInstallStatus{}
+ err := json.Unmarshal(installStatusJSON, installStatus)
+ if err != nil {
+ return false
+ }
+ return installStatus.IsRunning && !installStatus.IsFinished
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_rule.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_rule.go
new file mode 100644
index 0000000..a8bbd0c
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_rule.go
@@ -0,0 +1,193 @@
+package httpdns
+
+import (
+ "context"
+ "errors"
+ "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"
+)
+
+// HTTPDNSRuleService HTTPDNS规则服务
+type HTTPDNSRuleService struct {
+ services.BaseService
+ pb.UnimplementedHTTPDNSRuleServiceServer
+}
+
+func (this *HTTPDNSRuleService) CreateHTTPDNSCustomRule(ctx context.Context, req *pb.CreateHTTPDNSCustomRuleRequest) (*pb.CreateHTTPDNSCustomRuleResponse, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if req.Rule == nil {
+ return nil, errors.New("required 'rule'")
+ }
+ var ruleId int64
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ rule := &models.HTTPDNSCustomRule{
+ AppId: uint32(req.Rule.AppId),
+ DomainId: uint32(req.Rule.DomainId),
+ RuleName: req.Rule.RuleName,
+ LineScope: req.Rule.LineScope,
+ LineCarrier: req.Rule.LineCarrier,
+ LineRegion: req.Rule.LineRegion,
+ LineProvince: req.Rule.LineProvince,
+ LineContinent: req.Rule.LineContinent,
+ LineCountry: req.Rule.LineCountry,
+ TTL: req.Rule.Ttl,
+ IsOn: req.Rule.IsOn,
+ Priority: req.Rule.Priority,
+ }
+ ruleId, err = models.SharedHTTPDNSCustomRuleDAO.CreateRule(tx, rule)
+ if err != nil {
+ return err
+ }
+ for _, record := range req.Rule.Records {
+ _, err := models.SharedHTTPDNSCustomRuleRecordDAO.CreateRecord(tx, ruleId, record.RecordType, record.RecordValue, record.Weight, record.Sort)
+ if err != nil {
+ return err
+ }
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, req.Rule.AppId, models.HTTPDNSNodeTaskTypeRuleChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &pb.CreateHTTPDNSCustomRuleResponse{RuleId: ruleId}, nil
+}
+
+func (this *HTTPDNSRuleService) UpdateHTTPDNSCustomRule(ctx context.Context, req *pb.UpdateHTTPDNSCustomRuleRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if req.Rule == nil || req.Rule.Id <= 0 {
+ return nil, errors.New("invalid 'rule.id'")
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ oldRule, err := models.SharedHTTPDNSCustomRuleDAO.FindEnabledRule(tx, req.Rule.Id)
+ if err != nil {
+ return err
+ }
+ if oldRule == nil {
+ return errors.New("rule not found")
+ }
+
+ rule := &models.HTTPDNSCustomRule{
+ Id: uint32(req.Rule.Id),
+ RuleName: req.Rule.RuleName,
+ LineScope: req.Rule.LineScope,
+ LineCarrier: req.Rule.LineCarrier,
+ LineRegion: req.Rule.LineRegion,
+ LineProvince: req.Rule.LineProvince,
+ LineContinent: req.Rule.LineContinent,
+ LineCountry: req.Rule.LineCountry,
+ TTL: req.Rule.Ttl,
+ IsOn: req.Rule.IsOn,
+ Priority: req.Rule.Priority,
+ }
+ err = models.SharedHTTPDNSCustomRuleDAO.UpdateRule(tx, rule)
+ if err != nil {
+ return err
+ }
+ err = models.SharedHTTPDNSCustomRuleRecordDAO.DisableRecordsWithRuleId(tx, req.Rule.Id)
+ if err != nil {
+ return err
+ }
+ for _, record := range req.Rule.Records {
+ _, err := models.SharedHTTPDNSCustomRuleRecordDAO.CreateRecord(tx, req.Rule.Id, record.RecordType, record.RecordValue, record.Weight, record.Sort)
+ if err != nil {
+ return err
+ }
+ }
+ err = notifyHTTPDNSAppTasksByAppDbId(tx, int64(oldRule.AppId), models.HTTPDNSNodeTaskTypeRuleChanged)
+ if err != nil {
+ return err
+ }
+
+ targetAppDbId := req.Rule.AppId
+ if targetAppDbId <= 0 {
+ targetAppDbId = int64(oldRule.AppId)
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, targetAppDbId, models.HTTPDNSNodeTaskTypeRuleChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSRuleService) DeleteHTTPDNSCustomRule(ctx context.Context, req *pb.DeleteHTTPDNSCustomRuleRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ rule, err := models.SharedHTTPDNSCustomRuleDAO.FindEnabledRule(tx, req.RuleId)
+ if err != nil {
+ return err
+ }
+ if rule == nil {
+ return nil
+ }
+
+ err = models.SharedHTTPDNSCustomRuleDAO.DisableRule(tx, req.RuleId)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, int64(rule.AppId), models.HTTPDNSNodeTaskTypeRuleChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSRuleService) UpdateHTTPDNSCustomRuleStatus(ctx context.Context, req *pb.UpdateHTTPDNSCustomRuleStatusRequest) (*pb.RPCSuccess, error) {
+ _, err := this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ err = this.RunTx(func(tx *dbs.Tx) error {
+ rule, err := models.SharedHTTPDNSCustomRuleDAO.FindEnabledRule(tx, req.RuleId)
+ if err != nil {
+ return err
+ }
+ if rule == nil {
+ return nil
+ }
+
+ err = models.SharedHTTPDNSCustomRuleDAO.UpdateRuleStatus(tx, req.RuleId, req.IsOn)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByAppDbId(tx, int64(rule.AppId), models.HTTPDNSNodeTaskTypeRuleChanged)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return this.Success()
+}
+
+func (this *HTTPDNSRuleService) ListHTTPDNSCustomRulesWithDomainId(ctx context.Context, req *pb.ListHTTPDNSCustomRulesWithDomainIdRequest) (*pb.ListHTTPDNSCustomRulesWithDomainIdResponse, error) {
+ _, _, validateErr := this.ValidateAdminAndUser(ctx, true)
+ if validateErr != nil {
+ if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
+ return nil, validateErr
+ }
+ }
+ rules, err := models.SharedHTTPDNSCustomRuleDAO.ListEnabledRulesWithDomainId(this.NullTx(), req.DomainId)
+ if err != nil {
+ return nil, err
+ }
+ var pbRules []*pb.HTTPDNSCustomRule
+ for _, rule := range rules {
+ records, err := models.SharedHTTPDNSCustomRuleRecordDAO.ListEnabledRecordsWithRuleId(this.NullTx(), int64(rule.Id))
+ if err != nil {
+ return nil, err
+ }
+ pbRules = append(pbRules, toPBRule(rule, records))
+ }
+ return &pb.ListHTTPDNSCustomRulesWithDomainIdResponse{Rules: pbRules}, nil
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_runtime_log.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_runtime_log.go
new file mode 100644
index 0000000..a56ef93
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_runtime_log.go
@@ -0,0 +1,107 @@
+package httpdns
+
+import (
+ "context"
+ "github.com/TeaOSLab/EdgeAPI/internal/db/models"
+ "github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ timeutil "github.com/iwind/TeaGo/utils/time"
+ "time"
+)
+
+// HTTPDNSRuntimeLogService HTTPDNS运行日志服务
+type HTTPDNSRuntimeLogService struct {
+ services.BaseService
+ pb.UnimplementedHTTPDNSRuntimeLogServiceServer
+}
+
+func (this *HTTPDNSRuntimeLogService) CreateHTTPDNSRuntimeLogs(ctx context.Context, req *pb.CreateHTTPDNSRuntimeLogsRequest) (*pb.CreateHTTPDNSRuntimeLogsResponse, error) {
+ nodeIdInContext, err := this.ValidateHTTPDNSNode(ctx)
+ if err != nil {
+ _, err = this.ValidateAdmin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ }
+ for _, item := range req.Logs {
+ createdAt := item.CreatedAt
+ if createdAt <= 0 {
+ createdAt = time.Now().Unix()
+ }
+ day := item.Day
+ if len(day) == 0 {
+ day = timeutil.Format("Ymd")
+ }
+ nodeId := item.NodeId
+ // When called by HTTPDNS node, trust node id parsed from RPC token.
+ if nodeIdInContext > 0 {
+ nodeId = nodeIdInContext
+ }
+
+ clusterId := item.ClusterId
+ if clusterId <= 0 && nodeId > 0 {
+ clusterId, _ = models.SharedHTTPDNSNodeDAO.FindNodeClusterId(this.NullTx(), nodeId)
+ }
+
+ log := &models.HTTPDNSRuntimeLog{
+ ClusterId: uint32(clusterId),
+ NodeId: uint32(nodeId),
+ Level: item.Level,
+ Type: item.Type,
+ Module: item.Module,
+ Description: item.Description,
+ Count: item.Count,
+ RequestId: item.RequestId,
+ CreatedAt: uint64(createdAt),
+ Day: day,
+ }
+ err := models.SharedHTTPDNSRuntimeLogDAO.CreateLog(this.NullTx(), log)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return &pb.CreateHTTPDNSRuntimeLogsResponse{}, nil
+}
+
+func (this *HTTPDNSRuntimeLogService) ListHTTPDNSRuntimeLogs(ctx context.Context, req *pb.ListHTTPDNSRuntimeLogsRequest) (*pb.ListHTTPDNSRuntimeLogsResponse, error) {
+ _, _, err := this.ValidateAdminAndUser(ctx, true)
+ if err != nil {
+ return nil, err
+ }
+ total, err := models.SharedHTTPDNSRuntimeLogDAO.CountLogs(this.NullTx(), req.Day, req.ClusterId, req.NodeId, req.Level, req.Keyword)
+ if err != nil {
+ return nil, err
+ }
+ logs, err := models.SharedHTTPDNSRuntimeLogDAO.ListLogs(this.NullTx(), req.Day, req.ClusterId, req.NodeId, req.Level, req.Keyword, req.Offset, req.Size)
+ if err != nil {
+ return nil, err
+ }
+ var pbLogs []*pb.HTTPDNSRuntimeLog
+ for _, item := range logs {
+ clusterName, _ := models.SharedHTTPDNSClusterDAO.FindEnabledClusterName(this.NullTx(), int64(item.ClusterId))
+ nodeName := ""
+ node, _ := models.SharedHTTPDNSNodeDAO.FindEnabledNode(this.NullTx(), int64(item.NodeId))
+ if node != nil {
+ nodeName = node.Name
+ }
+ pbLogs = append(pbLogs, &pb.HTTPDNSRuntimeLog{
+ Id: int64(item.Id),
+ ClusterId: int64(item.ClusterId),
+ NodeId: int64(item.NodeId),
+ Level: item.Level,
+ Type: item.Type,
+ Module: item.Module,
+ Description: item.Description,
+ Count: item.Count,
+ RequestId: item.RequestId,
+ CreatedAt: int64(item.CreatedAt),
+ Day: item.Day,
+ ClusterName: clusterName,
+ NodeName: nodeName,
+ })
+ }
+ return &pb.ListHTTPDNSRuntimeLogsResponse{
+ Logs: pbLogs,
+ Total: total,
+ }, nil
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_sandbox.go b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_sandbox.go
new file mode 100644
index 0000000..b853f24
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/service_httpdns_sandbox.go
@@ -0,0 +1,252 @@
+package httpdns
+
+import (
+ "context"
+ "crypto/hmac"
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "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/rands"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// HTTPDNSSandboxService HTTPDNS解析测试服务
+type HTTPDNSSandboxService struct {
+ services.BaseService
+ pb.UnimplementedHTTPDNSSandboxServiceServer
+}
+
+// nodeResolveResponse 节点返回的 JSON 结构(对齐 EdgeHttpDNS resolve_server.go)
+type nodeResolveResponse struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ RequestID string `json:"requestId"`
+ Data *nodeResolveData `json:"data,omitempty"`
+}
+
+type nodeResolveData struct {
+ Domain string `json:"domain"`
+ QType string `json:"qtype"`
+ TTL int32 `json:"ttl"`
+ Records []*nodeResolveRecord `json:"records"`
+ Client *nodeClientInfo `json:"client"`
+ Summary string `json:"summary"`
+}
+
+type nodeResolveRecord struct {
+ Type string `json:"type"`
+ IP string `json:"ip"`
+ Weight int32 `json:"weight"`
+ Line string `json:"line"`
+ Region string `json:"region"`
+}
+
+type nodeClientInfo struct {
+ IP string `json:"ip"`
+ Region string `json:"region"`
+ Carrier string `json:"carrier"`
+ Country string `json:"country"`
+}
+
+func (this *HTTPDNSSandboxService) TestHTTPDNSResolve(ctx context.Context, req *pb.TestHTTPDNSResolveRequest) (*pb.TestHTTPDNSResolveResponse, error) {
+ _, _, err := this.ValidateAdminAndUser(ctx, true)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(req.AppId) == 0 || len(req.Domain) == 0 {
+ return nil, errors.New("appId 和 domain 不能为空")
+ }
+
+ app, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(this.NullTx(), req.AppId)
+ if err != nil {
+ return nil, err
+ }
+ if app == nil || !app.IsOn {
+ return &pb.TestHTTPDNSResolveResponse{
+ Code: "APP_NOT_FOUND_OR_DISABLED",
+ Message: "找不到指定的应用,或该应用已下线",
+ RequestId: "rid-" + rands.HexString(12),
+ }, nil
+ }
+ if req.ClusterId > 0 && req.ClusterId != int64(app.PrimaryClusterId) && req.ClusterId != int64(app.BackupClusterId) {
+ return &pb.TestHTTPDNSResolveResponse{
+ Code: "APP_CLUSTER_MISMATCH",
+ Message: "当前应用未绑定到该集群 (主集群: " + strconv.FormatInt(int64(app.PrimaryClusterId), 10) + ", 备用集群: " + strconv.FormatInt(int64(app.BackupClusterId), 10) + ")",
+ RequestId: "rid-" + rands.HexString(12),
+ }, nil
+ }
+
+ qtype := strings.ToUpper(strings.TrimSpace(req.Qtype))
+ if qtype == "" {
+ qtype = "A"
+ }
+
+ // 获取集群服务域名
+ clusterId := req.ClusterId
+ if clusterId <= 0 {
+ clusterId = int64(app.PrimaryClusterId)
+ }
+ cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(this.NullTx(), clusterId)
+ if err != nil {
+ return nil, err
+ }
+ if cluster == nil {
+ return &pb.TestHTTPDNSResolveResponse{
+ Code: "CLUSTER_NOT_FOUND",
+ Message: "找不到指定的集群",
+ RequestId: "rid-" + rands.HexString(12),
+ }, nil
+ }
+
+ serviceDomain := strings.TrimSpace(cluster.ServiceDomain)
+ if len(serviceDomain) == 0 {
+ return &pb.TestHTTPDNSResolveResponse{
+ Code: "NO_SERVICE_DOMAIN",
+ Message: "该集群未配置服务域名",
+ RequestId: "rid-" + rands.HexString(12),
+ }, nil
+ }
+
+ // 构造请求转发到 EdgeHttpDNS 节点
+ secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
+ if err != nil {
+ return nil, err
+ }
+
+ query := url.Values{}
+ query.Set("appId", req.AppId)
+ query.Set("dn", req.Domain)
+ query.Set("qtype", qtype)
+ if len(req.ClientIP) > 0 {
+ query.Set("cip", req.ClientIP)
+ }
+ if len(req.Sid) > 0 {
+ query.Set("sid", req.Sid)
+ }
+ if len(req.SdkVersion) > 0 {
+ query.Set("sdk_version", req.SdkVersion)
+ }
+ if len(req.Os) > 0 {
+ query.Set("os", req.Os)
+ }
+
+ // 应用开启验签时,沙盒自动生成签名参数,避免测试请求被拒绝
+ if secret != nil && secret.SignEnabled {
+ signSecret := strings.TrimSpace(secret.SignSecret)
+ if len(signSecret) == 0 {
+ return &pb.TestHTTPDNSResolveResponse{
+ Code: "SIGN_INVALID",
+ Message: "应用开启了请求验签,但未配置有效加签 Secret",
+ RequestId: "rid-" + rands.HexString(12),
+ Domain: req.Domain,
+ Qtype: qtype,
+ }, nil
+ }
+
+ exp := strconv.FormatInt(time.Now().Unix()+300, 10)
+ nonce := "sandbox-" + rands.HexString(16)
+ sign := buildSandboxResolveSign(signSecret, req.AppId, req.Domain, qtype, exp, nonce)
+
+ query.Set("exp", exp)
+ query.Set("nonce", nonce)
+ query.Set("sign", sign)
+ }
+
+ resolveURL := "https://" + serviceDomain + "/resolve?" + query.Encode()
+
+ httpClient := &http.Client{
+ Timeout: 5 * time.Second,
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true, // 沙盒测试环境允许自签名证书
+ },
+ },
+ }
+
+ httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, resolveURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("构建请求失败: %w", err)
+ }
+
+ resp, err := httpClient.Do(httpReq)
+ if err != nil {
+ return &pb.TestHTTPDNSResolveResponse{
+ Code: "NODE_UNREACHABLE",
+ Message: "无法连接到 HTTPDNS 节点: " + err.Error(),
+ RequestId: "rid-" + rands.HexString(12),
+ Domain: req.Domain,
+ Qtype: qtype,
+ }, nil
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
+ if err != nil {
+ return nil, fmt.Errorf("读取节点响应失败: %w", err)
+ }
+
+ // 解析节点返回的 JSON
+ var nodeResp nodeResolveResponse
+ if err := json.Unmarshal(body, &nodeResp); err != nil {
+ return &pb.TestHTTPDNSResolveResponse{
+ Code: "PARSE_ERROR",
+ Message: "解析节点返回数据失败: " + err.Error(),
+ RequestId: "rid-" + rands.HexString(12),
+ Domain: req.Domain,
+ Qtype: qtype,
+ }, nil
+ }
+
+ // 映射节点响应到 protobuf 响应
+ pbResp := &pb.TestHTTPDNSResolveResponse{
+ Code: nodeResp.Code,
+ Message: nodeResp.Message,
+ RequestId: nodeResp.RequestID,
+ Domain: req.Domain,
+ Qtype: qtype,
+ }
+
+ if nodeResp.Data != nil {
+ pbResp.Ttl = nodeResp.Data.TTL
+ pbResp.Summary = nodeResp.Data.Summary
+
+ if nodeResp.Data.Client != nil {
+ pbResp.ClientIP = nodeResp.Data.Client.IP
+ pbResp.ClientRegion = nodeResp.Data.Client.Region
+ pbResp.ClientCarrier = nodeResp.Data.Client.Carrier
+ pbResp.ClientCountry = nodeResp.Data.Client.Country
+ }
+
+ for _, rec := range nodeResp.Data.Records {
+ pbResp.Records = append(pbResp.Records, &pb.HTTPDNSResolveRecord{
+ Type: rec.Type,
+ Ip: rec.IP,
+ Ttl: nodeResp.Data.TTL,
+ Weight: rec.Weight,
+ Line: rec.Line,
+ Region: rec.Region,
+ })
+ }
+ }
+
+ return pbResp, nil
+}
+
+func buildSandboxResolveSign(signSecret string, appID string, domain string, qtype string, exp string, nonce string) string {
+ raw := strings.TrimSpace(appID) + "|" + strings.ToLower(strings.TrimSpace(domain)) + "|" + strings.ToUpper(strings.TrimSpace(qtype)) + "|" + strings.TrimSpace(exp) + "|" + strings.TrimSpace(nonce)
+ mac := hmac.New(sha256.New, []byte(strings.TrimSpace(signSecret)))
+ _, _ = mac.Write([]byte(raw))
+ return hex.EncodeToString(mac.Sum(nil))
+}
diff --git a/EdgeAPI/internal/rpc/services/httpdns/task_notify.go b/EdgeAPI/internal/rpc/services/httpdns/task_notify.go
new file mode 100644
index 0000000..a6a1088
--- /dev/null
+++ b/EdgeAPI/internal/rpc/services/httpdns/task_notify.go
@@ -0,0 +1,49 @@
+package httpdns
+
+import (
+ "github.com/TeaOSLab/EdgeAPI/internal/db/models"
+ "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
+ "github.com/iwind/TeaGo/dbs"
+)
+
+func notifyHTTPDNSClusterTask(tx *dbs.Tx, clusterId int64, taskType models.NodeTaskType) error {
+ if clusterId <= 0 {
+ return nil
+ }
+ return models.SharedNodeTaskDAO.CreateClusterTask(tx, nodeconfigs.NodeRoleHTTPDNS, clusterId, 0, 0, taskType)
+}
+
+func notifyHTTPDNSAppTasksByApp(tx *dbs.Tx, app *models.HTTPDNSApp, taskType models.NodeTaskType) error {
+ if app == nil {
+ return nil
+ }
+
+ primaryClusterId := int64(app.PrimaryClusterId)
+ backupClusterId := int64(app.BackupClusterId)
+
+ err := notifyHTTPDNSClusterTask(tx, primaryClusterId, taskType)
+ if err != nil {
+ return err
+ }
+
+ if backupClusterId > 0 && backupClusterId != primaryClusterId {
+ err = notifyHTTPDNSClusterTask(tx, backupClusterId, taskType)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func notifyHTTPDNSAppTasksByAppDbId(tx *dbs.Tx, appDbId int64, taskType models.NodeTaskType) error {
+ if appDbId <= 0 {
+ return nil
+ }
+
+ app, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(tx, appDbId)
+ if err != nil {
+ return err
+ }
+ return notifyHTTPDNSAppTasksByApp(tx, app, taskType)
+}
diff --git a/EdgeAPI/internal/rpc/services/service_base.go b/EdgeAPI/internal/rpc/services/service_base.go
index 66fb9fe..661ba99 100644
--- a/EdgeAPI/internal/rpc/services/service_base.go
+++ b/EdgeAPI/internal/rpc/services/service_base.go
@@ -83,6 +83,12 @@ func (this *BaseService) ValidateNSNode(ctx context.Context) (nodeId int64, err
return
}
+// ValidateHTTPDNSNode 校验HTTPDNS节点
+func (this *BaseService) ValidateHTTPDNSNode(ctx context.Context) (nodeId int64, err error) {
+ _, _, nodeId, err = rpcutils.ValidateRequest(ctx, rpcutils.UserTypeHTTPDNS)
+ return
+}
+
// ValidateUserNode 校验用户节点
func (this *BaseService) ValidateUserNode(ctx context.Context, canRest bool) (userId int64, err error) {
// 不允许REST调用
@@ -105,7 +111,7 @@ func (this *BaseService) ValidateAuthorityNode(ctx context.Context) (nodeId int6
func (this *BaseService) ValidateNodeId(ctx context.Context, roles ...rpcutils.UserType) (role rpcutils.UserType, nodeIntId int64, err error) {
// 默认包含大部分节点
if len(roles) == 0 {
- roles = []rpcutils.UserType{rpcutils.UserTypeNode, rpcutils.UserTypeCluster, rpcutils.UserTypeAdmin, rpcutils.UserTypeUser, rpcutils.UserTypeDNS, rpcutils.UserTypeReport, rpcutils.UserTypeLog, rpcutils.UserTypeAPI}
+ roles = []rpcutils.UserType{rpcutils.UserTypeNode, rpcutils.UserTypeCluster, rpcutils.UserTypeAdmin, rpcutils.UserTypeUser, rpcutils.UserTypeDNS, rpcutils.UserTypeHTTPDNS, rpcutils.UserTypeReport, rpcutils.UserTypeLog, rpcutils.UserTypeAPI}
}
if ctx == nil {
@@ -191,6 +197,8 @@ func (this *BaseService) ValidateNodeId(ctx context.Context, roles ...rpcutils.U
nodeIntId = 0
case rpcutils.UserTypeDNS:
nodeIntId, err = models.SharedNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
+ case rpcutils.UserTypeHTTPDNS:
+ nodeIntId, err = models.SharedHTTPDNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
case rpcutils.UserTypeReport:
nodeIntId, err = models.SharedReportNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
case rpcutils.UserTypeAuthority:
diff --git a/EdgeAPI/internal/rpc/services/service_node_task.go b/EdgeAPI/internal/rpc/services/service_node_task.go
index 5651fb7..b7fa3a7 100644
--- a/EdgeAPI/internal/rpc/services/service_node_task.go
+++ b/EdgeAPI/internal/rpc/services/service_node_task.go
@@ -19,7 +19,7 @@ type NodeTaskService struct {
// FindNodeTasks 获取单节点同步任务
func (this *NodeTaskService) FindNodeTasks(ctx context.Context, req *pb.FindNodeTasksRequest) (*pb.FindNodeTasksResponse, error) {
- nodeType, nodeId, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS)
+ nodeType, nodeId, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeHTTPDNS)
if err != nil {
return nil, err
}
@@ -65,7 +65,7 @@ func (this *NodeTaskService) FindNodeTasks(ctx context.Context, req *pb.FindNode
// ReportNodeTaskDone 报告同步任务结果
func (this *NodeTaskService) ReportNodeTaskDone(ctx context.Context, req *pb.ReportNodeTaskDoneRequest) (*pb.RPCSuccess, error) {
- _, _, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS)
+ _, _, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeHTTPDNS)
if err != nil {
return nil, err
}
diff --git a/EdgeAPI/internal/rpc/utils/utils.go b/EdgeAPI/internal/rpc/utils/utils.go
index 6ea6906..cb9f459 100644
--- a/EdgeAPI/internal/rpc/utils/utils.go
+++ b/EdgeAPI/internal/rpc/utils/utils.go
@@ -16,6 +16,7 @@ const (
UserTypeCluster = "cluster"
UserTypeStat = "stat"
UserTypeDNS = "dns"
+ UserTypeHTTPDNS = "httpdns"
UserTypeLog = "log"
UserTypeAPI = "api"
UserTypeAuthority = "authority"
diff --git a/EdgeAPI/internal/rpc/utils/utils_ext.go b/EdgeAPI/internal/rpc/utils/utils_ext.go
index aa8c0aa..a1ae946 100644
--- a/EdgeAPI/internal/rpc/utils/utils_ext.go
+++ b/EdgeAPI/internal/rpc/utils/utils_ext.go
@@ -142,6 +142,16 @@ func ValidateRequest(ctx context.Context, userTypes ...UserType) (userType UserT
return UserTypeUser, 0, 0, errors.New("context: not found node with id '" + nodeId + "'")
}
resultNodeId = nodeIntId
+ case UserTypeHTTPDNS:
+ nodeIntId, err := models.SharedHTTPDNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
+ if err != nil {
+ return UserTypeHTTPDNS, nodeIntId, 0, errors.New("context: " + err.Error())
+ }
+ if nodeIntId <= 0 {
+ return UserTypeHTTPDNS, nodeIntId, 0, errors.New("context: not found node with id '" + nodeId + "'")
+ }
+ nodeUserId = nodeIntId
+ resultNodeId = nodeIntId
}
if nodeUserId > 0 {
diff --git a/EdgeAPI/internal/rpc/utils/utils_ext_plus.go b/EdgeAPI/internal/rpc/utils/utils_ext_plus.go
index 49d8784..8a9e133 100644
--- a/EdgeAPI/internal/rpc/utils/utils_ext_plus.go
+++ b/EdgeAPI/internal/rpc/utils/utils_ext_plus.go
@@ -171,6 +171,16 @@ func ValidateRequest(ctx context.Context, userTypes ...UserType) (userType UserT
}
nodeUserId = nodeIntId
resultNodeId = nodeIntId
+ case UserTypeHTTPDNS:
+ nodeIntId, err := models.SharedHTTPDNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
+ if err != nil {
+ return UserTypeHTTPDNS, nodeIntId, 0, errors.New("context: " + err.Error())
+ }
+ if nodeIntId <= 0 {
+ return UserTypeHTTPDNS, nodeIntId, 0, errors.New("context: not found node with id '" + nodeId + "'")
+ }
+ nodeUserId = nodeIntId
+ resultNodeId = nodeIntId
case UserTypeReport:
nodeIntId, err := models.SharedReportNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
if err != nil {
diff --git a/EdgeAPI/internal/setup/clickhouse_upgrade.go b/EdgeAPI/internal/setup/clickhouse_upgrade.go
new file mode 100644
index 0000000..865c5fa
--- /dev/null
+++ b/EdgeAPI/internal/setup/clickhouse_upgrade.go
@@ -0,0 +1,132 @@
+package setup
+
+import (
+ "context"
+ "strings"
+ "time"
+
+ "github.com/TeaOSLab/EdgeAPI/internal/clickhouse"
+)
+
+// EnsureClickHouseTables 自动确保日志相关 ClickHouse 表存在。
+// 仅做 CREATE TABLE IF NOT EXISTS,不会覆盖已有表结构。
+func EnsureClickHouseTables() error {
+ client := clickhouse.NewClient()
+ if !client.IsConfigured() {
+ return nil
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ sqls := []string{
+ `CREATE TABLE IF NOT EXISTS logs_ingest
+(
+ timestamp DateTime CODEC(DoubleDelta, ZSTD(1)),
+ node_id UInt64,
+ cluster_id UInt64,
+ server_id UInt64,
+ host LowCardinality(String),
+ ip String,
+ method LowCardinality(String),
+ path String CODEC(ZSTD(1)),
+ status UInt16,
+ bytes_in UInt64 CODEC(Delta, ZSTD(1)),
+ bytes_out UInt64 CODEC(Delta, ZSTD(1)),
+ cost_ms UInt32 CODEC(Delta, ZSTD(1)),
+ ua String CODEC(ZSTD(1)),
+ referer String CODEC(ZSTD(1)),
+ log_type LowCardinality(String),
+ trace_id String,
+ firewall_policy_id UInt64 DEFAULT 0,
+ firewall_rule_group_id UInt64 DEFAULT 0,
+ firewall_rule_set_id UInt64 DEFAULT 0,
+ firewall_rule_id UInt64 DEFAULT 0,
+ request_headers String CODEC(ZSTD(3)) DEFAULT '',
+ request_body String CODEC(ZSTD(3)) DEFAULT '',
+ response_headers String CODEC(ZSTD(3)) DEFAULT '',
+ response_body String CODEC(ZSTD(3)) DEFAULT '',
+ INDEX idx_trace_id trace_id TYPE bloom_filter(0.01) GRANULARITY 4,
+ INDEX idx_ip ip TYPE bloom_filter(0.01) GRANULARITY 4,
+ INDEX idx_host host TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
+ INDEX idx_fw_policy firewall_policy_id TYPE minmax GRANULARITY 4,
+ INDEX idx_status status TYPE minmax GRANULARITY 4
+)
+ENGINE = MergeTree
+PARTITION BY toYYYYMMDD(timestamp)
+ORDER BY (timestamp, node_id, server_id, trace_id)
+SETTINGS index_granularity = 8192`,
+ `CREATE TABLE IF NOT EXISTS dns_logs_ingest
+(
+ timestamp DateTime CODEC(DoubleDelta, ZSTD(1)),
+ request_id String,
+ node_id UInt64,
+ cluster_id UInt64,
+ domain_id UInt64,
+ record_id UInt64,
+ remote_addr String,
+ question_name String,
+ question_type LowCardinality(String),
+ record_name String,
+ record_type LowCardinality(String),
+ record_value String,
+ networking LowCardinality(String),
+ is_recursive UInt8,
+ error String CODEC(ZSTD(1)),
+ ns_route_codes Array(String),
+ content_json String CODEC(ZSTD(3)) DEFAULT '',
+ INDEX idx_request_id request_id TYPE bloom_filter(0.01) GRANULARITY 4,
+ INDEX idx_remote_addr remote_addr TYPE bloom_filter(0.01) GRANULARITY 4,
+ INDEX idx_question_name question_name TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
+ INDEX idx_domain_id domain_id TYPE minmax GRANULARITY 4
+)
+ENGINE = MergeTree
+PARTITION BY toYYYYMMDD(timestamp)
+ORDER BY (timestamp, request_id, node_id)
+SETTINGS index_granularity = 8192`,
+ `CREATE TABLE IF NOT EXISTS httpdns_access_logs_ingest
+(
+ request_id String,
+ cluster_id UInt64,
+ node_id UInt64,
+ app_id String,
+ app_name String,
+ domain String,
+ qtype LowCardinality(String),
+ client_ip String,
+ client_region String,
+ carrier String,
+ sdk_version String,
+ os LowCardinality(String),
+ result_ips String,
+ status LowCardinality(String),
+ error_code String,
+ cost_ms UInt32,
+ created_at UInt64,
+ day String,
+ summary String CODEC(ZSTD(1)),
+ INDEX idx_request_id request_id TYPE bloom_filter(0.01) GRANULARITY 4,
+ INDEX idx_cluster_id cluster_id TYPE minmax GRANULARITY 4,
+ INDEX idx_node_id node_id TYPE minmax GRANULARITY 4,
+ INDEX idx_app_id app_id TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
+ INDEX idx_domain domain TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
+ INDEX idx_status status TYPE minmax GRANULARITY 4
+)
+ENGINE = MergeTree
+PARTITION BY day
+ORDER BY (day, created_at, request_id, node_id)
+SETTINGS index_granularity = 8192`,
+ }
+
+ for _, sql := range sqls {
+ stmt := strings.TrimSpace(sql)
+ if len(stmt) == 0 {
+ continue
+ }
+ if err := client.Execute(ctx, stmt); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/EdgeAPI/internal/setup/sql_upgrade.go b/EdgeAPI/internal/setup/sql_upgrade.go
index ff0e77b..205f16b 100644
--- a/EdgeAPI/internal/setup/sql_upgrade.go
+++ b/EdgeAPI/internal/setup/sql_upgrade.go
@@ -110,6 +110,9 @@ var upgradeFuncs = []*upgradeVersion{
{
"1.4.4", upgradeV1_4_4,
},
+ {
+ "1.4.8", upgradeV1_4_8,
+ },
}
// UpgradeSQLData 升级SQL数据
@@ -269,14 +272,14 @@ func upgradeV0_0_10(db *dbs.DB) error {
// v0.2.5
func upgradeV0_2_5(db *dbs.DB) error {
- // 更新用户
+ // 鏇存柊鐢ㄦ埛
_, err := db.Exec("UPDATE edgeUsers SET day=FROM_UNIXTIME(createdAt,'%Y%m%d') WHERE day IS NULL OR LENGTH(day)=0")
if err != nil {
return err
}
- // 更新防火墙规则
- ones, _, err := db.FindOnes("SELECT id, actions, action, actionOptions FROM edgeHTTPFirewallRuleSets WHERE actions IS NULL OR LENGTH(actions)=0")
+ // 更新防火墙规则
+ ones, _, err := db.FindOnes("SELECT id, actions, action, actionOptions FROM edgeHTTPFirewallRuleSets WHERE actions IS NULL OR LENGTH(actions)=0")
if err != nil {
return err
}
@@ -309,8 +312,8 @@ func upgradeV0_2_5(db *dbs.DB) error {
// v0.3.0
func upgradeV0_3_0(db *dbs.DB) error {
- // 升级健康检查
- ones, _, err := db.FindOnes("SELECT id,healthCheck FROM edgeNodeClusters WHERE state=1")
+ // 升级健康检查
+ ones, _, err := db.FindOnes("SELECT id,healthCheck FROM edgeNodeClusters WHERE state=1")
if err != nil {
return err
}
@@ -342,11 +345,10 @@ func upgradeV0_3_0(db *dbs.DB) error {
// v0.3.1
func upgradeV0_3_1(db *dbs.DB) error {
- // 清空域名统计,已使用分表代替
- // 因为可能有权限问题,所以我们忽略错误
- _, _ = db.Exec("TRUNCATE table edgeServerDomainHourlyStats")
+ // 娓呯┖鍩熷悕缁熻锛屽凡浣跨敤鍒嗚〃浠f浛
+ // 鍥犱负鍙兘鏈夋潈闄愰棶棰橈紝鎵€浠ユ垜浠拷鐣ラ敊璇? _, _ = db.Exec("TRUNCATE table edgeServerDomainHourlyStats")
- // 升级APIToken
+ // 鍗囩骇APIToken
ones, _, err := db.FindOnes("SELECT uniqueId,secret FROM edgeNodeClusters")
if err != nil {
return err
@@ -374,9 +376,9 @@ func upgradeV0_3_2(db *dbs.DB) error {
// gzip => compression
type HTTPGzipRef struct {
- IsPrior bool `yaml:"isPrior" json:"isPrior"` // 是否覆盖
- IsOn bool `yaml:"isOn" json:"isOn"` // 是否开启
- GzipId int64 `yaml:"gzipId" json:"gzipId"` // 使用的配置ID
+ IsPrior bool `yaml:"isPrior" json:"isPrior"` // 鏄惁瑕嗙洊
+ IsOn bool `yaml:"isOn" json:"isOn"` // 是否开启
+ GzipId int64 `yaml:"gzipId" json:"gzipId"` // 使用的配置ID
}
webOnes, _, err := db.FindOnes("SELECT id, gzip FROM edgeHTTPWebs WHERE gzip IS NOT NULL AND compression IS NULL")
@@ -458,7 +460,7 @@ func upgradeV0_3_2(db *dbs.DB) error {
}
}
- // 更新服务端口
+ // 鏇存柊鏈嶅姟绔彛
var serverDAO = models.NewServerDAO()
ones, err := serverDAO.Query(nil).
ResultPk().
@@ -479,14 +481,14 @@ func upgradeV0_3_2(db *dbs.DB) error {
// v0.3.3
func upgradeV0_3_3(db *dbs.DB) error {
- // 升级CC请求数Code
- _, err := db.Exec("UPDATE edgeHTTPFirewallRuleSets SET code='8002' WHERE name='CC请求数' AND code='8001'")
+ // 鍗囩骇CC璇锋眰鏁癈ode
+ _, err := db.Exec("UPDATE edgeHTTPFirewallRuleSets SET code='8002' WHERE name='CC璇锋眰鏁? AND code='8001'")
if err != nil {
return err
}
- // 清除节点
- // 删除7天以前的info日志
+ // 娓呴櫎鑺傜偣
+ // 鍒犻櫎7澶╀互鍓嶇殑info鏃ュ織
err = models.NewNodeLogDAO().DeleteExpiredLogsWithLevel(nil, "info", 7)
if err != nil {
return err
@@ -497,13 +499,13 @@ func upgradeV0_3_3(db *dbs.DB) error {
// v0.3.7
func upgradeV0_3_7(db *dbs.DB) error {
- // 修改所有edgeNodeGrants中的su为0
+ // 淇敼鎵€鏈塭dgeNodeGrants涓殑su涓?
_, err := db.Exec("UPDATE edgeNodeGrants SET su=0 WHERE su=1")
if err != nil {
return err
}
- // WAF预置分组
+ // WAF棰勭疆鍒嗙粍
_, err = db.Exec("UPDATE edgeHTTPFirewallRuleGroups SET isTemplate=1 WHERE LENGTH(code)>0")
if err != nil {
return err
@@ -514,7 +516,7 @@ func upgradeV0_3_7(db *dbs.DB) error {
// v0.4.0
func upgradeV0_4_0(db *dbs.DB) error {
- // 升级SYN Flood配置
+ // 鍗囩骇SYN Flood閰嶇疆
synFloodJSON, err := json.Marshal(firewallconfigs.NewSYNFloodConfig())
if err == nil {
_, err := db.Exec("UPDATE edgeHTTPFirewallPolicies SET synFlood=? WHERE synFlood IS NULL AND state=1", string(synFloodJSON))
@@ -528,13 +530,13 @@ func upgradeV0_4_0(db *dbs.DB) error {
// v0.4.1
func upgradeV0_4_1(db *dbs.DB) error {
- // 升级 servers.lastUserPlanId
+ // 鍗囩骇 servers.lastUserPlanId
_, err := db.Exec("UPDATE edgeServers SET lastUserPlanId=userPlanId WHERE userPlanId>0")
if err != nil {
return err
}
- // 执行域名统计清理
+ // 鎵ц鍩熷悕缁熻娓呯悊
err = stats.NewServerDomainHourlyStatDAO().CleanDays(nil, 7)
if err != nil {
return err
@@ -545,7 +547,7 @@ func upgradeV0_4_1(db *dbs.DB) error {
// v0.4.5
func upgradeV0_4_5(db *dbs.DB) error {
- // 升级访问日志自动分表
+ // 鍗囩骇璁块棶鏃ュ織鑷姩鍒嗚〃
{
var dao = models.NewSysSettingDAO()
valueJSON, err := dao.ReadSetting(nil, systemconfigs.SettingCodeAccessLogQueue)
@@ -569,7 +571,7 @@ func upgradeV0_4_5(db *dbs.DB) error {
}
}
- // 升级一个防SQL注入规则
+ // 鍗囩骇涓€涓槻SQL娉ㄥ叆瑙勫垯
{
ones, _, err := db.FindOnes(`SELECT id FROM edgeHTTPFirewallRules WHERE value=?`, "(updatexml|extractvalue|ascii|ord|char|chr|count|concat|rand|floor|substr|length|len|user|database|benchmark|analyse)\\s*\\(")
if err != nil {
@@ -589,7 +591,7 @@ func upgradeV0_4_5(db *dbs.DB) error {
// v0.4.7
func upgradeV0_4_7(db *dbs.DB) error {
- // 升级 edgeServers 中的 plainServerNames
+ // 鍗囩骇 edgeServers 涓殑 plainServerNames
{
ones, _, err := db.FindOnes("SELECT id,serverNames FROM edgeServers WHERE state=1")
if err != nil {
@@ -621,7 +623,7 @@ func upgradeV0_4_7(db *dbs.DB) error {
// v0.4.8
func upgradeV0_4_8(db *dbs.DB) error {
- // 设置edgeIPLists中的serverId
+ // 璁剧疆edgeIPLists涓殑serverId
{
firewallPolicyOnes, _, err := db.FindOnes("SELECT inbound,serverId FROM edgeHTTPFirewallPolicies WHERE serverId>0")
if err != nil {
@@ -673,7 +675,7 @@ func upgradeV0_4_8(db *dbs.DB) error {
// v0.4.11
func upgradeV0_4_11(db *dbs.DB) error {
- // 升级ns端口
+ // 鍗囩骇ns绔彛
{
// TCP
{
@@ -751,19 +753,19 @@ func upgradeV1_2_1(db *dbs.DB) error {
// 1.2.10
func upgradeV1_2_10(db *dbs.DB) error {
{
- type OldGlobalConfig struct {
- // HTTP & HTTPS相关配置
- HTTPAll struct {
- DomainAuditingIsOn bool `yaml:"domainAuditingIsOn" json:"domainAuditingIsOn"` // 域名是否需要审核
- DomainAuditingPrompt string `yaml:"domainAuditingPrompt" json:"domainAuditingPrompt"` // 域名审核的提示
- } `yaml:"httpAll" json:"httpAll"`
+ type OldGlobalConfig struct {
+ // HTTP & HTTPS鐩稿叧閰嶇疆
+ HTTPAll struct {
+ DomainAuditingIsOn bool `yaml:"domainAuditingIsOn" json:"domainAuditingIsOn"` // 域名是否需要审核
+ DomainAuditingPrompt string `yaml:"domainAuditingPrompt" json:"domainAuditingPrompt"` // 域名审核提示
+ } `yaml:"httpAll" json:"httpAll"`
- TCPAll struct {
- PortRangeMin int `yaml:"portRangeMin" json:"portRangeMin"` // 最小端口
- PortRangeMax int `yaml:"portRangeMax" json:"portRangeMax"` // 最大端口
- DenyPorts []int `yaml:"denyPorts" json:"denyPorts"` // 禁止使用的端口
- } `yaml:"tcpAll" json:"tcpAll"`
- }
+ TCPAll struct {
+ PortRangeMin int `yaml:"portRangeMin" json:"portRangeMin"` // 最小端口
+ PortRangeMax int `yaml:"portRangeMax" json:"portRangeMax"` // 最大端口
+ DenyPorts []int `yaml:"denyPorts" json:"denyPorts"` // 禁止端口
+ } `yaml:"tcpAll" json:"tcpAll"`
+ }
globalConfigValue, err := db.FindCol(0, "SELECT value FROM edgeSysSettings WHERE code='serverGlobalConfig'")
if err != nil {
@@ -1023,7 +1025,7 @@ func upgradeV1_3_2(db *dbs.DB) error {
}
}
- err = addRuleToGroup(ruleGroup.GetInt64("id"), "7010", "SQL注入检测", []*firewallconfigs.HTTPFirewallActionConfig{
+ err = addRuleToGroup(ruleGroup.GetInt64("id"), "7010", "SQL注入检测", []*firewallconfigs.HTTPFirewallActionConfig{
{
Code: firewallconfigs.HTTPFirewallActionPage,
Options: maps.Map{"status": 403, "body": ""},
@@ -1131,7 +1133,7 @@ func upgradeV1_3_2(db *dbs.DB) error {
}
}
- err = addRuleToGroup(ruleGroup.GetInt64("id"), "1010", "XSS攻击检测", []*firewallconfigs.HTTPFirewallActionConfig{
+ err = addRuleToGroup(ruleGroup.GetInt64("id"), "1010", "XSS攻击检测", []*firewallconfigs.HTTPFirewallActionConfig{
{
Code: firewallconfigs.HTTPFirewallActionPage,
Options: maps.Map{"status": 403, "body": ""},
@@ -1233,8 +1235,8 @@ func upgradeV1_3_4(db *dbs.DB) error {
// 1.4.4
func upgradeV1_4_4(db *dbs.DB) error {
- // 检查 encryption 字段是否已存在
- col, err := db.FindCol(0, "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='edgeHTTPWebs' AND COLUMN_NAME='encryption'")
+ // 检查 encryption 字段是否已存在
+ col, err := db.FindCol(0, "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='edgeHTTPWebs' AND COLUMN_NAME='encryption'")
if err != nil {
return err
}
@@ -1253,3 +1255,30 @@ func upgradeV1_4_4(db *dbs.DB) error {
return nil
}
+
+// 1.4.8
+func upgradeV1_4_8(db *dbs.DB) error {
+ return createHTTPDNSTables(db)
+}
+
+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 `edgeHTTPDNSApps` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`appId` varchar(64) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`primaryClusterId` bigint unsigned DEFAULT '0',`backupClusterId` bigint unsigned DEFAULT '0',`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 `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应用域名表(应用绑定的业务域名)'",
+ "CREATE TABLE IF NOT EXISTS `edgeHTTPDNSCustomRules` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`appId` bigint unsigned DEFAULT '0',`domainId` bigint unsigned DEFAULT '0',`ruleName` varchar(255) DEFAULT NULL,`lineScope` varchar(64) DEFAULT NULL,`lineCarrier` varchar(64) DEFAULT NULL,`lineRegion` varchar(64) DEFAULT NULL,`lineProvince` varchar(64) DEFAULT NULL,`lineContinent` varchar(64) DEFAULT NULL,`lineCountry` varchar(128) DEFAULT NULL,`ttl` int unsigned DEFAULT '30',`isOn` tinyint unsigned DEFAULT '1',`priority` int unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),KEY `domainId_isOn_priority` (`domainId`,`isOn`,`priority`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS自定义解析规则表(按线路/地域匹配)'",
+ "CREATE TABLE IF NOT EXISTS `edgeHTTPDNSCustomRuleRecords` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`ruleId` bigint unsigned DEFAULT '0',`recordType` varchar(32) DEFAULT NULL,`recordValue` varchar(255) DEFAULT NULL,`weight` int unsigned DEFAULT '0',`sort` int unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),KEY `ruleId` (`ruleId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS自定义规则记录值表(A/AAAA及权重)'",
+ "CREATE TABLE IF NOT EXISTS `edgeHTTPDNSAccessLogs` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`requestId` varchar(128) DEFAULT NULL,`clusterId` bigint unsigned DEFAULT '0',`nodeId` bigint unsigned DEFAULT '0',`appId` varchar(64) DEFAULT NULL,`appName` varchar(255) DEFAULT NULL,`domain` varchar(255) DEFAULT NULL,`qtype` varchar(16) DEFAULT NULL,`clientIP` varchar(64) DEFAULT NULL,`clientRegion` varchar(255) DEFAULT NULL,`carrier` varchar(128) DEFAULT NULL,`sdkVersion` varchar(64) DEFAULT NULL,`os` varchar(64) DEFAULT NULL,`resultIPs` text,`status` varchar(32) DEFAULT NULL,`errorCode` varchar(64) DEFAULT NULL,`costMs` int unsigned DEFAULT '0',`createdAt` bigint unsigned DEFAULT '0',`day` varchar(8) DEFAULT NULL,`summary` text,PRIMARY KEY (`id`),UNIQUE KEY `requestId_nodeId` (`requestId`,`nodeId`),KEY `day_cluster_node_domain_status_createdAt` (`day`,`clusterId`,`nodeId`,`domain`,`status`,`createdAt`),KEY `appId` (`appId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS访问日志表(解析请求与结果)'",
+ "CREATE TABLE IF NOT EXISTS `edgeHTTPDNSRuntimeLogs` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`clusterId` bigint unsigned DEFAULT '0',`nodeId` bigint unsigned DEFAULT '0',`level` varchar(32) DEFAULT NULL,`type` varchar(64) DEFAULT NULL,`module` varchar(64) DEFAULT NULL,`description` text,`count` bigint unsigned DEFAULT '1',`requestId` varchar(128) DEFAULT NULL,`createdAt` bigint unsigned DEFAULT '0',`day` varchar(8) DEFAULT NULL,PRIMARY KEY (`id`),KEY `day_cluster_node_level_createdAt` (`day`,`clusterId`,`nodeId`,`level`,`createdAt`),KEY `requestId` (`requestId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS运行日志表(节点运行与异常日志)'",
+ }
+ for _, sql := range sqls {
+ if _, err := db.Exec(sql); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+
diff --git a/EdgeAPI/internal/tasks/node_task_extractor.go b/EdgeAPI/internal/tasks/node_task_extractor.go
index 99b6567..82b4bbc 100644
--- a/EdgeAPI/internal/tasks/node_task_extractor.go
+++ b/EdgeAPI/internal/tasks/node_task_extractor.go
@@ -46,7 +46,7 @@ func (this *NodeTaskExtractor) Loop() error {
// 这里不解锁,是为了让任务N秒钟之内只运行一次
- for _, role := range []string{nodeconfigs.NodeRoleNode, nodeconfigs.NodeRoleDNS} {
+ for _, role := range []string{nodeconfigs.NodeRoleNode, nodeconfigs.NodeRoleDNS, nodeconfigs.NodeRoleHTTPDNS} {
err := models.SharedNodeTaskDAO.ExtractAllClusterTasks(nil, role)
if err != nil {
return err
diff --git a/EdgeAdmin/internal/const/const.go b/EdgeAdmin/internal/const/const.go
index fb41589..2d7b7e8 100644
--- a/EdgeAdmin/internal/const/const.go
+++ b/EdgeAdmin/internal/const/const.go
@@ -1,9 +1,9 @@
package teaconst
const (
- Version = "1.4.7" //1.3.9
+ Version = "1.4.8" //1.3.9
- APINodeVersion = "1.4.7" //1.3.9
+ APINodeVersion = "1.4.8" //1.3.9
ProductName = "Edge Admin"
ProcessName = "edge-admin"
diff --git a/EdgeAdmin/internal/rpc/rpc_client.go b/EdgeAdmin/internal/rpc/rpc_client.go
index 753a4e7..0bd112c 100644
--- a/EdgeAdmin/internal/rpc/rpc_client.go
+++ b/EdgeAdmin/internal/rpc/rpc_client.go
@@ -349,6 +349,38 @@ func (this *RPCClient) DNSTaskRPC() pb.DNSTaskServiceClient {
return pb.NewDNSTaskServiceClient(this.pickConn())
}
+func (this *RPCClient) HTTPDNSClusterRPC() pb.HTTPDNSClusterServiceClient {
+ return pb.NewHTTPDNSClusterServiceClient(this.pickConn())
+}
+
+func (this *RPCClient) HTTPDNSNodeRPC() pb.HTTPDNSNodeServiceClient {
+ return pb.NewHTTPDNSNodeServiceClient(this.pickConn())
+}
+
+func (this *RPCClient) HTTPDNSAppRPC() pb.HTTPDNSAppServiceClient {
+ return pb.NewHTTPDNSAppServiceClient(this.pickConn())
+}
+
+func (this *RPCClient) HTTPDNSDomainRPC() pb.HTTPDNSDomainServiceClient {
+ return pb.NewHTTPDNSDomainServiceClient(this.pickConn())
+}
+
+func (this *RPCClient) HTTPDNSRuleRPC() pb.HTTPDNSRuleServiceClient {
+ return pb.NewHTTPDNSRuleServiceClient(this.pickConn())
+}
+
+func (this *RPCClient) HTTPDNSAccessLogRPC() pb.HTTPDNSAccessLogServiceClient {
+ return pb.NewHTTPDNSAccessLogServiceClient(this.pickConn())
+}
+
+func (this *RPCClient) HTTPDNSRuntimeLogRPC() pb.HTTPDNSRuntimeLogServiceClient {
+ return pb.NewHTTPDNSRuntimeLogServiceClient(this.pickConn())
+}
+
+func (this *RPCClient) HTTPDNSSandboxRPC() pb.HTTPDNSSandboxServiceClient {
+ return pb.NewHTTPDNSSandboxServiceClient(this.pickConn())
+}
+
func (this *RPCClient) ACMEUserRPC() pb.ACMEUserServiceClient {
return pb.NewACMEUserServiceClient(this.pickConn())
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go
index 7a19ee8..2d4a89b 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go
@@ -17,6 +17,10 @@ func (this *AppAction) Init() {
func (this *AppAction) RunGet(params struct {
AppId int64
}) {
- app := pickApp(params.AppId)
+ app, err := findAppMap(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.RedirectURL("/httpdns/apps/domains?appId=" + strconv.FormatInt(app.GetInt64("id"), 10))
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go
index 7a1e314..6304160 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings.go
@@ -5,8 +5,9 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
+ "github.com/iwind/TeaGo/maps"
)
type AppSettingsAction struct {
@@ -22,33 +23,45 @@ func (this *AppSettingsAction) RunGet(params struct {
Section string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
- app := pickApp(params.AppId)
- // 顶部 tabbar
- httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "settings")
+ app, err := findAppMap(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
+ httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "settings")
- // 当前选中的 section
section := params.Section
if len(section) == 0 {
section = "basic"
}
this.Data["activeSection"] = section
- appIdStr := strconv.FormatInt(params.AppId, 10)
- this.Data["leftMenuItems"] = []map[string]interface{}{
+
+ appIDStr := strconv.FormatInt(app.GetInt64("id"), 10)
+ this.Data["leftMenuItems"] = []maps.Map{
{
"name": "基础配置",
- "url": "/httpdns/apps/app/settings?appId=" + appIdStr + "§ion=basic",
+ "url": "/httpdns/apps/app/settings?appId=" + appIDStr + "§ion=basic",
"isActive": section == "basic",
},
{
"name": "认证与密钥",
- "url": "/httpdns/apps/app/settings?appId=" + appIdStr + "§ion=auth",
+ "url": "/httpdns/apps/app/settings?appId=" + appIDStr + "§ion=auth",
"isActive": section == "auth",
},
}
- settings := loadAppSettings(app)
- this.Data["clusters"] = policies.LoadAvailableDeployClusters()
+ settings := maps.Map{
+ "appId": app.GetString("appId"),
+ "appStatus": app.GetBool("isOn"),
+ "primaryClusterId": app.GetInt64("primaryClusterId"),
+ "backupClusterId": app.GetInt64("backupClusterId"),
+ "signEnabled": app.GetBool("signEnabled"),
+ "signSecretPlain": app.GetString("signSecretPlain"),
+ "signSecretMasked": app.GetString("signSecretMasked"),
+ "signSecretUpdatedAt": app.GetString("signSecretUpdated"),
+ }
this.Data["app"] = app
this.Data["settings"] = settings
this.Show()
@@ -57,36 +70,37 @@ func (this *AppSettingsAction) RunGet(params struct {
func (this *AppSettingsAction) RunPost(params struct {
AppId int64
- AppStatus bool
- PrimaryClusterId int64
- BackupClusterId int64
+ AppStatus bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
- params.Must.Field("appId", params.AppId).Gt(0, "please select app")
- params.Must.Field("primaryClusterId", params.PrimaryClusterId).Gt(0, "please select primary cluster")
- if params.BackupClusterId > 0 && params.BackupClusterId == params.PrimaryClusterId {
- this.FailField("backupClusterId", "backup cluster must be different from primary cluster")
+ params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
+
+ appResp, err := this.RPC().HTTPDNSAppRPC().FindHTTPDNSApp(this.AdminContext(), &pb.FindHTTPDNSAppRequest{
+ AppDbId: params.AppId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ if appResp.GetApp() == nil {
+ this.Fail("找不到对应的应用")
+ return
}
- app := pickApp(params.AppId)
- settings := loadAppSettings(app)
- settings["appStatus"] = params.AppStatus
- settings["primaryClusterId"] = params.PrimaryClusterId
- settings["backupClusterId"] = params.BackupClusterId
+ _, err = this.RPC().HTTPDNSAppRPC().UpdateHTTPDNSApp(this.AdminContext(), &pb.UpdateHTTPDNSAppRequest{
+ AppDbId: params.AppId,
+ Name: appResp.GetApp().GetName(),
+ PrimaryClusterId: appResp.GetApp().GetPrimaryClusterId(),
+ BackupClusterId: appResp.GetApp().GetBackupClusterId(),
+ IsOn: params.AppStatus,
+ UserId: appResp.GetApp().GetUserId(),
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
- // SNI strategy is fixed to level2 empty.
- settings["sniPolicy"] = "level2"
- settings["level2Mode"] = "empty"
- settings["publicSniDomain"] = ""
- settings["echFallbackPolicy"] = "level2"
- settings["ecsMode"] = "off"
- settings["ecsIPv4Prefix"] = 24
- settings["ecsIPv6Prefix"] = 56
- settings["pinningMode"] = "off"
- settings["sanMode"] = "off"
-
- saveAppSettings(app.GetInt64("id"), settings)
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go
index c1391e2..a282c5d 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsResetSignSecret.go
@@ -2,6 +2,7 @@ package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
@@ -13,11 +14,16 @@ func (this *AppSettingsResetSignSecretAction) RunPost(params struct {
AppId int64
Must *actions.Must
- CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
- app := pickApp(params.AppId)
- resetSignSecret(app)
+ _, err := this.RPC().HTTPDNSAppRPC().ResetHTTPDNSAppSignSecret(this.AdminContext(), &pb.ResetHTTPDNSAppSignSecretRequest{
+ AppDbId: params.AppId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go
index 6b86394..57b9225 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettingsToggleSignEnabled.go
@@ -2,6 +2,7 @@ package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
@@ -14,14 +15,17 @@ func (this *AppSettingsToggleSignEnabledAction) RunPost(params struct {
IsOn int
Must *actions.Must
- CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
- app := pickApp(params.AppId)
- settings := loadAppSettings(app)
- settings["signEnabled"] = params.IsOn == 1
- saveAppSettings(app.GetInt64("id"), settings)
+ _, err := this.RPC().HTTPDNSAppRPC().UpdateHTTPDNSAppSignEnabled(this.AdminContext(), &pb.UpdateHTTPDNSAppSignEnabledRequest{
+ AppDbId: params.AppId,
+ SignEnabled: params.IsOn == 1,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings_store.go
deleted file mode 100644
index 4c327a2..0000000
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/appSettings_store.go
+++ /dev/null
@@ -1,223 +0,0 @@
-package apps
-
-import (
- "fmt"
- "sync"
- "time"
-
- "github.com/iwind/TeaGo/maps"
-)
-
-var appSettingsStore = struct {
- sync.RWMutex
- data map[int64]maps.Map
-}{
- data: map[int64]maps.Map{},
-}
-
-func defaultAppSettings(app maps.Map) maps.Map {
- signSecretPlain := randomPlainSecret("ss")
- return maps.Map{
- "appId": app.GetString("appId"),
- "primaryClusterId": app.GetInt64("clusterId"),
- "backupClusterId": int64(0),
- "signSecretPlain": signSecretPlain,
- "signSecretMasked": maskSecret(signSecretPlain),
- "signSecretUpdatedAt": "2026-02-20 12:30:00",
- "appStatus": app.GetBool("isOn"),
- "defaultTTL": 30,
- "fallbackTimeoutMs": 300,
- "sniPolicy": "level2",
- "level2Mode": "empty",
- "publicSniDomain": "",
- "echFallbackPolicy": "level2",
- "ecsMode": "off",
- "ecsIPv4Prefix": 24,
- "ecsIPv6Prefix": 56,
- "pinningMode": "off",
- "sanMode": "off",
- "signEnabled": true,
- }
-}
-
-func cloneSettings(settings maps.Map) maps.Map {
- return maps.Map{
- "appId": settings.GetString("appId"),
- "primaryClusterId": settings.GetInt64("primaryClusterId"),
- "backupClusterId": settings.GetInt64("backupClusterId"),
- "signSecretPlain": settings.GetString("signSecretPlain"),
- "signSecretMasked": settings.GetString("signSecretMasked"),
- "signSecretUpdatedAt": settings.GetString("signSecretUpdatedAt"),
- "appStatus": settings.GetBool("appStatus"),
- "defaultTTL": settings.GetInt("defaultTTL"),
- "fallbackTimeoutMs": settings.GetInt("fallbackTimeoutMs"),
- "sniPolicy": settings.GetString("sniPolicy"),
- "level2Mode": settings.GetString("level2Mode"),
- "publicSniDomain": settings.GetString("publicSniDomain"),
- "echFallbackPolicy": settings.GetString("echFallbackPolicy"),
- "ecsMode": settings.GetString("ecsMode"),
- "ecsIPv4Prefix": settings.GetInt("ecsIPv4Prefix"),
- "ecsIPv6Prefix": settings.GetInt("ecsIPv6Prefix"),
- "pinningMode": settings.GetString("pinningMode"),
- "sanMode": settings.GetString("sanMode"),
- "signEnabled": settings.GetBool("signEnabled"),
- }
-}
-
-func loadAppSettings(app maps.Map) maps.Map {
- appId := app.GetInt64("id")
- appSettingsStore.RLock()
- settings, ok := appSettingsStore.data[appId]
- appSettingsStore.RUnlock()
- if ok {
- if ensureSettingsFields(settings) {
- saveAppSettings(appId, settings)
- }
- return cloneSettings(settings)
- }
-
- settings = defaultAppSettings(app)
- saveAppSettings(appId, settings)
- return cloneSettings(settings)
-}
-
-func saveAppSettings(appId int64, settings maps.Map) {
- appSettingsStore.Lock()
- appSettingsStore.data[appId] = cloneSettings(settings)
- appSettingsStore.Unlock()
-}
-
-func deleteAppSettings(appId int64) {
- appSettingsStore.Lock()
- delete(appSettingsStore.data, appId)
- appSettingsStore.Unlock()
-}
-
-func resetSignSecret(app maps.Map) maps.Map {
- settings := loadAppSettings(app)
- signSecretPlain := randomPlainSecret("ss")
- settings["signSecretPlain"] = signSecretPlain
- settings["signSecretMasked"] = maskSecret(signSecretPlain)
- settings["signSecretUpdatedAt"] = nowDateTime()
- saveAppSettings(app.GetInt64("id"), settings)
- return settings
-}
-
-func nowDateTime() string {
- return time.Now().Format("2006-01-02 15:04:05")
-}
-
-func randomPlainSecret(prefix string) string {
- suffix := time.Now().UnixNano() & 0xffff
- return fmt.Sprintf("%s_%016x", prefix, suffix)
-}
-
-func maskSecret(secret string) string {
- if len(secret) < 4 {
- return "******"
- }
-
- prefix := ""
- for i := 0; i < len(secret); i++ {
- if secret[i] == '_' {
- prefix = secret[:i+1]
- break
- }
- }
- if len(prefix) == 0 {
- prefix = secret[:2]
- }
-
- if len(secret) <= 8 {
- return prefix + "xxxx"
- }
- return prefix + "xxxxxxxx" + secret[len(secret)-4:]
-}
-
-func ensureSettingsFields(settings maps.Map) bool {
- changed := false
-
- if settings.GetInt64("primaryClusterId") <= 0 {
- settings["primaryClusterId"] = int64(1)
- changed = true
- }
- if settings.GetInt64("backupClusterId") < 0 {
- settings["backupClusterId"] = int64(0)
- changed = true
- }
- if settings.GetInt64("backupClusterId") > 0 && settings.GetInt64("backupClusterId") == settings.GetInt64("primaryClusterId") {
- settings["backupClusterId"] = int64(0)
- changed = true
- }
-
- signSecretPlain := settings.GetString("signSecretPlain")
- if len(signSecretPlain) == 0 {
- signSecretPlain = randomPlainSecret("ss")
- settings["signSecretPlain"] = signSecretPlain
- changed = true
- }
- if len(settings.GetString("signSecretMasked")) == 0 {
- settings["signSecretMasked"] = maskSecret(signSecretPlain)
- changed = true
- }
- if len(settings.GetString("signSecretUpdatedAt")) == 0 {
- settings["signSecretUpdatedAt"] = nowDateTime()
- changed = true
- }
-
- if len(settings.GetString("sniPolicy")) == 0 {
- settings["sniPolicy"] = "level2"
- changed = true
- } else if settings.GetString("sniPolicy") != "level2" {
- settings["sniPolicy"] = "level2"
- changed = true
- }
- if settings.GetString("level2Mode") != "empty" {
- settings["level2Mode"] = "empty"
- changed = true
- }
- if len(settings.GetString("publicSniDomain")) > 0 {
- settings["publicSniDomain"] = ""
- changed = true
- }
- if len(settings.GetString("echFallbackPolicy")) == 0 {
- settings["echFallbackPolicy"] = "level2"
- changed = true
- } else if settings.GetString("echFallbackPolicy") != "level2" {
- settings["echFallbackPolicy"] = "level2"
- changed = true
- }
- if settings.GetString("ecsMode") != "off" {
- settings["ecsMode"] = "off"
- changed = true
- }
- if settings.GetInt("ecsIPv4Prefix") <= 0 {
- settings["ecsIPv4Prefix"] = 24
- changed = true
- }
- if settings.GetInt("ecsIPv6Prefix") <= 0 {
- settings["ecsIPv6Prefix"] = 56
- changed = true
- }
- if settings.GetString("pinningMode") != "off" {
- settings["pinningMode"] = "off"
- changed = true
- }
- if settings.GetString("sanMode") != "off" {
- settings["sanMode"] = "off"
- changed = true
- }
-
- return changed
-}
-
-// LoadAppSettingsByAppID exposes app settings for other httpdns sub-modules
-// such as sandbox mock responses.
-func LoadAppSettingsByAppID(appID string) maps.Map {
- for _, app := range mockApps() {
- if app.GetString("appId") == appID {
- return loadAppSettings(app)
- }
- }
- return nil
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/create.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/create.go
new file mode 100644
index 0000000..4b734ce
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/create.go
@@ -0,0 +1,92 @@
+package apps
+
+import (
+ "strconv"
+ "time"
+
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ "github.com/iwind/TeaGo/actions"
+ "github.com/iwind/TeaGo/maps"
+)
+
+type CreateAction struct {
+ actionutils.ParentAction
+}
+
+func (this *CreateAction) Init() {
+ this.Nav("", "", "create")
+}
+
+func (this *CreateAction) RunGet(params struct{}) {
+ clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ clusters := make([]maps.Map, 0, len(clusterResp.GetClusters()))
+ for _, cluster := range clusterResp.GetClusters() {
+ clusters = append(clusters, maps.Map{
+ "id": cluster.GetId(),
+ "name": cluster.GetName(),
+ })
+ }
+ this.Data["clusters"] = clusters
+
+ defaultPrimaryClusterId := int64(0)
+ for _, cluster := range clusterResp.GetClusters() {
+ if cluster.GetIsDefault() {
+ defaultPrimaryClusterId = cluster.GetId()
+ break
+ }
+ }
+ if defaultPrimaryClusterId <= 0 && len(clusters) > 0 {
+ defaultPrimaryClusterId = clusters[0].GetInt64("id")
+ }
+ this.Data["defaultPrimaryClusterId"] = defaultPrimaryClusterId
+
+ defaultBackupClusterId := int64(0)
+ for _, cluster := range clusters {
+ clusterId := cluster.GetInt64("id")
+ if clusterId > 0 && clusterId != defaultPrimaryClusterId {
+ defaultBackupClusterId = clusterId
+ break
+ }
+ }
+ this.Data["defaultBackupClusterId"] = defaultBackupClusterId
+
+ this.Show()
+}
+
+func (this *CreateAction) RunPost(params struct {
+ Name string
+ PrimaryClusterId int64
+ BackupClusterId int64
+ UserId int64
+
+ Must *actions.Must
+ CSRF *actionutils.CSRF
+}) {
+ params.Must.Field("name", params.Name).Require("请输入应用名称")
+ params.Must.Field("primaryClusterId", params.PrimaryClusterId).Gt(0, "请输入主服务集群")
+ if params.BackupClusterId > 0 && params.BackupClusterId == params.PrimaryClusterId {
+ this.FailField("backupClusterId", "备用服务集群必须和主服务集群不一致")
+ }
+
+ createResp, err := this.RPC().HTTPDNSAppRPC().CreateHTTPDNSApp(this.AdminContext(), &pb.CreateHTTPDNSAppRequest{
+ Name: params.Name,
+ AppId: "app" + strconv.FormatInt(time.Now().UnixNano()%1_000_000_000_000, 36),
+ PrimaryClusterId: params.PrimaryClusterId,
+ BackupClusterId: params.BackupClusterId,
+ IsOn: true,
+ SignEnabled: true,
+ UserId: params.UserId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
+ this.Data["appId"] = createResp.GetAppDbId()
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/createPopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/createPopup.go
deleted file mode 100644
index a71a42c..0000000
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/createPopup.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package apps
-
-import (
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
- "github.com/iwind/TeaGo/actions"
- "github.com/iwind/TeaGo/maps"
-)
-
-type CreatePopupAction struct {
- actionutils.ParentAction
-}
-
-func (this *CreatePopupAction) Init() {
- this.Nav("", "", "")
-}
-
-func (this *CreatePopupAction) RunGet(params struct{}) {
- clusters := policies.LoadAvailableDeployClusters()
- this.Data["clusters"] = clusters
-
- defaultPrimaryClusterId := policies.LoadDefaultClusterID()
- if defaultPrimaryClusterId <= 0 && len(clusters) > 0 {
- defaultPrimaryClusterId = clusters[0].GetInt64("id")
- }
- this.Data["defaultPrimaryClusterId"] = defaultPrimaryClusterId
-
- defaultBackupClusterId := int64(0)
- for _, cluster := range clusters {
- clusterId := cluster.GetInt64("id")
- if clusterId > 0 && clusterId != defaultPrimaryClusterId {
- defaultBackupClusterId = clusterId
- break
- }
- }
- this.Data["defaultBackupClusterId"] = defaultBackupClusterId
-
- // Mock users for dropdown
- this.Data["users"] = []maps.Map{
- {"id": int64(1), "name": "User A", "username": "zhangsan"},
- {"id": int64(2), "name": "User B", "username": "lisi"},
- {"id": int64(3), "name": "User C", "username": "wangwu"},
- }
-
- this.Show()
-}
-
-func (this *CreatePopupAction) RunPost(params struct {
- Name string
- PrimaryClusterId int64
- BackupClusterId int64
- UserId int64
-
- Must *actions.Must
- CSRF *actionutils.CSRF
-}) {
- params.Must.Field("name", params.Name).Require("please input app name")
- params.Must.Field("primaryClusterId", params.PrimaryClusterId).Gt(0, "please select primary cluster")
- if params.BackupClusterId > 0 && params.BackupClusterId == params.PrimaryClusterId {
- this.FailField("backupClusterId", "backup cluster must be different from primary cluster")
- }
- this.Success()
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go
index 1257bf1..19a75bd 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecords.go
@@ -20,25 +20,32 @@ func (this *CustomRecordsAction) RunGet(params struct {
}) {
httpdnsutils.AddLeftMenu(this.Parent())
- app := pickApp(params.AppId)
- // 自定义解析属于域名管理子页,顶部沿用应用 tabbar(高亮域名列表)
- httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "domains")
+ app, err := findAppMap(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "domains")
- domains := mockDomains(app.GetInt64("id"))
- domain := pickDomainFromDomains(domains, params.DomainId)
- domainName := domain.GetString("name")
+ domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ domain := findDomainMap(domains, params.DomainId)
records := make([]maps.Map, 0)
- for _, record := range loadCustomRecords(app.GetInt64("id")) {
- if len(domainName) > 0 && record.GetString("domain") != domainName {
- continue
+ if domain.GetInt64("id") > 0 {
+ records, err = listCustomRuleMaps(this.Parent(), domain.GetInt64("id"))
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ for _, record := range records {
+ record["domain"] = domain.GetString("name")
+ record["lineText"] = buildLineText(record)
+ record["recordValueText"] = buildRecordValueText(record)
}
- records = append(records, record)
- }
-
- for _, record := range records {
- record["lineText"] = buildLineText(record)
- record["recordValueText"] = buildRecordValueText(record)
}
this.Data["app"] = app
@@ -47,17 +54,3 @@ func (this *CustomRecordsAction) RunGet(params struct {
this.Show()
}
-func pickDomainFromDomains(domains []maps.Map, domainID int64) maps.Map {
- if len(domains) == 0 {
- return maps.Map{}
- }
- if domainID <= 0 {
- return domains[0]
- }
- for _, domain := range domains {
- if domain.GetInt64("id") == domainID {
- return domain
- }
- }
- return domains[0]
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go
index 22d6d63..93c1638 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsCreatePopup.go
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
@@ -23,11 +24,18 @@ func (this *CustomRecordsCreatePopupAction) RunGet(params struct {
DomainId int64
RecordId int64
}) {
- app := pickApp(params.AppId)
- this.Data["app"] = app
- domains := mockDomains(app.GetInt64("id"))
- domain := pickDomainFromDomains(domains, params.DomainId)
- this.Data["domain"] = domain
+ app, err := findAppMap(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
+ domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ domain := findDomainMap(domains, params.DomainId)
record := maps.Map{
"id": int64(0),
@@ -45,77 +53,35 @@ func (this *CustomRecordsCreatePopupAction) RunGet(params struct {
"recordItemsJson": `[{"type":"A","value":"","weight":100}]`,
}
- if params.RecordId > 0 {
- existing := findCustomRecord(app.GetInt64("id"), params.RecordId)
- if len(existing) > 0 {
- record["id"] = existing.GetInt64("id")
- if len(record.GetString("domain")) == 0 {
- record["domain"] = existing.GetString("domain")
+ if params.RecordId > 0 && domain.GetInt64("id") > 0 {
+ rules, err := listCustomRuleMaps(this.Parent(), domain.GetInt64("id"))
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ for _, rule := range rules {
+ if rule.GetInt64("id") != params.RecordId {
+ continue
}
-
- record["lineScope"] = strings.TrimSpace(existing.GetString("lineScope"))
- record["lineCarrier"] = strings.TrimSpace(existing.GetString("lineCarrier"))
- record["lineRegion"] = strings.TrimSpace(existing.GetString("lineRegion"))
- record["lineProvince"] = strings.TrimSpace(existing.GetString("lineProvince"))
- record["lineContinent"] = strings.TrimSpace(existing.GetString("lineContinent"))
- record["lineCountry"] = strings.TrimSpace(existing.GetString("lineCountry"))
- record["ruleName"] = existing.GetString("ruleName")
- record["weightEnabled"] = existing.GetBool("weightEnabled")
- record["ttl"] = existing.GetInt("ttl")
- record["isOn"] = existing.GetBool("isOn")
-
- recordItems := make([]maps.Map, 0)
- recordType := strings.ToUpper(strings.TrimSpace(existing.GetString("recordType")))
- values, _ := existing["recordValues"].([]maps.Map)
- for _, item := range values {
- itemType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
- if len(itemType) == 0 {
- itemType = recordType
- }
- if itemType != "A" && itemType != "AAAA" {
- itemType = "A"
- }
-
- recordItems = append(recordItems, maps.Map{
- "type": itemType,
- "value": strings.TrimSpace(item.GetString("value")),
- "weight": item.GetInt("weight"),
- })
- }
- if len(recordItems) == 0 {
- recordItems = append(recordItems, maps.Map{
- "type": "A",
- "value": "",
- "weight": 100,
- })
- }
- record["recordItemsJson"] = marshalJSON(recordItems, "[]")
+ record["id"] = rule.GetInt64("id")
+ record["domain"] = domain.GetString("name")
+ record["lineScope"] = rule.GetString("lineScope")
+ record["lineCarrier"] = defaultLineField(rule.GetString("lineCarrier"))
+ record["lineRegion"] = defaultLineField(rule.GetString("lineRegion"))
+ record["lineProvince"] = defaultLineField(rule.GetString("lineProvince"))
+ record["lineContinent"] = defaultLineField(rule.GetString("lineContinent"))
+ record["lineCountry"] = defaultLineField(rule.GetString("lineCountry"))
+ record["ruleName"] = rule.GetString("ruleName")
+ record["weightEnabled"] = rule.GetBool("weightEnabled")
+ record["ttl"] = rule.GetInt("ttl")
+ record["isOn"] = rule.GetBool("isOn")
+ record["recordItemsJson"] = marshalJSON(rule["recordValues"], "[]")
+ break
}
}
- if record.GetString("lineScope") != "china" && record.GetString("lineScope") != "overseas" {
- if len(strings.TrimSpace(record.GetString("lineContinent"))) > 0 || len(strings.TrimSpace(record.GetString("lineCountry"))) > 0 {
- record["lineScope"] = "overseas"
- } else {
- record["lineScope"] = "china"
- }
- }
- if len(record.GetString("lineCarrier")) == 0 {
- record["lineCarrier"] = "默认"
- }
- if len(record.GetString("lineRegion")) == 0 {
- record["lineRegion"] = "默认"
- }
- if len(record.GetString("lineProvince")) == 0 {
- record["lineProvince"] = "默认"
- }
- if len(record.GetString("lineContinent")) == 0 {
- record["lineContinent"] = "默认"
- }
- if len(record.GetString("lineCountry")) == 0 {
- record["lineCountry"] = "默认"
- }
-
+ this.Data["app"] = app
+ this.Data["domain"] = domain
this.Data["record"] = record
this.Data["isEditing"] = params.RecordId > 0
this.Show()
@@ -138,40 +104,26 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
RuleName string
RecordItemsJSON string
WeightEnabled bool
- TTL int
+ Ttl int
IsOn bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
- params.Must.Field("appId", params.AppId).Gt(0, "please select app")
+ params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
+ params.Must.Field("domainId", params.DomainId).Gt(0, "请选择所属域名")
- params.Domain = strings.TrimSpace(params.Domain)
params.LineScope = strings.ToLower(strings.TrimSpace(params.LineScope))
- params.RuleName = strings.TrimSpace(params.RuleName)
- params.RecordItemsJSON = strings.TrimSpace(params.RecordItemsJSON)
-
- domain := maps.Map{}
- if params.DomainId > 0 {
- domain = pickDomainFromDomains(mockDomains(params.AppId), params.DomainId)
- }
- if len(domain) > 0 {
- params.Domain = strings.TrimSpace(domain.GetString("name"))
- }
- if len(params.Domain) == 0 {
- this.Fail("please select domain")
- return
- }
-
if params.LineScope != "china" && params.LineScope != "overseas" {
params.LineScope = "china"
}
+ params.RuleName = strings.TrimSpace(params.RuleName)
if len(params.RuleName) == 0 {
- this.Fail("please input rule name")
+ this.Fail("请输入规则名称")
return
}
- if params.TTL <= 0 || params.TTL > 86400 {
- this.Fail("ttl should be in 1-86400")
+ if params.Ttl <= 0 || params.Ttl > 86400 {
+ this.Fail("TTL值必须在 1-86400 范围内")
return
}
@@ -181,11 +133,11 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
return
}
if len(recordValues) == 0 {
- this.Fail("please input record values")
+ this.Fail("请输入解析记录值")
return
}
if len(recordValues) > 10 {
- this.Fail("record values should be <= 10")
+ this.Fail("单个规则最多只能添加 10 条解析记录")
return
}
@@ -209,7 +161,6 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
if len(lineCountry) == 0 {
lineCountry = "默认"
}
-
if params.LineScope == "overseas" {
lineCarrier = ""
lineRegion = ""
@@ -219,40 +170,57 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
lineCountry = ""
}
- recordType := recordValues[0].GetString("type")
- if len(recordType) == 0 {
- recordType = "A"
+ records := make([]*pb.HTTPDNSRuleRecord, 0, len(recordValues))
+ for i, item := range recordValues {
+ records = append(records, &pb.HTTPDNSRuleRecord{
+ Id: 0,
+ RuleId: 0,
+ RecordType: item.GetString("type"),
+ RecordValue: item.GetString("value"),
+ Weight: int32(item.GetInt("weight")),
+ Sort: int32(i + 1),
+ })
}
- saveCustomRecord(params.AppId, maps.Map{
- "id": params.RecordId,
- "domain": params.Domain,
- "lineScope": params.LineScope,
- "lineCarrier": lineCarrier,
- "lineRegion": lineRegion,
- "lineProvince": lineProvince,
- "lineContinent": lineContinent,
- "lineCountry": lineCountry,
- "ruleName": params.RuleName,
- "sdnsParams": []maps.Map{},
- "recordType": recordType,
- "recordValues": recordValues,
- "weightEnabled": params.WeightEnabled,
- "ttl": params.TTL,
- "isOn": params.IsOn,
- })
+ rule := &pb.HTTPDNSCustomRule{
+ Id: params.RecordId,
+ AppId: params.AppId,
+ DomainId: params.DomainId,
+ RuleName: params.RuleName,
+ LineScope: params.LineScope,
+ LineCarrier: lineCarrier,
+ LineRegion: lineRegion,
+ LineProvince: lineProvince,
+ LineContinent: lineContinent,
+ LineCountry: lineCountry,
+ Ttl: int32(params.Ttl),
+ IsOn: params.IsOn,
+ Priority: 100,
+ Records: records,
+ }
+
+ if params.RecordId > 0 {
+ err = updateCustomRule(this.Parent(), rule)
+ } else {
+ _, err = createCustomRule(this.Parent(), rule)
+ }
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.Success()
}
func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
+ raw = strings.TrimSpace(raw)
if len(raw) == 0 {
return []maps.Map{}, nil
}
list := []maps.Map{}
if err := json.Unmarshal([]byte(raw), &list); err != nil {
- return nil, fmt.Errorf("record items json is invalid")
+ return nil, fmt.Errorf("解析记录格式不正确")
}
result := make([]maps.Map, 0, len(list))
@@ -263,10 +231,10 @@ func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
continue
}
if recordType != "A" && recordType != "AAAA" {
- return nil, fmt.Errorf("record type should be A or AAAA")
+ return nil, fmt.Errorf("记录类型只能是 A 或 AAAA")
}
if len(recordValue) == 0 {
- return nil, fmt.Errorf("record value should not be empty")
+ return nil, fmt.Errorf("记录值不能为空")
}
weight := item.GetInt("weight")
@@ -274,7 +242,7 @@ func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
weight = 100
}
if weight < 1 || weight > 100 {
- return nil, fmt.Errorf("weight should be in 1-100")
+ return nil, fmt.Errorf("权重值必须在 1-100 之间")
}
result = append(result, maps.Map{
@@ -283,7 +251,6 @@ func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
"weight": weight,
})
}
-
return result, nil
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go
index 4d8777c..27fed65 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsDelete.go
@@ -10,8 +10,12 @@ func (this *CustomRecordsDeleteAction) RunPost(params struct {
AppId int64
RecordId int64
}) {
- if params.AppId > 0 && params.RecordId > 0 {
- deleteCustomRecord(params.AppId, params.RecordId)
+ if params.RecordId > 0 {
+ err := deleteCustomRule(this.Parent(), params.RecordId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
}
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go
index 0e16fd3..8504532 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/customRecordsToggle.go
@@ -11,8 +11,12 @@ func (this *CustomRecordsToggleAction) RunPost(params struct {
RecordId int64
IsOn bool
}) {
- if params.AppId > 0 && params.RecordId > 0 {
- toggleCustomRecord(params.AppId, params.RecordId, params.IsOn)
+ if params.RecordId > 0 {
+ err := toggleCustomRule(this.Parent(), params.RecordId, params.IsOn)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
}
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/custom_records_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/custom_records_store.go
deleted file mode 100644
index d0419d1..0000000
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/custom_records_store.go
+++ /dev/null
@@ -1,244 +0,0 @@
-package apps
-
-import (
- "strconv"
- "strings"
- "sync"
- "time"
-
- "github.com/iwind/TeaGo/maps"
-)
-
-var customRecordStore = struct {
- sync.RWMutex
- nextID int64
- data map[int64][]maps.Map
-}{
- nextID: 1000,
- data: map[int64][]maps.Map{
- 1: {
- {
- "id": int64(1001),
- "domain": "api.business.com",
- "lineScope": "china",
- "lineCarrier": "电信",
- "lineRegion": "华东",
- "lineProvince": "上海",
- "ruleName": "上海电信灰度-v2",
- "sdnsParams": []maps.Map{},
- "recordType": "A",
- "recordValues": []maps.Map{{"type": "A", "value": "1.1.1.10", "weight": 100}},
- "weightEnabled": false,
- "ttl": 30,
- "isOn": true,
- "updatedAt": "2026-02-23 10:20:00",
- },
- },
- },
-}
-
-func loadCustomRecords(appID int64) []maps.Map {
- customRecordStore.RLock()
- defer customRecordStore.RUnlock()
-
- records := customRecordStore.data[appID]
- result := make([]maps.Map, 0, len(records))
- for _, record := range records {
- result = append(result, cloneCustomRecord(record))
- }
- return result
-}
-
-func countCustomRecordsByDomain(appID int64, domain string) int {
- domain = strings.ToLower(strings.TrimSpace(domain))
- if len(domain) == 0 {
- return 0
- }
-
- customRecordStore.RLock()
- defer customRecordStore.RUnlock()
-
- count := 0
- for _, record := range customRecordStore.data[appID] {
- if strings.ToLower(strings.TrimSpace(record.GetString("domain"))) == domain {
- count++
- }
- }
- return count
-}
-
-func findCustomRecord(appID int64, recordID int64) maps.Map {
- for _, record := range loadCustomRecords(appID) {
- if record.GetInt64("id") == recordID {
- return record
- }
- }
- return maps.Map{}
-}
-
-func saveCustomRecord(appID int64, record maps.Map) maps.Map {
- customRecordStore.Lock()
- defer customRecordStore.Unlock()
-
- if appID <= 0 {
- return maps.Map{}
- }
-
- record = cloneCustomRecord(record)
- recordID := record.GetInt64("id")
- if recordID <= 0 {
- customRecordStore.nextID++
- recordID = customRecordStore.nextID
- record["id"] = recordID
- }
- record["updatedAt"] = nowCustomRecordTime()
-
- records := customRecordStore.data[appID]
- found := false
- for i, oldRecord := range records {
- if oldRecord.GetInt64("id") == recordID {
- records[i] = cloneCustomRecord(record)
- found = true
- break
- }
- }
- if !found {
- records = append(records, cloneCustomRecord(record))
- }
- customRecordStore.data[appID] = records
-
- return cloneCustomRecord(record)
-}
-
-func deleteCustomRecord(appID int64, recordID int64) {
- customRecordStore.Lock()
- defer customRecordStore.Unlock()
-
- records := customRecordStore.data[appID]
- if len(records) == 0 {
- return
- }
-
- filtered := make([]maps.Map, 0, len(records))
- for _, record := range records {
- if record.GetInt64("id") == recordID {
- continue
- }
- filtered = append(filtered, record)
- }
- customRecordStore.data[appID] = filtered
-}
-
-func deleteCustomRecordsByApp(appID int64) {
- customRecordStore.Lock()
- defer customRecordStore.Unlock()
- delete(customRecordStore.data, appID)
-}
-
-func toggleCustomRecord(appID int64, recordID int64, isOn bool) {
- customRecordStore.Lock()
- defer customRecordStore.Unlock()
-
- records := customRecordStore.data[appID]
- for i, record := range records {
- if record.GetInt64("id") == recordID {
- record["isOn"] = isOn
- record["updatedAt"] = nowCustomRecordTime()
- records[i] = record
- break
- }
- }
- customRecordStore.data[appID] = records
-}
-
-func cloneCustomRecord(src maps.Map) maps.Map {
- dst := maps.Map{}
- for k, v := range src {
- switch k {
- case "sdnsParams", "recordValues":
- if list, ok := v.([]maps.Map); ok {
- cloned := make([]maps.Map, 0, len(list))
- for _, item := range list {
- m := maps.Map{}
- for k2, v2 := range item {
- m[k2] = v2
- }
- cloned = append(cloned, m)
- }
- dst[k] = cloned
- } else {
- dst[k] = []maps.Map{}
- }
- default:
- dst[k] = v
- }
- }
- return dst
-}
-
-func nowCustomRecordTime() string {
- return time.Now().Format("2006-01-02 15:04:05")
-}
-
-func buildLineText(record maps.Map) string {
- parts := []string{}
- if strings.TrimSpace(record.GetString("lineScope")) == "overseas" {
- parts = append(parts,
- strings.TrimSpace(record.GetString("lineContinent")),
- strings.TrimSpace(record.GetString("lineCountry")),
- )
- } else {
- parts = append(parts,
- strings.TrimSpace(record.GetString("lineCarrier")),
- strings.TrimSpace(record.GetString("lineRegion")),
- strings.TrimSpace(record.GetString("lineProvince")),
- )
- }
-
- finalParts := make([]string, 0, len(parts))
- for _, part := range parts {
- if len(part) == 0 || part == "默认" {
- continue
- }
- finalParts = append(finalParts, part)
- }
- if len(finalParts) == 0 {
- return "默认"
- }
- return strings.Join(finalParts, " / ")
-}
-
-func buildRecordValueText(record maps.Map) string {
- values, ok := record["recordValues"].([]maps.Map)
- if !ok || len(values) == 0 {
- return "-"
- }
-
- weightEnabled := record.GetBool("weightEnabled")
- defaultType := strings.ToUpper(strings.TrimSpace(record.GetString("recordType")))
- parts := make([]string, 0, len(values))
- for _, item := range values {
- value := strings.TrimSpace(item.GetString("value"))
- if len(value) == 0 {
- continue
- }
- recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
- if len(recordType) == 0 {
- recordType = defaultType
- }
- if recordType != "A" && recordType != "AAAA" {
- recordType = "A"
- }
- part := recordType + " " + value
- if weightEnabled {
- part += "(" + strconv.Itoa(item.GetInt("weight")) + ")"
- } else {
- // no extra suffix
- }
- parts = append(parts, part)
- }
- if len(parts) == 0 {
- return "-"
- }
- return strings.Join(parts, ", ")
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/delete.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/delete.go
index 6ce6c19..004fe45 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/delete.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/delete.go
@@ -17,10 +17,19 @@ func (this *DeleteAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
- app := pickApp(params.AppId)
+ app, err := findAppMap(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "delete")
this.Data["app"] = app
- this.Data["domainCount"] = len(mockDomains(app.GetInt64("id")))
+ domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ this.Data["domainCount"] = len(domains)
this.Show()
}
@@ -28,9 +37,10 @@ func (this *DeleteAction) RunPost(params struct {
AppId int64
}) {
if params.AppId > 0 {
- if deleteApp(params.AppId) {
- deleteAppSettings(params.AppId)
- deleteCustomRecordsByApp(params.AppId)
+ err := deleteAppByID(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go
index 08ef685..576de1a 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domains.go
@@ -17,15 +17,19 @@ func (this *DomainsAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
- app := pickApp(params.AppId)
+ app, err := findAppMap(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
// 构建顶部 tabbar
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "domains")
- domains := mockDomains(app.GetInt64("id"))
- for _, domain := range domains {
- domainName := domain.GetString("name")
- domain["customRecordCount"] = countCustomRecordsByDomain(app.GetInt64("id"), domainName)
+ domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
this.Data["app"] = app
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go
index ef26367..44cc478 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsCreatePopup.go
@@ -16,7 +16,12 @@ func (this *DomainsCreatePopupAction) Init() {
func (this *DomainsCreatePopupAction) RunGet(params struct {
AppId int64
}) {
- this.Data["app"] = pickApp(params.AppId)
+ app, err := findAppMap(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ this.Data["app"] = app
this.Show()
}
@@ -27,6 +32,13 @@ func (this *DomainsCreatePopupAction) RunPost(params struct {
Must *actions.Must
CSRF *actionutils.CSRF
}) {
- params.Must.Field("domain", params.Domain).Require("please input domain")
+ params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
+ params.Must.Field("domain", params.Domain).Require("请输入域名")
+
+ err := createDomain(this.Parent(), params.AppId, params.Domain)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go
index 8bd72d4..67042c3 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/domainsDelete.go
@@ -9,6 +9,12 @@ type DomainsDeleteAction struct {
func (this *DomainsDeleteAction) RunPost(params struct {
DomainId int64
}) {
- _ = params.DomainId
+ if params.DomainId > 0 {
+ err := deleteDomain(this.Parent(), params.DomainId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ }
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/formatters.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/formatters.go
new file mode 100644
index 0000000..5c52256
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/formatters.go
@@ -0,0 +1,92 @@
+package apps
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/iwind/TeaGo/maps"
+)
+
+func maskSecret(secret string) string {
+ secret = strings.TrimSpace(secret)
+ if len(secret) < 4 {
+ return "******"
+ }
+
+ prefix := ""
+ for i := 0; i < len(secret); i++ {
+ if secret[i] == '_' {
+ prefix = secret[:i+1]
+ break
+ }
+ }
+ if len(prefix) == 0 {
+ prefix = secret[:2]
+ }
+
+ if len(secret) <= 8 {
+ return prefix + "xxxx"
+ }
+ return prefix + "xxxxxxxx" + secret[len(secret)-4:]
+}
+
+func buildLineText(record maps.Map) string {
+ parts := []string{}
+ if strings.TrimSpace(record.GetString("lineScope")) == "overseas" {
+ parts = append(parts,
+ strings.TrimSpace(record.GetString("lineContinent")),
+ strings.TrimSpace(record.GetString("lineCountry")),
+ )
+ } else {
+ parts = append(parts,
+ strings.TrimSpace(record.GetString("lineCarrier")),
+ strings.TrimSpace(record.GetString("lineRegion")),
+ strings.TrimSpace(record.GetString("lineProvince")),
+ )
+ }
+
+ finalParts := make([]string, 0, len(parts))
+ for _, part := range parts {
+ if len(part) == 0 || part == "默认" {
+ continue
+ }
+ finalParts = append(finalParts, part)
+ }
+ if len(finalParts) == 0 {
+ return "默认"
+ }
+ return strings.Join(finalParts, " / ")
+}
+
+func buildRecordValueText(record maps.Map) string {
+ values, ok := record["recordValues"].([]maps.Map)
+ if !ok || len(values) == 0 {
+ return "-"
+ }
+
+ weightEnabled := record.GetBool("weightEnabled")
+ defaultType := strings.ToUpper(strings.TrimSpace(record.GetString("recordType")))
+ parts := make([]string, 0, len(values))
+ for _, item := range values {
+ value := strings.TrimSpace(item.GetString("value"))
+ if len(value) == 0 {
+ continue
+ }
+ recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
+ if len(recordType) == 0 {
+ recordType = defaultType
+ }
+ if recordType != "A" && recordType != "AAAA" {
+ recordType = "A"
+ }
+ part := recordType + " " + value
+ if weightEnabled {
+ part += "(" + strconv.Itoa(item.GetInt("weight")) + ")"
+ }
+ parts = append(parts, part)
+ }
+ if len(parts) == 0 {
+ return "-"
+ }
+ return strings.Join(parts, ", ")
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go
index bb17da6..232d390 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go
@@ -18,6 +18,11 @@ func (this *IndexAction) RunGet(params struct {
}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["keyword"] = params.Keyword
- this.Data["apps"] = filterApps(params.Keyword, "", "", "")
+ apps, err := listAppMaps(this.Parent(), params.Keyword)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ this.Data["apps"] = apps
this.Show()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go
index d9db84b..e705574 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go
@@ -15,13 +15,17 @@ func init() {
Prefix("/httpdns/apps").
Get("", new(IndexAction)).
Get("/app", new(AppAction)).
- Get("/sdk", new(SDKAction)).
+ Get("/sdk", new(SdkAction)).
+ GetPost("/sdk/upload", new(SdkUploadAction)).
+ Post("/sdk/upload/delete", new(SdkUploadDeleteAction)).
+ Get("/sdk/download", new(SdkDownloadAction)).
+ Get("/sdk/doc", new(SdkDocAction)).
GetPost("/app/settings", new(AppSettingsAction)).
Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)).
Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)).
Get("/domains", new(DomainsAction)).
Get("/customRecords", new(CustomRecordsAction)).
- GetPost("/createPopup", new(CreatePopupAction)).
+ GetPost("/create", new(CreateAction)).
GetPost("/delete", new(DeleteAction)).
GetPost("/domains/createPopup", new(DomainsCreatePopupAction)).
Post("/domains/delete", new(DomainsDeleteAction)).
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go
deleted file mode 100644
index 0587dd5..0000000
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go
+++ /dev/null
@@ -1,190 +0,0 @@
-package apps
-
-import (
- "strings"
- "sync"
-
- "github.com/iwind/TeaGo/maps"
-)
-
-var appStore = struct {
- sync.RWMutex
- data []maps.Map
-}{
- data: defaultMockApps(),
-}
-
-func defaultMockApps() []maps.Map {
- return []maps.Map{
- {
- "id": int64(1),
- "name": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
- "appId": "ab12xc34s2",
- "clusterId": int64(1),
- "domainCount": 3,
- "isOn": true,
- "authStatus": "enabled",
- "ecsMode": "auto",
- "pinningMode": "report",
- "sanMode": "strict",
- "riskLevel": "medium",
- "riskSummary": "Pinning \u5904\u4e8e\u89c2\u5bdf\u6a21\u5f0f",
- "secretVersion": "v2026.02.20",
- },
- {
- "id": int64(2),
- "name": "\u89c6\u9891\u7f51\u5173\u4e1a\u52a1",
- "appId": "vd8992ksm1",
- "clusterId": int64(2),
- "domainCount": 1,
- "isOn": true,
- "authStatus": "enabled",
- "ecsMode": "custom",
- "pinningMode": "enforce",
- "sanMode": "strict",
- "riskLevel": "low",
- "riskSummary": "\u5df2\u542f\u7528\u5f3a\u6821\u9a8c",
- "secretVersion": "v2026.02.18",
- },
- {
- "id": int64(3),
- "name": "\u6d77\u5916\u7070\u5ea6\u6d4b\u8bd5",
- "appId": "ov7711hkq9",
- "clusterId": int64(1),
- "domainCount": 2,
- "isOn": false,
- "authStatus": "disabled",
- "ecsMode": "off",
- "pinningMode": "off",
- "sanMode": "report",
- "riskLevel": "high",
- "riskSummary": "\u5e94\u7528\u5173\u95ed\u4e14\u8bc1\u4e66\u7b56\u7565\u504f\u5f31",
- "secretVersion": "v2026.01.30",
- },
- }
-}
-
-func cloneMap(src maps.Map) maps.Map {
- dst := maps.Map{}
- for k, v := range src {
- dst[k] = v
- }
- return dst
-}
-
-func cloneApps(apps []maps.Map) []maps.Map {
- result := make([]maps.Map, 0, len(apps))
- for _, app := range apps {
- result = append(result, cloneMap(app))
- }
- return result
-}
-
-func mockApps() []maps.Map {
- appStore.RLock()
- defer appStore.RUnlock()
- return cloneApps(appStore.data)
-}
-
-func deleteApp(appID int64) bool {
- if appID <= 0 {
- return false
- }
-
- appStore.Lock()
- defer appStore.Unlock()
-
- found := false
- filtered := make([]maps.Map, 0, len(appStore.data))
- for _, app := range appStore.data {
- if app.GetInt64("id") == appID {
- found = true
- continue
- }
- filtered = append(filtered, app)
- }
- if found {
- appStore.data = filtered
- }
- return found
-}
-
-func filterApps(keyword string, riskLevel string, ecsMode string, pinningMode string) []maps.Map {
- all := mockApps()
- if len(keyword) == 0 && len(riskLevel) == 0 && len(ecsMode) == 0 && len(pinningMode) == 0 {
- return all
- }
-
- keyword = strings.ToLower(strings.TrimSpace(keyword))
- result := make([]maps.Map, 0)
- for _, app := range all {
- if len(keyword) > 0 {
- name := strings.ToLower(app.GetString("name"))
- appID := strings.ToLower(app.GetString("appId"))
- if !strings.Contains(name, keyword) && !strings.Contains(appID, keyword) {
- continue
- }
- }
- if len(riskLevel) > 0 && app.GetString("riskLevel") != riskLevel {
- continue
- }
- if len(ecsMode) > 0 && app.GetString("ecsMode") != ecsMode {
- continue
- }
- if len(pinningMode) > 0 && app.GetString("pinningMode") != pinningMode {
- continue
- }
- result = append(result, app)
- }
-
- return result
-}
-
-func pickApp(appID int64) maps.Map {
- apps := mockApps()
- if len(apps) == 0 {
- return maps.Map{
- "id": int64(0),
- "name": "",
- "appId": "",
- "clusterId": int64(0),
- }
- }
-
- if appID <= 0 {
- return apps[0]
- }
- for _, app := range apps {
- if app.GetInt64("id") == appID {
- return app
- }
- }
- return apps[0]
-}
-
-func mockDomains(appID int64) []maps.Map {
- _ = appID
- return []maps.Map{
- {
- "id": int64(101),
- "name": "api.business.com",
- },
- {
- "id": int64(102),
- "name": "payment.business.com",
- },
- }
-}
-
-func pickDomain(domainID int64) maps.Map {
- domains := mockDomains(0)
- if domainID <= 0 {
- return domains[0]
- }
- for _, domain := range domains {
- if domain.GetInt64("id") == domainID {
- return domain
- }
- }
- return domains[0]
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies.go
deleted file mode 100644
index 36c5296..0000000
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package apps
-
-import (
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
- "github.com/iwind/TeaGo/actions"
- "github.com/iwind/TeaGo/maps"
-)
-
-type PoliciesAction struct {
- actionutils.ParentAction
-}
-
-func (this *PoliciesAction) Init() {
- this.Nav("httpdns", "app", "")
-}
-
-func (this *PoliciesAction) RunGet(params struct{}) {
- httpdnsutils.AddLeftMenu(this.Parent())
- this.Data["policies"] = loadGlobalPolicies()
- this.Show()
-}
-
-func (this *PoliciesAction) RunPost(params struct {
- DefaultTTL int
- DefaultSniPolicy string
- DefaultFallbackMs int
- ECSMode string
- ECSIPv4Prefix int
- ECSIPv6Prefix int
- PinningMode string
- SANMode string
-
- Must *actions.Must
- CSRF *actionutils.CSRF
-}) {
- params.Must.Field("defaultTTL", params.DefaultTTL).Gt(0, "默认 TTL 需要大于 0")
- params.Must.Field("defaultFallbackMs", params.DefaultFallbackMs).Gt(0, "默认超时需要大于 0")
-
- if params.DefaultTTL > 86400 {
- this.Fail("默认 TTL 不能超过 86400 秒")
- return
- }
- if params.DefaultFallbackMs > 10000 {
- this.Fail("默认超时不能超过 10000 毫秒")
- return
- }
- if params.DefaultSniPolicy != "level1" && params.DefaultSniPolicy != "level2" && params.DefaultSniPolicy != "level3" {
- this.Fail("默认 SNI 等级不正确")
- return
- }
- if params.ECSMode != "off" && params.ECSMode != "auto" && params.ECSMode != "custom" {
- this.Fail("ECS 模式不正确")
- return
- }
- if params.ECSIPv4Prefix < 0 || params.ECSIPv4Prefix > 32 {
- this.Fail("IPv4 掩码范围是 0-32")
- return
- }
- if params.ECSIPv6Prefix < 0 || params.ECSIPv6Prefix > 128 {
- this.Fail("IPv6 掩码范围是 0-128")
- return
- }
- if params.PinningMode != "off" && params.PinningMode != "report" && params.PinningMode != "enforce" {
- this.Fail("Pinning 策略不正确")
- return
- }
- if params.SANMode != "off" && params.SANMode != "report" && params.SANMode != "strict" {
- this.Fail("SAN 策略不正确")
- return
- }
-
- saveGlobalPolicies(maps.Map{
- "defaultTTL": params.DefaultTTL,
- "defaultSniPolicy": params.DefaultSniPolicy,
- "defaultFallbackMs": params.DefaultFallbackMs,
- "ecsMode": params.ECSMode,
- "ecsIPv4Prefix": params.ECSIPv4Prefix,
- "ecsIPv6Prefix": params.ECSIPv6Prefix,
- "pinningMode": params.PinningMode,
- "sanMode": params.SANMode,
- })
-
- this.Success()
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies_store.go
deleted file mode 100644
index 08aee8f..0000000
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/policies_store.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package apps
-
-import (
- "sync"
-
- "github.com/iwind/TeaGo/maps"
-)
-
-var globalPoliciesStore = struct {
- sync.RWMutex
- data maps.Map
-}{
- data: maps.Map{
- "defaultTTL": 30,
- "defaultSniPolicy": "level2",
- "defaultFallbackMs": 300,
- "ecsMode": "auto",
- "ecsIPv4Prefix": 24,
- "ecsIPv6Prefix": 56,
- "pinningMode": "report",
- "sanMode": "strict",
- },
-}
-
-func loadGlobalPolicies() maps.Map {
- globalPoliciesStore.RLock()
- defer globalPoliciesStore.RUnlock()
-
- return maps.Map{
- "defaultTTL": globalPoliciesStore.data.GetInt("defaultTTL"),
- "defaultSniPolicy": globalPoliciesStore.data.GetString("defaultSniPolicy"),
- "defaultFallbackMs": globalPoliciesStore.data.GetInt("defaultFallbackMs"),
- "ecsMode": globalPoliciesStore.data.GetString("ecsMode"),
- "ecsIPv4Prefix": globalPoliciesStore.data.GetInt("ecsIPv4Prefix"),
- "ecsIPv6Prefix": globalPoliciesStore.data.GetInt("ecsIPv6Prefix"),
- "pinningMode": globalPoliciesStore.data.GetString("pinningMode"),
- "sanMode": globalPoliciesStore.data.GetString("sanMode"),
- }
-}
-
-func saveGlobalPolicies(policies maps.Map) {
- globalPoliciesStore.Lock()
- globalPoliciesStore.data = maps.Map{
- "defaultTTL": policies.GetInt("defaultTTL"),
- "defaultSniPolicy": policies.GetString("defaultSniPolicy"),
- "defaultFallbackMs": policies.GetInt("defaultFallbackMs"),
- "ecsMode": policies.GetString("ecsMode"),
- "ecsIPv4Prefix": policies.GetInt("ecsIPv4Prefix"),
- "ecsIPv6Prefix": policies.GetInt("ecsIPv6Prefix"),
- "pinningMode": policies.GetString("pinningMode"),
- "sanMode": policies.GetString("sanMode"),
- }
- globalPoliciesStore.Unlock()
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/rpc_helpers.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/rpc_helpers.go
new file mode 100644
index 0000000..dddb9f4
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/rpc_helpers.go
@@ -0,0 +1,295 @@
+package apps
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ timeutil "github.com/iwind/TeaGo/utils/time"
+ "github.com/iwind/TeaGo/maps"
+)
+
+func listAppMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map, error) {
+ resp, err := parent.RPC().HTTPDNSAppRPC().ListHTTPDNSApps(parent.AdminContext(), &pb.ListHTTPDNSAppsRequest{
+ Offset: 0,
+ Size: 10_000,
+ Keyword: strings.TrimSpace(keyword),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ result := make([]maps.Map, 0, len(resp.GetApps()))
+ for _, app := range resp.GetApps() {
+ domainResp, err := parent.RPC().HTTPDNSDomainRPC().ListHTTPDNSDomainsWithAppId(parent.AdminContext(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
+ AppDbId: app.GetId(),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ result = append(result, appPBToMap(app, int64(len(domainResp.GetDomains()))))
+ }
+
+ return result, nil
+}
+
+func findAppMap(parent *actionutils.ParentAction, appDbId int64) (maps.Map, error) {
+ if appDbId > 0 {
+ resp, err := parent.RPC().HTTPDNSAppRPC().FindHTTPDNSApp(parent.AdminContext(), &pb.FindHTTPDNSAppRequest{
+ AppDbId: appDbId,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if resp.GetApp() != nil {
+ domainResp, err := parent.RPC().HTTPDNSDomainRPC().ListHTTPDNSDomainsWithAppId(parent.AdminContext(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
+ AppDbId: appDbId,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return appPBToMap(resp.GetApp(), int64(len(domainResp.GetDomains()))), nil
+ }
+ }
+
+ apps, err := listAppMaps(parent, "")
+ if err != nil {
+ return nil, err
+ }
+ if len(apps) == 0 {
+ return maps.Map{
+ "id": int64(0),
+ "name": "",
+ "appId": "",
+ }, nil
+ }
+ return apps[0], nil
+}
+
+func createApp(parent *actionutils.ParentAction, name string, primaryClusterId int64, backupClusterId int64) (int64, error) {
+ newAppId := "app" + strconv.FormatInt(time.Now().UnixNano()%1_000_000_000_000, 36)
+ resp, err := parent.RPC().HTTPDNSAppRPC().CreateHTTPDNSApp(parent.AdminContext(), &pb.CreateHTTPDNSAppRequest{
+ Name: strings.TrimSpace(name),
+ AppId: newAppId,
+ PrimaryClusterId: primaryClusterId,
+ BackupClusterId: backupClusterId,
+ IsOn: true,
+ SignEnabled: true,
+ })
+ if err != nil {
+ return 0, err
+ }
+ return resp.GetAppDbId(), nil
+}
+
+func deleteAppByID(parent *actionutils.ParentAction, appDbId int64) error {
+ _, err := parent.RPC().HTTPDNSAppRPC().DeleteHTTPDNSApp(parent.AdminContext(), &pb.DeleteHTTPDNSAppRequest{
+ AppDbId: appDbId,
+ })
+ return err
+}
+
+func updateAppSettings(parent *actionutils.ParentAction, appDbId int64, name string, primaryClusterId int64, backupClusterId int64, isOn bool, userId int64) error {
+ _, err := parent.RPC().HTTPDNSAppRPC().UpdateHTTPDNSApp(parent.AdminContext(), &pb.UpdateHTTPDNSAppRequest{
+ AppDbId: appDbId,
+ Name: strings.TrimSpace(name),
+ PrimaryClusterId: primaryClusterId,
+ BackupClusterId: backupClusterId,
+ IsOn: isOn,
+ UserId: userId,
+ })
+ return err
+}
+
+func updateAppSignEnabled(parent *actionutils.ParentAction, appDbId int64, signEnabled bool) error {
+ _, err := parent.RPC().HTTPDNSAppRPC().UpdateHTTPDNSAppSignEnabled(parent.AdminContext(), &pb.UpdateHTTPDNSAppSignEnabledRequest{
+ AppDbId: appDbId,
+ SignEnabled: signEnabled,
+ })
+ return err
+}
+
+func resetAppSignSecret(parent *actionutils.ParentAction, appDbId int64) (*pb.ResetHTTPDNSAppSignSecretResponse, error) {
+ return parent.RPC().HTTPDNSAppRPC().ResetHTTPDNSAppSignSecret(parent.AdminContext(), &pb.ResetHTTPDNSAppSignSecretRequest{
+ AppDbId: appDbId,
+ })
+}
+
+func listDomainMaps(parent *actionutils.ParentAction, appDbId int64, keyword string) ([]maps.Map, error) {
+ resp, err := parent.RPC().HTTPDNSDomainRPC().ListHTTPDNSDomainsWithAppId(parent.AdminContext(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
+ AppDbId: appDbId,
+ Keyword: strings.TrimSpace(keyword),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ result := make([]maps.Map, 0, len(resp.GetDomains()))
+ for _, domain := range resp.GetDomains() {
+ result = append(result, maps.Map{
+ "id": domain.GetId(),
+ "name": domain.GetDomain(),
+ "isOn": domain.GetIsOn(),
+ "customRecordCount": domain.GetRuleCount(),
+ })
+ }
+ return result, nil
+}
+
+func createDomain(parent *actionutils.ParentAction, appDbId int64, domain string) error {
+ _, err := parent.RPC().HTTPDNSDomainRPC().CreateHTTPDNSDomain(parent.AdminContext(), &pb.CreateHTTPDNSDomainRequest{
+ AppDbId: appDbId,
+ Domain: strings.TrimSpace(domain),
+ IsOn: true,
+ })
+ return err
+}
+
+func deleteDomain(parent *actionutils.ParentAction, domainId int64) error {
+ _, err := parent.RPC().HTTPDNSDomainRPC().DeleteHTTPDNSDomain(parent.AdminContext(), &pb.DeleteHTTPDNSDomainRequest{
+ DomainId: domainId,
+ })
+ return err
+}
+
+func findDomainMap(domains []maps.Map, domainID int64) maps.Map {
+ if len(domains) == 0 {
+ return maps.Map{}
+ }
+ if domainID <= 0 {
+ return domains[0]
+ }
+ for _, domain := range domains {
+ if domain.GetInt64("id") == domainID {
+ return domain
+ }
+ }
+ return domains[0]
+}
+
+func listCustomRuleMaps(parent *actionutils.ParentAction, domainId int64) ([]maps.Map, error) {
+ resp, err := parent.RPC().HTTPDNSRuleRPC().ListHTTPDNSCustomRulesWithDomainId(parent.AdminContext(), &pb.ListHTTPDNSCustomRulesWithDomainIdRequest{
+ DomainId: domainId,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ result := make([]maps.Map, 0, len(resp.GetRules()))
+ for _, rule := range resp.GetRules() {
+ recordValues := make([]maps.Map, 0, len(rule.GetRecords()))
+ recordType := "A"
+ weightEnabled := false
+ for _, record := range rule.GetRecords() {
+ if len(recordType) == 0 {
+ recordType = strings.ToUpper(strings.TrimSpace(record.GetRecordType()))
+ }
+ if record.GetWeight() > 0 && record.GetWeight() != 100 {
+ weightEnabled = true
+ }
+ recordValues = append(recordValues, maps.Map{
+ "type": strings.ToUpper(strings.TrimSpace(record.GetRecordType())),
+ "value": record.GetRecordValue(),
+ "weight": record.GetWeight(),
+ })
+ }
+ if len(recordValues) == 0 {
+ recordValues = append(recordValues, maps.Map{
+ "type": "A",
+ "value": "",
+ "weight": 100,
+ })
+ }
+
+ item := maps.Map{
+ "id": rule.GetId(),
+ "lineScope": rule.GetLineScope(),
+ "lineCarrier": defaultLineField(rule.GetLineCarrier()),
+ "lineRegion": defaultLineField(rule.GetLineRegion()),
+ "lineProvince": defaultLineField(rule.GetLineProvince()),
+ "lineContinent": defaultLineField(rule.GetLineContinent()),
+ "lineCountry": defaultLineField(rule.GetLineCountry()),
+ "ruleName": rule.GetRuleName(),
+ "recordType": recordType,
+ "recordValues": recordValues,
+ "weightEnabled": weightEnabled,
+ "ttl": rule.GetTtl(),
+ "isOn": rule.GetIsOn(),
+ "updatedAt": formatDateTime(rule.GetUpdatedAt()),
+ }
+ item["lineText"] = buildLineText(item)
+ item["recordValueText"] = buildRecordValueText(item)
+ result = append(result, item)
+ }
+ return result, nil
+}
+
+func createCustomRule(parent *actionutils.ParentAction, rule *pb.HTTPDNSCustomRule) (int64, error) {
+ resp, err := parent.RPC().HTTPDNSRuleRPC().CreateHTTPDNSCustomRule(parent.AdminContext(), &pb.CreateHTTPDNSCustomRuleRequest{
+ Rule: rule,
+ })
+ if err != nil {
+ return 0, err
+ }
+ return resp.GetRuleId(), nil
+}
+
+func updateCustomRule(parent *actionutils.ParentAction, rule *pb.HTTPDNSCustomRule) error {
+ _, err := parent.RPC().HTTPDNSRuleRPC().UpdateHTTPDNSCustomRule(parent.AdminContext(), &pb.UpdateHTTPDNSCustomRuleRequest{
+ Rule: rule,
+ })
+ return err
+}
+
+func deleteCustomRule(parent *actionutils.ParentAction, ruleId int64) error {
+ _, err := parent.RPC().HTTPDNSRuleRPC().DeleteHTTPDNSCustomRule(parent.AdminContext(), &pb.DeleteHTTPDNSCustomRuleRequest{
+ RuleId: ruleId,
+ })
+ return err
+}
+
+func toggleCustomRule(parent *actionutils.ParentAction, ruleId int64, isOn bool) error {
+ _, err := parent.RPC().HTTPDNSRuleRPC().UpdateHTTPDNSCustomRuleStatus(parent.AdminContext(), &pb.UpdateHTTPDNSCustomRuleStatusRequest{
+ RuleId: ruleId,
+ IsOn: isOn,
+ })
+ return err
+}
+
+func appPBToMap(app *pb.HTTPDNSApp, domainCount int64) maps.Map {
+ signSecret := app.GetSignSecret()
+ return maps.Map{
+ "id": app.GetId(),
+ "name": app.GetName(),
+ "appId": app.GetAppId(),
+ "clusterId": app.GetPrimaryClusterId(),
+ "primaryClusterId": app.GetPrimaryClusterId(),
+ "backupClusterId": app.GetBackupClusterId(),
+ "userId": app.GetUserId(),
+ "isOn": app.GetIsOn(),
+ "domainCount": domainCount,
+ "sniPolicyText": "隐匿 SNI",
+ "signEnabled": app.GetSignEnabled(),
+ "signSecretPlain": signSecret,
+ "signSecretMasked": maskSecret(signSecret),
+ "signSecretUpdated": formatDateTime(app.GetSignUpdatedAt()),
+ }
+}
+
+func defaultLineField(value string) string {
+ value = strings.TrimSpace(value)
+ if len(value) == 0 {
+ return "默认"
+ }
+ return value
+}
+
+func formatDateTime(ts int64) string {
+ if ts <= 0 {
+ return ""
+ }
+ return timeutil.FormatTime("Y-m-d H:i:s", ts)
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk.go
index ddeda94..95a2509 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk.go
@@ -5,23 +5,26 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
)
-type SDKAction struct {
+type SdkAction struct {
actionutils.ParentAction
}
-func (this *SDKAction) Init() {
+func (this *SdkAction) Init() {
this.Nav("httpdns", "app", "sdk")
}
-func (this *SDKAction) RunGet(params struct {
+func (this *SdkAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
- app := pickApp(params.AppId)
- // 构建顶部 tabbar
+ app, err := findAppMap(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "sdk")
-
this.Data["app"] = app
this.Show()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_doc.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_doc.go
new file mode 100644
index 0000000..f0f4112
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_doc.go
@@ -0,0 +1,54 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type SdkDocAction struct {
+ actionutils.ParentAction
+}
+
+func (this *SdkDocAction) Init() {
+ this.Nav("", "", "")
+}
+
+func (this *SdkDocAction) RunGet(params struct {
+ Platform string
+}) {
+ platform, _, readmeRelativePath, _, err := resolveSDKPlatform(params.Platform)
+ if err != nil {
+ this.Fail(err.Error())
+ return
+ }
+
+ var data []byte
+ uploadedDocPath := findUploadedSDKDocPath(platform)
+ if len(uploadedDocPath) > 0 {
+ data, err = os.ReadFile(uploadedDocPath)
+ }
+
+ sdkRoot, sdkRootErr := findSDKRoot()
+ if len(data) == 0 && sdkRootErr == nil {
+ readmePath := filepath.Join(sdkRoot, readmeRelativePath)
+ data, err = os.ReadFile(readmePath)
+ }
+
+ if len(data) == 0 {
+ localDocPath := findLocalSDKDocPath(platform)
+ if len(localDocPath) > 0 {
+ data, err = os.ReadFile(localDocPath)
+ }
+ }
+
+ if len(data) == 0 || err != nil {
+ this.Fail("当前服务器未找到 SDK 集成文档,请先在“SDK 集成”页面上传对应平台文档")
+ return
+ }
+
+ this.AddHeader("Content-Type", "text/markdown; charset=utf-8")
+ this.AddHeader("Content-Disposition", "attachment; filename=\"httpdns-sdk-"+strings.ToLower(platform)+"-README.md\";")
+ _, _ = this.ResponseWriter.Write(data)
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_download.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_download.go
new file mode 100644
index 0000000..8427521
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_download.go
@@ -0,0 +1,45 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "io"
+ "os"
+)
+
+type SdkDownloadAction struct {
+ actionutils.ParentAction
+}
+
+func (this *SdkDownloadAction) Init() {
+ this.Nav("", "", "")
+}
+
+func (this *SdkDownloadAction) RunGet(params struct {
+ Platform string
+}) {
+ _, _, _, filename, err := resolveSDKPlatform(params.Platform)
+ if err != nil {
+ this.Fail(err.Error())
+ return
+ }
+
+ archivePath := findSDKArchivePath(filename)
+ if len(archivePath) == 0 {
+ this.Fail("当前服务器未找到 SDK 包,请先在“SDK 集成”页面上传对应平台包: " + filename)
+ return
+ }
+
+ fp, err := os.Open(archivePath)
+ if err != nil {
+ this.Fail("打开 SDK 包失败: " + err.Error())
+ return
+ }
+ defer func() {
+ _ = fp.Close()
+ }()
+
+ this.AddHeader("Content-Type", "application/zip")
+ this.AddHeader("Content-Disposition", "attachment; filename=\""+filename+"\";")
+ this.AddHeader("X-Accel-Buffering", "no")
+ _, _ = io.Copy(this.ResponseWriter, fp)
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_helpers.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_helpers.go
new file mode 100644
index 0000000..42d75e5
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_helpers.go
@@ -0,0 +1,149 @@
+package apps
+
+import (
+ "errors"
+ "github.com/iwind/TeaGo/Tea"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+)
+
+func sdkUploadDir() string {
+ return filepath.Clean(Tea.Root + "/data/httpdns/sdk")
+}
+
+func findFirstExistingDir(paths []string) string {
+ for _, path := range paths {
+ stat, err := os.Stat(path)
+ if err == nil && stat.IsDir() {
+ return path
+ }
+ }
+ return ""
+}
+
+func findFirstExistingFile(paths []string) string {
+ for _, path := range paths {
+ stat, err := os.Stat(path)
+ if err == nil && !stat.IsDir() {
+ return path
+ }
+ }
+ return ""
+}
+
+func findNewestExistingFile(paths []string) string {
+ type fileInfo struct {
+ path string
+ modTime time.Time
+ }
+ result := fileInfo{}
+ for _, path := range paths {
+ stat, err := os.Stat(path)
+ if err != nil || stat.IsDir() {
+ continue
+ }
+ if len(result.path) == 0 || stat.ModTime().After(result.modTime) || (stat.ModTime().Equal(result.modTime) && path > result.path) {
+ result.path = path
+ result.modTime = stat.ModTime()
+ }
+ }
+ return result.path
+}
+
+func findSDKRoot() (string, error) {
+ candidates := []string{
+ filepath.Clean(Tea.Root + "/EdgeHttpDNS/sdk"),
+ filepath.Clean(Tea.Root + "/edge-httpdns/sdk"),
+ filepath.Clean(Tea.Root + "/edge-httpdns/edge-httpdns/sdk"),
+ filepath.Clean(Tea.Root + "/../EdgeHttpDNS/sdk"),
+ filepath.Clean(Tea.Root + "/../../EdgeHttpDNS/sdk"),
+ filepath.Clean(Tea.Root + "/../edge-httpdns/sdk"),
+ filepath.Clean(Tea.Root + "/../../edge-httpdns/sdk"),
+ }
+
+ dir := findFirstExistingDir(candidates)
+ if len(dir) > 0 {
+ return dir, nil
+ }
+
+ return "", errors.New("SDK files are not found on current server")
+}
+
+func resolveSDKPlatform(platform string) (key string, relativeDir string, readmeRelativePath string, downloadFilename string, err error) {
+ switch strings.ToLower(strings.TrimSpace(platform)) {
+ case "android":
+ return "android", "android", "android/README.md", "httpdns-sdk-android.zip", nil
+ case "ios":
+ return "ios", "ios", "ios/README.md", "httpdns-sdk-ios.zip", nil
+ case "flutter":
+ return "flutter", "flutter/aliyun_httpdns", "flutter/aliyun_httpdns/README.md", "httpdns-sdk-flutter.zip", nil
+ default:
+ return "", "", "", "", errors.New("invalid platform, expected one of: android, ios, flutter")
+ }
+}
+
+func findSDKArchivePath(downloadFilename string) string {
+ searchDirs := []string{sdkUploadDir()}
+
+ // 1) Exact filename first.
+ exactFiles := []string{}
+ for _, dir := range searchDirs {
+ exactFiles = append(exactFiles, filepath.Join(dir, downloadFilename))
+ }
+ path := findFirstExistingFile(exactFiles)
+ if len(path) > 0 {
+ return path
+ }
+
+ // 2) Version-suffixed archives, e.g. httpdns-sdk-android-v1.4.8.zip
+ base := strings.TrimSuffix(downloadFilename, ".zip")
+ patternName := base + "-*.zip"
+ matches := []string{}
+ for _, dir := range searchDirs {
+ found, _ := filepath.Glob(filepath.Join(dir, patternName))
+ for _, file := range found {
+ stat, err := os.Stat(file)
+ if err == nil && !stat.IsDir() {
+ matches = append(matches, file)
+ }
+ }
+ }
+ if len(matches) > 0 {
+ return findNewestExistingFile(matches)
+ }
+
+ return ""
+}
+
+func findUploadedSDKDocPath(platform string) string {
+ platform = strings.ToLower(strings.TrimSpace(platform))
+ if len(platform) == 0 {
+ return ""
+ }
+
+ searchDir := sdkUploadDir()
+ exact := filepath.Join(searchDir, "httpdns-sdk-"+platform+".md")
+ if file := findFirstExistingFile([]string{exact}); len(file) > 0 {
+ return file
+ }
+
+ pattern := filepath.Join(searchDir, "httpdns-sdk-"+platform+"-*.md")
+ matches, _ := filepath.Glob(pattern)
+ if len(matches) == 0 {
+ return ""
+ }
+ sort.Strings(matches)
+ return findNewestExistingFile(matches)
+}
+
+func findLocalSDKDocPath(platform string) string {
+ filename := strings.ToLower(strings.TrimSpace(platform)) + ".md"
+ candidates := []string{
+ filepath.Clean(Tea.Root + "/edge-admin/web/views/@default/httpdns/apps/docs/" + filename),
+ filepath.Clean(Tea.Root + "/EdgeAdmin/web/views/@default/httpdns/apps/docs/" + filename),
+ }
+ return findFirstExistingFile(candidates)
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_upload.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_upload.go
new file mode 100644
index 0000000..f7c5d13
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_upload.go
@@ -0,0 +1,264 @@
+package apps
+
+import (
+ "errors"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/iwind/TeaGo/actions"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type SdkUploadAction struct {
+ actionutils.ParentAction
+}
+
+func (this *SdkUploadAction) Init() {
+ this.Nav("httpdns", "app", "sdk")
+}
+
+func (this *SdkUploadAction) RunGet(params struct {
+ AppId int64
+}) {
+ httpdnsutils.AddLeftMenu(this.Parent())
+
+ app, err := findAppMap(this.Parent(), params.AppId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
+ httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "sdk")
+
+ this.Data["app"] = app
+ this.Data["defaultVersion"] = "1.0.0"
+ this.Data["uploadedFiles"] = listUploadedSDKFiles()
+ this.Show()
+}
+
+func (this *SdkUploadAction) RunPost(params struct {
+ AppId int64
+ Platform string
+ Version string
+ SDKFile *actions.File
+ DocFile *actions.File
+
+ Must *actions.Must
+}) {
+ params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
+
+ platform, _, _, downloadFilename, err := resolveSDKPlatform(params.Platform)
+ if err != nil {
+ this.Fail(err.Error())
+ return
+ }
+
+ version, err := normalizeSDKVersion(params.Version)
+ if err != nil {
+ this.Fail(err.Error())
+ return
+ }
+
+ if params.SDKFile == nil && params.DocFile == nil {
+ this.Fail("请至少上传一个文件")
+ return
+ }
+
+ uploadDir := sdkUploadDir()
+ err = os.MkdirAll(uploadDir, 0755)
+ if err != nil {
+ this.Fail("创建上传目录失败: " + err.Error())
+ return
+ }
+
+ if params.SDKFile != nil {
+ filename := strings.ToLower(strings.TrimSpace(params.SDKFile.Filename))
+ if !strings.HasSuffix(filename, ".zip") {
+ this.Fail("SDK 包仅支持 .zip 文件")
+ return
+ }
+
+ sdkData, readErr := params.SDKFile.Read()
+ if readErr != nil {
+ this.Fail("读取 SDK 包失败: " + readErr.Error())
+ return
+ }
+
+ baseName := strings.TrimSuffix(downloadFilename, ".zip")
+ err = saveSDKUploadFile(uploadDir, downloadFilename, sdkData)
+ if err == nil {
+ err = saveSDKUploadFile(uploadDir, baseName+"-v"+version+".zip", sdkData)
+ }
+ if err != nil {
+ this.Fail("保存 SDK 包失败: " + err.Error())
+ return
+ }
+ }
+
+ if params.DocFile != nil {
+ docName := strings.ToLower(strings.TrimSpace(params.DocFile.Filename))
+ if !strings.HasSuffix(docName, ".md") {
+ this.Fail("集成文档仅支持 .md 文件")
+ return
+ }
+
+ docData, readErr := params.DocFile.Read()
+ if readErr != nil {
+ this.Fail("读取集成文档失败: " + readErr.Error())
+ return
+ }
+
+ filename := "httpdns-sdk-" + platform + ".md"
+ err = saveSDKUploadFile(uploadDir, filename, docData)
+ if err == nil {
+ err = saveSDKUploadFile(uploadDir, "httpdns-sdk-"+platform+"-v"+version+".md", docData)
+ }
+ if err != nil {
+ this.Fail("保存集成文档失败: " + err.Error())
+ return
+ }
+ }
+
+ this.Success()
+}
+
+func normalizeSDKVersion(version string) (string, error) {
+ version = strings.TrimSpace(version)
+ if len(version) == 0 {
+ version = "1.0.0"
+ }
+ for _, c := range version {
+ if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-' {
+ continue
+ }
+ return "", errors.New("版本号格式不正确")
+ }
+ return version, nil
+}
+
+func saveSDKUploadFile(baseDir string, filename string, data []byte) error {
+ targetPath := filepath.Join(baseDir, filename)
+ tmpPath := targetPath + ".tmp"
+ err := os.WriteFile(tmpPath, data, 0644)
+ if err != nil {
+ return err
+ }
+ return os.Rename(tmpPath, targetPath)
+}
+
+func listUploadedSDKFiles() []map[string]interface{} {
+ dir := sdkUploadDir()
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return []map[string]interface{}{}
+ }
+
+ type item struct {
+ Name string
+ Platform string
+ FileType string
+ Version string
+ SizeBytes int64
+ UpdatedAt int64
+ }
+
+ items := make([]item, 0)
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ name := entry.Name()
+ platform, version, fileType, ok := parseSDKUploadFilename(name)
+ if !ok {
+ continue
+ }
+
+ info, statErr := entry.Info()
+ if statErr != nil {
+ continue
+ }
+
+ items = append(items, item{
+ Name: name,
+ Platform: platform,
+ FileType: fileType,
+ Version: version,
+ SizeBytes: info.Size(),
+ UpdatedAt: info.ModTime().Unix(),
+ })
+ }
+
+ sort.Slice(items, func(i, j int) bool {
+ if items[i].UpdatedAt == items[j].UpdatedAt {
+ return items[i].Name > items[j].Name
+ }
+ return items[i].UpdatedAt > items[j].UpdatedAt
+ })
+
+ result := make([]map[string]interface{}, 0, len(items))
+ for _, item := range items {
+ result = append(result, map[string]interface{}{
+ "name": item.Name,
+ "platform": item.Platform,
+ "fileType": item.FileType,
+ "version": item.Version,
+ "sizeText": formatSDKFileSize(item.SizeBytes),
+ "updatedAt": time.Unix(item.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
+ })
+ }
+ return result
+}
+
+func parseSDKUploadFilename(filename string) (platform string, version string, fileType string, ok bool) {
+ if !strings.HasPrefix(filename, "httpdns-sdk-") {
+ return "", "", "", false
+ }
+
+ ext := ""
+ switch {
+ case strings.HasSuffix(filename, ".zip"):
+ ext = ".zip"
+ fileType = "SDK包"
+ case strings.HasSuffix(filename, ".md"):
+ ext = ".md"
+ fileType = "集成文档"
+ default:
+ return "", "", "", false
+ }
+
+ main := strings.TrimSuffix(strings.TrimPrefix(filename, "httpdns-sdk-"), ext)
+ version = "latest"
+ if idx := strings.Index(main, "-v"); idx > 0 && idx+2 < len(main) {
+ version = main[idx+2:]
+ main = main[:idx]
+ }
+
+ main = strings.ToLower(strings.TrimSpace(main))
+ switch main {
+ case "android", "ios", "flutter":
+ platform = main
+ return platform, version, fileType, true
+ default:
+ return "", "", "", false
+ }
+}
+
+func formatSDKFileSize(size int64) string {
+ if size < 1024 {
+ return strconv.FormatInt(size, 10) + " B"
+ }
+ sizeKB := float64(size) / 1024
+ if sizeKB < 1024 {
+ return strconv.FormatFloat(sizeKB, 'f', 1, 64) + " KB"
+ }
+ sizeMB := sizeKB / 1024
+ if sizeMB < 1024 {
+ return strconv.FormatFloat(sizeMB, 'f', 1, 64) + " MB"
+ }
+ sizeGB := sizeMB / 1024
+ return strconv.FormatFloat(sizeGB, 'f', 1, 64) + " GB"
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_upload_delete.go b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_upload_delete.go
new file mode 100644
index 0000000..5a6b906
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/apps/sdk_upload_delete.go
@@ -0,0 +1,57 @@
+package apps
+
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type SdkUploadDeleteAction struct {
+ actionutils.ParentAction
+}
+
+func (this *SdkUploadDeleteAction) Init() {
+ this.Nav("httpdns", "app", "sdk")
+}
+
+func (this *SdkUploadDeleteAction) RunPost(params struct {
+ AppId int64
+ Filename string
+}) {
+ if params.AppId <= 0 {
+ this.Fail("请选择应用")
+ return
+ }
+
+ filename := strings.TrimSpace(params.Filename)
+ if len(filename) == 0 {
+ this.Fail("文件名不能为空")
+ return
+ }
+ if strings.Contains(filename, "/") || strings.Contains(filename, "\\") || strings.Contains(filename, "..") {
+ this.Fail("文件名不合法")
+ return
+ }
+ if !strings.HasPrefix(filename, "httpdns-sdk-") {
+ this.Fail("不允许删除该文件")
+ return
+ }
+ if !(strings.HasSuffix(filename, ".zip") || strings.HasSuffix(filename, ".md")) {
+ this.Fail("不允许删除该文件")
+ return
+ }
+
+ fullPath := filepath.Join(sdkUploadDir(), filename)
+ _, err := os.Stat(fullPath)
+ if err != nil {
+ this.Success()
+ return
+ }
+ if err = os.Remove(fullPath); err != nil {
+ this.Fail("删除失败: " + err.Error())
+ return
+ }
+
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go
index 645d21a..f0d80ca 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/certs.go
@@ -2,8 +2,6 @@ package clusters
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
)
type CertsAction struct {
@@ -15,7 +13,5 @@ func (this *CertsAction) Init() {
}
func (this *CertsAction) RunGet(params struct{}) {
- httpdnsutils.AddLeftMenu(this.Parent())
- this.Data["certs"] = policies.LoadPublicSNICertificates()
- this.Show()
+ this.RedirectURL("/httpdns/clusters")
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go
index 94bbb37..03dbd64 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster.go
@@ -1,8 +1,11 @@
package clusters
import (
+ "strings"
+
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/iwind/TeaGo/maps"
)
type ClusterAction struct {
@@ -20,19 +23,75 @@ func (this *ClusterAction) RunGet(params struct {
Keyword string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
- cluster := pickCluster(params.ClusterId)
- // 构建顶部 tabbar
+ cluster, err := findClusterMap(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "node")
+ nodes, err := listNodeMaps(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ nodes = filterClusterNodes(nodes, params.InstalledState, params.ActiveState, params.Keyword)
+
this.Data["clusterId"] = params.ClusterId
this.Data["cluster"] = cluster
this.Data["installState"] = params.InstalledState
this.Data["activeState"] = params.ActiveState
this.Data["keyword"] = params.Keyword
- nodes := mockNodes(params.ClusterId, params.InstalledState, params.ActiveState, params.Keyword)
this.Data["nodes"] = nodes
this.Data["hasNodes"] = len(nodes) > 0
this.Data["page"] = ""
this.Show()
}
+
+func filterClusterNodes(nodes []maps.Map, installedState int, activeState int, keyword string) []maps.Map {
+ keyword = strings.ToLower(strings.TrimSpace(keyword))
+
+ result := make([]maps.Map, 0, len(nodes))
+ for _, node := range nodes {
+ isInstalled := node.GetBool("isInstalled")
+ if installedState == 1 && !isInstalled {
+ continue
+ }
+ if installedState == 2 && isInstalled {
+ continue
+ }
+
+ status := node.GetMap("status")
+ isOnline := node.GetBool("isOn") && node.GetBool("isUp") && status.GetBool("isActive")
+ if activeState == 1 && !isOnline {
+ continue
+ }
+ if activeState == 2 && isOnline {
+ continue
+ }
+
+ if len(keyword) > 0 {
+ hit := strings.Contains(strings.ToLower(node.GetString("name")), keyword)
+ if !hit {
+ ipAddresses, ok := node["ipAddresses"].([]maps.Map)
+ if ok {
+ for _, ipAddr := range ipAddresses {
+ if strings.Contains(strings.ToLower(ipAddr.GetString("ip")), keyword) {
+ hit = true
+ break
+ }
+ }
+ }
+ }
+ if !hit {
+ continue
+ }
+ }
+
+ result = append(result, node)
+ }
+
+ return result
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go
index c516d9e..5e203f5 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/index.go
@@ -1,8 +1,10 @@
package node
import (
+ "time"
+
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/iwind/TeaGo/maps"
+ timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
@@ -18,51 +20,37 @@ func (this *IndexAction) RunGet(params struct {
ClusterId int64
NodeId int64
}) {
- this.Data["clusterId"] = params.ClusterId
- this.Data["nodeId"] = params.NodeId
- this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
-
- this.Data["nodeDatetime"] = "2026-02-22 12:00:00"
- this.Data["nodeTimeDiff"] = 0
- this.Data["shouldUpgrade"] = false
- this.Data["newVersion"] = ""
-
- this.Data["node"] = maps.Map{
- "id": params.NodeId,
- "name": "Mock HTTPDNS Node",
- "ipAddresses": []maps.Map{{"ip": "100.200.100.200", "name": "Public IP", "canAccess": true, "isOn": true, "isUp": true}},
- "cluster": maps.Map{"id": params.ClusterId, "name": "Mock Cluster", "installDir": "/opt/edge-httpdns"},
- "installDir": "/opt/edge-httpdns",
- "isInstalled": true,
- "uniqueId": "m-1234567890",
- "secret": "mock-secret-key",
- "isOn": true,
- "isUp": true,
- "apiNodeAddrs": []string{"192.168.1.100:8001"},
- "login": nil,
-
- "status": maps.Map{
- "isActive": true,
- "updatedAt": 1670000000,
- "hostname": "node-01.local",
- "cpuUsage": 0.15,
- "cpuUsageText": "15.00%",
- "memUsage": 0.45,
- "memUsageText": "45.00%",
- "connectionCount": 100,
- "buildVersion": "1.0.0",
- "cpuPhysicalCount": 4,
- "cpuLogicalCount": 8,
- "load1m": "0.50",
- "load5m": "0.60",
- "load15m": "0.70",
- "cacheTotalDiskSize": "10G",
- "cacheTotalMemorySize": "2G",
- "exePath": "/opt/edge-httpdns/bin/edge-httpdns",
- "apiSuccessPercent": 100.0,
- "apiAvgCostSeconds": 0.05,
- },
+ node, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ cluster, err := findHTTPDNSClusterMap(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
+ this.Data["clusterId"] = params.ClusterId
+ this.Data["nodeId"] = params.NodeId
+ this.Data["node"] = node
+ this.Data["currentCluster"] = cluster
+
+ status := node.GetMap("status")
+ updatedAt := status.GetInt64("updatedAt")
+ nodeDatetime := ""
+ nodeTimeDiff := int64(0)
+ if updatedAt > 0 {
+ nodeDatetime = timeutil.FormatTime("Y-m-d H:i:s", updatedAt)
+ nodeTimeDiff = time.Now().Unix() - updatedAt
+ if nodeTimeDiff < 0 {
+ nodeTimeDiff = -nodeTimeDiff
+ }
+ }
+ this.Data["nodeDatetime"] = nodeDatetime
+ this.Data["nodeTimeDiff"] = nodeTimeDiff
+
+ this.Data["shouldUpgrade"] = false
+ this.Data["newVersion"] = ""
this.Show()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go
index eeb79a6..c90c86d 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/install.go
@@ -1,8 +1,12 @@
-package node
+package node
import (
+ "encoding/json"
+ "strings"
+ "time"
+
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/iwind/TeaGo/maps"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type InstallAction struct {
@@ -14,28 +18,87 @@ func (this *InstallAction) Init() {
this.SecondMenu("nodes")
}
-func (this *InstallAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
+func (this *InstallAction) RunGet(params struct {
+ ClusterId int64
+ NodeId int64
+}) {
+ node, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ cluster, err := findHTTPDNSClusterMap(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
- this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
+ this.Data["currentCluster"] = cluster
+ this.Data["node"] = node
+ this.Data["installStatus"] = node.GetMap("installStatus")
- this.Data["apiEndpoints"] = "\"http://127.0.0.1:7788\""
- this.Data["sshAddr"] = "192.168.1.100:22"
-
- this.Data["node"] = maps.Map{
- "id": params.NodeId,
- "name": "Mock Node",
- "isInstalled": false,
- "uniqueId": "m-1234567890",
- "secret": "mock-secret-key",
- "installDir": "/opt/edge-httpdns",
- "cluster": maps.Map{"installDir": "/opt/edge-httpdns"},
+ apiNodesResp, err := this.RPC().APINodeRPC().FindAllEnabledAPINodes(this.AdminContext(), &pb.FindAllEnabledAPINodesRequest{})
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
- this.Data["installStatus"] = nil
-
+ apiEndpoints := make([]string, 0, 8)
+ for _, apiNode := range apiNodesResp.GetApiNodes() {
+ if !apiNode.GetIsOn() {
+ continue
+ }
+ apiEndpoints = append(apiEndpoints, apiNode.GetAccessAddrs()...)
+ }
+ if len(apiEndpoints) == 0 {
+ apiEndpoints = []string{"http://127.0.0.1:7788"}
+ }
+ this.Data["apiEndpoints"] = "\"" + strings.Join(apiEndpoints, "\", \"") + "\""
+ this.Data["sshAddr"] = ""
this.Show()
}
-func (this *InstallAction) RunPost(params struct{ NodeId int64 }) {
+func (this *InstallAction) RunPost(params struct {
+ NodeId int64
+}) {
+ nodeResp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ node := nodeResp.GetNode()
+ if node == nil {
+ this.Fail("节点不存在")
+ return
+ }
+
+ existingStatus := map[string]interface{}{}
+ if len(node.GetInstallStatusJSON()) > 0 {
+ _ = json.Unmarshal(node.GetInstallStatusJSON(), &existingStatus)
+ }
+
+ existingStatus["isRunning"] = true
+ existingStatus["isFinished"] = false
+ existingStatus["isOk"] = false
+ existingStatus["error"] = ""
+ existingStatus["errorCode"] = ""
+ existingStatus["updatedAt"] = time.Now().Unix()
+
+ installStatusJSON, _ := json.Marshal(existingStatus)
+ _, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
+ NodeId: params.NodeId,
+ IsUp: node.GetIsUp(),
+ IsInstalled: false,
+ IsActive: node.GetIsActive(),
+ StatusJSON: node.GetStatusJSON(),
+ InstallStatusJSON: installStatusJSON,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go
index e88e779..35e4168 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/logs.go
@@ -1,7 +1,11 @@
-package node
+package node
import (
+ "strings"
+
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ timeutil "github.com/iwind/TeaGo/utils/time"
"github.com/iwind/TeaGo/maps"
)
@@ -14,18 +18,65 @@ func (this *LogsAction) Init() {
this.SecondMenu("nodes")
}
-func (this *LogsAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
+func (this *LogsAction) RunGet(params struct {
+ ClusterId int64
+ NodeId int64
+ DayFrom string
+ DayTo string
+ Level string
+ Keyword string
+}) {
+ node, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ cluster, err := findHTTPDNSClusterMap(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
- this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
+ this.Data["currentCluster"] = cluster
+ this.Data["node"] = node
+ this.Data["dayFrom"] = params.DayFrom
+ this.Data["dayTo"] = params.DayTo
+ this.Data["level"] = params.Level
+ this.Data["keyword"] = params.Keyword
- this.Data["dayFrom"] = ""
- this.Data["dayTo"] = ""
- this.Data["keyword"] = ""
- this.Data["level"] = ""
- this.Data["logs"] = []maps.Map{}
+ day := strings.TrimSpace(params.DayFrom)
+ if len(day) == 0 {
+ day = strings.TrimSpace(params.DayTo)
+ }
+
+ resp, err := this.RPC().HTTPDNSRuntimeLogRPC().ListHTTPDNSRuntimeLogs(this.AdminContext(), &pb.ListHTTPDNSRuntimeLogsRequest{
+ Day: day,
+ ClusterId: params.ClusterId,
+ NodeId: params.NodeId,
+ Level: strings.TrimSpace(params.Level),
+ Keyword: strings.TrimSpace(params.Keyword),
+ Offset: 0,
+ Size: 100,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
+ logs := make([]maps.Map, 0, len(resp.GetLogs()))
+ for _, item := range resp.GetLogs() {
+ logs = append(logs, maps.Map{
+ "level": item.GetLevel(),
+ "tag": item.GetType(),
+ "description": item.GetDescription(),
+ "createdAt": item.GetCreatedAt(),
+ "createdTime": timeutil.FormatTime("Y-m-d H:i:s", item.GetCreatedAt()),
+ "count": item.GetCount(),
+ })
+ }
+ this.Data["logs"] = logs
this.Data["page"] = ""
- this.Data["node"] = maps.Map{"id": params.NodeId, "name": "Mock Node"}
-
this.Show()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/rpc_helpers.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/rpc_helpers.go
new file mode 100644
index 0000000..d4764e4
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/rpc_helpers.go
@@ -0,0 +1,183 @@
+package node
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ "github.com/iwind/TeaGo/maps"
+)
+
+func findHTTPDNSClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map, error) {
+ resp, err := parent.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(parent.AdminContext(), &pb.FindHTTPDNSClusterRequest{
+ ClusterId: clusterID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if resp.GetCluster() == nil {
+ return maps.Map{
+ "id": clusterID,
+ "name": "",
+ "installDir": "/opt/edge-httpdns",
+ }, nil
+ }
+
+ cluster := resp.GetCluster()
+ installDir := strings.TrimSpace(cluster.GetInstallDir())
+ if len(installDir) == 0 {
+ installDir = "/opt/edge-httpdns"
+ }
+ return maps.Map{
+ "id": cluster.GetId(),
+ "name": cluster.GetName(),
+ "installDir": installDir,
+ }, nil
+}
+
+func findHTTPDNSNodeMap(parent *actionutils.ParentAction, nodeID int64) (maps.Map, error) {
+ resp, err := parent.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(parent.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: nodeID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if resp.GetNode() == nil {
+ return maps.Map{}, nil
+ }
+
+ node := resp.GetNode()
+ statusMap := decodeNodeStatus(node.GetStatusJSON())
+ installStatusMap := decodeInstallStatus(node.GetInstallStatusJSON())
+
+ var ipAddresses = []maps.Map{}
+ if installStatusMap.Has("ipAddresses") {
+ for _, addr := range installStatusMap.GetSlice("ipAddresses") {
+ if addrMap, ok := addr.(map[string]interface{}); ok {
+ ipAddresses = append(ipAddresses, maps.Map(addrMap))
+ }
+ }
+ } else {
+ ip := node.GetName()
+ if savedIP := strings.TrimSpace(installStatusMap.GetString("ipAddr")); len(savedIP) > 0 {
+ ip = savedIP
+ } else if hostIP := strings.TrimSpace(statusMap.GetString("hostIP")); len(hostIP) > 0 {
+ ip = hostIP
+ }
+ ipAddresses = append(ipAddresses, maps.Map{
+ "id": node.GetId(),
+ "name": "Public IP",
+ "ip": ip,
+ "canAccess": true,
+ "isOn": node.GetIsOn(),
+ "isUp": node.GetIsUp(),
+ })
+ }
+
+ installDir := strings.TrimSpace(node.GetInstallDir())
+ if len(installDir) == 0 {
+ installDir = "/opt/edge-httpdns"
+ }
+
+ clusterMap, err := findHTTPDNSClusterMap(parent, node.GetClusterId())
+ if err != nil {
+ return nil, err
+ }
+
+ // 构造 node.login 用于 index.html 展示 SSH 信息
+ var loginMap maps.Map = nil
+ sshInfo := installStatusMap.GetMap("ssh")
+ if sshInfo != nil {
+ grantId := sshInfo.GetInt64("grantId")
+ var grantMap maps.Map = nil
+ if grantId > 0 {
+ grantResp, grantErr := parent.RPC().NodeGrantRPC().FindEnabledNodeGrant(parent.AdminContext(), &pb.FindEnabledNodeGrantRequest{
+ NodeGrantId: grantId,
+ })
+ if grantErr == nil && grantResp.GetNodeGrant() != nil {
+ g := grantResp.GetNodeGrant()
+ grantMap = maps.Map{
+ "id": g.Id,
+ "name": g.Name,
+ "methodName": g.Method,
+ "username": g.Username,
+ }
+ }
+ }
+
+ loginMap = maps.Map{
+ "params": maps.Map{
+ "host": sshInfo.GetString("host"),
+ "port": sshInfo.GetInt("port"),
+ },
+ "grant": grantMap,
+ }
+ }
+
+ return maps.Map{
+ "id": node.GetId(),
+ "clusterId": node.GetClusterId(),
+ "name": node.GetName(),
+ "isOn": node.GetIsOn(),
+ "isUp": node.GetIsUp(),
+ "isInstalled": node.GetIsInstalled(),
+ "isActive": node.GetIsActive(),
+ "uniqueId": node.GetUniqueId(),
+ "secret": node.GetSecret(),
+ "installDir": installDir,
+ "status": statusMap,
+ "installStatus": installStatusMap,
+ "cluster": clusterMap,
+ "login": loginMap,
+ "apiNodeAddrs": []string{},
+ "ipAddresses": ipAddresses,
+ }, nil
+}
+
+func decodeNodeStatus(raw []byte) maps.Map {
+ status := &nodeconfigs.NodeStatus{}
+ if len(raw) > 0 {
+ _ = json.Unmarshal(raw, status)
+ }
+ cpuText := fmt.Sprintf("%.2f%%", status.CPUUsage*100)
+ memText := fmt.Sprintf("%.2f%%", status.MemoryUsage*100)
+
+ return maps.Map{
+ "isActive": status.IsActive,
+ "updatedAt": status.UpdatedAt,
+ "hostname": status.Hostname,
+ "hostIP": status.HostIP,
+ "cpuUsage": status.CPUUsage,
+ "cpuUsageText": cpuText,
+ "memUsage": status.MemoryUsage,
+ "memUsageText": memText,
+ "load1m": status.Load1m,
+ "load5m": status.Load5m,
+ "load15m": status.Load15m,
+ "buildVersion": status.BuildVersion,
+ "cpuPhysicalCount": status.CPUPhysicalCount,
+ "cpuLogicalCount": status.CPULogicalCount,
+ "exePath": status.ExePath,
+ "apiSuccessPercent": status.APISuccessPercent,
+ "apiAvgCostSeconds": status.APIAvgCostSeconds,
+ }
+}
+
+func decodeInstallStatus(raw []byte) maps.Map {
+ result := maps.Map{
+ "isRunning": false,
+ "isFinished": true,
+ "isOk": true,
+ "error": "",
+ "errorCode": "",
+ }
+ if len(raw) == 0 {
+ return result
+ }
+
+ _ = json.Unmarshal(raw, &result)
+ return result
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go
index de506c5..55fa5c8 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/start.go
@@ -1,13 +1,41 @@
-package node
+package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type StartAction struct {
actionutils.ParentAction
}
-func (this *StartAction) RunPost(params struct{ NodeId int64 }) {
+func (this *StartAction) RunPost(params struct {
+ NodeId int64
+}) {
+ resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ node := resp.GetNode()
+ if node == nil {
+ this.Fail("节点不存在")
+ return
+ }
+
+ _, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
+ NodeId: params.NodeId,
+ IsUp: true,
+ IsInstalled: node.GetIsInstalled(),
+ IsActive: true,
+ StatusJSON: node.GetStatusJSON(),
+ InstallStatusJSON: node.GetInstallStatusJSON(),
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go
index a85d567..cc48d5c 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/status.go
@@ -2,7 +2,7 @@ package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/iwind/TeaGo/maps"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type StatusAction struct {
@@ -14,14 +14,28 @@ func (this *StatusAction) Init() {
this.SecondMenu("nodes")
}
-func (this *StatusAction) RunPost(params struct{ NodeId int64 }) {
- this.Data["installStatus"] = maps.Map{
- "isRunning": false,
- "isFinished": true,
- "isOk": true,
- "error": "",
- "errorCode": "",
+func (this *StatusAction) RunPost(params struct {
+ NodeId int64
+}) {
+ resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
- this.Data["isInstalled"] = true
+ if resp.GetNode() == nil {
+ this.Fail("节点不存在")
+ return
+ }
+
+ nodeMap, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
+ this.Data["installStatus"] = nodeMap.GetMap("installStatus")
+ this.Data["isInstalled"] = nodeMap.GetBool("isInstalled")
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go
index 10b3ad3..3865b20 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/stop.go
@@ -1,13 +1,41 @@
-package node
+package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type StopAction struct {
actionutils.ParentAction
}
-func (this *StopAction) RunPost(params struct{ NodeId int64 }) {
+func (this *StopAction) RunPost(params struct {
+ NodeId int64
+}) {
+ resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ node := resp.GetNode()
+ if node == nil {
+ this.Fail("节点不存在")
+ return
+ }
+
+ _, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
+ NodeId: params.NodeId,
+ IsUp: node.GetIsUp(),
+ IsInstalled: node.GetIsInstalled(),
+ IsActive: false,
+ StatusJSON: node.GetStatusJSON(),
+ InstallStatusJSON: node.GetInstallStatusJSON(),
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go
index 1e86260..3459d5f 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/update.go
@@ -1,7 +1,14 @@
-package node
+package node
import (
+ "encoding/json"
+ "net"
+ "regexp"
+ "strings"
+
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
@@ -14,28 +21,210 @@ func (this *UpdateAction) Init() {
this.SecondMenu("nodes")
}
-func (this *UpdateAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
+func (this *UpdateAction) RunGet(params struct {
+ ClusterId int64
+ NodeId int64
+}) {
+ node, err := findHTTPDNSNodeMap(this.Parent(), params.NodeId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ cluster, err := findHTTPDNSClusterMap(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
- this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
-
- this.Data["clusters"] = []maps.Map{{"id": params.ClusterId, "name": "Mock Cluster"}}
- this.Data["loginId"] = 0
- this.Data["sshHost"] = "192.168.1.100"
- this.Data["sshPort"] = 22
- this.Data["grant"] = nil
+ this.Data["currentCluster"] = cluster
+ this.Data["clusters"] = []maps.Map{cluster}
this.Data["apiNodeAddrs"] = []string{}
+ this.Data["node"] = node
- this.Data["node"] = maps.Map{
- "id": params.NodeId,
- "name": "Mock Node",
- "isOn": true,
- "ipAddresses": []maps.Map{},
+ sshHost := ""
+ sshPort := 22
+ ipAddresses := []maps.Map{}
+ var grantId int64
+ this.Data["grant"] = nil
+
+ resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ })
+ if err == nil && resp.GetNode() != nil {
+ if len(resp.GetNode().GetInstallStatusJSON()) > 0 {
+ installStatus := maps.Map{}
+ _ = json.Unmarshal(resp.GetNode().GetInstallStatusJSON(), &installStatus)
+ sshInfo := installStatus.GetMap("ssh")
+ if sshInfo != nil {
+ if h := strings.TrimSpace(sshInfo.GetString("host")); len(h) > 0 {
+ sshHost = h
+ }
+ if p := sshInfo.GetInt("port"); p > 0 {
+ sshPort = p
+ }
+ grantId = sshInfo.GetInt64("grantId")
+ }
+
+ if installStatus.Has("ipAddresses") {
+ for _, addr := range installStatus.GetSlice("ipAddresses") {
+ if addrMap, ok := addr.(map[string]interface{}); ok {
+ ipAddresses = append(ipAddresses, maps.Map(addrMap))
+ }
+ }
+ } else if ip := strings.TrimSpace(installStatus.GetString("ipAddr")); len(ip) > 0 {
+ ipAddresses = append(ipAddresses, maps.Map{
+ "ip": ip,
+ "name": "",
+ "canAccess": true,
+ "isOn": true,
+ "isUp": true,
+ })
+ }
+ }
+ }
+
+ this.Data["sshHost"] = sshHost
+ this.Data["sshPort"] = sshPort
+ this.Data["ipAddresses"] = ipAddresses
+
+ if grantId > 0 {
+ grantResp, grantErr := this.RPC().NodeGrantRPC().FindEnabledNodeGrant(this.AdminContext(), &pb.FindEnabledNodeGrantRequest{
+ NodeGrantId: grantId,
+ })
+ if grantErr == nil && grantResp.GetNodeGrant() != nil {
+ g := grantResp.GetNodeGrant()
+ this.Data["grant"] = maps.Map{
+ "id": g.Id,
+ "name": g.Name,
+ "methodName": g.Method,
+ }
+ }
}
this.Show()
}
-func (this *UpdateAction) RunPost(params struct{ NodeId int64 }) {
+func (this *UpdateAction) RunPost(params struct {
+ NodeId int64
+ Name string
+ ClusterId int64
+ IsOn bool
+ SshHost string
+ SshPort int
+ GrantId int64
+ IpAddressesJSON []byte
+
+ Must *actions.Must
+}) {
+ params.Name = strings.TrimSpace(params.Name)
+ params.Must.Field("name", params.Name).Require("请输入节点名称")
+
+ params.SshHost = strings.TrimSpace(params.SshHost)
+ hasSSHUpdate := len(params.SshHost) > 0 || params.SshPort > 0 || params.GrantId > 0
+ if hasSSHUpdate {
+ if len(params.SshHost) == 0 {
+ this.Fail("请输入 SSH 主机地址")
+ }
+ if params.SshPort <= 0 || params.SshPort > 65535 {
+ this.Fail("SSH 端口范围必须在 1-65535 之间")
+ }
+ if params.GrantId <= 0 {
+ this.Fail("请选择 SSH 登录认证")
+ }
+
+ if regexp.MustCompile(`^\d+\.\d+\.\d+\.\d+$`).MatchString(params.SshHost) && net.ParseIP(params.SshHost) == nil {
+ this.Fail("SSH 主机地址格式错误")
+ }
+ }
+
+ ipAddresses := []maps.Map{}
+ if len(params.IpAddressesJSON) > 0 {
+ err := json.Unmarshal(params.IpAddressesJSON, &ipAddresses)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ }
+
+ for _, addr := range ipAddresses {
+ ip := addr.GetString("ip")
+ if net.ParseIP(ip) == nil {
+ this.Fail("IP地址格式错误: " + ip)
+ }
+ }
+
+ needUpdateInstallStatus := hasSSHUpdate || len(ipAddresses) > 0
+
+ resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ if resp.GetNode() == nil {
+ this.Fail("节点不存在")
+ return
+ }
+ node := resp.GetNode()
+
+ installDir := strings.TrimSpace(node.GetInstallDir())
+ if len(installDir) == 0 {
+ installDir = "/opt/edge-httpdns"
+ }
+
+ _, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNode(this.AdminContext(), &pb.UpdateHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ Name: params.Name,
+ InstallDir: installDir,
+ IsOn: params.IsOn,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
+ if needUpdateInstallStatus {
+ installStatus := maps.Map{
+ "isRunning": false,
+ "isFinished": true,
+ "isOk": node.GetIsInstalled(),
+ "error": "",
+ "errorCode": "",
+ }
+ if len(node.GetInstallStatusJSON()) > 0 {
+ _ = json.Unmarshal(node.GetInstallStatusJSON(), &installStatus)
+ }
+ if hasSSHUpdate {
+ installStatus["ssh"] = maps.Map{
+ "host": params.SshHost,
+ "port": params.SshPort,
+ "grantId": params.GrantId,
+ }
+ }
+ if len(ipAddresses) > 0 {
+ installStatus["ipAddresses"] = ipAddresses
+ } else {
+ delete(installStatus, "ipAddresses")
+ delete(installStatus, "ipAddr") // Cleanup legacy
+ }
+
+ installStatusJSON, _ := json.Marshal(installStatus)
+ _, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
+ NodeId: params.NodeId,
+ IsUp: node.GetIsUp(),
+ IsInstalled: node.GetIsInstalled(),
+ IsActive: node.GetIsActive(),
+ StatusJSON: node.GetStatusJSON(),
+ InstallStatusJSON: installStatusJSON,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ }
+
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go
index f655c57..e713a97 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/node/updateInstallStatus.go
@@ -1,13 +1,58 @@
-package node
+package node
import (
+ "encoding/json"
+ "time"
+
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type UpdateInstallStatusAction struct {
actionutils.ParentAction
}
-func (this *UpdateInstallStatusAction) RunPost(params struct{ NodeId int64 }) {
+func (this *UpdateInstallStatusAction) RunPost(params struct {
+ NodeId int64
+ IsInstalled bool
+}) {
+ resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ node := resp.GetNode()
+ if node == nil {
+ this.Fail("节点不存在")
+ return
+ }
+
+ installStatus := map[string]interface{}{}
+ if len(node.GetInstallStatusJSON()) > 0 {
+ _ = json.Unmarshal(node.GetInstallStatusJSON(), &installStatus)
+ }
+ installStatus["isRunning"] = false
+ installStatus["isFinished"] = true
+ installStatus["isOk"] = params.IsInstalled
+ installStatus["error"] = ""
+ installStatus["errorCode"] = ""
+ installStatus["updatedAt"] = time.Now().Unix()
+
+ installStatusJSON, _ := json.Marshal(installStatus)
+
+ _, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
+ NodeId: params.NodeId,
+ IsUp: node.GetIsUp(),
+ IsInstalled: params.IsInstalled,
+ IsActive: node.GetIsActive(),
+ StatusJSON: node.GetStatusJSON(),
+ InstallStatusJSON: installStatusJSON,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go
index 9c87642..624ebd0 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings.go
@@ -7,7 +7,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions"
@@ -27,80 +27,79 @@ func (this *ClusterSettingsAction) RunGet(params struct {
Section string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
- cluster := pickCluster(params.ClusterId)
- settings := loadClusterSettings(cluster)
- cluster["name"] = settings.GetString("name")
- // 构建顶部 tabbar
+ cluster, err := findClusterMap(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "setting")
- // 当前选中的 section
- section := params.Section
+ section := strings.TrimSpace(params.Section)
if len(section) == 0 {
section = "basic"
}
- this.Data["activeSection"] = section
- // 左侧菜单
+ settings := maps.Map{
+ "name": cluster.GetString("name"),
+ "gatewayDomain": cluster.GetString("gatewayDomain"),
+ "cacheTtl": cluster.GetInt("defaultTTL"),
+ "fallbackTimeout": cluster.GetInt("fallbackTimeout"),
+ "installDir": cluster.GetString("installDir"),
+ "isOn": cluster.GetBool("isOn"),
+ "isDefaultCluster": cluster.GetBool("isDefault"),
+ }
+ if settings.GetInt("cacheTtl") <= 0 {
+ settings["cacheTtl"] = 30
+ }
+ if settings.GetInt("fallbackTimeout") <= 0 {
+ settings["fallbackTimeout"] = 300
+ }
+ if len(settings.GetString("installDir")) == 0 {
+ settings["installDir"] = "/opt/edge-httpdns"
+ }
+
+ listenAddresses := []*serverconfigs.NetworkAddressConfig{
+ {
+ Protocol: serverconfigs.ProtocolHTTPS,
+ Host: "",
+ PortRange: "443",
+ },
+ }
+ sslPolicy := &sslconfigs.SSLPolicy{
+ IsOn: true,
+ MinVersion: "TLS 1.1",
+ }
+
+ if rawTLS := strings.TrimSpace(cluster.GetString("tlsPolicyJSON")); len(rawTLS) > 0 {
+ tlsConfig := maps.Map{}
+ if err := json.Unmarshal([]byte(rawTLS), &tlsConfig); err == nil {
+ if listenRaw := tlsConfig.Get("listen"); listenRaw != nil {
+ if data, err := json.Marshal(listenRaw); err == nil {
+ _ = json.Unmarshal(data, &listenAddresses)
+ }
+ }
+ if sslRaw := tlsConfig.Get("sslPolicy"); sslRaw != nil {
+ if data, err := json.Marshal(sslRaw); err == nil {
+ _ = json.Unmarshal(data, sslPolicy)
+ }
+ }
+ }
+ }
+
+ this.Data["activeSection"] = section
cid := strconv.FormatInt(params.ClusterId, 10)
this.Data["leftMenuItems"] = []map[string]interface{}{
{"name": "基础设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "§ion=basic", "isActive": section == "basic"},
- {"name": "端口设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "§ion=tls", "isActive": section == "tls"},
+ {"name": "TLS", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "§ion=tls", "isActive": section == "tls"},
}
-
- settings["isDefaultCluster"] = (policies.LoadDefaultClusterID() == cluster.GetInt64("id"))
-
this.Data["cluster"] = cluster
- // 构造前端需要的 tlsConfig 格式
- var listenAddresses []*serverconfigs.NetworkAddressConfig
- listenAddrsRaw := settings.GetString("listenAddrsJSON")
- if len(listenAddrsRaw) > 0 {
- _ = json.Unmarshal([]byte(listenAddrsRaw), &listenAddresses)
- } else {
- // 默认 443 端口
- listenAddresses = []*serverconfigs.NetworkAddressConfig{
- {
- Protocol: serverconfigs.ProtocolHTTPS,
- Host: "",
- PortRange: "443",
- },
- }
- }
-
- // 构造前端需要的 SSLPolicy
- var sslPolicy *sslconfigs.SSLPolicy
- originCertPem := settings.GetString("originCertPem")
- originKeyPem := settings.GetString("originKeyPem")
- if len(originCertPem) > 0 && len(originKeyPem) > 0 {
- sslPolicy = &sslconfigs.SSLPolicy{
- IsOn: true,
- MinVersion: settings.GetString("tlsMinVersion"),
- CipherSuitesIsOn: settings.GetBool("tlsCipherSuitesOn"),
- OCSPIsOn: settings.GetBool("tlsOcspOn"),
- ClientAuthType: int(settings.GetInt32("tlsClientAuthType")),
- Certs: []*sslconfigs.SSLCertConfig{
- {
- IsOn: true,
- CertData: []byte(originCertPem),
- KeyData: []byte(originKeyPem),
- },
- },
- }
- } else {
- sslPolicy = &sslconfigs.SSLPolicy{
- IsOn: true,
- MinVersion: "TLS 1.1",
- }
- }
-
+ this.Data["settings"] = settings
this.Data["tlsConfig"] = maps.Map{
"isOn": true,
"listen": listenAddresses,
"sslPolicy": sslPolicy,
}
-
- this.Data["cluster"] = cluster
- this.Data["settings"] = settings
this.Show()
}
@@ -120,88 +119,80 @@ func (this *ClusterSettingsAction) RunPost(params struct {
Must *actions.Must
CSRF *actionutils.CSRF
}) {
- params.Must.Field("clusterId", params.ClusterId).Gt(0, "please select cluster")
- params.Must.Field("name", params.Name).Require("please input cluster name")
- params.Must.Field("gatewayDomain", params.GatewayDomain).Require("please input service domain")
- params.Must.Field("cacheTtl", params.CacheTtl).Gt(0, "cache ttl should be greater than 0")
- params.Must.Field("fallbackTimeout", params.FallbackTimeout).Gt(0, "fallback timeout should be greater than 0")
- params.Must.Field("installDir", params.InstallDir).Require("please input install dir")
+ params.Name = strings.TrimSpace(params.Name)
+ params.GatewayDomain = strings.TrimSpace(params.GatewayDomain)
+ params.InstallDir = strings.TrimSpace(params.InstallDir)
+ params.Must.Field("clusterId", params.ClusterId).Gt(0, "请选择集群")
+ params.Must.Field("name", params.Name).Require("请输入集群名称")
+ params.Must.Field("gatewayDomain", params.GatewayDomain).Require("请输入服务域名")
+ if params.CacheTtl <= 0 {
+ params.CacheTtl = 30
+ }
+ if params.FallbackTimeout <= 0 {
+ params.FallbackTimeout = 300
+ }
+ if len(params.InstallDir) == 0 {
+ params.InstallDir = "/opt/edge-httpdns"
+ }
if params.IsDefaultCluster && !params.IsOn {
this.Fail("默认集群必须保持启用状态")
return
}
- cluster := pickCluster(params.ClusterId)
- settings := loadClusterSettings(cluster)
- settings["name"] = strings.TrimSpace(params.Name)
- settings["gatewayDomain"] = strings.TrimSpace(params.GatewayDomain)
- settings["cacheTtl"] = int(params.CacheTtl)
- settings["fallbackTimeout"] = int(params.FallbackTimeout)
- settings["installDir"] = strings.TrimSpace(params.InstallDir)
- settings["isOn"] = params.IsOn
- // 处理地址
- var addresses = []*serverconfigs.NetworkAddressConfig{}
- if len(params.Addresses) > 0 {
- err := json.Unmarshal(params.Addresses, &addresses)
- if err != nil {
- this.Fail("端口地址解析失败:" + err.Error())
- }
-
- addressesJSON, _ := json.Marshal(addresses)
- settings["listenAddrsJSON"] = string(addressesJSON)
+ cluster, err := findClusterMap(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
- // 处理 SSL 配置
- var originCertPem = ""
- var originKeyPem = ""
- var tlsMinVersion = "TLS 1.1"
- var tlsCipherSuitesOn = false
- var tlsOcspOn = false
- var tlsClientAuthType = sslconfigs.SSLClientAuthType(0)
+ tlsConfig := maps.Map{}
+ if rawTLS := strings.TrimSpace(cluster.GetString("tlsPolicyJSON")); len(rawTLS) > 0 {
+ _ = json.Unmarshal([]byte(rawTLS), &tlsConfig)
+ }
+
+ if len(params.Addresses) > 0 {
+ var addresses []*serverconfigs.NetworkAddressConfig
+ if err := json.Unmarshal(params.Addresses, &addresses); err != nil {
+ this.Fail("监听端口配置格式不正确")
+ return
+ }
+ tlsConfig["listen"] = addresses
+ }
if len(params.SslPolicyJSON) > 0 {
sslPolicy := &sslconfigs.SSLPolicy{}
- err := json.Unmarshal(params.SslPolicyJSON, sslPolicy)
- if err == nil {
- tlsMinVersion = sslPolicy.MinVersion
- tlsCipherSuitesOn = sslPolicy.CipherSuitesIsOn
- tlsOcspOn = sslPolicy.OCSPIsOn
- tlsClientAuthType = sslconfigs.SSLClientAuthType(sslPolicy.ClientAuthType)
+ if err := json.Unmarshal(params.SslPolicyJSON, sslPolicy); err != nil {
+ this.Fail("TLS 配置格式不正确")
+ return
+ }
+ tlsConfig["sslPolicy"] = sslPolicy
+ }
- if len(sslPolicy.Certs) > 0 {
- cert := sslPolicy.Certs[0]
- originCertPem = string(cert.CertData)
- originKeyPem = string(cert.KeyData)
- }
+ var tlsPolicyJSON []byte
+ if len(tlsConfig) > 0 {
+ tlsPolicyJSON, err = json.Marshal(tlsConfig)
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
}
- if len(originCertPem) == 0 || len(originKeyPem) == 0 {
- this.Fail("请上传或选择证书")
- }
-
- settings["originHttps"] = true
- settings["originCertPem"] = originCertPem
- settings["originKeyPem"] = originKeyPem
- if len(tlsMinVersion) == 0 {
- tlsMinVersion = "TLS 1.1"
- }
- settings["tlsMinVersion"] = tlsMinVersion
- settings["tlsCipherSuitesOn"] = tlsCipherSuitesOn
- settings["tlsOcspOn"] = tlsOcspOn
- settings["tlsClientAuthType"] = int(tlsClientAuthType)
- settings["lastModifiedAt"] = nowDateTime()
- settings["certUpdatedAt"] = nowDateTime()
-
- saveClusterSettings(params.ClusterId, settings)
-
- currentDefaultClusterId := policies.LoadDefaultClusterID()
- if params.IsDefaultCluster {
- policies.SaveDefaultClusterID(params.ClusterId)
- } else if currentDefaultClusterId == params.ClusterId {
- policies.SaveDefaultClusterID(0)
+ _, err = this.RPC().HTTPDNSClusterRPC().UpdateHTTPDNSCluster(this.AdminContext(), &pb.UpdateHTTPDNSClusterRequest{
+ ClusterId: params.ClusterId,
+ Name: params.Name,
+ ServiceDomain: params.GatewayDomain,
+ DefaultTTL: params.CacheTtl,
+ FallbackTimeoutMs: params.FallbackTimeout,
+ InstallDir: params.InstallDir,
+ TlsPolicyJSON: tlsPolicyJSON,
+ IsOn: params.IsOn,
+ IsDefault: params.IsDefaultCluster,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
this.Success()
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings_store.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings_store.go
deleted file mode 100644
index f835484..0000000
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/clusterSettings_store.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package clusters
-
-import (
- "strings"
- "sync"
- "time"
-
- "github.com/iwind/TeaGo/maps"
-)
-
-var clusterSettingsStore = struct {
- sync.RWMutex
- data map[int64]maps.Map
-}{
- data: map[int64]maps.Map{},
-}
-
-func defaultClusterSettings(cluster maps.Map) maps.Map {
- installDir := strings.TrimSpace(cluster.GetString("installDir"))
- if len(installDir) == 0 {
- installDir = "/opt/edge-httpdns"
- }
-
- return maps.Map{
- "name": cluster.GetString("name"),
- "gatewayDomain": strings.TrimSpace(cluster.GetString("gatewayDomain")),
- "cacheTtl": cluster.GetInt("cacheTtl"),
- "fallbackTimeout": cluster.GetInt("fallbackTimeout"),
- "installDir": installDir,
- "isOn": cluster.GetBool("isOn"),
- "originHttps": true,
- "originCertPem": "",
- "originKeyPem": "",
- "tlsMinVersion": "TLS 1.1",
- "tlsCipherSuitesOn": false,
- "tlsOcspOn": false,
- "tlsClientAuthType": 0,
- "certUpdatedAt": "",
- "lastModifiedAt": "",
- }
-}
-
-func cloneClusterSettings(settings maps.Map) maps.Map {
- return maps.Map{
- "name": settings.GetString("name"),
- "gatewayDomain": settings.GetString("gatewayDomain"),
- "cacheTtl": settings.GetInt("cacheTtl"),
- "fallbackTimeout": settings.GetInt("fallbackTimeout"),
- "installDir": settings.GetString("installDir"),
- "isOn": settings.GetBool("isOn"),
- "originHttps": true,
- "originCertPem": settings.GetString("originCertPem"),
- "originKeyPem": settings.GetString("originKeyPem"),
- "tlsMinVersion": settings.GetString("tlsMinVersion"),
- "tlsCipherSuitesOn": settings.GetBool("tlsCipherSuitesOn"),
- "tlsOcspOn": settings.GetBool("tlsOcspOn"),
- "tlsClientAuthType": settings.GetInt("tlsClientAuthType"),
- "certUpdatedAt": settings.GetString("certUpdatedAt"),
- "lastModifiedAt": settings.GetString("lastModifiedAt"),
- }
-}
-
-func loadClusterSettings(cluster maps.Map) maps.Map {
- clusterId := cluster.GetInt64("id")
-
- clusterSettingsStore.RLock()
- settings, ok := clusterSettingsStore.data[clusterId]
- clusterSettingsStore.RUnlock()
- if ok {
- if len(settings.GetString("tlsMinVersion")) == 0 {
- settings["tlsMinVersion"] = "TLS 1.1"
- }
- return cloneClusterSettings(settings)
- }
-
- settings = defaultClusterSettings(cluster)
- saveClusterSettings(clusterId, settings)
- return cloneClusterSettings(settings)
-}
-
-func saveClusterSettings(clusterId int64, settings maps.Map) {
- clusterSettingsStore.Lock()
- clusterSettingsStore.data[clusterId] = cloneClusterSettings(settings)
- clusterSettingsStore.Unlock()
-}
-
-func applyClusterSettingsOverrides(cluster maps.Map) {
- clusterId := cluster.GetInt64("id")
- if clusterId <= 0 {
- return
- }
-
- clusterSettingsStore.RLock()
- settings, ok := clusterSettingsStore.data[clusterId]
- clusterSettingsStore.RUnlock()
- if !ok {
- return
- }
-
- cluster["name"] = settings.GetString("name")
- cluster["gatewayDomain"] = settings.GetString("gatewayDomain")
- cluster["cacheTtl"] = settings.GetInt("cacheTtl")
- cluster["fallbackTimeout"] = settings.GetInt("fallbackTimeout")
- cluster["installDir"] = settings.GetString("installDir")
- cluster["isOn"] = settings.GetBool("isOn")
-}
-
-func nowDateTime() string {
- return time.Now().Format("2006-01-02 15:04:05")
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go
index e8779f5..af2c55d 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/create.go
@@ -1,8 +1,12 @@
package clusters
import (
+ "strings"
+
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ "github.com/iwind/TeaGo/actions"
)
type CreateAction struct {
@@ -17,3 +21,48 @@ func (this *CreateAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Show()
}
+
+func (this *CreateAction) RunPost(params struct {
+ Name string
+ GatewayDomain string
+ CacheTtl int32
+ FallbackTimeout int32
+ InstallDir string
+ IsOn bool
+ IsDefault bool
+
+ Must *actions.Must
+}) {
+ params.Name = strings.TrimSpace(params.Name)
+ params.GatewayDomain = strings.TrimSpace(params.GatewayDomain)
+ params.InstallDir = strings.TrimSpace(params.InstallDir)
+ if len(params.InstallDir) == 0 {
+ params.InstallDir = "/opt/edge-httpdns"
+ }
+ if params.CacheTtl <= 0 {
+ params.CacheTtl = 30
+ }
+ if params.FallbackTimeout <= 0 {
+ params.FallbackTimeout = 300
+ }
+
+ params.Must.Field("name", params.Name).Require("请输入集群名称")
+ params.Must.Field("gatewayDomain", params.GatewayDomain).Require("请输入服务域名")
+
+ resp, err := this.RPC().HTTPDNSClusterRPC().CreateHTTPDNSCluster(this.AdminContext(), &pb.CreateHTTPDNSClusterRequest{
+ Name: params.Name,
+ ServiceDomain: params.GatewayDomain,
+ DefaultTTL: params.CacheTtl,
+ FallbackTimeoutMs: params.FallbackTimeout,
+ InstallDir: params.InstallDir,
+ IsOn: params.IsOn,
+ IsDefault: params.IsDefault,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
+ this.Data["clusterId"] = resp.GetClusterId()
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go
index 78f375e..b34fb45 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/createNode.go
@@ -1,8 +1,11 @@
package clusters
import (
+ "strings"
+
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/iwind/TeaGo/actions"
)
type CreateNodeAction struct {
@@ -13,13 +16,18 @@ func (this *CreateNodeAction) Init() {
this.Nav("httpdns", "cluster", "createNode")
}
-func (this *CreateNodeAction) RunGet(params struct{ ClusterId int64 }) {
+func (this *CreateNodeAction) RunGet(params struct {
+ ClusterId int64
+}) {
httpdnsutils.AddLeftMenu(this.Parent())
- cluster := pickCluster(params.ClusterId)
- // 构建顶部 tabbar
+ cluster, err := findClusterMap(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "node")
-
this.Data["clusterId"] = params.ClusterId
this.Data["cluster"] = cluster
this.Show()
@@ -28,6 +36,28 @@ func (this *CreateNodeAction) RunGet(params struct{ ClusterId int64 }) {
func (this *CreateNodeAction) RunPost(params struct {
ClusterId int64
Name string
+ InstallDir string
+
+ Must *actions.Must
}) {
+ params.Name = strings.TrimSpace(params.Name)
+ params.InstallDir = strings.TrimSpace(params.InstallDir)
+ params.Must.Field("clusterId", params.ClusterId).Gt(0, "请选择集群")
+ params.Must.Field("name", params.Name).Require("请输入节点名称")
+
+ if len(params.InstallDir) == 0 {
+ cluster, err := findClusterMap(this.Parent(), params.ClusterId)
+ if err == nil {
+ params.InstallDir = strings.TrimSpace(cluster.GetString("installDir"))
+ }
+ if len(params.InstallDir) == 0 {
+ params.InstallDir = "/opt/edge-httpdns"
+ }
+ }
+
+ if err := createNode(this.Parent(), params.ClusterId, params.Name, params.InstallDir); err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go
index 9081f33..4b624b9 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/delete.go
@@ -3,6 +3,7 @@ package clusters
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type DeleteAction struct {
@@ -17,11 +18,14 @@ func (this *DeleteAction) RunGet(params struct {
ClusterId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
- cluster := pickCluster(params.ClusterId)
- // 构建顶部 tabbar
+ cluster, err := findClusterMap(this.Parent(), params.ClusterId)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "delete")
-
this.Data["cluster"] = cluster
this.Show()
}
@@ -29,6 +33,12 @@ func (this *DeleteAction) RunGet(params struct {
func (this *DeleteAction) RunPost(params struct {
ClusterId int64
}) {
- _ = params.ClusterId
+ _, err := this.RPC().HTTPDNSClusterRPC().DeleteHTTPDNSCluster(this.AdminContext(), &pb.DeleteHTTPDNSClusterRequest{
+ ClusterId: params.ClusterId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go
index 7adcd40..eecc635 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/deleteNode.go
@@ -1,6 +1,18 @@
-package clusters
-import (
-"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
-)
-type DeleteNodeAction struct { actionutils.ParentAction }
-func (this *DeleteNodeAction) RunPost(params struct{ ClusterId int64; NodeId int64 }) { this.Success() }
+package clusters
+
+import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+
+type DeleteNodeAction struct {
+ actionutils.ParentAction
+}
+
+func (this *DeleteNodeAction) RunPost(params struct {
+ ClusterId int64
+ NodeId int64
+}) {
+ if err := deleteNode(this.Parent(), params.NodeId); err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ this.Success()
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go
index f1ead83..9f2e27b 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/index.go
@@ -13,8 +13,27 @@ func (this *IndexAction) Init() {
this.Nav("httpdns", "cluster", "")
}
-func (this *IndexAction) RunGet(params struct{}) {
+func (this *IndexAction) RunGet(params struct {
+ Keyword string
+}) {
httpdnsutils.AddLeftMenu(this.Parent())
- this.Data["clusters"] = mockClusters()
+ this.Data["keyword"] = params.Keyword
+
+ clusters, err := listClusterMaps(this.Parent(), params.Keyword)
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ this.Data["clusters"] = clusters
+
+ hasErrorLogs := false
+ for _, cluster := range clusters {
+ if cluster.GetInt("countAllNodes") > cluster.GetInt("countActiveNodes") {
+ hasErrorLogs = true
+ break
+ }
+ }
+ this.Data["hasErrorLogs"] = hasErrorLogs
+ this.Data["page"] = ""
this.Show()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go
index bfecd53..999e0ab 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/init.go
@@ -15,7 +15,7 @@ func init() {
Data("teaSubMenu", "cluster").
Prefix("/httpdns/clusters").
Get("", new(IndexAction)).
- Get("/create", new(CreateAction)).
+ GetPost("/create", new(CreateAction)).
Get("/cluster", new(ClusterAction)).
GetPost("/cluster/settings", new(ClusterSettingsAction)).
GetPost("/delete", new(DeleteAction)).
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go
deleted file mode 100644
index f8e91e7..0000000
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package clusters
-
-import "github.com/iwind/TeaGo/maps"
-
-func mockClusters() []maps.Map {
- clusters := []maps.Map{
- {
- "id": int64(1),
- "name": "gateway-cn-hz",
- "region": "cn-hangzhou",
- "gatewayDomain": "gw-hz.httpdns.example.com",
- "installDir": "/opt/edge-httpdns",
- "countAllNodes": 3,
- "countActiveNodes": 3,
- "countApps": 5,
- "cacheTtl": 30,
- "fallbackTimeout": 300,
- "isOn": true,
- },
- {
- "id": int64(2),
- "name": "gateway-cn-bj",
- "region": "cn-beijing",
- "gatewayDomain": "gw-bj.httpdns.example.com",
- "installDir": "/opt/edge-httpdns",
- "countAllNodes": 3,
- "countActiveNodes": 2,
- "countApps": 2,
- "cacheTtl": 30,
- "fallbackTimeout": 300,
- "isOn": true,
- },
- }
-
- for _, cluster := range clusters {
- applyClusterSettingsOverrides(cluster)
- }
-
- return clusters
-}
-
-func pickCluster(clusterId int64) maps.Map {
- clusters := mockClusters()
- if clusterId <= 0 {
- return clusters[0]
- }
- for _, c := range clusters {
- if c.GetInt64("id") == clusterId {
- return c
- }
- }
- return clusters[0]
-}
-
-func mockNodes(clusterId int64, installState int, activeState int, keyword string) []maps.Map {
- _ = clusterId
- return []maps.Map{
- {
- "id": int64(101),
- "name": "45.250.184.56",
- "isInstalled": true,
- "isOn": true,
- "isUp": true,
- "installStatus": maps.Map{
- "isRunning": false,
- "isFinished": true,
- "isOk": true,
- "error": "",
- },
- "status": maps.Map{
- "isActive": true,
- "updatedAt": 1700000000,
- "hostname": "node-01",
- "cpuUsage": 0.0253,
- "cpuUsageText": "2.53%",
- "memUsage": 0.5972,
- "memUsageText": "59.72%",
- "load1m": 0.02,
- },
- "ipAddresses": []maps.Map{
- {
- "id": 1,
- "name": "",
- "ip": "45.250.184.56",
- "canAccess": true,
- },
- },
- },
- {
- "id": int64(102),
- "name": "45.250.184.53",
- "isInstalled": true,
- "isOn": true,
- "isUp": true,
- "installStatus": maps.Map{
- "isRunning": false,
- "isFinished": true,
- "isOk": true,
- "error": "",
- },
- "status": maps.Map{
- "isActive": true,
- "updatedAt": 1700000000,
- "hostname": "node-02",
- "cpuUsage": 0.0039,
- "cpuUsageText": "0.39%",
- "memUsage": 0.0355,
- "memUsageText": "3.55%",
- "load1m": 0.0,
- },
- "ipAddresses": []maps.Map{
- {
- "id": 2,
- "name": "",
- "ip": "45.250.184.53",
- "canAccess": true,
- },
- },
- },
- }
-}
-
-func mockCerts() []maps.Map {
- return []maps.Map{
- {
- "id": int64(11),
- "domain": "resolve.edge.example.com",
- "issuer": "Mock CA",
- "expiresAt": "2026-12-31 23:59:59",
- },
- }
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/rpc_helpers.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/rpc_helpers.go
new file mode 100644
index 0000000..968cb16
--- /dev/null
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/rpc_helpers.go
@@ -0,0 +1,248 @@
+package clusters
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ "github.com/iwind/TeaGo/maps"
+)
+
+func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map, error) {
+ resp, err := parent.RPC().HTTPDNSClusterRPC().ListHTTPDNSClusters(parent.AdminContext(), &pb.ListHTTPDNSClustersRequest{
+ Offset: 0,
+ Size: 10_000,
+ Keyword: strings.TrimSpace(keyword),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ result := make([]maps.Map, 0, len(resp.GetClusters()))
+ for _, cluster := range resp.GetClusters() {
+ nodesResp, err := parent.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(parent.AdminContext(), &pb.ListHTTPDNSNodesRequest{
+ ClusterId: cluster.GetId(),
+ })
+ if err != nil {
+ return nil, err
+ }
+ countAllNodes := len(nodesResp.GetNodes())
+ countActiveNodes := 0
+ for _, node := range nodesResp.GetNodes() {
+ if node.GetIsOn() && node.GetIsUp() && node.GetIsActive() {
+ countActiveNodes++
+ }
+ }
+
+ result = append(result, maps.Map{
+ "id": cluster.GetId(),
+ "name": cluster.GetName(),
+ "gatewayDomain": cluster.GetServiceDomain(),
+ "defaultTTL": cluster.GetDefaultTTL(),
+ "fallbackTimeout": cluster.GetFallbackTimeoutMs(),
+ "installDir": cluster.GetInstallDir(),
+ "isOn": cluster.GetIsOn(),
+ "isDefault": cluster.GetIsDefault(),
+ "countAllNodes": countAllNodes,
+ "countActiveNodes": countActiveNodes,
+ })
+ }
+ return result, nil
+}
+
+func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map, error) {
+ if clusterID > 0 {
+ resp, err := parent.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(parent.AdminContext(), &pb.FindHTTPDNSClusterRequest{
+ ClusterId: clusterID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if resp.GetCluster() != nil {
+ cluster := resp.GetCluster()
+ return maps.Map{
+ "id": cluster.GetId(),
+ "name": cluster.GetName(),
+ "gatewayDomain": cluster.GetServiceDomain(),
+ "defaultTTL": cluster.GetDefaultTTL(),
+ "fallbackTimeout": cluster.GetFallbackTimeoutMs(),
+ "installDir": cluster.GetInstallDir(),
+ "isOn": cluster.GetIsOn(),
+ "isDefault": cluster.GetIsDefault(),
+ "tlsPolicyJSON": cluster.GetTlsPolicyJSON(),
+ }, nil
+ }
+ }
+
+ clusters, err := listClusterMaps(parent, "")
+ if err != nil {
+ return nil, err
+ }
+ if len(clusters) == 0 {
+ return maps.Map{
+ "id": int64(0),
+ "name": "",
+ "gatewayDomain": "",
+ "defaultTTL": 30,
+ "fallbackTimeout": 300,
+ "installDir": "/opt/edge-httpdns",
+ }, nil
+ }
+ return clusters[0], nil
+}
+
+func listNodeMaps(parent *actionutils.ParentAction, clusterID int64) ([]maps.Map, error) {
+ resp, err := parent.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(parent.AdminContext(), &pb.ListHTTPDNSNodesRequest{
+ ClusterId: clusterID,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ result := make([]maps.Map, 0, len(resp.GetNodes()))
+ for _, node := range resp.GetNodes() {
+ statusMap := decodeNodeStatus(node.GetStatusJSON())
+ installStatusMap := decodeInstallStatus(node.GetInstallStatusJSON())
+ ip := node.GetName()
+ if parsed := strings.TrimSpace(statusMap.GetString("hostIP")); len(parsed) > 0 {
+ ip = parsed
+ }
+ nodeMap := maps.Map{
+ "id": node.GetId(),
+ "clusterId": node.GetClusterId(),
+ "name": node.GetName(),
+ "isOn": node.GetIsOn(),
+ "isUp": node.GetIsUp(),
+ "isInstalled": node.GetIsInstalled(),
+ "isActive": node.GetIsActive(),
+ "installDir": node.GetInstallDir(),
+ "uniqueId": node.GetUniqueId(),
+ "secret": node.GetSecret(),
+ "status": statusMap,
+ "installStatus": installStatusMap,
+ "region": nil,
+ "login": nil,
+ "apiNodeAddrs": []string{},
+ "cluster": maps.Map{
+ "id": node.GetClusterId(),
+ "installDir": node.GetInstallDir(),
+ },
+ "ipAddresses": []maps.Map{
+ {
+ "id": node.GetId(),
+ "name": "Public IP",
+ "ip": ip,
+ "canAccess": true,
+ "isOn": node.GetIsOn(),
+ "isUp": node.GetIsUp(),
+ },
+ },
+ }
+ result = append(result, nodeMap)
+ }
+ return result, nil
+}
+
+func findNodeMap(parent *actionutils.ParentAction, nodeID int64) (maps.Map, error) {
+ resp, err := parent.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(parent.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: nodeID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if resp.GetNode() == nil {
+ return maps.Map{}, nil
+ }
+ nodes, err := listNodeMaps(parent, resp.GetNode().GetClusterId())
+ if err != nil {
+ return nil, err
+ }
+ for _, node := range nodes {
+ if node.GetInt64("id") == nodeID {
+ return node, nil
+ }
+ }
+ return maps.Map{
+ "id": resp.GetNode().GetId(),
+ "name": resp.GetNode().GetName(),
+ }, nil
+}
+
+func createNode(parent *actionutils.ParentAction, clusterID int64, name string, installDir string) error {
+ _, err := parent.RPC().HTTPDNSNodeRPC().CreateHTTPDNSNode(parent.AdminContext(), &pb.CreateHTTPDNSNodeRequest{
+ ClusterId: clusterID,
+ Name: strings.TrimSpace(name),
+ InstallDir: strings.TrimSpace(installDir),
+ IsOn: true,
+ })
+ return err
+}
+
+func updateNode(parent *actionutils.ParentAction, nodeID int64, name string, installDir string, isOn bool) error {
+ _, err := parent.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNode(parent.AdminContext(), &pb.UpdateHTTPDNSNodeRequest{
+ NodeId: nodeID,
+ Name: strings.TrimSpace(name),
+ InstallDir: strings.TrimSpace(installDir),
+ IsOn: isOn,
+ })
+ return err
+}
+
+func deleteNode(parent *actionutils.ParentAction, nodeID int64) error {
+ _, err := parent.RPC().HTTPDNSNodeRPC().DeleteHTTPDNSNode(parent.AdminContext(), &pb.DeleteHTTPDNSNodeRequest{
+ NodeId: nodeID,
+ })
+ return err
+}
+
+func decodeNodeStatus(raw []byte) maps.Map {
+ status := &nodeconfigs.NodeStatus{}
+ if len(raw) > 0 {
+ _ = json.Unmarshal(raw, status)
+ }
+ cpuText := fmt.Sprintf("%.2f%%", status.CPUUsage*100)
+ memText := fmt.Sprintf("%.2f%%", status.MemoryUsage*100)
+ return maps.Map{
+ "isActive": status.IsActive,
+ "updatedAt": status.UpdatedAt,
+ "hostname": status.Hostname,
+ "hostIP": status.HostIP,
+ "cpuUsage": status.CPUUsage,
+ "cpuUsageText": cpuText,
+ "memUsage": status.MemoryUsage,
+ "memUsageText": memText,
+ "load1m": status.Load1m,
+ "load5m": status.Load5m,
+ "load15m": status.Load15m,
+ "buildVersion": status.BuildVersion,
+ "cpuPhysicalCount": status.CPUPhysicalCount,
+ "cpuLogicalCount": status.CPULogicalCount,
+ "exePath": status.ExePath,
+ }
+}
+
+func decodeInstallStatus(raw []byte) maps.Map {
+ if len(raw) == 0 {
+ return maps.Map{
+ "isRunning": false,
+ "isFinished": true,
+ "isOk": true,
+ "error": "",
+ "errorCode": "",
+ }
+ }
+ result := maps.Map{}
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return maps.Map{
+ "isRunning": false,
+ "isFinished": true,
+ "isOk": true,
+ "error": "",
+ "errorCode": "",
+ }
+ }
+ return result
+}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go
index f8360e4..789bc98 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/clusters/updateNodeSSH.go
@@ -1,7 +1,14 @@
package clusters
import (
+ "encoding/json"
+ "net"
+ "regexp"
+ "strings"
+
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
+ "github.com/iwind/TeaGo/actions"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
@@ -12,18 +19,52 @@ type UpdateNodeSSHAction struct {
func (this *UpdateNodeSSHAction) RunGet(params struct {
NodeId int64
}) {
+ resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
+ clusterId := int64(0)
+ nodeName := ""
+ if resp.GetNode() != nil {
+ clusterId = resp.GetNode().GetClusterId()
+ nodeName = resp.GetNode().GetName()
+ }
+
this.Data["nodeId"] = params.NodeId
- this.Data["clusterId"] = 0
+ this.Data["clusterId"] = clusterId
this.Data["node"] = maps.Map{
"id": params.NodeId,
- "name": "Mock Node",
+ "name": nodeName,
}
- this.Data["loginId"] = 0
- this.Data["params"] = maps.Map{
- "host": "1.2.3.4",
+ loginParams := maps.Map{
+ "host": "",
"port": 22,
"grantId": 0,
}
+ this.Data["loginId"] = 0
+
+ if resp.GetNode() != nil && len(resp.GetNode().GetInstallStatusJSON()) > 0 {
+ installStatus := maps.Map{}
+ _ = json.Unmarshal(resp.GetNode().GetInstallStatusJSON(), &installStatus)
+ sshInfo := installStatus.GetMap("ssh")
+ if sshInfo != nil {
+ if host := strings.TrimSpace(sshInfo.GetString("host")); len(host) > 0 {
+ loginParams["host"] = host
+ }
+ if port := sshInfo.GetInt("port"); port > 0 {
+ loginParams["port"] = port
+ }
+ if grantID := sshInfo.GetInt64("grantId"); grantID > 0 {
+ loginParams["grantId"] = grantID
+ }
+ }
+ }
+
+ this.Data["params"] = loginParams
this.Data["grant"] = nil
this.Show()
}
@@ -34,6 +75,66 @@ func (this *UpdateNodeSSHAction) RunPost(params struct {
SshHost string
SshPort int
GrantId int64
+
+ Must *actions.Must
}) {
+ params.SshHost = strings.TrimSpace(params.SshHost)
+ params.Must.
+ Field("sshHost", params.SshHost).
+ Require("请输入 SSH 主机地址").
+ Field("sshPort", params.SshPort).
+ Gt(0, "SSH 端口必须大于 0").
+ Lt(65535, "SSH 端口必须小于 65535")
+ if params.GrantId <= 0 {
+ this.Fail("请选择节点登录认证信息")
+ }
+
+ if regexp.MustCompile(`^\d+\.\d+\.\d+\.\d+$`).MatchString(params.SshHost) && net.ParseIP(params.SshHost) == nil {
+ this.Fail("SSH 主机地址 IP 格式错误")
+ }
+
+ resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
+ NodeId: params.NodeId,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ node := resp.GetNode()
+ if node == nil {
+ this.Fail("节点不存在")
+ return
+ }
+
+ installStatus := maps.Map{
+ "isRunning": false,
+ "isFinished": true,
+ "isOk": node.GetIsInstalled(),
+ "error": "",
+ "errorCode": "",
+ }
+ if len(node.GetInstallStatusJSON()) > 0 {
+ _ = json.Unmarshal(node.GetInstallStatusJSON(), &installStatus)
+ }
+ installStatus["ssh"] = maps.Map{
+ "host": params.SshHost,
+ "port": params.SshPort,
+ "grantId": params.GrantId,
+ }
+
+ installStatusJSON, _ := json.Marshal(installStatus)
+ _, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNodeStatus(this.AdminContext(), &pb.UpdateHTTPDNSNodeStatusRequest{
+ NodeId: params.NodeId,
+ IsUp: node.GetIsUp(),
+ IsInstalled: node.GetIsInstalled(),
+ IsActive: node.GetIsActive(),
+ StatusJSON: node.GetStatusJSON(),
+ InstallStatusJSON: installStatusJSON,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+
this.Success()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go
index e8f7d74..a9d8c2b 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go
@@ -1,9 +1,6 @@
package ech
-import (
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
-)
+import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
type AuditAction struct {
actionutils.ParentAction
@@ -14,15 +11,5 @@ func (this *AuditAction) Init() {
}
func (this *AuditAction) RunGet(params struct{}) {
- httpdnsutils.AddLeftMenu(this.Parent())
- this.Data["auditLogs"] = []map[string]interface{}{
- {
- "id": 1,
- "scope": "tenant:1001 -> domain:api.business.com -> region:cn-hz",
- "operator": "admin",
- "result": "success",
- "createdAt": "2026-02-21 10:00:00",
- },
- }
- this.Show()
+ this.RedirectURL("/httpdns/apps")
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go
index b33aa5f..76b9832 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go
@@ -1,9 +1,6 @@
package ech
-import (
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
-)
+import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
type IndexAction struct {
actionutils.ParentAction
@@ -14,34 +11,5 @@ func (this *IndexAction) Init() {
}
func (this *IndexAction) RunGet(params struct{}) {
- httpdnsutils.AddLeftMenu(this.Parent())
- this.Data["health"] = map[string]interface{}{
- "keySyncRate": 0.9995,
- "decryptFailRate": 0.0001,
- "isHealthy": true,
- }
-
- this.Data["echLogs"] = []map[string]interface{}{
- {
- "id": 1024,
- "version": "ech-v20260221-01",
- "recordType": "HTTPS(Type65)",
- "publicKey": "BFCf8h5Qmock_public_key_for_ui_preview_lx9v2k7p0n",
- "publishTime": "2026-02-21 00:00:00",
- "syncStatus": "success",
- "nodesPending": 0,
- "isCurrent": true,
- },
- {
- "id": 1023,
- "version": "ech-v20260220-01",
- "recordType": "TXT",
- "publicKey": "BE9x3a2Qmock_prev_key_for_ui_preview_vd1n7x5k2c",
- "publishTime": "2026-02-20 00:00:00",
- "syncStatus": "success",
- "nodesPending": 0,
- "isCurrent": false,
- },
- }
- this.Show()
+ this.RedirectURL("/httpdns/apps")
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go b/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go
index 4fc36a2..18c35c5 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/ech/rollbackMfaPopup.go
@@ -16,8 +16,7 @@ func (this *RollbackMfaPopupAction) Init() {
func (this *RollbackMfaPopupAction) RunGet(params struct {
LogId int64
}) {
- this.Data["logId"] = params.LogId
- this.Show()
+ this.RedirectURL("/httpdns/apps")
}
func (this *RollbackMfaPopupAction) RunPost(params struct {
@@ -29,8 +28,5 @@ func (this *RollbackMfaPopupAction) RunPost(params struct {
Must *actions.Must
CSRF *actionutils.CSRF
}) {
- params.Must.Field("reason", params.Reason).Require("please input rollback reason")
- params.Must.Field("otpCode1", params.OtpCode1).Require("please input first otp code")
- params.Must.Field("otpCode2", params.OtpCode2).Require("please input second otp code")
- this.Success()
+ this.Fail("feature removed")
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go
index 102e538..546bcd3 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/policies/index.go
@@ -1,11 +1,6 @@
package policies
-import (
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
- "github.com/iwind/TeaGo/actions"
- "github.com/iwind/TeaGo/maps"
-)
+import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
type IndexAction struct {
actionutils.ParentAction
@@ -16,52 +11,9 @@ func (this *IndexAction) Init() {
}
func (this *IndexAction) RunGet(params struct{}) {
- httpdnsutils.AddLeftMenu(this.Parent())
- this.Data["policies"] = loadGlobalPolicies()
- this.Data["availableClusters"] = loadAvailableDeployClusters()
- this.Show()
+ this.RedirectURL("/httpdns/clusters")
}
-func (this *IndexAction) RunPost(params struct {
- DefaultClusterId int64
- DefaultTTL int
- DefaultFallbackMs int
-
- Must *actions.Must
- CSRF *actionutils.CSRF
-}) {
- if params.DefaultClusterId <= 0 || !isValidClusterID(params.DefaultClusterId) {
- this.Fail("please select a valid default cluster")
- return
- }
-
- params.Must.Field("defaultTTL", params.DefaultTTL).Gt(0, "default ttl should be > 0")
- params.Must.Field("defaultFallbackMs", params.DefaultFallbackMs).Gt(0, "default fallback should be > 0")
-
- if params.DefaultTTL > 86400 {
- this.Fail("default TTL should be <= 86400")
- return
- }
- if params.DefaultFallbackMs > 10000 {
- this.Fail("default fallback should be <= 10000 ms")
- return
- }
-
- saveGlobalPolicies(maps.Map{
- "defaultClusterId": params.DefaultClusterId,
- "enableUserDomainVerify": true,
- "defaultTTL": params.DefaultTTL,
- "defaultFallbackMs": params.DefaultFallbackMs,
- })
-
+func (this *IndexAction) RunPost(params struct{}) {
this.Success()
}
-
-func isValidClusterID(clusterID int64) bool {
- for _, cluster := range loadAvailableDeployClusters() {
- if cluster.GetInt64("id") == clusterID {
- return true
- }
- }
- return false
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go b/EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go
deleted file mode 100644
index b4f4080..0000000
--- a/EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go
+++ /dev/null
@@ -1,180 +0,0 @@
-package policies
-
-import (
- "strings"
- "sync"
-
- "github.com/iwind/TeaGo/maps"
-)
-
-var globalPoliciesStore = struct {
- sync.RWMutex
- data maps.Map
- publicSniPools []maps.Map
- publicSniCertificates []maps.Map
-}{
- data: maps.Map{
- "defaultClusterId": int64(1),
- "enableUserDomainVerify": true,
- "defaultTTL": 30,
- "defaultFallbackMs": 300,
- },
- publicSniPools: []maps.Map{
- {
- "domain": "public-sni-a.waf.example.com",
- "certId": "cert_public_sni_a",
- },
- {
- "domain": "public-sni-b.waf.example.com",
- "certId": "cert_public_sni_b",
- },
- },
- publicSniCertificates: []maps.Map{
- {
- "id": "cert_public_sni_a",
- "name": "public-sni-a.waf.example.com",
- "issuer": "Mock CA",
- "expiresAt": "2026-12-31 23:59:59",
- },
- {
- "id": "cert_public_sni_b",
- "name": "public-sni-b.waf.example.com",
- "issuer": "Mock CA",
- "expiresAt": "2027-03-31 23:59:59",
- },
- },
-}
-
-func loadGlobalPolicies() maps.Map {
- globalPoliciesStore.RLock()
- defer globalPoliciesStore.RUnlock()
-
- return maps.Map{
- "defaultClusterId": globalPoliciesStore.data.GetInt64("defaultClusterId"),
- "enableUserDomainVerify": true,
- "defaultTTL": globalPoliciesStore.data.GetInt("defaultTTL"),
- "defaultFallbackMs": globalPoliciesStore.data.GetInt("defaultFallbackMs"),
- }
-}
-
-func saveGlobalPolicies(policies maps.Map) {
- globalPoliciesStore.Lock()
- globalPoliciesStore.data = maps.Map{
- "defaultClusterId": policies.GetInt64("defaultClusterId"),
- "enableUserDomainVerify": true,
- "defaultTTL": policies.GetInt("defaultTTL"),
- "defaultFallbackMs": policies.GetInt("defaultFallbackMs"),
- }
-
- globalPoliciesStore.Unlock()
-}
-
-func loadPublicSNICertificates() []maps.Map {
- globalPoliciesStore.RLock()
- defer globalPoliciesStore.RUnlock()
-
- return cloneMapSlice(globalPoliciesStore.publicSniCertificates)
-}
-
-// LoadPublicSNICertificates returns global public SNI certificates for other httpdns modules.
-func LoadPublicSNICertificates() []maps.Map {
- return loadPublicSNICertificates()
-}
-
-func loadAvailableDeployClusters() []maps.Map {
- return []maps.Map{
- {
- "id": int64(1),
- "name": "gateway-cn-hz",
- "region": "cn-hangzhou",
- "gatewayDomain": "gw-hz.httpdns.example.com",
- },
- {
- "id": int64(2),
- "name": "gateway-cn-bj",
- "region": "cn-beijing",
- "gatewayDomain": "gw-bj.httpdns.example.com",
- },
- }
-}
-
-// LoadAvailableDeployClusters returns selectable clusters for HTTPDNS user defaults.
-func LoadAvailableDeployClusters() []maps.Map {
- return loadAvailableDeployClusters()
-}
-
-// LoadDefaultClusterID returns current default deploy cluster id for HTTPDNS users.
-func LoadDefaultClusterID() int64 {
- globalPoliciesStore.RLock()
- defer globalPoliciesStore.RUnlock()
- return globalPoliciesStore.data.GetInt64("defaultClusterId")
-}
-
-// SaveDefaultClusterID updates default deploy cluster id.
-// Pass 0 to clear and let caller fallback to first available cluster.
-func SaveDefaultClusterID(clusterID int64) {
- globalPoliciesStore.Lock()
- globalPoliciesStore.data["defaultClusterId"] = clusterID
- globalPoliciesStore.Unlock()
-}
-
-// LoadClusterGatewayByID returns gateway domain for the selected cluster.
-func LoadClusterGatewayByID(clusterID int64) string {
- clusters := loadAvailableDeployClusters()
- for _, cluster := range clusters {
- if cluster.GetInt64("id") == clusterID {
- gatewayDomain := strings.TrimSpace(cluster.GetString("gatewayDomain"))
- if len(gatewayDomain) > 0 {
- return gatewayDomain
- }
- }
- }
-
- if len(clusters) > 0 {
- fallback := strings.TrimSpace(clusters[0].GetString("gatewayDomain"))
- if len(fallback) > 0 {
- return fallback
- }
- }
-
- return "gw.httpdns.example.com"
-}
-
-// LoadPublicSNIPools returns global public SNI domain pool for other httpdns modules.
-func LoadPublicSNIPools() []maps.Map {
- globalPoliciesStore.RLock()
- defer globalPoliciesStore.RUnlock()
-
- return cloneMapSlice(globalPoliciesStore.publicSniPools)
-}
-
-// HasPublicSNIDomain checks if a public SNI domain exists in global pool.
-func HasPublicSNIDomain(domain string) bool {
- domain = strings.ToLower(strings.TrimSpace(domain))
- if len(domain) == 0 {
- return false
- }
-
- globalPoliciesStore.RLock()
- defer globalPoliciesStore.RUnlock()
-
- for _, pool := range globalPoliciesStore.publicSniPools {
- if strings.ToLower(strings.TrimSpace(pool.GetString("domain"))) == domain {
- return true
- }
- }
-
- return false
-}
-
-func cloneMapSlice(src []maps.Map) []maps.Map {
- result := make([]maps.Map, 0, len(src))
- for _, item := range src {
- cloned := maps.Map{}
- for k, v := range item {
- cloned[k] = v
- }
- result = append(result, cloned)
- }
- return result
-}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go
index eac62f1..2dbf574 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs/index.go
@@ -5,6 +5,8 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
@@ -40,120 +42,88 @@ func (this *IndexAction) RunGet(params struct {
this.Data["status"] = params.Status
this.Data["keyword"] = params.Keyword
- clusters := []map[string]interface{}{
- {"id": int64(1), "name": "gateway-cn-hz"},
- {"id": int64(2), "name": "gateway-cn-bj"},
+ clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
- nodes := []map[string]interface{}{
- {"id": int64(101), "clusterId": int64(1), "name": "hz-node-01"},
- {"id": int64(102), "clusterId": int64(1), "name": "hz-node-02"},
- {"id": int64(201), "clusterId": int64(2), "name": "bj-node-01"},
+ clusters := make([]map[string]interface{}, 0, len(clusterResp.GetClusters()))
+ nodes := make([]map[string]interface{}, 0)
+ for _, cluster := range clusterResp.GetClusters() {
+ clusters = append(clusters, map[string]interface{}{
+ "id": cluster.GetId(),
+ "name": cluster.GetName(),
+ })
+ nodeResp, err := this.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(this.AdminContext(), &pb.ListHTTPDNSNodesRequest{
+ ClusterId: cluster.GetId(),
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ for _, node := range nodeResp.GetNodes() {
+ nodes = append(nodes, map[string]interface{}{
+ "id": node.GetId(),
+ "clusterId": node.GetClusterId(),
+ "name": node.GetName(),
+ })
+ }
}
this.Data["clusters"] = clusters
this.Data["nodes"] = nodes
- allLogs := []map[string]interface{}{
- {
- "time": "2026-02-23 10:21:51",
- "clusterId": int64(1),
- "clusterName": "gateway-cn-hz",
- "nodeId": int64(101),
- "nodeName": "hz-node-01",
- "appName": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
- "appId": "ab12xc34s2",
- "domain": "api.business.com",
- "query": "A",
- "clientIp": "203.0.113.25",
- "os": "Android",
- "sdkVersion": "2.4.1",
- "ips": "198.51.100.10,198.51.100.11",
- "status": "success",
- "errorCode": "none",
- "costMs": 37,
- "pinningMode": "report",
- "pinningResult": "warn",
- "sanMode": "strict",
- "sanResult": "pass",
- },
- {
- "time": "2026-02-23 10:18:02",
- "clusterId": int64(2),
- "clusterName": "gateway-cn-bj",
- "nodeId": int64(201),
- "nodeName": "bj-node-01",
- "appName": "\u652f\u4ed8\u7f51\u5173\u4e1a\u52a1",
- "appId": "vd8992ksm1",
- "domain": "payment.business.com",
- "query": "A",
- "clientIp": "198.51.100.67",
- "os": "iOS",
- "sdkVersion": "2.3.9",
- "ips": "198.51.100.22",
- "status": "failed",
- "errorCode": "40301",
- "costMs": 52,
- "pinningMode": "enforce",
- "pinningResult": "fail",
- "sanMode": "strict",
- "sanResult": "fail",
- },
- {
- "time": "2026-02-23 10:12:44",
- "clusterId": int64(1),
- "clusterName": "gateway-cn-hz",
- "nodeId": int64(102),
- "nodeName": "hz-node-02",
- "appName": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
- "appId": "ab12xc34s2",
- "domain": "www.aliyun.com",
- "query": "A",
- "clientIp": "106.11.63.180",
- "os": "Unknown",
- "sdkVersion": "none",
- "ips": "3.33.120.190,15.197.120.33",
- "status": "success",
- "errorCode": "none",
- "costMs": 41,
- "pinningMode": "off",
- "pinningResult": "na",
- "sanMode": "report",
- "sanResult": "warn",
- },
+ logResp, err := this.RPC().HTTPDNSAccessLogRPC().ListHTTPDNSAccessLogs(this.AdminContext(), &pb.ListHTTPDNSAccessLogsRequest{
+ Day: "",
+ ClusterId: params.ClusterId,
+ NodeId: params.NodeId,
+ AppId: strings.TrimSpace(params.AppId),
+ Domain: strings.TrimSpace(params.Domain),
+ Status: strings.TrimSpace(params.Status),
+ Keyword: strings.TrimSpace(params.Keyword),
+ Offset: 0,
+ Size: 100,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
- status := strings.TrimSpace(strings.ToLower(params.Status))
- appID := strings.TrimSpace(strings.ToLower(params.AppId))
- domain := strings.TrimSpace(strings.ToLower(params.Domain))
- keyword := strings.TrimSpace(strings.ToLower(params.Keyword))
+ logs := make([]map[string]interface{}, 0, len(logResp.GetLogs()))
+ for _, item := range logResp.GetLogs() {
+ createdTime := ""
+ if item.GetCreatedAt() > 0 {
+ createdTime = timeutil.FormatTime("Y-m-d H:i:s", item.GetCreatedAt())
+ }
+ status := item.GetStatus()
+ if len(status) == 0 {
+ status = "failed"
+ }
+ errorCode := item.GetErrorCode()
+ if len(errorCode) == 0 {
+ errorCode = "none"
+ }
- filtered := make([]map[string]interface{}, 0)
- for _, log := range allLogs {
- if params.ClusterId > 0 && log["clusterId"].(int64) != params.ClusterId {
- continue
- }
- if params.NodeId > 0 && log["nodeId"].(int64) != params.NodeId {
- continue
- }
- if len(status) > 0 && log["status"].(string) != status {
- continue
- }
- if len(appID) > 0 && !strings.Contains(strings.ToLower(log["appId"].(string)), appID) {
- continue
- }
- if len(domain) > 0 && !strings.Contains(strings.ToLower(log["domain"].(string)), domain) {
- continue
- }
- if len(keyword) > 0 {
- if !strings.Contains(strings.ToLower(log["appName"].(string)), keyword) &&
- !strings.Contains(strings.ToLower(log["domain"].(string)), keyword) &&
- !strings.Contains(strings.ToLower(log["clientIp"].(string)), keyword) &&
- !strings.Contains(strings.ToLower(log["ips"].(string)), keyword) {
- continue
- }
- }
- filtered = append(filtered, log)
+ logs = append(logs, map[string]interface{}{
+ "time": createdTime,
+ "clusterId": item.GetClusterId(),
+ "clusterName": item.GetClusterName(),
+ "nodeId": item.GetNodeId(),
+ "nodeName": item.GetNodeName(),
+ "appName": item.GetAppName(),
+ "appId": item.GetAppId(),
+ "domain": item.GetDomain(),
+ "query": item.GetQtype(),
+ "clientIp": item.GetClientIP(),
+ "os": item.GetOs(),
+ "sdkVersion": item.GetSdkVersion(),
+ "ips": item.GetResultIPs(),
+ "status": status,
+ "errorCode": errorCode,
+ "costMs": item.GetCostMs(),
+ })
}
- this.Data["resolveLogs"] = filtered
+ this.Data["resolveLogs"] = logs
this.Show()
}
+
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go
index 9ccb264..6d2071e 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs/index.go
@@ -5,6 +5,8 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
@@ -40,105 +42,75 @@ func (this *IndexAction) RunGet(params struct {
this.Data["level"] = params.Level
this.Data["keyword"] = params.Keyword
- clusters := []map[string]interface{}{
- {"id": int64(1), "name": "gateway-cn-hz"},
- {"id": int64(2), "name": "gateway-cn-bj"},
+ clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
- nodes := []map[string]interface{}{
- {"id": int64(101), "clusterId": int64(1), "name": "hz-node-01"},
- {"id": int64(102), "clusterId": int64(1), "name": "hz-node-02"},
- {"id": int64(201), "clusterId": int64(2), "name": "bj-node-01"},
+ clusters := make([]map[string]interface{}, 0, len(clusterResp.GetClusters()))
+ nodes := make([]map[string]interface{}, 0)
+ for _, cluster := range clusterResp.GetClusters() {
+ clusters = append(clusters, map[string]interface{}{
+ "id": cluster.GetId(),
+ "name": cluster.GetName(),
+ })
+ nodeResp, err := this.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(this.AdminContext(), &pb.ListHTTPDNSNodesRequest{
+ ClusterId: cluster.GetId(),
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ for _, node := range nodeResp.GetNodes() {
+ nodes = append(nodes, map[string]interface{}{
+ "id": node.GetId(),
+ "clusterId": node.GetClusterId(),
+ "name": node.GetName(),
+ })
+ }
}
this.Data["clusters"] = clusters
this.Data["nodes"] = nodes
- allLogs := []map[string]interface{}{
- {
- "createdTime": "2026-02-23 10:30:12",
- "clusterId": int64(1),
- "clusterName": "gateway-cn-hz",
- "nodeId": int64(101),
- "nodeName": "hz-node-01",
- "level": "info",
- "tag": "config",
- "module": "resolver",
- "description": "\u914d\u7f6e\u70ed\u52a0\u8f7d\u5b8c\u6210\uff0c\u5df2\u542f\u7528\u6700\u65b0\u7b56\u7565\u5feb\u7167\u3002",
- "count": int64(1),
- "requestId": "rid-20260223-001",
- },
- {
- "createdTime": "2026-02-23 10:27:38",
- "clusterId": int64(2),
- "clusterName": "gateway-cn-bj",
- "nodeId": int64(201),
- "nodeName": "bj-node-01",
- "level": "warning",
- "tag": "sni",
- "module": "policy-engine",
- "description": "\u68c0\u6d4b\u5230 level3 \u6761\u4ef6\u4e0d\u6ee1\u8db3\uff0c\u81ea\u52a8\u964d\u7ea7\u5230 level2\u3002",
- "count": int64(3),
- "requestId": "rid-20260223-002",
- },
- {
- "createdTime": "2026-02-23 10:22:49",
- "clusterId": int64(1),
- "clusterName": "gateway-cn-hz",
- "nodeId": int64(102),
- "nodeName": "hz-node-02",
- "level": "success",
- "tag": "cache-refresh",
- "module": "cache",
- "description": "\u57df\u540d\u7f13\u5b58\u5237\u65b0\u4efb\u52a1\u5b8c\u6210\uff0c\u6210\u529f 2/2\u3002",
- "count": int64(1),
- "requestId": "rid-20260223-003",
- },
- {
- "createdTime": "2026-02-23 10:18:11",
- "clusterId": int64(1),
- "clusterName": "gateway-cn-hz",
- "nodeId": int64(101),
- "nodeName": "hz-node-01",
- "level": "error",
- "tag": "upstream",
- "module": "resolver",
- "description": "\u4e0a\u6e38\u6743\u5a01 DNS \u8bf7\u6c42\u8d85\u65f6\uff0c\u89e6\u53d1\u91cd\u8bd5\u540e\u6062\u590d\u3002",
- "count": int64(2),
- "requestId": "rid-20260223-004",
- },
+ day := strings.TrimSpace(params.DayFrom)
+ if len(day) == 0 {
+ day = strings.TrimSpace(params.DayTo)
+ }
+ logResp, err := this.RPC().HTTPDNSRuntimeLogRPC().ListHTTPDNSRuntimeLogs(this.AdminContext(), &pb.ListHTTPDNSRuntimeLogsRequest{
+ Day: day,
+ ClusterId: params.ClusterId,
+ NodeId: params.NodeId,
+ Level: strings.TrimSpace(params.Level),
+ Keyword: strings.TrimSpace(params.Keyword),
+ Offset: 0,
+ Size: 100,
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
- keyword := strings.TrimSpace(strings.ToLower(params.Keyword))
- filtered := make([]map[string]interface{}, 0)
- for _, log := range allLogs {
- if params.ClusterId > 0 && log["clusterId"].(int64) != params.ClusterId {
- continue
+ runtimeLogs := make([]map[string]interface{}, 0, len(logResp.GetLogs()))
+ for _, item := range logResp.GetLogs() {
+ createdTime := ""
+ if item.GetCreatedAt() > 0 {
+ createdTime = timeutil.FormatTime("Y-m-d H:i:s", item.GetCreatedAt())
}
- if params.NodeId > 0 && log["nodeId"].(int64) != params.NodeId {
- continue
- }
- if len(params.Level) > 0 && log["level"].(string) != params.Level {
- continue
- }
- if len(params.DayFrom) > 0 {
- if log["createdTime"].(string)[:10] < params.DayFrom {
- continue
- }
- }
- if len(params.DayTo) > 0 {
- if log["createdTime"].(string)[:10] > params.DayTo {
- continue
- }
- }
- if len(keyword) > 0 {
- if !strings.Contains(strings.ToLower(log["tag"].(string)), keyword) &&
- !strings.Contains(strings.ToLower(log["description"].(string)), keyword) &&
- !strings.Contains(strings.ToLower(log["nodeName"].(string)), keyword) {
- continue
- }
- }
- filtered = append(filtered, log)
+ runtimeLogs = append(runtimeLogs, map[string]interface{}{
+ "createdTime": createdTime,
+ "clusterId": item.GetClusterId(),
+ "clusterName": item.GetClusterName(),
+ "nodeId": item.GetNodeId(),
+ "nodeName": item.GetNodeName(),
+ "level": item.GetLevel(),
+ "tag": item.GetType(),
+ "module": item.GetModule(),
+ "description": item.GetDescription(),
+ "count": item.GetCount(),
+ "requestId": item.GetRequestId(),
+ })
}
-
- this.Data["runtimeLogs"] = filtered
+ this.Data["runtimeLogs"] = runtimeLogs
this.Show()
}
+
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go
index 4109d16..243c8e5 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/index.go
@@ -3,7 +3,8 @@ package sandbox
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
+ "github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
@@ -17,29 +18,48 @@ func (this *IndexAction) Init() {
func (this *IndexAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
- this.Data["clusters"] = policies.LoadAvailableDeployClusters()
- this.Data["apps"] = []map[string]interface{}{
- {
- "id": int64(1),
- "name": "主站移动业务",
- "appId": "ab12xc34s2",
- "clusterId": int64(1),
- "domains": []string{"api.business.com", "www.aliyun.com"},
- },
- {
- "id": int64(2),
- "name": "支付网关业务",
- "appId": "vd8992ksm1",
- "clusterId": int64(2),
- "domains": []string{"payment.business.com"},
- },
- {
- "id": int64(3),
- "name": "海外灰度测试",
- "appId": "ov7711hkq9",
- "clusterId": int64(1),
- "domains": []string{"global.example.com", "edge.example.com"},
- },
+ clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
+ clusters := make([]maps.Map, 0, len(clusterResp.GetClusters()))
+ for _, cluster := range clusterResp.GetClusters() {
+ clusters = append(clusters, maps.Map{
+ "id": cluster.GetId(),
+ "name": cluster.GetName(),
+ })
+ }
+ this.Data["clusters"] = clusters
+
+ appResp, err := this.RPC().HTTPDNSAppRPC().FindAllHTTPDNSApps(this.AdminContext(), &pb.FindAllHTTPDNSAppsRequest{})
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ apps := make([]maps.Map, 0, len(appResp.GetApps()))
+ for _, app := range appResp.GetApps() {
+ domainResp, err := this.RPC().HTTPDNSDomainRPC().ListHTTPDNSDomainsWithAppId(this.AdminContext(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
+ AppDbId: app.GetId(),
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
+ }
+ domains := make([]string, 0, len(domainResp.GetDomains()))
+ for _, domain := range domainResp.GetDomains() {
+ domains = append(domains, domain.GetDomain())
+ }
+ apps = append(apps, maps.Map{
+ "id": app.GetId(),
+ "name": app.GetName(),
+ "appId": app.GetAppId(),
+ "clusterId": app.GetPrimaryClusterId(),
+ "primaryClusterId": app.GetPrimaryClusterId(),
+ "backupClusterId": app.GetBackupClusterId(),
+ "domains": domains,
+ })
+ }
+ this.Data["apps"] = apps
this.Show()
}
diff --git a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go
index 24117ea..7eab116 100644
--- a/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go
+++ b/EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go
@@ -1,13 +1,19 @@
package sandbox
import (
- "net"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/hex"
"net/url"
"strconv"
+ "strings"
+ "time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
- "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/index/loginutils"
+ "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
+ "github.com/iwind/TeaGo/rands"
)
type TestAction struct {
@@ -21,96 +27,125 @@ func (this *TestAction) RunPost(params struct {
ClientIp string
Qtype string
}) {
- if len(params.ClientIp) == 0 {
- params.ClientIp = "203.0.113.100"
+ clientIP := strings.TrimSpace(params.ClientIp)
+ if len(clientIP) == 0 {
+ clientIP = strings.TrimSpace(loginutils.RemoteIP(&this.ActionObject))
+ }
+ qtype := strings.ToUpper(strings.TrimSpace(params.Qtype))
+ if len(qtype) == 0 {
+ qtype = "A"
}
- clientSubnet := this.maskSubnet(params.ClientIp, 24, 56)
- if len(clientSubnet) == 0 {
- clientSubnet = "203.0.113.0/24"
+ resp, err := this.RPC().HTTPDNSSandboxRPC().TestHTTPDNSResolve(this.AdminContext(), &pb.TestHTTPDNSResolveRequest{
+ ClusterId: params.ClusterId,
+ AppId: strings.TrimSpace(params.AppId),
+ Domain: strings.TrimSpace(params.Domain),
+ Qtype: qtype,
+ ClientIP: clientIP,
+ Sid: "",
+ SdkVersion: "",
+ Os: "",
+ })
+ if err != nil {
+ this.ErrorPage(err)
+ return
}
- sniPolicy := "empty"
- publicSNI := ""
- ecsMode := "off"
- ecsIPv4Prefix := 24
- ecsIPv6Prefix := 56
- pinningMode := "off"
- sanMode := "off"
+ clusterDomain := ""
+ if params.ClusterId > 0 {
+ clusterResp, findErr := this.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(this.AdminContext(), &pb.FindHTTPDNSClusterRequest{
+ ClusterId: params.ClusterId,
+ })
+ if findErr == nil && clusterResp.GetCluster() != nil {
+ clusterDomain = strings.TrimSpace(clusterResp.GetCluster().GetServiceDomain())
+ }
+ }
+ if len(clusterDomain) == 0 {
+ clusterDomain = "httpdns.example.com"
+ }
query := url.Values{}
query.Set("appId", params.AppId)
query.Set("dn", params.Domain)
- query.Set("cip", params.ClientIp)
- query.Set("qtype", params.Qtype)
- clusterServiceDomain := policies.LoadClusterGatewayByID(params.ClusterId)
- if len(clusterServiceDomain) == 0 {
- clusterServiceDomain = "gw.httpdns.example.com"
+ query.Set("qtype", qtype)
+ if len(clientIP) > 0 {
+ query.Set("cip", clientIP)
+ }
+
+ signEnabled, signSecret := this.findAppSignConfig(params.AppId)
+ if signEnabled && len(signSecret) > 0 {
+ exp := strconv.FormatInt(time.Now().Unix()+300, 10)
+ nonce := "sandbox-" + rands.HexString(16)
+ sign := buildSandboxResolveSign(signSecret, params.AppId, params.Domain, qtype, exp, nonce)
+ query.Set("exp", exp)
+ query.Set("nonce", nonce)
+ query.Set("sign", sign)
+ }
+ requestURL := "https://" + clusterDomain + "/resolve?" + query.Encode()
+
+ resultCode := 1
+ if strings.EqualFold(resp.GetCode(), "SUCCESS") {
+ resultCode = 0
+ }
+ rows := make([]maps.Map, 0, len(resp.GetRecords()))
+ ips := make([]string, 0, len(resp.GetRecords()))
+ lineName := strings.TrimSpace(resp.GetClientCarrier())
+ if len(lineName) == 0 {
+ lineName = "-"
+ }
+ for _, record := range resp.GetRecords() {
+ ips = append(ips, record.GetIp())
+ if lineName == "-" && len(record.GetLine()) > 0 {
+ lineName = record.GetLine()
+ }
+ rows = append(rows, maps.Map{
+ "domain": resp.GetDomain(),
+ "type": record.GetType(),
+ "ip": record.GetIp(),
+ "ttl": record.GetTtl(),
+ "region": record.GetRegion(),
+ "line": record.GetLine(),
+ })
}
- requestURL := "https://" + clusterServiceDomain + "/resolve?" + query.Encode()
this.Data["result"] = maps.Map{
- "code": 0,
- "message": "ok (mock)",
- "requestId": "mock-rid-20260221-001",
+ "code": resultCode,
+ "message": resp.GetMessage(),
+ "requestId": resp.GetRequestId(),
"data": maps.Map{
"request_url": requestURL,
- "client_ip": params.ClientIp,
- "client_region": "中国, 上海, 上海",
- "line_name": "默认线路",
- "ips": []string{"203.0.113.10", "203.0.113.11"},
- "records": []maps.Map{
- {
- "domain": params.Domain,
- "type": params.Qtype,
- "ip": "203.0.113.10",
- "ttl": 30,
- "region": "中国-上海-上海",
- "line": "默认线路",
- },
- {
- "domain": params.Domain,
- "type": params.Qtype,
- "ip": "203.0.113.11",
- "ttl": 30,
- "region": "中国-上海-上海",
- "line": "默认线路",
- },
- },
- "ttl": 30,
- "sni_policy": sniPolicy,
- "public_sni": publicSNI,
- "client_subnet": clientSubnet,
- "ecs_mode": ecsMode,
- "ecs_ipv4_prefix": ecsIPv4Prefix,
- "ecs_ipv6_prefix": ecsIPv6Prefix,
- "pinning_mode": pinningMode,
- "san_mode": sanMode,
- "fingerprint_algo": "sha256",
- "cert_fingerprints": []string{"8f41ab8d32fca7f9a2b0fdd09a0e2ccf31e5579cd3a0b65f1a9f2f2ec8f38d4a"},
- "verify_headers": maps.Map{
- "X-Edge-Resolve-AppId": params.AppId,
- "X-Edge-Resolve-Expire": "1768953600",
- "X-Edge-Resolve-Nonce": "mock-nonce-123456",
- "X-Edge-Resolve-Target": params.Domain,
- "X-Edge-Resolve-Sign": "mock-signature-base64url",
- },
+ "client_ip": resp.GetClientIP(),
+ "client_region": resp.GetClientRegion(),
+ "line_name": lineName,
+ "ips": ips,
+ "records": rows,
+ "ttl": resp.GetTtl(),
},
}
this.Success()
}
-func (this *TestAction) maskSubnet(ip string, ipv4Prefix int, ipv6Prefix int) string {
- parsed := net.ParseIP(ip)
- if parsed == nil {
- return ""
+func (this *TestAction) findAppSignConfig(appId string) (bool, string) {
+ appId = strings.TrimSpace(appId)
+ if len(appId) == 0 {
+ return false, ""
}
- ipv4 := parsed.To4()
- if ipv4 != nil {
- mask := net.CIDRMask(ipv4Prefix, 32)
- return ipv4.Mask(mask).String() + "/" + strconv.Itoa(ipv4Prefix)
+ resp, err := this.RPC().HTTPDNSAppRPC().FindAllHTTPDNSApps(this.AdminContext(), &pb.FindAllHTTPDNSAppsRequest{})
+ if err != nil {
+ return false, ""
}
- mask := net.CIDRMask(ipv6Prefix, 128)
- return parsed.Mask(mask).String() + "/" + strconv.Itoa(ipv6Prefix)
+ for _, app := range resp.GetApps() {
+ if strings.EqualFold(strings.TrimSpace(app.GetAppId()), appId) {
+ return app.GetSignEnabled(), strings.TrimSpace(app.GetSignSecret())
+ }
+ }
+ return false, ""
+}
+
+func buildSandboxResolveSign(signSecret string, appID string, domain string, qtype string, exp string, nonce string) string {
+ raw := strings.TrimSpace(appID) + "|" + strings.ToLower(strings.TrimSpace(domain)) + "|" + strings.ToUpper(strings.TrimSpace(qtype)) + "|" + strings.TrimSpace(exp) + "|" + strings.TrimSpace(nonce)
+ mac := hmac.New(sha256.New, []byte(strings.TrimSpace(signSecret)))
+ _, _ = mac.Write([]byte(raw))
+ return hex.EncodeToString(mac.Sum(nil))
}
diff --git a/EdgeAdmin/internal/web/import.go b/EdgeAdmin/internal/web/import.go
index 10f2c89..73167f8 100644
--- a/EdgeAdmin/internal/web/import.go
+++ b/EdgeAdmin/internal/web/import.go
@@ -141,9 +141,7 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters"
- _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/ech"
- _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/guide"
- _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
+ _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/sandbox"
diff --git a/EdgeAdmin/test_db.go b/EdgeAdmin/test_db.go
new file mode 100644
index 0000000..02ecb46
--- /dev/null
+++ b/EdgeAdmin/test_db.go
@@ -0,0 +1 @@
+package main; import ("fmt"; "github.com/TeaOSLab/EdgeAPI/internal/db/models"; "github.com/iwind/TeaGo/dbs"; "github.com/iwind/TeaGo/Tea"); func main() { Tea.Env = "prod"; dbs.OnReady(func(){ apps, _ := models.SharedHTTPDNSAppDAO.FindAllEnabledApps(nil); for _, app := range apps { fmt.Printf("App: %s, Primary: %d\n", app.AppId, app.PrimaryClusterId) } }); }
diff --git a/EdgeAdmin/test_db2.go b/EdgeAdmin/test_db2.go
new file mode 100644
index 0000000..9f26f1c
--- /dev/null
+++ b/EdgeAdmin/test_db2.go
@@ -0,0 +1 @@
+package main; import ("fmt"; "github.com/TeaOSLab/EdgeAPI/internal/db/models"; _ "github.com/go-sql-driver/mysql"; "github.com/iwind/TeaGo/dbs"; "github.com/iwind/TeaGo/Tea"); func main() { Tea.Env = "prod"; dbConfig := &dbs.Config{Driver: "mysql", Dsn: "edge:123456@tcp(127.0.0.1:3306)/edge?charset=utf8mb4&parseTime=true&loc=Local", Prefix: "edge"}; dbs.DefaultDB(dbConfig); apps, _ := models.SharedHTTPDNSAppDAO.FindAllEnabledApps(nil); for _, app := range apps { fmt.Printf("App: %s, Primary: %d\n", app.AppId, app.PrimaryClusterId) } }
diff --git a/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html b/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html
index 3e06600..dda96cc 100644
--- a/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html
+++ b/EdgeAdmin/web/views/@default/httpdns/apps/@menu.html
@@ -1,4 +1,4 @@