Compare commits

...

38 Commits

Author SHA1 Message Date
robin
fb8348ef47 merge 2026-03-05 19:45:50 +08:00
robin
491ade1bc3 sdk final 2026-03-05 16:59:19 +08:00
robin
a10f3f3740 ios demo 2026-03-05 02:52:45 +08:00
robin
49776c3d0a ios test 2026-03-05 02:44:43 +08:00
robin
49021c7415 Merge branch 'feat-httpdns-sdk' 2026-03-04 21:37:30 +08:00
robin
c040d7c90d SDK下载乱码问题修复 2026-03-04 19:44:21 +08:00
robin
30a1b4fa5e 文件恢复 2026-03-04 19:14:57 +08:00
robin
54e808a85d 编码修复 2026-03-04 18:41:01 +08:00
robin
c6cbf3c505 chore: remove .claude from repository 2026-03-04 18:07:36 +08:00
robin
532891fad0 feat: sync httpdns sdk/platform updates without large binaries 2026-03-04 17:59:14 +08:00
robin
853897a6f8 节点自动升级功能之前的版本 2026-03-02 23:42:55 +08:00
robin
2a76d1773d 换成单集群模式 2026-03-02 20:07:53 +08:00
robin
5d0b7c7e91 带阿里标识的版本 2026-02-28 18:55:33 +08:00
robin
150799f41d 管理端全部功能跑通 2026-02-27 10:35:22 +08:00
robin
4d275c921d 前端页面 2026-02-24 22:43:49 +08:00
robin
2eb32b9f1f 前端页面 2026-02-24 19:10:27 +08:00
robin
60dc87e0f2 前端页面 2026-02-24 11:33:44 +08:00
Robin
f3af234308 阿里sdk 2026-02-20 17:56:24 +08:00
robin
9d941f8bb9 文件补充 2026-02-20 12:30:55 +08:00
robin
9378cdc8f0 补充文件 2026-02-19 22:40:33 +08:00
robin
6d6462c65a 菜单样式更新 2026-02-16 10:32:35 +08:00
robin
7d83a9f7f4 UI update 2026-02-15 19:45:30 +08:00
robin
92ffa1ab77 登录页面改造 2026-02-15 15:16:20 +08:00
robin
2a811183c4 登录页面改造 2026-02-15 14:27:28 +08:00
robin
afbc0cde7e 登录页面改造 2026-02-15 02:11:49 +08:00
robin
468d0eeffc 登录页面改造 2026-02-15 01:56:39 +08:00
robin
dd396d31b5 样式修改 2026-02-14 23:23:35 +08:00
robin
eafac7a204 文件清理 2026-02-14 17:28:12 +08:00
robin
4259026c6e 文件清理 2026-02-14 17:12:38 +08:00
robin
f38505e210 chore: sync local changes 2026-02-14 01:43:38 +08:00
robin
e9093baffb 引入lumberjack和fluentbit自动分发 2026-02-13 22:36:17 +08:00
robin
c6da67db79 lumberjack改造前 2026-02-12 21:37:55 +08:00
robin
c28317ee07 dns clickhouse改造 2026-02-10 23:43:05 +08:00
robin
1bb8140a41 dns clickhouse改造 2026-02-10 19:30:44 +08:00
robin
4812ad5aaf 错误日志查询问题修复 2026-02-08 22:44:12 +08:00
robin
b7388d83b0 日常查询由mysql改为clickhouse 2026-02-08 02:00:51 +08:00
robin
bc223fd1aa 主分支代码 2026-02-07 20:30:31 +08:00
unknown
3b042d1dad 1.4.5.2 2026-02-04 20:27:13 +08:00
1035 changed files with 99046 additions and 1182 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
*.sh text eol=lf
*.bash text eol=lf
*.zsh text eol=lf

6
.gitignore vendored
View File

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

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

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

View File

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

View File

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

@@ -12,15 +12,15 @@ import (
)
const (
envHost = "CLICKHOUSE_HOST"
envPort = "CLICKHOUSE_PORT"
envUser = "CLICKHOUSE_USER"
envPassword = "CLICKHOUSE_PASSWORD"
envDatabase = "CLICKHOUSE_DATABASE"
envScheme = "CLICKHOUSE_SCHEME"
defaultPort = 8443
defaultDB = "default"
defaultScheme = "https"
envHost = "CLICKHOUSE_HOST"
envPort = "CLICKHOUSE_PORT"
envUser = "CLICKHOUSE_USER"
envPassword = "CLICKHOUSE_PASSWORD"
envDatabase = "CLICKHOUSE_DATABASE"
envScheme = "CLICKHOUSE_SCHEME"
defaultPort = 8443
defaultDB = "default"
defaultScheme = "https"
)
var (

View File

@@ -0,0 +1,294 @@
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
AppIds []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)+"'")
} else if len(f.AppIds) > 0 {
validAppIds := make([]string, 0, len(f.AppIds))
for _, appID := range f.AppIds {
appID = strings.TrimSpace(appID)
if len(appID) == 0 {
continue
}
validAppIds = append(validAppIds, "'"+escapeString(appID)+"'")
}
if len(validAppIds) == 0 {
conditions = append(conditions, "1 = 0")
} else {
conditions = append(conditions, "app_id IN ("+strings.Join(validAppIds, ",")+")")
}
}
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

@@ -351,8 +351,8 @@ func RowToPB(r *LogsIngestRow) *pb.HTTPAccessLog {
RemoteAddr: r.IP,
RequestMethod: r.Method,
RequestPath: r.Path,
RequestURI: r.Path, // 前端使用 requestURI 显示完整路径
Scheme: "http", // 默认 http日志中未存储实际值
RequestURI: r.Path, // 前端使用 requestURI 显示完整路径
Scheme: "http", // 默认 http日志中未存储实际值
Proto: "HTTP/1.1", // 默认值,日志中未存储实际值
Status: int32(r.Status),
RequestLength: int64(r.BytesIn),

View File

@@ -1,7 +1,7 @@
package teaconst
const (
Version = "1.4.7" //1.3.9
Version = "1.4.9" //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.9" //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.9" //1.3.8.2
UserNodeVersion = "1.4.9" //1.3.8.2
ReportNodeVersion = "0.1.5"
DefaultMaxNodes int32 = 50

View File

@@ -0,0 +1,134 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"strings"
)
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 {
return this.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, nil, domain, status, keyword)
}
func (this *HTTPDNSAccessLogDAO) BuildListQueryWithAppIds(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, appIds []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(appIds) > 0 {
validAppIds := make([]string, 0, len(appIds))
for _, value := range appIds {
value = strings.TrimSpace(value)
if len(value) == 0 {
continue
}
validAppIds = append(validAppIds, value)
}
if len(validAppIds) == 0 {
query = query.Where("1 = 0")
} else {
query = query.Attr("appId", validAppIds)
}
}
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.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, nil, 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.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, nil, domain, status, keyword).
Offset(offset).
Limit(size).
Slice(&result).
FindAll()
return
}
func (this *HTTPDNSAccessLogDAO) CountLogsWithAppIds(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, appIds []string, domain string, status string, keyword string) (int64, error) {
return this.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, appIds, domain, status, keyword).Count()
}
func (this *HTTPDNSAccessLogDAO) ListLogsWithAppIds(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, appIds []string, domain string, status string, keyword string, offset int64, size int64) (result []*HTTPDNSAccessLog, err error) {
_, err = this.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, appIds, 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,239 @@
package models
import (
"encoding/json"
_ "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, clusterIdsJSON []byte, isOn bool, userId int64) (int64, error) {
var op = NewHTTPDNSAppOperator()
op.Name = name
op.AppId = appId
if len(clusterIdsJSON) > 0 {
op.ClusterIdsJSON = string(clusterIdsJSON)
} else {
op.ClusterIdsJSON = "[]"
}
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, clusterIdsJSON []byte, isOn bool, userId int64) error {
var op = NewHTTPDNSAppOperator()
op.Id = appDbId
op.Name = name
if len(clusterIdsJSON) > 0 {
op.ClusterIdsJSON = string(clusterIdsJSON)
} else {
op.ClusterIdsJSON = "[]"
}
op.IsOn = isOn
op.UserId = userId
op.UpdatedAt = time.Now().Unix()
return this.Save(tx, op)
}
// ReadAppClusterIds reads cluster IDs from ClusterIdsJSON.
func (this *HTTPDNSAppDAO) ReadAppClusterIds(app *HTTPDNSApp) []int64 {
if app == nil {
return nil
}
if len(app.ClusterIdsJSON) > 0 {
var ids []int64
if err := json.Unmarshal([]byte(app.ClusterIdsJSON), &ids); err == nil && len(ids) > 0 {
return ids
}
}
return nil
}
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) FindEnabledAppWithUser(tx *dbs.Tx, appDbId int64, userId int64) (*HTTPDNSApp, error) {
one, err := this.Query(tx).
Pk(appDbId).
State(HTTPDNSAppStateEnabled).
Attr("userId", userId).
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) FindEnabledAppWithAppIdAndUser(tx *dbs.Tx, appId string, userId int64) (*HTTPDNSApp, error) {
one, err := this.Query(tx).
State(HTTPDNSAppStateEnabled).
Attr("appId", appId).
Attr("userId", userId).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSApp), nil
}
func (this *HTTPDNSAppDAO) FindLatestEnabledAppWithNameAndUser(tx *dbs.Tx, name string, userId int64) (*HTTPDNSApp, error) {
one, err := this.Query(tx).
State(HTTPDNSAppStateEnabled).
Attr("name", name).
Attr("userId", userId).
DescPk().
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) ListEnabledAppsWithUser(tx *dbs.Tx, userId int64, offset int64, size int64, keyword string) (result []*HTTPDNSApp, err error) {
query := this.Query(tx).
State(HTTPDNSAppStateEnabled).
Attr("userId", userId).
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) CountEnabledAppsWithUser(tx *dbs.Tx, userId int64, keyword string) (int64, error) {
query := this.Query(tx).State(HTTPDNSAppStateEnabled).Attr("userId", userId)
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
}
func (this *HTTPDNSAppDAO) FindAllEnabledAppsWithUser(tx *dbs.Tx, userId int64) (result []*HTTPDNSApp, err error) {
_, err = this.Query(tx).
State(HTTPDNSAppStateEnabled).
Attr("userId", userId).
AscPk().
Slice(&result).
FindAll()
return
}
func (this *HTTPDNSAppDAO) ListEnabledAppIdsWithUser(tx *dbs.Tx, userId int64) (result []string, err error) {
apps, err := this.FindAllEnabledAppsWithUser(tx, userId)
if err != nil {
return nil, err
}
result = make([]string, 0, len(apps))
for _, app := range apps {
if app == nil || len(app.AppId) == 0 {
continue
}
result = append(result, app.AppId)
}
return
}

View File

