管理端全部功能跑通

This commit is contained in:
robin
2026-02-27 10:35:22 +08:00
parent 4d275c921d
commit 150799f41d
263 changed files with 22664 additions and 4053 deletions

View File

@@ -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 ..."

View File

@@ -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{})")
}
}

View File

@@ -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
}
}

View File

@@ -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
)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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()
}

View File

@@ -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{}
}

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -9,6 +9,8 @@ type NodeParams struct {
Endpoints []string
NodeId string
Secret string
TLSCertData []byte
TLSKeyData []byte
IsUpgrading bool // 是否为升级
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 &copyTargets
}
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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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:

View File

@@ -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
}

View File

@@ -16,6 +16,7 @@ const (
UserTypeCluster = "cluster"
UserTypeStat = "stat"
UserTypeDNS = "dns"
UserTypeHTTPDNS = "httpdns"
UserTypeLog = "log"
UserTypeAPI = "api"
UserTypeAuthority = "authority"

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -110,6 +110,9 @@ var upgradeFuncs = []*upgradeVersion{
{
"1.4.4", upgradeV1_4_4,
},
{
"1.4.8", upgradeV1_4_8,
},
}
// UpgradeSQLData 升级SQL数据
@@ -269,7 +272,7 @@ 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
@@ -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,7 +376,7 @@ func upgradeV0_3_2(db *dbs.DB) error {
// gzip => compression
type HTTPGzipRef struct {
IsPrior bool `yaml:"isPrior" json:"isPrior"` // 是否覆盖
IsPrior bool `yaml:"isPrior" json:"isPrior"` // 鏄惁瑕嗙洊
IsOn bool `yaml:"isOn" json:"isOn"` // 是否开启
GzipId int64 `yaml:"gzipId" json:"gzipId"` // 使用的配置ID
}
@@ -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
{
@@ -752,16 +754,16 @@ func upgradeV1_2_1(db *dbs.DB) error {
func upgradeV1_2_10(db *dbs.DB) error {
{
type OldGlobalConfig struct {
// HTTP & HTTPS相关配置
// HTTP & HTTPS鐩稿叧閰嶇疆
HTTPAll struct {
DomainAuditingIsOn bool `yaml:"domainAuditingIsOn" json:"domainAuditingIsOn"` // 域名是否需要审核
DomainAuditingPrompt string `yaml:"domainAuditingPrompt" json:"domainAuditingPrompt"` // 域名审核提示
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"` // 禁止使用的端口
DenyPorts []int `yaml:"denyPorts" json:"denyPorts"` // 禁止端口
} `yaml:"tcpAll" json:"tcpAll"`
}
@@ -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
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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())
}

View File

@@ -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))
}

View File

@@ -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 + "&section=basic",
"url": "/httpdns/apps/app/settings?appId=" + appIDStr + "&section=basic",
"isActive": section == "basic",
},
{
"name": "认证与密钥",
"url": "/httpdns/apps/app/settings?appId=" + appIdStr + "&section=auth",
"url": "/httpdns/apps/app/settings?appId=" + appIDStr + "&section=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()
@@ -58,35 +71,36 @@ func (this *AppSettingsAction) RunPost(params struct {
AppId int64
AppStatus bool
PrimaryClusterId int64
BackupClusterId int64
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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -20,26 +20,33 @@ 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
}
records = append(records, record)
}
for _, record := range records {
record["domain"] = domain.GetString("name")
record["lineText"] = buildLineText(record)
record["recordValueText"] = buildRecordValueText(record)
}
}
this.Data["app"] = app
this.Data["domain"] = domain
@@ -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]
}

View File

@@ -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
}
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
for _, rule := range rules {
if rule.GetInt64("id") != params.RecordId {
continue
}
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
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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, ", ")
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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()
}

View File

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

View File

@@ -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()
}

View File

@@ -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)).

View File

@@ -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]
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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"
}

View File

@@ -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()
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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
}) {
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["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["nodeDatetime"] = "2026-02-22 12:00:00"
this.Data["nodeTimeDiff"] = 0
this.Data["shouldUpgrade"] = false
this.Data["newVersion"] = ""
this.Data["node"] = maps.Map{
"id": params.NodeId,
"name": "Mock HTTPDNS Node",
"ipAddresses": []maps.Map{{"ip": "100.200.100.200", "name": "Public IP", "canAccess": true, "isOn": true, "isUp": true}},
"cluster": maps.Map{"id": params.ClusterId, "name": "Mock Cluster", "installDir": "/opt/edge-httpdns"},
"installDir": "/opt/edge-httpdns",
"isInstalled": true,
"uniqueId": "m-1234567890",
"secret": "mock-secret-key",
"isOn": true,
"isUp": true,
"apiNodeAddrs": []string{"192.168.1.100:8001"},
"login": nil,
"status": maps.Map{
"isActive": true,
"updatedAt": 1670000000,
"hostname": "node-01.local",
"cpuUsage": 0.15,
"cpuUsageText": "15.00%",
"memUsage": 0.45,
"memUsageText": "45.00%",
"connectionCount": 100,
"buildVersion": "1.0.0",
"cpuPhysicalCount": 4,
"cpuLogicalCount": 8,
"load1m": "0.50",
"load5m": "0.60",
"load15m": "0.70",
"cacheTotalDiskSize": "10G",
"cacheTotalMemorySize": "2G",
"exePath": "/opt/edge-httpdns/bin/edge-httpdns",
"apiSuccessPercent": 100.0,
"apiAvgCostSeconds": 0.05,
},
}
this.Show()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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",
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,
"ipAddresses": []maps.Map{},
"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()
}

View File

@@ -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()
}

View File

@@ -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
// 左侧菜单
cid := strconv.FormatInt(params.ClusterId, 10)
this.Data["leftMenuItems"] = []map[string]interface{}{
{"name": "基础设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=basic", "isActive": section == "basic"},
{"name": "端口设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=tls", "isActive": section == "tls"},
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"
}
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{
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{
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 + "&section=basic", "isActive": section == "basic"},
{"name": "TLS", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=tls", "isActive": section == "tls"},
}
this.Data["cluster"] = cluster
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)
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
if err != nil {
this.Fail("端口地址解析失败:" + err.Error())
this.ErrorPage(err)
return
}
addressesJSON, _ := json.Marshal(addresses)
settings["listenAddrsJSON"] = string(addressesJSON)
tlsConfig := maps.Map{}
if rawTLS := strings.TrimSpace(cluster.GetString("tlsPolicyJSON")); len(rawTLS) > 0 {
_ = json.Unmarshal([]byte(rawTLS), &tlsConfig)
}
// 处理 SSL 配置
var originCertPem = ""
var originKeyPem = ""
var tlsMinVersion = "TLS 1.1"
var tlsCipherSuitesOn = false
var tlsOcspOn = false
var tlsClientAuthType = sslconfigs.SSLClientAuthType(0)
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 len(sslPolicy.Certs) > 0 {
cert := sslPolicy.Certs[0]
originCertPem = string(cert.CertData)
originKeyPem = string(cert.KeyData)
if err := json.Unmarshal(params.SslPolicyJSON, sslPolicy); err != nil {
this.Fail("TLS 配置格式不正确")
return
}
tlsConfig["sslPolicy"] = sslPolicy
}
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()

View File

@@ -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")
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

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