管理端全部功能跑通

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

View File

@@ -0,0 +1,125 @@
package httpdns
import (
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
func toPBCluster(cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
if cluster == nil {
return nil
}
return &pb.HTTPDNSCluster{
Id: int64(cluster.Id),
IsOn: cluster.IsOn,
IsDefault: cluster.IsDefault,
Name: cluster.Name,
ServiceDomain: cluster.ServiceDomain,
DefaultTTL: cluster.DefaultTTL,
FallbackTimeoutMs: cluster.FallbackTimeoutMs,
InstallDir: cluster.InstallDir,
TlsPolicyJSON: cluster.TLSPolicy,
CreatedAt: int64(cluster.CreatedAt),
UpdatedAt: int64(cluster.UpdatedAt),
}
}
func toPBNode(node *models.HTTPDNSNode) *pb.HTTPDNSNode {
if node == nil {
return nil
}
return &pb.HTTPDNSNode{
Id: int64(node.Id),
ClusterId: int64(node.ClusterId),
Name: node.Name,
IsOn: node.IsOn,
IsUp: node.IsUp,
IsInstalled: node.IsInstalled,
IsActive: node.IsActive,
UniqueId: node.UniqueId,
Secret: node.Secret,
InstallDir: node.InstallDir,
StatusJSON: node.Status,
InstallStatusJSON: node.InstallStatus,
CreatedAt: int64(node.CreatedAt),
UpdatedAt: int64(node.UpdatedAt),
}
}
func toPBApp(app *models.HTTPDNSApp, secret *models.HTTPDNSAppSecret) *pb.HTTPDNSApp {
if app == nil {
return nil
}
var signEnabled bool
var signSecret string
var signUpdatedAt int64
if secret != nil {
signEnabled = secret.SignEnabled
signSecret = secret.SignSecret
signUpdatedAt = int64(secret.SignUpdatedAt)
}
return &pb.HTTPDNSApp{
Id: int64(app.Id),
Name: app.Name,
AppId: app.AppId,
IsOn: app.IsOn,
PrimaryClusterId: int64(app.PrimaryClusterId),
BackupClusterId: int64(app.BackupClusterId),
SniMode: app.SNIMode,
SignEnabled: signEnabled,
SignSecret: signSecret,
SignUpdatedAt: signUpdatedAt,
CreatedAt: int64(app.CreatedAt),
UpdatedAt: int64(app.UpdatedAt),
UserId: int64(app.UserId),
}
}
func toPBDomain(domain *models.HTTPDNSDomain, ruleCount int64) *pb.HTTPDNSDomain {
if domain == nil {
return nil
}
return &pb.HTTPDNSDomain{
Id: int64(domain.Id),
AppId: int64(domain.AppId),
Domain: domain.Domain,
IsOn: domain.IsOn,
CreatedAt: int64(domain.CreatedAt),
UpdatedAt: int64(domain.UpdatedAt),
RuleCount: ruleCount,
}
}
func toPBRule(rule *models.HTTPDNSCustomRule, records []*models.HTTPDNSCustomRuleRecord) *pb.HTTPDNSCustomRule {
if rule == nil {
return nil
}
var pbRecords []*pb.HTTPDNSRuleRecord
for _, record := range records {
pbRecords = append(pbRecords, &pb.HTTPDNSRuleRecord{
Id: int64(record.Id),
RuleId: int64(record.RuleId),
RecordType: record.RecordType,
RecordValue: record.RecordValue,
Weight: record.Weight,
Sort: record.Sort,
})
}
return &pb.HTTPDNSCustomRule{
Id: int64(rule.Id),
AppId: int64(rule.AppId),
DomainId: int64(rule.DomainId),
RuleName: rule.RuleName,
LineScope: rule.LineScope,
LineCarrier: rule.LineCarrier,
LineRegion: rule.LineRegion,
LineProvince: rule.LineProvince,
LineContinent: rule.LineContinent,
LineCountry: rule.LineCountry,
Ttl: rule.TTL,
IsOn: rule.IsOn,
Priority: rule.Priority,
UpdatedAt: int64(rule.UpdatedAt),
Records: pbRecords,
}
}

View File

@@ -0,0 +1,258 @@
package httpdns
import (
"context"
"log"
"strconv"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/clickhouse"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type HTTPDNSAccessLogService struct {
services.BaseService
pb.UnimplementedHTTPDNSAccessLogServiceServer
}
func (s *HTTPDNSAccessLogService) CreateHTTPDNSAccessLogs(ctx context.Context, req *pb.CreateHTTPDNSAccessLogsRequest) (*pb.CreateHTTPDNSAccessLogsResponse, error) {
nodeIdInContext, err := s.ValidateHTTPDNSNode(ctx)
if err != nil {
_, err = s.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
}
if len(req.GetLogs()) == 0 {
return &pb.CreateHTTPDNSAccessLogsResponse{}, nil
}
mysqlLogs := make([]*models.HTTPDNSAccessLog, 0, len(req.GetLogs()))
chLogs := make([]*pb.HTTPDNSAccessLog, 0, len(req.GetLogs()))
seen := map[string]struct{}{}
for _, item := range req.GetLogs() {
if item == nil {
continue
}
nodeId := item.GetNodeId()
// When called by HTTPDNS node, trust node id parsed from RPC token.
if nodeIdInContext > 0 {
nodeId = nodeIdInContext
}
clusterId := item.GetClusterId()
if clusterId <= 0 && nodeId > 0 {
clusterId, _ = models.SharedHTTPDNSNodeDAO.FindNodeClusterId(s.NullTx(), nodeId)
}
key := item.GetRequestId() + "#" + strconv.FormatInt(nodeId, 10)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
createdAt := item.GetCreatedAt()
if createdAt <= 0 {
createdAt = time.Now().Unix()
}
day := item.GetDay()
if len(day) == 0 {
day = timeutil.Format("Ymd")
}
mysqlLogs = append(mysqlLogs, &models.HTTPDNSAccessLog{
RequestId: item.GetRequestId(),
ClusterId: uint32(clusterId),
NodeId: uint32(nodeId),
AppId: item.GetAppId(),
AppName: item.GetAppName(),
Domain: item.GetDomain(),
QType: item.GetQtype(),
ClientIP: item.GetClientIP(),
ClientRegion: item.GetClientRegion(),
Carrier: item.GetCarrier(),
SDKVersion: item.GetSdkVersion(),
OS: item.GetOs(),
ResultIPs: item.GetResultIPs(),
Status: item.GetStatus(),
ErrorCode: item.GetErrorCode(),
CostMs: item.GetCostMs(),
CreatedAt: uint64(createdAt),
Day: day,
Summary: item.GetSummary(),
})
chLogs = append(chLogs, &pb.HTTPDNSAccessLog{
RequestId: item.GetRequestId(),
ClusterId: clusterId,
NodeId: nodeId,
AppId: item.GetAppId(),
AppName: item.GetAppName(),
Domain: item.GetDomain(),
Qtype: item.GetQtype(),
ClientIP: item.GetClientIP(),
ClientRegion: item.GetClientRegion(),
Carrier: item.GetCarrier(),
SdkVersion: item.GetSdkVersion(),
Os: item.GetOs(),
ResultIPs: item.GetResultIPs(),
Status: item.GetStatus(),
ErrorCode: item.GetErrorCode(),
CostMs: item.GetCostMs(),
CreatedAt: createdAt,
Day: day,
Summary: item.GetSummary(),
})
}
if s.canWriteHTTPDNSAccessLogsToMySQL() {
for _, item := range mysqlLogs {
err = models.SharedHTTPDNSAccessLogDAO.CreateLog(s.NullTx(), item)
if err != nil {
if models.CheckSQLDuplicateErr(err) {
continue
}
return nil, err
}
}
}
store := clickhouse.NewHTTPDNSAccessLogsStore()
if s.canWriteHTTPDNSAccessLogsToClickHouse() && store.Client().IsConfigured() && len(chLogs) > 0 {
err = store.Insert(ctx, chLogs)
if err != nil {
log.Println("[HTTPDNS_ACCESS_LOG]write clickhouse failed, keep mysql success:", err.Error())
}
}
return &pb.CreateHTTPDNSAccessLogsResponse{}, nil
}
func (s *HTTPDNSAccessLogService) ListHTTPDNSAccessLogs(ctx context.Context, req *pb.ListHTTPDNSAccessLogsRequest) (*pb.ListHTTPDNSAccessLogsResponse, error) {
_, _, err := s.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
store := clickhouse.NewHTTPDNSAccessLogsStore()
canReadFromClickHouse := s.shouldReadHTTPDNSAccessLogsFromClickHouse() && store.Client().IsConfigured()
canReadFromMySQL := s.shouldReadHTTPDNSAccessLogsFromMySQL()
if canReadFromClickHouse {
resp, listErr := s.listFromClickHouse(ctx, store, req)
if listErr == nil {
return resp, nil
}
log.Println("[HTTPDNS_ACCESS_LOG]read clickhouse failed, fallback mysql:", listErr.Error())
if !canReadFromMySQL {
return nil, listErr
}
}
if !canReadFromMySQL {
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: []*pb.HTTPDNSAccessLog{},
Total: 0,
}, nil
}
total, err := models.SharedHTTPDNSAccessLogDAO.CountLogs(s.NullTx(), req.GetDay(), req.GetClusterId(), req.GetNodeId(), req.GetAppId(), req.GetDomain(), req.GetStatus(), req.GetKeyword())
if err != nil {
return nil, err
}
logs, err := models.SharedHTTPDNSAccessLogDAO.ListLogs(s.NullTx(), req.GetDay(), req.GetClusterId(), req.GetNodeId(), req.GetAppId(), req.GetDomain(), req.GetStatus(), req.GetKeyword(), req.GetOffset(), req.GetSize())
if err != nil {
return nil, err
}
result := make([]*pb.HTTPDNSAccessLog, 0, len(logs))
for _, item := range logs {
if item == nil {
continue
}
clusterName, _ := models.SharedHTTPDNSClusterDAO.FindEnabledClusterName(s.NullTx(), int64(item.ClusterId))
nodeName := ""
node, _ := models.SharedHTTPDNSNodeDAO.FindEnabledNode(s.NullTx(), int64(item.NodeId))
if node != nil {
nodeName = node.Name
}
result = append(result, &pb.HTTPDNSAccessLog{
Id: int64(item.Id),
RequestId: item.RequestId,
ClusterId: int64(item.ClusterId),
NodeId: int64(item.NodeId),
AppId: item.AppId,
AppName: item.AppName,
Domain: item.Domain,
Qtype: item.QType,
ClientIP: item.ClientIP,
ClientRegion: item.ClientRegion,
Carrier: item.Carrier,
SdkVersion: item.SDKVersion,
Os: item.OS,
ResultIPs: item.ResultIPs,
Status: item.Status,
ErrorCode: item.ErrorCode,
CostMs: item.CostMs,
CreatedAt: int64(item.CreatedAt),
Day: item.Day,
Summary: item.Summary,
NodeName: nodeName,
ClusterName: clusterName,
})
}
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: result,
Total: total,
}, nil
}
func (s *HTTPDNSAccessLogService) listFromClickHouse(ctx context.Context, store *clickhouse.HTTPDNSAccessLogsStore, req *pb.ListHTTPDNSAccessLogsRequest) (*pb.ListHTTPDNSAccessLogsResponse, error) {
filter := clickhouse.HTTPDNSAccessLogListFilter{
Day: req.GetDay(),
ClusterId: req.GetClusterId(),
NodeId: req.GetNodeId(),
AppId: req.GetAppId(),
Domain: req.GetDomain(),
Status: req.GetStatus(),
Keyword: req.GetKeyword(),
Offset: req.GetOffset(),
Size: req.GetSize(),
}
total, err := store.Count(ctx, filter)
if err != nil {
return nil, err
}
rows, err := store.List(ctx, filter)
if err != nil {
return nil, err
}
result := make([]*pb.HTTPDNSAccessLog, 0, len(rows))
for _, row := range rows {
item := clickhouse.HTTPDNSRowToPB(row)
if item == nil {
continue
}
clusterName, _ := models.SharedHTTPDNSClusterDAO.FindEnabledClusterName(s.NullTx(), item.GetClusterId())
nodeName := ""
node, _ := models.SharedHTTPDNSNodeDAO.FindEnabledNode(s.NullTx(), item.GetNodeId())
if node != nil {
nodeName = node.Name
}
item.ClusterName = clusterName
item.NodeName = nodeName
result = append(result, item)
}
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: result,
Total: total,
}, nil
}