@@ -0,0 +1,34 @@
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
ClusterIdsJSON string `field:"clusterIdsJSON"` // cluster ids json
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
ClusterIdsJSON any // cluster ids json
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,146 @@
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())
// 兼容历史数据:如果已存在(可能是停用状态)则直接恢复并更新,避免 UNIQUE(appId) 冲突
old, err := this.Query(tx).
Attr("appId", appDbId).
Find()
if err != nil {
return "", 0, err
}
if old != nil {
oldSecret := old.(*HTTPDNSAppSecret)
_, err = this.Query(tx).
Pk(oldSecret.Id).
Set("signEnabled", signEnabled).
Set("signSecret", signSecret).
Set("signUpdatedAt", now).
Set("updatedAt", now).
Set("state", HTTPDNSAppSecretStateEnabled).
Update()
return signSecret, now, err
}
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,191 @@
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, autoRemoteStart bool, accessLogIsOn bool, timeZone string) (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.AutoRemoteStart = autoRemoteStart
op.AccessLogIsOn = accessLogIsOn
op.TimeZone = timeZone
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, autoRemoteStart bool, accessLogIsOn bool, timeZone string) 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.AutoRemoteStart = autoRemoteStart
op.AccessLogIsOn = accessLogIsOn
op.TimeZone = timeZone
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) FindDefaultPrimaryClusterId(tx *dbs.Tx) (int64, error) {
col, err := this.Query(tx).
State(HTTPDNSClusterStateEnabled).
Attr("isDefault", true).
Result("id").
AscPk().
FindCol(nil)
if err != nil {
return 0, err
}
if col == nil {
return 0, nil
}
return types.Int64(col), nil
}
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,44 @@
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策略
AutoRemoteStart bool `field:"autoRemoteStart"` // 自动远程启动
AccessLogIsOn bool `field:"accessLogIsOn"` // 访问日志是否开启
TimeZone string `field:"timeZone"` // 时区
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策略
AutoRemoteStart any // 自动远程启动
AccessLogIsOn any // 访问日志是否开启
TimeZone any // 时区
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,327 @@
package models
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"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 鏌ヨ鑺傜偣鎵€灞為泦缇D
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
}
// FindAllInactiveNodesWithClusterId 取得一个集群离线的HTTPDNS节点
func (this *HTTPDNSNodeDAO) FindAllInactiveNodesWithClusterId(tx *dbs.Tx, clusterId int64) (result []*HTTPDNSNode, err error) {
_, err = this.Query(tx).
State(HTTPDNSNodeStateEnabled).
Attr("clusterId", clusterId).
Attr("isOn", true). // 只监控启用的节点
Attr("isInstalled", true). // 只监控已经安装的节点
Attr("isActive", false). // 当前处于离线状态
Result("id", "name").
Slice(&result).
FindAll()
return
}
// UpdateNodeStatus 更新节点状态
func (this *HTTPDNSNodeDAO) UpdateNodeStatus(tx *dbs.Tx, nodeId int64, isUp bool, isInstalled bool, isActive bool, statusJSON []byte, installStatusJSON []byte) error {
var op = NewHTTPDNSNodeOperator()
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
}
// CountAllLowerVersionNodesWithClusterId 璁$畻鍗曚釜闆嗙兢涓墍鏈変綆浜庢煇涓増鏈殑鑺傜偣鏁伴噺
func (this *HTTPDNSNodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (int64, error) {
return this.Query(tx).
State(HTTPDNSNodeStateEnabled).
Attr("clusterId", clusterId).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").
Where("JSON_EXTRACT(status, '$.arch')=:arch").
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
Param("os", os).
Param("arch", arch).
Param("version", utils.VersionToLong(version)).
Count()
}
// FindAllLowerVersionNodesWithClusterId 鏌ユ壘鍗曚釜闆嗙兢涓墍鏈変綆浜庢煇涓増鏈殑鑺傜偣
func (this *HTTPDNSNodeDAO) FindAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (result []*HTTPDNSNode, err error) {
_, err = this.Query(tx).
State(HTTPDNSNodeStateEnabled).
Attr("clusterId", clusterId).
Attr("isOn", true).
Attr("isUp", true).
Attr("isActive", true).
Where("status IS NOT NULL").
Where("JSON_EXTRACT(status, '$.os')=:os").
Where("JSON_EXTRACT(status, '$.arch')=:arch").
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
Param("os", os).
Param("arch", arch).
Param("version", utils.VersionToLong(version)).
DescPk().
Slice(&result).
FindAll()
return
}
// UpdateNodeIsInstalled 鏇存柊鑺傜偣瀹夎鐘舵€佷綅
func (this *HTTPDNSNodeDAO) UpdateNodeIsInstalled(tx *dbs.Tx, nodeId int64, isInstalled bool) error {
_, err := this.Query(tx).
Pk(nodeId).
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

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

View File

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

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

View File

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

View File

@@ -246,7 +246,7 @@ func (this *UserDAO) CreateUser(tx *dbs.Tx, username string,
}
// UpdateUser 修改用户
func (this *UserDAO) UpdateUser(tx *dbs.Tx, userId int64, username string, password string, fullname string, mobile string, tel string, email string, remark string, isOn bool, nodeClusterId int64, bandwidthAlgo systemconfigs.BandwidthAlgo) error {
func (this *UserDAO) UpdateUser(tx *dbs.Tx, userId int64, username string, password string, fullname string, mobile string, tel string, email string, remark string, isOn bool, nodeClusterId int64, bandwidthAlgo systemconfigs.BandwidthAlgo, httpdnsClusterIdsJSON []byte) error {
if userId <= 0 {
return errors.New("invalid userId")
}
@@ -265,6 +265,11 @@ func (this *UserDAO) UpdateUser(tx *dbs.Tx, userId int64, username string, passw
op.ClusterId = nodeClusterId
op.BandwidthAlgo = bandwidthAlgo
op.IsOn = isOn
if len(httpdnsClusterIdsJSON) > 0 {
op.HttpdnsClusterIds = string(httpdnsClusterIdsJSON)
} else {
op.HttpdnsClusterIds = "[]"
}
err := this.Save(tx, op)
if err != nil {
return err
@@ -466,6 +471,21 @@ func (this *UserDAO) FindUserClusterId(tx *dbs.Tx, userId int64) (int64, error)
FindInt64Col(0)
}
// UpdateUserHttpdnsClusterIds 更新用户的HTTPDNS关联集群ID列表
func (this *UserDAO) UpdateUserHttpdnsClusterIds(tx *dbs.Tx, userId int64, httpdnsClusterIdsJSON []byte) error {
if userId <= 0 {
return errors.New("invalid userId")
}
if len(httpdnsClusterIdsJSON) == 0 {
httpdnsClusterIdsJSON = []byte("[]")
}
_, err := this.Query(tx).
Pk(userId).
Set("httpdnsClusterIds", httpdnsClusterIdsJSON).
Update()
return err
}
// UpdateUserFeatures 更新单个用户Features
func (this *UserDAO) UpdateUserFeatures(tx *dbs.Tx, userId int64, featuresJSON []byte) error {
if userId <= 0 {

View File

@@ -37,6 +37,7 @@ const (
UserField_BandwidthAlgo dbs.FieldName = "bandwidthAlgo" // 带宽算法
UserField_BandwidthModifier dbs.FieldName = "bandwidthModifier" // 带宽修正值
UserField_Lang dbs.FieldName = "lang" // 语言代号
UserField_HttpdnsClusterIds dbs.FieldName = "httpdnsClusterIds" // HTTPDNS关联集群ID列表
)
// User 用户
@@ -75,6 +76,7 @@ type User struct {
BandwidthAlgo string `field:"bandwidthAlgo"` // 带宽算法
BandwidthModifier float64 `field:"bandwidthModifier"` // 带宽修正值
Lang string `field:"lang"` // 语言代号
HttpdnsClusterIds dbs.JSON `field:"httpdnsClusterIds"` // HTTPDNS关联集群ID列表
}
type UserOperator struct {
@@ -112,6 +114,7 @@ type UserOperator struct {
BandwidthAlgo any // 带宽算法
BandwidthModifier any // 带宽修正值
Lang any // 语言代号
HttpdnsClusterIds any // HTTPDNS关联集群ID列表
}
func NewUserOperator() *UserOperator {

View File

@@ -15,8 +15,9 @@ var SharedDeployManager = NewDeployManager()
type DeployManager struct {
dir string
nodeFiles []*DeployFile
nsNodeFiles []*DeployFile
nodeFiles []*DeployFile
nsNodeFiles []*DeployFile
httpdnsNodeFiles []*DeployFile
locker sync.Mutex
}
@@ -28,6 +29,7 @@ func NewDeployManager() *DeployManager {
}
manager.LoadNodeFiles()
manager.LoadNSNodeFiles()
manager.LoadHTTPDNSNodeFiles()
return manager
}
@@ -141,6 +143,61 @@ func (this *DeployManager) FindNSNodeFile(os string, arch string) *DeployFile {
return nil
}
// LoadHTTPDNSNodeFiles 加载所有HTTPDNS节点安装文件
func (this *DeployManager) LoadHTTPDNSNodeFiles() []*DeployFile {
this.locker.Lock()
defer this.locker.Unlock()
if len(this.httpdnsNodeFiles) > 0 {
return this.httpdnsNodeFiles
}
var keyMap = map[string]*DeployFile{} // key => File
var reg = regexp.MustCompile(`^edge-httpdns-(\w+)-(\w+)-v([0-9.]+)\.zip$`)
for _, file := range files.NewFile(this.dir).List() {
var name = file.Name()
if !reg.MatchString(name) {
continue
}
var matches = reg.FindStringSubmatch(name)
var osName = matches[1]
var arch = matches[2]
var version = matches[3]
var key = osName + "_" + arch
oldFile, ok := keyMap[key]
if ok && stringutil.VersionCompare(oldFile.Version, version) > 0 {
continue
}
keyMap[key] = &DeployFile{
OS: osName,
Arch: arch,
Version: version,
Path: file.Path(),
}
}
var result = []*DeployFile{}
for _, v := range keyMap {
result = append(result, v)
}
this.httpdnsNodeFiles = result
return result
}
// FindHTTPDNSNodeFile 查找特定平台的HTTPDNS节点安装文件
func (this *DeployManager) FindHTTPDNSNodeFile(os string, arch string) *DeployFile {
for _, file := range this.LoadHTTPDNSNodeFiles() {
if file.OS == os && file.Arch == arch {
return file
}
}
return nil
}
// Reload 重置缓存
func (this *DeployManager) Reload() {
this.locker.Lock()
@@ -148,4 +205,5 @@ func (this *DeployManager) Reload() {
this.nodeFiles = nil
this.nsNodeFiles = nil
this.httpdnsNodeFiles = nil
}

View File

@@ -33,11 +33,13 @@ const (
fluentBitServiceName = "fluent-bit"
fluentBitDefaultBinPath = "/opt/fluent-bit/bin/fluent-bit"
fluentBitLocalPackagesRoot = "packages"
fluentBitHTTPPathPattern = "/var/log/edge/edge-node/*.log"
fluentBitDNSPathPattern = "/var/log/edge/edge-dns/*.log"
fluentBitManagedMarker = "managed-by-edgeapi"
fluentBitRoleNode = "node"
fluentBitRoleDNS = "dns"
fluentBitHTTPPathPattern = "/var/log/edge/edge-node/*.log"
fluentBitDNSPathPattern = "/var/log/edge/edge-dns/*.log"
fluentBitHTTPDNSPathPattern = "/var/log/edge/edge-httpdns/*.log"
fluentBitManagedMarker = "managed-by-edgeapi"
fluentBitRoleNode = "node"
fluentBitRoleDNS = "dns"
fluentBitRoleHTTPDNS = "httpdns"
)
var errFluentBitLocalPackageNotFound = errors.New("fluent-bit local package not found")
@@ -57,10 +59,11 @@ type fluentBitManagedMeta struct {
}
type fluentBitDesiredConfig struct {
Roles []string
ClickHouse *systemconfigs.ClickHouseSetting
HTTPPathPattern string
DNSPathPattern string
Roles []string
ClickHouse *systemconfigs.ClickHouseSetting
HTTPPathPattern string
DNSPathPattern string
HTTPDNSPathPattern string
}
// SetupFluentBit 安装并托管 Fluent Bit 配置(离线包 + 平台渲染配置)。
@@ -343,6 +346,8 @@ func mapNodeRole(role nodeconfigs.NodeRole) (string, error) {
return fluentBitRoleNode, nil
case nodeconfigs.NodeRoleDNS:
return fluentBitRoleDNS, nil
case nodeconfigs.NodeRoleHTTPDNS:
return fluentBitRoleHTTPDNS, nil
default:
return "", fmt.Errorf("unsupported fluent-bit role '%s'", role)
}
@@ -352,7 +357,7 @@ func normalizeRoles(rawRoles []string) []string {
roleSet := map[string]struct{}{}
for _, role := range rawRoles {
role = strings.ToLower(strings.TrimSpace(role))
if role != fluentBitRoleNode && role != fluentBitRoleDNS {
if role != fluentBitRoleNode && role != fluentBitRoleDNS && role != fluentBitRoleHTTPDNS {
continue
}
roleSet[role] = struct{}{}
@@ -418,6 +423,7 @@ func (this *BaseInstaller) buildDesiredFluentBitConfig(roles []string) (*fluentB
httpPathPattern := fluentBitHTTPPathPattern
dnsPathPattern := fluentBitDNSPathPattern
httpdnsPathPattern := fluentBitHTTPDNSPathPattern
publicPolicyPath, err := this.readPublicAccessLogPolicyPath()
if err != nil {
return nil, err
@@ -427,13 +433,15 @@ func (this *BaseInstaller) buildDesiredFluentBitConfig(roles []string) (*fluentB
pattern := strings.TrimRight(policyDir, "/") + "/*.log"
httpPathPattern = pattern
dnsPathPattern = pattern
httpdnsPathPattern = pattern
}
return &fluentBitDesiredConfig{
Roles: normalizeRoles(roles),
ClickHouse: ch,
HTTPPathPattern: httpPathPattern,
DNSPathPattern: dnsPathPattern,
Roles: normalizeRoles(roles),
ClickHouse: ch,
HTTPPathPattern: httpPathPattern,
DNSPathPattern: dnsPathPattern,
HTTPDNSPathPattern: httpdnsPathPattern,
}, nil
}
@@ -554,6 +562,7 @@ func renderManagedConfig(desired *fluentBitDesiredConfig) (string, error) {
insertHTTP := url.QueryEscape(fmt.Sprintf("INSERT INTO %s.logs_ingest FORMAT JSONEachRow", desired.ClickHouse.Database))
insertDNS := url.QueryEscape(fmt.Sprintf("INSERT INTO %s.dns_logs_ingest FORMAT JSONEachRow", desired.ClickHouse.Database))
insertHTTPDNS := url.QueryEscape(fmt.Sprintf("INSERT INTO %s.httpdns_access_logs_ingest FORMAT JSONEachRow", desired.ClickHouse.Database))
lines := []string{
"# " + fluentBitManagedMarker,
@@ -602,6 +611,23 @@ func renderManagedConfig(desired *fluentBitDesiredConfig) (string, error) {
)
}
if hasRole(desired.Roles, fluentBitRoleHTTPDNS) {
lines = append(lines,
"[INPUT]",
" Name tail",
" Path "+desired.HTTPDNSPathPattern,
" Tag app.httpdns.logs",
" Parser json",
" Refresh_Interval 2",
" Read_from_Head false",
" DB /var/lib/fluent-bit/httpdns-logs.db",
" storage.type filesystem",
" Mem_Buf_Limit 256MB",
" Skip_Long_Lines On",
"",
)
}
if hasRole(desired.Roles, fluentBitRoleNode) {
lines = append(lines,
"[OUTPUT]",
@@ -664,6 +690,37 @@ func renderManagedConfig(desired *fluentBitDesiredConfig) (string, error) {
lines = append(lines, "")
}
if hasRole(desired.Roles, fluentBitRoleHTTPDNS) {
lines = append(lines,
"[OUTPUT]",
" Name http",
" Match app.httpdns.logs",
" Host "+desired.ClickHouse.Host,
" Port "+strconv.Itoa(desired.ClickHouse.Port),
" URI /?query="+insertHTTPDNS,
" Format json_lines",
" http_user ${CH_USER}",
" http_passwd ${CH_PASSWORD}",
" json_date_key timestamp",
" json_date_format epoch",
" workers 2",
" net.keepalive On",
" Retry_Limit False",
)
if useTLS {
lines = append(lines, " tls On")
if desired.ClickHouse.TLSSkipVerify {
lines = append(lines, " tls.verify Off")
} else {
lines = append(lines, " tls.verify On")
}
if strings.TrimSpace(desired.ClickHouse.TLSServerName) != "" {
lines = append(lines, " tls.vhost "+strings.TrimSpace(desired.ClickHouse.TLSServerName))
}
}
lines = append(lines, "")
}
return strings.Join(lines, "\n"), nil
}

View File

@@ -0,0 +1,236 @@
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))
}
listenAddr := strings.TrimSpace(nodeParams.HTTPDNSListenAddr)
if len(listenAddr) == 0 {
listenAddr = ":443"
}
configData := []byte(`rpc.endpoints: [ ${endpoints} ]
nodeId: "${nodeId}"
secret: "${nodeSecret}"
https.listenAddr: "${listenAddr}"
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("${listenAddr}"), []byte(listenAddr))
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

@@ -6,10 +6,13 @@ import (
)
type NodeParams struct {
Endpoints []string
NodeId string
Secret string
IsUpgrading bool // 是否为升级
Endpoints []string
NodeId string
Secret string
TLSCertData []byte
TLSKeyData []byte
HTTPDNSListenAddr string
IsUpgrading bool // 是否为升级
}
func (this *NodeParams) Validate() error {

View File

@@ -0,0 +1,416 @@
package installers
import (
"encoding/json"
"errors"
"fmt"
"net"
"strconv"
"strings"
"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/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/logs"
)
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, installStatus)
if err != nil {
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
}
httpdnsListenAddr, err := q.resolveClusterTLSListenAddr(cluster)
if err != nil {
installStatus.ErrorCode = "INVALID_TLS_LISTEN"
return err
}
params := &NodeParams{
Endpoints: apiEndpoints,
NodeId: node.UniqueId,
Secret: node.Secret,
TLSCertData: tlsCertData,
TLSKeyData: tlsKeyData,
HTTPDNSListenAddr: httpdnsListenAddr,
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)
}
// StartNode 启动HTTPDNS节点
func (q *HTTPDNSNodeQueue) StartNode(nodeId int64) error {
node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(nil, nodeId)
if err != nil {
return err
}
if node == nil {
return errors.New("can not find node, ID '" + numberutils.FormatInt64(nodeId) + "'")
}
// 登录信息
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleHTTPDNS, nodeId)
if err != nil {
return err
}
if login == nil {
return newGrantError("can not find node login information")
}
loginParams, err := login.DecodeSSHParams()
if err != nil {
return newGrantError(err.Error())
}
if len(strings.TrimSpace(loginParams.Host)) == 0 {
return newGrantError("ssh host should not be empty")
}
if loginParams.Port <= 0 {
loginParams.Port = 22
}
if loginParams.GrantId <= 0 {
return newGrantError("can not find node grant")
}
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
if err != nil {
return err
}
if grant == nil {
return newGrantError("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
}
installer := &HTTPDNSNodeInstaller{}
err = installer.Login(&Credentials{
Host: strings.TrimSpace(loginParams.Host),
Port: loginParams.Port,
Username: grant.Username,
Password: grant.Password,
PrivateKey: grant.PrivateKey,
Passphrase: grant.Passphrase,
Method: grant.Method,
Sudo: grant.Su == 1,
})
if err != nil {
return err
}
defer func() {
_ = installer.Close()
}()
installDir := strings.TrimSpace(node.InstallDir)
if len(installDir) == 0 {
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(nil, int64(node.ClusterId))
if err != nil {
return err
}
if cluster == nil {
return errors.New("can not find cluster, ID '" + numberutils.FormatInt64(int64(node.ClusterId)) + "'")
}
installDir = strings.TrimSpace(cluster.InstallDir)
if len(installDir) == 0 {
installDir = installer.client.UserHome() + "/edge-httpdns"
}
}
_, appDir := resolveHTTPDNSInstallPaths(installDir)
exeFile := appDir + "/bin/edge-httpdns"
_, err = installer.client.Stat(exeFile)
if err != nil {
return errors.New("httpdns node is not installed correctly, can not find executable file: " + exeFile)
}
// 先尝试 systemd 拉起
_, _, _ = installer.client.Exec("/usr/bin/systemctl start edge-httpdns")
_, stderr, err := installer.client.Exec(exeFile + " start")
if err != nil {
return fmt.Errorf("start failed: %w", err)
}
if len(strings.TrimSpace(stderr)) > 0 {
return errors.New("start failed: " + strings.TrimSpace(stderr))
}
return nil
}
func (q *HTTPDNSNodeQueue) resolveClusterTLSCertPair(cluster *models.HTTPDNSCluster) ([]byte, []byte, error) {
if cluster == nil {
return nil, nil, errors.New("cluster not found")
}
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) resolveClusterTLSListenAddr(cluster *models.HTTPDNSCluster) (string, error) {
const defaultListenAddr = ":443"
if cluster == nil || len(cluster.TLSPolicy) == 0 {
return defaultListenAddr, nil
}
tlsConfig, err := serverconfigs.NewTLSProtocolConfigFromJSON(cluster.TLSPolicy)
if err != nil {
return "", fmt.Errorf("decode cluster tls listen failed: %w", err)
}
for _, listen := range tlsConfig.Listen {
if listen == nil {
continue
}
if err := listen.Init(); err != nil {
return "", fmt.Errorf("invalid cluster tls listen address '%s': %w", listen.PortRange, err)
}
if listen.MinPort <= 0 {
continue
}
host := strings.TrimSpace(listen.Host)
return net.JoinHostPort(host, strconv.Itoa(listen.MinPort)), nil
}
return defaultListenAddr, nil
}
func (q *HTTPDNSNodeQueue) parseSSHInfo(node *models.HTTPDNSNode, installStatus *models.NodeInstallStatus) (string, int, int64, error) {
if node == nil {
return "", 0, 0, errors.New("node should not be nil")
}
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleHTTPDNS, int64(node.Id))
if err != nil {
return "", 0, 0, err
}
if login == nil {
installStatus.ErrorCode = "EMPTY_SSH"
return "", 0, 0, errors.New("ssh login not found for node '" + numberutils.FormatInt64(int64(node.Id)) + "'")
}
sshParams, err := login.DecodeSSHParams()
if err != nil {
installStatus.ErrorCode = "EMPTY_SSH"
return "", 0, 0, err
}
if len(sshParams.Host) == 0 {
installStatus.ErrorCode = "EMPTY_SSH_HOST"
return "", 0, 0, errors.New("ssh host should not be empty")
}
if sshParams.Port <= 0 {
sshParams.Port = 22
}
if sshParams.GrantId <= 0 {
installStatus.ErrorCode = "EMPTY_GRANT"
return "", 0, 0, errors.New("grant id should not be empty")
}
return sshParams.Host, sshParams.Port, sshParams.GrantId, nil
}

View File

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

View File

@@ -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,220 @@
package httpdns
import (
"encoding/json"
"log"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/dbs"
)
func toPBCluster(cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
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),
AutoRemoteStart: cluster.AutoRemoteStart,
AccessLogIsOn: cluster.AccessLogIsOn,
TimeZone: cluster.TimeZone,
}
}
// toPBClusterWithResolvedCerts 转换集群并解析证书引用为实际 PEM 数据
// 供节点调用的 RPC 使用,确保节点能拿到完整的证书内容
func toPBClusterWithResolvedCerts(tx *dbs.Tx, cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
pbCluster := toPBCluster(cluster)
if pbCluster == nil {
return nil
}
resolved := resolveTLSPolicyCerts(tx, cluster.TLSPolicy)
if resolved != nil {
pbCluster.TlsPolicyJSON = resolved
}
return pbCluster
}
// resolveTLSPolicyCerts 将 tlsPolicyJSON 中的 certRefs 解析为带实际 PEM 数据的 certs
func resolveTLSPolicyCerts(tx *dbs.Tx, tlsPolicyJSON []byte) []byte {
if len(tlsPolicyJSON) == 0 {
return nil
}
// 解析外层结构: {"listen": [...], "sslPolicy": {...}}
var tlsConfig map[string]json.RawMessage
if err := json.Unmarshal(tlsPolicyJSON, &tlsConfig); err != nil {
return nil
}
sslPolicyData, ok := tlsConfig["sslPolicy"]
if !ok || len(sslPolicyData) == 0 {
return nil
}
var sslPolicy sslconfigs.SSLPolicy
if err := json.Unmarshal(sslPolicyData, &sslPolicy); err != nil {
return nil
}
// 检查 certs 是否已经有实际数据
for _, cert := range sslPolicy.Certs {
if cert != nil && len(cert.CertData) > 128 && len(cert.KeyData) > 128 {
return nil // 已有完整 PEM 数据,无需处理
}
}
// 从 certRefs 解析实际证书数据
if len(sslPolicy.CertRefs) == 0 {
return nil
}
var resolvedCerts []*sslconfigs.SSLCertConfig
for _, ref := range sslPolicy.CertRefs {
if ref == nil || ref.CertId <= 0 {
continue
}
certConfig, err := models.SharedSSLCertDAO.ComposeCertConfig(tx, ref.CertId, false, nil, nil)
if err != nil {
log.Println("[HTTPDNS]resolve cert", ref.CertId, "failed:", err.Error())
continue
}
if certConfig == nil || len(certConfig.CertData) == 0 || len(certConfig.KeyData) == 0 {
continue
}
resolvedCerts = append(resolvedCerts, certConfig)
}
if len(resolvedCerts) == 0 {
return nil
}
// 把解析后的证书写回 sslPolicy.Certs
sslPolicy.Certs = resolvedCerts
newPolicyData, err := json.Marshal(&sslPolicy)
if err != nil {
return nil
}
tlsConfig["sslPolicy"] = newPolicyData
result, err := json.Marshal(tlsConfig)
if err != nil {
return nil
}
return result
}
func toPBNode(node *models.HTTPDNSNode) *pb.HTTPDNSNode {
if node == nil {
return nil
}
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)
}
// 构建 clusterIdsJSON
clusterIds := models.SharedHTTPDNSAppDAO.ReadAppClusterIds(app)
clusterIdsJSON, _ := json.Marshal(clusterIds)
return &pb.HTTPDNSApp{
Id: int64(app.Id),
Name: app.Name,
AppId: app.AppId,
IsOn: app.IsOn,
SniMode: app.SNIMode,
SignEnabled: signEnabled,
SignSecret: signSecret,
SignUpdatedAt: signUpdatedAt,
CreatedAt: int64(app.CreatedAt),
UpdatedAt: int64(app.UpdatedAt),
UserId: int64(app.UserId),
ClusterIdsJSON: clusterIdsJSON,
}
}
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,287 @@
package httpdns
import (
"context"
"log"
"strconv"
"strings"
"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) {
_, userId, err := s.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
allowedAppIds := []string(nil)
if userId > 0 {
if len(strings.TrimSpace(req.GetAppId())) > 0 {
app, err := ensureAppAccessByAppId(s.NullTx(), req.GetAppId(), userId)
if err != nil {
return nil, err
}
if app == nil {
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: []*pb.HTTPDNSAccessLog{},
Total: 0,
}, nil
}
} else {
allowedAppIds, err = models.SharedHTTPDNSAppDAO.ListEnabledAppIdsWithUser(s.NullTx(), userId)
if err != nil {
return nil, err
}
if len(allowedAppIds) == 0 {
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: []*pb.HTTPDNSAccessLog{},
Total: 0,
}, nil
}
}
}
store := clickhouse.NewHTTPDNSAccessLogsStore()
canReadFromClickHouse := s.shouldReadHTTPDNSAccessLogsFromClickHouse() && store.Client().IsConfigured()
canReadFromMySQL := s.shouldReadHTTPDNSAccessLogsFromMySQL()
if canReadFromClickHouse {
resp, listErr := s.listFromClickHouse(ctx, store, req, allowedAppIds)
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.CountLogsWithAppIds(s.NullTx(), req.GetDay(), req.GetClusterId(), req.GetNodeId(), req.GetAppId(), allowedAppIds, req.GetDomain(), req.GetStatus(), req.GetKeyword())
if err != nil {
return nil, err
}
logs, err := models.SharedHTTPDNSAccessLogDAO.ListLogsWithAppIds(s.NullTx(), req.GetDay(), req.GetClusterId(), req.GetNodeId(), req.GetAppId(), allowedAppIds, 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, allowedAppIds []string) (*pb.ListHTTPDNSAccessLogsResponse, error) {
filter := clickhouse.HTTPDNSAccessLogListFilter{
Day: req.GetDay(),
ClusterId: req.GetClusterId(),
NodeId: req.GetNodeId(),
AppId: req.GetAppId(),
AppIds: allowedAppIds,
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,370 @@
package httpdns
import (
"context"
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/iwind/TeaGo/dbs"
"strings"
"time"
"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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
if userId > 0 {
req.UserId = userId
}
appName := strings.TrimSpace(req.Name)
appId := strings.TrimSpace(req.AppId)
if len(appName) == 0 || len(appId) == 0 {
return nil, errors.New("required 'name' and 'appId'")
}
var appDbId int64
now := time.Now().Unix()
err = this.RunTx(func(tx *dbs.Tx) error {
// 用户端防重复提交:短时间内同用户同应用名仅创建一次。
if req.UserId > 0 {
latest, err := models.SharedHTTPDNSAppDAO.FindLatestEnabledAppWithNameAndUser(tx, appName, req.UserId)
if err != nil {
return err
}
if latest != nil && int64(latest.CreatedAt) >= now-5 {
appDbId = int64(latest.Id)
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(tx, appDbId)
if err != nil {
return err
}
if secret == nil {
_, _, err = models.SharedHTTPDNSAppSecretDAO.InitAppSecret(tx, appDbId, req.SignEnabled)
if err != nil {
return err
}
}
return nil
}
}
exists, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(tx, appId)
if err != nil {
return err
}
if exists != nil {
return errors.New("appId already exists")
}
// 使用 clusterIdsJSON若为空则从用户关联集群获取
clusterIdsJSON := req.ClusterIdsJSON
if len(clusterIdsJSON) == 0 || string(clusterIdsJSON) == "[]" || string(clusterIdsJSON) == "null" {
// 读取用户关联的 HTTPDNS 集群
if req.UserId > 0 {
user, userErr := models.SharedUserDAO.FindEnabledUser(tx, req.UserId, nil)
if userErr != nil {
return userErr
}
if user != nil && len(user.HttpdnsClusterIds) > 0 {
var userClusterIds []int64
if json.Unmarshal([]byte(user.HttpdnsClusterIds), &userClusterIds) == nil && len(userClusterIds) > 0 {
clusterIdsJSON, _ = json.Marshal(userClusterIds)
}
}
}
}
// 如果仍然没有集群,则不允许创建
if len(clusterIdsJSON) == 0 || string(clusterIdsJSON) == "[]" || string(clusterIdsJSON) == "null" {
return errors.New("用户尚未分配 HTTPDNS 集群,无法创建应用")
}
appDbId, err = models.SharedHTTPDNSAppDAO.CreateApp(tx, appName, appId, clusterIdsJSON, 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
}
// readHTTPDNSDefaultClusterIdList reads default cluster IDs from UserRegisterConfig.
func readHTTPDNSDefaultClusterIdList(tx *dbs.Tx) ([]int64, error) {
// 优先从 UserRegisterConfig 中读取
configJSON, err := models.SharedSysSettingDAO.ReadSetting(tx, systemconfigs.SettingCodeUserRegisterConfig)
if err != nil {
return nil, err
}
if len(configJSON) > 0 {
var config userconfigs.UserRegisterConfig
if err := json.Unmarshal(configJSON, &config); err == nil {
if len(config.HTTPDNSDefaultClusterIds) > 0 {
// 验证集群有效性
var validIds []int64
for _, id := range config.HTTPDNSDefaultClusterIds {
if id <= 0 {
continue
}
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(tx, id)
if err != nil {
return nil, err
}
if cluster != nil && cluster.IsOn {
validIds = append(validIds, id)
}
}
if len(validIds) > 0 {
return validIds, nil
}
}
}
}
return nil, nil
}
func (this *HTTPDNSAppService) UpdateHTTPDNSApp(ctx context.Context, req *pb.UpdateHTTPDNSAppRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
oldApp, err := ensureAppAccess(tx, req.AppDbId, userId)
if err != nil {
return err
}
if oldApp == nil {
return errors.New("app not found")
}
targetUserId := req.UserId
if targetUserId <= 0 {
targetUserId = oldApp.UserId
}
if userId > 0 {
targetUserId = userId
}
err = models.SharedHTTPDNSAppDAO.UpdateApp(tx, req.AppDbId, req.Name, req.ClusterIdsJSON, req.IsOn, targetUserId)
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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
app, err := ensureAppAccess(tx, req.AppDbId, userId)
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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
app, err := ensureAppAccess(this.NullTx(), req.AppDbId, userId)
if err != nil {
return nil, err
}
if app == nil {
return &pb.FindHTTPDNSAppResponse{}, nil
}
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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
var apps []*models.HTTPDNSApp
if userId > 0 {
apps, err = models.SharedHTTPDNSAppDAO.ListEnabledAppsWithUser(this.NullTx(), userId, req.Offset, req.Size, req.Keyword)
} else {
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) {
_, userId, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
}
var apps []*models.HTTPDNSApp
var err error
if validateErr == nil && userId > 0 {
apps, err = models.SharedHTTPDNSAppDAO.FindAllEnabledAppsWithUser(this.NullTx(), userId)
} else {
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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
app, err := ensureAppAccess(tx, req.AppDbId, userId)
if err != nil {
return err
}
if app == nil {
return errors.New("app not found")
}
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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
var signSecret string
var updatedAt int64
err = this.RunTx(func(tx *dbs.Tx) error {
app, err := ensureAppAccess(tx, req.AppDbId, userId)
if err != nil {
return err
}
if app == nil {
return errors.New("app not found")
}
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,216 @@
package httpdns
import (
"context"
"errors"
"fmt"
"strings"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// HTTPDNSClusterService HTTPDNS集群服务
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, req.AutoRemoteStart, req.AccessLogIsOn, req.TimeZone)
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
}
// Compatibility fallback:
// If protobuf schemas between edge-admin and edge-api are inconsistent,
// these newly-added fields may be lost on the wire. Read gRPC metadata as fallback.
if md, ok := metadata.FromIncomingContext(ctx); ok {
if values := md.Get("x-httpdns-auto-remote-start"); len(values) > 0 {
raw := strings.ToLower(strings.TrimSpace(values[0]))
req.AutoRemoteStart = raw == "1" || raw == "true" || raw == "on" || raw == "yes" || raw == "enabled"
}
if values := md.Get("x-httpdns-access-log-is-on"); len(values) > 0 {
raw := strings.ToLower(strings.TrimSpace(values[0]))
req.AccessLogIsOn = raw == "1" || raw == "true" || raw == "on" || raw == "yes" || raw == "enabled"
}
if values := md.Get("x-httpdns-time-zone"); len(values) > 0 {
raw := strings.TrimSpace(values[0])
if len(raw) > 0 {
req.TimeZone = raw
}
}
}
err = this.RunTx(func(tx *dbs.Tx) error {
// 先读取旧的 TLS 配置,用于判断是否真正发生了变化
var oldTLSJSON string
oldCluster, findErr := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(tx, req.ClusterId)
if findErr == nil && oldCluster != nil {
oldTLSJSON = string(oldCluster.TLSPolicy)
}
err = models.SharedHTTPDNSClusterDAO.UpdateCluster(tx, req.ClusterId, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault, req.AutoRemoteStart, req.AccessLogIsOn, req.TimeZone)
if err != nil {
return err
}
taskType := models.HTTPDNSNodeTaskTypeConfigChanged
if len(req.TlsPolicyJSON) > 0 && string(req.TlsPolicyJSON) != oldTLSJSON {
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
}
if cluster != nil {
_ = grpc.SetHeader(ctx, metadata.Pairs(
"x-httpdns-auto-remote-start", fmt.Sprintf("%t", cluster.AutoRemoteStart),
"x-httpdns-access-log-is-on", fmt.Sprintf("%t", cluster.AccessLogIsOn),
"x-httpdns-time-zone", cluster.TimeZone,
))
}
return &pb.FindHTTPDNSClusterResponse{Cluster: toPBCluster(cluster)}, nil
}
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)
isNode := false
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
isNode = true
}
clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(this.NullTx())
if err != nil {
return nil, err
}
var pbClusters []*pb.HTTPDNSCluster
for _, cluster := range clusters {
if isNode {
// 节点调用时解析证书引用,嵌入实际 PEM 数据
pbClusters = append(pbClusters, toPBClusterWithResolvedCerts(this.NullTx(), cluster))
} else {
pbClusters = append(pbClusters, toPBCluster(cluster))
}
}
return &pb.FindAllHTTPDNSClustersResponse{Clusters: pbClusters}, nil
}
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,128 @@
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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
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 {
app, err := ensureAppAccess(tx, req.AppDbId, userId)
if err != nil {
return err
}
if app == nil {
return errors.New("app not found")
}
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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
domain, app, err := ensureDomainAccess(tx, req.DomainId, userId)
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(app.Id), models.HTTPDNSNodeTaskTypeDomainChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSDomainService) UpdateHTTPDNSDomainStatus(ctx context.Context, req *pb.UpdateHTTPDNSDomainStatusRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
domain, app, err := ensureDomainAccess(tx, req.DomainId, userId)
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(app.Id), models.HTTPDNSNodeTaskTypeDomainChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSDomainService) ListHTTPDNSDomainsWithAppId(ctx context.Context, req *pb.ListHTTPDNSDomainsWithAppIdRequest) (*pb.ListHTTPDNSDomainsWithAppIdResponse, error) {
_, userId, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
} else if userId > 0 {
app, err := ensureAppAccess(this.NullTx(), req.AppDbId, userId)
if err != nil {
return nil, err
}
if app == nil {
return &pb.ListHTTPDNSDomainsWithAppIdResponse{}, nil
}
}
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,409 @@
package httpdns
import (
"context"
"encoding/json"
"errors"
"io"
"path/filepath"
"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/EdgeAPI/internal/setup"
rpcutils "github.com/TeaOSLab/EdgeAPI/internal/rpc/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
stringutil "github.com/iwind/TeaGo/utils/string"
)
// 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
}
pbNode := toPBNode(node)
// 认证信息
if pbNode != nil {
login, loginErr := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(this.NullTx(), nodeconfigs.NodeRoleHTTPDNS, nodeId)
if loginErr != nil {
return nil, loginErr
}
if login != nil {
pbNode.NodeLogin = &pb.NodeLogin{
Id: int64(login.Id),
Name: login.Name,
Type: login.Type,
Params: login.Params,
}
}
}
return &pb.FindHTTPDNSNodeResponse{Node: pbNode}, 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()
}
// UpdateHTTPDNSNodeLogin 修改HTTPDNS节点登录信息
func (this *HTTPDNSNodeService) UpdateHTTPDNSNodeLogin(ctx context.Context, req *pb.UpdateHTTPDNSNodeLoginRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
if req.NodeLogin.Id <= 0 {
loginId, createErr := models.SharedNodeLoginDAO.CreateNodeLogin(tx, nodeconfigs.NodeRoleHTTPDNS, req.NodeId, req.NodeLogin.Name, req.NodeLogin.Type, req.NodeLogin.Params)
if createErr != nil {
return nil, createErr
}
req.NodeLogin.Id = loginId
}
err = models.SharedNodeLoginDAO.UpdateNodeLogin(tx, req.NodeLogin.Id, req.NodeLogin.Name, req.NodeLogin.Type, req.NodeLogin.Params)
if err != nil {
return nil, err
}
return this.Success()
}
// CheckHTTPDNSNodeLatestVersion 检查HTTPDNS节点新版本
func (this *HTTPDNSNodeService) CheckHTTPDNSNodeLatestVersion(ctx context.Context, req *pb.CheckHTTPDNSNodeLatestVersionRequest) (*pb.CheckHTTPDNSNodeLatestVersionResponse, error) {
_, _, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeAdmin, rpcutils.UserTypeHTTPDNS)
if err != nil {
return nil, err
}
deployFiles := installers.SharedDeployManager.LoadHTTPDNSNodeFiles()
for _, file := range deployFiles {
if file.OS == req.Os && file.Arch == req.Arch && stringutil.VersionCompare(file.Version, req.CurrentVersion) > 0 {
return &pb.CheckHTTPDNSNodeLatestVersionResponse{
HasNewVersion: true,
NewVersion: file.Version,
}, nil
}
}
return &pb.CheckHTTPDNSNodeLatestVersionResponse{HasNewVersion: false}, nil
}
// DownloadHTTPDNSNodeInstallationFile 下载最新HTTPDNS节点安装文件
func (this *HTTPDNSNodeService) DownloadHTTPDNSNodeInstallationFile(ctx context.Context, req *pb.DownloadHTTPDNSNodeInstallationFileRequest) (*pb.DownloadHTTPDNSNodeInstallationFileResponse, error) {
nodeId, err := this.ValidateHTTPDNSNode(ctx)
if err != nil {
return nil, err
}
// 检查自动升级开关
upgradeConfig, _ := setup.LoadUpgradeConfig()
if upgradeConfig != nil && !upgradeConfig.AutoUpgrade {
return &pb.DownloadHTTPDNSNodeInstallationFileResponse{}, nil
}
var file = installers.SharedDeployManager.FindHTTPDNSNodeFile(req.Os, req.Arch)
if file == nil {
return &pb.DownloadHTTPDNSNodeInstallationFileResponse{}, nil
}
sum, err := file.Sum()
if err != nil {
return nil, err
}
data, offset, err := file.Read(req.ChunkOffset)
if err != nil && err != io.EOF {
return nil, err
}
// 增加下载速度监控
installers.SharedUpgradeLimiter.UpdateNodeBytes(nodeconfigs.NodeRoleHTTPDNS, nodeId, int64(len(data)))
return &pb.DownloadHTTPDNSNodeInstallationFileResponse{
Sum: sum,
Offset: offset,
ChunkData: data,
Version: file.Version,
Filename: filepath.Base(file.Path),
}, nil
}
// CountAllUpgradeHTTPDNSNodesWithClusterId 计算需要升级的HTTPDNS节点数量
func (this *HTTPDNSNodeService) CountAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, req *pb.CountAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*pb.RPCCountResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
deployFiles := installers.SharedDeployManager.LoadHTTPDNSNodeFiles()
total := int64(0)
for _, deployFile := range deployFiles {
count, err := models.SharedHTTPDNSNodeDAO.CountAllLowerVersionNodesWithClusterId(tx, req.ClusterId, deployFile.OS, deployFile.Arch, deployFile.Version)
if err != nil {
return nil, err
}
total += count
}
return this.SuccessCount(total)
}
// FindAllUpgradeHTTPDNSNodesWithClusterId 列出所有需要升级的HTTPDNS节点
func (this *HTTPDNSNodeService) FindAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, req *pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
deployFiles := installers.SharedDeployManager.LoadHTTPDNSNodeFiles()
var result []*pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse_HTTPDNSNodeUpgrade
for _, deployFile := range deployFiles {
nodes, err := models.SharedHTTPDNSNodeDAO.FindAllLowerVersionNodesWithClusterId(tx, req.ClusterId, deployFile.OS, deployFile.Arch, deployFile.Version)
if err != nil {
return nil, err
}
for _, node := range nodes {
// 解析状态获取当前版本
var oldVersion string
if len(node.Status) > 0 {
var statusMap map[string]interface{}
if json.Unmarshal(node.Status, &statusMap) == nil {
if v, ok := statusMap["buildVersion"]; ok {
oldVersion, _ = v.(string)
}
}
}
pbNode := toPBNode(node)
// 认证信息
login, loginErr := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(tx, nodeconfigs.NodeRoleHTTPDNS, int64(node.Id))
if loginErr != nil {
return nil, loginErr
}
if login != nil && pbNode != nil {
pbNode.NodeLogin = &pb.NodeLogin{
Id: int64(login.Id),
Name: login.Name,
Type: login.Type,
Params: login.Params,
}
}
result = append(result, &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse_HTTPDNSNodeUpgrade{
Node: pbNode,
Os: deployFile.OS,
Arch: deployFile.Arch,
OldVersion: oldVersion,
NewVersion: deployFile.Version,
})
}
}
return &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse{Nodes: result}, nil
}
// UpgradeHTTPDNSNode 升级单个HTTPDNS节点
func (this *HTTPDNSNodeService) UpgradeHTTPDNSNode(ctx context.Context, req *pb.UpgradeHTTPDNSNodeRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
err = models.SharedHTTPDNSNodeDAO.UpdateNodeIsInstalled(tx, req.NodeId, false)
if err != nil {
return nil, err
}
// 重置安装状态
installStatus, err := models.SharedHTTPDNSNodeDAO.FindNodeInstallStatus(tx, req.NodeId)
if err != nil {
return nil, err
}
if installStatus == nil {
installStatus = &models.NodeInstallStatus{}
}
installStatus.IsOk = false
installStatus.IsFinished = false
err = models.SharedHTTPDNSNodeDAO.UpdateNodeInstallStatus(tx, req.NodeId, installStatus)
if err != nil {
return nil, err
}
goman.New(func() {
installErr := installers.SharedHTTPDNSNodeQueue().InstallNodeProcess(req.NodeId, true)
if installErr != nil {
logs.Println("[RPC][HTTPDNS]upgrade node failed:", installErr.Error())
}
})
return this.Success()
}
func shouldTriggerHTTPDNSInstall(installStatusJSON []byte) bool {
if len(installStatusJSON) == 0 {
return false
}
installStatus := &models.NodeInstallStatus{}
err := json.Unmarshal(installStatusJSON, installStatus)
if err != nil {
return false
}
return installStatus.IsRunning && !installStatus.IsFinished
}

View File

@@ -0,0 +1,206 @@
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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
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 {
domain, app, err := ensureDomainAccess(tx, req.Rule.DomainId, userId)
if err != nil {
return err
}
if domain == nil || app == nil {
return errors.New("domain not found")
}
rule := &models.HTTPDNSCustomRule{
AppId: domain.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, int64(app.Id), 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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
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, app, err := ensureRuleAccess(tx, req.Rule.Id, userId)
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(app.Id), models.HTTPDNSNodeTaskTypeRuleChanged)
if err != nil {
return err
}
targetAppDbId := int64(app.Id)
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) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
rule, app, err := ensureRuleAccess(tx, req.RuleId, userId)
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(app.Id), models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSRuleService) UpdateHTTPDNSCustomRuleStatus(ctx context.Context, req *pb.UpdateHTTPDNSCustomRuleStatusRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
rule, app, err := ensureRuleAccess(tx, req.RuleId, userId)
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(app.Id), models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSRuleService) ListHTTPDNSCustomRulesWithDomainId(ctx context.Context, req *pb.ListHTTPDNSCustomRulesWithDomainIdRequest) (*pb.ListHTTPDNSCustomRulesWithDomainIdResponse, error) {
_, userId, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
} else if userId > 0 {
domain, _, err := ensureDomainAccess(this.NullTx(), req.DomainId, userId)
if err != nil {
return nil, err
}
if domain == nil {
return &pb.ListHTTPDNSCustomRulesWithDomainIdResponse{}, nil
}
}
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,285 @@
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) {
_, userId, 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 userId > 0 && app != nil && app.UserId != userId {
return nil, errors.New("access denied")
}
if app == nil || !app.IsOn {
return &pb.TestHTTPDNSResolveResponse{
Code: "APP_NOT_FOUND_OR_DISABLED",
Message: "找不到指定的应用,或该应用已下线",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
// 检查集群是否绑定
appClusterIds := models.SharedHTTPDNSAppDAO.ReadAppClusterIds(app)
if req.ClusterId > 0 {
var found bool
for _, cid := range appClusterIds {
if cid == req.ClusterId {
found = true
break
}
}
if !found {
return &pb.TestHTTPDNSResolveResponse{
Code: "APP_CLUSTER_MISMATCH",
Message: "当前应用未绑定到该集群",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
}
qtype := strings.ToUpper(strings.TrimSpace(req.Qtype))
if qtype == "" {
qtype = "A"
}
// 获取集群服务域名
clusterId := req.ClusterId
if clusterId <= 0 && len(appClusterIds) > 0 {
clusterId = appClusterIds[0]
}
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
}
port := "443"
if len(cluster.TLSPolicy) > 0 {
var tlsConfig map[string]interface{}
if err := json.Unmarshal(cluster.TLSPolicy, &tlsConfig); err == nil {
if listenRaw, ok := tlsConfig["listen"]; ok && listenRaw != nil {
if data, err := json.Marshal(listenRaw); err == nil {
var listenAddresses []map[string]interface{}
if err := json.Unmarshal(data, &listenAddresses); err == nil {
if len(listenAddresses) > 0 {
if portRange, ok := listenAddresses[0]["portRange"].(string); ok && len(portRange) > 0 {
port = portRange
}
}
}
}
}
}
}
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 + ":" + port + "/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,47 @@
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
}
clusterIds := models.SharedHTTPDNSAppDAO.ReadAppClusterIds(app)
notified := map[int64]bool{}
for _, clusterId := range clusterIds {
if clusterId <= 0 || notified[clusterId] {
continue
}
notified[clusterId] = true
err := notifyHTTPDNSClusterTask(tx, clusterId, 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

@@ -0,0 +1,81 @@
package httpdns
import (
"errors"
"strings"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/iwind/TeaGo/dbs"
)
func ensureAppAccess(tx *dbs.Tx, appDbId int64, userId int64) (*models.HTTPDNSApp, error) {
app, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(tx, appDbId)
if err != nil {
return nil, err
}
if app == nil {
return nil, nil
}
if userId > 0 && app.UserId != userId {
return nil, errors.New("access denied")
}
return app, nil
}
func ensureAppAccessByAppId(tx *dbs.Tx, appId string, userId int64) (*models.HTTPDNSApp, error) {
appId = strings.TrimSpace(appId)
if len(appId) == 0 {
return nil, nil
}
app, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(tx, appId)
if err != nil {
return nil, err
}
if app == nil {
return nil, nil
}
if userId > 0 && app.UserId != userId {
return nil, errors.New("access denied")
}
return app, nil
}
func ensureDomainAccess(tx *dbs.Tx, domainId int64, userId int64) (*models.HTTPDNSDomain, *models.HTTPDNSApp, error) {
domain, err := models.SharedHTTPDNSDomainDAO.FindEnabledDomain(tx, domainId)
if err != nil {
return nil, nil, err
}
if domain == nil {
return nil, nil, nil
}
app, err := ensureAppAccess(tx, int64(domain.AppId), userId)
if err != nil {
return nil, nil, err
}
if app == nil {
return nil, nil, nil
}
return domain, app, nil
}
func ensureRuleAccess(tx *dbs.Tx, ruleId int64, userId int64) (*models.HTTPDNSCustomRule, *models.HTTPDNSApp, error) {
rule, err := models.SharedHTTPDNSCustomRuleDAO.FindEnabledRule(tx, ruleId)
if err != nil {
return nil, nil, err
}
if rule == nil {
return nil, nil, nil
}
app, err := ensureAppAccess(tx, int64(rule.AppId), userId)
if err != nil {
return nil, nil, err
}
if app == nil {
return nil, nil, nil
}
return rule, app, nil
}

View File

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

View File

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

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

View File

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

@@ -71,7 +71,7 @@ func (this *UserService) UpdateUser(ctx context.Context, req *pb.UpdateUserReque
return nil, err
}
err = models.SharedUserDAO.UpdateUser(tx, req.UserId, req.Username, req.Password, req.Fullname, req.Mobile, req.Tel, req.Email, req.Remark, req.IsOn, req.NodeClusterId, req.BandwidthAlgo)
err = models.SharedUserDAO.UpdateUser(tx, req.UserId, req.Username, req.Password, req.Fullname, req.Mobile, req.Tel, req.Email, req.Remark, req.IsOn, req.NodeClusterId, req.BandwidthAlgo, req.HttpdnsClusterIdsJSON)
if err != nil {
return nil, err
}
@@ -242,6 +242,20 @@ func (this *UserService) FindEnabledUser(ctx context.Context, req *pb.FindEnable
}
}
// 用户功能列表
var pbFeatures []*pb.UserFeature
userFeatures, err := models.SharedUserDAO.FindUserFeatures(tx, req.UserId)
if err != nil {
return nil, err
}
for _, f := range userFeatures {
pbFeatures = append(pbFeatures, &pb.UserFeature{
Name: f.Name,
Code: f.Code,
Description: f.Description,
})
}
return &pb.FindEnabledUserResponse{
User: &pb.User{
Id: int64(user.Id),
@@ -265,6 +279,8 @@ func (this *UserService) FindEnabledUser(ctx context.Context, req *pb.FindEnable
BandwidthAlgo: user.BandwidthAlgo,
OtpLogin: pbOtpAuth,
Lang: user.Lang,
Features: pbFeatures,
HttpdnsClusterIdsJSON: user.HttpdnsClusterIds,
},
}, nil
}

View File

@@ -5,6 +5,7 @@ package users
import (
"context"
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -142,13 +143,24 @@ func (this *UserService) RegisterUser(ctx context.Context, req *pb.RegisterUserR
return errors.New("the username exists already")
}
features := registerConfig.Features
// 创建用户
userId, err := models.SharedUserDAO.CreateUser(tx, req.Username, req.Password, req.Fullname, req.Mobile, "", req.Email, "", req.Source, registerConfig.ClusterId, registerConfig.Features, req.Ip, !registerConfig.RequireVerification)
userId, err := models.SharedUserDAO.CreateUser(tx, req.Username, req.Password, req.Fullname, req.Mobile, "", req.Email, "", req.Source, registerConfig.ClusterId, features, req.Ip, !registerConfig.RequireVerification)
if err != nil {
return err
}
createdUserId = userId
// 自动关联默认 HTTPDNS 集群
if registerConfig.HTTPDNSIsOn && len(registerConfig.HTTPDNSDefaultClusterIds) > 0 {
httpdnsJSON, _ := json.Marshal(registerConfig.HTTPDNSDefaultClusterIds)
err = models.SharedUserDAO.UpdateUserHttpdnsClusterIds(tx, userId, httpdnsJSON)
if err != nil {
return err
}
}
// 发送激活邮件
if len(req.Email) > 0 && registerConfig.EmailVerification.IsOn {
_, err := models.SharedUserEmailVerificationDAO.CreateVerification(tx, userId, req.Email)

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 DEFAULT '' CODEC(ZSTD(3)),
request_body String DEFAULT '' CODEC(ZSTD(3)),
response_headers String DEFAULT '' CODEC(ZSTD(3)),
response_body String DEFAULT '' CODEC(ZSTD(3)),
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 DEFAULT '' CODEC(ZSTD(3)),
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,12 @@ var upgradeFuncs = []*upgradeVersion{
{
"1.4.4", upgradeV1_4_4,
},
{
"1.4.8", upgradeV1_4_8,
},
{
"1.4.9", upgradeV1_4_9,
},
}
// UpgradeSQLData 升级SQL数据
@@ -269,14 +275,14 @@ func upgradeV0_0_10(db *dbs.DB) error {
// v0.2.5
func upgradeV0_2_5(db *dbs.DB) error {
// 更新用户
// 鏇存柊鐢ㄦ埛
_, err := db.Exec("UPDATE edgeUsers SET day=FROM_UNIXTIME(createdAt,'%Y%m%d') WHERE day IS NULL OR LENGTH(day)=0")
if err != nil {
return err
}
// 更新防火墙规则
ones, _, err := db.FindOnes("SELECT id, actions, action, actionOptions FROM edgeHTTPFirewallRuleSets WHERE actions IS NULL OR LENGTH(actions)=0")
// 更新防火墙规则
ones, _, err := db.FindOnes("SELECT id, actions, action, actionOptions FROM edgeHTTPFirewallRuleSets WHERE actions IS NULL OR LENGTH(actions)=0")
if err != nil {
return err
}
@@ -309,8 +315,8 @@ func upgradeV0_2_5(db *dbs.DB) error {
// v0.3.0
func upgradeV0_3_0(db *dbs.DB) error {
// 升级健康检查
ones, _, err := db.FindOnes("SELECT id,healthCheck FROM edgeNodeClusters WHERE state=1")
// 升级健康检查
ones, _, err := db.FindOnes("SELECT id,healthCheck FROM edgeNodeClusters WHERE state=1")
if err != nil {
return err
}
@@ -342,11 +348,10 @@ func upgradeV0_3_0(db *dbs.DB) error {
// v0.3.1
func upgradeV0_3_1(db *dbs.DB) error {
// 清空域名统计,已使用分表代替
// 因为可能有权限问题,所以我们忽略错误
_, _ = db.Exec("TRUNCATE table edgeServerDomainHourlyStats")
// 娓呯┖鍩熷悕缁熻锛屽凡浣跨敤鍒嗚〃浠f浛
// 鍥犱负鍙兘鏈夋潈闄愰棶棰橈紝鎵€浠ユ垜浠拷鐣ラ敊璇? _, _ = db.Exec("TRUNCATE table edgeServerDomainHourlyStats")
// 升级APIToken
// 鍗囩骇APIToken
ones, _, err := db.FindOnes("SELECT uniqueId,secret FROM edgeNodeClusters")
if err != nil {
return err
@@ -374,9 +379,9 @@ func upgradeV0_3_2(db *dbs.DB) error {
// gzip => compression
type HTTPGzipRef struct {
IsPrior bool `yaml:"isPrior" json:"isPrior"` // 是否覆盖
IsOn bool `yaml:"isOn" json:"isOn"` // 是否开启
GzipId int64 `yaml:"gzipId" json:"gzipId"` // 使用的配置ID
IsPrior bool `yaml:"isPrior" json:"isPrior"` // 鏄惁瑕嗙洊
IsOn bool `yaml:"isOn" json:"isOn"` // 是否开启
GzipId int64 `yaml:"gzipId" json:"gzipId"` // 使用的配置ID
}
webOnes, _, err := db.FindOnes("SELECT id, gzip FROM edgeHTTPWebs WHERE gzip IS NOT NULL AND compression IS NULL")
@@ -458,7 +463,7 @@ func upgradeV0_3_2(db *dbs.DB) error {
}
}
// 更新服务端口
// 鏇存柊鏈嶅姟绔彛
var serverDAO = models.NewServerDAO()
ones, err := serverDAO.Query(nil).
ResultPk().
@@ -479,14 +484,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 +502,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 +519,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 +533,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 +550,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 +574,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 +594,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 +626,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 +678,7 @@ func upgradeV0_4_8(db *dbs.DB) error {
// v0.4.11
func upgradeV0_4_11(db *dbs.DB) error {
// 升级ns端口
// 鍗囩骇ns绔彛
{
// TCP
{
@@ -751,19 +756,19 @@ func upgradeV1_2_1(db *dbs.DB) error {
// 1.2.10
func upgradeV1_2_10(db *dbs.DB) error {
{
type OldGlobalConfig struct {
// HTTP & HTTPS相关配置
HTTPAll struct {
DomainAuditingIsOn bool `yaml:"domainAuditingIsOn" json:"domainAuditingIsOn"` // 域名是否需要审核
DomainAuditingPrompt string `yaml:"domainAuditingPrompt" json:"domainAuditingPrompt"` // 域名审核提示
} `yaml:"httpAll" json:"httpAll"`
type OldGlobalConfig struct {
// HTTP & HTTPS鐩稿叧閰嶇疆
HTTPAll struct {
DomainAuditingIsOn bool `yaml:"domainAuditingIsOn" json:"domainAuditingIsOn"` // 域名是否需要审核
DomainAuditingPrompt string `yaml:"domainAuditingPrompt" json:"domainAuditingPrompt"` // 域名审核提示
} `yaml:"httpAll" json:"httpAll"`
TCPAll struct {
PortRangeMin int `yaml:"portRangeMin" json:"portRangeMin"` // 最小端口
PortRangeMax int `yaml:"portRangeMax" json:"portRangeMax"` // 最大端口
DenyPorts []int `yaml:"denyPorts" json:"denyPorts"` // 禁止使用的端口
} `yaml:"tcpAll" json:"tcpAll"`
}
TCPAll struct {
PortRangeMin int `yaml:"portRangeMin" json:"portRangeMin"` // 最小端口
PortRangeMax int `yaml:"portRangeMax" json:"portRangeMax"` // 最大端口
DenyPorts []int `yaml:"denyPorts" json:"denyPorts"` // 禁止端口
} `yaml:"tcpAll" json:"tcpAll"`
}
globalConfigValue, err := db.FindCol(0, "SELECT value FROM edgeSysSettings WHERE code='serverGlobalConfig'")
if err != nil {
@@ -1023,7 +1028,7 @@ func upgradeV1_3_2(db *dbs.DB) error {
}
}
err = addRuleToGroup(ruleGroup.GetInt64("id"), "7010", "SQL注入检测", []*firewallconfigs.HTTPFirewallActionConfig{
err = addRuleToGroup(ruleGroup.GetInt64("id"), "7010", "SQL注入检测", []*firewallconfigs.HTTPFirewallActionConfig{
{
Code: firewallconfigs.HTTPFirewallActionPage,
Options: maps.Map{"status": 403, "body": ""},
@@ -1131,7 +1136,7 @@ func upgradeV1_3_2(db *dbs.DB) error {
}
}
err = addRuleToGroup(ruleGroup.GetInt64("id"), "1010", "XSS攻击检测", []*firewallconfigs.HTTPFirewallActionConfig{
err = addRuleToGroup(ruleGroup.GetInt64("id"), "1010", "XSS攻击检测", []*firewallconfigs.HTTPFirewallActionConfig{
{
Code: firewallconfigs.HTTPFirewallActionPage,
Options: maps.Map{"status": 403, "body": ""},
@@ -1233,8 +1238,8 @@ func upgradeV1_3_4(db *dbs.DB) error {
// 1.4.4
func upgradeV1_4_4(db *dbs.DB) error {
// 检查 encryption 字段是否已存在
col, err := db.FindCol(0, "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='edgeHTTPWebs' AND COLUMN_NAME='encryption'")
// 检查 encryption 字段是否已存在
col, err := db.FindCol(0, "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='edgeHTTPWebs' AND COLUMN_NAME='encryption'")
if err != nil {
return err
}
@@ -1253,3 +1258,58 @@ func upgradeV1_4_4(db *dbs.DB) error {
return nil
}
// 1.4.8
func upgradeV1_4_8(db *dbs.DB) error {
err := createHTTPDNSTables(db)
if err != nil {
return err
}
// edgeUsers: 增加 httpdnsClusterIds 字段
_, alterErr := db.Exec("ALTER TABLE `edgeUsers` ADD COLUMN `httpdnsClusterIds` text DEFAULT NULL")
if alterErr != nil {
if strings.Contains(alterErr.Error(), "Duplicate column") {
return nil
}
return alterErr
}
return nil
}
// 1.4.9
func upgradeV1_4_9(db *dbs.DB) error {
_, err := db.Exec("ALTER TABLE `edgeHTTPDNSClusters` ALTER COLUMN `installDir` SET DEFAULT '/root/edge-httpdns'")
if err != nil {
return err
}
_, err = db.Exec("ALTER TABLE `edgeHTTPDNSNodes` ALTER COLUMN `installDir` SET DEFAULT '/root/edge-httpdns'")
if err != nil {
return err
}
return nil
}
func createHTTPDNSTables(db *dbs.DB) error {
sqls := []string{
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSClusters` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isDefault` tinyint unsigned DEFAULT '0',`serviceDomain` varchar(255) DEFAULT NULL,`defaultTTL` int unsigned DEFAULT '30',`fallbackTimeoutMs` int unsigned DEFAULT '300',`installDir` varchar(255) DEFAULT '/root/edge-httpdns',`tlsPolicy` json DEFAULT NULL,`autoRemoteStart` tinyint unsigned DEFAULT '0',`accessLogIsOn` tinyint unsigned DEFAULT '0',`timeZone` varchar(128) NOT NULL DEFAULT '',`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),KEY `name` (`name`),KEY `isDefault` (`isDefault`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS集群配置表默认TTL、回退超时、服务域名等'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSNodes` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`clusterId` bigint unsigned DEFAULT '0',`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isUp` tinyint unsigned DEFAULT '0',`isInstalled` tinyint unsigned DEFAULT '0',`isActive` tinyint unsigned DEFAULT '0',`uniqueId` varchar(64) DEFAULT NULL,`secret` varchar(64) DEFAULT NULL,`installDir` varchar(255) DEFAULT '/root/edge-httpdns',`status` json DEFAULT NULL,`installStatus` json DEFAULT NULL,`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `uniqueId` (`uniqueId`),KEY `clusterId` (`clusterId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS节点表节点基础信息与运行状态'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSApps` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`appId` varchar(64) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`clusterIdsJSON` text DEFAULT NULL,`sniMode` varchar(64) DEFAULT 'fixed_hide',`userId` bigint unsigned DEFAULT '0',`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `appId` (`appId`),KEY `name` (`name`),KEY `userId` (`userId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS应用表应用与集群绑定关系'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSAppSecrets` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`appId` bigint unsigned DEFAULT '0',`signEnabled` tinyint unsigned DEFAULT '0',`signSecret` varchar(255) DEFAULT NULL,`signUpdatedAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `appId` (`appId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS应用密钥表请求验签开关与加签Secret'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSDomains` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`appId` bigint unsigned DEFAULT '0',`domain` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `appId_domain` (`appId`,`domain`),KEY `domain` (`domain`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS应用域名表应用绑定的业务域名'",
"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

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

View File

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

View File

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

View File

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

View File

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

BIN
EdgeAdmin/edge-admin Normal file

Binary file not shown.

View File

@@ -18,6 +18,7 @@ const (
AdminModuleCodeServer AdminModuleCode = "server" // 网站
AdminModuleCodeNode AdminModuleCode = "node" // 节点
AdminModuleCodeDNS AdminModuleCode = "dns" // DNS
AdminModuleCodeHttpDNS AdminModuleCode = "httpdns" // HTTPDNS
AdminModuleCodeNS AdminModuleCode = "ns" // 域名服务
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
@@ -106,7 +107,19 @@ func AllowModule(adminId int64, module string) bool {
list, ok := sharedAdminModuleMapping[adminId]
if ok {
return list.Allow(module)
if list.Allow(module) {
return true
}
// Backward compatibility: old admin module sets may not contain "httpdns".
// In that case, reuse related CDN module permissions to keep HTTPDNS visible/accessible.
if module == AdminModuleCodeHttpDNS {
return list.Allow(AdminModuleCodeDNS) ||
list.Allow(AdminModuleCodeNode) ||
list.Allow(AdminModuleCodeServer)
}
return false
}
return false
@@ -226,6 +239,11 @@ func AllModuleMaps(langCode string) []maps.Map {
"code": AdminModuleCodeDNS,
"url": "/dns",
},
{
"name": "HTTPDNS",
"code": AdminModuleCodeHttpDNS,
"url": "/httpdns/clusters",
},
}
if teaconst.IsPlus {
m = append(m, maps.Map{

View File

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

View File

@@ -1,9 +1,9 @@
package teaconst
const (
Version = "1.4.7" //1.3.9
Version = "1.4.9" //1.3.9
APINodeVersion = "1.4.7" //1.3.9
APINodeVersion = "1.4.9" //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

@@ -1,45 +1,52 @@
package utils
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"errors"
"sync"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/logs"
"github.com/miekg/dns"
"sync"
)
var sharedDNSClient *dns.Client
var sharedDNSConfig *dns.ClientConfig
var dnsClient *dns.Client
var dnsConfig *dns.ClientConfig
func init() {
if !teaconst.IsMain {
return
}
// The teaconst.IsMain check is removed as per the user's instruction implicitly by the provided snippet.
// if !teaconst.IsMain {
// return
// }
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
logs.Println("ERROR: configure dns client failed: " + err.Error())
return
// Fallback for Windows or systems without resolv.conf
config = &dns.ClientConfig{
Servers: []string{"8.8.8.8", "8.8.4.4"},
Search: []string{},
Port: "53",
Ndots: 1,
Timeout: 5,
Attempts: 2,
}
logs.Println("WARNING: configure dns client: /etc/resolv.conf not found, using fallback 8.8.8.8")
}
sharedDNSConfig = config
sharedDNSClient = &dns.Client{}
dnsConfig = config
dnsClient = new(dns.Client)
}
// LookupCNAME 获取CNAME
func LookupCNAME(host string) (string, error) {
var m = new(dns.Msg)
if dnsClient == nil || dnsConfig == nil {
return "", errors.New("dns client not initialized")
}
m.SetQuestion(host+".", dns.TypeCNAME)
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(host), dns.TypeCNAME)
m.RecursionDesired = true
var lastErr error
var success = false
var result = ""
var serverAddrs = sharedDNSConfig.Servers
var serverAddrs = dnsConfig.Servers
{
var publicDNSHosts = []string{"8.8.8.8" /** Google **/, "8.8.4.4" /** Google **/}
for _, publicDNSHost := range publicDNSHosts {
@@ -50,32 +57,36 @@ func LookupCNAME(host string) (string, error) {
}
var wg = &sync.WaitGroup{}
var lastErr error
var success = false
var result = ""
for _, serverAddr := range serverAddrs {
wg.Add(1)
go func(serverAddr string) {
go func(server string) {
defer wg.Done()
r, _, err := sharedDNSClient.Exchange(m, configutils.QuoteIP(serverAddr)+":"+sharedDNSConfig.Port)
if err != nil {
r, _, err := dnsClient.Exchange(m, server+":"+dnsConfig.Port)
if err == nil && r != nil && r.Rcode == dns.RcodeSuccess {
for _, ans := range r.Answer {
if cname, ok := ans.(*dns.CNAME); ok {
success = true
result = cname.Target
}
}
} else if err != nil {
lastErr = err
return
}
success = true
if len(r.Answer) == 0 {
return
}
result = r.Answer[0].(*dns.CNAME).Target
}(serverAddr)
}
wg.Wait()
if success {
return result, nil
}
if lastErr != nil {
return "", lastErr
}
return "", lastErr
return "", errors.New("lookup failed")
}

View File

@@ -0,0 +1,123 @@
package httpdns
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"regexp"
"strings"
)
type AddPortPopupAction struct {
actionutils.ParentAction
}
func (this *AddPortPopupAction) Init() {
this.Nav("", "", "")
}
func (this *AddPortPopupAction) RunGet(params struct {
Protocol string
From string
SupportRange bool
}) {
this.Data["from"] = params.From
var protocols = serverconfigs.FindAllServerProtocols()
if len(params.Protocol) > 0 {
result := []maps.Map{}
for _, p := range protocols {
if p.GetString("code") == params.Protocol {
result = append(result, p)
}
}
protocols = result
}
this.Data["protocols"] = protocols
this.Data["supportRange"] = params.SupportRange
this.Show()
}
func (this *AddPortPopupAction) RunPost(params struct {
SupportRange bool
Protocol string
Address string
Must *actions.Must
}) {
// 校验地址
var addr = maps.Map{
"protocol": params.Protocol,
"host": "",
"portRange": "",
"minPort": 0,
"maxPort": 0,
}
var portRegexp = regexp.MustCompile(`^\d+$`)
if portRegexp.MatchString(params.Address) { // 单个端口
addr["portRange"] = this.checkPort(params.Address)
} else if params.SupportRange && regexp.MustCompile(`^\d+\s*-\s*\d+$`).MatchString(params.Address) { // Port1-Port2
addr["portRange"], addr["minPort"], addr["maxPort"] = this.checkPortRange(params.Address)
} else if strings.Contains(params.Address, ":") { // IP:Port
index := strings.LastIndex(params.Address, ":")
addr["host"] = strings.TrimSpace(params.Address[:index])
port := strings.TrimSpace(params.Address[index+1:])
if portRegexp.MatchString(port) {
addr["portRange"] = this.checkPort(port)
} else if params.SupportRange && regexp.MustCompile(`^\d+\s*-\s*\d+$`).MatchString(port) { // Port1-Port2
addr["portRange"], addr["minPort"], addr["maxPort"] = this.checkPortRange(port)
} else {
this.FailField("address", "请输入正确的端口或者网络地址")
}
} else {
this.FailField("address", "请输入正确的端口或者网络地址")
}
this.Data["address"] = addr
this.Success()
}
func (this *AddPortPopupAction) checkPort(port string) (portRange string) {
var intPort = types.Int(port)
if intPort < 1 {
this.FailField("address", "端口号不能小于1")
}
if intPort > 65535 {
this.FailField("address", "端口号不能大于65535")
}
return port
}
func (this *AddPortPopupAction) checkPortRange(port string) (portRange string, minPort int, maxPort int) {
var pieces = strings.Split(port, "-")
var piece1 = strings.TrimSpace(pieces[0])
var piece2 = strings.TrimSpace(pieces[1])
var port1 = types.Int(piece1)
var port2 = types.Int(piece2)
if port1 < 1 {
this.FailField("address", "端口号不能小于1")
}
if port1 > 65535 {
this.FailField("address", "端口号不能大于65535")
}
if port2 < 1 {
this.FailField("address", "端口号不能小于1")
}
if port2 > 65535 {
this.FailField("address", "端口号不能大于65535")
}
if port1 > port2 {
port1, port2 = port2, port1
}
return types.String(port1) + "-" + types.String(port2), port1, port2
}

View File

@@ -0,0 +1,26 @@
package apps
import (
"strconv"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
)
type AppAction struct {
actionutils.ParentAction
}
func (this *AppAction) Init() {
this.Nav("httpdns", "app", "")
}
func (this *AppAction) RunGet(params struct {
AppId int64
}) {
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
this.RedirectURL("/httpdns/apps/domains?appId=" + strconv.FormatInt(app.GetInt64("id"), 10))
}

View File

@@ -0,0 +1,175 @@
package apps
import (
"encoding/json"
"strconv"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type AppSettingsAction struct {
actionutils.ParentAction
}
func (this *AppSettingsAction) Init() {
this.Nav("httpdns", "app", "settings")
}
func (this *AppSettingsAction) RunGet(params struct {
AppId int64
Section string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "settings")
section := params.Section
if len(section) == 0 {
section = "basic"
}
this.Data["activeSection"] = section
appIDStr := strconv.FormatInt(app.GetInt64("id"), 10)
this.Data["leftMenuItems"] = []maps.Map{
{
"name": "基础配置",
"url": "/httpdns/apps/app/settings?appId=" + appIDStr + "&section=basic",
"isActive": section == "basic",
},
{
"name": "认证与密钥",
"url": "/httpdns/apps/app/settings?appId=" + appIDStr + "&section=auth",
"isActive": section == "auth",
},
}
clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
clusters := make([]maps.Map, 0, len(clusterResp.GetClusters()))
clusterApiAddressMap := map[int64]string{}
clusterNameMap := map[int64]string{}
for _, cluster := range clusterResp.GetClusters() {
clusterId := cluster.GetId()
clusterName := cluster.GetName()
port := "443"
if rawTLS := cluster.GetTlsPolicyJSON(); len(rawTLS) > 0 {
var tlsConfig map[string]interface{}
if err := json.Unmarshal(rawTLS, &tlsConfig); err == nil {
if listenRaw, ok := tlsConfig["listen"]; ok && listenRaw != nil {
if data, err := json.Marshal(listenRaw); err == nil {
var listenAddresses []map[string]interface{}
if err := json.Unmarshal(data, &listenAddresses); err == nil {
if len(listenAddresses) > 0 {
if portRange, ok := listenAddresses[0]["portRange"].(string); ok && len(portRange) > 0 {
port = portRange
}
}
}
}
}
}
}
apiAddress := "https://" + cluster.GetServiceDomain() + ":" + port
clusters = append(clusters, maps.Map{
"id": clusterId,
"name": clusterName,
})
clusterApiAddressMap[clusterId] = apiAddress
clusterNameMap[clusterId] = clusterName
}
// 读取应用绑定的集群列表,取第一个作为当前选中。
var selectedClusterId int64
if raw := app.Get("clusterIds"); raw != nil {
if ids, ok := raw.([]int64); ok && len(ids) > 0 {
selectedClusterId = ids[0]
}
}
// 构建服务地址列表。
serviceAddresses := make([]maps.Map, 0)
if selectedClusterId > 0 {
addr := clusterApiAddressMap[selectedClusterId]
name := clusterNameMap[selectedClusterId]
if len(addr) > 0 {
serviceAddresses = append(serviceAddresses, maps.Map{
"address": addr,
"clusterName": name,
})
}
}
settings := maps.Map{
"appId": app.GetString("appId"),
"appStatus": app.GetBool("isOn"),
"selectedClusterId": selectedClusterId,
"serviceAddresses": serviceAddresses,
"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.Data["clusters"] = clusters
this.Show()
}
func (this *AppSettingsAction) RunPost(params struct {
AppId int64
AppStatus bool
ClusterId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
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
}
var clusterIds []int64
if params.ClusterId > 0 {
clusterIds = []int64{params.ClusterId}
}
clusterIdsJSON, _ := json.Marshal(clusterIds)
_, err = this.RPC().HTTPDNSAppRPC().UpdateHTTPDNSApp(this.AdminContext(), &pb.UpdateHTTPDNSAppRequest{
AppDbId: params.AppId,
Name: appResp.GetApp().GetName(),
ClusterIdsJSON: clusterIdsJSON,
IsOn: params.AppStatus,
UserId: appResp.GetApp().GetUserId(),
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,29 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
type AppSettingsResetSignSecretAction struct {
actionutils.ParentAction
}
func (this *AppSettingsResetSignSecretAction) RunPost(params struct {
AppId int64
Must *actions.Must
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
_, err := this.RPC().HTTPDNSAppRPC().ResetHTTPDNSAppSignSecret(this.AdminContext(), &pb.ResetHTTPDNSAppSignSecretRequest{
AppDbId: params.AppId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,31 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
type AppSettingsToggleSignEnabledAction struct {
actionutils.ParentAction
}
func (this *AppSettingsToggleSignEnabledAction) RunPost(params struct {
AppId int64
IsOn int
Must *actions.Must
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
_, err := this.RPC().HTTPDNSAppRPC().UpdateHTTPDNSAppSignEnabled(this.AdminContext(), &pb.UpdateHTTPDNSAppSignEnabledRequest{
AppDbId: params.AppId,
SignEnabled: params.IsOn == 1,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,89 @@
package apps
import (
"encoding/json"
"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
usersResp, err := this.RPC().UserRPC().ListEnabledUsers(this.AdminContext(), &pb.ListEnabledUsersRequest{
Offset: 0,
Size: 10_000,
})
if err != nil {
this.ErrorPage(err)
return
}
users := make([]maps.Map, 0, len(usersResp.GetUsers()))
for _, user := range usersResp.GetUsers() {
users = append(users, maps.Map{
"id": user.GetId(),
"fullname": user.GetFullname(),
"username": user.GetUsername(),
})
}
this.Data["users"] = users
this.Show()
}
func (this *CreateAction) RunPost(params struct {
Name string
ClusterId int64
UserId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("name", params.Name).Require("请输入应用名称")
if params.ClusterId <= 0 {
this.FailField("clusterId", "请选择集群")
return
}
clusterIdsJSON, _ := json.Marshal([]int64{params.ClusterId})
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),
ClusterIdsJSON: clusterIdsJSON,
IsOn: true,
SignEnabled: true,
UserId: params.UserId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["appId"] = createResp.GetAppDbId()
this.Success()
}

View File

@@ -0,0 +1,56 @@
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/maps"
)
type CustomRecordsAction struct {
actionutils.ParentAction
}
func (this *CustomRecordsAction) Init() {
this.Nav("httpdns", "app", "")
}
func (this *CustomRecordsAction) RunGet(params struct {
AppId int64
DomainId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "domains")
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
domain := findDomainMap(domains, params.DomainId)
records := make([]maps.Map, 0)
if domain.GetInt64("id") > 0 {
records, err = listCustomRuleMaps(this.Parent(), domain.GetInt64("id"))
if err != nil {
this.ErrorPage(err)
return
}
for _, record := range records {
record["domain"] = domain.GetString("name")
record["lineText"] = buildLineText(record)
record["recordValueText"] = buildRecordValueText(record)
}
}
this.Data["app"] = app
this.Data["domain"] = domain
this.Data["records"] = records
this.Show()
}

View File

@@ -0,0 +1,263 @@
package apps
import (
"encoding/json"
"fmt"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type CustomRecordsCreatePopupAction struct {
actionutils.ParentAction
}
func (this *CustomRecordsCreatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *CustomRecordsCreatePopupAction) RunGet(params struct {
AppId int64
DomainId int64
RecordId int64
}) {
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
domains, err := listDomainMaps(this.Parent(), app.GetInt64("id"), "")
if err != nil {
this.ErrorPage(err)
return
}
domain := findDomainMap(domains, params.DomainId)
record := maps.Map{
"id": int64(0),
"domain": domain.GetString("name"),
"lineScope": "china",
"lineCarrier": "默认",
"lineRegion": "默认",
"lineProvince": "默认",
"lineContinent": "默认",
"lineCountry": "默认",
"ruleName": "",
"weightEnabled": false,
"ttl": 60,
"isOn": true,
"recordItemsJson": `[{"type":"A","value":"","weight":100}]`,
}
if params.RecordId > 0 && domain.GetInt64("id") > 0 {
rules, err := listCustomRuleMaps(this.Parent(), domain.GetInt64("id"))
if err != nil {
this.ErrorPage(err)
return
}
for _, rule := range rules {
if rule.GetInt64("id") != params.RecordId {
continue
}
record["id"] = rule.GetInt64("id")
record["domain"] = domain.GetString("name")
record["lineScope"] = rule.GetString("lineScope")
record["lineCarrier"] = defaultLineField(rule.GetString("lineCarrier"))
record["lineRegion"] = defaultLineField(rule.GetString("lineRegion"))
record["lineProvince"] = defaultLineField(rule.GetString("lineProvince"))
record["lineContinent"] = defaultLineField(rule.GetString("lineContinent"))
record["lineCountry"] = defaultLineField(rule.GetString("lineCountry"))
record["ruleName"] = rule.GetString("ruleName")
record["weightEnabled"] = rule.GetBool("weightEnabled")
record["ttl"] = rule.GetInt("ttl")
record["isOn"] = rule.GetBool("isOn")
record["recordItemsJson"] = marshalJSON(rule["recordValues"], "[]")
break
}
}
this.Data["app"] = app
this.Data["domain"] = domain
this.Data["record"] = record
this.Data["isEditing"] = params.RecordId > 0
this.Show()
}
func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
AppId int64
DomainId int64
RecordId int64
Domain string
LineScope string
LineCarrier string
LineRegion string
LineProvince string
LineContinent string
LineCountry string
RuleName string
RecordItemsJSON string
WeightEnabled bool
Ttl int
IsOn bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
params.Must.Field("domainId", params.DomainId).Gt(0, "请选择所属域名")
params.LineScope = strings.ToLower(strings.TrimSpace(params.LineScope))
if params.LineScope != "china" && params.LineScope != "overseas" {
params.LineScope = "china"
}
params.RuleName = strings.TrimSpace(params.RuleName)
if len(params.RuleName) == 0 {
this.Fail("请输入规则名称")
return
}
if params.Ttl <= 0 || params.Ttl > 86400 {
this.Fail("TTL值必须在 1-86400 范围内")
return
}
recordValues, err := parseRecordItemsJSON(params.RecordItemsJSON, params.WeightEnabled)
if err != nil {
this.Fail(err.Error())
return
}
if len(recordValues) == 0 {
this.Fail("请输入解析记录值")
return
}
if len(recordValues) > 10 {
this.Fail("单个规则最多只能添加 10 条解析记录")
return
}
lineCarrier := strings.TrimSpace(params.LineCarrier)
lineRegion := strings.TrimSpace(params.LineRegion)
lineProvince := strings.TrimSpace(params.LineProvince)
lineContinent := strings.TrimSpace(params.LineContinent)
lineCountry := strings.TrimSpace(params.LineCountry)
if len(lineCarrier) == 0 {
lineCarrier = "默认"
}
if len(lineRegion) == 0 {
lineRegion = "默认"
}
if len(lineProvince) == 0 {
lineProvince = "默认"
}
if len(lineContinent) == 0 {
lineContinent = "默认"
}
if len(lineCountry) == 0 {
lineCountry = "默认"
}
if params.LineScope == "overseas" {
lineCarrier = ""
lineRegion = ""
lineProvince = ""
} else {
lineContinent = ""
lineCountry = ""
}
records := make([]*pb.HTTPDNSRuleRecord, 0, len(recordValues))
for i, item := range recordValues {
records = append(records, &pb.HTTPDNSRuleRecord{
Id: 0,
RuleId: 0,
RecordType: item.GetString("type"),
RecordValue: item.GetString("value"),
Weight: int32(item.GetInt("weight")),
Sort: int32(i + 1),
})
}
rule := &pb.HTTPDNSCustomRule{
Id: params.RecordId,
AppId: params.AppId,
DomainId: params.DomainId,
RuleName: params.RuleName,
LineScope: params.LineScope,
LineCarrier: lineCarrier,
LineRegion: lineRegion,
LineProvince: lineProvince,
LineContinent: lineContinent,
LineCountry: lineCountry,
Ttl: int32(params.Ttl),
IsOn: params.IsOn,
Priority: 100,
Records: records,
}
if params.RecordId > 0 {
err = updateCustomRule(this.Parent(), rule)
} else {
_, err = createCustomRule(this.Parent(), rule)
}
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}
func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
raw = strings.TrimSpace(raw)
if len(raw) == 0 {
return []maps.Map{}, nil
}
list := []maps.Map{}
if err := json.Unmarshal([]byte(raw), &list); err != nil {
return nil, fmt.Errorf("解析记录格式不正确")
}
result := make([]maps.Map, 0, len(list))
for _, item := range list {
recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
recordValue := strings.TrimSpace(item.GetString("value"))
if len(recordType) == 0 && len(recordValue) == 0 {
continue
}
if recordType != "A" && recordType != "AAAA" {
return nil, fmt.Errorf("记录类型只能是 A 或 AAAA")
}
if len(recordValue) == 0 {
return nil, fmt.Errorf("记录值不能为空")
}
weight := item.GetInt("weight")
if !weightEnabled {
weight = 100
}
if weight < 1 || weight > 100 {
return nil, fmt.Errorf("权重值必须在 1-100 之间")
}
result = append(result, maps.Map{
"type": recordType,
"value": recordValue,
"weight": weight,
})
}
return result, nil
}
func marshalJSON(v interface{}, fallback string) string {
b, err := json.Marshal(v)
if err != nil {
return fallback
}
return string(b)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
)
type DomainsCreatePopupAction struct {
actionutils.ParentAction
}
func (this *DomainsCreatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *DomainsCreatePopupAction) RunGet(params struct {
AppId int64
}) {
app, err := findAppMap(this.Parent(), params.AppId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["app"] = app
this.Show()
}
func (this *DomainsCreatePopupAction) RunPost(params struct {
AppId int64
Domain string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
params.Must.Field("domain", params.Domain).Require("请输入域名")
err := createDomain(this.Parent(), params.AppId, params.Domain)
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("httpdns", "app", "")
}
func (this *IndexAction) RunGet(params struct {
Keyword string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
this.Data["keyword"] = params.Keyword
apps, err := listAppMaps(this.Parent(), params.Keyword)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["apps"] = apps
this.Data["page"] = ""
this.Show()
}

View File

@@ -0,0 +1,38 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
Data("teaMenu", "httpdns").
Data("teaSubMenu", "app").
Prefix("/httpdns/apps").
Get("", new(IndexAction)).
Get("/app", new(AppAction)).
Get("/sdk", new(SdkAction)).
GetPost("/sdk/upload", new(SdkUploadAction)).
Post("/sdk/upload/delete", new(SdkUploadDeleteAction)).
Get("/sdk/check", new(SdkCheckAction)).
Get("/sdk/download", new(SdkDownloadAction)).
Get("/sdk/doc", new(SdkDocAction)).
GetPost("/app/settings", new(AppSettingsAction)).
Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)).
Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)).
Get("/domains", new(DomainsAction)).
Get("/customRecords", new(CustomRecordsAction)).
GetPost("/create", new(CreateAction)).
GetPost("/delete", new(DeleteAction)).
GetPost("/domains/createPopup", new(DomainsCreatePopupAction)).
Post("/domains/delete", new(DomainsDeleteAction)).
GetPost("/customRecords/createPopup", new(CustomRecordsCreatePopupAction)).
Post("/customRecords/delete", new(CustomRecordsDeleteAction)).
Post("/customRecords/toggle", new(CustomRecordsToggleAction)).
EndAll()
})
}

View File

@@ -0,0 +1,396 @@
package apps
import (
"encoding/json"
"strconv"
"strings"
"time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
func listAppMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map, error) {
clusterNameMap, err := loadHTTPDNSClusterNameMap(parent)
if err != nil {
return nil, err
}
userMapByID, err := loadHTTPDNSUserMap(parent)
if err != nil {
return nil, err
}
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())), clusterNameMap, userMapByID))
}
return result, nil
}
func findAppMap(parent *actionutils.ParentAction, appDbId int64) (maps.Map, error) {
clusterNameMap, err := loadHTTPDNSClusterNameMap(parent)
if err != nil {
return nil, err
}
userMapByID, err := loadHTTPDNSUserMap(parent)
if err != nil {
return nil, err
}
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())), clusterNameMap, userMapByID), 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, clusterIdsJSON []byte) (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,
ClusterIdsJSON: clusterIdsJSON,
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, clusterIdsJSON []byte, isOn bool, userId int64) error {
_, err := parent.RPC().HTTPDNSAppRPC().UpdateHTTPDNSApp(parent.AdminContext(), &pb.UpdateHTTPDNSAppRequest{
AppDbId: appDbId,
Name: strings.TrimSpace(name),
ClusterIdsJSON: clusterIdsJSON,
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, clusterMapByID map[int64]maps.Map, userMapByID map[int64]maps.Map) maps.Map {
signSecret := app.GetSignSecret()
// 读取集群 ID 列表
var clusterIds []int64
if len(app.GetClusterIdsJSON()) > 0 {
_ = json.Unmarshal(app.GetClusterIdsJSON(), &clusterIds)
}
// 构建集群映射列表
var clusterMaps []maps.Map
for _, cid := range clusterIds {
cm := clusterMapByID[cid]
if cm == nil {
cm = maps.Map{"id": cid, "name": "", "apiAddress": ""}
}
clusterMaps = append(clusterMaps, cm)
}
var userMap maps.Map
if app.GetUserId() > 0 {
userMap = userMapByID[app.GetUserId()]
if userMap == nil {
userMap = maps.Map{
"id": app.GetUserId(),
"fullname": "用户#" + strconv.FormatInt(app.GetUserId(), 10),
"username": "-",
}
}
}
return maps.Map{
"id": app.GetId(),
"name": app.GetName(),
"appId": app.GetAppId(),
"clusterIds": clusterIds,
"clusters": clusterMaps,
"userId": app.GetUserId(),
"user": userMap,
"isOn": app.GetIsOn(),
"domainCount": domainCount,
"sniPolicyText": "隐匿 SNI",
"signEnabled": app.GetSignEnabled(),
"signSecretPlain": signSecret,
"signSecretMasked": maskSecret(signSecret),
"signSecretUpdated": formatDateTime(app.GetSignUpdatedAt()),
}
}
func loadHTTPDNSClusterNameMap(parent *actionutils.ParentAction) (map[int64]maps.Map, error) {
resp, err := parent.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(parent.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
return nil, err
}
result := map[int64]maps.Map{}
for _, cluster := range resp.GetClusters() {
port := "443"
if rawTLS := cluster.GetTlsPolicyJSON(); len(rawTLS) > 0 {
tlsConfig := maps.Map{}
if err := json.Unmarshal(rawTLS, &tlsConfig); err == nil {
if listenRaw := tlsConfig.Get("listen"); listenRaw != nil {
var listenAddresses []maps.Map
if data, err := json.Marshal(listenRaw); err == nil {
if err := json.Unmarshal(data, &listenAddresses); err == nil {
if len(listenAddresses) > 0 && len(listenAddresses[0].GetString("portRange")) > 0 {
port = listenAddresses[0].GetString("portRange")
}
}
}
}
}
}
apiAddress := "https://" + cluster.GetServiceDomain() + ":" + port
result[cluster.GetId()] = maps.Map{
"id": cluster.GetId(),
"name": cluster.GetName(),
"apiAddress": apiAddress,
}
}
return result, nil
}
func loadHTTPDNSUserMap(parent *actionutils.ParentAction) (map[int64]maps.Map, error) {
resp, err := parent.RPC().UserRPC().ListEnabledUsers(parent.AdminContext(), &pb.ListEnabledUsersRequest{
Offset: 0,
Size: 10_000,
})
if err != nil {
return nil, err
}
result := map[int64]maps.Map{}
for _, user := range resp.GetUsers() {
result[user.GetId()] = maps.Map{
"id": user.GetId(),
"fullname": user.GetFullname(),
"username": user.GetUsername(),
}
}
return result, nil
}
func defaultLineField(value string) string {
value = strings.TrimSpace(value)
if len(value) == 0 {
return "默认"
}
return value
}
func formatDateTime(ts int64) string {
if ts <= 0 {
return ""
}
return timeutil.FormatTime("Y-m-d H:i:s", ts)
}

View File

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

View File

@@ -0,0 +1,68 @@
package apps
import (
"net/url"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
)
type SdkCheckAction struct {
actionutils.ParentAction
}
func (this *SdkCheckAction) Init() {
this.Nav("", "", "")
}
func (this *SdkCheckAction) RunGet(params struct {
Platform string
Version string
Type string
}) {
platform, _, _, filename, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Data["exists"] = false
this.Data["message"] = err.Error()
this.Success()
return
}
version := strings.TrimSpace(params.Version)
t := strings.ToLower(strings.TrimSpace(params.Type))
if t == "doc" {
docPath := findUploadedSDKDocPath(platform, version)
if len(docPath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "当前平台/版本尚未上传集成文档"
this.Success()
return
}
downloadURL := "/httpdns/apps/sdk/doc?platform=" + url.QueryEscape(platform)
if len(version) > 0 {
downloadURL += "&version=" + url.QueryEscape(version)
}
this.Data["exists"] = true
this.Data["url"] = downloadURL
this.Success()
return
}
archivePath := findSDKArchivePath(filename, version)
if len(archivePath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "当前平台/版本尚未上传 SDK 安装包"
this.Success()
return
}
downloadURL := "/httpdns/apps/sdk/download?platform=" + url.QueryEscape(platform)
if len(version) > 0 {
downloadURL += "&version=" + url.QueryEscape(version)
}
downloadURL += "&raw=1"
this.Data["exists"] = true
this.Data["url"] = downloadURL
this.Success()
}

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