View File

@@ -0,0 +1,19 @@
//go:build !plus
package httpdns
func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToMySQL() bool {
return true
}
func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToClickHouse() bool {
return true
}
func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromClickHouse() bool {
return true
}
func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromMySQL() bool {
return true
}

View File

@@ -0,0 +1,124 @@
//go:build plus
package httpdns
import (
"log"
"sync"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
)
var (
httpDNSAccessLogWriteTargetsLocker sync.RWMutex
httpDNSAccessLogWriteTargetsCache = &serverconfigs.AccessLogWriteTargets{
File: true,
MySQL: true,
ClickHouse: false,
}
httpDNSAccessLogWriteTargetsExpireAt int64
)
const httpDNSAccessLogWriteTargetsCacheTTL = 10 * time.Second
func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToMySQL() bool {
targets := s.readHTTPDNSAccessLogWriteTargets()
if targets == nil {
return true
}
return targets.MySQL
}
func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToClickHouse() bool {
targets := s.readHTTPDNSAccessLogWriteTargets()
if targets == nil {
return false
}
return targets.ClickHouse
}
func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromClickHouse() bool {
targets := s.readHTTPDNSAccessLogWriteTargets()
if targets == nil {
return false
}
return targets.ClickHouse
}
func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromMySQL() bool {
targets := s.readHTTPDNSAccessLogWriteTargets()
if targets == nil {
return true
}
return targets.MySQL
}
func (s *HTTPDNSAccessLogService) readHTTPDNSAccessLogWriteTargets() *serverconfigs.AccessLogWriteTargets {
now := time.Now().Unix()
httpDNSAccessLogWriteTargetsLocker.RLock()
if now < httpDNSAccessLogWriteTargetsExpireAt && httpDNSAccessLogWriteTargetsCache != nil {
targets := *httpDNSAccessLogWriteTargetsCache
httpDNSAccessLogWriteTargetsLocker.RUnlock()
return &targets
}
httpDNSAccessLogWriteTargetsLocker.RUnlock()
httpDNSAccessLogWriteTargetsLocker.Lock()
defer httpDNSAccessLogWriteTargetsLocker.Unlock()
// double-check
now = time.Now().Unix()
if now < httpDNSAccessLogWriteTargetsExpireAt && httpDNSAccessLogWriteTargetsCache != nil {
targets := *httpDNSAccessLogWriteTargetsCache
return &targets
}
targets := s.loadHTTPDNSAccessLogWriteTargetsFromPolicy()
if targets == nil {
targets = &serverconfigs.AccessLogWriteTargets{
File: true,
MySQL: true,
ClickHouse: false,
}
}
httpDNSAccessLogWriteTargetsCache = targets
httpDNSAccessLogWriteTargetsExpireAt = time.Now().Add(httpDNSAccessLogWriteTargetsCacheTTL).Unix()
copyTargets := *targets
return &copyTargets
}
func (s *HTTPDNSAccessLogService) loadHTTPDNSAccessLogWriteTargetsFromPolicy() *serverconfigs.AccessLogWriteTargets {
tx := s.NullTx()
publicPolicyId, err := models.SharedHTTPAccessLogPolicyDAO.FindCurrentPublicPolicyId(tx)
if err != nil {
log.Println("[HTTPDNS_ACCESS_LOG]load public access log policy failed:", err.Error())
return nil
}
if publicPolicyId <= 0 {
return &serverconfigs.AccessLogWriteTargets{
File: true,
MySQL: true,
ClickHouse: false,
}
}
policy, err := models.SharedHTTPAccessLogPolicyDAO.FindEnabledHTTPAccessLogPolicy(tx, publicPolicyId)
if err != nil {
log.Println("[HTTPDNS_ACCESS_LOG]load access log policy detail failed:", err.Error())
return nil
}
if policy == nil {
return &serverconfigs.AccessLogWriteTargets{
File: true,
MySQL: true,
ClickHouse: false,
}
}
return serverconfigs.ParseWriteTargetsFromPolicy(policy.WriteTargets, policy.Type, policy.DisableDefaultDB)
}

View File

@@ -0,0 +1,246 @@
package httpdns
import (
"context"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
"strings"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
)
// HTTPDNSAppService HTTPDNS应用服务
type HTTPDNSAppService struct {
services.BaseService
pb.UnimplementedHTTPDNSAppServiceServer
}
func (this *HTTPDNSAppService) CreateHTTPDNSApp(ctx context.Context, req *pb.CreateHTTPDNSAppRequest) (*pb.CreateHTTPDNSAppResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
if len(req.Name) == 0 || len(req.AppId) == 0 {
return nil, errors.New("required 'name' and 'appId'")
}
if req.PrimaryClusterId <= 0 {
return nil, errors.New("required 'primaryClusterId'")
}
var appDbId int64
err = this.RunTx(func(tx *dbs.Tx) error {
exists, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(tx, strings.TrimSpace(req.AppId))
if err != nil {
return err
}
if exists != nil {
return errors.New("appId already exists")
}
appDbId, err = models.SharedHTTPDNSAppDAO.CreateApp(tx, req.Name, strings.TrimSpace(req.AppId), req.PrimaryClusterId, req.BackupClusterId, req.IsOn, req.UserId)
if err != nil {
return err
}
_, _, err = models.SharedHTTPDNSAppSecretDAO.InitAppSecret(tx, appDbId, req.SignEnabled)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, appDbId, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSAppResponse{AppDbId: appDbId}, nil
}
func (this *HTTPDNSAppService) UpdateHTTPDNSApp(ctx context.Context, req *pb.UpdateHTTPDNSAppRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
oldApp, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(tx, req.AppDbId)
if err != nil {
return err
}
if oldApp == nil {
return errors.New("app not found")
}
err = models.SharedHTTPDNSAppDAO.UpdateApp(tx, req.AppDbId, req.Name, req.PrimaryClusterId, req.BackupClusterId, req.IsOn, req.UserId)
if err != nil {
return err
}
err = notifyHTTPDNSAppTasksByApp(tx, oldApp, models.HTTPDNSNodeTaskTypeAppChanged)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSAppService) DeleteHTTPDNSApp(ctx context.Context, req *pb.DeleteHTTPDNSAppRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
app, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(tx, req.AppDbId)
if err != nil {
return err
}
if app == nil {
return nil
}
// 1) 先停用规则记录
rules, err := models.SharedHTTPDNSCustomRuleDAO.ListEnabledRulesWithAppId(tx, req.AppDbId)
if err != nil {
return err
}
for _, rule := range rules {
err = models.SharedHTTPDNSCustomRuleRecordDAO.DisableRecordsWithRuleId(tx, int64(rule.Id))
if err != nil {
return err
}
}
// 2) 停用规则、域名、密钥
err = models.SharedHTTPDNSCustomRuleDAO.DisableRulesWithAppId(tx, req.AppDbId)
if err != nil {
return err
}
err = models.SharedHTTPDNSDomainDAO.DisableDomainsWithAppId(tx, req.AppDbId)
if err != nil {
return err
}
err = models.SharedHTTPDNSAppSecretDAO.DisableAppSecret(tx, req.AppDbId)
if err != nil {
return err
}
// 3) 删除该应用的 MySQL 访问日志,避免残留
err = models.SharedHTTPDNSAccessLogDAO.DeleteLogsWithAppId(tx, app.AppId)
if err != nil {
return err
}
// 4) 最后停用应用
err = models.SharedHTTPDNSAppDAO.DisableApp(tx, req.AppDbId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByApp(tx, app, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSAppService) FindHTTPDNSApp(ctx context.Context, req *pb.FindHTTPDNSAppRequest) (*pb.FindHTTPDNSAppResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
app, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(this.NullTx(), req.AppDbId)
if err != nil {
return nil, err
}
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), req.AppDbId)
if err != nil {
return nil, err
}
return &pb.FindHTTPDNSAppResponse{App: toPBApp(app, secret)}, nil
}
func (this *HTTPDNSAppService) ListHTTPDNSApps(ctx context.Context, req *pb.ListHTTPDNSAppsRequest) (*pb.ListHTTPDNSAppsResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
apps, err := models.SharedHTTPDNSAppDAO.ListEnabledApps(this.NullTx(), req.Offset, req.Size, req.Keyword)
if err != nil {
return nil, err
}
var pbApps []*pb.HTTPDNSApp
for _, app := range apps {
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
if err != nil {
return nil, err
}
pbApps = append(pbApps, toPBApp(app, secret))
}
return &pb.ListHTTPDNSAppsResponse{Apps: pbApps}, nil
}
func (this *HTTPDNSAppService) FindAllHTTPDNSApps(ctx context.Context, req *pb.FindAllHTTPDNSAppsRequest) (*pb.FindAllHTTPDNSAppsResponse, error) {
_, _, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
}
apps, err := models.SharedHTTPDNSAppDAO.FindAllEnabledApps(this.NullTx())
if err != nil {
return nil, err
}
var pbApps []*pb.HTTPDNSApp
for _, app := range apps {
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
if err != nil {
return nil, err
}
pbApps = append(pbApps, toPBApp(app, secret))
}
return &pb.FindAllHTTPDNSAppsResponse{Apps: pbApps}, nil
}
func (this *HTTPDNSAppService) UpdateHTTPDNSAppSignEnabled(ctx context.Context, req *pb.UpdateHTTPDNSAppSignEnabledRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
err := models.SharedHTTPDNSAppSecretDAO.UpdateSignEnabled(tx, req.AppDbId, req.SignEnabled)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSAppService) ResetHTTPDNSAppSignSecret(ctx context.Context, req *pb.ResetHTTPDNSAppSignSecretRequest) (*pb.ResetHTTPDNSAppSignSecretResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var signSecret string
var updatedAt int64
err = this.RunTx(func(tx *dbs.Tx) error {
var err error
signSecret, updatedAt, err = models.SharedHTTPDNSAppSecretDAO.ResetSignSecret(tx, req.AppDbId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return &pb.ResetHTTPDNSAppSignSecretResponse{
SignSecret: signSecret,
UpdatedAt: updatedAt,
}, nil
}

View File

@@ -0,0 +1,170 @@
package httpdns
import (
"context"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
)
// HTTPDNSClusterService HTTPDNS集群服务
type HTTPDNSClusterService struct {
services.BaseService
pb.UnimplementedHTTPDNSClusterServiceServer
}
func (this *HTTPDNSClusterService) CreateHTTPDNSCluster(ctx context.Context, req *pb.CreateHTTPDNSClusterRequest) (*pb.CreateHTTPDNSClusterResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
if len(req.Name) == 0 {
return nil, errors.New("required 'name'")
}
var clusterId int64
err = this.RunTx(func(tx *dbs.Tx) error {
clusterId, err = models.SharedHTTPDNSClusterDAO.CreateCluster(tx, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, clusterId, models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSClusterResponse{ClusterId: clusterId}, nil
}
func (this *HTTPDNSClusterService) UpdateHTTPDNSCluster(ctx context.Context, req *pb.UpdateHTTPDNSClusterRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
err = models.SharedHTTPDNSClusterDAO.UpdateCluster(tx, req.ClusterId, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault)
if err != nil {
return err
}
taskType := models.HTTPDNSNodeTaskTypeConfigChanged
if len(req.TlsPolicyJSON) > 0 {
taskType = models.HTTPDNSNodeTaskTypeTLSChanged
}
return notifyHTTPDNSClusterTask(tx, req.ClusterId, taskType)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSClusterService) DeleteHTTPDNSCluster(ctx context.Context, req *pb.DeleteHTTPDNSClusterRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
err = models.SharedHTTPDNSClusterDAO.DisableCluster(tx, req.ClusterId)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, req.ClusterId, models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSClusterService) FindHTTPDNSCluster(ctx context.Context, req *pb.FindHTTPDNSClusterRequest) (*pb.FindHTTPDNSClusterResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(this.NullTx(), req.ClusterId)
if err != nil {
return nil, err
}
return &pb.FindHTTPDNSClusterResponse{Cluster: toPBCluster(cluster)}, nil
}
func (this *HTTPDNSClusterService) ListHTTPDNSClusters(ctx context.Context, req *pb.ListHTTPDNSClustersRequest) (*pb.ListHTTPDNSClustersResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
clusters, err := models.SharedHTTPDNSClusterDAO.ListEnabledClusters(this.NullTx(), req.Offset, req.Size, req.Keyword)
if err != nil {
return nil, err
}
var pbClusters []*pb.HTTPDNSCluster
for _, cluster := range clusters {
pbClusters = append(pbClusters, toPBCluster(cluster))
}
return &pb.ListHTTPDNSClustersResponse{Clusters: pbClusters}, nil
}
func (this *HTTPDNSClusterService) FindAllHTTPDNSClusters(ctx context.Context, req *pb.FindAllHTTPDNSClustersRequest) (*pb.FindAllHTTPDNSClustersResponse, error) {
_, _, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
}
clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(this.NullTx())
if err != nil {
return nil, err
}
var pbClusters []*pb.HTTPDNSCluster
for _, cluster := range clusters {
pbClusters = append(pbClusters, toPBCluster(cluster))
}
return &pb.FindAllHTTPDNSClustersResponse{Clusters: pbClusters}, nil
}
func (this *HTTPDNSClusterService) UpdateHTTPDNSClusterDefault(ctx context.Context, req *pb.UpdateHTTPDNSClusterDefaultRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
err = models.SharedHTTPDNSClusterDAO.UpdateDefaultCluster(tx, req.ClusterId)
if err != nil {
return err
}
clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(tx)
if err != nil {
return err
}
for _, cluster := range clusters {
err = notifyHTTPDNSClusterTask(tx, int64(cluster.Id), models.HTTPDNSNodeTaskTypeConfigChanged)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSClusterService) ListHTTPDNSNodesWithClusterId(ctx context.Context, req *pb.ListHTTPDNSNodesWithClusterIdRequest) (*pb.ListHTTPDNSNodesWithClusterIdResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
nodes, err := models.SharedHTTPDNSNodeDAO.ListEnabledNodes(this.NullTx(), req.ClusterId)
if err != nil {
return nil, err
}
var pbNodes []*pb.HTTPDNSNode
for _, node := range nodes {
pbNodes = append(pbNodes, toPBNode(node))
}
return &pb.ListHTTPDNSNodesWithClusterIdResponse{Nodes: pbNodes}, nil
}

View File

@@ -0,0 +1,112 @@
package httpdns
import (
"context"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
)
// HTTPDNSDomainService HTTPDNS域名服务
type HTTPDNSDomainService struct {
services.BaseService
pb.UnimplementedHTTPDNSDomainServiceServer
}
func (this *HTTPDNSDomainService) CreateHTTPDNSDomain(ctx context.Context, req *pb.CreateHTTPDNSDomainRequest) (*pb.CreateHTTPDNSDomainResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
if req.AppDbId <= 0 || len(req.Domain) == 0 {
return nil, errors.New("required 'appDbId' and 'domain'")
}
var domainId int64
err = this.RunTx(func(tx *dbs.Tx) error {
domainId, err = models.SharedHTTPDNSDomainDAO.CreateDomain(tx, req.AppDbId, req.Domain, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeDomainChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSDomainResponse{DomainId: domainId}, nil
}
func (this *HTTPDNSDomainService) DeleteHTTPDNSDomain(ctx context.Context, req *pb.DeleteHTTPDNSDomainRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
domain, err := models.SharedHTTPDNSDomainDAO.FindEnabledDomain(tx, req.DomainId)
if err != nil {
return err
}
if domain == nil {
return nil
}
err = models.SharedHTTPDNSDomainDAO.DisableDomain(tx, req.DomainId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, int64(domain.AppId), models.HTTPDNSNodeTaskTypeDomainChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSDomainService) UpdateHTTPDNSDomainStatus(ctx context.Context, req *pb.UpdateHTTPDNSDomainStatusRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
domain, err := models.SharedHTTPDNSDomainDAO.FindEnabledDomain(tx, req.DomainId)
if err != nil {
return err
}
if domain == nil {
return nil
}
err = models.SharedHTTPDNSDomainDAO.UpdateDomainStatus(tx, req.DomainId, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, int64(domain.AppId), models.HTTPDNSNodeTaskTypeDomainChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSDomainService) ListHTTPDNSDomainsWithAppId(ctx context.Context, req *pb.ListHTTPDNSDomainsWithAppIdRequest) (*pb.ListHTTPDNSDomainsWithAppIdResponse, error) {
_, _, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
}
domains, err := models.SharedHTTPDNSDomainDAO.ListEnabledDomainsWithAppId(this.NullTx(), req.AppDbId, req.Keyword)
if err != nil {
return nil, err
}
var pbDomains []*pb.HTTPDNSDomain
for _, domain := range domains {
ruleCount, err := models.SharedHTTPDNSCustomRuleDAO.CountEnabledRulesWithDomainId(this.NullTx(), int64(domain.Id))
if err != nil {
return nil, err
}
pbDomains = append(pbDomains, toPBDomain(domain, ruleCount))
}
return &pb.ListHTTPDNSDomainsWithAppIdResponse{Domains: pbDomains}, nil
}

View File

@@ -0,0 +1,185 @@
package httpdns
import (
"context"
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/goman"
"github.com/TeaOSLab/EdgeAPI/internal/installers"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
)
// HTTPDNSNodeService HTTPDNS节点服务
type HTTPDNSNodeService struct {
services.BaseService
pb.UnimplementedHTTPDNSNodeServiceServer
}
func (this *HTTPDNSNodeService) CreateHTTPDNSNode(ctx context.Context, req *pb.CreateHTTPDNSNodeRequest) (*pb.CreateHTTPDNSNodeResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
if req.ClusterId <= 0 {
return nil, errors.New("required 'clusterId'")
}
var nodeId int64
err = this.RunTx(func(tx *dbs.Tx) error {
nodeId, err = models.SharedHTTPDNSNodeDAO.CreateNode(tx, req.ClusterId, req.Name, req.InstallDir, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, req.ClusterId, models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSNodeResponse{NodeId: nodeId}, nil
}
func (this *HTTPDNSNodeService) UpdateHTTPDNSNode(ctx context.Context, req *pb.UpdateHTTPDNSNodeRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, req.NodeId)
if err != nil {
return err
}
if node == nil {
return errors.New("node not found")
}
err = models.SharedHTTPDNSNodeDAO.UpdateNode(tx, req.NodeId, req.Name, req.InstallDir, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, int64(node.ClusterId), models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSNodeService) DeleteHTTPDNSNode(ctx context.Context, req *pb.DeleteHTTPDNSNodeRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, req.NodeId)
if err != nil {
return err
}
if node == nil {
return nil
}
err = models.SharedHTTPDNSNodeDAO.DisableNode(tx, req.NodeId)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, int64(node.ClusterId), models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSNodeService) FindHTTPDNSNode(ctx context.Context, req *pb.FindHTTPDNSNodeRequest) (*pb.FindHTTPDNSNodeResponse, error) {
nodeId := req.NodeId
if nodeId <= 0 {
parsedNodeId, nodeErr := this.ValidateHTTPDNSNode(ctx)
if nodeErr != nil {
return nil, errors.New("invalid 'nodeId'")
}
nodeId = parsedNodeId
} else {
_, _, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
}
}
node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(this.NullTx(), nodeId)
if err != nil {
return nil, err
}
return &pb.FindHTTPDNSNodeResponse{Node: toPBNode(node)}, nil
}
func (this *HTTPDNSNodeService) ListHTTPDNSNodes(ctx context.Context, req *pb.ListHTTPDNSNodesRequest) (*pb.ListHTTPDNSNodesResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
nodes, err := models.SharedHTTPDNSNodeDAO.ListEnabledNodes(this.NullTx(), req.ClusterId)
if err != nil {
return nil, err
}
var pbNodes []*pb.HTTPDNSNode
for _, node := range nodes {
pbNodes = append(pbNodes, toPBNode(node))
}
return &pb.ListHTTPDNSNodesResponse{Nodes: pbNodes}, nil
}
func (this *HTTPDNSNodeService) UpdateHTTPDNSNodeStatus(ctx context.Context, req *pb.UpdateHTTPDNSNodeStatusRequest) (*pb.RPCSuccess, error) {
nodeId := req.GetNodeId()
isAdminCaller := false
if nodeId > 0 {
if _, adminErr := this.ValidateAdmin(ctx); adminErr == nil {
isAdminCaller = true
}
}
if !isAdminCaller {
if nodeId <= 0 {
parsedNodeId, err := this.ValidateHTTPDNSNode(ctx)
if err != nil {
return nil, err
}
nodeId = parsedNodeId
}
}
if nodeId <= 0 {
return nil, errors.New("invalid 'nodeId'")
}
err := models.SharedHTTPDNSNodeDAO.UpdateNodeStatus(this.NullTx(), nodeId, req.GetIsUp(), req.GetIsInstalled(), req.GetIsActive(), req.GetStatusJSON(), req.GetInstallStatusJSON())
if err != nil {
return nil, err
}
if isAdminCaller && shouldTriggerHTTPDNSInstall(req.GetInstallStatusJSON()) {
goman.New(func() {
installErr := installers.SharedHTTPDNSNodeQueue().InstallNodeProcess(nodeId, false)
if installErr != nil {
logs.Println("[RPC][HTTPDNS]install node failed:", installErr.Error())
}
})
}
return this.Success()
}
func shouldTriggerHTTPDNSInstall(installStatusJSON []byte) bool {
if len(installStatusJSON) == 0 {
return false
}
installStatus := &models.NodeInstallStatus{}
err := json.Unmarshal(installStatusJSON, installStatus)
if err != nil {
return false
}
return installStatus.IsRunning && !installStatus.IsFinished
}

View File

@@ -0,0 +1,193 @@
package httpdns
import (
"context"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
)
// HTTPDNSRuleService HTTPDNS规则服务
type HTTPDNSRuleService struct {
services.BaseService
pb.UnimplementedHTTPDNSRuleServiceServer
}
func (this *HTTPDNSRuleService) CreateHTTPDNSCustomRule(ctx context.Context, req *pb.CreateHTTPDNSCustomRuleRequest) (*pb.CreateHTTPDNSCustomRuleResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
if req.Rule == nil {
return nil, errors.New("required 'rule'")
}
var ruleId int64
err = this.RunTx(func(tx *dbs.Tx) error {
rule := &models.HTTPDNSCustomRule{
AppId: uint32(req.Rule.AppId),
DomainId: uint32(req.Rule.DomainId),
RuleName: req.Rule.RuleName,
LineScope: req.Rule.LineScope,
LineCarrier: req.Rule.LineCarrier,
LineRegion: req.Rule.LineRegion,
LineProvince: req.Rule.LineProvince,
LineContinent: req.Rule.LineContinent,
LineCountry: req.Rule.LineCountry,
TTL: req.Rule.Ttl,
IsOn: req.Rule.IsOn,
Priority: req.Rule.Priority,
}
ruleId, err = models.SharedHTTPDNSCustomRuleDAO.CreateRule(tx, rule)
if err != nil {
return err
}
for _, record := range req.Rule.Records {
_, err := models.SharedHTTPDNSCustomRuleRecordDAO.CreateRecord(tx, ruleId, record.RecordType, record.RecordValue, record.Weight, record.Sort)
if err != nil {
return err
}
}
return notifyHTTPDNSAppTasksByAppDbId(tx, req.Rule.AppId, models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSCustomRuleResponse{RuleId: ruleId}, nil
}
func (this *HTTPDNSRuleService) UpdateHTTPDNSCustomRule(ctx context.Context, req *pb.UpdateHTTPDNSCustomRuleRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
if req.Rule == nil || req.Rule.Id <= 0 {
return nil, errors.New("invalid 'rule.id'")
}
err = this.RunTx(func(tx *dbs.Tx) error {
oldRule, err := models.SharedHTTPDNSCustomRuleDAO.FindEnabledRule(tx, req.Rule.Id)
if err != nil {
return err
}
if oldRule == nil {
return errors.New("rule not found")
}
rule := &models.HTTPDNSCustomRule{
Id: uint32(req.Rule.Id),
RuleName: req.Rule.RuleName,
LineScope: req.Rule.LineScope,
LineCarrier: req.Rule.LineCarrier,
LineRegion: req.Rule.LineRegion,
LineProvince: req.Rule.LineProvince,
LineContinent: req.Rule.LineContinent,
LineCountry: req.Rule.LineCountry,
TTL: req.Rule.Ttl,
IsOn: req.Rule.IsOn,
Priority: req.Rule.Priority,
}
err = models.SharedHTTPDNSCustomRuleDAO.UpdateRule(tx, rule)
if err != nil {
return err
}
err = models.SharedHTTPDNSCustomRuleRecordDAO.DisableRecordsWithRuleId(tx, req.Rule.Id)
if err != nil {
return err
}
for _, record := range req.Rule.Records {
_, err := models.SharedHTTPDNSCustomRuleRecordDAO.CreateRecord(tx, req.Rule.Id, record.RecordType, record.RecordValue, record.Weight, record.Sort)
if err != nil {
return err
}
}
err = notifyHTTPDNSAppTasksByAppDbId(tx, int64(oldRule.AppId), models.HTTPDNSNodeTaskTypeRuleChanged)
if err != nil {
return err
}
targetAppDbId := req.Rule.AppId
if targetAppDbId <= 0 {
targetAppDbId = int64(oldRule.AppId)
}
return notifyHTTPDNSAppTasksByAppDbId(tx, targetAppDbId, models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSRuleService) DeleteHTTPDNSCustomRule(ctx context.Context, req *pb.DeleteHTTPDNSCustomRuleRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
rule, err := models.SharedHTTPDNSCustomRuleDAO.FindEnabledRule(tx, req.RuleId)
if err != nil {
return err
}
if rule == nil {
return nil
}
err = models.SharedHTTPDNSCustomRuleDAO.DisableRule(tx, req.RuleId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, int64(rule.AppId), models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSRuleService) UpdateHTTPDNSCustomRuleStatus(ctx context.Context, req *pb.UpdateHTTPDNSCustomRuleStatusRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
rule, err := models.SharedHTTPDNSCustomRuleDAO.FindEnabledRule(tx, req.RuleId)
if err != nil {
return err
}
if rule == nil {
return nil
}
err = models.SharedHTTPDNSCustomRuleDAO.UpdateRuleStatus(tx, req.RuleId, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, int64(rule.AppId), models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSRuleService) ListHTTPDNSCustomRulesWithDomainId(ctx context.Context, req *pb.ListHTTPDNSCustomRulesWithDomainIdRequest) (*pb.ListHTTPDNSCustomRulesWithDomainIdResponse, error) {
_, _, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
}
rules, err := models.SharedHTTPDNSCustomRuleDAO.ListEnabledRulesWithDomainId(this.NullTx(), req.DomainId)
if err != nil {
return nil, err
}
var pbRules []*pb.HTTPDNSCustomRule
for _, rule := range rules {
records, err := models.SharedHTTPDNSCustomRuleRecordDAO.ListEnabledRecordsWithRuleId(this.NullTx(), int64(rule.Id))
if err != nil {
return nil, err
}
pbRules = append(pbRules, toPBRule(rule, records))
}
return &pb.ListHTTPDNSCustomRulesWithDomainIdResponse{Rules: pbRules}, nil
}

View File

@@ -0,0 +1,107 @@
package httpdns
import (
"context"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
)
// HTTPDNSRuntimeLogService HTTPDNS运行日志服务
type HTTPDNSRuntimeLogService struct {
services.BaseService
pb.UnimplementedHTTPDNSRuntimeLogServiceServer
}
func (this *HTTPDNSRuntimeLogService) CreateHTTPDNSRuntimeLogs(ctx context.Context, req *pb.CreateHTTPDNSRuntimeLogsRequest) (*pb.CreateHTTPDNSRuntimeLogsResponse, error) {
nodeIdInContext, err := this.ValidateHTTPDNSNode(ctx)
if err != nil {
_, err = this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
}
for _, item := range req.Logs {
createdAt := item.CreatedAt
if createdAt <= 0 {
createdAt = time.Now().Unix()
}
day := item.Day
if len(day) == 0 {
day = timeutil.Format("Ymd")
}
nodeId := item.NodeId
// When called by HTTPDNS node, trust node id parsed from RPC token.
if nodeIdInContext > 0 {
nodeId = nodeIdInContext
}
clusterId := item.ClusterId
if clusterId <= 0 && nodeId > 0 {
clusterId, _ = models.SharedHTTPDNSNodeDAO.FindNodeClusterId(this.NullTx(), nodeId)
}
log := &models.HTTPDNSRuntimeLog{
ClusterId: uint32(clusterId),
NodeId: uint32(nodeId),
Level: item.Level,
Type: item.Type,
Module: item.Module,
Description: item.Description,
Count: item.Count,
RequestId: item.RequestId,
CreatedAt: uint64(createdAt),
Day: day,
}
err := models.SharedHTTPDNSRuntimeLogDAO.CreateLog(this.NullTx(), log)
if err != nil {
return nil, err
}
}
return &pb.CreateHTTPDNSRuntimeLogsResponse{}, nil
}
func (this *HTTPDNSRuntimeLogService) ListHTTPDNSRuntimeLogs(ctx context.Context, req *pb.ListHTTPDNSRuntimeLogsRequest) (*pb.ListHTTPDNSRuntimeLogsResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
total, err := models.SharedHTTPDNSRuntimeLogDAO.CountLogs(this.NullTx(), req.Day, req.ClusterId, req.NodeId, req.Level, req.Keyword)
if err != nil {
return nil, err
}
logs, err := models.SharedHTTPDNSRuntimeLogDAO.ListLogs(this.NullTx(), req.Day, req.ClusterId, req.NodeId, req.Level, req.Keyword, req.Offset, req.Size)
if err != nil {
return nil, err
}
var pbLogs []*pb.HTTPDNSRuntimeLog
for _, item := range logs {
clusterName, _ := models.SharedHTTPDNSClusterDAO.FindEnabledClusterName(this.NullTx(), int64(item.ClusterId))
nodeName := ""
node, _ := models.SharedHTTPDNSNodeDAO.FindEnabledNode(this.NullTx(), int64(item.NodeId))
if node != nil {
nodeName = node.Name
}
pbLogs = append(pbLogs, &pb.HTTPDNSRuntimeLog{
Id: int64(item.Id),
ClusterId: int64(item.ClusterId),
NodeId: int64(item.NodeId),
Level: item.Level,
Type: item.Type,
Module: item.Module,
Description: item.Description,
Count: item.Count,
RequestId: item.RequestId,
CreatedAt: int64(item.CreatedAt),
Day: item.Day,
ClusterName: clusterName,
NodeName: nodeName,
})
}
return &pb.ListHTTPDNSRuntimeLogsResponse{
Logs: pbLogs,
Total: total,
}, nil
}

View File

@@ -0,0 +1,252 @@
package httpdns
import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/rands"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
// HTTPDNSSandboxService HTTPDNS解析测试服务
type HTTPDNSSandboxService struct {
services.BaseService
pb.UnimplementedHTTPDNSSandboxServiceServer
}
// nodeResolveResponse 节点返回的 JSON 结构(对齐 EdgeHttpDNS resolve_server.go
type nodeResolveResponse struct {
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"requestId"`
Data *nodeResolveData `json:"data,omitempty"`
}
type nodeResolveData struct {
Domain string `json:"domain"`
QType string `json:"qtype"`
TTL int32 `json:"ttl"`
Records []*nodeResolveRecord `json:"records"`
Client *nodeClientInfo `json:"client"`
Summary string `json:"summary"`
}
type nodeResolveRecord struct {
Type string `json:"type"`
IP string `json:"ip"`
Weight int32 `json:"weight"`
Line string `json:"line"`
Region string `json:"region"`
}
type nodeClientInfo struct {
IP string `json:"ip"`
Region string `json:"region"`
Carrier string `json:"carrier"`
Country string `json:"country"`
}
func (this *HTTPDNSSandboxService) TestHTTPDNSResolve(ctx context.Context, req *pb.TestHTTPDNSResolveRequest) (*pb.TestHTTPDNSResolveResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
if len(req.AppId) == 0 || len(req.Domain) == 0 {
return nil, errors.New("appId 和 domain 不能为空")
}
app, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(this.NullTx(), req.AppId)
if err != nil {
return nil, err
}
if app == nil || !app.IsOn {
return &pb.TestHTTPDNSResolveResponse{
Code: "APP_NOT_FOUND_OR_DISABLED",
Message: "找不到指定的应用,或该应用已下线",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
if req.ClusterId > 0 && req.ClusterId != int64(app.PrimaryClusterId) && req.ClusterId != int64(app.BackupClusterId) {
return &pb.TestHTTPDNSResolveResponse{
Code: "APP_CLUSTER_MISMATCH",
Message: "当前应用未绑定到该集群 (主集群: " + strconv.FormatInt(int64(app.PrimaryClusterId), 10) + ", 备用集群: " + strconv.FormatInt(int64(app.BackupClusterId), 10) + ")",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
qtype := strings.ToUpper(strings.TrimSpace(req.Qtype))
if qtype == "" {
qtype = "A"
}
// 获取集群服务域名
clusterId := req.ClusterId
if clusterId <= 0 {
clusterId = int64(app.PrimaryClusterId)
}
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(this.NullTx(), clusterId)
if err != nil {
return nil, err
}
if cluster == nil {
return &pb.TestHTTPDNSResolveResponse{
Code: "CLUSTER_NOT_FOUND",
Message: "找不到指定的集群",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
serviceDomain := strings.TrimSpace(cluster.ServiceDomain)
if len(serviceDomain) == 0 {
return &pb.TestHTTPDNSResolveResponse{
Code: "NO_SERVICE_DOMAIN",
Message: "该集群未配置服务域名",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
// 构造请求转发到 EdgeHttpDNS 节点
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("appId", req.AppId)
query.Set("dn", req.Domain)
query.Set("qtype", qtype)
if len(req.ClientIP) > 0 {
query.Set("cip", req.ClientIP)
}
if len(req.Sid) > 0 {
query.Set("sid", req.Sid)
}
if len(req.SdkVersion) > 0 {
query.Set("sdk_version", req.SdkVersion)
}
if len(req.Os) > 0 {
query.Set("os", req.Os)
}
// 应用开启验签时,沙盒自动生成签名参数,避免测试请求被拒绝
if secret != nil && secret.SignEnabled {
signSecret := strings.TrimSpace(secret.SignSecret)
if len(signSecret) == 0 {
return &pb.TestHTTPDNSResolveResponse{
Code: "SIGN_INVALID",
Message: "应用开启了请求验签,但未配置有效加签 Secret",
RequestId: "rid-" + rands.HexString(12),
Domain: req.Domain,
Qtype: qtype,
}, nil
}
exp := strconv.FormatInt(time.Now().Unix()+300, 10)
nonce := "sandbox-" + rands.HexString(16)
sign := buildSandboxResolveSign(signSecret, req.AppId, req.Domain, qtype, exp, nonce)
query.Set("exp", exp)
query.Set("nonce", nonce)
query.Set("sign", sign)
}
resolveURL := "https://" + serviceDomain + "/resolve?" + query.Encode()
httpClient := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // 沙盒测试环境允许自签名证书
},
},
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, resolveURL, nil)
if err != nil {
return nil, fmt.Errorf("构建请求失败: %w", err)
}
resp, err := httpClient.Do(httpReq)
if err != nil {
return &pb.TestHTTPDNSResolveResponse{
Code: "NODE_UNREACHABLE",
Message: "无法连接到 HTTPDNS 节点: " + err.Error(),
RequestId: "rid-" + rands.HexString(12),
Domain: req.Domain,
Qtype: qtype,
}, nil
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
if err != nil {
return nil, fmt.Errorf("读取节点响应失败: %w", err)
}
// 解析节点返回的 JSON
var nodeResp nodeResolveResponse
if err := json.Unmarshal(body, &nodeResp); err != nil {
return &pb.TestHTTPDNSResolveResponse{
Code: "PARSE_ERROR",
Message: "解析节点返回数据失败: " + err.Error(),
RequestId: "rid-" + rands.HexString(12),
Domain: req.Domain,
Qtype: qtype,
}, nil
}
// 映射节点响应到 protobuf 响应
pbResp := &pb.TestHTTPDNSResolveResponse{
Code: nodeResp.Code,
Message: nodeResp.Message,
RequestId: nodeResp.RequestID,
Domain: req.Domain,
Qtype: qtype,
}
if nodeResp.Data != nil {
pbResp.Ttl = nodeResp.Data.TTL
pbResp.Summary = nodeResp.Data.Summary
if nodeResp.Data.Client != nil {
pbResp.ClientIP = nodeResp.Data.Client.IP
pbResp.ClientRegion = nodeResp.Data.Client.Region
pbResp.ClientCarrier = nodeResp.Data.Client.Carrier
pbResp.ClientCountry = nodeResp.Data.Client.Country
}
for _, rec := range nodeResp.Data.Records {
pbResp.Records = append(pbResp.Records, &pb.HTTPDNSResolveRecord{
Type: rec.Type,
Ip: rec.IP,
Ttl: nodeResp.Data.TTL,
Weight: rec.Weight,
Line: rec.Line,
Region: rec.Region,
})
}
}
return pbResp, nil
}
func buildSandboxResolveSign(signSecret string, appID string, domain string, qtype string, exp string, nonce string) string {
raw := strings.TrimSpace(appID) + "|" + strings.ToLower(strings.TrimSpace(domain)) + "|" + strings.ToUpper(strings.TrimSpace(qtype)) + "|" + strings.TrimSpace(exp) + "|" + strings.TrimSpace(nonce)
mac := hmac.New(sha256.New, []byte(strings.TrimSpace(signSecret)))
_, _ = mac.Write([]byte(raw))
return hex.EncodeToString(mac.Sum(nil))
}

View File

@@ -0,0 +1,49 @@
package httpdns
import (
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/iwind/TeaGo/dbs"
)
func notifyHTTPDNSClusterTask(tx *dbs.Tx, clusterId int64, taskType models.NodeTaskType) error {
if clusterId <= 0 {
return nil
}
return models.SharedNodeTaskDAO.CreateClusterTask(tx, nodeconfigs.NodeRoleHTTPDNS, clusterId, 0, 0, taskType)
}
func notifyHTTPDNSAppTasksByApp(tx *dbs.Tx, app *models.HTTPDNSApp, taskType models.NodeTaskType) error {
if app == nil {
return nil
}
primaryClusterId := int64(app.PrimaryClusterId)
backupClusterId := int64(app.BackupClusterId)
err := notifyHTTPDNSClusterTask(tx, primaryClusterId, taskType)
if err != nil {
return err
}
if backupClusterId > 0 && backupClusterId != primaryClusterId {
err = notifyHTTPDNSClusterTask(tx, backupClusterId, taskType)
if err != nil {
return err
}
}
return nil
}
func notifyHTTPDNSAppTasksByAppDbId(tx *dbs.Tx, appDbId int64, taskType models.NodeTaskType) error {
if appDbId <= 0 {
return nil
}
app, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(tx, appDbId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByApp(tx, app, taskType)
}

View File

@@ -83,6 +83,12 @@ func (this *BaseService) ValidateNSNode(ctx context.Context) (nodeId int64, err
return
}
// ValidateHTTPDNSNode 校验HTTPDNS节点
func (this *BaseService) ValidateHTTPDNSNode(ctx context.Context) (nodeId int64, err error) {
_, _, nodeId, err = rpcutils.ValidateRequest(ctx, rpcutils.UserTypeHTTPDNS)
return
}
// ValidateUserNode 校验用户节点
func (this *BaseService) ValidateUserNode(ctx context.Context, canRest bool) (userId int64, err error) {
// 不允许REST调用
@@ -105,7 +111,7 @@ func (this *BaseService) ValidateAuthorityNode(ctx context.Context) (nodeId int6
func (this *BaseService) ValidateNodeId(ctx context.Context, roles ...rpcutils.UserType) (role rpcutils.UserType, nodeIntId int64, err error) {
// 默认包含大部分节点
if len(roles) == 0 {
roles = []rpcutils.UserType{rpcutils.UserTypeNode, rpcutils.UserTypeCluster, rpcutils.UserTypeAdmin, rpcutils.UserTypeUser, rpcutils.UserTypeDNS, rpcutils.UserTypeReport, rpcutils.UserTypeLog, rpcutils.UserTypeAPI}
roles = []rpcutils.UserType{rpcutils.UserTypeNode, rpcutils.UserTypeCluster, rpcutils.UserTypeAdmin, rpcutils.UserTypeUser, rpcutils.UserTypeDNS, rpcutils.UserTypeHTTPDNS, rpcutils.UserTypeReport, rpcutils.UserTypeLog, rpcutils.UserTypeAPI}
}
if ctx == nil {
@@ -191,6 +197,8 @@ func (this *BaseService) ValidateNodeId(ctx context.Context, roles ...rpcutils.U
nodeIntId = 0
case rpcutils.UserTypeDNS:
nodeIntId, err = models.SharedNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
case rpcutils.UserTypeHTTPDNS:
nodeIntId, err = models.SharedHTTPDNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
case rpcutils.UserTypeReport:
nodeIntId, err = models.SharedReportNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
case rpcutils.UserTypeAuthority:

View File

@@ -19,7 +19,7 @@ type NodeTaskService struct {
// FindNodeTasks 获取单节点同步任务
func (this *NodeTaskService) FindNodeTasks(ctx context.Context, req *pb.FindNodeTasksRequest) (*pb.FindNodeTasksResponse, error) {
nodeType, nodeId, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS)
nodeType, nodeId, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeHTTPDNS)
if err != nil {
return nil, err
}
@@ -65,7 +65,7 @@ func (this *NodeTaskService) FindNodeTasks(ctx context.Context, req *pb.FindNode
// ReportNodeTaskDone 报告同步任务结果
func (this *NodeTaskService) ReportNodeTaskDone(ctx context.Context, req *pb.ReportNodeTaskDoneRequest) (*pb.RPCSuccess, error) {
_, _, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS)
_, _, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeHTTPDNS)
if err != nil {
return nil, err
}